From d7a19132912ae99e95ce005e84a3389234fa9859 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 24 Oct 2024 14:25:28 +0200 Subject: [PATCH] Refactor registration data and attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Excel FSP: multiple per program (#5848) * Excel FSP: multiple per program * Program registration attribute portal (#5858) * Frontend alligning to new registration attribute & fsp config change Renamed portal models according to new data structure, registration attribtes & new fsp conig Added missing import Check voucher support with fspName instead of label * Styling changes --------- Co-authored-by: Ruben * Fix.excel setup (#5970) * fix: get match column correctly AB#30281 * fix: lingering case of StatusEnum AB#30281 * fix: do not duplicate response rows with multiple fspConfigs * fix: api-test AB#30281 * fix: fix and unskip api-test on get fsp instructions AB#30281 * fix: api-test on snapshot AB#30281 * feat: return separate template file per fsp config * fix: snapshot template api test * chore: remove hasReconciliation * chore: split off interface * fix: resolve comments --------- Co-authored-by: Ruben Co-authored-by: jannisvisser Validate fsp config and required attributes for Registration data refactor (#5860) * AB#30474 Validate fsp config and required attributes WIP * AB#30474 & AB#30781 Validate registration with program registration attribute refactor * Migrate refactored registration data and program attributes (#5977) Migrations refactor registration data Co-authored-by: Ruben * Select the right kind of type while migrating program questions * Changes based on comments on the new validation of registrations * Expect required fsp attributes to be filled during payment * chore: rm unused methods / unskip tests / typos (#6004) --------- Co-authored-by: Ruben Co-authored-by: jannisvisser fix: make seed mock script work again (#6003) * fix: make seed mock script work again * fix: revert to abbreviated sequence name test: api-tests on update chosen fsp AB#31025 (#6006) Fix.test data (#6015) * fix: api-test excel * fix: make test data work * fix: calculate totalmultiplierSum in dryRun case * fix: import of boolean attribute Program fsp configs endpoints (#5989) * Some initial refactor and draft code Add code framework for endpoints Get endpoint implementation done WIP implement post endpoint * Manage fsp config enpoints * Comments jannis manage program fsp config --------- Co-authored-by: Diderik van Wingerden Co-authored-by: Ruben fix api & unit & e2e tests (#6010) * WIP: fix api & unit tests * Fixed playwright tests & typechecks after registration data refactor * fix: getProgramWithManyAttributes + drop persistence (#6016) Fixed playwright tests & typechecks after registration data refactor fix: getProgramWithManyAttributes + drop persistence fix: other k6 tests chore: update import csv endpoint path + rm remnants of import-as-invited test: add k6 test for import 1000 registrations temp change GA k6 test file to import1000registrations to test length typo fix: small performance increase + set allowed duration + switch gh action test back to default one Co-authored-by: Ruben Enabled portal e2e tests again * Fix bug in cbe query & added api test (#6031) Co-authored-by: Ruben * Api test for programFspConfigName is not equal to the fsp name (#6030) * Api test for programFspConfigName is not equal to the fsp name * Api test for programFspConfigName is not equal to the fsp name * chore: update snapshot filename --------- Co-authored-by: Ruben Co-authored-by: jannisvisser * Refactor the code validateRequiredFinancialServiceProviderConfiguration (#6029) Co-authored-by: Ruben * Remove permissions for refactor registration data (#6034) Co-authored-by: Ruben * Refactor registration data processing comments on AB#30189 * Made api test less twitchy * Out of date tests * Allow non-required properties in the portal & change import registrat… (#6024) * Allow non-required properties in the portal & change import registrations url * Refactor registration data comment on non-required properties in portal --------- Co-authored-by: Ruben --------- Co-authored-by: Ruben Co-authored-by: jannisvisser lint fix import Fix safaricom attributes Chore.cleanup code (#6036) * fix: rm lingering cases of program-question/custom-attribute/etc * chore: more cleanup * Fix.permission update fsp (#6041) * fix: check specifically on update-fsp permission in patch endpoint * test: api-test on bulk update chosen fsp * fix: process PR comments --- .gitignore | 2 + .../PersonalInformationPopUp.ts | 104 +- e2e/pages/Table/TableModule.ts | 15 +- e2e/portalicious/pages/RegistrationsPage.ts | 51 +- .../ExportDuplicateRegistrations.spec.ts | 5 +- .../ExportHighVolumeOfRegistrations.spec.ts | 2 +- .../ExportRegistrationsDataChanges.spec.ts | 3 +- .../ExportRegistrationsList.spec.ts | 5 +- .../test-program-with-validation-1000.csv | 2 +- .../test-registrations-KRCS-1000.csv | 2 +- .../test-registrations-KRCS.csv | 2 +- .../test-registrations-OCW-no-refereceId.csv | 2 +- .../test-registrations-OCW-scoped.csv | 2 +- .../test-registrations-OCW.csv | 2 +- .../test-registrations-PV-scoped.csv | 2 +- .../test-registrations-PV.csv | 2 +- .../test-registrations-demo-1000.csv | 2 +- .../test-registrations-demo.csv | 2 +- ...st-registrations-eth-joint-response-10.csv | 2 +- .../test-registrations-test-westeros-1000.csv | 1001 --------- .../test-registrations-westeros-1000.csv | 1001 +++++++++ .../test-registrations-westeros-20.csv | 42 +- .../OpenPopUpViewAndEdit.spec.ts | 6 +- ...UpdateCustomAttributesSuccessfully.spec.ts | 4 +- .../UpdateFinancialServiceProvider.spec.ts | 46 +- .../UpdatePaymentMultiplierInvalid.spec.ts | 14 +- .../UpdatePhoneNumberInvalid.spec.ts | 35 +- ....ts => MakeFailedPaymentSafaricom.spec.ts} | 20 +- .../Safaricom/RetryPaymentSafaricom.spec.ts | 11 +- .../ValidateFspInPaPopUp.spec.ts | 9 +- .../ViewActivityFspOverview.spec.ts | 20 +- .../ViewPersonalInformationTable.spec.ts | 9 +- features/121-Portal/Change_password.feature | 37 - .../121-Portal/Delete_people_affected.feature | 25 - .../Edit_Info_Person_Affected.feature | 139 -- .../Export_Intersolve_Visa_cards.feature | 19 - .../121-Portal/Export_PA_data_changes.feature | 20 - .../121-Portal/Export_Payment_Details.feature | 41 - ...ort_duplicate_people_affected_list.feature | 20 - .../121-Portal/Export_people_affected.feature | 28 - .../121-Portal/Export_unused_vouchers.feature | 19 - .../121-Portal/Import_registrations.feature | 52 - .../Include_people_affected.feature | 33 - features/121-Portal/Make_new_payment.feature | 319 --- .../Manage_Intersolve_Visa_card.feature | 88 - features/121-Portal/Manage_Users.feature | 21 - ...nage_payment_via_import_and_export.feature | 50 - .../121-Portal/Manage_team_members.feature | 72 - .../Mark_as_no_longer_eligible.feature | 17 - .../Navigate_home_and_main_menu.feature | 33 - .../121-Portal/Navigate_program_menu.feature | 69 - .../Navigate_program_phases.feature | 0 ...t_or_End_inclusion_people_affected.feature | 64 - .../Send_message_to_people_affected.feature | 43 - .../121-Portal/View_PA_profile_page.feature | 93 - .../View_and_Manage_people_affected.feature | 222 -- .../121-Portal/View_dashboard_page.feature | 69 - .../View_last_payment_overview.feature | 21 - features/121-Portal/View_messages.feature | 43 - .../View_payment_history_popup.feature | 57 - ...nd_Update_program_custom_attribute.feature | 21 - .../Export_vouchers_to_cancel.feature | 12 - features/Admin-user/Load_seed_data.feature | 22 - features/Admin-user/Manage_Roles.feature | 48 - .../Sync_Intersolve_Visa_Customer.feature | 14 - features/Admin-user/Update_program.feature | 26 - .../Update_program_question.feature | 20 - ...nd_reminder_on_uncollected_voucher.feature | 24 - features/Other/Claim_digital_voucher.feature | 53 - features/README.md | 129 -- ...egistration-activity-overview.component.ts | 45 +- ...stration-personal-information.component.ts | 19 +- .../registration-profile.component.html | 2 +- .../registration-profile.component.ts | 2 +- .../update-fsp/update-fsp.component.html | 50 +- .../update-fsp/update-fsp.component.ts | 102 +- .../update-property-item.component.html | 18 +- .../update-property-item.component.ts | 85 +- .../Portal/src/app/enums/fsp-name.enum.ts | 6 +- .../Portal/src/app/mocks/api.programs.mock.ts | 17 +- .../Portal/src/app/models/actions.model.ts | 1 - .../Portal/src/app/models/attribute.model.ts | 5 +- .../src/app/models/bulk-actions.models.ts | 2 +- interfaces/Portal/src/app/models/fsp.model.ts | 39 +- .../Portal/src/app/models/payment.model.ts | 3 +- .../Portal/src/app/models/person.model.ts | 7 +- .../Portal/src/app/models/program.model.ts | 24 +- .../models/registration-attribute.model.ts | 11 + .../src/app/models/transaction.model.ts | 6 +- .../bulk-import/bulk-import.component.html | 2 +- .../bulk-import/bulk-import.component.ts | 10 +- .../edit-person-affected-popup.component.html | 25 +- .../edit-person-affected-popup.component.ts | 102 +- .../export-fsp-instructions.component.ts | 20 +- .../make-payment/make-payment.component.ts | 14 +- .../app/program/metrics/metrics.component.ts | 2 +- .../payment-status-popup.component.ts | 6 +- .../program-payout.component.html | 3 +- .../program-payout.component.ts | 32 +- .../program-people-affected.component.html | 4 +- .../program-people-affected.component.ts | 28 +- ...n-activity-detail-accordion.component.html | 8 +- ...ion-activity-detail-accordion.component.ts | 6 +- .../src/app/services/past-payments.service.ts | 5 +- .../services/programs-service-api.service.ts | 66 +- .../Portal/src/app/services/table.service.ts | 55 +- .../message-editor.component.ts | 1 - .../Portal/src/app/shared/payment-result.ts | 20 +- .../Portal/src/app/shared/payment.utils.ts | 4 +- .../utils/check-attribute-input.utils.ts | 16 +- interfaces/Portal/src/assets/i18n/ar.json | 9 +- interfaces/Portal/src/assets/i18n/en.json | 9 +- interfaces/Portal/src/assets/i18n/es.json | 9 +- interfaces/Portal/src/assets/i18n/fr.json | 9 +- interfaces/Portal/src/assets/i18n/nl.json | 9 +- .../registration-menu.component.ts | 2 +- .../registrations-table.component.ts | 13 +- ...vice-provider-configuration.api.service.ts | 21 + ...al-service-provider-configuration.model.ts | 6 + .../financial-service-provider.helper.ts | 8 + .../financial-service-provider.model.ts | 4 +- .../domains/project/project.api.service.ts | 12 +- .../src/app/domains/project/project.helper.ts | 79 +- .../src/app/domains/project/project.model.ts | 4 +- .../registration/registration.api.service.ts | 2 +- .../project-monitoring.page.ts | 6 +- .../create-payment.component.html | 4 +- .../create-payment.component.ts | 60 +- .../activity-log-expanded-row.component.ts | 5 +- .../table-cell-overview.component.ts | 2 +- ...-registration-personal-information.page.ts | 4 +- .../export-registrations.component.ts | 6 +- .../src/app/services/export.service.ts | 33 +- .../src/app/services/messaging.service.ts | 3 +- k6/README.md | 2 + k6/helpers/registration-default.data.js | 40 +- k6/models/programs.js | 36 +- k6/models/registrations.js | 17 +- k6/tests/getProgramWithManyAttributes.js | 39 +- k6/tests/import1000Registrations.js | 63 + k6/tests/statusChangePaymentInLargeProgram.js | 38 +- package-lock.json | 126 ++ package.json | 1 + services/.env.example | 2 + services/121-service/.knip.json | 3 +- services/121-service/module-dependencies.md | 24 +- services/121-service/package-lock.json | 11 + services/121-service/package.json | 1 + .../121-service/src/actions/action.entity.ts | 1 - .../src/actions/utils/action.mapper.spec.ts | 4 +- .../src/activities/activities.mapper.ts | 7 +- .../src/activities/activities.service.ts | 2 +- .../transaction-activity.interface.ts | 5 +- services/121-service/src/app.module.ts | 11 +- .../src/events/events.service.spec.ts | 121 +- .../121-service/src/events/events.service.ts | 6 +- .../update-financial-service-provider.dto.ts | 66 - ...ancial-service-provider-attributes.enum.ts | 12 + .../financial-service-provider-name.enum.ts | 64 +- ...-service-provider-integration-type.enum.ts | 4 + ...ncial-service-provider-settings.helpers.ts | 36 + .../financial-service-provider.controller.ts | 147 +- .../financial-service-provider.dto.ts | 40 + .../financial-service-provider.entity.ts | 69 - .../financial-service-provider.module.ts | 26 +- .../financial-service-provider.service.ts | 159 +- ...ancial-service-providers-settings.const.ts | 171 ++ .../fsp-question.entity.ts | 74 - ...al-service-provider-question.repository.ts | 25 - .../financial-service-provider.repository.ts | 25 - .../src/metrics/dto/registration-type.dto.ts | 5 - .../121-service/src/metrics/metrics.module.ts | 8 +- .../src/metrics/metrics.service.ts | 404 ++-- .../migrate-visa/migrate-visa.controller.ts | 50 - .../src/migrate-visa/migrate-visa.module.ts | 17 - .../src/migrate-visa/migrate-visa.service.ts | 729 ------- ...0490970-remove-migrate-name-partner-org.ts | 52 +- ...54693178991-phasesAndEditableProperties.ts | 89 +- .../1656412499569-registrationData.ts | 189 +- ...330965061-migrate-data-changes-to-event.ts | 2 - ...0966062-migrate-status-changes-to-event.ts | 2 - ...isplay-name-portal-data-to-display-name.ts | 45 +- ...871246-remove-startedRegistration-state.ts | 2 - ...-remove-deleted-state-from-registration.ts | 2 - ...program-registration-attribute-refactor.ts | 616 ++++++ ...issing-program-fsp-confg-properties-seq.ts | 27 + .../message-incoming.service.ts | 24 +- .../message-queues.service.spec.ts | 7 +- .../message-queues/message-queues.service.ts | 17 +- .../message-template.service.ts | 10 +- .../src/payments/dto/fsp-instructions.dto.ts | 1 + .../dto/get-import-template-response.dto.ts | 4 + .../dto/import-reconciliation-response.dto.ts | 18 + .../src/payments/dto/pa-payment-data.dto.ts | 4 +- .../payments/dto/pa-payment-retry-data.dto.ts | 5 + .../dto/payment-transaction-result.dto.ts | 1 + .../dto/reconciliation-feedback.dto.ts | 27 + .../dto/transaction-relation-details.dto.ts | 1 + .../commercial-bank-ethiopia.api.service.ts | 14 +- .../commercial-bank-ethiopia.module.ts | 4 +- .../commercial-bank-ethiopia.service.spec.ts | 12 +- .../commercial-bank-ethiopia.service.ts | 209 +- .../dto/commercial-bank-ethiopia-job.dto.ts | 1 - .../excel/dto/excel-fsp-instructions.dto.ts | 19 +- .../fsp-integration/excel/excel.module.ts | 17 +- .../excel/excel.service.spec.ts | 155 -- .../fsp-integration/excel/excel.service.ts | 392 ++-- .../intersolve-visa.service.ts | 4 +- .../intersolve-store-voucher-options.dto.ts | 1 + .../dto/intersolve-voucher-job.dto.ts | 1 - .../intersolve-voucher.module.ts | 4 +- .../intersolve-voucher.service.spec.ts | 34 +- .../intersolve-voucher.service.ts | 101 +- .../intersolve-voucher-cron.service.ts | 64 +- .../reconciliation-return-type.interface.ts | 10 + .../src/payments/payments.controller.ts | 14 +- .../src/payments/payments.module.ts | 16 +- .../src/payments/payments.service.ts | 650 +++--- .../dto/get-audited-transaction.dto.ts | 5 +- .../transactions/dto/get-transaction.dto.ts | 14 +- .../transactions/transaction.entity.ts | 15 +- .../transactions/transaction.repository.ts | 30 +- .../transactions/transactions.module.ts | 2 - .../transactions/transactions.service.ts | 98 +- .../program-attributes.module.ts | 8 +- .../program-attributes.service.spec.ts | 72 +- .../program-attributes.service.ts | 175 +- ...ice-provider-configuration-property.dto.ts | 22 + ...cial-service-provider-configuration.dto.ts | 53 + ...der-configuration-property-response.dto.ts | 9 + ...ice-provider-configuration-response.dto.ts | 39 + ...ice-provider-configuration-property.dto.ts | 12 + ...cial-service-provider-configuration.dto.ts | 26 + ...-provider-configuration-property.entity.ts | 60 + ...l-service-provider-configuration.entity.ts | 56 + .../required-username-password.interface.ts | 4 + .../interfaces/username-password.interface.ts | 4 + ...vice-provider-configuration.mapper.spec.ts | 189 ++ ...l-service-provider-configuration.mapper.ts | 123 ++ ...l-service-provider-configuration.entity.ts | 65 - ...vice-provider-configurations.controller.ts | 312 ++- ...-service-provider-configurations.module.ts | 14 +- ...vice-provider-configurations.repository.ts | 117 +- ...ce-provider-configurations.service.spec.ts | 464 ++++- ...service-provider-configurations.service.ts | 456 ++++- .../create-program-custom-attribute.dto.ts | 52 - .../create-program-fsp-configuration.dto.ts | 25 - .../src/programs/dto/create-program.dto.ts | 220 +- .../src/programs/dto/found-program.dto.ts | 4 +- ... => program-registration-attribute.dto.ts} | 82 +- .../src/programs/dto/program-return.dto.ts | 218 +- .../update-program-fsp-configuration.dto.ts | 22 - .../src/programs/dto/validation-info.dto.ts | 7 - .../program-registration-attribute.mapper.ts | 55 + .../program-custom-attribute.entity.ts | 64 - ... program-registration-attribute.entity.ts} | 52 +- .../src/programs/program.entity.ts | 148 +- .../src/programs/programs.controller.ts | 165 +- .../src/programs/programs.module.ts | 15 +- .../src/programs/programs.service.ts | 574 ++---- .../overwrite-fsp-display-name.helper.ts | 77 - .../const/filter-operation.const.ts | 10 +- .../dto/bulk-action-result.dto.ts | 9 +- .../src/registration/dto/bulk-import.dto.ts | 17 +- .../src/registration/dto/bulk-update.dto.ts | 14 - .../src/registration/dto/custom-data.dto.ts | 21 - .../dto/registration-data-relation.model.ts | 5 +- .../dto/registration-update-job.dto.ts | 4 +- .../src/registration/dto/set-fsp.dto.ts | 24 - .../dto/update-registration.dto.ts | 38 +- .../dto/validate-registration-config.dto.ts | 13 - .../validate-registration-error-object.dto.ts | 6 - .../enum/custom-data-attributes.ts | 83 - .../enum/registration-attribute.enum.ts | 48 + .../enum/registration-csv-validation.enum.ts | 4 - ...registration-validation-input-type.enum.ts | 4 + .../validate-registration-config.interface.ts | 5 + ...ate-registration-error-object.interface.ts | 6 + .../validated-registration-input.interface.ts | 23 + .../queue-registrations-update.service.ts | 11 +- .../registration-data.module.ts | 4 +- .../registration-data.service.ts | 260 +-- .../registration-data.scoped.repository.ts | 40 +- .../registrations-update.processor.ts | 11 +- .../registration-attribute-data.entity.ts | 54 + .../registration/registration-data.entity.ts | 89 - .../registration/registration-view.entity.ts | 43 +- .../src/registration/registration.entity.ts | 22 +- .../registration/registrations.controller.ts | 119 +- .../src/registration/registrations.module.ts | 14 +- .../src/registration/registrations.service.ts | 627 +++--- .../services/inclusion-score.service.ts | 86 +- .../services/registrations-bulk.service.ts | 28 +- .../registrations-import.service.spec.ts | 43 +- .../services/registrations-import.service.ts | 409 ++-- .../registrations-pagination.service.ts | 125 +- ...stration-data-type.class.validator.spec.ts | 97 - .../registration-data-type.class.validator.ts | 234 --- .../registrations-input-validator.spec.ts | 216 +- .../registrations-input-validator.ts | 1058 +++++++--- ...gistrations-input.validator.helper.spec.ts | 39 +- .../registrations-input.validator.helper.ts | 18 +- .../121-service/src/scoped.repository.spec.ts | 6 +- .../121-service/src/scripts/seed-helper.ts | 175 +- services/121-service/src/scripts/seed-init.ts | 18 - .../src/scripts/seed-mock-helpers.ts | 13 +- .../mock-intersolve-voucher-attributes.sql | 13 +- .../scripts/sql/mock-make-phone-unique.sql | 8 +- .../scripts/sql/mock-payment-transactions.sql | 8 +- .../scripts/sql/mock-registration-data.sql | 16 +- .../src/scripts/sql/mock-registrations.sql | 4 +- ...mock-transations-one-per-registrations.sql | 8 +- .../src/scripts/sql/mock-visa-customers.sql | 4 +- .../fsp/fsp-commercial-bank-ethiopia.json | 23 - .../src/seed-data/fsp/fsp-excel.json | 9 - .../seed-data/fsp/fsp-intersolve-visa.json | 73 - .../fsp/fsp-intersolve-voucher-paper.json | 8 - .../fsp/fsp-intersolve-voucher-whatsapp.json | 24 - .../src/seed-data/fsp/fsp-safaricom.json | 22 - .../seed-data/mock/registration-pv.data.ts | 9 +- .../src/seed-data/mock/visa-card.data.ts | 9 +- .../src/seed-data/program/program-demo.json | 160 +- .../program/program-eth-joint-response.json | 37 +- .../program/program-joint-response-ANE.json | 103 +- .../program-joint-response-EKHCDC.json | 103 +- .../program-joint-response-dorcas.json | 103 +- .../program/program-krcs-baringo.json | 408 ++-- .../program/program-krcs-turkana.json | 408 ++-- .../program/program-krcs-westpokot.json | 408 ++-- .../seed-data/program/program-nlrc-ocw.json | 138 +- .../seed-data/program/program-nlrc-pv.json | 197 +- .../program/program-pilot-dorcas-eth.json | 730 +++++++ .../seed-data/program/program-pilot-lbn.json | 1788 +++++++++++++++++ .../seed-data/program/program-pilot-ukr.json | 417 ++++ .../program/program-pilot-zoa-eth.json | 730 +++++++ .../program/program-test-one-admin.json | 102 +- .../src/seed-data/program/program-test.json | 234 ++- .../seed-data/program/program-validation.json | 87 +- .../program-existence.interceptor.ts | 66 + ...transaction-job-processors.service.spec.ts | 22 +- .../transaction-job-processors.service.ts | 124 +- .../intersolve-visa-transaction-job.dto.ts | 13 +- .../dto/safaricom-transaction-job.dto.ts | 5 +- .../transaction-queues.service.spec.ts | 2 + .../src/user/enum/permission.enum.ts | 5 +- .../utils/file-import/file-import.service.ts | 5 +- .../registration-data-query.service.ts | 25 +- .../createFindWhereOptions.helper.spec.ts | 21 +- services/121-service/src/wrapper.type.ts | 4 - services/121-service/swagger.json | 202 +- .../test-registrations-OCW.csv | 12 +- ...est-registrations-patch-OCW-chosen-FSP.csv | 3 + ...s-patch-OCW-without-phoneNumber-column.csv | 7 +- .../test-registrations-patch-OCW.csv | 1 - .../export-validation-report.test.ts | 29 +- .../121-service/test/event/get-event.test.ts | 15 +- .../test/fixtures/scoped-registrations.ts | 22 +- .../121-service/test/helpers/assert.helper.ts | 33 +- ...l-service-provider-configuration.helper.ts | 146 ++ .../test/helpers/program.helper.ts | 102 +- .../test/helpers/registration.helper.ts | 6 +- .../test/helpers/utility.helper.ts | 21 +- .../program-exists.interceptor.test.ts | 33 + .../test/metrics/export-list.test.ts | 2 +- .../do-payment-fsp-excel.test.ts.snap | 130 +- ...o-payment-commercial-bank-ethiopia.test.ts | 81 + .../test/payment/do-payment-fsp-excel.test.ts | 167 +- ...st.ts => do-payment-fsp-safaricom.test.ts} | 81 +- .../do-payment-fsp-visa-debit-error.test.ts | 40 +- .../payment/do-payment-fsp-voucher.test.ts | 3 +- .../payment/payment-count-completed.test.ts | 3 +- .../test/payment/payment-in-progress.test.ts | 4 +- .../create-fsp-configuration.test.ts.snap | 543 ----- .../__snapshots__/create-program.test.ts.snap | 623 +++--- .../program/create-custom-attribute.test.ts | 97 - .../program/create-fsp-configuration.test.ts | 61 - .../program/create-program-question.test.ts | 107 - ...ate-program-registration-attribute.test.ts | 112 ++ .../test/program/create-program.test.ts | 47 +- ...ial-service-provider-configuration.test.ts | 357 ++++ .../test/program/message-template.test.ts | 4 - .../121-service/test/program/notes.test.ts | 31 +- .../test/program/update-program.test.ts | 44 - ...ate-payment-amount-multiplier.test.ts.snap | 25 + .../import-registration.test.ts.snap | 18 +- ...ervice-provider-configuration.test.ts.snap | 8 + .../update-registration.test.ts.snap | 8 + .../bulk-update-registration.test.ts | 269 +++ ...alculate-payment-amount-multiplier.test.ts | 130 ++ .../registrations/import-registration.test.ts | 199 +- .../mass-update-registration.test.ts | 126 -- .../get-registration-permission.test.ts | 3 +- .../pagination/get-registration.test.ts | 26 +- .../pagination/pagination-data.ts | 119 +- .../registrations-by-phone-number.test.ts | 118 +- .../send-message-with-placeholder.test.ts | 15 +- .../send-templated-message.test.ts | 3 +- ...ial-service-provider-configuration.test.ts | 255 +++ .../registrations/update-registration.test.ts | 280 ++- services/121-service/test/tsconfig.json | 2 +- .../121-service/test/users/user-roles.test.ts | 18 +- .../test/visa-card/block-visa-card.test.ts | 4 +- services/121-service/tsconfig.json | 2 +- .../mock-service/src/twilio/twilio.service.ts | 8 + 404 files changed, 16565 insertions(+), 14600 deletions(-) delete mode 100644 e2e/test-registration-data/test-registrations-test-westeros-1000.csv create mode 100644 e2e/test-registration-data/test-registrations-westeros-1000.csv rename e2e/tests/121-Portal/MakeNewPayment/Safaricom/{MissingNationalIdErrorSafaricom.spec.ts => MakeFailedPaymentSafaricom.spec.ts} (84%) delete mode 100644 features/121-Portal/Change_password.feature delete mode 100644 features/121-Portal/Delete_people_affected.feature delete mode 100644 features/121-Portal/Edit_Info_Person_Affected.feature delete mode 100644 features/121-Portal/Export_Intersolve_Visa_cards.feature delete mode 100644 features/121-Portal/Export_PA_data_changes.feature delete mode 100644 features/121-Portal/Export_Payment_Details.feature delete mode 100644 features/121-Portal/Export_duplicate_people_affected_list.feature delete mode 100644 features/121-Portal/Export_people_affected.feature delete mode 100644 features/121-Portal/Export_unused_vouchers.feature delete mode 100644 features/121-Portal/Import_registrations.feature delete mode 100644 features/121-Portal/Include_people_affected.feature delete mode 100644 features/121-Portal/Make_new_payment.feature delete mode 100644 features/121-Portal/Manage_Intersolve_Visa_card.feature delete mode 100644 features/121-Portal/Manage_Users.feature delete mode 100644 features/121-Portal/Manage_payment_via_import_and_export.feature delete mode 100644 features/121-Portal/Manage_team_members.feature delete mode 100644 features/121-Portal/Mark_as_no_longer_eligible.feature delete mode 100644 features/121-Portal/Navigate_home_and_main_menu.feature delete mode 100644 features/121-Portal/Navigate_program_menu.feature delete mode 100644 features/121-Portal/Navigate_program_phases.feature delete mode 100644 features/121-Portal/Reject_or_End_inclusion_people_affected.feature delete mode 100644 features/121-Portal/Send_message_to_people_affected.feature delete mode 100644 features/121-Portal/View_PA_profile_page.feature delete mode 100644 features/121-Portal/View_and_Manage_people_affected.feature delete mode 100644 features/121-Portal/View_dashboard_page.feature delete mode 100644 features/121-Portal/View_last_payment_overview.feature delete mode 100644 features/121-Portal/View_messages.feature delete mode 100644 features/121-Portal/View_payment_history_popup.feature delete mode 100644 features/Admin-user/Add_And_Update_program_custom_attribute.feature delete mode 100644 features/Admin-user/Export_vouchers_to_cancel.feature delete mode 100644 features/Admin-user/Load_seed_data.feature delete mode 100644 features/Admin-user/Manage_Roles.feature delete mode 100644 features/Admin-user/Sync_Intersolve_Visa_Customer.feature delete mode 100644 features/Admin-user/Update_program.feature delete mode 100644 features/Admin-user/Update_program_question.feature delete mode 100644 features/Automated/Send_reminder_on_uncollected_voucher.feature delete mode 100644 features/Other/Claim_digital_voucher.feature delete mode 100644 features/README.md create mode 100644 interfaces/Portal/src/app/models/registration-attribute.model.ts create mode 100644 interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.api.service.ts create mode 100644 interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.model.ts create mode 100644 interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.helper.ts create mode 100644 k6/tests/import1000Registrations.js delete mode 100644 services/121-service/src/financial-service-providers/dto/update-financial-service-provider.dto.ts create mode 100644 services/121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum.ts create mode 100644 services/121-service/src/financial-service-providers/financial-service-provider-integration-type.enum.ts create mode 100644 services/121-service/src/financial-service-providers/financial-service-provider-settings.helpers.ts create mode 100644 services/121-service/src/financial-service-providers/financial-service-provider.dto.ts delete mode 100644 services/121-service/src/financial-service-providers/financial-service-provider.entity.ts create mode 100644 services/121-service/src/financial-service-providers/financial-service-providers-settings.const.ts delete mode 100644 services/121-service/src/financial-service-providers/fsp-question.entity.ts delete mode 100644 services/121-service/src/financial-service-providers/repositories/financial-service-provider-question.repository.ts delete mode 100644 services/121-service/src/financial-service-providers/repositories/financial-service-provider.repository.ts delete mode 100644 services/121-service/src/metrics/dto/registration-type.dto.ts delete mode 100644 services/121-service/src/migrate-visa/migrate-visa.controller.ts delete mode 100644 services/121-service/src/migrate-visa/migrate-visa.module.ts delete mode 100644 services/121-service/src/migrate-visa/migrate-visa.service.ts create mode 100644 services/121-service/src/migration/1729605362361-program-registration-attribute-refactor.ts create mode 100644 services/121-service/src/migration/1729848646578-missing-program-fsp-confg-properties-seq.ts create mode 100644 services/121-service/src/payments/dto/get-import-template-response.dto.ts create mode 100644 services/121-service/src/payments/dto/import-reconciliation-response.dto.ts create mode 100644 services/121-service/src/payments/dto/pa-payment-retry-data.dto.ts create mode 100644 services/121-service/src/payments/dto/reconciliation-feedback.dto.ts delete mode 100644 services/121-service/src/payments/fsp-integration/excel/excel.service.spec.ts create mode 100644 services/121-service/src/payments/interfaces/reconciliation-return-type.interface.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/interfaces/required-username-password.interface.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.spec.ts create mode 100644 services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.ts delete mode 100644 services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity.ts delete mode 100644 services/121-service/src/programs/dto/create-program-custom-attribute.dto.ts delete mode 100644 services/121-service/src/programs/dto/create-program-fsp-configuration.dto.ts rename services/121-service/src/programs/dto/{program-question.dto.ts => program-registration-attribute.dto.ts} (62%) delete mode 100644 services/121-service/src/programs/dto/update-program-fsp-configuration.dto.ts delete mode 100644 services/121-service/src/programs/dto/validation-info.dto.ts create mode 100644 services/121-service/src/programs/mappers/program-registration-attribute.mapper.ts delete mode 100644 services/121-service/src/programs/program-custom-attribute.entity.ts rename services/121-service/src/programs/{program-question.entity.ts => program-registration-attribute.entity.ts} (56%) delete mode 100644 services/121-service/src/programs/utils/overwrite-fsp-display-name.helper.ts delete mode 100644 services/121-service/src/registration/dto/bulk-update.dto.ts delete mode 100644 services/121-service/src/registration/dto/custom-data.dto.ts delete mode 100644 services/121-service/src/registration/dto/set-fsp.dto.ts delete mode 100644 services/121-service/src/registration/dto/validate-registration-config.dto.ts delete mode 100644 services/121-service/src/registration/dto/validate-registration-error-object.dto.ts delete mode 100644 services/121-service/src/registration/enum/custom-data-attributes.ts create mode 100644 services/121-service/src/registration/enum/registration-attribute.enum.ts delete mode 100644 services/121-service/src/registration/enum/registration-csv-validation.enum.ts create mode 100644 services/121-service/src/registration/enum/registration-validation-input-type.enum.ts create mode 100644 services/121-service/src/registration/interfaces/validate-registration-config.interface.ts create mode 100644 services/121-service/src/registration/interfaces/validate-registration-error-object.interface.ts create mode 100644 services/121-service/src/registration/interfaces/validated-registration-input.interface.ts create mode 100644 services/121-service/src/registration/registration-attribute-data.entity.ts delete mode 100644 services/121-service/src/registration/registration-data.entity.ts delete mode 100644 services/121-service/src/registration/validators/registration-data-type.class.validator.spec.ts delete mode 100644 services/121-service/src/registration/validators/registration-data-type.class.validator.ts delete mode 100644 services/121-service/src/seed-data/fsp/fsp-commercial-bank-ethiopia.json delete mode 100644 services/121-service/src/seed-data/fsp/fsp-excel.json delete mode 100644 services/121-service/src/seed-data/fsp/fsp-intersolve-visa.json delete mode 100644 services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-paper.json delete mode 100644 services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-whatsapp.json delete mode 100644 services/121-service/src/seed-data/fsp/fsp-safaricom.json create mode 100644 services/121-service/src/seed-data/program/program-pilot-dorcas-eth.json create mode 100644 services/121-service/src/seed-data/program/program-pilot-lbn.json create mode 100644 services/121-service/src/seed-data/program/program-pilot-ukr.json create mode 100644 services/121-service/src/seed-data/program/program-pilot-zoa-eth.json create mode 100644 services/121-service/src/shared/interceptors/program-existence.interceptor.ts create mode 100644 services/121-service/test-registration-data/test-registrations-patch-OCW-chosen-FSP.csv create mode 100644 services/121-service/test/helpers/program-financial-service-provider-configuration.helper.ts create mode 100644 services/121-service/test/interceptors/program-exists.interceptor.test.ts create mode 100644 services/121-service/test/payment/do-payment-commercial-bank-ethiopia.test.ts rename services/121-service/test/payment/{do-payment-safaricom.test.ts => do-payment-fsp-safaricom.test.ts} (87%) delete mode 100644 services/121-service/test/program/__snapshots__/create-fsp-configuration.test.ts.snap delete mode 100644 services/121-service/test/program/create-custom-attribute.test.ts delete mode 100644 services/121-service/test/program/create-fsp-configuration.test.ts delete mode 100644 services/121-service/test/program/create-program-question.test.ts create mode 100644 services/121-service/test/program/create-program-registration-attribute.test.ts create mode 100644 services/121-service/test/program/manage-program-financial-service-provider-configuration.test.ts create mode 100644 services/121-service/test/registrations/__snapshots__/calculate-payment-amount-multiplier.test.ts.snap create mode 100644 services/121-service/test/registrations/__snapshots__/update-registration-program-financial-service-provider-configuration.test.ts.snap create mode 100644 services/121-service/test/registrations/__snapshots__/update-registration.test.ts.snap create mode 100644 services/121-service/test/registrations/bulk-update-registration.test.ts create mode 100644 services/121-service/test/registrations/calculate-payment-amount-multiplier.test.ts delete mode 100644 services/121-service/test/registrations/mass-update-registration.test.ts create mode 100644 services/121-service/test/registrations/update-registration-program-financial-service-provider-configuration.test.ts diff --git a/.gitignore b/.gitignore index 68bdb4c8d6..34b3ea475a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ node_modules # TS Build output logs *.tsbuildinfo + +**/.DS_Store diff --git a/e2e/pages/PersonalInformationPopUp/PersonalInformationPopUp.ts b/e2e/pages/PersonalInformationPopUp/PersonalInformationPopUp.ts index 007053e41b..759a154c8b 100644 --- a/e2e/pages/PersonalInformationPopUp/PersonalInformationPopUp.ts +++ b/e2e/pages/PersonalInformationPopUp/PersonalInformationPopUp.ts @@ -2,22 +2,12 @@ import { expect, Locator } from '@playwright/test'; import { Page } from 'playwright'; import englishTranslations from '@121-portal/src/assets/i18n/en.json'; -import visaIntersolveTranslations from '@121-service/src/seed-data/fsp/fsp-intersolve-visa.json'; -import programTestTranslations from '@121-service/src/seed-data/program/program-test.json'; +import programTest from '@121-service/src/seed-data/program/program-test.json'; const updateSuccesfullNotification = englishTranslations.common['update-success']; const programWesterosQuestionName = - programTestTranslations.programQuestions[1].label.en; -const fspCahngeWarning = - englishTranslations['page'].program['program-people-affected'][ - 'edit-person-affected-popup' - ].fspChangeWarning; -const visaQuestionStreet = visaIntersolveTranslations.questions[0].label.en; -const visaQuestionHouseNumberAddition = - visaIntersolveTranslations.questions[2].label.en; -const visaQuestionPostalCode = visaIntersolveTranslations.questions[3].label.en; -const visaQuestionCity = visaIntersolveTranslations.questions[4].label.en; + programTest.programRegistrationAttributes[1].label.en; class PersonalInformationPopup { readonly page: Page; @@ -121,8 +111,29 @@ class PersonalInformationPopup { const saveButton = this.page.getByRole('button', { name: saveButtonName }); const okButton = this.page.getByRole('button', { name: okButtonName }); const alertMessage = this.page.getByRole('alertdialog'); - const fieldInput = fieldSelector.getByRole('textbox'); - await fieldInput.fill(newValue); + + // Locate the field input which can be a textbox, textarea, number input, phone input, select, or checkbox + const fieldInput = fieldSelector.locator( + 'ion-input input, textarea, input[type="number"], input[type="tel"], ion-select, ion-checkbox', + ); + + if ( + await fieldInput.evaluate( + (el) => el.tagName.toLowerCase() === 'ion-select', + ) + ) { + await fieldInput.selectOption(newValue); + } else if ( + await fieldInput.evaluate( + (el) => el.tagName.toLowerCase() === 'ion-input', + ) + ) { + // Locate the native input element within ion-input + const nativeInput = fieldInput.locator('input'); + await nativeInput.fill(newValue); + } else { + await fieldInput.fill(newValue); + } await this.page.waitForLoadState('networkidle'); await fieldSelector.getByText(saveButtonName).click(); @@ -258,7 +269,7 @@ class PersonalInformationPopup { } async selectFspInputForm({ filterValue }: { filterValue: string }) { - const fieldSelector = this.personAffectedPopUpFsp; + const fieldSelector = this.editPersonAffectedPopUp; const updatePropertyItem = fieldSelector.locator( 'app-update-property-item', ); @@ -270,60 +281,63 @@ class PersonalInformationPopup { return inputForm.getByRole('textbox'); } + async updateAttributeByLabel({ + labelText, + newValue, + }: { + labelText: string; + newValue: string; + }) { + const saveButtonName = englishTranslations.common.save; + const okButtonName = englishTranslations.common.ok; + const labelElement = this.editPersonAffectedPopUp + .locator(`text=${labelText}`) + .first(); + const parentElement = labelElement.locator('..').locator('..'); + + await this.updateField({ + fieldSelector: parentElement, + newValue, + saveButtonName, + okButtonName, + reasonText: (newValue) => `Change ${labelText} to ${newValue}`, + }); + } + async updatefinancialServiceProvider({ fspNewName, fspOldName, saveButtonName, okButtonName, + newAttributes, }: { fspNewName: string; fspOldName: string; saveButtonName: string; okButtonName: string; + newAttributes: { labelText: string; newValue: string }[]; }) { + // Loop over the attributes and update each one + for (const attribute of newAttributes) { + await this.updateAttributeByLabel({ + labelText: attribute.labelText, + newValue: attribute.newValue, + }); + } + const dropdown = this.page.getByRole('radio'); - const warning = fspCahngeWarning; - const newValue = 'Nieuwe straat'; const fieldSelector = this.personAffectedPopUpFsp; const okButton = this.page.getByRole('button', { name: okButtonName }); - const streetAdressInput = await this.selectFspInputForm({ - filterValue: visaQuestionStreet, - }); - const numberAdditionInput = await this.selectFspInputForm({ - filterValue: visaQuestionHouseNumberAddition, - }); - const postalCodeInput = await this.selectFspInputForm({ - filterValue: visaQuestionPostalCode, - }); - const cityInput = await this.selectFspInputForm({ - filterValue: visaQuestionCity, - }); await this.validateFspNamePresentInEditPopUp(fspOldName); await this.financialServiceProviderDropdown.click(); await dropdown.getByText(fspNewName).click(); - await this.validateFspWarningInEditPopUp(warning); - - await streetAdressInput.fill(newValue); - await this.personAffectedHouseNumber.getByLabel('').click(); - await this.personAffectedHouseNumber.getByLabel('').fill('2'); - await numberAdditionInput.fill('D'); - await postalCodeInput.fill('1234AB'); - await cityInput.fill('Amsterdam'); - await postalCodeInput.click(); await fieldSelector.getByText(saveButtonName).click(); await okButton.waitFor({ state: 'visible' }); await okButton.click(); } - - async validateFspWarningInEditPopUp(warning: string) { - await this.page.waitForLoadState('networkidle'); - const element = this.page.locator('ion-text.ion-padding.md.hydrated'); - const text = await element.textContent(); - expect(text).toContain(warning); - } } export default PersonalInformationPopup; diff --git a/e2e/pages/Table/TableModule.ts b/e2e/pages/Table/TableModule.ts index 0f781bf86c..48aeeecd5b 100644 --- a/e2e/pages/Table/TableModule.ts +++ b/e2e/pages/Table/TableModule.ts @@ -5,7 +5,14 @@ import { Locator, Page } from 'playwright'; import * as XLSX from 'xlsx'; import englishTranslations from '@121-portal/src/assets/i18n/en.json'; -import visaFspIntersolve from '@121-service/src/seed-data/fsp/fsp-intersolve-visa.json'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; + +const fsp = FINANCIAL_SERVICE_PROVIDER_SETTINGS.find( + (fsp) => fsp.name === FinancialServiceProviders.intersolveVisa, +); + +const labelVisaEn = fsp ? fsp.defaultLabel.en : undefined; const paymentLabel = englishTranslations.page.program['program-people-affected'].actions.doPayment; @@ -472,7 +479,7 @@ class TableModule { // Assert the values of the row Object.entries(assertionData).forEach(([key, value]) => { - expect(rowToAssert[key]).toBe(value); + expect(rowToAssert![key]).toBe(value); }); } @@ -483,7 +490,7 @@ class TableModule { for (let i = 1; i <= count; i++) { const fsp = this.page.locator(TableModule.getRow(i)); const fspText = (await fsp.textContent())?.trim(); - const isVisaFsp = fspText?.includes(visaFspIntersolve.displayName.en); + const isVisaFsp = fspText?.includes(labelVisaEn!); if ( (shouldSelectVisa && isVisaFsp) || @@ -503,7 +510,7 @@ class TableModule { for (let i = 1; i <= count; i++) { const fsp = this.page.locator(TableModule.getRow(i)); const fspText = (await fsp.textContent())?.trim(); - const isVisaFsp = fspText?.includes(visaFspIntersolve.displayName.en); + const isVisaFsp = fspText?.includes(labelVisaEn!); if ( (shouldIncludeVisa && isVisaFsp) || diff --git a/e2e/portalicious/pages/RegistrationsPage.ts b/e2e/portalicious/pages/RegistrationsPage.ts index e1ff1eb17b..7dccb4e3fb 100644 --- a/e2e/portalicious/pages/RegistrationsPage.ts +++ b/e2e/portalicious/pages/RegistrationsPage.ts @@ -16,7 +16,7 @@ const expectedColumnsSelectedRegistrationsExport = [ 'paymentAmountMultiplier', 'paymentCount', 'registrationCreatedDate', - 'fspDisplayName', + 'programFinancialServiceProviderConfigurationLabel', 'scope', 'namePartnerOrganization', 'fullName', @@ -42,7 +42,8 @@ const expectedColumnsDuplicateRegistrationsExport = [ 'referenceId', 'id', 'status', - 'fsp', + 'financialServiceProviderName', + 'programFinancialServiceProviderConfigurationLabel', 'scope', 'phoneNumber', 'whatsappPhoneNumber', @@ -55,12 +56,12 @@ interface ExportPaAssertionData { id: number; paymentAmountMultiplier: number; preferredLanguage: string; - fspDisplayName: string; + programFinancialServiceProviderConfigurationLabel: string; whatsappPhoneNumber?: string; } interface ExportStatusAndDataChangesData { - paId: number; + referenceId: string; changedBy: string; type: string; newValue: string; @@ -70,7 +71,7 @@ interface ExportStatusAndDataChangesData { interface ExportDuplicateRegistrationsData { id: number; status: string; - fsp: string; + programFinancialServiceProviderConfigurationLabel: string; name: string; duplicateWithIds: string; } @@ -318,11 +319,27 @@ class RegistrationsPage extends BasePage { col.toLowerCase().trim(), ); - if ( - !normalizedExpectedColumns.every((col) => actualColumns.includes(col)) || - normalizedExpectedColumns.length !== actualColumns.length - ) { - throw new Error('Column validation failed'); + const missingColumns = normalizedExpectedColumns.filter( + (col) => !actualColumns.includes(col), + ); + const extraColumns = actualColumns.filter( + (col) => !normalizedExpectedColumns.includes(col), + ); + + if (missingColumns.length > 0 || extraColumns.length > 0) { + const errorMessage = [ + 'Column validation failed:', + missingColumns.length > 0 + ? `Missing columns: ${missingColumns.join(', ')}` + : '', + extraColumns.length > 0 + ? `Extra columns: ${extraColumns.join(', ')}` + : '', + ] + .filter(Boolean) + .join(' '); + + throw new Error(errorMessage); } const mappedAssertionData = Object.keys(assertionData).reduce( @@ -354,7 +371,7 @@ class RegistrationsPage extends BasePage { id, paymentAmountMultiplier, preferredLanguage, - fspDisplayName, + programFinancialServiceProviderConfigurationLabel, }: ExportPaAssertionData, validateRowCount?: { condition: boolean; minRowCount: number }, ) { @@ -363,7 +380,7 @@ class RegistrationsPage extends BasePage { id, paymentAmountMultiplier, preferredLanguage, - fspDisplayName, + programFinancialServiceProviderConfigurationLabel, }; await this.exportAndAssertData( expectedColumnsSelectedRegistrationsExport, @@ -377,7 +394,7 @@ class RegistrationsPage extends BasePage { async exportAndAssertStatusAndDataChanges( registrationIndex: number, { - paId, + referenceId, changedBy, type, newValue, @@ -386,7 +403,7 @@ class RegistrationsPage extends BasePage { validateRowCount?: { condition: boolean; minRowCount: number }, ) { const assertionData = { - paId, + referenceId, changedBy, type, newValue, @@ -396,7 +413,7 @@ class RegistrationsPage extends BasePage { expectedColumnsStatusAndDataChangesExport, assertionData, registrationIndex, - undefined, + referenceId, validateRowCount, ); } @@ -406,7 +423,7 @@ class RegistrationsPage extends BasePage { { id, status, - fsp, + programFinancialServiceProviderConfigurationLabel, name, duplicateWithIds, }: ExportDuplicateRegistrationsData, @@ -415,7 +432,7 @@ class RegistrationsPage extends BasePage { const assertionData = { id, status, - fsp, + programFinancialServiceProviderConfigurationLabel, name, duplicateWithIds, }; diff --git a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportDuplicateRegistrations.spec.ts b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportDuplicateRegistrations.spec.ts index 21ab54ece1..8edbca1e9a 100644 --- a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportDuplicateRegistrations.spec.ts +++ b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportDuplicateRegistrations.spec.ts @@ -15,7 +15,8 @@ import RegistrationsPage from '@121-e2e/portalicious/pages/RegistrationsPage'; // Export duplicate registrations const id = 2; const status = 'included'; -const fsp = 'Albert Heijn voucher WhatsApp'; +const programFinancialServiceProviderConfigurationLabel = + 'Albert Heijn voucher WhatsApp'; const name = 'Jan Janssen'; const duplicateWithIds = '3'; @@ -53,7 +54,7 @@ test('[29318] Export duplicate people affected list', async ({ page }) => { await registrations.exportAndAssertDuplicates(0, { id, status, - fsp, + programFinancialServiceProviderConfigurationLabel, name, duplicateWithIds, }); diff --git a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportHighVolumeOfRegistrations.spec.ts b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportHighVolumeOfRegistrations.spec.ts index 23e1b03aec..cc652386ed 100644 --- a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportHighVolumeOfRegistrations.spec.ts +++ b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportHighVolumeOfRegistrations.spec.ts @@ -59,7 +59,7 @@ test('[29359] Export inclusion list with 15000 PAs', async ({ page }) => { status, paymentAmountMultiplier, preferredLanguage, - fspDisplayName, + programFinancialServiceProviderConfigurationLabel: fspDisplayName, }, { condition: true, minRowCount: 15000 }, ); diff --git a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsDataChanges.spec.ts b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsDataChanges.spec.ts index 35c4366754..9e413b6491 100644 --- a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsDataChanges.spec.ts +++ b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsDataChanges.spec.ts @@ -13,7 +13,6 @@ import LoginPage from '@121-e2e/portalicious/pages/LoginPage'; import RegistrationsPage from '@121-e2e/portalicious/pages/RegistrationsPage'; // Export status & data changes -const paId = 4; const changedBy = 'admin@example.org'; const type = 'registrationStatusChange'; const newValue = 'included'; @@ -51,7 +50,7 @@ test('[29337] Export all People Affected data changes', async ({ page }) => { 'Export status & data changes', ); await registrations.exportAndAssertStatusAndDataChanges(0, { - paId, + referenceId: registrationsPV[0].referenceId, // Assert only the first registration changedBy, type, newValue, diff --git a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsList.spec.ts b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsList.spec.ts index 82c0f22faf..610240ae60 100644 --- a/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsList.spec.ts +++ b/e2e/portalicious/tests/ExportPeopleAffectedList/ExportRegistrationsList.spec.ts @@ -17,7 +17,8 @@ const status = 'included'; const id = 1; const paymentAmountMultiplier = 1; const preferredLanguage = 'nl'; -const fspDisplayName = 'Albert Heijn voucher WhatsApp'; +const programFinancialServiceProviderConfigurationLabel = + 'Albert Heijn voucher WhatsApp'; test.beforeEach(async ({ page }) => { await resetDB(SeedScript.nlrcMultiple); @@ -55,7 +56,7 @@ test('[29358] Export People Affected list', async ({ page }) => { status, paymentAmountMultiplier, preferredLanguage, - fspDisplayName, + programFinancialServiceProviderConfigurationLabel, }); }); }); diff --git a/e2e/test-registration-data/test-program-with-validation-1000.csv b/e2e/test-registration-data/test-program-with-validation-1000.csv index d6af39eaae..fb264e0549 100644 --- a/e2e/test-registration-data/test-program-with-validation-1000.csv +++ b/e2e/test-registration-data/test-program-with-validation-1000.csv @@ -1,4 +1,4 @@ -phoneNumber,preferredLanguage,fspName,paymentAmountMultiplier,nameFirst,nameLast,nameMiddle,whatsappPhoneNumber +phoneNumber,preferredLanguage,programFinancialServiceProviderConfigurationName,paymentAmountMultiplier,nameFirst,nameLast,nameMiddle,whatsappPhoneNumber 63910680830,en,Intersolve-voucher-whatsapp,2,Micheal,Shelton,Cory,29738048450 81101414999,nl,Intersolve-voucher-whatsapp,2,Brandon,Gray,Ora,89312620536 20718425180,nl,Intersolve-voucher-paper,2,Kyle,Cook,Mario,30073112391 diff --git a/e2e/test-registration-data/test-registrations-KRCS-1000.csv b/e2e/test-registration-data/test-registrations-KRCS-1000.csv index 00a357ced1..f66e7bc1a8 100644 --- a/e2e/test-registration-data/test-registrations-KRCS-1000.csv +++ b/e2e/test-registration-data/test-registrations-KRCS-1000.csv @@ -1,4 +1,4 @@ -referenceId,fspName,phoneNumber,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,gender,age,maritalStatus,registrationType,nationalId,nameAlternate,totalHH,totalSub5,totalAbove60,otherSocialAssistance,county,subCounty,ward,location,subLocation,village,nearestSchool,areaType,mainSourceLivelihood,mainSourceLivelihoodOther,Male05,Female05,Male612,Female612,Male1324,Female1324,Male2559,Female2559,Male60,Female60,maleTotal,femaleTotal,householdMembersDisability,disabilityAmount,householdMembersChronicIllness,chronicIllnessAmount,householdMembersPregnantLactating,pregnantLactatingAmount,habitableRooms,tenureStatusOfDwelling,ownerOccupiedState,ownerOccupiedStateOther,rentedFrom,rentedFromOther,constructionMaterialRoof,ifRoofOtherSpecify,constructionMaterialWall,ifWallOtherSpecify,constructionMaterialFloor,ifFloorOtherSpecify,dwellingRisk,ifRiskOtherSpecify,mainSourceOfWater,ifWaterOtherSpecify,pigs,ifYesPigs,chicken,mainModeHumanWasteDisposal,ifHumanWasteOtherSpecify,cookingFuel,ifFuelOtherSpecify,Lighting,ifLightingOtherSpecify,householdItems,excoticCattle,ifYesExoticCattle,IndigenousCattle,ifYesIndigenousCattle,sheep,ifYesSheep,goats,ifYesGoats,camels,ifYesCamels,donkeys,ifYesDonkeys,ifYesChicken,howManyBirths,howManyDeaths,householdConditions,skipMeals,receivingBenefits,ifYesNameProgramme,typeOfBenefit,ifOtherBenefit,ifCash,ifInKind,feedbackOnRespons,ifYesFeedback,whoDecidesHowToSpend,possibilityForConflicts,genderedDivision,ifYesElaborate,geopoint +referenceId,programFinancialServiceProviderConfigurationName,phoneNumber,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,gender,age,maritalStatus,registrationType,nationalId,nameAlternate,totalHH,totalSub5,totalAbove60,otherSocialAssistance,county,subCounty,ward,location,subLocation,village,nearestSchool,areaType,mainSourceLivelihood,mainSourceLivelihoodOther,Male05,Female05,Male612,Female612,Male1324,Female1324,Male2559,Female2559,Male60,Female60,maleTotal,femaleTotal,householdMembersDisability,disabilityAmount,householdMembersChronicIllness,chronicIllnessAmount,householdMembersPregnantLactating,pregnantLactatingAmount,habitableRooms,tenureStatusOfDwelling,ownerOccupiedState,ownerOccupiedStateOther,rentedFrom,rentedFromOther,constructionMaterialRoof,ifRoofOtherSpecify,constructionMaterialWall,ifWallOtherSpecify,constructionMaterialFloor,ifFloorOtherSpecify,dwellingRisk,ifRiskOtherSpecify,mainSourceOfWater,ifWaterOtherSpecify,pigs,ifYesPigs,chicken,mainModeHumanWasteDisposal,ifHumanWasteOtherSpecify,cookingFuel,ifFuelOtherSpecify,Lighting,ifLightingOtherSpecify,householdItems,excoticCattle,ifYesExoticCattle,IndigenousCattle,ifYesIndigenousCattle,sheep,ifYesSheep,goats,ifYesGoats,camels,ifYesCamels,donkeys,ifYesDonkeys,ifYesChicken,howManyBirths,howManyDeaths,householdConditions,skipMeals,receivingBenefits,ifYesNameProgramme,typeOfBenefit,ifOtherBenefit,ifCash,ifInKind,feedbackOnRespons,ifYesFeedback,whoDecidesHowToSpend,possibilityForConflicts,genderedDivision,ifYesElaborate,geopoint b7559784-db28-448e-8ef7-e1b927b95bd4,Safaricom,254708374149,en,1,6,Barbara Floyd,male,25,married,self,32121321,test,56,1,1,no,ethiopia,ethiopia,dsa,21,2113,adis abea,213321,urban,salary_from_formal_employment,213,1,0,0,0,0,0,0,0,0,0,0,0,no,0,no,0,no,0,0,Owner occupied,purchased,0,individual,0,tin,31213,tiles,231312,cement,asdsd,fire,123213,lake,dasdas,no,123123,no,septic_tank,31213,electricity,asdsda,electricity,dasasd,none,no,12231123,no,123132123,no,12312312,no,312123,no,312123,no,213312,2,0,0,poor,no,0,0,in_kind,2123312,12312,132132,no,312123,male_household_head,no,no,asddas,123231 f5eaff25-882a-4a50-b3a3-156da01f4aa5,Safaricom,254708374149,en,1,6,Barbara Floyd,male,25,married,self,32121321,test,56,1,1,no,ethiopia,ethiopia,dsa,21,2113,adis abea,213321,urban,salary_from_formal_employment,213,1,0,0,0,0,0,0,0,0,0,0,0,no,0,no,0,no,0,0,Owner occupied,purchased,0,individual,0,tin,31213,tiles,231312,cement,asdsd,fire,123213,lake,dasdas,no,123123,no,septic_tank,31213,electricity,asdsda,electricity,dasasd,none,no,12231123,no,123132123,no,12312312,no,312123,no,312123,no,213312,2,0,0,poor,no,0,0,in_kind,2123312,12312,132132,no,312123,male_household_head,no,no,asddas,123231 d2f7dfde-7406-428d-8cd7-f67f2cf92faf,Safaricom,254708374149,en,1,6,Barbara Floyd,male,25,married,self,32121321,test,56,1,1,no,ethiopia,ethiopia,dsa,21,2113,adis abea,213321,urban,salary_from_formal_employment,213,1,0,0,0,0,0,0,0,0,0,0,0,no,0,no,0,no,0,0,Owner occupied,purchased,0,individual,0,tin,31213,tiles,231312,cement,asdsd,fire,123213,lake,dasdas,no,123123,no,septic_tank,31213,electricity,asdsda,electricity,dasasd,none,no,12231123,no,123132123,no,12312312,no,312123,no,312123,no,213312,2,0,0,poor,no,0,0,in_kind,2123312,12312,132132,no,312123,male_household_head,no,no,asddas,123231 diff --git a/e2e/test-registration-data/test-registrations-KRCS.csv b/e2e/test-registration-data/test-registrations-KRCS.csv index 068370d749..01a831f9d7 100644 --- a/e2e/test-registration-data/test-registrations-KRCS.csv +++ b/e2e/test-registration-data/test-registrations-KRCS.csv @@ -1,4 +1,4 @@ -fspName,phoneNumber,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,gender,age,maritalStatus,registrationType,nationalId,nameAlternate,totalHH,totalSub5,totalAbove60,otherSocialAssistance,county,subCounty,ward,location,subLocation,village,nearestSchool,areaType,mainSourceLivelihood,mainSourceLivelihoodOther,Male05,Female05,Male612,Female612,Male1324,Female1324,Male2559,Female2559,Male60,Female60,maleTotal,femaleTotal,householdMembersDisability,disabilityAmount,householdMembersChronicIllness,chronicIllnessAmount,householdMembersPregnantLactating,pregnantLactatingAmount,habitableRooms,tenureStatusOfDwelling,ownerOccupiedState,ownerOccupiedStateOther,rentedFrom,rentedFromOther,constructionMaterialRoof,ifRoofOtherSpecify,constructionMaterialWall,ifWallOtherSpecify,constructionMaterialFloor,ifFloorOtherSpecify,dwellingRisk,ifRiskOtherSpecify,mainSourceOfWater,ifWaterOtherSpecify,pigs,ifYesPigs,chicken,mainModeHumanWasteDisposal,ifHumanWasteOtherSpecify,cookingFuel,ifFuelOtherSpecify,Lighting,ifLightingOtherSpecify,householdItems,excoticCattle,ifYesExoticCattle,IndigenousCattle,ifYesIndigenousCattle,sheep,ifYesSheep,goats,ifYesGoats,camels,ifYesCamels,donkeys,ifYesDonkeys,ifYesChicken,howManyBirths,howManyDeaths,householdConditions,skipMeals,receivingBenefits,ifYesNameProgramme,typeOfBenefit,ifOtherBenefit,ifCash,ifInKind,feedbackOnRespons,ifYesFeedback,whoDecidesHowToSpend,possibilityForConflicts,genderedDivision,ifYesElaborate,geopoint +programFinancialServiceProviderConfigurationName,phoneNumber,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,gender,age,maritalStatus,registrationType,nationalId,nameAlternate,totalHH,totalSub5,totalAbove60,otherSocialAssistance,county,subCounty,ward,location,subLocation,village,nearestSchool,areaType,mainSourceLivelihood,mainSourceLivelihoodOther,Male05,Female05,Male612,Female612,Male1324,Female1324,Male2559,Female2559,Male60,Female60,maleTotal,femaleTotal,householdMembersDisability,disabilityAmount,householdMembersChronicIllness,chronicIllnessAmount,householdMembersPregnantLactating,pregnantLactatingAmount,habitableRooms,tenureStatusOfDwelling,ownerOccupiedState,ownerOccupiedStateOther,rentedFrom,rentedFromOther,constructionMaterialRoof,ifRoofOtherSpecify,constructionMaterialWall,ifWallOtherSpecify,constructionMaterialFloor,ifFloorOtherSpecify,dwellingRisk,ifRiskOtherSpecify,mainSourceOfWater,ifWaterOtherSpecify,pigs,ifYesPigs,chicken,mainModeHumanWasteDisposal,ifHumanWasteOtherSpecify,cookingFuel,ifFuelOtherSpecify,Lighting,ifLightingOtherSpecify,householdItems,excoticCattle,ifYesExoticCattle,IndigenousCattle,ifYesIndigenousCattle,sheep,ifYesSheep,goats,ifYesGoats,camels,ifYesCamels,donkeys,ifYesDonkeys,ifYesChicken,howManyBirths,howManyDeaths,householdConditions,skipMeals,receivingBenefits,ifYesNameProgramme,typeOfBenefit,ifOtherBenefit,ifCash,ifInKind,feedbackOnRespons,ifYesFeedback,whoDecidesHowToSpend,possibilityForConflicts,genderedDivision,ifYesElaborate,geopoint Safaricom,254708374149,en,1,6,Barbara Floyd,male,25,singleParent,self,32121321,test,56,1,1,no,ethiopia,ethiopia,dsa,21,2113,adis abea,213321,urban,salary_from_formal_employment,213,1,0,0,0,0,0,0,0,0,0,0,0,no,0,no,0,no,0,0,Owner occupied,purchased,0,individual,0,tin,31213,tiles,231312,cement,asdsd,fire,123213,Water vendor,dasdas,no,123123,no,septic_tank,31213,electricity,asdsda,electricity,dasasd,television__tv,no,12231123,no,123132123,no,12312312,no,312123,no,312123,no,213312,2,0,0,poor,no,0,0,in_kind,2123312,12312,132132,no,312123,male_household_head,no,no,asddas,123231 Safaricom,254708374149,en,1,6,Rebecca Rodgers,male,25,singleParent,self,32121321,test,56,1,1,no,ethiopia,ethiopia,dsa,21,2113,adis abea,213321,urban,salary_from_formal_employment,213,1,0,0,0,0,0,0,0,0,0,0,0,no,0,no,0,no,0,0,Owner occupied,purchased,0,individual,0,tin,31213,tiles,231312,cement,asdsd,fire,123213,Water vendor,dasdas,no,123123,no,septic_tank,31213,electricity,asdsda,electricity,dasasd,television__tv,no,12231123,no,123132123,no,12312312,no,312123,no,312123,no,213312,2,0,0,poor,no,0,0,in_kind,2123312,12312,132132,no,312123,male_household_head,no,no,asddas,123231 Safaricom,254708374149,en,1,6,Eddie Sharp,male,25,singleParent,self,32121321,test,56,1,1,no,ethiopia,ethiopia,dsa,21,2113,adis abea,213321,urban,salary_from_formal_employment,213,1,0,0,0,0,0,0,0,0,0,0,0,no,0,no,0,no,0,0,Owner occupied,purchased,0,individual,0,tin,31213,tiles,231312,cement,asdsd,fire,123213,Water vendor,dasdas,no,123123,no,septic_tank,31213,electricity,asdsda,electricity,dasasd,television__tv,no,12231123,no,123132123,no,12312312,no,312123,no,312123,no,213312,2,0,0,poor,no,0,0,in_kind,2123312,12312,132132,no,312123,male_household_head,no,no,asddas,123231 diff --git a/e2e/test-registration-data/test-registrations-OCW-no-refereceId.csv b/e2e/test-registration-data/test-registrations-OCW-no-refereceId.csv index be5fa736be..0f596df868 100644 --- a/e2e/test-registration-data/test-registrations-OCW-no-refereceId.csv +++ b/e2e/test-registration-data/test-registrations-OCW-no-refereceId.csv @@ -1,4 +1,4 @@ -preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,fspName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity +preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,programFinancialServiceProviderConfigurationName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity en,1,,Test succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag en,1,,Test mock-fail-create-customer,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag en,1,,Test mock-fail-create-customer,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag diff --git a/e2e/test-registration-data/test-registrations-OCW-scoped.csv b/e2e/test-registration-data/test-registrations-OCW-scoped.csv index f23158446c..bca7144797 100644 --- a/e2e/test-registration-data/test-registrations-OCW-scoped.csv +++ b/e2e/test-registration-data/test-registrations-OCW-scoped.csv @@ -1,4 +1,4 @@ -referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,fspName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity,scope +referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,programFinancialServiceProviderConfigurationName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity,scope ,en,1,,Test succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,zeeland.middelburg ,en,1,,Test mock-fail-create-customer,14155238887,Intersolve-visa,14155238887,Straat,1,A,1234AB,Den Haag,zeeland.middelburg ,en,1,,Test mock-fail-create-wallet,14155238888,Intersolve-visa,14155238888,Straat,1,A,1234AB,Den Haag,zeeland.middelburg diff --git a/e2e/test-registration-data/test-registrations-OCW.csv b/e2e/test-registration-data/test-registrations-OCW.csv index a116126759..4fa0c795f8 100644 --- a/e2e/test-registration-data/test-registrations-OCW.csv +++ b/e2e/test-registration-data/test-registrations-OCW.csv @@ -1,4 +1,4 @@ -referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,fspName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity +referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,programFinancialServiceProviderConfigurationName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity 00dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 01dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test mock-fail-create-customer,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 02dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test mock-fail-create-customer,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag diff --git a/e2e/test-registration-data/test-registrations-PV-scoped.csv b/e2e/test-registration-data/test-registrations-PV-scoped.csv index dda245c78f..00e3eb6e0f 100644 --- a/e2e/test-registration-data/test-registrations-PV-scoped.csv +++ b/e2e/test-registration-data/test-registrations-PV-scoped.csv @@ -1,4 +1,4 @@ -namePartnerOrganization,preferredLanguage,paymentAmountMultiplier,fullName,phoneNumber,fspName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity,scope +namePartnerOrganization,preferredLanguage,paymentAmountMultiplier,fullName,phoneNumber,programFinancialServiceProviderConfigurationName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity,scope ABC,en,1,Henk de Vries,31600000000,Intersolve-voucher-whatsapp,31600000000,,,,,,utrecht.houten DEF,en,1,Jan Klaassen,31600000001,Intersolve-voucher-paper,,,,,,,zeeland.middelburg ABC,en,1,Ahmed Al-Saadi,14155238886,Intersolve-visa,14155238885,Straat,1,A,1234AB,Den Haag,utrecht.houten diff --git a/e2e/test-registration-data/test-registrations-PV.csv b/e2e/test-registration-data/test-registrations-PV.csv index c900229742..55b4af256c 100644 --- a/e2e/test-registration-data/test-registrations-PV.csv +++ b/e2e/test-registration-data/test-registrations-PV.csv @@ -1,4 +1,4 @@ -referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,fspName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity +referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,programFinancialServiceProviderConfigurationName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity 00dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 01dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-create-customer,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 02dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-create-customer,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag diff --git a/e2e/test-registration-data/test-registrations-demo-1000.csv b/e2e/test-registration-data/test-registrations-demo-1000.csv index 5801c57475..4446db29cb 100644 --- a/e2e/test-registration-data/test-registrations-demo-1000.csv +++ b/e2e/test-registration-data/test-registrations-demo-1000.csv @@ -1,4 +1,4 @@ -phoneNumber,preferredLanguage,fspName,paymentAmountMultiplier,name,middlename,firstname,gender,birthdate,documentid,headhousehold,males,boys,women,girls,householdsituation,maritalsituation,pregnant,breastfeeding,womendisabled,mendisabled,girlsdisabled,boysdisabled,childrenmalnourished,malnourishedtreatment,whatsappPhoneNumber +phoneNumber,preferredLanguage,programFinancialServiceProviderConfigurationName,paymentAmountMultiplier,name,middlename,firstname,gender,birthdate,documentid,headhousehold,males,boys,women,girls,householdsituation,maritalsituation,pregnant,breastfeeding,womendisabled,mendisabled,girlsdisabled,boysdisabled,childrenmalnourished,malnourishedtreatment,whatsappPhoneNumber 63772290692,nl,Intersolve-voucher-whatsapp,1,Gibbs,Max,Johanna,Féminin,30-11-2000,GoNdk,Oui,1,8,0,3,returnee,married,4,5,3,4,8,1,8,Oui,00144693326 70746587715,nl,Intersolve-voucher-whatsapp,1,Freeman,Ethan,Glenn,Masculin ,30-11-2000,pxZUb,Oui,3,0,2,3,autochtonehost,abandoned,3,0,4,7,2,5,3,Non,39325874466 58007695278,en,Intersolve-voucher-whatsapp,2,Jordan,Leila,Jean,Féminin,15-03-1990,jufXU,Oui,0,7,3,2,autochtoneprivate,married,3,2,1,0,7,8,7,Non,93252247657 diff --git a/e2e/test-registration-data/test-registrations-demo.csv b/e2e/test-registration-data/test-registrations-demo.csv index d5801083a1..a860862242 100644 --- a/e2e/test-registration-data/test-registrations-demo.csv +++ b/e2e/test-registration-data/test-registrations-demo.csv @@ -1,4 +1,4 @@ -referenceId,fspName,phoneNumber,preferredLanguage,paymentAmountMultiplier,whatsappPhoneNumber,name,middlename,firstname,gender,birthdate,documentid,headhousehold,males,boys,women,girls,householdsituation,maritalsituation,pregnant,breastfeeding,womendisabled,mendisabled,girlsdisabled,boysdisabled,childrenmalnourished,malnourishedtreatment,whatsappPhoneNumber +referenceId,programFinancialServiceProviderConfigurationName,phoneNumber,preferredLanguage,paymentAmountMultiplier,whatsappPhoneNumber,name,middlename,firstname,gender,birthdate,documentid,headhousehold,males,boys,women,girls,householdsituation,maritalsituation,pregnant,breastfeeding,womendisabled,mendisabled,girlsdisabled,boysdisabled,childrenmalnourished,malnourishedtreatment,whatsappPhoneNumber ,Excel,31600000000,en,1,,Test Test Test,Test,Test,Male,31-12-1970,123,Yes,1,1,1,1,Retournee,Married,1,1,1,1,1,1,1,No, ,Excel,31600000001,en,1,,Test Test Test,Test,Test,Male,31-12-1971,234,Yes,1,1,1,1,Retournee,Married,1,1,1,1,1,1,1,No, ,Excel,31600000002,en,1,,Test Test Test,Test,Test,Male,31-12-1972,345,Yes,1,1,1,1,Retournee,Married,1,1,1,1,1,1,1,No, diff --git a/e2e/test-registration-data/test-registrations-eth-joint-response-10.csv b/e2e/test-registration-data/test-registrations-eth-joint-response-10.csv index 22c22eabe8..88c3ef7cfa 100644 --- a/e2e/test-registration-data/test-registrations-eth-joint-response-10.csv +++ b/e2e/test-registration-data/test-registrations-eth-joint-response-10.csv @@ -1,4 +1,4 @@ -referenceId,phoneNumber,preferredLanguage,paymentAmountMultiplier,fspName,maxPayments,fullName,idNumber,age,gender,howManyFemale,howManyMale,totalFamilyMembers,howManyFemaleUnder18,howManyMaleUnder18,howManyFemaleOver18,howManyMaleOver18,howManyFemaleDisabilityUnder18,howManyMaleDisabilityUnder18,howManyFemaleDisabilityOver18,howManyMaleDisabilityOver18,bankAccountNumber +referenceId,phoneNumber,preferredLanguage,paymentAmountMultiplier,programFinancialServiceProviderConfigurationName,maxPayments,fullName,idNumber,age,gender,howManyFemale,howManyMale,totalFamilyMembers,howManyFemaleUnder18,howManyMaleUnder18,howManyFemaleOver18,howManyMaleOver18,howManyFemaleDisabilityUnder18,howManyMaleDisabilityUnder18,howManyFemaleDisabilityOver18,howManyMaleDisabilityOver18,bankAccountNumber osV3YlTAv5dJ0AV$X5sdBd^$8va5QZZsV@!C,52264463005001,en,1,Commercial-bank-ethiopia,3,ANDUALEM MOHAMMED YIMER,39231855170,48,Male,9,7,2,2,8,4,5,6,1,7,2,407951684723597 0lO[ZX6tsEZfA6Ga8VyRNHCn%&AQ[^LakJ#(,52264463005002,en,1,Commercial-bank-ethiopia,3,Barbara Floyd,32622403964,31,Female,5,6,0,4,4,2,3,1,5,5,0,407951684723595 J%%jEbUvNQ]D3E)3*S5i^vQX1^*YZdKO6[)D,52264463005003,en,1,Commercial-bank-ethiopia,3,Rebecca Rodgers,72798209938,62,Male,6,8,3,7,1,7,9,5,2,2,3,407951684723596 diff --git a/e2e/test-registration-data/test-registrations-test-westeros-1000.csv b/e2e/test-registration-data/test-registrations-test-westeros-1000.csv deleted file mode 100644 index 6b2bd03d99..0000000000 --- a/e2e/test-registration-data/test-registrations-test-westeros-1000.csv +++ /dev/null @@ -1,1001 +0,0 @@ -phoneNumber,preferredLanguage,fspName,knowsNothing,textCustomAttribute,name,dob,house,dragon,skills,whatsappPhoneNumber,personalId,fixedChoice,openAnswer,date,accountId -44289054723,en,FSP - all attributes,false,Cpvdi,Philip Grant,15-03-1990,lannister,53,demoOption1,59008866125,ODmgOZzI,no,CIV,16-05-1990,54489265 -55750352306,nl,FSP - no attributes,false,bqFHo,Luke Gomez,30-11-2000,greyjoy,07,demoOption2,29342449989,VeWtEUns,no,nXN,27-10-2002,43784156 -27883373741,en,Intersolve-voucher-whatsapp,false,BunDP,Lester Cortez,30-11-2000,greyjoy,41,demoOption2,16517945997,DqwOsUvL,yes,eyY,27-10-2002,81305916 -73609651628,en,FSP - no attributes,true,IHsUW,Terry Wallace,15-03-1990,stark,76,demoOption2,46082513349,nhvoZbzA,no,yDy,16-05-1990,38243576 -36225186458,en,Excel,false,GrYvU,Josie Watts,15-03-1990,stark,84,demoOption1,17848130562,fUdtaogP,yes,fKZ,27-10-2002,67956212 -41450377722,en,Intersolve-voucher-whatsapp,false,urgYp,Martin Kim,30-11-2000,greyjoy,51,demoOption3,50049611675,DTwGcgMD,yes,pOM,27-10-2002,45167920 -47033686665,nl,FSP - all attributes,true,VxJsH,Brett Kim,15-03-1990,greyjoy,80,demoOption1,33470153853,YlpxzQXe,yes,cLr,27-10-2002,08902455 -15576066176,en,Excel,false,AOawO,Ina Webster,15-03-1990,lannister,12,demoOption3,79895725900,RgxMbAfT,no,udB,16-05-1990,25408770 -07854025644,en,Bank A,false,JEiLw,John Love,30-11-2000,lannister,02,demoOption1,80624764436,CKlDoQmm,yes,HgO,27-10-2002,36315888 -56191435798,nl,Intersolve-voucher-whatsapp,true,jcIxa,Katherine Hammond,15-03-1990,greyjoy,19,demoOption2,13946717244,UYcozjbz,yes,SoP,16-05-1990,29801679 -24958004492,nl,FSP - all attributes,true,URGfI,Jorge Norton,30-11-2000,greyjoy,96,demoOption3,22764294704,TQrdxdQv,no,PKJ,27-10-2002,01453688 -19268281893,nl,Excel,true,ijoPq,Lulu Lloyd,30-11-2000,greyjoy,22,demoOption3,33153598997,KSUKuGbb,yes,vmB,27-10-2002,47961761 -18335973172,nl,Intersolve-voucher-whatsapp,true,bglrk,Amelia Campbell,15-03-1990,stark,37,demoOption2,81893287976,SRSjZZCA,yes,RtY,16-05-1990,39299549 -49244267457,en,FSP - no attributes,true,Nhvlv,Dominic Goodman,30-11-2000,stark,27,demoOption1,95546333707,buObmOEy,no,ucl,27-10-2002,16486955 -23230452842,nl,Excel,false,gbQlR,Allen Brooks,15-03-1990,stark,81,demoOption3,51622627860,jellvRvo,yes,EVN,27-10-2002,91947347 -37284762030,nl,FSP - all attributes,false,txGUl,Alfred Carroll,30-11-2000,stark,88,demoOption1,46405809566,qLoIturt,yes,LBi,27-10-2002,54545587 -15300863253,en,FSP - no attributes,false,Qgjqj,Margaret Guerrero,15-03-1990,lannister,30,demoOption3,88692918512,vaoDcAbf,yes,dhO,16-05-1990,42823461 -13079334174,nl,Excel,true,HZnHw,Garrett Ortiz,30-11-2000,lannister,50,demoOption1,38491386161,kAiJPRMU,no,oqE,27-10-2002,27648904 -40011667404,en,FSP - all attributes,false,foBSd,Elva Carter,30-11-2000,stark,89,demoOption2,02000871763,gHgAaMfz,yes,gIY,27-10-2002,25165830 -56960578191,en,FSP - no attributes,true,romWq,Alma Moreno,30-11-2000,lannister,94,demoOption2,48075221766,MLAcGIAa,no,nkF,16-05-1990,37765635 -04080147674,en,FSP - all attributes,false,iveTa,Linnie Brewer,15-03-1990,lannister,21,demoOption1,49053825729,AdgQadjx,yes,gGf,16-05-1990,69880741 -24239124936,en,FSP - no attributes,false,TSoxH,Cordelia Morris,30-11-2000,lannister,07,demoOption2,35422724759,YjsTZzcl,yes,VKW,16-05-1990,07005353 -93610134799,en,FSP - all attributes,false,cUgUi,Minnie Evans,30-11-2000,lannister,28,demoOption1,90677434217,KtGMVswk,yes,XKa,27-10-2002,08765336 -25147396185,nl,Intersolve-voucher-whatsapp,false,GwlAz,Duane Nichols,30-11-2000,lannister,50,demoOption1,53311048728,qLwkdrVh,no,wSt,27-10-2002,73505533 -37779549737,nl,Excel,false,swbjw,Henrietta James,30-11-2000,greyjoy,01,demoOption1,37729459527,hcHtQeKK,yes,xRB,27-10-2002,11290866 -35332461865,nl,Intersolve-voucher-whatsapp,true,IaaGc,Mary Bowen,15-03-1990,greyjoy,38,demoOption3,45739522163,VDqehQhF,yes,QxG,16-05-1990,78785727 -58481546707,en,FSP - no attributes,true,TjOVQ,Belle Valdez,30-11-2000,lannister,62,demoOption1,63604642447,RXHfoqKz,no,RUc,16-05-1990,50777437 -98554749207,en,Excel,true,GLPZx,Cameron Watson,15-03-1990,lannister,33,demoOption2,64748778391,RToiLvDT,no,mZF,16-05-1990,95456906 -80062561910,nl,Excel,false,EGcuN,Blanche Young,30-11-2000,stark,37,demoOption3,69766627657,kpYwMvdQ,no,uDg,27-10-2002,50930282 -43161116446,nl,FSP - no attributes,false,RkPco,Lora Shaw,30-11-2000,lannister,81,demoOption3,60358385024,RyKejcWr,no,XUe,27-10-2002,35218642 -93768945152,en,FSP - all attributes,true,MlVmT,Walter Morton,15-03-1990,lannister,31,demoOption1,18982662706,WrrnNksT,no,dCA,16-05-1990,61160423 -28872700529,nl,FSP - all attributes,true,CxzqE,Mittie Gibbs,15-03-1990,greyjoy,94,demoOption2,14269917496,nDIPsyvZ,no,UAb,27-10-2002,28649035 -67518079864,en,Intersolve-voucher-whatsapp,false,pJBBD,Beulah Reynolds,15-03-1990,lannister,61,demoOption1,19339124759,yiRzuRqs,yes,qXh,27-10-2002,94025208 -33301860108,nl,FSP - all attributes,false,FJdbR,Tyler Cruz,30-11-2000,lannister,92,demoOption1,36938727465,TIXbtPlM,yes,dmM,27-10-2002,43931560 -81598730905,en,FSP - no attributes,true,YwPIs,Charlie Doyle,30-11-2000,stark,94,demoOption2,92426039384,jzEnOJrG,yes,lCJ,16-05-1990,79025695 -21465534315,en,FSP - no attributes,true,TcirE,Francis Warner,15-03-1990,greyjoy,01,demoOption3,75374453146,xwJKHQwg,no,Qod,16-05-1990,85096447 -80402709946,en,FSP - no attributes,false,DrjCD,Victoria McLaughlin,15-03-1990,stark,33,demoOption2,61715631912,mzisBzhq,no,zZT,27-10-2002,27491213 -96467665456,nl,FSP - all attributes,true,pjHsS,Frederick Bowman,30-11-2000,lannister,05,demoOption2,50684157609,xzTmtpRc,no,pZk,16-05-1990,21097465 -90500374238,en,Bank A,false,NVcLz,Richard Cox,15-03-1990,greyjoy,87,demoOption2,90277262190,QgXLYEJp,no,xWy,16-05-1990,19649652 -54273629263,en,FSP - no attributes,false,iQPku,Franklin Romero,15-03-1990,greyjoy,29,demoOption1,76758769376,bcUqIIQQ,yes,rQg,16-05-1990,28410328 -92684223419,nl,Intersolve-voucher-whatsapp,true,rPVRE,Louisa Williamson,15-03-1990,greyjoy,54,demoOption1,14766513271,JLlomfmm,yes,iWg,16-05-1990,53478813 -78905236269,nl,Excel,true,btTIT,Dora Richards,30-11-2000,lannister,59,demoOption3,27063301472,OhFnmPhx,yes,BZi,16-05-1990,83229576 -42941008240,en,FSP - no attributes,true,IiVuX,Gene Haynes,15-03-1990,stark,82,demoOption1,44890157877,pnTSIvYZ,yes,Dfu,27-10-2002,37198743 -32093196598,en,FSP - no attributes,false,lbNcY,Violet Brady,30-11-2000,lannister,96,demoOption3,23366739777,AMrEQIjf,yes,qLq,16-05-1990,75151947 -29354590298,nl,Bank A,true,mPIMk,Phillip Clarke,30-11-2000,greyjoy,95,demoOption1,94470126487,IDhAgRWF,no,GeQ,16-05-1990,58035833 -26824063798,en,Intersolve-voucher-whatsapp,false,hfqPu,Lura Jordan,30-11-2000,lannister,51,demoOption1,61470033141,PpOSxOKU,no,cwD,27-10-2002,78253089 -90336095808,nl,FSP - no attributes,false,SlxmI,Callie Little,30-11-2000,greyjoy,68,demoOption3,93535229579,wSLnRKQY,yes,niG,16-05-1990,47353578 -87718370501,en,Excel,false,RsOdt,Ruth Cummings,15-03-1990,greyjoy,33,demoOption1,90065536462,TuAtrKJr,yes,unB,16-05-1990,35467548 -50928918987,en,Bank A,true,eCFKW,Ollie Bell,15-03-1990,greyjoy,90,demoOption2,22611707387,OZQxySko,no,mxP,27-10-2002,08648561 -45550791229,nl,Excel,true,FHdtv,Hannah Fox,30-11-2000,stark,97,demoOption3,18654530591,jnXQACnu,no,GGr,27-10-2002,54656188 -28783367146,nl,FSP - all attributes,false,xnblF,Irene Logan,30-11-2000,greyjoy,56,demoOption2,99302435135,soChOuHA,no,JgU,27-10-2002,30767384 -88798765997,en,Intersolve-voucher-whatsapp,true,REoct,Julian Lynch,30-11-2000,lannister,26,demoOption1,57660860357,LvWIKfFC,yes,KjM,27-10-2002,40264380 -76609155137,en,Excel,true,ZjKMs,Irene Page,15-03-1990,greyjoy,98,demoOption1,13355729230,FhKazHgI,yes,ddF,16-05-1990,50921542 -12616983918,en,FSP - no attributes,true,TZhCj,John Dennis,15-03-1990,stark,86,demoOption1,97358322670,mUxbUTyD,yes,xlz,16-05-1990,99917595 -23459238408,nl,Excel,true,HKxTm,Roger Guzman,30-11-2000,lannister,47,demoOption1,71888037481,GxUykSaJ,no,IWP,27-10-2002,98321770 -76394353339,en,FSP - no attributes,true,lXvQp,Rosetta Peterson,15-03-1990,lannister,16,demoOption3,90105478261,JcvogHDy,no,yEI,27-10-2002,89119688 -03100297338,en,Excel,false,MAdHW,Nathan Guzman,15-03-1990,greyjoy,71,demoOption1,31543065020,ZrLoWpwF,yes,dBn,16-05-1990,86641134 -16908625395,nl,Excel,false,tfxoB,Gary Alvarado,30-11-2000,lannister,71,demoOption1,22130484413,IFTklhpN,no,JUm,16-05-1990,28898449 -84120779697,nl,FSP - no attributes,true,yPRFF,Andrew Pena,15-03-1990,lannister,36,demoOption2,51206609961,RpTjwQsg,no,tox,27-10-2002,76529357 -28418210787,en,Excel,true,pPUxO,Austin Harmon,30-11-2000,lannister,18,demoOption2,61123082043,qnntwrFv,no,qOf,16-05-1990,20225735 -60885040245,en,FSP - no attributes,false,pzwoT,Jason Coleman,15-03-1990,greyjoy,35,demoOption2,88561466946,gdKNmXob,yes,kHp,16-05-1990,27031636 -42907157166,nl,Intersolve-voucher-whatsapp,true,rEzqM,Ricardo Ball,30-11-2000,stark,10,demoOption2,71953501391,VPBvqZhm,no,dPw,27-10-2002,29468468 -13312986689,en,Bank A,false,IEQBr,Chase Alexander,15-03-1990,greyjoy,20,demoOption1,43634012502,kMVnSxxw,yes,bXP,16-05-1990,44578430 -25792811053,nl,FSP - no attributes,true,xyMJi,Theodore Douglas,30-11-2000,greyjoy,78,demoOption2,85971296056,XWtTsYTH,no,pha,16-05-1990,17214261 -42266000162,en,FSP - no attributes,true,bJhHu,Jack Wade,30-11-2000,greyjoy,99,demoOption2,47231657437,UOHjxxlC,yes,pzA,16-05-1990,69720205 -60629031521,en,Bank A,true,xPZhh,Cody Byrd,30-11-2000,stark,02,demoOption3,51990706177,YDUBiZSH,no,ZeU,27-10-2002,16253722 -88176029681,en,FSP - all attributes,true,pLpcP,Sue Collier,15-03-1990,stark,75,demoOption2,26783454511,iCUaEAgL,no,cXz,16-05-1990,42502113 -01550239114,en,FSP - no attributes,false,ymlzV,Mayme Buchanan,30-11-2000,lannister,35,demoOption2,13803612826,hxsKTmcs,no,mEB,27-10-2002,22622225 -50891664341,en,FSP - no attributes,true,oesSz,Willie Hart,30-11-2000,greyjoy,46,demoOption2,55124869019,tesEBkQK,yes,tHO,27-10-2002,75366206 -28321421988,nl,FSP - no attributes,true,lquIc,Douglas Romero,15-03-1990,lannister,09,demoOption1,80190132481,xFkvkKhz,no,Fmg,27-10-2002,29661005 -47930845596,en,FSP - all attributes,true,Ddjdo,Lillian Parker,15-03-1990,lannister,37,demoOption2,98094500885,EIVstMqF,yes,rpg,16-05-1990,98758274 -01260180269,nl,Excel,true,NtJxg,Nannie Hubbard,15-03-1990,stark,17,demoOption3,81564510275,tKulSXcn,yes,Qsl,16-05-1990,14079861 -30837861746,en,Excel,true,XJwkS,Olga Warner,30-11-2000,greyjoy,09,demoOption1,85079837124,vUCSEQrB,yes,jcJ,27-10-2002,84301388 -08910562497,en,Excel,true,neXHR,Susan Bailey,15-03-1990,lannister,82,demoOption2,28206492774,jCOuOvqG,yes,JZH,27-10-2002,66711174 -66195821441,nl,Intersolve-voucher-whatsapp,false,QMGhU,Clyde Joseph,15-03-1990,lannister,89,demoOption1,74970894543,sxKEfris,yes,lHq,16-05-1990,56721513 -51723265252,nl,Intersolve-voucher-whatsapp,true,bfulQ,Alexander Young,15-03-1990,lannister,15,demoOption1,13432888906,YZiziXxj,no,uMt,16-05-1990,45722640 -43976152416,nl,FSP - no attributes,true,BBjcu,Herman Carlson,15-03-1990,greyjoy,81,demoOption2,04144723830,ZkUVuZgJ,no,lPn,16-05-1990,62189331 -65874537874,nl,Bank A,false,DuoHr,Fanny Sullivan,30-11-2000,stark,75,demoOption1,11072548150,DBNattjH,no,fpn,27-10-2002,77350145 -35069482618,en,FSP - all attributes,true,GpPCg,Jayden Wise,30-11-2000,stark,62,demoOption2,63112902401,TvjixGQW,yes,ncX,16-05-1990,81768055 -78690306980,en,Bank A,false,shZOW,Duane Boone,15-03-1990,stark,92,demoOption2,49318535252,HUivyKaW,yes,jQU,27-10-2002,62301539 -51239869605,nl,Excel,true,KxuSC,Lydia Sanders,15-03-1990,lannister,58,demoOption3,35498964847,uXqsWCKI,no,bVo,16-05-1990,24205648 -19698119171,en,Excel,true,vOnuZ,Eliza Rice,30-11-2000,lannister,22,demoOption2,28997796671,DRAtyBPw,no,EzY,27-10-2002,27274054 -73626757505,nl,FSP - no attributes,true,qlamh,Gregory Pratt,15-03-1990,stark,36,demoOption1,73415361391,CsQpsHOs,yes,nPC,16-05-1990,48886012 -70881917395,en,Intersolve-voucher-whatsapp,false,QeUsL,Ray McLaughlin,30-11-2000,greyjoy,39,demoOption1,31765821465,yjrfeGXW,no,obX,27-10-2002,84958096 -96637340898,nl,FSP - no attributes,false,LkdMR,Carolyn Marsh,15-03-1990,greyjoy,72,demoOption2,31313154702,wnhoJfDw,no,FOJ,27-10-2002,62013379 -28312306097,nl,FSP - no attributes,false,lDgyg,Barry Ford,30-11-2000,stark,26,demoOption2,71730172252,VEOpxkvn,no,Llk,27-10-2002,13192208 -61960446588,nl,Excel,false,gwvgw,Oscar Gray,30-11-2000,lannister,41,demoOption2,10093906992,adPQwIGW,no,ojj,27-10-2002,23087559 -48608090243,nl,Bank A,true,RFzgr,Bryan Hardy,15-03-1990,greyjoy,43,demoOption3,67164549851,wJXoWevS,no,icL,27-10-2002,83608072 -13812886419,nl,Bank A,true,OSYyG,Johnny Moore,30-11-2000,greyjoy,69,demoOption2,11039744828,BbXSXLjc,no,Rum,27-10-2002,47579493 -67916514741,nl,Intersolve-voucher-whatsapp,true,TPtmV,Alfred Wagner,30-11-2000,lannister,71,demoOption1,96109262266,gXJYxCgK,no,HjH,27-10-2002,57202708 -97156683400,en,Bank A,true,RThvh,Melvin Burgess,30-11-2000,greyjoy,17,demoOption1,40693675362,RLWmnMUQ,no,ZTO,16-05-1990,79095412 -14125524802,nl,Excel,false,NjYGD,Bryan Singleton,30-11-2000,greyjoy,76,demoOption2,51737409057,tbkBCLtk,no,KmE,16-05-1990,45508474 -13607575826,en,Excel,true,NQYsp,Steven Jacobs,15-03-1990,stark,50,demoOption2,12345510338,lNmvZthI,yes,rUq,16-05-1990,43482087 -01554056305,en,FSP - all attributes,false,ljtVh,Leona Erickson,30-11-2000,greyjoy,34,demoOption2,32319552059,SFnNbQsr,yes,hFq,27-10-2002,97421680 -30754371355,en,FSP - all attributes,false,Tbrsr,Mark Watson,30-11-2000,greyjoy,79,demoOption1,49266907333,TQpPVCry,yes,wBo,16-05-1990,41439724 -44828813259,en,Intersolve-voucher-whatsapp,true,jYzmX,Nicholas McCarthy,15-03-1990,stark,32,demoOption3,17578876300,JApTImYv,yes,OIH,16-05-1990,63890429 -48927928124,en,Intersolve-voucher-whatsapp,true,FfSkL,Addie Christensen,15-03-1990,lannister,81,demoOption1,00088458882,HiZqDFLn,no,TaL,27-10-2002,46127137 -24037346499,nl,Intersolve-voucher-whatsapp,false,eLglU,Floyd Bowman,15-03-1990,stark,98,demoOption3,73689938952,XWMigabt,no,smr,27-10-2002,25309570 -89387467470,nl,FSP - no attributes,false,boyta,Francis Nelson,30-11-2000,stark,52,demoOption3,89450522055,TTcOYlrc,no,mBM,27-10-2002,11296756 -14663342747,nl,Intersolve-voucher-whatsapp,false,pOfgS,Beulah Frank,15-03-1990,stark,34,demoOption1,82100964646,DIpeChka,no,FKa,16-05-1990,65161122 -28655669154,nl,FSP - no attributes,false,uvShJ,Marion Shelton,30-11-2000,stark,51,demoOption3,00483030877,wOLlKkxV,yes,MTY,16-05-1990,97356522 -13815759644,en,FSP - all attributes,true,NhcAB,Evan Phelps,30-11-2000,lannister,94,demoOption3,62779116205,XAtCgHul,no,gZp,16-05-1990,80569472 -40279134595,nl,FSP - all attributes,true,WGNdf,Rosetta Anderson,30-11-2000,stark,32,demoOption1,67758433763,fIiOIurp,yes,SmZ,16-05-1990,32986680 -92710608948,en,FSP - no attributes,false,WGraY,Keith Sherman,30-11-2000,stark,52,demoOption3,21249023767,VPbtZitw,no,csa,27-10-2002,27635592 -06268712527,en,Excel,true,MFKDE,Leroy Mills,15-03-1990,greyjoy,16,demoOption1,48455635322,MNmhQaqd,yes,pjG,16-05-1990,04762992 -58331970351,nl,Intersolve-voucher-whatsapp,true,tUOzM,Ina Fisher,15-03-1990,stark,79,demoOption3,81936090148,hxXiRaYW,yes,sxM,27-10-2002,77843706 -98919924121,nl,FSP - all attributes,true,xTjiw,Blanche Hansen,30-11-2000,greyjoy,78,demoOption1,55986978769,gMWLCCGB,no,soc,27-10-2002,85574078 -22470995050,en,Bank A,true,FLKbD,Melvin Taylor,15-03-1990,stark,72,demoOption3,61077446288,vrOxrezd,yes,XjM,16-05-1990,87968423 -24846623004,nl,Intersolve-voucher-whatsapp,true,uTymF,Daniel McLaughlin,15-03-1990,lannister,94,demoOption2,40907539389,RMmqXvBL,yes,Cus,27-10-2002,48683398 -75260513058,nl,Bank A,false,bVjCL,Louise McCormick,15-03-1990,lannister,23,demoOption3,67726298203,jYOgcfZN,yes,DjX,16-05-1990,67912532 -95922098433,nl,Intersolve-voucher-whatsapp,true,ECids,Henrietta Watkins,30-11-2000,lannister,28,demoOption3,84437578037,ZQACPwzr,no,WNs,16-05-1990,62444034 -94161838751,nl,Bank A,false,QycUJ,Tom Quinn,15-03-1990,greyjoy,00,demoOption1,55236939987,MMIyHlgn,no,aOw,16-05-1990,38262549 -14040411888,nl,FSP - no attributes,true,AWgNC,Barbara Kim,30-11-2000,lannister,37,demoOption1,87951879565,PxEHtKKS,yes,RNI,27-10-2002,64163706 -84784857779,en,FSP - all attributes,false,HWRAh,Erik Pope,15-03-1990,greyjoy,99,demoOption3,40885869099,vrieFFWJ,no,ocy,16-05-1990,38423125 -08369395669,en,FSP - all attributes,false,CjyyG,Alex Francis,30-11-2000,lannister,40,demoOption3,81799642202,jbdSFsqZ,yes,llj,16-05-1990,71442246 -87770337735,en,Bank A,false,UXmtz,Lizzie Fisher,30-11-2000,stark,75,demoOption1,29500693655,WFborrJX,yes,aMf,27-10-2002,61208302 -20507032326,nl,Excel,true,dBiDU,Genevieve Bush,15-03-1990,greyjoy,85,demoOption1,92439230152,vGJnmpPj,no,RFz,16-05-1990,74501854 -66824134753,en,Excel,false,niZbt,Walter Stokes,30-11-2000,stark,67,demoOption3,75096176383,siDzqRAM,yes,yRI,16-05-1990,00739947 -80076895942,nl,Excel,false,AQGaf,Marc Wilkerson,30-11-2000,lannister,08,demoOption2,57286162565,uilpWhxR,yes,Xol,27-10-2002,02206117 -10719368410,en,Excel,false,pDFKB,Marian McKinney,15-03-1990,greyjoy,12,demoOption1,35235891573,kSlffPnW,yes,xun,16-05-1990,33266788 -43059013855,nl,Bank A,false,JOPra,Celia Grant,15-03-1990,stark,45,demoOption3,88933562139,PoAwrVXs,yes,INe,27-10-2002,03466172 -84076755373,en,Intersolve-voucher-whatsapp,true,ShIno,Sarah Snyder,15-03-1990,lannister,63,demoOption1,14081039989,hxIitBmk,no,MYE,16-05-1990,02576404 -56055074358,nl,Intersolve-voucher-whatsapp,true,kaezt,Eula Warren,15-03-1990,lannister,88,demoOption2,50473835047,FfNiOwwy,yes,xzy,16-05-1990,58101512 -03394491248,en,FSP - all attributes,false,UbofE,Ethan Newton,30-11-2000,lannister,89,demoOption3,30674168710,nKbVZltG,yes,aLU,16-05-1990,77866025 -89899631337,en,Excel,true,uoSQu,Vincent Weber,15-03-1990,lannister,06,demoOption3,01225241067,JHMdJqRm,no,yCe,16-05-1990,25219608 -31449837172,nl,FSP - no attributes,true,BCKGe,Georgia Daniel,30-11-2000,stark,09,demoOption3,23297712603,elFuKUWE,no,SEL,16-05-1990,99006608 -16734477654,nl,Intersolve-voucher-whatsapp,true,OxcuN,Dustin Rice,30-11-2000,stark,66,demoOption2,25189965901,qfaXhWpx,no,Mqx,16-05-1990,87527507 -93883121376,nl,Intersolve-voucher-whatsapp,true,FNzia,Iva Barrett,15-03-1990,stark,45,demoOption1,57276262535,vuRleiwv,yes,OUE,16-05-1990,89731116 -53449751111,nl,Bank A,false,FTcvb,Gary Hawkins,15-03-1990,lannister,00,demoOption3,29657083477,QcvzhlHa,yes,RsD,27-10-2002,34964537 -90892975683,en,Excel,true,OuENQ,Lida Norton,30-11-2000,greyjoy,69,demoOption3,06401220866,uqeJgrec,yes,YkM,27-10-2002,93740566 -09700261052,nl,Excel,false,YiFPt,Daniel Wells,30-11-2000,stark,49,demoOption3,09538239332,ldmkQGFE,no,IRX,27-10-2002,18124261 -78639436837,en,Excel,true,QQrWF,Rhoda Warren,30-11-2000,lannister,21,demoOption2,70116135884,lyaNsyxY,yes,BHI,27-10-2002,87820791 -04818947983,en,Bank A,false,suooo,Calvin McCormick,15-03-1990,stark,82,demoOption3,67963494349,oQzWqydv,no,OOB,27-10-2002,88364826 -90536631883,nl,FSP - all attributes,true,jfPmV,Betty McKinney,15-03-1990,greyjoy,37,demoOption1,25157578880,tJgKbtcv,yes,Fau,27-10-2002,58888310 -53551643987,nl,Bank A,true,DqgxA,Rosalie Bates,15-03-1990,lannister,97,demoOption2,60093880061,Usmwrure,yes,lWI,16-05-1990,53607412 -66469876378,en,FSP - all attributes,true,MbdaQ,Lily Warner,15-03-1990,lannister,41,demoOption3,00558175699,dWzfrSwK,yes,snP,27-10-2002,56539621 -71283271921,nl,FSP - all attributes,false,OxNcH,Bradley Taylor,15-03-1990,greyjoy,57,demoOption3,79697456658,Cedxyejp,yes,EHp,16-05-1990,13216017 -57090020977,en,Bank A,true,VWrRT,Cora Pena,30-11-2000,greyjoy,17,demoOption2,61321068840,xrmdkfiZ,yes,yEH,16-05-1990,94899200 -42147214435,en,FSP - all attributes,false,oATOH,Darrell Cannon,15-03-1990,greyjoy,79,demoOption2,12852940227,kxqSdpWl,yes,mgq,16-05-1990,34069842 -16345991177,nl,FSP - no attributes,true,SxEcw,Cameron Gordon,30-11-2000,lannister,54,demoOption2,96960958429,pulIRfNX,no,NkP,27-10-2002,24436445 -61140700315,nl,FSP - all attributes,false,EctBQ,Fannie Evans,15-03-1990,greyjoy,41,demoOption2,75064002471,cepLUMoL,yes,Ups,16-05-1990,04431141 -36176553129,en,FSP - all attributes,false,lQtbY,Paul Casey,30-11-2000,greyjoy,04,demoOption3,73054795272,LpsskyYG,no,ePn,27-10-2002,63008538 -01374038604,nl,Intersolve-voucher-whatsapp,false,ZPSTH,Katie Stewart,15-03-1990,lannister,56,demoOption3,56632347011,rZYIrzEj,no,KDt,16-05-1990,50751905 -25125193542,nl,Intersolve-voucher-whatsapp,true,PHuGB,Francisco Tate,30-11-2000,stark,27,demoOption3,39634835357,OuVOKUsL,no,xYr,27-10-2002,41335620 -75267378839,nl,FSP - no attributes,true,LHTuT,Ethel Ward,15-03-1990,stark,94,demoOption1,83911412231,dFRvSAXU,no,hoW,16-05-1990,89942261 -33732303229,nl,Intersolve-voucher-whatsapp,false,LZUAP,Darrell Poole,30-11-2000,greyjoy,99,demoOption3,18817861447,pgvyxryL,yes,WHn,16-05-1990,57525203 -74580786274,en,Excel,false,EoBxS,Adele McGuire,30-11-2000,stark,95,demoOption1,43799087219,IzGPoBog,no,JGj,27-10-2002,91326038 -56326654474,nl,Excel,true,VJwcH,Leah Harvey,30-11-2000,greyjoy,83,demoOption2,04578001350,CRwQNHUa,no,Pwa,27-10-2002,82192762 -16930881437,nl,Intersolve-voucher-whatsapp,false,ADiuH,Eugene Harrington,15-03-1990,stark,20,demoOption1,50837916663,DLwcrKhg,no,EhB,16-05-1990,55271793 -88040669327,nl,Bank A,false,FpyqN,Mabelle Barker,30-11-2000,lannister,44,demoOption2,00694232643,FfXFhLfA,no,HnQ,27-10-2002,91021651 -24847319435,nl,Intersolve-voucher-whatsapp,true,Delej,Philip Potter,15-03-1990,lannister,69,demoOption2,71910659785,FNGMpPSn,yes,wgL,16-05-1990,59183854 -86481783240,en,FSP - no attributes,true,ivcVf,Dominic Reid,15-03-1990,lannister,63,demoOption1,02400742375,uFOjeMxt,no,aom,16-05-1990,50276288 -46115955361,nl,FSP - all attributes,true,FtYFt,Gordon Conner,15-03-1990,greyjoy,67,demoOption2,93906995863,RddbPtvr,yes,SWB,27-10-2002,59042532 -54808443241,en,FSP - all attributes,false,lReqw,Ruth Hudson,15-03-1990,lannister,75,demoOption3,93722743909,nmyRNUUm,yes,Prk,27-10-2002,54995931 -37696589882,en,Intersolve-voucher-whatsapp,true,Vywum,Mathilda Barnett,15-03-1990,stark,59,demoOption2,31667142847,zeyZXTaw,no,WtX,27-10-2002,74947990 -16033513710,nl,Excel,true,eAsgW,Miguel Thomas,30-11-2000,stark,41,demoOption2,14245538342,RnNyuJtO,yes,bEJ,16-05-1990,51878820 -34120270499,en,Intersolve-voucher-whatsapp,true,FWalv,Eva George,30-11-2000,lannister,39,demoOption1,06474591921,jzTSEJdd,no,Tfo,27-10-2002,71606936 -27395383656,nl,FSP - all attributes,true,bYoxU,Darrell Love,30-11-2000,greyjoy,38,demoOption3,70075674790,CfCcJZqQ,no,bRt,16-05-1990,14360537 -65967918470,nl,FSP - all attributes,true,AxqkD,Amelia Montgomery,30-11-2000,lannister,70,demoOption3,40318706486,ZFcrtWSN,no,IzK,16-05-1990,97017248 -79116251487,nl,Intersolve-voucher-whatsapp,true,IuFSk,James Jefferson,15-03-1990,greyjoy,09,demoOption3,15315269538,qSntrwAt,no,ztc,16-05-1990,53949422 -14454955900,nl,FSP - all attributes,true,NuJIf,Chris Griffin,15-03-1990,stark,46,demoOption2,49023393332,aIhwjJnD,yes,uck,27-10-2002,42198671 -51237951352,nl,FSP - all attributes,true,UrrwV,Josie Shaw,30-11-2000,stark,77,demoOption1,10995913485,KItdSezf,no,Mui,16-05-1990,57292033 -97588334079,en,Bank A,true,yOFPC,Leon Maxwell,15-03-1990,greyjoy,18,demoOption1,64784434926,yMjIHqIF,yes,KZj,27-10-2002,74244143 -18701612165,en,Excel,true,KogTP,Olga Parker,15-03-1990,stark,87,demoOption1,69621922743,kxNvEqjh,no,qrC,16-05-1990,63303416 -04543800901,en,Intersolve-voucher-whatsapp,false,Pkkws,Danny Fox,30-11-2000,greyjoy,72,demoOption3,02345304927,aZXjSHxz,yes,tjB,16-05-1990,39865971 -35797424014,nl,FSP - no attributes,false,hiwny,Sadie Moore,30-11-2000,stark,13,demoOption1,61384145956,ojsFWTlH,no,oED,16-05-1990,01742256 -77891777700,en,Excel,false,TLlOC,Della Briggs,30-11-2000,stark,85,demoOption3,74028500643,xQDZWCOl,no,VYo,16-05-1990,44734117 -54232088697,nl,Intersolve-voucher-whatsapp,true,TlMbk,Carolyn Jennings,15-03-1990,greyjoy,72,demoOption3,62502158461,zFqqEVwZ,yes,qnf,16-05-1990,94164714 -46059047761,en,FSP - all attributes,false,JcsOi,Amelia King,15-03-1990,lannister,40,demoOption3,58729395598,mzMdVHKq,no,VWZ,16-05-1990,02233168 -76575489012,en,FSP - no attributes,true,wBcjy,Gerald Hart,15-03-1990,greyjoy,20,demoOption1,28199872969,BzIfnnUo,no,Suk,27-10-2002,19371334 -50013723920,nl,FSP - all attributes,true,suTOg,Ethan Strickland,30-11-2000,greyjoy,27,demoOption1,49708879058,wjYjEaps,no,oDS,16-05-1990,30494271 -31277266145,nl,Intersolve-voucher-whatsapp,true,qASIE,Nora Evans,30-11-2000,lannister,57,demoOption1,36529676773,mxalaGlC,no,sws,27-10-2002,93110196 -38546319060,en,Excel,true,ejrnT,Lewis Goodwin,15-03-1990,greyjoy,66,demoOption3,43090643409,XXsvhVRJ,no,BmC,27-10-2002,45555003 -04988475449,en,FSP - no attributes,true,tHIIs,Pauline Hawkins,30-11-2000,lannister,62,demoOption2,01439545303,OYVqjfCY,yes,KeQ,27-10-2002,56925157 -93729086667,nl,Bank A,true,qoryr,Jayden Spencer,30-11-2000,greyjoy,87,demoOption2,40393680058,unyforlG,yes,RRh,27-10-2002,79427608 -95518145600,en,Bank A,true,SjwUj,Augusta Chapman,30-11-2000,greyjoy,69,demoOption1,86728152487,WMRZBSNn,yes,gMA,16-05-1990,79449656 -07249104423,en,Intersolve-voucher-whatsapp,true,JCkEB,Ann Webb,30-11-2000,lannister,07,demoOption3,93850741281,wcLSfFkY,yes,slW,16-05-1990,52902360 -52920157887,nl,Excel,true,tOlqd,Erik Reyes,15-03-1990,lannister,00,demoOption3,81216845638,xpdTMqUL,no,oyR,27-10-2002,35647891 -51169072720,en,Bank A,true,SEVJf,Birdie Chavez,15-03-1990,stark,93,demoOption1,75616843105,cfBJxvCm,yes,gMH,27-10-2002,69550250 -19253849826,en,Excel,true,CfFHA,Peter Nelson,15-03-1990,lannister,16,demoOption2,93869752446,AchRmlMF,yes,GRO,16-05-1990,93243907 -40402664921,en,FSP - all attributes,true,SqaOg,Mamie Cole,30-11-2000,greyjoy,46,demoOption3,88738249193,kXtncXJN,no,rhh,16-05-1990,71048847 -43646359408,nl,FSP - no attributes,true,nfdsu,Logan Wong,15-03-1990,greyjoy,68,demoOption1,23629815761,BOCrJwXZ,no,YSh,16-05-1990,58144803 -14544255915,en,FSP - all attributes,true,AXyIO,Arthur Sutton,15-03-1990,lannister,43,demoOption2,53938912135,npujGWbc,no,XqG,27-10-2002,76875551 -30060471246,en,FSP - all attributes,false,ysiWv,Ricardo Hodges,15-03-1990,greyjoy,18,demoOption2,34081495890,NcneWIgx,no,WAI,27-10-2002,27165397 -20837300080,en,FSP - all attributes,false,Uxevq,Philip Summers,15-03-1990,lannister,36,demoOption2,73690357445,nKcAhoFA,no,HrL,16-05-1990,99336665 -76509318951,en,Intersolve-voucher-whatsapp,true,IHkJY,Adele Fields,30-11-2000,greyjoy,94,demoOption1,17140051818,TYGGZrle,yes,MaZ,16-05-1990,97319009 -76228197858,en,Excel,false,HUzSN,Elmer Douglas,15-03-1990,greyjoy,55,demoOption1,62468235119,ijTLrTwJ,yes,VKg,27-10-2002,27202239 -04471715534,en,FSP - no attributes,false,qPTci,Clifford Harmon,15-03-1990,greyjoy,27,demoOption2,77320359233,NmZLgiaW,yes,TfA,16-05-1990,03732963 -43731994838,en,Excel,true,ZWOXx,Christina Cruz,30-11-2000,greyjoy,23,demoOption3,29946251858,LLHpbMKK,no,QZu,16-05-1990,68960185 -62743168963,nl,Bank A,true,HmqGj,Lena Adkins,30-11-2000,stark,82,demoOption2,14876280623,nfclRHrX,no,srb,16-05-1990,72942014 -95393685932,nl,Excel,false,JUhOn,Victoria Hudson,30-11-2000,lannister,39,demoOption2,47232295898,gGkqJWHI,no,Hyi,27-10-2002,08619323 -32011365830,en,FSP - no attributes,false,CIYTK,Bryan Powell,15-03-1990,stark,90,demoOption1,19418446642,BMlJtKmP,yes,XjS,27-10-2002,41085132 -41242869932,en,Excel,true,TKGQm,Ernest Flores,15-03-1990,lannister,87,demoOption3,45786260355,paCToaoW,no,JIm,27-10-2002,06125978 -25728413144,en,FSP - no attributes,true,IUurr,Barry McCormick,15-03-1990,stark,79,demoOption2,89593690718,ulZBSxtn,no,CWG,16-05-1990,58629677 -97728906289,en,FSP - all attributes,false,pRxfH,Garrett Mills,15-03-1990,greyjoy,12,demoOption1,15399044305,weBLSgBm,no,OTh,16-05-1990,41811154 -03471679077,en,Intersolve-voucher-whatsapp,true,lJWMZ,Raymond Stokes,30-11-2000,lannister,20,demoOption3,70573796272,fjVYiORy,yes,jDN,27-10-2002,12112762 -22164947675,nl,FSP - all attributes,true,bUMLo,Jerome Andrews,15-03-1990,greyjoy,49,demoOption3,21717959368,veujoCei,yes,Sdj,16-05-1990,60302758 -02202598048,nl,FSP - all attributes,true,LjPSo,Virginia Rodriquez,30-11-2000,greyjoy,32,demoOption3,37796680686,hGoRyPCe,yes,LDr,27-10-2002,44676011 -81873578536,en,Excel,true,eVFcT,Caleb Gill,15-03-1990,greyjoy,05,demoOption1,97717764418,HwiztwET,yes,pnF,16-05-1990,47034200 -82509526253,en,Excel,true,ZphHi,Theresa Garza,30-11-2000,greyjoy,98,demoOption3,34833667659,kBHUFYmL,yes,DzE,16-05-1990,64661422 -73803171864,nl,Intersolve-voucher-whatsapp,false,XpRSp,Benjamin Ray,15-03-1990,stark,25,demoOption2,53166241762,ylamlrdb,no,Xji,27-10-2002,49690598 -63842816750,nl,Bank A,false,qjvrR,Adele King,30-11-2000,greyjoy,99,demoOption3,73018872090,PwqmoVty,yes,hmf,27-10-2002,01329521 -89291175802,nl,FSP - no attributes,false,GPCOf,Randy Nichols,15-03-1990,stark,82,demoOption1,82218627131,kQICBSgR,yes,Iqr,16-05-1990,72163261 -94494028082,en,FSP - no attributes,true,xscBA,Lilly Henry,15-03-1990,stark,28,demoOption3,28630961403,fltCXnqP,yes,PyZ,27-10-2002,99275634 -31544615100,nl,Bank A,false,ANzRg,Irene Olson,15-03-1990,lannister,92,demoOption2,73023967380,omUAwthO,no,pOg,27-10-2002,57962984 -99632000989,en,Bank A,false,vydjO,Mina Rodriquez,15-03-1990,greyjoy,48,demoOption1,35712998148,ToFSEPLv,no,Udc,16-05-1990,66272033 -90934514510,en,FSP - no attributes,false,ZXhJd,Mayme Nash,15-03-1990,stark,61,demoOption1,97445592456,otqrcKzZ,yes,Agf,27-10-2002,70465330 -40915100778,nl,FSP - no attributes,true,XIdtu,May Webster,15-03-1990,greyjoy,26,demoOption2,49733659192,yyIWimBo,no,znA,27-10-2002,12988692 -03527449062,en,Excel,false,hhPaL,Josie Frank,30-11-2000,stark,78,demoOption2,91867529117,AoSpBufp,yes,Cqf,16-05-1990,13878610 -37070230769,nl,FSP - no attributes,false,cPOXL,Mario Sanders,15-03-1990,greyjoy,38,demoOption1,32220542044,EdLHEIHz,yes,FxA,16-05-1990,93982932 -23595031680,nl,FSP - no attributes,true,qySbQ,Susan Benson,30-11-2000,lannister,72,demoOption2,16203240920,FgCNvzEq,yes,Rqo,27-10-2002,10566776 -24182897518,en,Excel,false,edcjP,Bernard Woods,30-11-2000,stark,04,demoOption1,90356625766,hBZfVkXy,yes,nTP,16-05-1990,84922479 -15235136946,nl,FSP - no attributes,true,vTtyF,Leroy Hamilton,15-03-1990,lannister,29,demoOption3,55408256913,KoULIyWO,yes,KYm,27-10-2002,94411376 -68510177541,en,FSP - all attributes,true,nTmHP,Leroy Hansen,15-03-1990,stark,13,demoOption3,39065487315,WzjtRQPu,no,TaP,27-10-2002,71678674 -81882423536,nl,Excel,false,YgRSd,Bruce Holt,15-03-1990,lannister,82,demoOption2,37876062357,fkobeWiv,no,zCW,16-05-1990,79215553 -48267058918,nl,Bank A,true,WjFMn,Edgar Poole,15-03-1990,stark,25,demoOption3,15266148470,udkWIAJo,no,lsd,27-10-2002,99304191 -23426838274,nl,Excel,false,bdTRN,Mittie Ortiz,30-11-2000,lannister,04,demoOption2,54082756368,fdmLkmQL,no,ndL,27-10-2002,43935994 -10617930712,nl,Excel,true,epQuS,Bertha Kim,15-03-1990,stark,20,demoOption2,17868355425,XStUgYpF,yes,tEp,27-10-2002,92770859 -32875799324,nl,Excel,true,NLgkC,Todd Saunders,15-03-1990,lannister,38,demoOption2,02099389669,SyKeEGxL,yes,rJg,27-10-2002,92050286 -02674672392,en,FSP - all attributes,false,KCzpj,Harold Benson,15-03-1990,stark,15,demoOption3,99497354043,dDhpAIjr,no,pmg,16-05-1990,73138993 -43758996805,en,FSP - no attributes,false,rHhlu,Chad Jackson,15-03-1990,lannister,42,demoOption2,12290255100,bPujnOiK,no,HFH,27-10-2002,92754554 -18854032437,en,Intersolve-voucher-whatsapp,false,WAYrg,Louis Rodgers,15-03-1990,stark,05,demoOption2,46179760926,gMzZTxtM,no,nxM,27-10-2002,67839794 -48420807206,nl,FSP - no attributes,true,WhnYj,Lillian Moody,15-03-1990,lannister,27,demoOption2,79867569224,VEaBIveq,yes,yAf,27-10-2002,15935149 -24921732861,nl,Excel,false,oklIg,Polly Knight,30-11-2000,stark,53,demoOption1,63760051263,yGWszsOo,yes,Pyq,27-10-2002,56397684 -46123765692,nl,FSP - all attributes,true,JZfZX,Noah Palmer,30-11-2000,stark,20,demoOption1,65664048861,XRFPkpzt,yes,JJm,27-10-2002,20914659 -38226329276,nl,Intersolve-voucher-whatsapp,true,BwBXU,Nathan Burns,30-11-2000,lannister,19,demoOption2,69377764160,pYsywuiC,yes,Fyg,16-05-1990,05945991 -16386973039,en,Bank A,false,uZKfj,Marcus Roy,15-03-1990,lannister,48,demoOption1,97767142077,tCgltmGq,no,OYv,16-05-1990,05951731 -23014805875,nl,FSP - no attributes,true,rVgWy,John Davidson,30-11-2000,lannister,61,demoOption3,42795936570,qzijngcI,yes,Wlr,27-10-2002,88656219 -57540796450,nl,FSP - all attributes,true,OaFQz,Micheal Collier,30-11-2000,lannister,65,demoOption3,26502728652,NiSDmWse,yes,TcT,27-10-2002,78152047 -80534016051,nl,FSP - no attributes,false,vDxFy,Jose Sims,15-03-1990,lannister,90,demoOption1,42415048575,DBpVIPGn,no,JTB,16-05-1990,70827660 -24846084594,nl,Intersolve-voucher-whatsapp,true,ojrSg,Edgar Reyes,15-03-1990,lannister,61,demoOption1,08851558812,RCrRaGti,yes,ZDZ,27-10-2002,95986226 -90518570923,nl,Excel,false,ptGnC,Kyle Burgess,30-11-2000,stark,75,demoOption2,99923100690,HxMXlptN,yes,NFL,16-05-1990,08347391 -09444604528,nl,Excel,false,yhWFo,Isabella Turner,15-03-1990,lannister,05,demoOption2,84115351430,oIKasAJQ,no,Lmu,16-05-1990,74776048 -21415603967,en,Excel,true,LrSQM,Lawrence Wade,30-11-2000,lannister,86,demoOption3,08062496759,lqfpAkKz,yes,eHi,16-05-1990,13044958 -39821134104,en,Intersolve-voucher-whatsapp,false,KPcte,Ethel Ballard,15-03-1990,lannister,35,demoOption3,21862101214,ENEIzukH,no,Rix,27-10-2002,58452646 -73446343410,en,FSP - all attributes,true,mRTre,Ora Hayes,15-03-1990,lannister,33,demoOption1,20104289503,MCMJfasb,no,uKS,27-10-2002,91341437 -48340091352,nl,Excel,true,pOppB,Fanny Long,15-03-1990,lannister,60,demoOption2,24289496055,DODcnONn,yes,ZhM,27-10-2002,44168658 -86243871502,nl,Excel,true,vAaBY,Cora Hanson,15-03-1990,greyjoy,19,demoOption2,25937342341,OJNUyQyD,yes,zzk,27-10-2002,02021250 -96978027748,nl,Bank A,false,WmJER,Ray Dixon,15-03-1990,lannister,59,demoOption3,59502100673,hzcKvXEt,yes,dhh,16-05-1990,46778896 -40490936558,nl,Intersolve-voucher-whatsapp,false,SxtpU,Jessie Gonzales,15-03-1990,greyjoy,51,demoOption1,82887663425,bDRHtBfM,yes,PRu,27-10-2002,13613324 -32673807851,en,FSP - all attributes,true,BrARq,Tom Reed,30-11-2000,stark,19,demoOption1,76169258779,YgGXguIA,yes,gha,27-10-2002,64335200 -91118821175,en,Intersolve-voucher-whatsapp,true,qmPKy,Willie Lambert,15-03-1990,lannister,47,demoOption3,83957723592,cOMdCohQ,yes,peX,27-10-2002,73667866 -02323286499,nl,Bank A,false,rMSAB,Celia Gutierrez,30-11-2000,lannister,61,demoOption2,32002915285,JuVSzRkb,yes,pqp,27-10-2002,12033355 -67380251806,en,FSP - no attributes,true,BModM,Arthur Gregory,15-03-1990,greyjoy,02,demoOption3,12995206680,FOTBmGNv,yes,kaR,16-05-1990,76999301 -38120022194,nl,Excel,true,tTcwi,Inez Warren,30-11-2000,greyjoy,13,demoOption2,12699126659,IehGsuJK,no,lyu,27-10-2002,59844990 -00171655124,nl,Bank A,false,qlDsa,Eunice Atkins,15-03-1990,stark,59,demoOption2,61673103561,PRHMwJGd,yes,yED,27-10-2002,33808090 -15365274961,nl,FSP - all attributes,true,hXmZF,Hunter McDonald,30-11-2000,lannister,99,demoOption1,07137247273,CVuFRFxJ,no,JjI,27-10-2002,06147090 -93938170055,en,Intersolve-voucher-whatsapp,true,Hmmvj,Roger McKinney,15-03-1990,lannister,44,demoOption1,80327396793,WLUlbuUd,no,tht,16-05-1990,86663086 -64599517408,en,FSP - no attributes,false,fTSqs,Glenn Wong,30-11-2000,greyjoy,79,demoOption3,93869289120,GpipovSJ,no,FbN,27-10-2002,69247167 -66075054111,nl,FSP - no attributes,false,NabAG,Myrtie Norman,15-03-1990,stark,17,demoOption2,73563848141,SRXLqzuI,no,tYo,16-05-1990,16598464 -88421572658,nl,Intersolve-voucher-whatsapp,true,MKrll,Bessie Morton,30-11-2000,stark,74,demoOption2,36421972118,EEBuZljf,yes,VbU,27-10-2002,75553284 -89707529269,en,FSP - all attributes,true,XOOan,Leroy Lynch,30-11-2000,stark,51,demoOption2,55568861392,zuBzBRyl,no,ENG,16-05-1990,43529754 -87203229897,en,Excel,true,AXHuX,Olivia Ford,30-11-2000,lannister,45,demoOption1,65642333664,dyzmQAhd,no,zkw,16-05-1990,95084435 -48651329854,en,FSP - no attributes,false,dUIaX,Ray McGee,15-03-1990,lannister,52,demoOption1,80166758251,jFzTWrdo,no,AQE,16-05-1990,19631495 -15310293014,en,FSP - all attributes,true,HhqxX,Thomas Sanders,30-11-2000,stark,75,demoOption2,77607938188,NCDzNJVV,no,XGk,16-05-1990,67854079 -25904288672,nl,FSP - all attributes,false,HSwwT,Christopher Peterson,15-03-1990,lannister,57,demoOption1,75383296747,MUgfDedx,no,jxs,16-05-1990,83641071 -98035498468,en,Intersolve-voucher-whatsapp,false,XnTVG,Lois Brock,15-03-1990,stark,90,demoOption2,56489038440,WpmHmtJV,no,WlO,27-10-2002,94177521 -11295633819,nl,Excel,true,WtpAa,Russell Ortega,30-11-2000,greyjoy,79,demoOption1,89749653162,lSGjoANC,no,HOs,27-10-2002,09650262 -02662816064,en,Bank A,true,YVxlz,Nathaniel Gibbs,15-03-1990,lannister,04,demoOption1,13179028843,CJCSzTVK,yes,qJz,16-05-1990,41494475 -11816857352,nl,FSP - all attributes,false,gsHwV,Genevieve Murphy,15-03-1990,greyjoy,70,demoOption3,98519943036,KxrAuyyC,yes,vHN,16-05-1990,23750109 -68824150501,nl,Bank A,false,wRoaY,Jorge Davis,30-11-2000,greyjoy,65,demoOption2,07547757778,QcEIYujs,no,yzZ,27-10-2002,80148531 -35743018119,nl,Intersolve-voucher-whatsapp,false,mpVkK,Timothy Wilkerson,15-03-1990,stark,04,demoOption2,77890508026,ATicSDEi,no,Tae,27-10-2002,10261768 -05581288298,nl,Excel,true,zmOIe,Antonio Parsons,15-03-1990,greyjoy,78,demoOption2,56113579456,zRFAnARF,no,DzS,16-05-1990,26360496 -06324668552,nl,Intersolve-voucher-whatsapp,true,mQWpD,Celia Morton,30-11-2000,stark,45,demoOption1,13709188465,LfRQybYU,no,eYX,27-10-2002,21772409 -02308391764,nl,Bank A,false,yJTEi,Harriet Atkins,30-11-2000,stark,92,demoOption3,78128189882,owWPnoHy,yes,OnB,27-10-2002,65451829 -82367032886,en,FSP - all attributes,false,zOJWY,Robert Hopkins,15-03-1990,greyjoy,14,demoOption2,18917845921,DDalirae,yes,nmb,27-10-2002,37614374 -33397774295,nl,Intersolve-voucher-whatsapp,true,KCbpK,Randy Newman,15-03-1990,greyjoy,98,demoOption2,31174810641,NHwduSjz,yes,ckG,27-10-2002,14107419 -53312541522,en,Excel,false,JQvZZ,Fanny Hunter,30-11-2000,stark,10,demoOption2,04492810812,bKtMeEOX,yes,Zqd,27-10-2002,14882593 -68361760854,en,Bank A,false,qWZXi,Marian Boone,30-11-2000,greyjoy,41,demoOption3,03597256713,yJgZTQEc,no,teX,16-05-1990,69620146 -84778947281,en,FSP - all attributes,true,TiAyG,Myra Cunningham,30-11-2000,stark,09,demoOption1,76010509636,HdbmLCvy,no,EGi,27-10-2002,13886048 -22245599782,en,FSP - all attributes,true,iGSAU,Seth George,30-11-2000,greyjoy,18,demoOption2,20667731296,hbGTbTCL,no,fRo,27-10-2002,37170688 -15838469200,en,FSP - no attributes,false,wqwKA,Travis Morgan,30-11-2000,lannister,47,demoOption1,93701453431,lwBxGfrr,yes,eiM,16-05-1990,71693590 -72007497273,nl,Intersolve-voucher-whatsapp,false,omUAc,Bruce Rice,30-11-2000,greyjoy,99,demoOption3,69894727921,ReyMhrBv,yes,MlF,16-05-1990,78875127 -63104375520,en,Intersolve-voucher-whatsapp,true,JfSQH,Isaac Simon,30-11-2000,lannister,48,demoOption1,38716549759,uEPLDNWz,no,ncK,16-05-1990,17146573 -69333763592,nl,Excel,true,UiBCS,Margaret Peterson,15-03-1990,lannister,18,demoOption1,32889710343,IZGDOMzY,no,OAy,27-10-2002,77910242 -02909144354,en,Bank A,false,upvVW,Vera Wheeler,30-11-2000,lannister,04,demoOption2,36838726720,iDQDPeIz,no,toy,16-05-1990,00572125 -63071607657,en,FSP - no attributes,false,Cehjp,Ruby Sharp,15-03-1990,lannister,97,demoOption1,52677706162,NfooUAAJ,yes,Zxb,27-10-2002,84657622 -69430968658,en,FSP - all attributes,true,BSFWS,Dora James,15-03-1990,greyjoy,19,demoOption3,98242372321,duVaBVqG,yes,LkS,27-10-2002,60130668 -39730059082,nl,Intersolve-voucher-whatsapp,true,aLgLo,Olive Wise,30-11-2000,lannister,18,demoOption1,28449249307,UjTDMQem,no,mBs,16-05-1990,22344359 -65341611805,en,FSP - no attributes,false,GIxSe,Harold Fields,15-03-1990,lannister,35,demoOption2,71539347106,fMMZBqXt,yes,tPp,27-10-2002,01117999 -96808280585,nl,FSP - all attributes,true,UExsf,Herbert Cooper,30-11-2000,lannister,09,demoOption1,91115543489,uUnilzaw,yes,eBx,27-10-2002,26973096 -06771335247,en,FSP - all attributes,true,tqJmI,Manuel Lindsey,15-03-1990,lannister,10,demoOption1,15932959363,hBFHcQML,no,fto,27-10-2002,37619397 -41131386189,en,Intersolve-voucher-whatsapp,true,HntpA,Jim Hall,15-03-1990,greyjoy,83,demoOption1,61129297572,ChoUZMHI,yes,OoR,27-10-2002,53657676 -39210814871,en,FSP - no attributes,false,BEeTT,Bertha Silva,30-11-2000,stark,53,demoOption3,56903301100,cQiTvcNv,no,XSD,27-10-2002,63821481 -29905044671,nl,FSP - no attributes,false,NpXpH,Alfred Griffin,30-11-2000,stark,52,demoOption1,49646917892,fphSOYfL,no,rye,27-10-2002,64526777 -90094120314,nl,Excel,true,QnbOI,Nicholas Watson,15-03-1990,lannister,01,demoOption1,29229521182,WgPrnvDY,yes,yHY,16-05-1990,71370728 -95683351608,nl,FSP - no attributes,true,uEbbe,Ola McGee,15-03-1990,stark,30,demoOption3,92250234021,vRAhBIAy,yes,FcM,27-10-2002,49307276 -48706139437,nl,Intersolve-voucher-whatsapp,true,SLoDt,Bettie Boone,15-03-1990,stark,84,demoOption1,85511008393,YVLLYmaz,yes,siP,16-05-1990,45270964 -63357832504,nl,FSP - no attributes,true,yCLTe,Paul Reeves,15-03-1990,stark,01,demoOption3,43392764314,NJzaaiWd,yes,RLt,16-05-1990,38805017 -66932400995,nl,FSP - all attributes,false,NCQNR,Elmer Santos,30-11-2000,stark,78,demoOption2,53847408128,SCWmQeaA,no,twY,16-05-1990,35882064 -21365359571,en,Bank A,false,jvcYK,Elijah Garza,30-11-2000,greyjoy,70,demoOption3,48913839711,jPtokUwW,yes,jWX,27-10-2002,41229923 -92516029803,en,Bank A,false,jZuVr,Russell Cooper,15-03-1990,greyjoy,09,demoOption3,60072884714,IROUsVOQ,yes,YQT,16-05-1990,87609240 -20495736476,en,FSP - all attributes,false,oCBEw,Kenneth Harrison,15-03-1990,lannister,35,demoOption3,67435827179,zQRltZMd,no,TpD,27-10-2002,52483674 -10383864207,nl,FSP - no attributes,false,uVmYN,Mina Edwards,15-03-1990,greyjoy,21,demoOption3,44463161124,gxXdyWKu,yes,epx,27-10-2002,67706568 -37362110057,en,Excel,false,DLOaM,Randy Brown,15-03-1990,greyjoy,47,demoOption2,85009206288,gADtFnQD,yes,gKp,16-05-1990,49948293 -51519838360,en,Excel,false,VPcHK,Isabella Hall,30-11-2000,stark,45,demoOption3,98940249455,YtNTsqZj,no,PfP,27-10-2002,30793202 -17953998136,nl,Intersolve-voucher-whatsapp,true,zynoe,May Riley,30-11-2000,greyjoy,34,demoOption2,02924848588,IMmboeLo,no,rGv,27-10-2002,09175567 -13927079912,en,FSP - all attributes,true,rpZMS,Delia Obrien,15-03-1990,lannister,09,demoOption3,35595362985,vPNpuysr,yes,Pwc,27-10-2002,76073106 -08596331573,en,FSP - no attributes,true,UJqmc,Harry Hale,15-03-1990,lannister,18,demoOption1,26472185179,vzgwfuGn,yes,Juk,27-10-2002,46291704 -87911795587,en,Excel,false,pwhRS,Eliza Walton,15-03-1990,lannister,50,demoOption3,09291102324,EPPuipQn,yes,GbV,16-05-1990,10158850 -55324110727,en,Intersolve-voucher-whatsapp,true,iBwFd,Cody Black,15-03-1990,stark,49,demoOption3,35366699227,VSOIjOVY,no,HZq,16-05-1990,39017407 -94963897998,en,FSP - no attributes,false,QlokI,Bernice Fowler,30-11-2000,lannister,19,demoOption1,52124984156,loAJSBCY,no,cBW,16-05-1990,28334613 -35262760317,nl,Bank A,true,WIihY,Dollie Gomez,15-03-1990,greyjoy,01,demoOption3,81878332907,ykakJfQz,yes,GJS,27-10-2002,71701382 -36749619690,nl,FSP - no attributes,true,lVslR,Katherine Stone,30-11-2000,lannister,31,demoOption3,15638960413,FUFyrbvC,no,kKV,16-05-1990,41942326 -66040687409,nl,Intersolve-voucher-whatsapp,false,kRKfz,Edgar Phelps,15-03-1990,lannister,65,demoOption2,69111798441,CrIwQWVu,no,cmI,16-05-1990,59048516 -30929421938,en,Excel,false,ZCKyY,Samuel Price,30-11-2000,stark,17,demoOption2,10393027841,EsaxCMOL,no,pNi,27-10-2002,45197194 -08474540889,en,FSP - all attributes,true,iqcDC,Jerome Erickson,15-03-1990,greyjoy,70,demoOption2,70891992696,cpuOkZNs,no,xxI,27-10-2002,80107798 -78999500410,en,Bank A,true,HOQxY,Henry Daniel,15-03-1990,lannister,40,demoOption3,49583548288,DfHVxdYG,no,gjt,16-05-1990,98881219 -76038143346,en,Intersolve-voucher-whatsapp,false,enBHV,Cornelia Howell,30-11-2000,greyjoy,59,demoOption3,95594854126,ThYjyrRl,no,UkU,27-10-2002,66639487 -90488289980,en,FSP - all attributes,true,EjuUJ,Dennis Blake,30-11-2000,stark,31,demoOption2,31241491895,SeRzjAXT,yes,HuH,27-10-2002,74254368 -00502261743,nl,Excel,false,ioQhU,Virginia Townsend,15-03-1990,lannister,24,demoOption3,06299988094,jHExxJGP,yes,vkr,16-05-1990,17412741 -16771104415,nl,FSP - no attributes,true,ptxeY,Betty Buchanan,15-03-1990,lannister,44,demoOption3,03293720921,TPZqyYDj,no,eXJ,27-10-2002,69651132 -24874033121,en,FSP - no attributes,true,fKUWJ,Cecilia Barker,15-03-1990,lannister,44,demoOption1,09805619820,iZRQPTTe,yes,KCc,16-05-1990,72918757 -00908469375,nl,Bank A,true,woYyW,Nell Beck,30-11-2000,greyjoy,51,demoOption2,53146933740,CqeMTRfP,no,pAN,27-10-2002,91338211 -70929856399,nl,FSP - all attributes,false,NrlYa,Johnny Blair,15-03-1990,lannister,01,demoOption2,62789036390,smaafwYa,no,pMU,16-05-1990,74709249 -22041645579,nl,Intersolve-voucher-whatsapp,true,Qiawr,Larry Swanson,30-11-2000,lannister,81,demoOption2,83088089362,YKfGijfu,yes,PJS,27-10-2002,89519564 -32839680589,en,Bank A,false,qpezV,Amy Rodgers,15-03-1990,lannister,66,demoOption2,81616352210,THPFzwZW,yes,LzG,27-10-2002,58336900 -70425646686,en,Intersolve-voucher-whatsapp,true,Toacz,Flora Allison,30-11-2000,lannister,76,demoOption3,29128892806,XbxsSSlz,no,PQA,27-10-2002,22407281 -83229204889,nl,Intersolve-voucher-whatsapp,false,GCvlN,Harry Harris,15-03-1990,stark,53,demoOption3,14630067797,JXHLxVJD,no,onO,16-05-1990,85533156 -86581951579,en,Intersolve-voucher-whatsapp,false,wfVBB,Betty Delgado,30-11-2000,lannister,61,demoOption3,59779333744,GWmgHVbk,no,Bdc,27-10-2002,57010719 -45124789137,nl,Excel,true,ISJPO,Hattie Wade,15-03-1990,greyjoy,34,demoOption2,66268893728,vwUjwNmY,yes,Qwa,27-10-2002,81803896 -86897411112,en,Bank A,false,njGiC,Isaiah Roberson,15-03-1990,greyjoy,45,demoOption2,02838047702,CxnhTUZi,yes,iJh,16-05-1990,89087289 -56946336642,en,Excel,true,wynbw,Lou Valdez,30-11-2000,lannister,83,demoOption3,87512915182,OjRheoDl,no,zHf,27-10-2002,45775266 -04265633327,nl,FSP - no attributes,true,GVvLn,Ora George,30-11-2000,greyjoy,96,demoOption2,64774439399,aBRpXlAT,yes,BeY,27-10-2002,46593969 -91402076351,nl,Bank A,false,aFHTy,Travis Jackson,15-03-1990,stark,03,demoOption2,08029426814,iYEyAcTx,yes,ZWY,27-10-2002,41063163 -02553982583,en,Intersolve-voucher-whatsapp,true,ZBJOQ,Sally Carr,30-11-2000,stark,63,demoOption1,39245026607,TSwbjOpE,no,UqS,27-10-2002,45440124 -69145092797,nl,Bank A,false,DhzWI,Dean Silva,15-03-1990,lannister,45,demoOption3,03764662876,rNkTTEMN,yes,iIs,16-05-1990,18413461 -36245053413,nl,Intersolve-voucher-whatsapp,false,arQCJ,Bobby Kelly,15-03-1990,stark,11,demoOption1,05269921421,XjCpwRKS,yes,sOF,27-10-2002,32982300 -30264950550,en,Excel,false,bAxdm,William Blair,30-11-2000,greyjoy,10,demoOption1,46130838647,JRNGAyPg,yes,mlh,27-10-2002,76533412 -42780837712,nl,Excel,false,yWFsq,Carrie Fleming,15-03-1990,stark,79,demoOption2,74900850311,KLZHMCsj,yes,RFC,16-05-1990,94980102 -40794409662,nl,Intersolve-voucher-whatsapp,true,ujTnZ,Genevieve Morgan,30-11-2000,stark,80,demoOption1,04297646337,DpIAURwi,no,MZm,16-05-1990,59221868 -24832215838,en,Bank A,true,yEves,Etta Vargas,30-11-2000,greyjoy,51,demoOption2,34209914092,kDqvYRoU,no,klL,27-10-2002,75324810 -33014140812,en,FSP - no attributes,false,rMRGG,Nora Hamilton,15-03-1990,greyjoy,92,demoOption1,26349975917,FeuiFHoR,yes,iNJ,16-05-1990,44158426 -88399748846,nl,FSP - all attributes,false,FRqUd,Carlos Peters,30-11-2000,stark,82,demoOption1,27417733711,gdioFVNb,no,CYt,27-10-2002,91888716 -47352879895,nl,FSP - no attributes,true,LzFKa,Aaron Daniel,15-03-1990,greyjoy,72,demoOption1,38739248377,bWXNRoOS,no,nbF,16-05-1990,36543209 -85011725245,en,Bank A,false,YWYlD,Mattie McBride,15-03-1990,lannister,49,demoOption3,01882601154,rvSdYfXy,no,QlI,27-10-2002,50566209 -31240401922,en,FSP - all attributes,true,WEcYX,Roger Berry,30-11-2000,stark,77,demoOption3,11227664080,NYfweXoE,yes,DCp,16-05-1990,14524202 -18756743369,en,Bank A,false,FEpfd,Eunice Phelps,15-03-1990,stark,62,demoOption3,60624222952,FDcVkFby,yes,sQX,27-10-2002,39592954 -35614274239,nl,Intersolve-voucher-whatsapp,true,EcZyp,Jeanette Peters,30-11-2000,greyjoy,39,demoOption1,61834774569,pQdoZqYA,no,cjM,27-10-2002,65123888 -00040378851,nl,Bank A,false,imsxS,Ray Roberson,30-11-2000,greyjoy,51,demoOption3,43768442196,tDwQKDPL,yes,urm,16-05-1990,82642029 -50636480334,en,Excel,false,MMEQL,Jim Collins,30-11-2000,greyjoy,41,demoOption3,52359796292,aRJCCDSo,yes,Ayf,16-05-1990,67896456 -17897620557,en,Bank A,true,xDexg,Rhoda Bradley,15-03-1990,lannister,15,demoOption1,27580564196,PIQDUwsU,yes,BMI,16-05-1990,84530716 -82035281970,nl,Excel,true,YmhPr,Lelia Potter,30-11-2000,stark,85,demoOption2,68034469565,jJqFAPmg,yes,tFa,16-05-1990,57611219 -13672350358,nl,Bank A,true,VjyDs,Danny Santos,15-03-1990,stark,16,demoOption2,29472775548,ngqzXCnt,no,fdq,27-10-2002,26015755 -76281258182,nl,Bank A,true,Tlmue,Erik Sharp,15-03-1990,lannister,83,demoOption1,39266151505,sSKGZDYq,no,HtW,16-05-1990,21943852 -95555622866,en,Excel,true,VdKqV,Charlotte Gardner,15-03-1990,lannister,88,demoOption1,89710764643,HdLYUWQu,yes,zes,16-05-1990,97446202 -73786093180,nl,FSP - no attributes,true,uAJmO,Lenora Cox,30-11-2000,lannister,06,demoOption3,62605731518,UKwQrAbg,yes,sVc,16-05-1990,99694904 -26291298727,nl,Bank A,false,JBFyp,Michael Lindsey,15-03-1990,stark,71,demoOption1,28671693919,hcfnmaQY,no,DWl,16-05-1990,39125910 -16496564931,en,FSP - all attributes,true,arcFT,Alejandro Morales,30-11-2000,lannister,72,demoOption3,73929869498,ykSklmDj,yes,vXX,16-05-1990,55045078 -17005668194,en,Bank A,true,gPlPT,Helen Burton,30-11-2000,lannister,09,demoOption3,98763598914,EUPRnVbN,no,teH,27-10-2002,90778609 -61113257892,nl,FSP - no attributes,false,BBLxH,Janie Haynes,30-11-2000,stark,69,demoOption1,99136756077,SrzFgDUG,no,aEn,27-10-2002,52758012 -86177673485,en,Intersolve-voucher-whatsapp,false,sofOV,Addie Wagner,15-03-1990,lannister,78,demoOption1,14138226082,eAIxLEFV,yes,bDB,16-05-1990,57567256 -44079768641,nl,Bank A,true,hdOIo,William Park,30-11-2000,stark,70,demoOption3,50168832485,ouiwcTMe,no,Arz,27-10-2002,44149899 -19311879944,nl,FSP - no attributes,false,tIoEB,Helen Davis,15-03-1990,lannister,41,demoOption1,56043222454,mKOnizMt,no,Mbk,27-10-2002,22249055 -06574574522,nl,FSP - all attributes,true,YQzpq,Lydia Mason,30-11-2000,stark,50,demoOption3,62067630439,jjBOQxLJ,yes,vde,27-10-2002,92837280 -90753941145,en,FSP - no attributes,false,BdeDB,Hallie Reyes,30-11-2000,greyjoy,92,demoOption1,55095082706,nfkZhDwQ,no,IAJ,27-10-2002,16497325 -29779561496,nl,FSP - no attributes,true,GaWkA,Katie Carter,30-11-2000,stark,70,demoOption1,56675146724,QLdPGZXa,yes,SRC,27-10-2002,43895532 -90272761258,nl,Intersolve-voucher-whatsapp,false,SEFxb,Inez Hall,15-03-1990,stark,13,demoOption1,76695340261,kGlHrbrz,yes,cgK,16-05-1990,08155531 -65436462495,en,Bank A,false,USgew,Dennis Davidson,30-11-2000,lannister,14,demoOption3,18891081554,SFSQduYW,no,Wwk,27-10-2002,95655029 -04274251394,en,Bank A,true,zPIxc,Zachary Romero,15-03-1990,stark,88,demoOption2,62561768832,DAdsPQDc,yes,btn,27-10-2002,70611871 -42132706268,en,Excel,false,SSqqv,Timothy Bryant,30-11-2000,stark,59,demoOption2,92551268395,lUikwbVO,yes,jdi,16-05-1990,05151301 -79666757528,en,Bank A,false,GCiRv,Jesse Patrick,30-11-2000,stark,76,demoOption2,83603052634,QXlLtgjs,yes,Iou,16-05-1990,04016921 -20236770527,en,Intersolve-voucher-whatsapp,false,rijPR,Inez King,15-03-1990,greyjoy,61,demoOption2,65001097481,vLKydnEC,no,hwV,27-10-2002,25254523 -87886888012,nl,Bank A,false,OWYZI,Douglas Ryan,15-03-1990,stark,67,demoOption1,81934911130,ZDswkJIe,no,uXX,16-05-1990,96958092 -49857364921,nl,Intersolve-voucher-whatsapp,true,iYRpY,Elizabeth Malone,15-03-1990,lannister,18,demoOption1,34091781002,ngMvnTMx,no,qWo,16-05-1990,82234945 -44423831630,en,FSP - no attributes,false,TNugZ,Nicholas Wright,15-03-1990,lannister,28,demoOption3,38534797871,NYbnYgzm,no,blw,27-10-2002,64332954 -72269757928,nl,Bank A,false,hQJkg,Bessie Hanson,30-11-2000,greyjoy,32,demoOption3,14312380874,LAzrvNHm,yes,kOi,27-10-2002,13487176 -18031534180,nl,Bank A,false,vEXeI,Justin Gregory,15-03-1990,stark,77,demoOption1,96702517376,mMVcnqmH,no,VkV,16-05-1990,42597916 -43412833495,en,Excel,true,eYVKX,Rhoda Bryan,15-03-1990,greyjoy,61,demoOption1,56509042002,MtXpdprp,no,FWR,27-10-2002,42888412 -46309508536,nl,Excel,true,CJgRe,Ricky Coleman,30-11-2000,stark,51,demoOption2,61905065682,tBIGbIww,yes,tYq,27-10-2002,31243837 -29374832579,nl,Excel,true,TMfgR,Caroline Goodman,15-03-1990,greyjoy,16,demoOption1,14064370909,XbZiuoWw,no,bBI,27-10-2002,84017336 -88009877626,en,Intersolve-voucher-whatsapp,true,wYYPC,Philip Bowen,30-11-2000,greyjoy,41,demoOption1,95377482122,cwworCSd,yes,Sfq,16-05-1990,99711938 -06175772162,nl,FSP - no attributes,true,lRmHe,Vincent Collier,15-03-1990,lannister,14,demoOption2,56149191183,pwUWAiKi,yes,HCD,27-10-2002,27750156 -83461248070,en,Bank A,true,AHJTx,Elmer Lloyd,15-03-1990,stark,49,demoOption3,42335959759,qFzuFsQq,no,bYa,16-05-1990,81365269 -08485285042,en,Excel,true,bMdkW,Francis Tucker,30-11-2000,stark,06,demoOption1,62269898391,loEbrquT,no,ZlO,16-05-1990,71856621 -20712464388,en,FSP - all attributes,true,UbvWI,Milton Ruiz,15-03-1990,stark,94,demoOption2,05568762719,HDkIKtIf,no,Xad,16-05-1990,43979610 -00212050739,en,Excel,false,HHrIK,Bryan Mills,15-03-1990,greyjoy,70,demoOption3,65294514107,nQBZSspl,no,ssU,27-10-2002,81253002 -39484474644,en,Bank A,true,sQvIP,Christopher Griffin,30-11-2000,stark,31,demoOption2,72829154973,xQzzrzqi,no,exo,16-05-1990,96537659 -61871137925,en,FSP - all attributes,false,hnWVL,Betty Day,30-11-2000,stark,34,demoOption1,08307763997,uVIRtPCg,yes,Lto,27-10-2002,95966563 -33910141924,en,Bank A,false,fmnHT,Charlotte McCarthy,15-03-1990,stark,57,demoOption2,63053976553,EOVdHHMi,yes,cnw,16-05-1990,45067191 -55068551465,nl,Bank A,false,GZNXl,Mathilda Stewart,30-11-2000,greyjoy,83,demoOption2,95726020805,KreeJBnb,yes,Kfj,27-10-2002,78246692 -44269586280,nl,Intersolve-voucher-whatsapp,true,JsvPh,Rosalie Lynch,15-03-1990,lannister,07,demoOption1,71067184460,bnLtosJw,no,ooC,16-05-1990,61676437 -67797718915,nl,Bank A,false,Twdnz,Hester Robbins,15-03-1990,greyjoy,21,demoOption2,74934063648,rjdDENNx,yes,INh,27-10-2002,64672590 -84888380441,nl,Bank A,false,fmkuv,Lilly McKenzie,15-03-1990,stark,72,demoOption2,55287932132,CMLtBDEj,no,OFM,27-10-2002,85594612 -66301178168,en,Bank A,false,lTUpK,Daisy Figueroa,15-03-1990,greyjoy,93,demoOption2,85877032141,lFGRYQQv,no,otF,27-10-2002,24586653 -12143631641,nl,Bank A,true,qmSta,Russell Schultz,30-11-2000,greyjoy,98,demoOption1,01212422116,gPNXSKUK,yes,AFA,27-10-2002,26342831 -05140170897,nl,Excel,false,RVJjk,Agnes Barnett,15-03-1990,lannister,09,demoOption2,70531448422,dMuzvFOM,no,xdc,27-10-2002,82215762 -99911010606,en,FSP - no attributes,false,lmLTp,Ethan Hoffman,15-03-1990,lannister,18,demoOption2,05925143729,JMVMxSKQ,yes,aTi,16-05-1990,29878798 -44704824766,en,FSP - all attributes,true,KbeFz,Ollie Hunter,30-11-2000,lannister,87,demoOption3,50214614714,GfXPjpDm,yes,cud,16-05-1990,10496266 -65180839555,nl,FSP - all attributes,false,akywI,Ophelia Dixon,30-11-2000,greyjoy,46,demoOption1,15459938791,opAkUJAx,no,SGC,27-10-2002,75879035 -30706405573,nl,Bank A,false,WsMhd,Garrett Hall,30-11-2000,stark,51,demoOption3,07009805421,sXaJyiOL,yes,ZKP,27-10-2002,16128433 -11209153958,en,Bank A,true,nghzG,Theodore Aguilar,30-11-2000,stark,48,demoOption2,25516558580,deWKHStr,yes,Icd,16-05-1990,45656851 -90149929796,en,Intersolve-voucher-whatsapp,true,SkhDr,Lillie Kelley,15-03-1990,lannister,89,demoOption2,20152290460,nBjeVKiQ,no,ZkC,27-10-2002,72330557 -67703856618,en,Bank A,false,DQYwr,Mattie Clayton,30-11-2000,lannister,07,demoOption1,64921297609,hyDIYsug,yes,mWk,16-05-1990,72644177 -21248681351,en,Bank A,false,Kydch,Gussie Schmidt,15-03-1990,lannister,33,demoOption2,72901989673,QXFOZXrS,no,YSR,27-10-2002,36679211 -33722839723,nl,Bank A,true,qiisi,Mayme Austin,30-11-2000,greyjoy,95,demoOption2,86727519243,MWjMvNbX,yes,IRx,27-10-2002,03244859 -89286935497,en,Intersolve-voucher-whatsapp,true,YyfAb,Loretta Park,15-03-1990,greyjoy,00,demoOption3,77720943327,jGDNlvZx,no,CVi,27-10-2002,25871055 -14150154952,nl,FSP - no attributes,false,gTWEz,Lucas Leonard,30-11-2000,stark,74,demoOption1,61357894384,kceGkorj,yes,Kpc,16-05-1990,26978235 -91407915043,nl,Intersolve-voucher-whatsapp,false,okQHD,Theodore Steele,30-11-2000,lannister,37,demoOption3,75326695301,WpXUDZiV,no,cRC,16-05-1990,16431826 -87522472369,nl,Bank A,true,ernLm,Isaac Mathis,15-03-1990,stark,16,demoOption2,57778199048,ifnBvVWl,no,QRW,16-05-1990,23100498 -61522502310,en,Bank A,true,Foajx,Philip Hart,15-03-1990,greyjoy,88,demoOption3,76799587686,kMmskHgW,no,Apj,27-10-2002,35292626 -39152986818,nl,Intersolve-voucher-whatsapp,false,Dpqje,Victoria Nash,30-11-2000,greyjoy,00,demoOption1,82185512009,laOQrgOx,no,UNz,27-10-2002,00744515 -21222261859,nl,Excel,true,EjyWK,Ellen Harrington,30-11-2000,stark,71,demoOption3,36939429212,NNXFuFyT,no,naR,27-10-2002,44514145 -45028953305,nl,FSP - no attributes,false,lzCxv,Lucy Peterson,15-03-1990,greyjoy,31,demoOption3,34303751830,WauyvMIi,no,zgm,16-05-1990,49693038 -69220477656,en,Intersolve-voucher-whatsapp,true,GWsKo,Sue Harper,30-11-2000,lannister,98,demoOption2,77529358645,uXlguNck,yes,kdN,16-05-1990,25127957 -03617881552,nl,FSP - all attributes,false,DNSPy,Chris Holt,30-11-2000,greyjoy,64,demoOption2,16986091713,YjLMhtiP,no,VKG,27-10-2002,81499374 -95449320522,nl,FSP - all attributes,false,SsVbg,Jordan Medina,30-11-2000,greyjoy,98,demoOption3,67035806166,DDtZXieX,no,Obe,27-10-2002,32203601 -78505036724,nl,Excel,true,fcCAt,Lloyd Reed,30-11-2000,greyjoy,15,demoOption2,60318988062,SbmQOaZT,no,iUt,27-10-2002,68837901 -19026584170,en,Excel,true,HFmYZ,Violet Blair,30-11-2000,lannister,90,demoOption1,81508579671,ZmiaQwuB,yes,EnY,27-10-2002,85936253 -08309644250,en,Excel,false,aXoZr,Abbie Morton,15-03-1990,lannister,31,demoOption1,20015879787,rGigNWhN,yes,BZo,16-05-1990,29034733 -05996498612,en,Excel,false,MVnWD,Violet Wade,30-11-2000,stark,17,demoOption3,59620462323,YatrxTXB,no,wLO,16-05-1990,67142391 -80097198251,en,FSP - no attributes,false,hdoQK,Jayden Brown,30-11-2000,stark,62,demoOption2,23948122181,VpVVpAOT,no,oWu,16-05-1990,32910516 -68393046001,nl,Excel,true,voSpt,Alberta Evans,15-03-1990,lannister,97,demoOption3,11871508113,lbhzAgUp,no,IdF,27-10-2002,63174276 -57675846085,nl,FSP - no attributes,true,vAWNx,Micheal Campbell,30-11-2000,greyjoy,23,demoOption2,78761814267,xNBeLeQx,yes,FSa,16-05-1990,63845375 -02900942393,nl,FSP - no attributes,true,xIZfa,Hester Phillips,15-03-1990,greyjoy,13,demoOption1,51683053650,ttbaKXdh,no,Awt,16-05-1990,25751116 -17011537364,nl,FSP - all attributes,true,yBEUo,Wesley Bailey,30-11-2000,greyjoy,20,demoOption1,60708161192,xCtLOtgF,no,Fbg,16-05-1990,90291393 -52965678386,en,FSP - all attributes,false,XJxci,Lettie Parsons,30-11-2000,greyjoy,06,demoOption2,26855478894,ayXNcRte,yes,Zta,27-10-2002,98324754 -20396769971,en,Bank A,true,pdUOf,Derrick Peterson,30-11-2000,lannister,05,demoOption1,52710684361,htmHOVZs,yes,uTf,27-10-2002,53187069 -16312551604,en,Excel,true,xqlgd,Katherine Hines,15-03-1990,greyjoy,68,demoOption2,68165279560,nkrYDPbn,yes,yGF,16-05-1990,48273078 -39184274611,en,FSP - all attributes,true,LAqMq,Delia Reynolds,30-11-2000,lannister,97,demoOption3,67650846919,lxFQvMZU,yes,qoJ,16-05-1990,95198905 -84606173263,nl,Bank A,true,tgZtF,Martin White,15-03-1990,lannister,83,demoOption3,73510865681,eeJGjQya,yes,RCe,27-10-2002,19958815 -84243201081,nl,Bank A,true,QysKT,Nathan Ramos,30-11-2000,greyjoy,48,demoOption3,38252016799,TkqgEAZB,no,GoW,16-05-1990,97152726 -91817613480,nl,Bank A,true,eFmLN,Harold Ellis,15-03-1990,stark,77,demoOption2,84942626539,hVCikDvt,yes,hZW,16-05-1990,85133752 -94487393080,en,Bank A,true,YikjM,Sophia Hill,15-03-1990,lannister,66,demoOption1,81579472479,CBKardPk,no,yQO,16-05-1990,53643491 -08340201591,nl,Bank A,true,OLjcW,Gregory Walton,30-11-2000,stark,39,demoOption2,35453181322,eEGyvJCL,yes,ueG,27-10-2002,78442377 -66063703405,en,FSP - all attributes,false,VyXRX,Clayton Perry,15-03-1990,stark,35,demoOption2,52344340165,MFaXqrcv,yes,FMC,16-05-1990,02503316 -97899933800,nl,Bank A,false,RwZHr,Tillie Ramirez,15-03-1990,lannister,43,demoOption1,78604654714,qkgKkydD,yes,DHg,27-10-2002,35555356 -43086812029,nl,Bank A,false,UyBdL,Johnny McKenzie,30-11-2000,lannister,74,demoOption1,84250281221,jVwEsjlk,no,TMe,16-05-1990,20685674 -75219022641,nl,FSP - no attributes,false,vBOWq,John Barnes,30-11-2000,stark,87,demoOption1,94638284678,bLmRjoXY,yes,GFZ,27-10-2002,43966438 -87395057364,en,Bank A,false,LYaFS,Alex Simpson,30-11-2000,stark,78,demoOption3,70846836198,duxgVfiD,yes,cNv,16-05-1990,03686287 -29993336619,nl,Excel,false,dAgYx,Eula Blair,15-03-1990,stark,43,demoOption3,82571975783,JOPnmvrI,no,aCx,16-05-1990,72585201 -09609063594,en,Bank A,true,cbFZK,Luke Singleton,15-03-1990,greyjoy,58,demoOption1,25883640584,QxmFsvNV,yes,uyI,27-10-2002,88892590 -85680765987,en,Bank A,true,wJIWS,Ryan Holmes,15-03-1990,lannister,25,demoOption2,28345284119,teNecvYS,yes,iUz,27-10-2002,20361034 -57614226399,en,Excel,false,vUhbk,Brent Pratt,15-03-1990,lannister,37,demoOption1,73685256267,jXZoptHX,yes,HjR,27-10-2002,71579580 -51920388654,en,Intersolve-voucher-whatsapp,true,ptLdL,Carolyn Vargas,15-03-1990,lannister,91,demoOption1,67157403099,MDEpOosg,yes,gkn,27-10-2002,40201952 -65065965879,nl,FSP - no attributes,true,ziGQR,Leah Boone,30-11-2000,lannister,22,demoOption1,06343447219,AqWpdhxb,no,BOV,16-05-1990,27886319 -09575038267,nl,FSP - all attributes,true,NhOsj,Carl Sandoval,30-11-2000,lannister,28,demoOption1,59212364154,nZZVTPTF,yes,akb,16-05-1990,70103669 -61553955294,nl,Excel,true,wSDLQ,Daisy Bishop,15-03-1990,stark,31,demoOption3,20225199120,ZKEJIFdW,no,Gkw,16-05-1990,98816690 -87825529176,en,Bank A,false,wfoPS,Ruth McBride,15-03-1990,stark,78,demoOption2,83005364643,fSTwBYNH,yes,Uma,16-05-1990,81310855 -31541086530,en,FSP - all attributes,false,oqePM,Connor Mendoza,15-03-1990,stark,49,demoOption2,30152301212,sNPMQzUP,no,MER,27-10-2002,06685020 -51842139158,en,Bank A,false,TEORD,Mario Hopkins,30-11-2000,greyjoy,69,demoOption3,76643814399,NtvrkAUZ,no,nxb,16-05-1990,03135111 -14205752964,en,FSP - all attributes,false,HiBQR,Milton Reed,15-03-1990,stark,78,demoOption3,12444193709,XGRIRqjf,yes,yaw,16-05-1990,37037164 -20743409582,nl,FSP - no attributes,true,ExVrk,Vera Stevenson,30-11-2000,lannister,73,demoOption1,59196563790,hgRitJwQ,yes,cVq,16-05-1990,15048230 -04060248961,en,Excel,false,QqYEA,Linnie Watts,15-03-1990,greyjoy,51,demoOption1,69206701237,opSQCGma,no,vYG,16-05-1990,60116550 -84611304505,nl,FSP - all attributes,true,EzvfZ,Patrick Cunningham,30-11-2000,lannister,18,demoOption1,10320535416,tSRWaVld,no,NBn,16-05-1990,92878994 -57415284192,en,FSP - no attributes,false,wNaud,Noah Francis,15-03-1990,stark,25,demoOption3,51318210121,GWdZjppa,yes,qUE,27-10-2002,43308908 -90584892876,nl,Excel,true,jOESe,Derrick Hines,30-11-2000,lannister,34,demoOption2,71904584972,aGsXpAKB,yes,rhx,16-05-1990,62128380 -22651714848,en,Bank A,false,qmijP,Dominic Ray,15-03-1990,stark,87,demoOption3,95610085725,sBgnNMbx,yes,oPd,27-10-2002,48356095 -49063321330,nl,Excel,true,ptKYn,Ralph Perkins,15-03-1990,stark,26,demoOption3,72617726744,vzOmAzQQ,yes,mOr,16-05-1990,77419077 -76696337196,nl,Bank A,true,pVSmO,Fannie Schmidt,15-03-1990,greyjoy,64,demoOption2,38808543993,yJOWZDIt,no,BkH,16-05-1990,50085288 -92630606438,nl,Bank A,true,YFMiR,Jennie Singleton,15-03-1990,greyjoy,74,demoOption3,38748608223,uRRCEICJ,yes,bSi,27-10-2002,50344166 -30039879377,en,FSP - no attributes,false,jSapa,Lettie Terry,30-11-2000,lannister,45,demoOption1,59034709477,XAXgcaCv,yes,oQy,27-10-2002,90532766 -17018590054,en,Intersolve-voucher-whatsapp,true,IqKvJ,Lloyd Nguyen,30-11-2000,stark,95,demoOption1,78159992567,NkUVLcMN,no,nJx,16-05-1990,47743179 -42374924792,nl,Intersolve-voucher-whatsapp,true,TcMAu,Leroy Chambers,30-11-2000,greyjoy,27,demoOption1,89616059114,PitNGEek,yes,SVQ,27-10-2002,55013801 -61644391214,nl,Excel,true,LRcgC,Vera Chavez,15-03-1990,stark,94,demoOption2,77734637890,kkMPmaOP,yes,gWg,16-05-1990,37221849 -19248893373,en,Excel,false,zwLSo,Mabel Alvarado,30-11-2000,greyjoy,48,demoOption1,67938330125,Ogeozhto,no,DRG,16-05-1990,99436924 -10387605974,nl,Bank A,true,FQhix,Craig Thornton,30-11-2000,stark,05,demoOption1,99417951069,UsTZugpJ,no,Yuo,16-05-1990,78276144 -38165981837,en,FSP - no attributes,true,UgbNR,Walter Larson,15-03-1990,lannister,21,demoOption3,54850767225,IKDYwHFe,no,qkS,16-05-1990,44587016 -94540668990,en,FSP - no attributes,true,HEkxH,Irene Harrison,30-11-2000,stark,97,demoOption3,47433301669,dvyJyRbE,no,SWN,16-05-1990,85390877 -78476264317,en,FSP - no attributes,false,vdrdb,Matilda Miller,15-03-1990,stark,73,demoOption2,46177052395,ghZIHASZ,yes,uNY,27-10-2002,47293307 -58442810151,nl,Intersolve-voucher-whatsapp,false,zzmXm,Adrian Colon,15-03-1990,stark,08,demoOption3,87155061733,tTfXDHVX,yes,SMJ,27-10-2002,98202114 -76000038255,en,FSP - all attributes,true,kLhbO,Anthony Owen,30-11-2000,lannister,22,demoOption3,50867833340,GFNpUPRd,no,aAj,27-10-2002,02970365 -65810844205,en,FSP - all attributes,false,XKVtU,Wayne Sullivan,15-03-1990,lannister,46,demoOption3,15159318597,pbRJtJDB,yes,qnw,16-05-1990,52516745 -06956924307,en,FSP - no attributes,true,XSxxc,Duane Silva,15-03-1990,greyjoy,43,demoOption3,20191985090,ilQWKdlO,no,Lri,27-10-2002,84388560 -06594592158,nl,FSP - no attributes,true,wdBCS,Mina Wong,30-11-2000,lannister,77,demoOption3,88921772879,nNxvhFNO,yes,hNI,16-05-1990,56774432 -17364456240,nl,Intersolve-voucher-whatsapp,true,WvTLj,Beatrice Leonard,30-11-2000,lannister,31,demoOption2,79773033684,idnQhVED,yes,qaP,27-10-2002,83795712 -79759936148,en,FSP - all attributes,true,VqtbG,Joe Pena,15-03-1990,lannister,68,demoOption1,99392817195,qxcndFaA,no,PsC,16-05-1990,06883277 -76604554578,en,Excel,false,AeTmA,Scott Campbell,15-03-1990,stark,93,demoOption1,78119721903,HUnSLuvn,no,dwZ,27-10-2002,35436543 -51426810829,nl,Bank A,false,yTxpI,Margaret Carlson,30-11-2000,greyjoy,98,demoOption2,62676034591,dSizTqBA,yes,Dva,27-10-2002,46378703 -53536040833,nl,FSP - no attributes,false,XfRZN,Olga Clayton,30-11-2000,lannister,89,demoOption3,24040886370,TlBwwMuK,no,FOJ,27-10-2002,95964915 -51169655265,en,FSP - all attributes,true,AHiVP,Belle Cole,30-11-2000,greyjoy,66,demoOption3,52520689985,kHlsZfhw,yes,bOa,27-10-2002,96318149 -34981114454,nl,FSP - no attributes,true,uOzRj,Hester Haynes,15-03-1990,greyjoy,72,demoOption1,49063821386,iwnNihPz,no,LJI,16-05-1990,83406846 -21142150140,en,Excel,true,FCdxz,Maurice Barker,15-03-1990,stark,23,demoOption3,29094019274,HIAlOSCi,no,UbI,27-10-2002,00789747 -83435690634,en,FSP - no attributes,true,PYFqu,Lettie Ryan,30-11-2000,greyjoy,20,demoOption2,10596812884,jVYtSzdS,yes,oSK,16-05-1990,52375653 -14485155645,en,Intersolve-voucher-whatsapp,false,oTMOV,Katie Bass,15-03-1990,stark,86,demoOption1,10141100520,WuFqqhId,yes,dYe,16-05-1990,46425656 -52325056996,en,Intersolve-voucher-whatsapp,true,hhgft,Virgie Gonzalez,15-03-1990,greyjoy,42,demoOption3,90208560311,VzrozIVj,yes,Rxc,27-10-2002,76111344 -72329828858,nl,FSP - all attributes,false,Enefc,Alan Peterson,30-11-2000,stark,78,demoOption1,20587743237,AdgbgApA,yes,gGY,16-05-1990,41124950 -63256152478,nl,FSP - no attributes,true,WRXSl,Ruby Castro,15-03-1990,lannister,94,demoOption2,60892246450,VENrhkkq,no,Grz,16-05-1990,33259416 -13697055766,en,FSP - no attributes,true,qlQmI,Leroy Nash,30-11-2000,lannister,32,demoOption1,31686771313,GUmMohLH,no,kWm,16-05-1990,63903904 -71120102317,nl,FSP - no attributes,true,vzAtr,Dennis Vasquez,15-03-1990,stark,33,demoOption2,50771685308,GBXnwgcA,yes,EVO,16-05-1990,02662056 -38288755067,en,Bank A,false,JmkqB,Virginia Park,15-03-1990,lannister,66,demoOption3,51039865822,AYGjmLIT,no,huh,16-05-1990,69292371 -70769927089,en,Excel,true,kGiDY,Bertha McKenzie,30-11-2000,greyjoy,63,demoOption3,23999636120,KgDizFNi,yes,CKG,16-05-1990,93189179 -10700883747,en,Bank A,false,TIazX,Marion Tucker,15-03-1990,lannister,65,demoOption2,54118270516,oVIcHpaJ,no,mTC,16-05-1990,70900455 -70190667839,nl,Excel,true,srAAp,Steve Obrien,15-03-1990,lannister,50,demoOption1,37602707227,YAIeedsu,yes,VUy,16-05-1990,74272089 -41191398158,en,FSP - all attributes,true,PZFFc,Isaac Perez,15-03-1990,greyjoy,87,demoOption1,53684020121,OXBhPThV,no,ZGK,16-05-1990,23963385 -48216946224,nl,Bank A,true,FmjuL,Mary Byrd,30-11-2000,stark,61,demoOption1,90546896168,LIqmVrIY,yes,pef,16-05-1990,43016553 -21387039755,nl,Excel,false,etDoK,Lora Castro,15-03-1990,lannister,66,demoOption2,12376615040,mPuJBcAc,yes,hpE,27-10-2002,23248019 -34553669280,en,Bank A,false,wgSzi,Philip Webb,30-11-2000,lannister,35,demoOption3,57203414822,vhRzsmIg,no,IgI,27-10-2002,15557060 -51125219877,nl,FSP - all attributes,false,SHWNO,Florence Norton,15-03-1990,greyjoy,84,demoOption2,89430675494,NvCvHqgC,yes,edJ,27-10-2002,93395145 -98755387549,nl,Excel,false,TTlrC,Dollie Hayes,30-11-2000,greyjoy,51,demoOption1,16374530194,WFpCMDFN,yes,zws,27-10-2002,60283155 -42870336279,en,FSP - no attributes,false,bAweM,Jerry Bush,30-11-2000,stark,87,demoOption3,30611947340,ASLdLlty,no,SmH,16-05-1990,68287991 -05460480845,nl,FSP - no attributes,true,EHXdE,Polly Henry,30-11-2000,greyjoy,39,demoOption1,40410700718,bQUbPjVs,no,HXe,27-10-2002,53316380 -80841807256,nl,Excel,false,yGcVn,Lucas Malone,15-03-1990,stark,62,demoOption3,87974412192,zGdgFKyN,no,sow,27-10-2002,35853454 -38498529292,en,Bank A,true,BVUbc,Stanley Frazier,30-11-2000,greyjoy,86,demoOption1,91758421758,NduwEjWo,yes,Cdv,16-05-1990,70106811 -75050356268,en,Bank A,true,TyHfx,Benjamin Nichols,30-11-2000,lannister,67,demoOption3,21734397017,gYLVZQvi,yes,NxM,27-10-2002,13272273 -69164066969,en,Bank A,true,sVuTy,Marian Berry,15-03-1990,greyjoy,54,demoOption3,59500667789,pgeDPmvG,yes,ZKx,27-10-2002,99569305 -55839502198,en,Bank A,true,FFDTI,Herman Swanson,15-03-1990,stark,64,demoOption2,75263812109,NWIQDroL,no,kNB,27-10-2002,86832258 -33865776857,en,Bank A,true,LKQRA,Matthew Baker,15-03-1990,stark,45,demoOption3,41748935256,lswToAxw,no,otl,27-10-2002,08791442 -57995472485,en,Excel,false,MdoFc,Emily Hardy,15-03-1990,greyjoy,50,demoOption2,76530492623,AySGgYpL,yes,Vji,27-10-2002,58890270 -28314364018,nl,FSP - no attributes,false,URDNS,Johanna Wilson,30-11-2000,stark,84,demoOption2,20045438351,frkvTBpy,yes,kYU,16-05-1990,71565795 -31443774745,en,Excel,true,jwyIg,Ruby Vasquez,30-11-2000,greyjoy,12,demoOption2,89154221723,VSIXNhxT,no,nWH,16-05-1990,00634722 -37983683705,nl,Bank A,false,eyfwb,Isaac White,30-11-2000,stark,14,demoOption1,17413838353,iObXxqcB,no,xhY,27-10-2002,76881337 -46715867487,nl,Excel,true,PAEOz,Jesus McCormick,30-11-2000,lannister,97,demoOption2,33932628515,QAkOnenT,yes,xEb,16-05-1990,30271755 -71588961319,en,Intersolve-voucher-whatsapp,false,JeRXz,Lelia Logan,15-03-1990,stark,57,demoOption3,98679402120,JDGkFVoK,yes,vGO,16-05-1990,45689400 -22054693128,nl,Bank A,false,LhGsi,Bertha Bishop,30-11-2000,lannister,27,demoOption1,39371964246,BjsxXSDg,no,cLX,16-05-1990,59551981 -71876768115,nl,Excel,false,hsRpU,Dennis Sutton,15-03-1990,lannister,11,demoOption3,02525262806,yHYpiBzQ,no,yDo,27-10-2002,27411282 -10471220772,en,Bank A,true,VnvUF,Eric Pena,30-11-2000,greyjoy,99,demoOption3,29532211262,UmRokKNt,yes,GSe,27-10-2002,27331334 -74979498090,nl,Excel,true,NprPG,Lucas Pratt,30-11-2000,greyjoy,99,demoOption1,66779975707,LRZydCGP,no,GTY,27-10-2002,40854395 -27160242829,en,Bank A,false,lkqZe,Brett Luna,15-03-1990,lannister,53,demoOption1,17529208639,nuicIAMI,yes,BOl,16-05-1990,49526872 -86190465788,en,FSP - all attributes,true,UQkns,Julian Santiago,30-11-2000,stark,48,demoOption1,45946424457,BnZLTyJM,no,GGB,27-10-2002,06724880 -48076066820,en,Intersolve-voucher-whatsapp,true,kOZRY,Jane Reyes,30-11-2000,stark,10,demoOption3,49968633474,fnUyBPeo,yes,LEH,16-05-1990,41636052 -51227356535,nl,Excel,false,MgOIG,Lily Vasquez,30-11-2000,greyjoy,01,demoOption3,24246685856,QrqnOpZi,yes,KYr,27-10-2002,62463325 -63508275814,en,Excel,false,LIrQJ,Nathan Ramos,15-03-1990,greyjoy,25,demoOption2,52687273770,EKCiRONY,no,exP,27-10-2002,88028547 -06877383764,nl,Bank A,false,QbIHc,Dominic Watts,15-03-1990,lannister,58,demoOption1,98685607024,pHLFFYlg,no,lRu,16-05-1990,08624383 -74456307182,en,Excel,false,urHIh,Elmer Harrison,30-11-2000,lannister,86,demoOption1,29256634106,RXanMcjr,no,XqX,16-05-1990,26799167 -46155884851,nl,Intersolve-voucher-whatsapp,false,wNKGm,Sophia Bell,15-03-1990,greyjoy,12,demoOption2,68027855603,IJEtGsOg,no,WaM,16-05-1990,27743027 -03814962270,en,Excel,true,IwjKE,Lizzie Reynolds,15-03-1990,greyjoy,46,demoOption3,11176230479,fWTuOJsO,no,aRn,27-10-2002,31078760 -94777264225,en,Excel,false,zhaus,Catherine Barker,15-03-1990,lannister,46,demoOption1,84216779859,zjfVTTQa,yes,Kua,27-10-2002,95912266 -64457667687,nl,FSP - all attributes,true,bTNba,Shane Oliver,15-03-1990,greyjoy,65,demoOption2,47328468867,dMtRLcrP,yes,yCT,16-05-1990,89506199 -99760726605,en,Intersolve-voucher-whatsapp,true,vSljR,Maggie Harrington,15-03-1990,greyjoy,03,demoOption2,51774685587,WPQGRtJh,yes,oGc,27-10-2002,26689665 -17413412429,nl,FSP - no attributes,false,TRQsK,Nora Howard,15-03-1990,stark,62,demoOption2,50146324400,fsxPxeuM,yes,ZVQ,16-05-1990,03774172 -94298915487,nl,Intersolve-voucher-whatsapp,false,Wnyug,Earl Joseph,15-03-1990,greyjoy,95,demoOption3,41373282884,qzghngmw,no,nTB,27-10-2002,63741776 -65863334408,en,FSP - all attributes,true,ErynH,Beatrice Riley,15-03-1990,greyjoy,57,demoOption1,20566942282,JDYHxIdW,no,cAd,27-10-2002,30968002 -11586444651,nl,Intersolve-voucher-whatsapp,true,ENLrz,Abbie Chandler,15-03-1990,lannister,92,demoOption1,97175054015,tpCWCrsP,yes,VnZ,27-10-2002,36335578 -47655785844,en,Intersolve-voucher-whatsapp,false,JRlWN,Johnny Chapman,30-11-2000,greyjoy,86,demoOption1,96093631428,TeevoIbc,no,xwi,16-05-1990,22603703 -57019630827,nl,Excel,true,hYjCa,Matilda Reynolds,15-03-1990,stark,01,demoOption3,82097510187,LMbYaHiu,no,btr,16-05-1990,00480272 -98241195513,nl,Bank A,false,MKCUT,Mitchell Sparks,30-11-2000,stark,84,demoOption2,93543117223,WhzRsHUJ,no,PAF,16-05-1990,82561941 -88884250981,en,Bank A,false,MWQyJ,Olga Hart,30-11-2000,stark,74,demoOption1,34407217332,CbivTuhi,yes,KFG,27-10-2002,98487742 -65997362116,en,Bank A,true,VdrKP,Olga Harvey,15-03-1990,stark,70,demoOption1,79211174295,VkjHoYcq,no,UsM,16-05-1990,99922776 -34162176197,nl,Excel,true,nDiBn,Nancy Pierce,30-11-2000,stark,19,demoOption1,74026277144,oISUrXLQ,yes,dky,16-05-1990,36099038 -11894153337,en,Excel,true,meQPu,Amelia Hunter,30-11-2000,greyjoy,16,demoOption3,67761409433,RWinpiWt,yes,CJa,27-10-2002,20661272 -96270626784,nl,Excel,true,zYfZS,Andrew Klein,15-03-1990,lannister,07,demoOption2,10721868840,ptsFQuRW,yes,nEa,27-10-2002,62740045 -05182727797,en,FSP - no attributes,true,SLIcZ,Kenneth Taylor,15-03-1990,greyjoy,29,demoOption1,14590602132,IjMfJAHF,yes,vww,27-10-2002,41394529 -16761330492,en,FSP - all attributes,true,PthIS,Aiden Carson,30-11-2000,lannister,06,demoOption3,60114749592,duJbhmdG,yes,Rvm,27-10-2002,40230931 -55824397632,en,Intersolve-voucher-whatsapp,true,JtpvF,Gertrude Mason,30-11-2000,stark,18,demoOption1,94219887484,pwjkbLze,yes,RzD,27-10-2002,01193998 -50601174408,nl,Excel,false,rSFAk,Gussie Roberts,15-03-1990,greyjoy,65,demoOption3,36531387487,lojpwVnC,no,Vde,16-05-1990,89080951 -32758965517,nl,Excel,true,WleUY,Mary Herrera,15-03-1990,lannister,13,demoOption2,48162723643,vvfGhvTe,yes,prG,16-05-1990,67203658 -23433097913,en,FSP - all attributes,false,ShoNz,Addie Caldwell,15-03-1990,lannister,15,demoOption3,96294749257,ZIEuElSF,no,hMf,16-05-1990,85065874 -95720196183,en,FSP - no attributes,true,Vljei,Gregory Buchanan,15-03-1990,greyjoy,01,demoOption1,24168988890,DDjeFeKf,yes,Qfd,16-05-1990,72174562 -75616224735,nl,Excel,true,FOtJX,Lela Cohen,30-11-2000,lannister,07,demoOption3,01015020364,azdeXcDZ,no,IJQ,27-10-2002,64487845 -96128641475,nl,Bank A,false,dAuYm,Lawrence Spencer,15-03-1990,lannister,44,demoOption2,94746809829,BUnuiWZl,no,rwD,16-05-1990,45003960 -33633783071,en,FSP - no attributes,true,yyxcs,James Dixon,15-03-1990,greyjoy,14,demoOption2,11353958728,lLTlGKIm,no,ANA,27-10-2002,98588694 -92944232552,nl,FSP - no attributes,true,XmgqW,Jordan Hayes,15-03-1990,lannister,74,demoOption3,00093474191,cHvNXdEr,yes,sfH,27-10-2002,72175677 -74537684769,nl,Excel,false,OOsrf,Hilda Buchanan,15-03-1990,stark,19,demoOption1,80594552146,UVfpeVpw,no,Xpv,16-05-1990,46779546 -68426092863,nl,FSP - no attributes,true,RbCdW,Chris Glover,30-11-2000,lannister,90,demoOption1,51058705787,HCFdbKWK,yes,Cqq,27-10-2002,77915774 -74688663001,nl,FSP - no attributes,true,hgCKR,Evan Willis,30-11-2000,stark,18,demoOption3,85450260668,uIsIoSaS,yes,cOi,16-05-1990,31655053 -11097899169,en,FSP - all attributes,true,fKMbn,Fred Christensen,15-03-1990,lannister,75,demoOption3,58883688002,APbChKga,yes,wYs,27-10-2002,21453832 -45255203426,en,Excel,false,pTVzf,Rachel Dunn,30-11-2000,stark,95,demoOption3,15677128839,UUQtHHaw,no,pGC,27-10-2002,32859281 -03436738333,nl,FSP - no attributes,true,BYfFi,Hettie Stokes,15-03-1990,lannister,71,demoOption1,73086534326,jENQggEG,yes,MEy,16-05-1990,88696889 -92091078721,en,Intersolve-voucher-whatsapp,true,ewvxo,Abbie Joseph,30-11-2000,stark,37,demoOption1,86599058210,GzIrhcUO,no,uaq,16-05-1990,59961067 -86680742699,en,Excel,false,HxvGl,Bessie Murphy,15-03-1990,lannister,93,demoOption2,77891364336,deOPyKho,yes,SQv,27-10-2002,38016206 -42584212274,nl,FSP - all attributes,false,umLeH,Winifred Lucas,30-11-2000,lannister,08,demoOption3,31069828121,VQNrHkLL,no,Vzn,27-10-2002,30935567 -25549085370,en,Excel,true,UdkRU,Earl Ferguson,30-11-2000,greyjoy,76,demoOption1,90522340904,vmdZEhci,yes,vmz,27-10-2002,00385217 -66720285997,en,Excel,false,dPonZ,Austin Fields,30-11-2000,greyjoy,13,demoOption2,12631412491,CWlJeAnZ,yes,xKQ,16-05-1990,24786737 -25346274734,en,FSP - no attributes,true,DAAqj,Chase Munoz,15-03-1990,greyjoy,34,demoOption3,21286040095,NanpAqep,no,kaS,27-10-2002,15673270 -28238946817,nl,Intersolve-voucher-whatsapp,true,RxWkX,Darrell Cobb,15-03-1990,greyjoy,39,demoOption3,60885598563,KeBhoLgy,no,CVQ,16-05-1990,01902568 -56008757827,nl,FSP - no attributes,false,yHQem,Caroline Bailey,30-11-2000,lannister,98,demoOption1,60561834352,cjFcAcwc,yes,OOi,27-10-2002,29870280 -93491206462,nl,Bank A,false,wzBCh,Maria Vargas,15-03-1990,lannister,28,demoOption1,20737214247,kuFbDODn,no,CAQ,27-10-2002,37195139 -70725657113,en,Intersolve-voucher-whatsapp,false,gfhLx,Albert Daniel,30-11-2000,greyjoy,50,demoOption1,50586222282,HNWZRsHa,yes,Awa,16-05-1990,41924067 -41969185730,nl,FSP - no attributes,false,auvgc,Hattie Harper,15-03-1990,greyjoy,28,demoOption1,70479031519,QWRURGVH,no,WOg,16-05-1990,35777531 -80555726552,en,FSP - no attributes,true,XQwHb,Adam Garrett,15-03-1990,lannister,42,demoOption2,81604211586,FJdujgXy,yes,tKD,16-05-1990,68094261 -26364030217,en,FSP - all attributes,false,ZhZkM,Jonathan Hunt,15-03-1990,greyjoy,70,demoOption2,33071130847,RhILoXNx,yes,liJ,16-05-1990,08268410 -76281440938,nl,Excel,true,PWyfK,Jennie Caldwell,30-11-2000,lannister,40,demoOption1,75586581896,zuQxuHzK,yes,vvP,16-05-1990,50211573 -92259104235,en,FSP - all attributes,true,GjfQp,Ryan Jefferson,15-03-1990,greyjoy,38,demoOption3,20532014697,HUJvmkyj,no,FIA,16-05-1990,01512509 -71579119523,nl,Excel,false,FqpVK,Lettie Logan,30-11-2000,lannister,85,demoOption1,08212793277,FkZupvTe,yes,eKY,16-05-1990,44121244 -46868146396,en,Bank A,true,msulJ,Joseph Henry,30-11-2000,stark,30,demoOption1,33473476597,JJaoNvVQ,yes,FZz,27-10-2002,64327253 -57807489749,en,Intersolve-voucher-whatsapp,true,VAtOA,Adele Roberts,15-03-1990,stark,95,demoOption3,79324852916,rYdBAqKL,no,FJI,16-05-1990,35638952 -21084018749,nl,FSP - no attributes,true,vfBFO,Rose Nelson,15-03-1990,greyjoy,15,demoOption2,48046197901,iWkoljVf,yes,eIv,27-10-2002,42627212 -18363625309,nl,FSP - all attributes,true,Fgddo,Elizabeth Wilkerson,15-03-1990,stark,19,demoOption1,97986497215,KFgUasgd,no,Nzd,16-05-1990,14375980 -89164012329,nl,Excel,true,bRBUN,Josephine Garza,30-11-2000,greyjoy,51,demoOption1,65211079263,YwpMeueK,no,Jeb,16-05-1990,65268582 -52775795261,en,FSP - no attributes,false,snLcA,Christopher Frazier,15-03-1990,greyjoy,20,demoOption2,22870245975,zDcxnhXd,yes,uWg,16-05-1990,16764022 -44856428558,en,FSP - no attributes,true,sgbJy,Lillian Bell,30-11-2000,stark,82,demoOption3,55989697653,TDQdVPod,yes,Psj,16-05-1990,07883270 -16473596431,nl,Excel,true,JRjsz,Gary Miller,15-03-1990,greyjoy,06,demoOption3,26675966814,djXNIwNy,no,cOu,27-10-2002,87362180 -87215965246,nl,Intersolve-voucher-whatsapp,false,sKxDS,Mason Klein,15-03-1990,greyjoy,23,demoOption1,32849589235,DpikdDYz,no,pSX,27-10-2002,27790343 -72318505528,nl,Intersolve-voucher-whatsapp,false,XydKG,Rodney Strickland,30-11-2000,lannister,04,demoOption2,60418708440,yoCPoBhj,yes,JHt,16-05-1990,05701279 -47334872604,en,Intersolve-voucher-whatsapp,false,eUpng,Ruby Ramos,15-03-1990,stark,97,demoOption1,47596689419,EdByxHDi,yes,Zby,27-10-2002,80325998 -57349871949,nl,Excel,true,oPXqO,Jordan Shaw,15-03-1990,greyjoy,10,demoOption3,77985663937,wvCbrQIj,yes,tUB,16-05-1990,43510787 -39080257133,en,Intersolve-voucher-whatsapp,true,gMHTJ,Brent Harvey,15-03-1990,greyjoy,65,demoOption3,52495562034,tPuehiuh,no,Pnf,16-05-1990,74820896 -03094093495,en,FSP - no attributes,false,leUeI,Mattie Perkins,30-11-2000,stark,32,demoOption1,83893445923,zfeoxMLL,yes,mYb,16-05-1990,69566833 -12244077023,en,Intersolve-voucher-whatsapp,true,XYUoc,Fred Lindsey,15-03-1990,lannister,03,demoOption2,37975812603,khfxBVCC,no,gjy,27-10-2002,15065480 -63106503350,nl,Intersolve-voucher-whatsapp,false,FwlYP,Amelia Delgado,15-03-1990,greyjoy,03,demoOption3,26248370480,FpeNBBcd,no,XdI,16-05-1990,05098457 -09345930852,en,FSP - no attributes,false,nLnve,Lela Ortiz,30-11-2000,stark,84,demoOption2,02746436152,dcfAQNPC,yes,mbb,27-10-2002,17160747 -80919211322,en,FSP - all attributes,true,CtQri,Nathaniel Park,30-11-2000,greyjoy,31,demoOption3,18821864357,SWMXnOLN,yes,yWl,27-10-2002,08007155 -08404772626,en,FSP - no attributes,true,nRDop,Inez Evans,30-11-2000,stark,21,demoOption1,15076225709,SGtLMEZT,yes,lWB,16-05-1990,38153968 -07417051385,nl,FSP - no attributes,true,rszji,Rosetta Russell,15-03-1990,greyjoy,55,demoOption1,33783534816,NUCAbZPp,no,xgr,16-05-1990,24207267 -04360486447,nl,FSP - all attributes,true,iWJgK,Craig Warren,15-03-1990,lannister,61,demoOption2,76195796791,XhVVVUva,yes,Lwp,16-05-1990,60278427 -63454887278,en,Intersolve-voucher-whatsapp,true,pRaug,Blake Wise,15-03-1990,stark,94,demoOption3,79172136610,FpaCUsMk,no,iFd,27-10-2002,83221926 -56019854694,nl,FSP - no attributes,true,pKIAe,Oscar Rose,30-11-2000,lannister,47,demoOption2,17536355187,fmyZRAjT,no,Tml,16-05-1990,10638330 -89486908811,en,Bank A,false,qgETM,Francis Munoz,15-03-1990,lannister,09,demoOption1,85822980992,uSeBusMu,yes,RtK,27-10-2002,20813727 -50991083152,nl,Excel,false,Fzmyy,Teresa Hansen,15-03-1990,greyjoy,78,demoOption2,10404714051,RRIcaYJR,no,tHY,16-05-1990,19004426 -38941025190,nl,FSP - no attributes,false,KcnrS,Elsie Hodges,30-11-2000,lannister,71,demoOption2,12329349527,rNhjmeCP,yes,QDR,16-05-1990,16731896 -99744840750,en,Excel,true,KDBGc,Adele Norman,15-03-1990,lannister,75,demoOption2,43604046290,vNKwDeYz,no,aFX,27-10-2002,86068239 -53238730897,en,Excel,false,SAgeq,Nathaniel Abbott,30-11-2000,lannister,50,demoOption3,82600768077,UThwzFgI,yes,ejC,16-05-1990,64226077 -37829509258,en,Bank A,false,EZUQT,Mike Ballard,30-11-2000,stark,01,demoOption3,79289324107,XWhBGUIF,yes,aZc,27-10-2002,30129276 -70533879017,en,Bank A,true,RPjol,Gussie Conner,15-03-1990,stark,52,demoOption3,84387378820,AveYusAh,yes,LAu,16-05-1990,25395266 -28698299394,nl,FSP - all attributes,false,euopL,Bill Brady,30-11-2000,stark,42,demoOption2,18123096291,aXnxnKoF,yes,TkK,16-05-1990,49149665 -36437800762,en,Intersolve-voucher-whatsapp,false,hHSLe,Mina Powell,30-11-2000,lannister,39,demoOption1,13930331373,gxGgkQdo,no,NOn,16-05-1990,14101704 -49421270872,nl,FSP - all attributes,false,UADIs,Beatrice Sanchez,30-11-2000,greyjoy,29,demoOption1,40996654967,bQNagohG,no,xOd,16-05-1990,65450577 -63364564099,en,Bank A,false,DYlmv,Gordon Wilson,30-11-2000,lannister,96,demoOption3,82431874389,LdYaBSEe,yes,cLq,27-10-2002,40367330 -26126178628,nl,FSP - no attributes,false,JgHfE,Estelle Cox,30-11-2000,stark,48,demoOption1,18600126533,NoXtioRe,yes,wwz,27-10-2002,05927650 -99600064538,nl,FSP - no attributes,true,xQxHm,Sadie Jimenez,15-03-1990,lannister,98,demoOption3,10243585343,ruDbAsFJ,no,Hlu,27-10-2002,25835697 -39326795763,en,FSP - all attributes,true,PrPAA,Carrie Burke,30-11-2000,greyjoy,47,demoOption1,71776897612,YrVIanNq,yes,ZVw,16-05-1990,76472476 -91386266066,nl,FSP - all attributes,true,QyArM,Walter Jefferson,30-11-2000,lannister,38,demoOption1,77028086460,JRiZyUTL,yes,wzm,27-10-2002,58510199 -99731194703,en,FSP - no attributes,true,nmXtH,Loretta Wong,30-11-2000,lannister,80,demoOption3,53164814587,AQxedOew,no,WzD,27-10-2002,94416688 -52163485292,en,Bank A,true,vcXIl,Theresa Pittman,15-03-1990,stark,23,demoOption2,98668152544,kGwhZzil,no,ubJ,27-10-2002,35323280 -05112693636,en,Bank A,true,GXEsh,Jane Hardy,30-11-2000,lannister,74,demoOption1,70625372745,ULSpBcQp,no,bfR,16-05-1990,30868353 -90833288682,en,Bank A,true,SjrmD,Isabella Clarke,30-11-2000,stark,15,demoOption3,95294809298,OyTTzSUk,no,FCm,16-05-1990,44198804 -05612630729,en,Excel,false,PKftE,Rodney May,15-03-1990,greyjoy,21,demoOption3,50762345833,qYbMHsYu,no,qmP,27-10-2002,75888720 -96301646898,en,Excel,false,QBXbz,Mayme Blair,15-03-1990,stark,78,demoOption3,96472324836,TYwwUEUg,no,ZVe,27-10-2002,20364624 -46897886418,en,Excel,false,tprnc,Daniel Thornton,30-11-2000,lannister,11,demoOption2,12649392443,hStUSvfo,yes,yfl,16-05-1990,65521169 -20943679443,nl,Excel,true,oSIfp,Jerry Bailey,30-11-2000,greyjoy,58,demoOption1,29654241953,koGpTVnk,no,Thh,27-10-2002,09784959 -96829214991,en,Excel,true,jCyXL,Daisy Floyd,30-11-2000,lannister,76,demoOption1,21833322904,YUzfTyDi,yes,lkF,27-10-2002,22781731 -06972270004,en,Intersolve-voucher-whatsapp,true,hhSZJ,Louisa Ryan,15-03-1990,stark,31,demoOption1,52045994411,ExyOIbgK,no,wiP,27-10-2002,05172399 -67486466788,nl,Intersolve-voucher-whatsapp,true,hlStH,Keith Mills,30-11-2000,lannister,30,demoOption1,92672983644,JthvKzHd,no,ZME,16-05-1990,33529842 -26336555654,nl,Excel,false,hGurR,Dora Stanley,15-03-1990,lannister,50,demoOption2,31908625455,BCkLtoDH,no,iZp,27-10-2002,72568661 -83242831467,en,FSP - no attributes,true,DKUyx,Lela Sullivan,30-11-2000,greyjoy,06,demoOption1,79007347178,MCSbqwsz,yes,XiL,27-10-2002,74325417 -57225597127,nl,Intersolve-voucher-whatsapp,true,efPAo,Danny Gibson,15-03-1990,greyjoy,52,demoOption3,25000382906,oaQHklqi,yes,ZoN,27-10-2002,60379765 -26863988248,en,Bank A,false,fSIcN,Brian Haynes,30-11-2000,greyjoy,68,demoOption3,47230764793,ElyLRKVe,no,Kha,27-10-2002,04276445 -05824013954,nl,Intersolve-voucher-whatsapp,false,sZUoo,Francisco Conner,30-11-2000,stark,40,demoOption2,47209300324,jLFsVyrp,no,HRe,16-05-1990,52848980 -40092618219,nl,FSP - all attributes,true,xVrZS,Frank Patterson,30-11-2000,greyjoy,63,demoOption2,27918620970,svlfUIjM,no,kuc,27-10-2002,48363471 -21334829670,en,FSP - all attributes,false,bFYfF,Philip Cruz,15-03-1990,greyjoy,49,demoOption3,38533811804,dFfmvclo,yes,lJu,27-10-2002,75329581 -94213203511,nl,FSP - no attributes,true,WVylB,Alexander Delgado,30-11-2000,greyjoy,47,demoOption2,12806414323,LxdIHIfo,no,pol,16-05-1990,64839898 -71370412384,en,FSP - all attributes,true,oQWgt,Cameron Lamb,15-03-1990,lannister,15,demoOption3,42319621587,mIfLUvaZ,no,CBp,27-10-2002,62957898 -11191401426,en,Intersolve-voucher-whatsapp,true,UuNOA,Alex Hernandez,30-11-2000,lannister,61,demoOption1,28273129931,usoAAuKH,yes,DCj,16-05-1990,78692031 -26056176661,nl,FSP - no attributes,true,pVmEh,Rosie Alvarado,15-03-1990,stark,85,demoOption1,96768419039,hQcovZbX,yes,ykR,16-05-1990,07167360 -62176676089,en,Bank A,false,kNXSv,Jose Morrison,30-11-2000,greyjoy,50,demoOption3,29855848295,QlkDHDDV,yes,YEx,16-05-1990,01233532 -79187488315,nl,Excel,false,Blhvk,Hilda Stevenson,15-03-1990,lannister,84,demoOption2,26502138993,aaymVWFV,no,LyO,16-05-1990,87562039 -77240201078,nl,Excel,true,rFgQZ,Elnora Cook,15-03-1990,greyjoy,98,demoOption3,82850353485,eJVahWzF,yes,SoG,27-10-2002,64629971 -68611717423,en,Excel,false,YIakZ,Grace Hogan,15-03-1990,greyjoy,06,demoOption2,03899264141,ssQSNNyg,yes,geq,27-10-2002,77185602 -85207493952,en,FSP - all attributes,true,qmOsi,Carl Pittman,15-03-1990,greyjoy,65,demoOption2,27873119583,FXictwNu,yes,kmQ,16-05-1990,74465025 -26798925557,nl,FSP - no attributes,true,OovaR,Miguel Jensen,30-11-2000,greyjoy,39,demoOption3,27147041262,XKAYlhec,no,MSL,27-10-2002,34182064 -21359257526,nl,Bank A,true,wakCM,Kevin Harmon,30-11-2000,greyjoy,94,demoOption1,69791212783,krWtbeuA,yes,qRg,27-10-2002,77872063 -35774027091,nl,Bank A,false,NbdVb,Sarah Glover,30-11-2000,stark,91,demoOption1,60323878690,vjmHcYez,no,XcD,16-05-1990,98834848 -58180405341,en,FSP - all attributes,false,OWEAx,Virgie Higgins,30-11-2000,stark,66,demoOption2,33396805715,nQXKUUeF,no,kmB,16-05-1990,43524208 -71105975632,en,Intersolve-voucher-whatsapp,true,qGAKU,Sylvia Holmes,15-03-1990,lannister,91,demoOption1,73492396408,BUbaYDrc,yes,NHx,16-05-1990,91158008 -28659777894,nl,Intersolve-voucher-whatsapp,true,jtmpe,Francis Spencer,30-11-2000,greyjoy,78,demoOption1,64311209887,zrkWQSFn,yes,taS,16-05-1990,23425824 -80594162331,nl,FSP - no attributes,true,kaIrM,Sue Peters,30-11-2000,stark,23,demoOption2,61387060046,oiAKxLGJ,no,bLo,16-05-1990,95271912 -54700483585,en,FSP - no attributes,true,hIqDb,Leah Black,15-03-1990,greyjoy,42,demoOption1,13912624543,GbDLzvhh,no,Tzy,27-10-2002,27133786 -13129636355,en,Bank A,false,EqFvc,Mabelle Tyler,30-11-2000,greyjoy,35,demoOption3,24019371668,rxdETYed,no,DaB,27-10-2002,45448017 -25176550609,nl,Bank A,false,BzfUw,Jerome Abbott,15-03-1990,greyjoy,75,demoOption1,49530166819,bjixjFkl,yes,VdY,27-10-2002,53926284 -66544322667,nl,Bank A,true,PKsfy,Dylan Hall,15-03-1990,greyjoy,61,demoOption3,23696122332,NYFhwfXO,yes,vdp,16-05-1990,67876731 -86286585706,en,Excel,false,yUoaH,Eric Sherman,30-11-2000,stark,45,demoOption1,03439977666,uBKIVNxA,yes,wqV,27-10-2002,39383113 -33422301167,nl,FSP - all attributes,false,wOqdS,Carlos Stone,30-11-2000,lannister,06,demoOption1,96673068871,JywpImdF,no,zby,27-10-2002,57842231 -88282266917,en,FSP - all attributes,true,vQhlV,Stella Smith,15-03-1990,stark,33,demoOption1,38995316486,XMwtBGfJ,yes,sHE,27-10-2002,08870083 -80452611442,nl,Excel,false,jqQYq,Tony Atkins,15-03-1990,stark,88,demoOption1,98862159712,BLvvpJCB,yes,ush,16-05-1990,20979281 -19078005257,en,FSP - no attributes,true,PvMOI,Mina Mann,30-11-2000,stark,37,demoOption3,08857294576,dWJKznff,yes,HWQ,16-05-1990,54693357 -75961480975,nl,Excel,false,giZcd,Benjamin Potter,15-03-1990,lannister,17,demoOption2,77692749265,uumTEiPJ,no,LBF,16-05-1990,74376285 -64817034038,en,FSP - all attributes,true,yhEdS,Belle Sutton,30-11-2000,greyjoy,91,demoOption2,16930402002,sMHCCxSV,yes,kkx,16-05-1990,60978020 -31726561099,en,FSP - all attributes,false,YuCfm,Nathaniel Brady,30-11-2000,stark,03,demoOption2,55440384603,VHZJEfhN,yes,uUH,27-10-2002,14633166 -99120000838,en,Bank A,true,jNKOc,Chester Wallace,15-03-1990,lannister,28,demoOption1,77919399698,JZKDYgMz,yes,YTg,16-05-1990,90936694 -78634798660,nl,Bank A,false,mpyhP,Darrell Torres,15-03-1990,stark,50,demoOption1,37813514847,xvmIJdjt,yes,CpF,16-05-1990,87999029 -40916001283,nl,FSP - no attributes,true,byXcT,Dale Brady,30-11-2000,stark,89,demoOption2,75208956905,phLmuZKd,yes,Axs,27-10-2002,22019902 -65974059849,en,FSP - no attributes,false,gzfTT,Mary Bryant,15-03-1990,stark,44,demoOption3,14536865565,vuGQNSwW,yes,wfv,16-05-1990,83256392 -90163775370,nl,Excel,true,sTCFe,Hattie Wheeler,30-11-2000,greyjoy,66,demoOption3,53065283769,GgTrssOk,yes,Vjh,16-05-1990,36665706 -04739745577,en,FSP - no attributes,true,TZyvt,Tom Blair,30-11-2000,lannister,05,demoOption1,55302595441,TjyrRIRo,yes,BjU,16-05-1990,62467832 -00883351548,nl,FSP - no attributes,false,AxSNP,Justin Bush,15-03-1990,greyjoy,13,demoOption1,39921307216,cVLpyBEX,no,QxB,27-10-2002,41597356 -14197922779,nl,Bank A,false,vomjX,George Chandler,30-11-2000,lannister,21,demoOption1,98282978498,EdxDVDKv,yes,iZL,16-05-1990,21440079 -13536581035,nl,FSP - no attributes,true,gZcuA,Agnes Garrett,15-03-1990,stark,10,demoOption1,92390637061,OviPezZa,no,siC,27-10-2002,72858535 -44625085296,en,FSP - no attributes,false,Haiha,Maria Bush,30-11-2000,stark,82,demoOption2,46264308647,bMVwHMPi,no,ikK,27-10-2002,71469861 -39377295418,en,Bank A,false,bwUgq,Eugenia Parker,30-11-2000,stark,54,demoOption1,02905601298,YlzFzkyj,no,bYq,27-10-2002,19968522 -34063923774,nl,FSP - no attributes,false,eIKMn,Manuel Jones,15-03-1990,stark,48,demoOption3,72303906161,yQIZwuyk,yes,AGd,27-10-2002,07100178 -10014824253,nl,FSP - no attributes,false,gdlDl,Marc Warren,30-11-2000,stark,42,demoOption3,31064624135,qCyvydPg,yes,Qng,27-10-2002,28965746 -93893788728,en,FSP - no attributes,false,vcRBW,Fred Simon,30-11-2000,greyjoy,14,demoOption3,31959848095,LpZilUsw,yes,syw,27-10-2002,48922554 -53505421978,en,FSP - no attributes,false,Rxcns,Matthew Guerrero,15-03-1990,greyjoy,01,demoOption3,57440833885,fIvopbHv,yes,avB,16-05-1990,80868146 -75131734760,nl,FSP - all attributes,true,jHFUc,Bruce Ellis,15-03-1990,greyjoy,51,demoOption3,76737746717,OQzJWQoz,yes,SVr,27-10-2002,69883316 -42434273610,en,Bank A,true,vyZLD,Jeremy Mathis,15-03-1990,greyjoy,35,demoOption2,31126284775,pOeFMsKB,yes,Cui,16-05-1990,00904046 -68562246122,en,FSP - no attributes,true,izYYo,Bobby Stevenson,30-11-2000,greyjoy,97,demoOption1,17344592048,yXUDAoux,yes,eif,16-05-1990,11961608 -54851642006,en,Intersolve-voucher-whatsapp,false,dTNAn,Tom Miles,15-03-1990,stark,08,demoOption2,36589494129,qPfINWjo,no,gDt,16-05-1990,22353289 -02462557312,en,FSP - all attributes,false,zZZfG,Lela Henderson,30-11-2000,lannister,24,demoOption2,09067073559,JsVYnjxX,no,WWj,16-05-1990,99206644 -31714186937,nl,Bank A,false,RaATT,Caroline Bowen,15-03-1990,greyjoy,11,demoOption1,11405487303,RgMJNMpN,no,tas,27-10-2002,12294950 -44002832228,en,Excel,true,iDxHg,Maurice Morrison,15-03-1990,stark,50,demoOption2,22964988164,HzZQTbqY,no,iFs,27-10-2002,34741498 -27084427180,en,Intersolve-voucher-whatsapp,true,wWtNg,Eula Sims,15-03-1990,lannister,95,demoOption1,37061646184,uYqpBLsQ,no,EIW,27-10-2002,24491786 -54407420653,en,FSP - no attributes,false,tsKJB,Lenora Elliott,30-11-2000,lannister,73,demoOption2,17256297475,kYPMfqqG,yes,mzn,27-10-2002,64019447 -80456235880,en,FSP - all attributes,false,emCpj,Alex Davidson,30-11-2000,stark,53,demoOption2,05202610172,DcZFAZSh,no,dYO,16-05-1990,43447974 -89841495755,en,Intersolve-voucher-whatsapp,true,sjlvR,Augusta Rowe,15-03-1990,lannister,19,demoOption2,05305876793,bXnIpNHV,yes,LPv,27-10-2002,93567505 -52782716570,en,FSP - all attributes,false,SQvHY,Christian Fowler,30-11-2000,stark,18,demoOption1,47039332054,NjQBTUQT,yes,gcQ,27-10-2002,32800775 -71419770090,nl,FSP - all attributes,false,GIRmX,Ola Gilbert,15-03-1990,lannister,72,demoOption3,86545811988,erMCmrDX,yes,flc,16-05-1990,55882378 -35915506763,nl,Intersolve-voucher-whatsapp,true,kZCux,Todd Bass,30-11-2000,lannister,95,demoOption3,19562195318,oASnHhzr,yes,yoM,27-10-2002,03724276 -83786462780,nl,Bank A,false,vWedZ,Myra Long,30-11-2000,lannister,44,demoOption2,33385452462,fpAEimZe,yes,XKK,16-05-1990,84747240 -69785900086,en,Excel,true,eWvKC,Rena Powers,30-11-2000,greyjoy,11,demoOption2,78605514326,LLEzkgun,yes,YPe,27-10-2002,59684985 -22842635852,en,Intersolve-voucher-whatsapp,false,UICqm,Bruce Dean,30-11-2000,lannister,05,demoOption1,53193455957,NxJAXpOI,no,TXb,16-05-1990,43325607 -54141362981,nl,FSP - all attributes,false,rOIzg,Brent McCoy,30-11-2000,stark,65,demoOption3,25126173110,bHdNJHkB,no,GZI,16-05-1990,86894125 -20552266409,en,Excel,true,FphJi,Jorge Gill,30-11-2000,lannister,55,demoOption1,56080660023,LPqGOQSv,no,YRS,27-10-2002,17121344 -27758928667,en,Excel,false,zHuAN,Mamie McCarthy,30-11-2000,greyjoy,02,demoOption1,63988213536,IOKOQcXP,no,day,27-10-2002,73098155 -26610145441,nl,FSP - no attributes,false,uNWrn,Georgie Guzman,15-03-1990,greyjoy,27,demoOption2,57381268781,WXSidtKE,yes,WFC,27-10-2002,16347128 -57798988418,en,Intersolve-voucher-whatsapp,true,TzKla,Roger Saunders,15-03-1990,lannister,46,demoOption3,62944849954,mUXzRkes,no,qfA,16-05-1990,47160567 -03742844751,nl,FSP - no attributes,false,PNCrb,Blake Davis,30-11-2000,stark,68,demoOption3,01777685110,gcljpXHU,yes,Ywq,27-10-2002,85396034 -10721619588,nl,Excel,true,popmW,Lola Nguyen,30-11-2000,greyjoy,09,demoOption3,75117154157,ryiSyykR,no,QxB,16-05-1990,90487496 -92674910087,en,Excel,false,aRMiu,Jerry Wolfe,15-03-1990,stark,86,demoOption1,02128974347,KIvjzFqu,no,gmg,16-05-1990,00721350 -78335193454,en,Intersolve-voucher-whatsapp,true,tCnjY,Ricky Jensen,15-03-1990,greyjoy,82,demoOption1,55643438162,fyhfawHk,no,fGY,16-05-1990,64577877 -89088386702,nl,Bank A,true,kWPHU,Virginia Palmer,15-03-1990,greyjoy,63,demoOption3,58234098519,yKuXAZDU,no,QJy,27-10-2002,30130384 -84192859808,en,Intersolve-voucher-whatsapp,true,JtQMd,Ella Oliver,15-03-1990,stark,07,demoOption2,21782939278,IUqRezbC,no,Ftn,27-10-2002,33626239 -41011467772,nl,FSP - all attributes,true,RjgQW,Nelle Davidson,15-03-1990,greyjoy,71,demoOption2,49121530730,nNPeHVpi,yes,Dol,27-10-2002,92461809 -25314153207,nl,Intersolve-voucher-whatsapp,true,uuyAj,Tommy Rice,30-11-2000,greyjoy,11,demoOption2,40286932881,HkRpHOVi,no,hCc,16-05-1990,17942499 -49506659313,en,Bank A,false,vATMu,Birdie Stephens,30-11-2000,greyjoy,61,demoOption1,96329200433,ocClSFyY,yes,lAL,27-10-2002,43479423 -96364437306,en,FSP - no attributes,true,XnfNy,Douglas Soto,15-03-1990,stark,69,demoOption3,55014497388,AuBdwnWl,yes,JZP,16-05-1990,84510699 -74425010217,nl,FSP - no attributes,true,IdmaE,Roger Fitzgerald,15-03-1990,greyjoy,14,demoOption2,43222263924,UVcCfqFN,no,ZNK,16-05-1990,59831794 -26118593717,nl,Excel,false,fBLhy,Leroy Austin,15-03-1990,greyjoy,50,demoOption3,14829297891,xqozeMjC,no,GgH,16-05-1990,49088396 -33014580750,en,FSP - all attributes,true,DfeZw,Mitchell McDaniel,15-03-1990,greyjoy,04,demoOption3,50976794938,aDpttxAg,yes,BXL,16-05-1990,44656324 -69493500870,en,Bank A,true,CQbyL,Maurice Sparks,30-11-2000,greyjoy,57,demoOption3,31758877223,HtDdkomo,yes,THn,16-05-1990,23018354 -28727084388,en,FSP - no attributes,false,StuJS,Beulah Figueroa,30-11-2000,lannister,76,demoOption1,14278347161,QmpCJStD,no,PUK,16-05-1990,89560149 -72494078662,en,Bank A,false,LowOo,Pearl Francis,30-11-2000,lannister,59,demoOption3,47134627800,BDieFAia,yes,vHj,16-05-1990,85567994 -45188705826,en,Excel,true,SeBZZ,Steven Morrison,30-11-2000,greyjoy,80,demoOption1,97885597222,CrXPawBW,yes,XdF,16-05-1990,41637648 -65376105092,nl,Intersolve-voucher-whatsapp,true,HTmDN,Tom Dixon,30-11-2000,greyjoy,42,demoOption2,10164684767,isoodnjw,yes,IDD,16-05-1990,55162201 -99614732628,nl,FSP - all attributes,true,CPdOE,Timothy Griffith,30-11-2000,stark,70,demoOption3,19511471154,alUHjFSe,yes,Yvc,27-10-2002,55108115 -90776244533,nl,FSP - no attributes,true,aVGOK,John Sharp,30-11-2000,greyjoy,72,demoOption1,51682830451,SAsViWvy,yes,iQy,16-05-1990,05054681 -46857024937,en,Intersolve-voucher-whatsapp,false,pSJQN,Glenn Stevens,15-03-1990,greyjoy,75,demoOption1,07768093502,lMXEyMBL,no,VOa,16-05-1990,61970173 -18866799564,nl,Bank A,false,mGRbJ,Gerald Curtis,15-03-1990,greyjoy,74,demoOption1,11222438655,VkkyQJTI,yes,LUy,16-05-1990,46575726 -70072014963,nl,Bank A,false,rSmmd,Joe Dunn,30-11-2000,lannister,32,demoOption1,07317394663,YMdRoMAL,yes,lro,27-10-2002,01988182 -05184102674,nl,FSP - no attributes,true,Zpplb,Ethel Watkins,30-11-2000,greyjoy,46,demoOption1,20718118717,QvDOPRIq,no,Kvy,27-10-2002,16266028 -68036525372,nl,Bank A,false,EjxeG,Logan Olson,30-11-2000,lannister,49,demoOption1,47687755275,pwrACExP,no,eeq,16-05-1990,64781008 -57369919915,en,Excel,true,aXVhy,Mabel Cannon,15-03-1990,greyjoy,81,demoOption1,06233328357,XKPjqaZa,yes,zpZ,27-10-2002,60963242 -70949414051,nl,Bank A,true,DNthV,Stanley Griffith,30-11-2000,stark,44,demoOption3,95717579067,obsiCPqk,yes,Qhq,27-10-2002,79507057 -69507117885,nl,FSP - all attributes,false,smQsw,Nellie Reynolds,30-11-2000,greyjoy,05,demoOption1,35849910388,iGZrVJey,no,ECs,27-10-2002,09498025 -97778761402,nl,Intersolve-voucher-whatsapp,true,ZNoXz,Scott Campbell,15-03-1990,stark,57,demoOption1,73584851448,OMYAVvMj,no,JIj,16-05-1990,81854281 -16970365715,nl,Excel,true,XaYOi,Ernest Thornton,30-11-2000,greyjoy,84,demoOption2,34096001403,SFgYSnQg,yes,FZu,27-10-2002,24226295 -07700960418,nl,FSP - no attributes,true,zLgRS,Edna McCoy,30-11-2000,greyjoy,34,demoOption3,92246964500,giMGGjap,yes,FOU,27-10-2002,43778569 -60916158580,en,Bank A,true,YaNzf,Herman Ramirez,30-11-2000,stark,53,demoOption3,24000337937,PocylpEv,yes,EIS,16-05-1990,50502707 -37223412689,nl,FSP - all attributes,false,upqBD,Hunter Pratt,30-11-2000,lannister,61,demoOption2,82417237171,WLxbUXFm,no,Xdy,27-10-2002,75864860 -08384179955,nl,Bank A,true,vFQlU,Philip Adkins,30-11-2000,stark,58,demoOption3,01841604627,yRmMlijX,yes,GzY,27-10-2002,75454402 -38523594321,en,FSP - all attributes,true,ISjsT,Genevieve Floyd,15-03-1990,stark,11,demoOption3,02503956761,lDBhuEod,yes,aLP,27-10-2002,71572578 -63363902654,en,Bank A,true,zjGpR,Harry Chambers,15-03-1990,greyjoy,77,demoOption3,11155945278,crUuqQLj,no,Prr,27-10-2002,14549461 -41405612893,nl,FSP - all attributes,true,QHQah,Janie Quinn,15-03-1990,greyjoy,64,demoOption2,27471937346,ONNzbwSl,yes,Trq,27-10-2002,14665840 -18219629922,nl,Excel,true,ylXRu,Jeffery Daniels,30-11-2000,stark,75,demoOption3,42313972308,CUpKZRaS,no,hWa,27-10-2002,08383028 -43961421323,nl,FSP - no attributes,false,nxHRK,Clarence Duncan,15-03-1990,greyjoy,76,demoOption1,66933592742,jsCNKBBL,no,lYK,27-10-2002,74220525 -69868553576,en,FSP - all attributes,false,GglBc,Johnny Lamb,15-03-1990,stark,28,demoOption3,03608843472,zcfEgSfh,yes,pQw,27-10-2002,72044196 -29866904799,en,Intersolve-voucher-whatsapp,true,VZcLI,Lucille Bowers,30-11-2000,lannister,69,demoOption2,84822333532,BYPcvOEG,yes,zsQ,27-10-2002,48917736 -44249566697,nl,FSP - all attributes,false,GhgEp,Logan Steele,15-03-1990,stark,47,demoOption2,30311759388,ZATMjKOq,no,vmR,16-05-1990,32143591 -73022390333,nl,Excel,true,MiKFN,Anne Welch,30-11-2000,greyjoy,08,demoOption2,60346835634,LSvDCffo,no,CBN,27-10-2002,36879491 -04975775025,en,FSP - no attributes,false,sTSnX,Scott Tran,15-03-1990,greyjoy,97,demoOption3,08967444999,bEsxQxBR,yes,vLu,27-10-2002,38526686 -36020743385,en,Excel,false,eotiC,Lillie Hubbard,30-11-2000,greyjoy,68,demoOption3,91679524440,gxNGtaPu,yes,Hyq,16-05-1990,66780395 -53467224747,en,Bank A,true,EdDRE,Anne Moreno,30-11-2000,lannister,46,demoOption3,96152639945,PskmEhdi,yes,Qgf,27-10-2002,19914535 -32227754213,en,Intersolve-voucher-whatsapp,false,YMVvi,Mittie Little,30-11-2000,lannister,85,demoOption3,86083198963,vHChAngE,no,AQD,27-10-2002,17481224 -71100733957,nl,Bank A,true,IUJbO,Mark Sims,30-11-2000,greyjoy,83,demoOption3,03195474884,JaAoeAkb,yes,RnM,16-05-1990,49811173 -99359423165,nl,Bank A,true,uFpkh,Ernest Hudson,30-11-2000,lannister,33,demoOption3,57705435542,KJSkpHsA,no,YOG,27-10-2002,58403912 -50179500028,nl,Excel,false,SXzHy,Mittie McKenzie,15-03-1990,stark,04,demoOption1,59718385013,DJBJmMTL,yes,jGV,27-10-2002,33077638 -65033949549,nl,Excel,false,lIewI,Raymond Pratt,15-03-1990,stark,18,demoOption2,53918522960,OjNvMdab,yes,UXO,27-10-2002,37108095 -91865902006,nl,Bank A,true,OcTOc,Hulda Barker,15-03-1990,stark,70,demoOption1,83552737094,rMdqdQHb,yes,HvP,27-10-2002,09341257 -72167059697,nl,Intersolve-voucher-whatsapp,true,lAZRS,Hulda Adkins,15-03-1990,greyjoy,74,demoOption1,61229539409,aMurQrpV,yes,bqZ,16-05-1990,25733478 -44654177188,nl,Bank A,false,QLuGZ,Carl Jefferson,15-03-1990,greyjoy,82,demoOption1,91967610321,ZRjVojsO,no,GlA,27-10-2002,50296973 -77471224004,nl,FSP - no attributes,true,urGlF,Derrick Wallace,30-11-2000,stark,34,demoOption1,79873690252,ZQcbWqNW,no,srB,27-10-2002,09874848 -81350723407,en,FSP - all attributes,true,niTuk,Laura Reed,30-11-2000,greyjoy,48,demoOption1,26706612300,ubGElpTk,no,JUF,16-05-1990,71722902 -22723212382,nl,FSP - all attributes,true,NxkiM,Ollie Maldonado,15-03-1990,greyjoy,53,demoOption3,27118235781,pJeqmgxP,yes,ahB,27-10-2002,39315036 -58097392488,en,Excel,true,dxlbt,Bernard Adkins,30-11-2000,greyjoy,62,demoOption1,68851038365,YHGMjBzZ,yes,Jrs,16-05-1990,97414645 -53252038659,en,Excel,true,CmmsV,Christian Greene,15-03-1990,greyjoy,23,demoOption3,59037291483,LvZTgrSz,yes,lXW,27-10-2002,23434632 -21926816499,en,Bank A,false,hZmEH,Ricky Hayes,15-03-1990,greyjoy,55,demoOption1,67795966097,sFhgUHvW,no,Vil,27-10-2002,13966227 -10999287966,en,FSP - no attributes,false,qIGzD,Celia Clark,15-03-1990,greyjoy,25,demoOption2,53679056807,dIRomgbZ,yes,Xug,16-05-1990,64188364 -38150426801,nl,Bank A,false,LYfZG,Lura Perry,30-11-2000,greyjoy,15,demoOption2,47828163057,ZCyUyltw,yes,khK,16-05-1990,85884375 -82295256858,nl,Intersolve-voucher-whatsapp,false,ImvDu,Martha Drake,30-11-2000,stark,12,demoOption1,54396111133,uUxHthAL,yes,pXm,27-10-2002,39453036 -55928553585,nl,FSP - no attributes,false,wRTRn,Mittie Wilkins,30-11-2000,stark,37,demoOption1,38364728622,tbqGQbgD,no,RPt,16-05-1990,96283552 -43318497007,nl,Intersolve-voucher-whatsapp,false,NyZix,Allen Sutton,30-11-2000,stark,40,demoOption1,32778038363,wNkpFJJf,yes,Cia,16-05-1990,96368962 -17277232664,nl,Excel,true,BEbJT,Mattie Gordon,15-03-1990,greyjoy,43,demoOption3,40621769985,XyfBVtRn,no,Lrt,16-05-1990,52987035 -50569701427,nl,Excel,false,LGVfr,Steven Harrison,30-11-2000,lannister,26,demoOption2,71158366567,IMBjaugV,yes,ZQQ,16-05-1990,98557142 -28461471637,en,FSP - all attributes,true,IlVcj,Flora Brock,15-03-1990,lannister,11,demoOption2,06304551748,OSQNwSbU,yes,aEB,27-10-2002,04915957 -29186100102,en,Bank A,true,iDmtK,Inez Goodwin,15-03-1990,stark,93,demoOption1,02824597064,yhnmGERc,no,gvZ,16-05-1990,74112318 -09049890853,nl,FSP - no attributes,false,wHUZL,Oscar Lloyd,30-11-2000,greyjoy,34,demoOption3,40920220421,KxvGbSum,yes,UNc,27-10-2002,70103993 -33368875684,en,FSP - no attributes,false,FxpNz,Randy Dawson,30-11-2000,greyjoy,93,demoOption2,98621232736,YjkjBjOY,no,kzK,16-05-1990,26589715 -60178483429,en,FSP - all attributes,false,pkHbW,Elijah Boone,30-11-2000,lannister,04,demoOption2,41291080994,YFJwgQhl,yes,gZa,16-05-1990,90882852 -52290151230,nl,FSP - all attributes,false,LrjyR,Barry Butler,15-03-1990,lannister,27,demoOption2,57816223707,xTvseXke,no,OMr,16-05-1990,03137898 -33418735722,nl,Intersolve-voucher-whatsapp,true,ZMGzF,Russell Rios,15-03-1990,greyjoy,35,demoOption2,79645599756,ehYvFODz,yes,tXd,27-10-2002,81970187 -98821084773,nl,FSP - all attributes,true,iQSfm,Caleb Tucker,30-11-2000,greyjoy,13,demoOption3,85166937147,TqYYyGIj,no,dJp,16-05-1990,55026212 -18597593772,en,Excel,false,rqSQm,Adeline White,30-11-2000,greyjoy,21,demoOption2,12216482785,hbTaimOe,yes,nLT,27-10-2002,23848695 -16927125121,en,FSP - all attributes,false,fWsRh,Jacob Taylor,30-11-2000,lannister,35,demoOption3,45497425614,XbtGIGZV,yes,KQQ,16-05-1990,53125026 -79786658329,nl,Bank A,true,XLTPP,Winifred Owens,30-11-2000,stark,27,demoOption2,87967585069,malZdJLV,no,LTG,16-05-1990,03601295 -30715655130,nl,Bank A,false,hnOGC,Myrtie Burns,30-11-2000,stark,12,demoOption1,65502620900,UOgeoByR,yes,Xqj,16-05-1990,87328252 -14535116204,nl,FSP - all attributes,false,CDHcW,Oscar Frazier,15-03-1990,greyjoy,85,demoOption2,39241083248,rhUlhmvc,no,llw,27-10-2002,22139557 -48476706235,nl,Bank A,false,UJXIn,Inez Tucker,30-11-2000,lannister,29,demoOption1,31290450120,ibtVPhBi,no,xTt,27-10-2002,27401839 -61019099002,en,FSP - no attributes,true,vHWNB,Etta Bryant,30-11-2000,stark,96,demoOption1,40857566530,WfUgJAiG,yes,zgJ,16-05-1990,51161307 -89755970940,nl,Bank A,false,eBMdD,Craig Barker,30-11-2000,stark,75,demoOption3,99407912476,QihgFcPv,yes,aQU,27-10-2002,33273367 -21262252907,en,FSP - all attributes,true,pHygl,Violet Medina,15-03-1990,lannister,51,demoOption1,09160841685,dGLMPgqA,yes,HkH,16-05-1990,46944897 -46962970098,nl,Bank A,false,VOxPk,Chad Chavez,15-03-1990,greyjoy,60,demoOption2,94614270589,tmtgbwtC,yes,Nzh,27-10-2002,91423047 -26649131444,nl,Bank A,true,FlKNN,Gertrude Richardson,15-03-1990,stark,98,demoOption3,81260558411,iBRuWbkc,no,dvf,27-10-2002,30445330 -83010769916,en,FSP - no attributes,false,LCyzv,Clarence Cruz,15-03-1990,lannister,95,demoOption1,93612667928,vkuKlIxB,no,wnK,27-10-2002,65574538 -22636223490,en,Excel,false,jzEsU,Dominic Silva,30-11-2000,stark,16,demoOption2,89424410293,eprYVFJI,yes,sEZ,16-05-1990,93282604 -74984242844,nl,FSP - no attributes,false,oNKhv,Anne Marshall,15-03-1990,greyjoy,85,demoOption1,90757642669,rBHDTdkD,yes,WyE,16-05-1990,55407872 -24924196872,nl,Excel,true,VVbip,Delia Wilkins,30-11-2000,greyjoy,15,demoOption2,38193117764,eZkOeFBx,yes,ePT,27-10-2002,61242587 -05711702996,en,Intersolve-voucher-whatsapp,false,YEeuu,Gertrude Anderson,15-03-1990,lannister,47,demoOption1,00931780555,ZkKqNyeV,yes,ngX,27-10-2002,09547769 -63623134494,nl,Intersolve-voucher-whatsapp,false,DsGOk,Lida Thomas,15-03-1990,stark,79,demoOption1,29226784304,QVFwTPLT,no,VmJ,27-10-2002,72565149 -55908876429,en,Bank A,false,YokFY,Frank Salazar,15-03-1990,stark,08,demoOption2,72292273041,jqofcMxx,no,wMN,16-05-1990,52688993 -77320522890,en,FSP - no attributes,false,eouYv,Emilie Martin,30-11-2000,lannister,91,demoOption2,00866505112,mDbYdiYy,no,Xwh,16-05-1990,97029866 -13815770082,en,FSP - all attributes,false,cnRYY,Joseph Zimmerman,15-03-1990,lannister,03,demoOption3,89211043576,kUfNajEa,no,RfF,27-10-2002,17756385 -85411743863,nl,FSP - no attributes,false,XmRUB,Bill Dennis,15-03-1990,stark,52,demoOption2,95404424333,OXTvZMtS,no,Hkw,27-10-2002,12403171 -64182650315,en,FSP - all attributes,false,FyJtq,Harry Luna,30-11-2000,greyjoy,85,demoOption1,66522358278,XmfYJCki,yes,wUZ,27-10-2002,30566934 -77155372082,en,Intersolve-voucher-whatsapp,false,vordV,Jared Patrick,15-03-1990,lannister,37,demoOption2,82983315162,CaMIJAde,no,zSK,16-05-1990,61153768 -38723277025,en,FSP - no attributes,false,ViHcO,Barbara Nelson,15-03-1990,greyjoy,45,demoOption2,64606097945,HVsgxzmB,yes,MAj,16-05-1990,87184628 -44712213744,nl,Bank A,true,NUsbB,Teresa Morton,30-11-2000,greyjoy,97,demoOption1,44659693458,bUssMQjx,yes,cnm,16-05-1990,65244400 -78329797404,nl,FSP - all attributes,true,PZbpo,Viola Graham,15-03-1990,lannister,18,demoOption1,43814429656,TBcKLIAj,yes,mCX,27-10-2002,76117849 -29880613471,en,FSP - no attributes,false,oALyt,Mollie Parsons,30-11-2000,greyjoy,26,demoOption3,10246098514,ZTLNGEIg,no,BrA,16-05-1990,53538994 -21409248667,nl,Intersolve-voucher-whatsapp,true,Bstyb,Alberta Burgess,15-03-1990,greyjoy,71,demoOption3,06860766593,gIdBfhOV,yes,eqr,27-10-2002,36154786 -32850744703,en,FSP - no attributes,false,HMNxO,Joel Underwood,30-11-2000,lannister,17,demoOption3,02856638758,EFPaEEbo,no,qIm,16-05-1990,07792893 -88280939643,nl,FSP - no attributes,false,owGGI,Myrtie Graham,15-03-1990,greyjoy,06,demoOption3,06742434785,nMykviRL,no,JxT,27-10-2002,19270186 -70192388760,nl,Excel,false,fyJiA,Barry Doyle,30-11-2000,stark,16,demoOption2,15341061874,GmqWtQmm,yes,bYd,27-10-2002,99351687 -28864517633,nl,FSP - all attributes,false,AAymx,Antonio Chandler,30-11-2000,greyjoy,07,demoOption2,02691664017,NdYSjfVf,no,qRJ,27-10-2002,59349198 -45945667655,en,Bank A,false,qmBzY,Peter Neal,15-03-1990,stark,50,demoOption3,44756650871,xKrKGVaA,no,lxV,16-05-1990,18714066 -45705301796,en,Excel,true,GAaMK,Lela Warner,30-11-2000,stark,11,demoOption2,21419598673,COSTNwPG,no,Sho,16-05-1990,38630903 -52961770260,nl,Excel,true,mmqEW,Dean Harrington,30-11-2000,lannister,70,demoOption2,18075991611,wqWBuDNP,no,ryf,27-10-2002,75338289 -95003213912,en,FSP - no attributes,true,KtiiO,Agnes Coleman,30-11-2000,stark,11,demoOption2,54076245946,qZKONExL,no,xQZ,27-10-2002,68730398 -25929854446,nl,Bank A,true,CnCGm,Alma Rice,30-11-2000,stark,82,demoOption1,58461460024,xAnZXbMv,yes,RPz,16-05-1990,73521364 -58935400449,en,Intersolve-voucher-whatsapp,false,ZYxqs,Mayme Vargas,30-11-2000,stark,82,demoOption1,31297202928,IoExnkdv,no,Jjh,16-05-1990,96484299 -60107043062,nl,FSP - all attributes,false,lRGJt,Arthur Kennedy,15-03-1990,greyjoy,02,demoOption3,96388231837,VXwvATVP,no,RDV,27-10-2002,96601961 -66486649213,nl,FSP - no attributes,true,rWqqx,Maggie Jordan,15-03-1990,greyjoy,44,demoOption3,56581563661,qcysCLYI,yes,iit,16-05-1990,36647372 -57838088272,nl,FSP - all attributes,false,YfumQ,Leila Walton,15-03-1990,stark,17,demoOption3,23836781502,kDXtrkov,no,PmM,27-10-2002,33966036 -43087998544,en,Excel,true,qMrfy,Travis Gross,15-03-1990,stark,09,demoOption1,11625167088,bOyBKBuW,no,fhd,16-05-1990,08362432 -12993718890,en,Intersolve-voucher-whatsapp,true,MzbFj,Cody Morton,30-11-2000,lannister,64,demoOption1,30123853157,hCyexsBP,yes,yyb,27-10-2002,08506796 -86923410303,en,Excel,false,HOwYx,Dorothy Rhodes,15-03-1990,lannister,09,demoOption2,42876712339,RzFjujHh,no,Cvg,16-05-1990,41902274 -50303816707,en,FSP - no attributes,false,woMaA,Tommy Lane,30-11-2000,lannister,45,demoOption3,91826830787,mjHhUdvY,no,EFP,27-10-2002,49337356 -10539817591,en,FSP - no attributes,false,PpZlt,Clyde Moreno,30-11-2000,lannister,13,demoOption1,21867568530,JEtGOkqs,yes,Bnb,27-10-2002,11314917 -31086244747,en,Bank A,true,bZteO,Sean Richardson,15-03-1990,lannister,09,demoOption1,32969543264,tWSZBbae,yes,AIH,16-05-1990,97857488 -65337118254,nl,FSP - no attributes,false,tZWmL,Luke Shaw,30-11-2000,lannister,86,demoOption1,83669040622,ntkCbCdY,no,RbM,27-10-2002,32242738 -86035803916,nl,Excel,true,EClaB,Adelaide Cruz,15-03-1990,greyjoy,72,demoOption1,89500363815,wdyuLxcs,no,xEM,27-10-2002,84025373 -47023936967,nl,Intersolve-voucher-whatsapp,false,KBzOl,Blake Sparks,30-11-2000,lannister,46,demoOption3,47249635306,rawffTcU,no,qlU,27-10-2002,65597747 -84876668941,en,Excel,false,IkDFf,Ruth Parks,30-11-2000,lannister,60,demoOption1,03842765858,aIBSppJT,yes,Tqg,27-10-2002,53872452 -61466224437,en,Intersolve-voucher-whatsapp,true,KidkM,Ella George,30-11-2000,greyjoy,22,demoOption1,01693664723,kfggptXL,no,wbL,27-10-2002,10007872 -61623148853,nl,Intersolve-voucher-whatsapp,true,QwuKI,Essie Morgan,15-03-1990,stark,15,demoOption3,56628515257,XZldwnHv,no,lsb,16-05-1990,37007275 -37252431386,en,FSP - no attributes,false,slYif,Birdie Hampton,15-03-1990,stark,50,demoOption1,12020798579,JknrQxzP,no,Ydr,27-10-2002,73635745 -65519843521,en,Excel,true,IAUBz,Charlotte Campbell,30-11-2000,greyjoy,19,demoOption2,82966224017,gtEVsfNz,no,JMZ,16-05-1990,75173212 -99325424774,nl,FSP - no attributes,false,wVhhY,Henry Benson,30-11-2000,greyjoy,22,demoOption1,73836299834,uTAmgxYH,yes,ReW,16-05-1990,89763628 -45099540735,nl,Intersolve-voucher-whatsapp,true,wWGnd,Bertie Cole,15-03-1990,stark,07,demoOption3,55966306230,OVsdcaIh,no,eaC,16-05-1990,07651807 -36239964167,en,Excel,true,jsSVW,William Jensen,30-11-2000,greyjoy,42,demoOption1,23639692492,lVMkmFxL,yes,Pbw,27-10-2002,87080389 -86834466376,en,Excel,false,YHdXH,Mina Bradley,30-11-2000,greyjoy,50,demoOption3,97742523290,WkdQeZgS,yes,xQM,16-05-1990,18189114 -31746913464,nl,Intersolve-voucher-whatsapp,true,lgUga,Floyd Baldwin,15-03-1990,lannister,41,demoOption2,38630943219,viGVqDxE,yes,ctv,27-10-2002,95567885 -31502474837,nl,Excel,false,pKxlp,Richard Santiago,15-03-1990,lannister,23,demoOption3,58699933239,ULEbBJup,yes,hZa,16-05-1990,28749710 -24905476742,nl,Intersolve-voucher-whatsapp,true,UZtSZ,Rebecca Pierce,30-11-2000,greyjoy,54,demoOption1,54468691221,jOeDWgUl,yes,nIV,27-10-2002,98748769 -60113540308,en,FSP - no attributes,true,kuGzM,Devin Rodriguez,30-11-2000,stark,68,demoOption2,79796283693,xXodoCwp,no,VLd,27-10-2002,71143252 -36980793229,en,Excel,true,biwQB,Stella Paul,15-03-1990,stark,28,demoOption1,36960900764,ONGyLzaw,yes,Dwo,27-10-2002,12607614 -35769362349,nl,FSP - all attributes,false,yzwNk,Josephine Norton,30-11-2000,lannister,44,demoOption1,37766906049,VhmMADjo,no,sQx,16-05-1990,56393470 -63270004795,nl,FSP - no attributes,false,FaTbB,Ricky Evans,15-03-1990,stark,64,demoOption1,79873960394,kYipgTAE,yes,hoa,27-10-2002,05315092 -51965322293,nl,Bank A,true,amDkq,Lilly Gibson,30-11-2000,stark,86,demoOption3,36957181555,ujLhtrpx,yes,Rtm,27-10-2002,13566775 -89068652070,en,Intersolve-voucher-whatsapp,false,uzmmR,Corey Rodgers,15-03-1990,lannister,43,demoOption1,21608144322,ilnjFArS,no,zpz,16-05-1990,09422767 -06497125219,nl,Excel,true,GyvFa,Stella Hansen,30-11-2000,greyjoy,46,demoOption1,33390301304,ACKnBHTr,yes,PuM,27-10-2002,50535197 -71887903922,en,Bank A,true,GFcMI,Mason Gibbs,15-03-1990,lannister,59,demoOption3,27981802655,hswZZZmA,no,xZT,16-05-1990,45968118 -53184327504,en,Bank A,true,AGiGw,Beulah Powell,15-03-1990,stark,11,demoOption3,06641345077,UtQTOZMq,yes,AJd,27-10-2002,89002672 -74505035547,nl,Excel,true,GOFfJ,Elnora Ward,15-03-1990,lannister,87,demoOption1,55107958385,qJoUFJYV,yes,rXL,16-05-1990,12863014 -80470209197,en,Bank A,false,ODQZY,Vernon Keller,15-03-1990,greyjoy,45,demoOption2,29025446396,EXljJCGr,no,cNJ,27-10-2002,49940322 -65263081540,en,Intersolve-voucher-whatsapp,false,PZQio,Jennie Hawkins,30-11-2000,lannister,25,demoOption2,81315066560,NdDSZzsV,no,VET,27-10-2002,51077433 -17265135232,nl,Intersolve-voucher-whatsapp,true,fLLYh,Johanna McCarthy,15-03-1990,lannister,23,demoOption2,67662815213,oDAEfJCP,no,mFx,16-05-1990,55990685 -75724630924,nl,Bank A,false,ioofO,Nathaniel Silva,15-03-1990,lannister,94,demoOption2,36471826143,rRhovEIn,yes,aCc,16-05-1990,38712413 -63629369536,en,FSP - no attributes,false,RuXrN,Rosa Newton,30-11-2000,greyjoy,34,demoOption3,46612743751,umjtFRon,no,ZhS,16-05-1990,53363007 -57258868332,en,FSP - no attributes,true,svvfV,Paul Bowman,15-03-1990,lannister,70,demoOption3,81139595022,DToDIqiT,yes,GtT,27-10-2002,47937690 -07839623151,en,FSP - no attributes,false,Avnci,Catherine Vasquez,30-11-2000,greyjoy,87,demoOption1,31757643641,wKQsBnCt,yes,HHf,27-10-2002,99239566 -99664118682,en,Intersolve-voucher-whatsapp,true,gXAon,Lela Johnson,15-03-1990,stark,59,demoOption1,87194544430,PwaVSHiO,yes,mjq,27-10-2002,52940784 -06734182409,en,Intersolve-voucher-whatsapp,false,CsINL,Daisy Holt,15-03-1990,lannister,09,demoOption3,57328208033,pJNKTnqe,yes,HyG,16-05-1990,21567289 -03913456460,nl,Bank A,false,YBDqS,Jay Cruz,15-03-1990,stark,14,demoOption1,08961753234,ByUwxgyf,no,CTv,27-10-2002,38146429 -05297355640,nl,FSP - all attributes,true,NnCtS,Carrie Hoffman,15-03-1990,lannister,93,demoOption2,41570749237,VhsqQHqZ,no,pEb,16-05-1990,01188154 -30127436968,en,Intersolve-voucher-whatsapp,true,XiqTZ,Alberta Padilla,15-03-1990,lannister,24,demoOption3,26405484296,BaBcVeBq,no,GYa,27-10-2002,44398238 -78321833583,en,Bank A,true,TjwaW,Kate Clayton,30-11-2000,stark,80,demoOption1,43039306189,bqBjjOol,no,Sql,16-05-1990,68034638 -13284750053,nl,Excel,false,xubtn,Emma Wright,15-03-1990,lannister,92,demoOption1,52917970663,SmrWjGCc,no,Bse,27-10-2002,88335242 -30412640563,nl,Intersolve-voucher-whatsapp,true,JQkpZ,Mathilda Walton,15-03-1990,greyjoy,28,demoOption2,77583296690,dfyeBiio,yes,SEN,27-10-2002,58529084 -22880365881,en,Bank A,false,LylaM,Harry Newton,15-03-1990,lannister,17,demoOption3,40046695691,pHoDDbBk,no,lQc,16-05-1990,91750194 -22771756122,en,Bank A,true,MSpSr,Henrietta Wells,30-11-2000,lannister,68,demoOption3,70013150338,jplkxCXL,yes,Bgz,27-10-2002,65682640 -40616158925,nl,Excel,false,HkwNU,Bernard Cannon,15-03-1990,stark,27,demoOption2,74680802840,TcvWHkvL,no,JVI,16-05-1990,74902542 -56987183962,en,Intersolve-voucher-whatsapp,true,UrIlt,Gene Bryan,15-03-1990,lannister,98,demoOption3,29182263225,dCoVoXvq,no,snD,16-05-1990,05474595 -57423372658,nl,FSP - no attributes,false,lCdmc,Irene Harris,15-03-1990,lannister,96,demoOption1,74143381568,HggCzHuw,no,aDu,16-05-1990,85958498 -41648266493,en,Excel,false,pMixl,Leah Peters,15-03-1990,lannister,93,demoOption2,74116979113,kRqyBoUP,no,yup,27-10-2002,54142876 -99949588636,en,FSP - all attributes,true,kdepF,Marc Webster,30-11-2000,lannister,38,demoOption2,15283517175,FpAKkCfB,no,ebr,16-05-1990,80289235 -26092552640,nl,FSP - all attributes,true,ghCuF,Ian Ramos,30-11-2000,stark,94,demoOption2,68761323792,QGjsyflu,yes,gGK,27-10-2002,66552188 -90525384128,nl,Excel,true,tXKpa,Gussie Munoz,30-11-2000,greyjoy,83,demoOption1,66868273667,hocPVLot,yes,qKP,16-05-1990,01242470 -79163180236,en,Excel,false,WFLgU,Eula Bennett,30-11-2000,lannister,60,demoOption1,28340293265,AAzZybXN,yes,Kes,16-05-1990,73707890 -54860399722,en,Excel,true,vxDpy,Brian Sims,15-03-1990,greyjoy,83,demoOption3,60230313148,sDFuKdXN,yes,WAM,27-10-2002,97886475 -41559523368,en,Excel,true,hhbvD,Warren Reyes,30-11-2000,stark,54,demoOption1,62388882343,yNKijkvP,yes,alB,27-10-2002,79325738 -61720145813,nl,FSP - no attributes,true,DmPrC,Nina Schneider,30-11-2000,lannister,66,demoOption3,61142953273,JfJAfqbr,yes,ZaP,27-10-2002,21939206 -26668851536,en,Intersolve-voucher-whatsapp,true,hgarq,Annie Guerrero,15-03-1990,lannister,55,demoOption3,20078223631,kzKejjGT,yes,jgj,27-10-2002,10280926 -27306769773,nl,Intersolve-voucher-whatsapp,false,YMmRE,Adele Gonzalez,30-11-2000,greyjoy,08,demoOption2,17367299060,RIjsmhNi,yes,BOO,16-05-1990,34494528 -91863486189,en,Bank A,true,DiOdC,Olivia Dixon,30-11-2000,lannister,12,demoOption2,45597017443,qzmGbRnd,no,Ian,27-10-2002,15550598 -26098503114,nl,FSP - no attributes,true,HgBpz,Harvey Taylor,30-11-2000,stark,30,demoOption3,81261251844,lDDSTdrB,yes,ZBz,27-10-2002,93583432 -50208286680,nl,FSP - no attributes,false,Bshxe,Sophie Elliott,30-11-2000,lannister,78,demoOption3,68243379836,oltgQCPH,yes,uUe,27-10-2002,92743039 -46213597609,nl,Excel,false,uYiuX,Mabelle Page,30-11-2000,lannister,42,demoOption2,30765367626,NrzpKDtu,yes,CFa,16-05-1990,68266791 -89243270481,en,FSP - all attributes,false,VRfNs,Kathryn Moore,30-11-2000,stark,49,demoOption2,82888468381,htDCGGxi,yes,Egg,16-05-1990,94251970 -72609971227,nl,Excel,false,yhwKd,Mitchell Oliver,30-11-2000,greyjoy,55,demoOption1,83949602481,osYwsIsF,no,WAX,16-05-1990,42718553 -49644752882,nl,Bank A,true,jhMGn,Ricardo Maldonado,15-03-1990,stark,42,demoOption2,44888037001,PvXJPMQZ,yes,jik,16-05-1990,11446640 -52508878377,en,Bank A,false,JciQw,Rose Flores,30-11-2000,greyjoy,13,demoOption1,21389927218,VlSCBiCv,no,FFY,16-05-1990,62410334 -11966629990,en,FSP - no attributes,true,eTEeZ,Louise Welch,15-03-1990,stark,52,demoOption2,02024781313,SLczsgAQ,no,hSs,27-10-2002,66261988 -15204741815,en,Bank A,false,jQzMZ,John Saunders,15-03-1990,lannister,76,demoOption1,59188016321,eDTweCcC,no,uKX,27-10-2002,38758300 -26761436086,nl,FSP - all attributes,false,mOlDX,Joshua Johnston,30-11-2000,lannister,70,demoOption3,76473345974,tthFlITx,yes,gNf,27-10-2002,25365798 -69303524019,nl,Intersolve-voucher-whatsapp,true,FJFpw,Brian Patterson,15-03-1990,greyjoy,55,demoOption2,14945351138,DTiFuJlw,yes,fxP,16-05-1990,35056332 -96915009530,en,Intersolve-voucher-whatsapp,false,rXuxb,Marguerite Guerrero,15-03-1990,lannister,56,demoOption2,09381260369,gWyjaQoY,yes,hIp,27-10-2002,51256224 -64306622154,nl,FSP - no attributes,false,odKXH,Estella Sutton,30-11-2000,lannister,73,demoOption1,30594176049,djqdEOtg,yes,Wrd,16-05-1990,11380010 -16714023536,nl,Excel,true,lOJPh,Maude Ramirez,15-03-1990,greyjoy,50,demoOption1,28174453126,xzNcHnVr,yes,HKQ,16-05-1990,41886498 -23208843907,en,FSP - all attributes,false,CjWjB,Ann Barnett,15-03-1990,stark,92,demoOption1,42189903873,HIIIsYSI,yes,SZt,27-10-2002,52945127 -85282596473,en,Intersolve-voucher-whatsapp,true,rahvn,Ricky Hernandez,15-03-1990,lannister,28,demoOption3,35496015596,kHXteDZp,no,VEy,27-10-2002,97313596 -89172977606,en,Intersolve-voucher-whatsapp,true,GLClb,Ida Pope,30-11-2000,greyjoy,31,demoOption3,98679064942,ejanzynQ,yes,Gqo,27-10-2002,99177665 -24414904436,nl,Excel,true,ypLAf,Lois Long,15-03-1990,lannister,73,demoOption2,52475279158,ckLjmTuG,yes,Ehq,16-05-1990,63301679 -72992742863,nl,Bank A,false,AVfKP,Bill Carr,30-11-2000,greyjoy,31,demoOption1,39332915376,gEFwVapG,no,ILB,16-05-1990,87160182 -04039025838,en,FSP - all attributes,true,OplhQ,Lillie McKinney,30-11-2000,greyjoy,02,demoOption3,75457967556,gqnEAuxU,yes,TYy,16-05-1990,56625867 -94553968212,nl,Excel,false,UADFY,Garrett Maldonado,30-11-2000,greyjoy,31,demoOption3,86508845369,izsIkHfM,yes,pNT,16-05-1990,79203938 -61908145985,en,Intersolve-voucher-whatsapp,true,cRZWb,Belle Hines,15-03-1990,greyjoy,65,demoOption3,43065541024,oPWwQpZl,yes,SLd,27-10-2002,82401614 -91341722496,en,Intersolve-voucher-whatsapp,true,oyxhr,Fred Diaz,15-03-1990,lannister,73,demoOption3,96036954407,aUyjECDY,no,Doi,27-10-2002,13792910 -46635590798,nl,FSP - all attributes,false,UJwMS,Lula Blake,30-11-2000,stark,70,demoOption2,97958187838,vSwzPLkK,no,JuW,16-05-1990,65732079 -85247445897,en,FSP - all attributes,false,RiFnS,Tommy Paul,30-11-2000,greyjoy,88,demoOption3,96523328715,TCInuYnQ,no,kHK,16-05-1990,47965514 -70293141934,nl,FSP - all attributes,false,UQXLZ,Mae Pope,30-11-2000,lannister,26,demoOption3,27805434504,ezAbVYUp,no,Ufx,27-10-2002,69459876 -89772772766,nl,Excel,false,DGbUB,Clifford Ferguson,30-11-2000,lannister,99,demoOption2,17888331988,qDxlzkvk,no,WGq,27-10-2002,29826125 -84329687232,nl,FSP - no attributes,false,hEUqe,Alexander Cole,15-03-1990,greyjoy,48,demoOption2,28055201491,rZpHhksr,no,FyS,16-05-1990,92932538 -81683330390,nl,Intersolve-voucher-whatsapp,false,fhXqW,Linnie Alvarez,15-03-1990,stark,76,demoOption2,17203927857,QWaKjEyX,no,lys,16-05-1990,78013717 -62696741350,nl,FSP - no attributes,true,WDPei,Henry Peters,30-11-2000,stark,10,demoOption3,39179558952,ZYhTdfub,yes,TFc,16-05-1990,13335247 -51548730306,nl,Intersolve-voucher-whatsapp,false,yxhXT,Cora Hogan,15-03-1990,stark,46,demoOption2,28330351573,WCsiaAkF,yes,HLv,27-10-2002,74325594 -02808750475,nl,FSP - no attributes,false,GNrwY,Alma Sandoval,15-03-1990,stark,29,demoOption2,61647033262,ybcgoizc,no,UVf,16-05-1990,30746583 -57949608201,en,Excel,false,KaIFn,Bess Mason,15-03-1990,lannister,07,demoOption2,07486879373,AMtBaYtq,yes,hnW,16-05-1990,45810678 -84746656417,en,Intersolve-voucher-whatsapp,false,fdOhT,Frederick Pratt,30-11-2000,lannister,98,demoOption3,99258964983,hrOXBYaG,yes,MoG,27-10-2002,64961703 -05839569016,en,Intersolve-voucher-whatsapp,true,BBNIZ,Lilly Reeves,30-11-2000,stark,32,demoOption2,97557278508,fLApbxKo,yes,qGz,27-10-2002,04607575 -53599405543,en,FSP - no attributes,true,LtLED,Alejandro Marshall,30-11-2000,greyjoy,25,demoOption1,68961665708,vZhNGHON,yes,vTK,27-10-2002,25038827 -62882687019,en,Bank A,false,nqjcU,Delia Wise,30-11-2000,stark,09,demoOption3,45528287575,eixEKtmY,yes,PsT,16-05-1990,85791099 -65282353578,nl,FSP - no attributes,true,fpvxQ,Jay Larson,30-11-2000,stark,91,demoOption2,15382721825,mZAdmgVB,yes,Woy,16-05-1990,57107304 -34630230028,en,FSP - all attributes,true,AXiev,Clyde Keller,15-03-1990,lannister,15,demoOption3,98332439448,MfIitTEo,no,yxR,27-10-2002,61609680 -38338205808,en,Bank A,false,qBSvf,Eva Snyder,15-03-1990,stark,07,demoOption1,58345798755,yAerLsof,yes,UPU,16-05-1990,72770123 -27043987635,nl,FSP - all attributes,false,LMjGO,Rachel Rose,30-11-2000,lannister,65,demoOption2,82422236592,hGYRjHsh,no,Sri,16-05-1990,31969448 -62970111546,nl,Bank A,false,DuYir,Lura Jefferson,15-03-1990,lannister,45,demoOption3,40735513043,mYmfbyMd,no,ZNi,16-05-1990,75386852 -28481264448,nl,Excel,true,zslBV,Eunice Turner,30-11-2000,lannister,96,demoOption3,30274111461,NmdRiAdN,yes,VYw,27-10-2002,81522967 -55798117942,en,Bank A,false,UnHmy,Edna Daniel,30-11-2000,lannister,53,demoOption1,12931989415,bmDucJEY,no,Vrf,27-10-2002,12609447 -29649622558,nl,Bank A,false,Qadxq,Cory Chandler,30-11-2000,lannister,06,demoOption2,44081692175,osJWguQu,no,dkZ,27-10-2002,27631253 -95298559445,nl,Intersolve-voucher-whatsapp,true,aBNdy,Ricky Sharp,30-11-2000,greyjoy,62,demoOption2,22410751279,scTmvZGi,no,vma,27-10-2002,93672761 -79933020354,en,Bank A,false,EikpT,Charlotte Newman,15-03-1990,stark,40,demoOption3,08721859258,QnoCjJdu,no,CRC,16-05-1990,55488315 -94142344952,nl,FSP - all attributes,false,bqqSA,John Peters,15-03-1990,greyjoy,00,demoOption1,05268774619,AvejvhdH,yes,vVP,16-05-1990,77028667 -62087284134,en,FSP - no attributes,true,BdJCU,Aaron Warner,30-11-2000,lannister,63,demoOption3,68008273497,SoIfkOoy,yes,zVK,16-05-1990,62649380 -01938662886,nl,Excel,false,KVvOp,Beulah Steele,30-11-2000,greyjoy,32,demoOption2,68009188128,iSGlsVsa,yes,qAU,27-10-2002,44485898 -99122327667,nl,FSP - no attributes,false,bnrPg,Virgie Evans,15-03-1990,stark,03,demoOption1,52538602580,TDLNTvMk,no,XMr,27-10-2002,70235505 -12401274449,en,Bank A,false,GnaBk,Emilie Brewer,30-11-2000,lannister,45,demoOption2,49129757570,urOUeLwX,no,ocR,16-05-1990,13167977 -61995676537,nl,Excel,true,CVsqq,Kevin Pittman,15-03-1990,lannister,92,demoOption2,66216106996,kmCxnwMq,yes,guA,27-10-2002,18267781 -17229896674,nl,FSP - all attributes,false,xCbwN,Eunice Ray,15-03-1990,stark,72,demoOption3,83247648564,gobPKmCh,no,BKR,16-05-1990,53405489 -20604955953,nl,FSP - no attributes,false,RltKZ,Derek Gutierrez,30-11-2000,greyjoy,63,demoOption3,10218073180,aLzsFYqv,no,VOL,16-05-1990,74144475 -66112298340,nl,Excel,false,rIoOz,Max Bailey,30-11-2000,stark,32,demoOption2,72258020595,hLUWkcmA,yes,bGP,16-05-1990,76683438 -74145446970,en,Excel,false,Vemcm,Alvin Vaughn,15-03-1990,stark,73,demoOption2,40525079401,zzvAsrNn,no,AQs,27-10-2002,99707420 -84141371093,en,Intersolve-voucher-whatsapp,false,MJGgL,Leonard Banks,30-11-2000,lannister,59,demoOption1,75234248479,GKPFSXNR,no,MQW,27-10-2002,75852287 -28778278736,en,FSP - all attributes,false,ISlHQ,Carlos Crawford,30-11-2000,stark,79,demoOption1,86100290325,YgUqdJvM,no,Xnh,27-10-2002,82595017 -97003060150,nl,Intersolve-voucher-whatsapp,false,iyDjU,Ella Figueroa,15-03-1990,lannister,53,demoOption2,71811635695,qyYReUwv,yes,Ifx,27-10-2002,74549707 -13248806707,en,Intersolve-voucher-whatsapp,true,tLTzZ,Amelia Richardson,30-11-2000,greyjoy,09,demoOption2,68633515850,ysYRopoT,yes,usm,16-05-1990,64846660 -88151683423,nl,FSP - no attributes,true,WQwFm,Effie Kelly,15-03-1990,lannister,32,demoOption1,41983867072,kxxZuQup,no,JSY,27-10-2002,43272626 -03965081935,nl,FSP - all attributes,true,AQkZk,Kevin McKinney,30-11-2000,greyjoy,17,demoOption3,81390395574,iNkSHpwC,no,RHO,16-05-1990,36113665 -41295310874,en,Excel,false,CKYvN,Jeffery Mason,30-11-2000,lannister,69,demoOption1,92926917519,noSUGaat,no,YtD,16-05-1990,65120102 -99185589862,en,FSP - no attributes,true,AbxHm,Leila White,30-11-2000,greyjoy,14,demoOption3,81470593031,jQRJTvoh,no,Zbf,16-05-1990,96192113 -08701430845,en,Intersolve-voucher-whatsapp,false,oHuoR,Alta Shelton,15-03-1990,stark,65,demoOption2,75268678119,WgmkpELB,yes,Tdu,27-10-2002,61704390 -14626866355,nl,Bank A,true,PzlNu,Rena May,30-11-2000,greyjoy,03,demoOption1,14091368268,NXaqpUnx,no,hjt,27-10-2002,38117299 -56788309267,en,Intersolve-voucher-whatsapp,false,Lgwqi,Harriett Beck,15-03-1990,greyjoy,88,demoOption1,02434174492,oyUgKGQj,yes,GNa,27-10-2002,35268120 -47189850188,en,Excel,true,DsMPN,Wesley Erickson,15-03-1990,greyjoy,61,demoOption3,41892827372,ARAWgvop,no,vIK,27-10-2002,56922055 -84993783013,en,Excel,true,ElWRa,Mike Little,30-11-2000,greyjoy,50,demoOption3,48465226969,hpkznwtc,no,osp,27-10-2002,62064950 -91259576203,nl,FSP - no attributes,true,TeEJr,Maria Delgado,15-03-1990,greyjoy,51,demoOption2,30947098892,gSvSrnyb,no,BMi,16-05-1990,60275422 -08319336938,en,FSP - all attributes,true,rXxZQ,Ida Osborne,30-11-2000,lannister,79,demoOption3,95324004683,eUmOxJZs,no,euz,27-10-2002,40890099 -41358647563,en,FSP - all attributes,true,KliSM,Marvin Lawrence,30-11-2000,greyjoy,32,demoOption3,97805346734,KyDZzpMZ,yes,BCH,16-05-1990,79048854 -17612532912,nl,Intersolve-voucher-whatsapp,false,rxVOg,Shane Joseph,15-03-1990,greyjoy,99,demoOption2,94749532957,GcGCqenC,yes,OMR,27-10-2002,16666901 -13740091546,en,Bank A,true,lRkqc,Max Delgado,30-11-2000,lannister,90,demoOption2,88541182046,MdgXKkNz,yes,pCI,27-10-2002,41869049 -75147177161,nl,FSP - all attributes,false,iGuQr,Danny Cummings,30-11-2000,greyjoy,44,demoOption1,08291740152,zQSjUqzO,yes,vwg,27-10-2002,07854282 -78637905780,nl,Excel,false,LiZnI,Willie Bryant,30-11-2000,stark,76,demoOption2,35101200508,BkxunDnn,yes,UBv,27-10-2002,56394614 -29261595674,nl,FSP - no attributes,false,WtdCY,Bruce Diaz,15-03-1990,lannister,11,demoOption2,50025960327,EEUzofwr,yes,ztl,27-10-2002,62515299 -32059524948,en,Bank A,true,gKoBX,Joel Greer,15-03-1990,stark,71,demoOption1,52826386789,RajsNphQ,no,oku,27-10-2002,99046651 -10214738893,nl,Intersolve-voucher-whatsapp,true,iTeXF,Lillian Pratt,15-03-1990,lannister,13,demoOption2,02777321247,htAIERIx,yes,NLe,16-05-1990,77869517 -13197872815,en,Intersolve-voucher-whatsapp,false,vGVAh,Linnie Warren,30-11-2000,greyjoy,63,demoOption1,79893515000,dJJOcYui,yes,DDX,16-05-1990,44112783 -48116638365,en,FSP - no attributes,true,gEEkH,Seth McCoy,15-03-1990,lannister,67,demoOption1,89233941048,mJPyReDV,yes,yZR,27-10-2002,46543031 -05018786704,en,FSP - all attributes,true,xnRkt,Todd Hodges,30-11-2000,greyjoy,92,demoOption3,63698851833,vLkZqWgL,no,aJF,16-05-1990,23303051 -97595659330,en,FSP - no attributes,false,IBnwt,Lou Todd,30-11-2000,lannister,70,demoOption2,00013289276,ScqDXULK,no,OVm,27-10-2002,82129878 -79300524440,en,Bank A,false,UBowD,Lela Burns,15-03-1990,lannister,08,demoOption2,77110105563,tkadDkAL,no,iTr,27-10-2002,76109956 -60715825077,en,Intersolve-voucher-whatsapp,true,TWrwK,Francis Thompson,15-03-1990,greyjoy,34,demoOption2,34304990921,cxDQaNRC,no,cVX,16-05-1990,37108526 -22273422184,nl,FSP - all attributes,true,jhDez,Minnie Mack,30-11-2000,greyjoy,94,demoOption1,97026381390,galVluLe,yes,NVk,16-05-1990,24627131 -59291109121,nl,Bank A,true,XPOUm,Nina Frazier,30-11-2000,lannister,80,demoOption2,65011149708,WBNQmFxr,no,QRw,27-10-2002,60728969 -74233973565,en,Intersolve-voucher-whatsapp,false,KOsel,Ian Townsend,15-03-1990,greyjoy,26,demoOption3,19664125323,bZBykCZn,yes,idQ,27-10-2002,51602042 -96206560913,en,Bank A,true,SVIyV,Ann Boone,30-11-2000,greyjoy,57,demoOption2,08211146428,bNPHCSjB,yes,msu,27-10-2002,60984325 -57601351245,nl,Bank A,true,SfzPA,Vernon Taylor,30-11-2000,stark,64,demoOption2,30727650741,VALiIOzq,no,ACw,16-05-1990,54423823 -59444513541,nl,Intersolve-voucher-whatsapp,false,VBNlL,Inez Glover,30-11-2000,lannister,40,demoOption1,09957825082,mKxAVpdu,no,HmW,27-10-2002,62255423 -64017373149,en,Bank A,true,LhLYR,Victoria Wallace,30-11-2000,greyjoy,32,demoOption1,53256420784,MxOiHpef,no,QTe,16-05-1990,86597895 -49282341974,nl,Excel,false,hfwNd,Dorothy Aguilar,30-11-2000,stark,85,demoOption1,26454734892,NnaFLqpE,no,MfF,16-05-1990,31087068 -41708843536,nl,Bank A,false,JhkSd,Blake Underwood,15-03-1990,lannister,08,demoOption2,53315897662,XrZARtOt,yes,bFe,16-05-1990,58739719 -04572734098,nl,Excel,false,uXpJw,Roy Hill,30-11-2000,stark,86,demoOption3,06807218193,DcysHbbN,no,mwm,16-05-1990,29975169 -05699106589,en,FSP - all attributes,true,CNgLa,Shawn Wheeler,15-03-1990,stark,63,demoOption1,82462474207,gjUmHDCF,yes,tuL,27-10-2002,28878173 -46407765937,nl,Intersolve-voucher-whatsapp,false,dDKpD,Theresa Dunn,30-11-2000,lannister,45,demoOption3,50362161098,DTwAvWxm,no,SKs,27-10-2002,03259160 -83181759183,en,Excel,false,kvyvt,Mattie Neal,15-03-1990,lannister,72,demoOption1,72489354928,nabWQWhO,no,fVf,27-10-2002,03930832 -50773550417,en,Excel,false,dNebU,Nina Conner,30-11-2000,stark,24,demoOption3,74708077288,kOZMiLRJ,no,RtA,27-10-2002,56521080 -38812840418,nl,FSP - no attributes,true,vUWIu,Edith Webster,15-03-1990,stark,20,demoOption1,32093265298,RdaYtdnr,yes,CTo,27-10-2002,40512977 -36967492512,en,Bank A,true,JzihD,Myrtie Herrera,15-03-1990,greyjoy,57,demoOption2,36670266231,mQppQxCz,no,nxy,16-05-1990,84313291 -50000034756,nl,FSP - no attributes,true,dXYEA,Clifford West,15-03-1990,stark,93,demoOption2,08370530351,aJDEdJWb,yes,XOA,27-10-2002,49931854 -68622970716,en,Excel,true,POkNd,Stella Green,30-11-2000,lannister,75,demoOption2,26979466849,QUkINaKy,no,yEi,16-05-1990,42647959 -71928683115,nl,Excel,false,eVzgm,Hunter Foster,30-11-2000,lannister,41,demoOption1,10028778145,QdJMgHzy,yes,rdq,27-10-2002,64888264 -74396921360,en,Bank A,true,QMDjT,Jason Patton,30-11-2000,greyjoy,98,demoOption3,01857650098,ELlqZvkz,no,Oys,16-05-1990,92830579 -59233708807,en,FSP - all attributes,false,eIIhS,Oscar Baldwin,15-03-1990,lannister,95,demoOption1,79653249165,GHGmBNfH,no,Mdt,27-10-2002,88943361 -16410477235,nl,FSP - all attributes,false,uWJGJ,Elmer Coleman,30-11-2000,greyjoy,90,demoOption1,30731221490,ITtBzgCw,no,LfT,27-10-2002,86099294 -89253486972,en,FSP - all attributes,false,aYltp,Louise Vega,15-03-1990,lannister,53,demoOption3,17129085889,CoumLdje,no,IbE,16-05-1990,89236904 -92147272012,en,Bank A,true,INWJh,Winnie Boyd,15-03-1990,lannister,55,demoOption1,09327050688,rLUQIwUJ,yes,aDX,27-10-2002,75886207 -23857406687,nl,Intersolve-voucher-whatsapp,true,mYFJs,Rose Washington,30-11-2000,stark,53,demoOption3,51737549725,GbCcOEhj,yes,AtO,16-05-1990,26715157 -50487616800,nl,FSP - all attributes,true,dnenL,Estella Burke,15-03-1990,greyjoy,70,demoOption3,30853676620,GbNhMJjs,yes,Dks,27-10-2002,87007432 -23493626706,nl,FSP - no attributes,true,JLFCj,Clara Francis,30-11-2000,greyjoy,43,demoOption2,32886930172,zLYoXWrq,no,WSW,27-10-2002,39415081 -25323004367,nl,Bank A,true,VLagA,Emily McKenzie,30-11-2000,greyjoy,91,demoOption3,28522439870,fYARiPOA,yes,yue,27-10-2002,28723350 -96815037247,en,Excel,false,Avxwi,Bertha Allison,30-11-2000,greyjoy,87,demoOption1,62460833853,SMRnKCfl,yes,pem,16-05-1990,13649810 -07629094041,en,Excel,true,pHoPn,Amanda Holmes,15-03-1990,greyjoy,67,demoOption1,00675785861,MFvGcoYQ,no,yMr,16-05-1990,26511233 -96936150980,en,FSP - no attributes,true,areDV,Thomas Casey,15-03-1990,greyjoy,01,demoOption1,11684555198,xWNrtfFQ,no,jQt,27-10-2002,65754515 -47309469664,nl,Excel,false,QOJCh,Jeanette Walton,15-03-1990,greyjoy,27,demoOption1,79860691750,OiBLrBBd,yes,xNE,16-05-1990,52764247 -02340848718,en,FSP - no attributes,true,AlaRb,Estella McBride,30-11-2000,lannister,51,demoOption3,87067694720,gIRCZSue,yes,sLt,27-10-2002,81714709 -35637281032,en,Bank A,false,TNLPp,Ivan Blake,15-03-1990,lannister,55,demoOption1,73495314360,bhteeJXQ,no,abR,27-10-2002,70818842 -67313321361,nl,Intersolve-voucher-whatsapp,true,TlwdH,Milton Munoz,30-11-2000,greyjoy,16,demoOption2,19043125213,PJaNGgIe,yes,iaJ,16-05-1990,75800941 -05210841697,en,FSP - all attributes,true,PesBB,Mathilda Higgins,30-11-2000,greyjoy,38,demoOption1,81967496905,HrscTyFI,no,fZQ,27-10-2002,64594387 -07070930739,en,Intersolve-voucher-whatsapp,true,ybUtT,Thomas Frank,15-03-1990,greyjoy,17,demoOption1,75692108868,PiVRYAxa,yes,Jdu,27-10-2002,84818529 -73381333620,nl,Excel,true,CqjkV,Kate Black,30-11-2000,lannister,27,demoOption2,61675450171,ERTNJvZk,no,cFv,27-10-2002,80393500 -84993006516,nl,FSP - all attributes,false,ahWDu,Hettie Powers,15-03-1990,lannister,38,demoOption1,37345821309,sOfUEftS,yes,XoD,16-05-1990,12560307 -05817640931,nl,Intersolve-voucher-whatsapp,true,UqBOd,Gerald Bryant,30-11-2000,lannister,14,demoOption1,61747775094,CtYuysef,no,deg,27-10-2002,87998031 -41676236943,en,FSP - all attributes,true,goMGF,Marguerite Ortiz,30-11-2000,stark,52,demoOption3,93962425835,bHgVUxaU,yes,MlD,27-10-2002,38488673 -56424464206,nl,Excel,true,cydUg,Elsie Chapman,30-11-2000,lannister,62,demoOption2,96367027396,ixcnBJVg,yes,KTX,27-10-2002,30836386 -84279128429,en,Intersolve-voucher-whatsapp,false,Tgfts,Harry Neal,15-03-1990,lannister,20,demoOption3,91417162676,qidEFGjf,no,Pzd,16-05-1990,94248811 -18683752181,nl,FSP - no attributes,false,cGSZr,Ida McKinney,30-11-2000,lannister,69,demoOption2,98322833711,xOCLlJEH,no,AAY,27-10-2002,83598001 -86054410279,nl,FSP - all attributes,true,pnvZj,Minerva Pena,15-03-1990,greyjoy,93,demoOption2,52781085237,OENvuUhZ,no,Ish,27-10-2002,79300611 -28447739455,nl,FSP - all attributes,false,kUUhI,Jimmy Chandler,30-11-2000,lannister,44,demoOption2,69965243363,JSRMsWqx,yes,vJu,27-10-2002,12644326 -19093251359,en,Excel,true,jnWOo,Stanley Blake,30-11-2000,greyjoy,77,demoOption2,56667886349,vhxEqizW,yes,AAI,27-10-2002,27491613 -08101796923,en,Intersolve-voucher-whatsapp,false,MFfzd,Cornelia Bennett,15-03-1990,greyjoy,51,demoOption2,57860970716,TZhrPCWO,no,UsI,16-05-1990,71295754 -86005614971,nl,Intersolve-voucher-whatsapp,true,mhgPM,Lenora Buchanan,15-03-1990,greyjoy,24,demoOption2,53016858101,BulbxEuo,no,EgC,27-10-2002,90732874 -20928641968,en,FSP - no attributes,true,PZCad,Mason Richards,15-03-1990,lannister,17,demoOption1,48881758823,ALKTqPmF,no,tEq,27-10-2002,41399437 diff --git a/e2e/test-registration-data/test-registrations-westeros-1000.csv b/e2e/test-registration-data/test-registrations-westeros-1000.csv new file mode 100644 index 0000000000..66874fb00b --- /dev/null +++ b/e2e/test-registration-data/test-registrations-westeros-1000.csv @@ -0,0 +1,1001 @@ +phoneNumber,preferredLanguage,programFinancialServiceProviderConfigurationName,knowsNothing,name,dob,house,dragon,skills,whatsappPhoneNumber,personalId,fixedChoice,openAnswer,date,accountId,nationalId +44289054723,en,Safaricom,false,Philip Grant,15-03-1990,lannister,53,demoOption1,59008866125,ODmgOZzI,no,CIV,16-05-1990,54489265,54489265 +55750352306,nl,Safaricom,false,Luke Gomez,30-11-2000,greyjoy,7,demoOption2,29342449989,VeWtEUns,no,nXN,27-10-2002,43784156,43784156 +27883373741,en,Intersolve-voucher-whatsapp,false,Lester Cortez,30-11-2000,greyjoy,41,demoOption2,16517945997,DqwOsUvL,yes,eyY,27-10-2002,81305916,81305916 +73609651628,en,Safaricom,true,Terry Wallace,15-03-1990,stark,76,demoOption2,46082513349,nhvoZbzA,no,yDy,16-05-1990,38243576,38243576 +36225186458,en,ironBank,false,Josie Watts,15-03-1990,stark,84,demoOption1,17848130562,fUdtaogP,yes,fKZ,27-10-2002,67956212,67956212 +41450377722,en,Intersolve-voucher-whatsapp,false,Martin Kim,30-11-2000,greyjoy,51,demoOption3,50049611675,DTwGcgMD,yes,pOM,27-10-2002,45167920,45167920 +47033686665,nl,Safaricom,true,Brett Kim,15-03-1990,greyjoy,80,demoOption1,33470153853,YlpxzQXe,yes,cLr,27-10-2002,8902455,8902455 +15576066176,en,ironBank,false,Ina Webster,15-03-1990,lannister,12,demoOption3,79895725900,RgxMbAfT,no,udB,16-05-1990,25408770,25408770 +7854025644,en,Safaricom,false,John Love,30-11-2000,lannister,2,demoOption1,80624764436,CKlDoQmm,yes,HgO,27-10-2002,36315888,36315888 +56191435798,nl,Intersolve-voucher-whatsapp,true,Katherine Hammond,15-03-1990,greyjoy,19,demoOption2,13946717244,UYcozjbz,yes,SoP,16-05-1990,29801679,29801679 +24958004492,nl,Safaricom,true,Jorge Norton,30-11-2000,greyjoy,96,demoOption3,22764294704,TQrdxdQv,no,PKJ,27-10-2002,1453688,1453688 +19268281893,nl,gringotts,true,Lulu Lloyd,30-11-2000,greyjoy,22,demoOption3,33153598997,KSUKuGbb,yes,vmB,27-10-2002,47961761,47961761 +18335973172,nl,Intersolve-voucher-whatsapp,true,Amelia Campbell,15-03-1990,stark,37,demoOption2,81893287976,SRSjZZCA,yes,RtY,16-05-1990,39299549,39299549 +49244267457,en,Safaricom,true,Dominic Goodman,30-11-2000,stark,27,demoOption1,95546333707,buObmOEy,no,ucl,27-10-2002,16486955,16486955 +23230452842,nl,gringotts,false,Allen Brooks,15-03-1990,stark,81,demoOption3,51622627860,jellvRvo,yes,EVN,27-10-2002,91947347,91947347 +37284762030,nl,Safaricom,false,Alfred Carroll,30-11-2000,stark,88,demoOption1,46405809566,qLoIturt,yes,LBi,27-10-2002,54545587,54545587 +15300863253,en,Safaricom,false,Margaret Guerrero,15-03-1990,lannister,30,demoOption3,88692918512,vaoDcAbf,yes,dhO,16-05-1990,42823461,42823461 +13079334174,nl,gringotts,true,Garrett Ortiz,30-11-2000,lannister,50,demoOption1,38491386161,kAiJPRMU,no,oqE,27-10-2002,27648904,27648904 +40011667404,en,Safaricom,false,Elva Carter,30-11-2000,stark,89,demoOption2,2000871763,gHgAaMfz,yes,gIY,27-10-2002,25165830,25165830 +56960578191,en,Safaricom,true,Alma Moreno,30-11-2000,lannister,94,demoOption2,48075221766,MLAcGIAa,no,nkF,16-05-1990,37765635,37765635 +4080147674,en,Safaricom,false,Linnie Brewer,15-03-1990,lannister,21,demoOption1,49053825729,AdgQadjx,yes,gGf,16-05-1990,69880741,69880741 +24239124936,en,Safaricom,false,Cordelia Morris,30-11-2000,lannister,7,demoOption2,35422724759,YjsTZzcl,yes,VKW,16-05-1990,7005353,7005353 +93610134799,en,Safaricom,false,Minnie Evans,30-11-2000,lannister,28,demoOption1,90677434217,KtGMVswk,yes,XKa,27-10-2002,8765336,8765336 +25147396185,nl,Intersolve-voucher-whatsapp,false,Duane Nichols,30-11-2000,lannister,50,demoOption1,53311048728,qLwkdrVh,no,wSt,27-10-2002,73505533,73505533 +37779549737,nl,gringotts,false,Henrietta James,30-11-2000,greyjoy,1,demoOption1,37729459527,hcHtQeKK,yes,xRB,27-10-2002,11290866,11290866 +35332461865,nl,Intersolve-voucher-whatsapp,true,Mary Bowen,15-03-1990,greyjoy,38,demoOption3,45739522163,VDqehQhF,yes,QxG,16-05-1990,78785727,78785727 +58481546707,en,Safaricom,true,Belle Valdez,30-11-2000,lannister,62,demoOption1,63604642447,RXHfoqKz,no,RUc,16-05-1990,50777437,50777437 +98554749207,en,ironBank,true,Cameron Watson,15-03-1990,lannister,33,demoOption2,64748778391,RToiLvDT,no,mZF,16-05-1990,95456906,95456906 +80062561910,nl,gringotts,false,Blanche Young,30-11-2000,stark,37,demoOption3,69766627657,kpYwMvdQ,no,uDg,27-10-2002,50930282,50930282 +43161116446,nl,Safaricom,false,Lora Shaw,30-11-2000,lannister,81,demoOption3,60358385024,RyKejcWr,no,XUe,27-10-2002,35218642,35218642 +93768945152,en,Safaricom,true,Walter Morton,15-03-1990,lannister,31,demoOption1,18982662706,WrrnNksT,no,dCA,16-05-1990,61160423,61160423 +28872700529,nl,Safaricom,true,Mittie Gibbs,15-03-1990,greyjoy,94,demoOption2,14269917496,nDIPsyvZ,no,UAb,27-10-2002,28649035,28649035 +67518079864,en,Intersolve-voucher-whatsapp,false,Beulah Reynolds,15-03-1990,lannister,61,demoOption1,19339124759,yiRzuRqs,yes,qXh,27-10-2002,94025208,94025208 +33301860108,nl,Safaricom,false,Tyler Cruz,30-11-2000,lannister,92,demoOption1,36938727465,TIXbtPlM,yes,dmM,27-10-2002,43931560,43931560 +81598730905,en,Safaricom,true,Charlie Doyle,30-11-2000,stark,94,demoOption2,92426039384,jzEnOJrG,yes,lCJ,16-05-1990,79025695,79025695 +21465534315,en,Safaricom,true,Francis Warner,15-03-1990,greyjoy,1,demoOption3,75374453146,xwJKHQwg,no,Qod,16-05-1990,85096447,85096447 +80402709946,en,Safaricom,false,Victoria McLaughlin,15-03-1990,stark,33,demoOption2,61715631912,mzisBzhq,no,zZT,27-10-2002,27491213,27491213 +96467665456,nl,Safaricom,true,Frederick Bowman,30-11-2000,lannister,5,demoOption2,50684157609,xzTmtpRc,no,pZk,16-05-1990,21097465,21097465 +90500374238,en,Safaricom,false,Richard Cox,15-03-1990,greyjoy,87,demoOption2,90277262190,QgXLYEJp,no,xWy,16-05-1990,19649652,19649652 +54273629263,en,Safaricom,false,Franklin Romero,15-03-1990,greyjoy,29,demoOption1,76758769376,bcUqIIQQ,yes,rQg,16-05-1990,28410328,28410328 +92684223419,nl,Intersolve-voucher-whatsapp,true,Louisa Williamson,15-03-1990,greyjoy,54,demoOption1,14766513271,JLlomfmm,yes,iWg,16-05-1990,53478813,53478813 +78905236269,nl,ironBank,true,Dora Richards,30-11-2000,lannister,59,demoOption3,27063301472,OhFnmPhx,yes,BZi,16-05-1990,83229576,83229576 +42941008240,en,Safaricom,true,Gene Haynes,15-03-1990,stark,82,demoOption1,44890157877,pnTSIvYZ,yes,Dfu,27-10-2002,37198743,37198743 +32093196598,en,Safaricom,false,Violet Brady,30-11-2000,lannister,96,demoOption3,23366739777,AMrEQIjf,yes,qLq,16-05-1990,75151947,75151947 +29354590298,nl,Safaricom,true,Phillip Clarke,30-11-2000,greyjoy,95,demoOption1,94470126487,IDhAgRWF,no,GeQ,16-05-1990,58035833,58035833 +26824063798,en,Intersolve-voucher-whatsapp,false,Lura Jordan,30-11-2000,lannister,51,demoOption1,61470033141,PpOSxOKU,no,cwD,27-10-2002,78253089,78253089 +90336095808,nl,Safaricom,false,Callie Little,30-11-2000,greyjoy,68,demoOption3,93535229579,wSLnRKQY,yes,niG,16-05-1990,47353578,47353578 +87718370501,en,gringotts,false,Ruth Cummings,15-03-1990,greyjoy,33,demoOption1,90065536462,TuAtrKJr,yes,unB,16-05-1990,35467548,35467548 +50928918987,en,Safaricom,true,Ollie Bell,15-03-1990,greyjoy,90,demoOption2,22611707387,OZQxySko,no,mxP,27-10-2002,8648561,8648561 +45550791229,nl,ironBank,true,Hannah Fox,30-11-2000,stark,97,demoOption3,18654530591,jnXQACnu,no,GGr,27-10-2002,54656188,54656188 +28783367146,nl,Safaricom,false,Irene Logan,30-11-2000,greyjoy,56,demoOption2,99302435135,soChOuHA,no,JgU,27-10-2002,30767384,30767384 +88798765997,en,Intersolve-voucher-whatsapp,true,Julian Lynch,30-11-2000,lannister,26,demoOption1,57660860357,LvWIKfFC,yes,KjM,27-10-2002,40264380,40264380 +76609155137,en,gringotts,true,Irene Page,15-03-1990,greyjoy,98,demoOption1,13355729230,FhKazHgI,yes,ddF,16-05-1990,50921542,50921542 +12616983918,en,Safaricom,true,John Dennis,15-03-1990,stark,86,demoOption1,97358322670,mUxbUTyD,yes,xlz,16-05-1990,99917595,99917595 +23459238408,nl,ironBank,true,Roger Guzman,30-11-2000,lannister,47,demoOption1,71888037481,GxUykSaJ,no,IWP,27-10-2002,98321770,98321770 +76394353339,en,Safaricom,true,Rosetta Peterson,15-03-1990,lannister,16,demoOption3,90105478261,JcvogHDy,no,yEI,27-10-2002,89119688,89119688 +3100297338,en,gringotts,false,Nathan Guzman,15-03-1990,greyjoy,71,demoOption1,31543065020,ZrLoWpwF,yes,dBn,16-05-1990,86641134,86641134 +16908625395,nl,ironBank,false,Gary Alvarado,30-11-2000,lannister,71,demoOption1,22130484413,IFTklhpN,no,JUm,16-05-1990,28898449,28898449 +84120779697,nl,Safaricom,true,Andrew Pena,15-03-1990,lannister,36,demoOption2,51206609961,RpTjwQsg,no,tox,27-10-2002,76529357,76529357 +28418210787,en,gringotts,true,Austin Harmon,30-11-2000,lannister,18,demoOption2,61123082043,qnntwrFv,no,qOf,16-05-1990,20225735,20225735 +60885040245,en,Safaricom,false,Jason Coleman,15-03-1990,greyjoy,35,demoOption2,88561466946,gdKNmXob,yes,kHp,16-05-1990,27031636,27031636 +42907157166,nl,Intersolve-voucher-whatsapp,true,Ricardo Ball,30-11-2000,stark,10,demoOption2,71953501391,VPBvqZhm,no,dPw,27-10-2002,29468468,29468468 +13312986689,en,Safaricom,false,Chase Alexander,15-03-1990,greyjoy,20,demoOption1,43634012502,kMVnSxxw,yes,bXP,16-05-1990,44578430,44578430 +25792811053,nl,Safaricom,true,Theodore Douglas,30-11-2000,greyjoy,78,demoOption2,85971296056,XWtTsYTH,no,pha,16-05-1990,17214261,17214261 +42266000162,en,Safaricom,true,Jack Wade,30-11-2000,greyjoy,99,demoOption2,47231657437,UOHjxxlC,yes,pzA,16-05-1990,69720205,69720205 +60629031521,en,Safaricom,true,Cody Byrd,30-11-2000,stark,2,demoOption3,51990706177,YDUBiZSH,no,ZeU,27-10-2002,16253722,16253722 +88176029681,en,Safaricom,true,Sue Collier,15-03-1990,stark,75,demoOption2,26783454511,iCUaEAgL,no,cXz,16-05-1990,42502113,42502113 +1550239114,en,Safaricom,false,Mayme Buchanan,30-11-2000,lannister,35,demoOption2,13803612826,hxsKTmcs,no,mEB,27-10-2002,22622225,22622225 +50891664341,en,Safaricom,true,Willie Hart,30-11-2000,greyjoy,46,demoOption2,55124869019,tesEBkQK,yes,tHO,27-10-2002,75366206,75366206 +28321421988,nl,Safaricom,true,Douglas Romero,15-03-1990,lannister,9,demoOption1,80190132481,xFkvkKhz,no,Fmg,27-10-2002,29661005,29661005 +47930845596,en,Safaricom,true,Lillian Parker,15-03-1990,lannister,37,demoOption2,98094500885,EIVstMqF,yes,rpg,16-05-1990,98758274,98758274 +1260180269,nl,ironBank,true,Nannie Hubbard,15-03-1990,stark,17,demoOption3,81564510275,tKulSXcn,yes,Qsl,16-05-1990,14079861,14079861 +30837861746,en,gringotts,true,Olga Warner,30-11-2000,greyjoy,9,demoOption1,85079837124,vUCSEQrB,yes,jcJ,27-10-2002,84301388,84301388 +8910562497,en,gringotts,true,Susan Bailey,15-03-1990,lannister,82,demoOption2,28206492774,jCOuOvqG,yes,JZH,27-10-2002,66711174,66711174 +66195821441,nl,Intersolve-voucher-whatsapp,false,Clyde Joseph,15-03-1990,lannister,89,demoOption1,74970894543,sxKEfris,yes,lHq,16-05-1990,56721513,56721513 +51723265252,nl,Intersolve-voucher-whatsapp,true,Alexander Young,15-03-1990,lannister,15,demoOption1,13432888906,YZiziXxj,no,uMt,16-05-1990,45722640,45722640 +43976152416,nl,Safaricom,true,Herman Carlson,15-03-1990,greyjoy,81,demoOption2,4144723830,ZkUVuZgJ,no,lPn,16-05-1990,62189331,62189331 +65874537874,nl,Safaricom,false,Fanny Sullivan,30-11-2000,stark,75,demoOption1,11072548150,DBNattjH,no,fpn,27-10-2002,77350145,77350145 +35069482618,en,Safaricom,true,Jayden Wise,30-11-2000,stark,62,demoOption2,63112902401,TvjixGQW,yes,ncX,16-05-1990,81768055,81768055 +78690306980,en,Safaricom,false,Duane Boone,15-03-1990,stark,92,demoOption2,49318535252,HUivyKaW,yes,jQU,27-10-2002,62301539,62301539 +51239869605,nl,ironBank,true,Lydia Sanders,15-03-1990,lannister,58,demoOption3,35498964847,uXqsWCKI,no,bVo,16-05-1990,24205648,24205648 +19698119171,en,gringotts,true,Eliza Rice,30-11-2000,lannister,22,demoOption2,28997796671,DRAtyBPw,no,EzY,27-10-2002,27274054,27274054 +73626757505,nl,Safaricom,true,Gregory Pratt,15-03-1990,stark,36,demoOption1,73415361391,CsQpsHOs,yes,nPC,16-05-1990,48886012,48886012 +70881917395,en,Intersolve-voucher-whatsapp,false,Ray McLaughlin,30-11-2000,greyjoy,39,demoOption1,31765821465,yjrfeGXW,no,obX,27-10-2002,84958096,84958096 +96637340898,nl,Safaricom,false,Carolyn Marsh,15-03-1990,greyjoy,72,demoOption2,31313154702,wnhoJfDw,no,FOJ,27-10-2002,62013379,62013379 +28312306097,nl,Safaricom,false,Barry Ford,30-11-2000,stark,26,demoOption2,71730172252,VEOpxkvn,no,Llk,27-10-2002,13192208,13192208 +61960446588,nl,ironBank,false,Oscar Gray,30-11-2000,lannister,41,demoOption2,10093906992,adPQwIGW,no,ojj,27-10-2002,23087559,23087559 +48608090243,nl,Safaricom,true,Bryan Hardy,15-03-1990,greyjoy,43,demoOption3,67164549851,wJXoWevS,no,icL,27-10-2002,83608072,83608072 +13812886419,nl,Safaricom,true,Johnny Moore,30-11-2000,greyjoy,69,demoOption2,11039744828,BbXSXLjc,no,Rum,27-10-2002,47579493,47579493 +67916514741,nl,Intersolve-voucher-whatsapp,true,Alfred Wagner,30-11-2000,lannister,71,demoOption1,96109262266,gXJYxCgK,no,HjH,27-10-2002,57202708,57202708 +97156683400,en,Safaricom,true,Melvin Burgess,30-11-2000,greyjoy,17,demoOption1,40693675362,RLWmnMUQ,no,ZTO,16-05-1990,79095412,79095412 +14125524802,nl,ironBank,false,Bryan Singleton,30-11-2000,greyjoy,76,demoOption2,51737409057,tbkBCLtk,no,KmE,16-05-1990,45508474,45508474 +13607575826,en,gringotts,true,Steven Jacobs,15-03-1990,stark,50,demoOption2,12345510338,lNmvZthI,yes,rUq,16-05-1990,43482087,43482087 +1554056305,en,Safaricom,false,Leona Erickson,30-11-2000,greyjoy,34,demoOption2,32319552059,SFnNbQsr,yes,hFq,27-10-2002,97421680,97421680 +30754371355,en,Safaricom,false,Mark Watson,30-11-2000,greyjoy,79,demoOption1,49266907333,TQpPVCry,yes,wBo,16-05-1990,41439724,41439724 +44828813259,en,Intersolve-voucher-whatsapp,true,Nicholas McCarthy,15-03-1990,stark,32,demoOption3,17578876300,JApTImYv,yes,OIH,16-05-1990,63890429,63890429 +48927928124,en,Intersolve-voucher-whatsapp,true,Addie Christensen,15-03-1990,lannister,81,demoOption1,88458882,HiZqDFLn,no,TaL,27-10-2002,46127137,46127137 +24037346499,nl,Intersolve-voucher-whatsapp,false,Floyd Bowman,15-03-1990,stark,98,demoOption3,73689938952,XWMigabt,no,smr,27-10-2002,25309570,25309570 +89387467470,nl,Safaricom,false,Francis Nelson,30-11-2000,stark,52,demoOption3,89450522055,TTcOYlrc,no,mBM,27-10-2002,11296756,11296756 +14663342747,nl,Intersolve-voucher-whatsapp,false,Beulah Frank,15-03-1990,stark,34,demoOption1,82100964646,DIpeChka,no,FKa,16-05-1990,65161122,65161122 +28655669154,nl,Safaricom,false,Marion Shelton,30-11-2000,stark,51,demoOption3,483030877,wOLlKkxV,yes,MTY,16-05-1990,97356522,97356522 +13815759644,en,Safaricom,true,Evan Phelps,30-11-2000,lannister,94,demoOption3,62779116205,XAtCgHul,no,gZp,16-05-1990,80569472,80569472 +40279134595,nl,Safaricom,true,Rosetta Anderson,30-11-2000,stark,32,demoOption1,67758433763,fIiOIurp,yes,SmZ,16-05-1990,32986680,32986680 +92710608948,en,Safaricom,false,Keith Sherman,30-11-2000,stark,52,demoOption3,21249023767,VPbtZitw,no,csa,27-10-2002,27635592,27635592 +6268712527,en,gringotts,true,Leroy Mills,15-03-1990,greyjoy,16,demoOption1,48455635322,MNmhQaqd,yes,pjG,16-05-1990,4762992,4762992 +58331970351,nl,Intersolve-voucher-whatsapp,true,Ina Fisher,15-03-1990,stark,79,demoOption3,81936090148,hxXiRaYW,yes,sxM,27-10-2002,77843706,77843706 +98919924121,nl,Safaricom,true,Blanche Hansen,30-11-2000,greyjoy,78,demoOption1,55986978769,gMWLCCGB,no,soc,27-10-2002,85574078,85574078 +22470995050,en,Safaricom,true,Melvin Taylor,15-03-1990,stark,72,demoOption3,61077446288,vrOxrezd,yes,XjM,16-05-1990,87968423,87968423 +24846623004,nl,Intersolve-voucher-whatsapp,true,Daniel McLaughlin,15-03-1990,lannister,94,demoOption2,40907539389,RMmqXvBL,yes,Cus,27-10-2002,48683398,48683398 +75260513058,nl,Safaricom,false,Louise McCormick,15-03-1990,lannister,23,demoOption3,67726298203,jYOgcfZN,yes,DjX,16-05-1990,67912532,67912532 +95922098433,nl,Intersolve-voucher-whatsapp,true,Henrietta Watkins,30-11-2000,lannister,28,demoOption3,84437578037,ZQACPwzr,no,WNs,16-05-1990,62444034,62444034 +94161838751,nl,Safaricom,false,Tom Quinn,15-03-1990,greyjoy,0,demoOption1,55236939987,MMIyHlgn,no,aOw,16-05-1990,38262549,38262549 +14040411888,nl,Safaricom,true,Barbara Kim,30-11-2000,lannister,37,demoOption1,87951879565,PxEHtKKS,yes,RNI,27-10-2002,64163706,64163706 +84784857779,en,Safaricom,false,Erik Pope,15-03-1990,greyjoy,99,demoOption3,40885869099,vrieFFWJ,no,ocy,16-05-1990,38423125,38423125 +8369395669,en,Safaricom,false,Alex Francis,30-11-2000,lannister,40,demoOption3,81799642202,jbdSFsqZ,yes,llj,16-05-1990,71442246,71442246 +87770337735,en,Safaricom,false,Lizzie Fisher,30-11-2000,stark,75,demoOption1,29500693655,WFborrJX,yes,aMf,27-10-2002,61208302,61208302 +20507032326,nl,ironBank,true,Genevieve Bush,15-03-1990,greyjoy,85,demoOption1,92439230152,vGJnmpPj,no,RFz,16-05-1990,74501854,74501854 +66824134753,en,gringotts,false,Walter Stokes,30-11-2000,stark,67,demoOption3,75096176383,siDzqRAM,yes,yRI,16-05-1990,739947,739947 +80076895942,nl,ironBank,false,Marc Wilkerson,30-11-2000,lannister,8,demoOption2,57286162565,uilpWhxR,yes,Xol,27-10-2002,2206117,2206117 +10719368410,en,gringotts,false,Marian McKinney,15-03-1990,greyjoy,12,demoOption1,35235891573,kSlffPnW,yes,xun,16-05-1990,33266788,33266788 +43059013855,nl,Safaricom,false,Celia Grant,15-03-1990,stark,45,demoOption3,88933562139,PoAwrVXs,yes,INe,27-10-2002,3466172,3466172 +84076755373,en,Intersolve-voucher-whatsapp,true,Sarah Snyder,15-03-1990,lannister,63,demoOption1,14081039989,hxIitBmk,no,MYE,16-05-1990,2576404,2576404 +56055074358,nl,Intersolve-voucher-whatsapp,true,Eula Warren,15-03-1990,lannister,88,demoOption2,50473835047,FfNiOwwy,yes,xzy,16-05-1990,58101512,58101512 +3394491248,en,Safaricom,false,Ethan Newton,30-11-2000,lannister,89,demoOption3,30674168710,nKbVZltG,yes,aLU,16-05-1990,77866025,77866025 +89899631337,en,gringotts,true,Vincent Weber,15-03-1990,lannister,6,demoOption3,1225241067,JHMdJqRm,no,yCe,16-05-1990,25219608,25219608 +31449837172,nl,Safaricom,true,Georgia Daniel,30-11-2000,stark,9,demoOption3,23297712603,elFuKUWE,no,SEL,16-05-1990,99006608,99006608 +16734477654,nl,Intersolve-voucher-whatsapp,true,Dustin Rice,30-11-2000,stark,66,demoOption2,25189965901,qfaXhWpx,no,Mqx,16-05-1990,87527507,87527507 +93883121376,nl,Intersolve-voucher-whatsapp,true,Iva Barrett,15-03-1990,stark,45,demoOption1,57276262535,vuRleiwv,yes,OUE,16-05-1990,89731116,89731116 +53449751111,nl,Safaricom,false,Gary Hawkins,15-03-1990,lannister,0,demoOption3,29657083477,QcvzhlHa,yes,RsD,27-10-2002,34964537,34964537 +90892975683,en,gringotts,true,Lida Norton,30-11-2000,greyjoy,69,demoOption3,6401220866,uqeJgrec,yes,YkM,27-10-2002,93740566,93740566 +9700261052,nl,ironBank,false,Daniel Wells,30-11-2000,stark,49,demoOption3,9538239332,ldmkQGFE,no,IRX,27-10-2002,18124261,18124261 +78639436837,en,gringotts,true,Rhoda Warren,30-11-2000,lannister,21,demoOption2,70116135884,lyaNsyxY,yes,BHI,27-10-2002,87820791,87820791 +4818947983,en,Safaricom,false,Calvin McCormick,15-03-1990,stark,82,demoOption3,67963494349,oQzWqydv,no,OOB,27-10-2002,88364826,88364826 +90536631883,nl,Safaricom,true,Betty McKinney,15-03-1990,greyjoy,37,demoOption1,25157578880,tJgKbtcv,yes,Fau,27-10-2002,58888310,58888310 +53551643987,nl,Safaricom,true,Rosalie Bates,15-03-1990,lannister,97,demoOption2,60093880061,Usmwrure,yes,lWI,16-05-1990,53607412,53607412 +66469876378,en,Safaricom,true,Lily Warner,15-03-1990,lannister,41,demoOption3,558175699,dWzfrSwK,yes,snP,27-10-2002,56539621,56539621 +71283271921,nl,Safaricom,false,Bradley Taylor,15-03-1990,greyjoy,57,demoOption3,79697456658,Cedxyejp,yes,EHp,16-05-1990,13216017,13216017 +57090020977,en,Safaricom,true,Cora Pena,30-11-2000,greyjoy,17,demoOption2,61321068840,xrmdkfiZ,yes,yEH,16-05-1990,94899200,94899200 +42147214435,en,Safaricom,false,Darrell Cannon,15-03-1990,greyjoy,79,demoOption2,12852940227,kxqSdpWl,yes,mgq,16-05-1990,34069842,34069842 +16345991177,nl,Safaricom,true,Cameron Gordon,30-11-2000,lannister,54,demoOption2,96960958429,pulIRfNX,no,NkP,27-10-2002,24436445,24436445 +61140700315,nl,Safaricom,false,Fannie Evans,15-03-1990,greyjoy,41,demoOption2,75064002471,cepLUMoL,yes,Ups,16-05-1990,4431141,4431141 +36176553129,en,Safaricom,false,Paul Casey,30-11-2000,greyjoy,4,demoOption3,73054795272,LpsskyYG,no,ePn,27-10-2002,63008538,63008538 +1374038604,nl,Intersolve-voucher-whatsapp,false,Katie Stewart,15-03-1990,lannister,56,demoOption3,56632347011,rZYIrzEj,no,KDt,16-05-1990,50751905,50751905 +25125193542,nl,Intersolve-voucher-whatsapp,true,Francisco Tate,30-11-2000,stark,27,demoOption3,39634835357,OuVOKUsL,no,xYr,27-10-2002,41335620,41335620 +75267378839,nl,Safaricom,true,Ethel Ward,15-03-1990,stark,94,demoOption1,83911412231,dFRvSAXU,no,hoW,16-05-1990,89942261,89942261 +33732303229,nl,Intersolve-voucher-whatsapp,false,Darrell Poole,30-11-2000,greyjoy,99,demoOption3,18817861447,pgvyxryL,yes,WHn,16-05-1990,57525203,57525203 +74580786274,en,gringotts,false,Adele McGuire,30-11-2000,stark,95,demoOption1,43799087219,IzGPoBog,no,JGj,27-10-2002,91326038,91326038 +56326654474,nl,ironBank,true,Leah Harvey,30-11-2000,greyjoy,83,demoOption2,4578001350,CRwQNHUa,no,Pwa,27-10-2002,82192762,82192762 +16930881437,nl,Intersolve-voucher-whatsapp,false,Eugene Harrington,15-03-1990,stark,20,demoOption1,50837916663,DLwcrKhg,no,EhB,16-05-1990,55271793,55271793 +88040669327,nl,Safaricom,false,Mabelle Barker,30-11-2000,lannister,44,demoOption2,694232643,FfXFhLfA,no,HnQ,27-10-2002,91021651,91021651 +24847319435,nl,Intersolve-voucher-whatsapp,true,Philip Potter,15-03-1990,lannister,69,demoOption2,71910659785,FNGMpPSn,yes,wgL,16-05-1990,59183854,59183854 +86481783240,en,Safaricom,true,Dominic Reid,15-03-1990,lannister,63,demoOption1,2400742375,uFOjeMxt,no,aom,16-05-1990,50276288,50276288 +46115955361,nl,Safaricom,true,Gordon Conner,15-03-1990,greyjoy,67,demoOption2,93906995863,RddbPtvr,yes,SWB,27-10-2002,59042532,59042532 +54808443241,en,Safaricom,false,Ruth Hudson,15-03-1990,lannister,75,demoOption3,93722743909,nmyRNUUm,yes,Prk,27-10-2002,54995931,54995931 +37696589882,en,Intersolve-voucher-whatsapp,true,Mathilda Barnett,15-03-1990,stark,59,demoOption2,31667142847,zeyZXTaw,no,WtX,27-10-2002,74947990,74947990 +16033513710,nl,ironBank,true,Miguel Thomas,30-11-2000,stark,41,demoOption2,14245538342,RnNyuJtO,yes,bEJ,16-05-1990,51878820,51878820 +34120270499,en,Intersolve-voucher-whatsapp,true,Eva George,30-11-2000,lannister,39,demoOption1,6474591921,jzTSEJdd,no,Tfo,27-10-2002,71606936,71606936 +27395383656,nl,Safaricom,true,Darrell Love,30-11-2000,greyjoy,38,demoOption3,70075674790,CfCcJZqQ,no,bRt,16-05-1990,14360537,14360537 +65967918470,nl,Safaricom,true,Amelia Montgomery,30-11-2000,lannister,70,demoOption3,40318706486,ZFcrtWSN,no,IzK,16-05-1990,97017248,97017248 +79116251487,nl,Intersolve-voucher-whatsapp,true,James Jefferson,15-03-1990,greyjoy,9,demoOption3,15315269538,qSntrwAt,no,ztc,16-05-1990,53949422,53949422 +14454955900,nl,Safaricom,true,Chris Griffin,15-03-1990,stark,46,demoOption2,49023393332,aIhwjJnD,yes,uck,27-10-2002,42198671,42198671 +51237951352,nl,Safaricom,true,Josie Shaw,30-11-2000,stark,77,demoOption1,10995913485,KItdSezf,no,Mui,16-05-1990,57292033,57292033 +97588334079,en,Safaricom,true,Leon Maxwell,15-03-1990,greyjoy,18,demoOption1,64784434926,yMjIHqIF,yes,KZj,27-10-2002,74244143,74244143 +18701612165,en,gringotts,true,Olga Parker,15-03-1990,stark,87,demoOption1,69621922743,kxNvEqjh,no,qrC,16-05-1990,63303416,63303416 +4543800901,en,Intersolve-voucher-whatsapp,false,Danny Fox,30-11-2000,greyjoy,72,demoOption3,2345304927,aZXjSHxz,yes,tjB,16-05-1990,39865971,39865971 +35797424014,nl,Safaricom,false,Sadie Moore,30-11-2000,stark,13,demoOption1,61384145956,ojsFWTlH,no,oED,16-05-1990,1742256,1742256 +77891777700,en,gringotts,false,Della Briggs,30-11-2000,stark,85,demoOption3,74028500643,xQDZWCOl,no,VYo,16-05-1990,44734117,44734117 +54232088697,nl,Intersolve-voucher-whatsapp,true,Carolyn Jennings,15-03-1990,greyjoy,72,demoOption3,62502158461,zFqqEVwZ,yes,qnf,16-05-1990,94164714,94164714 +46059047761,en,Safaricom,false,Amelia King,15-03-1990,lannister,40,demoOption3,58729395598,mzMdVHKq,no,VWZ,16-05-1990,2233168,2233168 +76575489012,en,Safaricom,true,Gerald Hart,15-03-1990,greyjoy,20,demoOption1,28199872969,BzIfnnUo,no,Suk,27-10-2002,19371334,19371334 +50013723920,nl,Safaricom,true,Ethan Strickland,30-11-2000,greyjoy,27,demoOption1,49708879058,wjYjEaps,no,oDS,16-05-1990,30494271,30494271 +31277266145,nl,Intersolve-voucher-whatsapp,true,Nora Evans,30-11-2000,lannister,57,demoOption1,36529676773,mxalaGlC,no,sws,27-10-2002,93110196,93110196 +38546319060,en,gringotts,true,Lewis Goodwin,15-03-1990,greyjoy,66,demoOption3,43090643409,XXsvhVRJ,no,BmC,27-10-2002,45555003,45555003 +4988475449,en,Safaricom,true,Pauline Hawkins,30-11-2000,lannister,62,demoOption2,1439545303,OYVqjfCY,yes,KeQ,27-10-2002,56925157,56925157 +93729086667,nl,Safaricom,true,Jayden Spencer,30-11-2000,greyjoy,87,demoOption2,40393680058,unyforlG,yes,RRh,27-10-2002,79427608,79427608 +95518145600,en,Safaricom,true,Augusta Chapman,30-11-2000,greyjoy,69,demoOption1,86728152487,WMRZBSNn,yes,gMA,16-05-1990,79449656,79449656 +7249104423,en,Intersolve-voucher-whatsapp,true,Ann Webb,30-11-2000,lannister,7,demoOption3,93850741281,wcLSfFkY,yes,slW,16-05-1990,52902360,52902360 +52920157887,nl,ironBank,true,Erik Reyes,15-03-1990,lannister,0,demoOption3,81216845638,xpdTMqUL,no,oyR,27-10-2002,35647891,35647891 +51169072720,en,Safaricom,true,Birdie Chavez,15-03-1990,stark,93,demoOption1,75616843105,cfBJxvCm,yes,gMH,27-10-2002,69550250,69550250 +19253849826,en,gringotts,true,Peter Nelson,15-03-1990,lannister,16,demoOption2,93869752446,AchRmlMF,yes,GRO,16-05-1990,93243907,93243907 +40402664921,en,Safaricom,true,Mamie Cole,30-11-2000,greyjoy,46,demoOption3,88738249193,kXtncXJN,no,rhh,16-05-1990,71048847,71048847 +43646359408,nl,Safaricom,true,Logan Wong,15-03-1990,greyjoy,68,demoOption1,23629815761,BOCrJwXZ,no,YSh,16-05-1990,58144803,58144803 +14544255915,en,Safaricom,true,Arthur Sutton,15-03-1990,lannister,43,demoOption2,53938912135,npujGWbc,no,XqG,27-10-2002,76875551,76875551 +30060471246,en,Safaricom,false,Ricardo Hodges,15-03-1990,greyjoy,18,demoOption2,34081495890,NcneWIgx,no,WAI,27-10-2002,27165397,27165397 +20837300080,en,Safaricom,false,Philip Summers,15-03-1990,lannister,36,demoOption2,73690357445,nKcAhoFA,no,HrL,16-05-1990,99336665,99336665 +76509318951,en,Intersolve-voucher-whatsapp,true,Adele Fields,30-11-2000,greyjoy,94,demoOption1,17140051818,TYGGZrle,yes,MaZ,16-05-1990,97319009,97319009 +76228197858,en,gringotts,false,Elmer Douglas,15-03-1990,greyjoy,55,demoOption1,62468235119,ijTLrTwJ,yes,VKg,27-10-2002,27202239,27202239 +4471715534,en,Safaricom,false,Clifford Harmon,15-03-1990,greyjoy,27,demoOption2,77320359233,NmZLgiaW,yes,TfA,16-05-1990,3732963,3732963 +43731994838,en,gringotts,true,Christina Cruz,30-11-2000,greyjoy,23,demoOption3,29946251858,LLHpbMKK,no,QZu,16-05-1990,68960185,68960185 +62743168963,nl,Safaricom,true,Lena Adkins,30-11-2000,stark,82,demoOption2,14876280623,nfclRHrX,no,srb,16-05-1990,72942014,72942014 +95393685932,nl,ironBank,false,Victoria Hudson,30-11-2000,lannister,39,demoOption2,47232295898,gGkqJWHI,no,Hyi,27-10-2002,8619323,8619323 +32011365830,en,Safaricom,false,Bryan Powell,15-03-1990,stark,90,demoOption1,19418446642,BMlJtKmP,yes,XjS,27-10-2002,41085132,41085132 +41242869932,en,gringotts,true,Ernest Flores,15-03-1990,lannister,87,demoOption3,45786260355,paCToaoW,no,JIm,27-10-2002,6125978,6125978 +25728413144,en,Safaricom,true,Barry McCormick,15-03-1990,stark,79,demoOption2,89593690718,ulZBSxtn,no,CWG,16-05-1990,58629677,58629677 +97728906289,en,Safaricom,false,Garrett Mills,15-03-1990,greyjoy,12,demoOption1,15399044305,weBLSgBm,no,OTh,16-05-1990,41811154,41811154 +3471679077,en,Intersolve-voucher-whatsapp,true,Raymond Stokes,30-11-2000,lannister,20,demoOption3,70573796272,fjVYiORy,yes,jDN,27-10-2002,12112762,12112762 +22164947675,nl,Safaricom,true,Jerome Andrews,15-03-1990,greyjoy,49,demoOption3,21717959368,veujoCei,yes,Sdj,16-05-1990,60302758,60302758 +2202598048,nl,Safaricom,true,Virginia Rodriquez,30-11-2000,greyjoy,32,demoOption3,37796680686,hGoRyPCe,yes,LDr,27-10-2002,44676011,44676011 +81873578536,en,gringotts,true,Caleb Gill,15-03-1990,greyjoy,5,demoOption1,97717764418,HwiztwET,yes,pnF,16-05-1990,47034200,47034200 +82509526253,en,gringotts,true,Theresa Garza,30-11-2000,greyjoy,98,demoOption3,34833667659,kBHUFYmL,yes,DzE,16-05-1990,64661422,64661422 +73803171864,nl,Intersolve-voucher-whatsapp,false,Benjamin Ray,15-03-1990,stark,25,demoOption2,53166241762,ylamlrdb,no,Xji,27-10-2002,49690598,49690598 +63842816750,nl,Safaricom,false,Adele King,30-11-2000,greyjoy,99,demoOption3,73018872090,PwqmoVty,yes,hmf,27-10-2002,1329521,1329521 +89291175802,nl,Safaricom,false,Randy Nichols,15-03-1990,stark,82,demoOption1,82218627131,kQICBSgR,yes,Iqr,16-05-1990,72163261,72163261 +94494028082,en,Safaricom,true,Lilly Henry,15-03-1990,stark,28,demoOption3,28630961403,fltCXnqP,yes,PyZ,27-10-2002,99275634,99275634 +31544615100,nl,Safaricom,false,Irene Olson,15-03-1990,lannister,92,demoOption2,73023967380,omUAwthO,no,pOg,27-10-2002,57962984,57962984 +99632000989,en,Safaricom,false,Mina Rodriquez,15-03-1990,greyjoy,48,demoOption1,35712998148,ToFSEPLv,no,Udc,16-05-1990,66272033,66272033 +90934514510,en,Safaricom,false,Mayme Nash,15-03-1990,stark,61,demoOption1,97445592456,otqrcKzZ,yes,Agf,27-10-2002,70465330,70465330 +40915100778,nl,Safaricom,true,May Webster,15-03-1990,greyjoy,26,demoOption2,49733659192,yyIWimBo,no,znA,27-10-2002,12988692,12988692 +3527449062,en,gringotts,false,Josie Frank,30-11-2000,stark,78,demoOption2,91867529117,AoSpBufp,yes,Cqf,16-05-1990,13878610,13878610 +37070230769,nl,Safaricom,false,Mario Sanders,15-03-1990,greyjoy,38,demoOption1,32220542044,EdLHEIHz,yes,FxA,16-05-1990,93982932,93982932 +23595031680,nl,Safaricom,true,Susan Benson,30-11-2000,lannister,72,demoOption2,16203240920,FgCNvzEq,yes,Rqo,27-10-2002,10566776,10566776 +24182897518,en,gringotts,false,Bernard Woods,30-11-2000,stark,4,demoOption1,90356625766,hBZfVkXy,yes,nTP,16-05-1990,84922479,84922479 +15235136946,nl,Safaricom,true,Leroy Hamilton,15-03-1990,lannister,29,demoOption3,55408256913,KoULIyWO,yes,KYm,27-10-2002,94411376,94411376 +68510177541,en,Safaricom,true,Leroy Hansen,15-03-1990,stark,13,demoOption3,39065487315,WzjtRQPu,no,TaP,27-10-2002,71678674,71678674 +81882423536,nl,ironBank,false,Bruce Holt,15-03-1990,lannister,82,demoOption2,37876062357,fkobeWiv,no,zCW,16-05-1990,79215553,79215553 +48267058918,nl,Safaricom,true,Edgar Poole,15-03-1990,stark,25,demoOption3,15266148470,udkWIAJo,no,lsd,27-10-2002,99304191,99304191 +23426838274,nl,ironBank,false,Mittie Ortiz,30-11-2000,lannister,4,demoOption2,54082756368,fdmLkmQL,no,ndL,27-10-2002,43935994,43935994 +10617930712,nl,ironBank,true,Bertha Kim,15-03-1990,stark,20,demoOption2,17868355425,XStUgYpF,yes,tEp,27-10-2002,92770859,92770859 +32875799324,nl,ironBank,true,Todd Saunders,15-03-1990,lannister,38,demoOption2,2099389669,SyKeEGxL,yes,rJg,27-10-2002,92050286,92050286 +2674672392,en,Safaricom,false,Harold Benson,15-03-1990,stark,15,demoOption3,99497354043,dDhpAIjr,no,pmg,16-05-1990,73138993,73138993 +43758996805,en,Safaricom,false,Chad Jackson,15-03-1990,lannister,42,demoOption2,12290255100,bPujnOiK,no,HFH,27-10-2002,92754554,92754554 +18854032437,en,Intersolve-voucher-whatsapp,false,Louis Rodgers,15-03-1990,stark,5,demoOption2,46179760926,gMzZTxtM,no,nxM,27-10-2002,67839794,67839794 +48420807206,nl,Safaricom,true,Lillian Moody,15-03-1990,lannister,27,demoOption2,79867569224,VEaBIveq,yes,yAf,27-10-2002,15935149,15935149 +24921732861,nl,ironBank,false,Polly Knight,30-11-2000,stark,53,demoOption1,63760051263,yGWszsOo,yes,Pyq,27-10-2002,56397684,56397684 +46123765692,nl,Safaricom,true,Noah Palmer,30-11-2000,stark,20,demoOption1,65664048861,XRFPkpzt,yes,JJm,27-10-2002,20914659,20914659 +38226329276,nl,Intersolve-voucher-whatsapp,true,Nathan Burns,30-11-2000,lannister,19,demoOption2,69377764160,pYsywuiC,yes,Fyg,16-05-1990,5945991,5945991 +16386973039,en,Safaricom,false,Marcus Roy,15-03-1990,lannister,48,demoOption1,97767142077,tCgltmGq,no,OYv,16-05-1990,5951731,5951731 +23014805875,nl,Safaricom,true,John Davidson,30-11-2000,lannister,61,demoOption3,42795936570,qzijngcI,yes,Wlr,27-10-2002,88656219,88656219 +57540796450,nl,Safaricom,true,Micheal Collier,30-11-2000,lannister,65,demoOption3,26502728652,NiSDmWse,yes,TcT,27-10-2002,78152047,78152047 +80534016051,nl,Safaricom,false,Jose Sims,15-03-1990,lannister,90,demoOption1,42415048575,DBpVIPGn,no,JTB,16-05-1990,70827660,70827660 +24846084594,nl,Intersolve-voucher-whatsapp,true,Edgar Reyes,15-03-1990,lannister,61,demoOption1,8851558812,RCrRaGti,yes,ZDZ,27-10-2002,95986226,95986226 +90518570923,nl,ironBank,false,Kyle Burgess,30-11-2000,stark,75,demoOption2,99923100690,HxMXlptN,yes,NFL,16-05-1990,8347391,8347391 +9444604528,nl,ironBank,false,Isabella Turner,15-03-1990,lannister,5,demoOption2,84115351430,oIKasAJQ,no,Lmu,16-05-1990,74776048,74776048 +21415603967,en,gringotts,true,Lawrence Wade,30-11-2000,lannister,86,demoOption3,8062496759,lqfpAkKz,yes,eHi,16-05-1990,13044958,13044958 +39821134104,en,Intersolve-voucher-whatsapp,false,Ethel Ballard,15-03-1990,lannister,35,demoOption3,21862101214,ENEIzukH,no,Rix,27-10-2002,58452646,58452646 +73446343410,en,Safaricom,true,Ora Hayes,15-03-1990,lannister,33,demoOption1,20104289503,MCMJfasb,no,uKS,27-10-2002,91341437,91341437 +48340091352,nl,ironBank,true,Fanny Long,15-03-1990,lannister,60,demoOption2,24289496055,DODcnONn,yes,ZhM,27-10-2002,44168658,44168658 +86243871502,nl,ironBank,true,Cora Hanson,15-03-1990,greyjoy,19,demoOption2,25937342341,OJNUyQyD,yes,zzk,27-10-2002,2021250,2021250 +96978027748,nl,Safaricom,false,Ray Dixon,15-03-1990,lannister,59,demoOption3,59502100673,hzcKvXEt,yes,dhh,16-05-1990,46778896,46778896 +40490936558,nl,Intersolve-voucher-whatsapp,false,Jessie Gonzales,15-03-1990,greyjoy,51,demoOption1,82887663425,bDRHtBfM,yes,PRu,27-10-2002,13613324,13613324 +32673807851,en,Safaricom,true,Tom Reed,30-11-2000,stark,19,demoOption1,76169258779,YgGXguIA,yes,gha,27-10-2002,64335200,64335200 +91118821175,en,Intersolve-voucher-whatsapp,true,Willie Lambert,15-03-1990,lannister,47,demoOption3,83957723592,cOMdCohQ,yes,peX,27-10-2002,73667866,73667866 +2323286499,nl,Safaricom,false,Celia Gutierrez,30-11-2000,lannister,61,demoOption2,32002915285,JuVSzRkb,yes,pqp,27-10-2002,12033355,12033355 +67380251806,en,Safaricom,true,Arthur Gregory,15-03-1990,greyjoy,2,demoOption3,12995206680,FOTBmGNv,yes,kaR,16-05-1990,76999301,76999301 +38120022194,nl,ironBank,true,Inez Warren,30-11-2000,greyjoy,13,demoOption2,12699126659,IehGsuJK,no,lyu,27-10-2002,59844990,59844990 +171655124,nl,Safaricom,false,Eunice Atkins,15-03-1990,stark,59,demoOption2,61673103561,PRHMwJGd,yes,yED,27-10-2002,33808090,33808090 +15365274961,nl,Safaricom,true,Hunter McDonald,30-11-2000,lannister,99,demoOption1,7137247273,CVuFRFxJ,no,JjI,27-10-2002,6147090,6147090 +93938170055,en,Intersolve-voucher-whatsapp,true,Roger McKinney,15-03-1990,lannister,44,demoOption1,80327396793,WLUlbuUd,no,tht,16-05-1990,86663086,86663086 +64599517408,en,Safaricom,false,Glenn Wong,30-11-2000,greyjoy,79,demoOption3,93869289120,GpipovSJ,no,FbN,27-10-2002,69247167,69247167 +66075054111,nl,Safaricom,false,Myrtie Norman,15-03-1990,stark,17,demoOption2,73563848141,SRXLqzuI,no,tYo,16-05-1990,16598464,16598464 +88421572658,nl,Intersolve-voucher-whatsapp,true,Bessie Morton,30-11-2000,stark,74,demoOption2,36421972118,EEBuZljf,yes,VbU,27-10-2002,75553284,75553284 +89707529269,en,Safaricom,true,Leroy Lynch,30-11-2000,stark,51,demoOption2,55568861392,zuBzBRyl,no,ENG,16-05-1990,43529754,43529754 +87203229897,en,gringotts,true,Olivia Ford,30-11-2000,lannister,45,demoOption1,65642333664,dyzmQAhd,no,zkw,16-05-1990,95084435,95084435 +48651329854,en,Safaricom,false,Ray McGee,15-03-1990,lannister,52,demoOption1,80166758251,jFzTWrdo,no,AQE,16-05-1990,19631495,19631495 +15310293014,en,Safaricom,true,Thomas Sanders,30-11-2000,stark,75,demoOption2,77607938188,NCDzNJVV,no,XGk,16-05-1990,67854079,67854079 +25904288672,nl,Safaricom,false,Christopher Peterson,15-03-1990,lannister,57,demoOption1,75383296747,MUgfDedx,no,jxs,16-05-1990,83641071,83641071 +98035498468,en,Intersolve-voucher-whatsapp,false,Lois Brock,15-03-1990,stark,90,demoOption2,56489038440,WpmHmtJV,no,WlO,27-10-2002,94177521,94177521 +11295633819,nl,ironBank,true,Russell Ortega,30-11-2000,greyjoy,79,demoOption1,89749653162,lSGjoANC,no,HOs,27-10-2002,9650262,9650262 +2662816064,en,Safaricom,true,Nathaniel Gibbs,15-03-1990,lannister,4,demoOption1,13179028843,CJCSzTVK,yes,qJz,16-05-1990,41494475,41494475 +11816857352,nl,Safaricom,false,Genevieve Murphy,15-03-1990,greyjoy,70,demoOption3,98519943036,KxrAuyyC,yes,vHN,16-05-1990,23750109,23750109 +68824150501,nl,Safaricom,false,Jorge Davis,30-11-2000,greyjoy,65,demoOption2,7547757778,QcEIYujs,no,yzZ,27-10-2002,80148531,80148531 +35743018119,nl,Intersolve-voucher-whatsapp,false,Timothy Wilkerson,15-03-1990,stark,4,demoOption2,77890508026,ATicSDEi,no,Tae,27-10-2002,10261768,10261768 +5581288298,nl,ironBank,true,Antonio Parsons,15-03-1990,greyjoy,78,demoOption2,56113579456,zRFAnARF,no,DzS,16-05-1990,26360496,26360496 +6324668552,nl,Intersolve-voucher-whatsapp,true,Celia Morton,30-11-2000,stark,45,demoOption1,13709188465,LfRQybYU,no,eYX,27-10-2002,21772409,21772409 +2308391764,nl,Safaricom,false,Harriet Atkins,30-11-2000,stark,92,demoOption3,78128189882,owWPnoHy,yes,OnB,27-10-2002,65451829,65451829 +82367032886,en,Safaricom,false,Robert Hopkins,15-03-1990,greyjoy,14,demoOption2,18917845921,DDalirae,yes,nmb,27-10-2002,37614374,37614374 +33397774295,nl,Intersolve-voucher-whatsapp,true,Randy Newman,15-03-1990,greyjoy,98,demoOption2,31174810641,NHwduSjz,yes,ckG,27-10-2002,14107419,14107419 +53312541522,en,gringotts,false,Fanny Hunter,30-11-2000,stark,10,demoOption2,4492810812,bKtMeEOX,yes,Zqd,27-10-2002,14882593,14882593 +68361760854,en,Safaricom,false,Marian Boone,30-11-2000,greyjoy,41,demoOption3,3597256713,yJgZTQEc,no,teX,16-05-1990,69620146,69620146 +84778947281,en,Safaricom,true,Myra Cunningham,30-11-2000,stark,9,demoOption1,76010509636,HdbmLCvy,no,EGi,27-10-2002,13886048,13886048 +22245599782,en,Safaricom,true,Seth George,30-11-2000,greyjoy,18,demoOption2,20667731296,hbGTbTCL,no,fRo,27-10-2002,37170688,37170688 +15838469200,en,Safaricom,false,Travis Morgan,30-11-2000,lannister,47,demoOption1,93701453431,lwBxGfrr,yes,eiM,16-05-1990,71693590,71693590 +72007497273,nl,Intersolve-voucher-whatsapp,false,Bruce Rice,30-11-2000,greyjoy,99,demoOption3,69894727921,ReyMhrBv,yes,MlF,16-05-1990,78875127,78875127 +63104375520,en,Intersolve-voucher-whatsapp,true,Isaac Simon,30-11-2000,lannister,48,demoOption1,38716549759,uEPLDNWz,no,ncK,16-05-1990,17146573,17146573 +69333763592,nl,ironBank,true,Margaret Peterson,15-03-1990,lannister,18,demoOption1,32889710343,IZGDOMzY,no,OAy,27-10-2002,77910242,77910242 +2909144354,en,Safaricom,false,Vera Wheeler,30-11-2000,lannister,4,demoOption2,36838726720,iDQDPeIz,no,toy,16-05-1990,572125,572125 +63071607657,en,Safaricom,false,Ruby Sharp,15-03-1990,lannister,97,demoOption1,52677706162,NfooUAAJ,yes,Zxb,27-10-2002,84657622,84657622 +69430968658,en,Safaricom,true,Dora James,15-03-1990,greyjoy,19,demoOption3,98242372321,duVaBVqG,yes,LkS,27-10-2002,60130668,60130668 +39730059082,nl,Intersolve-voucher-whatsapp,true,Olive Wise,30-11-2000,lannister,18,demoOption1,28449249307,UjTDMQem,no,mBs,16-05-1990,22344359,22344359 +65341611805,en,Safaricom,false,Harold Fields,15-03-1990,lannister,35,demoOption2,71539347106,fMMZBqXt,yes,tPp,27-10-2002,1117999,1117999 +96808280585,nl,Safaricom,true,Herbert Cooper,30-11-2000,lannister,9,demoOption1,91115543489,uUnilzaw,yes,eBx,27-10-2002,26973096,26973096 +6771335247,en,Safaricom,true,Manuel Lindsey,15-03-1990,lannister,10,demoOption1,15932959363,hBFHcQML,no,fto,27-10-2002,37619397,37619397 +41131386189,en,Intersolve-voucher-whatsapp,true,Jim Hall,15-03-1990,greyjoy,83,demoOption1,61129297572,ChoUZMHI,yes,OoR,27-10-2002,53657676,53657676 +39210814871,en,Safaricom,false,Bertha Silva,30-11-2000,stark,53,demoOption3,56903301100,cQiTvcNv,no,XSD,27-10-2002,63821481,63821481 +29905044671,nl,Safaricom,false,Alfred Griffin,30-11-2000,stark,52,demoOption1,49646917892,fphSOYfL,no,rye,27-10-2002,64526777,64526777 +90094120314,nl,ironBank,true,Nicholas Watson,15-03-1990,lannister,1,demoOption1,29229521182,WgPrnvDY,yes,yHY,16-05-1990,71370728,71370728 +95683351608,nl,Safaricom,true,Ola McGee,15-03-1990,stark,30,demoOption3,92250234021,vRAhBIAy,yes,FcM,27-10-2002,49307276,49307276 +48706139437,nl,Intersolve-voucher-whatsapp,true,Bettie Boone,15-03-1990,stark,84,demoOption1,85511008393,YVLLYmaz,yes,siP,16-05-1990,45270964,45270964 +63357832504,nl,Safaricom,true,Paul Reeves,15-03-1990,stark,1,demoOption3,43392764314,NJzaaiWd,yes,RLt,16-05-1990,38805017,38805017 +66932400995,nl,Safaricom,false,Elmer Santos,30-11-2000,stark,78,demoOption2,53847408128,SCWmQeaA,no,twY,16-05-1990,35882064,35882064 +21365359571,en,Safaricom,false,Elijah Garza,30-11-2000,greyjoy,70,demoOption3,48913839711,jPtokUwW,yes,jWX,27-10-2002,41229923,41229923 +92516029803,en,Safaricom,false,Russell Cooper,15-03-1990,greyjoy,9,demoOption3,60072884714,IROUsVOQ,yes,YQT,16-05-1990,87609240,87609240 +20495736476,en,Safaricom,false,Kenneth Harrison,15-03-1990,lannister,35,demoOption3,67435827179,zQRltZMd,no,TpD,27-10-2002,52483674,52483674 +10383864207,nl,Safaricom,false,Mina Edwards,15-03-1990,greyjoy,21,demoOption3,44463161124,gxXdyWKu,yes,epx,27-10-2002,67706568,67706568 +37362110057,en,gringotts,false,Randy Brown,15-03-1990,greyjoy,47,demoOption2,85009206288,gADtFnQD,yes,gKp,16-05-1990,49948293,49948293 +51519838360,en,gringotts,false,Isabella Hall,30-11-2000,stark,45,demoOption3,98940249455,YtNTsqZj,no,PfP,27-10-2002,30793202,30793202 +17953998136,nl,Intersolve-voucher-whatsapp,true,May Riley,30-11-2000,greyjoy,34,demoOption2,2924848588,IMmboeLo,no,rGv,27-10-2002,9175567,9175567 +13927079912,en,Safaricom,true,Delia Obrien,15-03-1990,lannister,9,demoOption3,35595362985,vPNpuysr,yes,Pwc,27-10-2002,76073106,76073106 +8596331573,en,Safaricom,true,Harry Hale,15-03-1990,lannister,18,demoOption1,26472185179,vzgwfuGn,yes,Juk,27-10-2002,46291704,46291704 +87911795587,en,gringotts,false,Eliza Walton,15-03-1990,lannister,50,demoOption3,9291102324,EPPuipQn,yes,GbV,16-05-1990,10158850,10158850 +55324110727,en,Intersolve-voucher-whatsapp,true,Cody Black,15-03-1990,stark,49,demoOption3,35366699227,VSOIjOVY,no,HZq,16-05-1990,39017407,39017407 +94963897998,en,Safaricom,false,Bernice Fowler,30-11-2000,lannister,19,demoOption1,52124984156,loAJSBCY,no,cBW,16-05-1990,28334613,28334613 +35262760317,nl,Safaricom,true,Dollie Gomez,15-03-1990,greyjoy,1,demoOption3,81878332907,ykakJfQz,yes,GJS,27-10-2002,71701382,71701382 +36749619690,nl,Safaricom,true,Katherine Stone,30-11-2000,lannister,31,demoOption3,15638960413,FUFyrbvC,no,kKV,16-05-1990,41942326,41942326 +66040687409,nl,Intersolve-voucher-whatsapp,false,Edgar Phelps,15-03-1990,lannister,65,demoOption2,69111798441,CrIwQWVu,no,cmI,16-05-1990,59048516,59048516 +30929421938,en,gringotts,false,Samuel Price,30-11-2000,stark,17,demoOption2,10393027841,EsaxCMOL,no,pNi,27-10-2002,45197194,45197194 +8474540889,en,Safaricom,true,Jerome Erickson,15-03-1990,greyjoy,70,demoOption2,70891992696,cpuOkZNs,no,xxI,27-10-2002,80107798,80107798 +78999500410,en,Safaricom,true,Henry Daniel,15-03-1990,lannister,40,demoOption3,49583548288,DfHVxdYG,no,gjt,16-05-1990,98881219,98881219 +76038143346,en,Intersolve-voucher-whatsapp,false,Cornelia Howell,30-11-2000,greyjoy,59,demoOption3,95594854126,ThYjyrRl,no,UkU,27-10-2002,66639487,66639487 +90488289980,en,Safaricom,true,Dennis Blake,30-11-2000,stark,31,demoOption2,31241491895,SeRzjAXT,yes,HuH,27-10-2002,74254368,74254368 +502261743,nl,ironBank,false,Virginia Townsend,15-03-1990,lannister,24,demoOption3,6299988094,jHExxJGP,yes,vkr,16-05-1990,17412741,17412741 +16771104415,nl,Safaricom,true,Betty Buchanan,15-03-1990,lannister,44,demoOption3,3293720921,TPZqyYDj,no,eXJ,27-10-2002,69651132,69651132 +24874033121,en,Safaricom,true,Cecilia Barker,15-03-1990,lannister,44,demoOption1,9805619820,iZRQPTTe,yes,KCc,16-05-1990,72918757,72918757 +908469375,nl,Safaricom,true,Nell Beck,30-11-2000,greyjoy,51,demoOption2,53146933740,CqeMTRfP,no,pAN,27-10-2002,91338211,91338211 +70929856399,nl,Safaricom,false,Johnny Blair,15-03-1990,lannister,1,demoOption2,62789036390,smaafwYa,no,pMU,16-05-1990,74709249,74709249 +22041645579,nl,Intersolve-voucher-whatsapp,true,Larry Swanson,30-11-2000,lannister,81,demoOption2,83088089362,YKfGijfu,yes,PJS,27-10-2002,89519564,89519564 +32839680589,en,Safaricom,false,Amy Rodgers,15-03-1990,lannister,66,demoOption2,81616352210,THPFzwZW,yes,LzG,27-10-2002,58336900,58336900 +70425646686,en,Intersolve-voucher-whatsapp,true,Flora Allison,30-11-2000,lannister,76,demoOption3,29128892806,XbxsSSlz,no,PQA,27-10-2002,22407281,22407281 +83229204889,nl,Intersolve-voucher-whatsapp,false,Harry Harris,15-03-1990,stark,53,demoOption3,14630067797,JXHLxVJD,no,onO,16-05-1990,85533156,85533156 +86581951579,en,Intersolve-voucher-whatsapp,false,Betty Delgado,30-11-2000,lannister,61,demoOption3,59779333744,GWmgHVbk,no,Bdc,27-10-2002,57010719,57010719 +45124789137,nl,ironBank,true,Hattie Wade,15-03-1990,greyjoy,34,demoOption2,66268893728,vwUjwNmY,yes,Qwa,27-10-2002,81803896,81803896 +86897411112,en,Safaricom,false,Isaiah Roberson,15-03-1990,greyjoy,45,demoOption2,2838047702,CxnhTUZi,yes,iJh,16-05-1990,89087289,89087289 +56946336642,en,gringotts,true,Lou Valdez,30-11-2000,lannister,83,demoOption3,87512915182,OjRheoDl,no,zHf,27-10-2002,45775266,45775266 +4265633327,nl,Safaricom,true,Ora George,30-11-2000,greyjoy,96,demoOption2,64774439399,aBRpXlAT,yes,BeY,27-10-2002,46593969,46593969 +91402076351,nl,Safaricom,false,Travis Jackson,15-03-1990,stark,3,demoOption2,8029426814,iYEyAcTx,yes,ZWY,27-10-2002,41063163,41063163 +2553982583,en,Intersolve-voucher-whatsapp,true,Sally Carr,30-11-2000,stark,63,demoOption1,39245026607,TSwbjOpE,no,UqS,27-10-2002,45440124,45440124 +69145092797,nl,Safaricom,false,Dean Silva,15-03-1990,lannister,45,demoOption3,3764662876,rNkTTEMN,yes,iIs,16-05-1990,18413461,18413461 +36245053413,nl,Intersolve-voucher-whatsapp,false,Bobby Kelly,15-03-1990,stark,11,demoOption1,5269921421,XjCpwRKS,yes,sOF,27-10-2002,32982300,32982300 +30264950550,en,gringotts,false,William Blair,30-11-2000,greyjoy,10,demoOption1,46130838647,JRNGAyPg,yes,mlh,27-10-2002,76533412,76533412 +42780837712,nl,ironBank,false,Carrie Fleming,15-03-1990,stark,79,demoOption2,74900850311,KLZHMCsj,yes,RFC,16-05-1990,94980102,94980102 +40794409662,nl,Intersolve-voucher-whatsapp,true,Genevieve Morgan,30-11-2000,stark,80,demoOption1,4297646337,DpIAURwi,no,MZm,16-05-1990,59221868,59221868 +24832215838,en,Safaricom,true,Etta Vargas,30-11-2000,greyjoy,51,demoOption2,34209914092,kDqvYRoU,no,klL,27-10-2002,75324810,75324810 +33014140812,en,Safaricom,false,Nora Hamilton,15-03-1990,greyjoy,92,demoOption1,26349975917,FeuiFHoR,yes,iNJ,16-05-1990,44158426,44158426 +88399748846,nl,Safaricom,false,Carlos Peters,30-11-2000,stark,82,demoOption1,27417733711,gdioFVNb,no,CYt,27-10-2002,91888716,91888716 +47352879895,nl,Safaricom,true,Aaron Daniel,15-03-1990,greyjoy,72,demoOption1,38739248377,bWXNRoOS,no,nbF,16-05-1990,36543209,36543209 +85011725245,en,Safaricom,false,Mattie McBride,15-03-1990,lannister,49,demoOption3,1882601154,rvSdYfXy,no,QlI,27-10-2002,50566209,50566209 +31240401922,en,Safaricom,true,Roger Berry,30-11-2000,stark,77,demoOption3,11227664080,NYfweXoE,yes,DCp,16-05-1990,14524202,14524202 +18756743369,en,Safaricom,false,Eunice Phelps,15-03-1990,stark,62,demoOption3,60624222952,FDcVkFby,yes,sQX,27-10-2002,39592954,39592954 +35614274239,nl,Intersolve-voucher-whatsapp,true,Jeanette Peters,30-11-2000,greyjoy,39,demoOption1,61834774569,pQdoZqYA,no,cjM,27-10-2002,65123888,65123888 +40378851,nl,Safaricom,false,Ray Roberson,30-11-2000,greyjoy,51,demoOption3,43768442196,tDwQKDPL,yes,urm,16-05-1990,82642029,82642029 +50636480334,en,gringotts,false,Jim Collins,30-11-2000,greyjoy,41,demoOption3,52359796292,aRJCCDSo,yes,Ayf,16-05-1990,67896456,67896456 +17897620557,en,Safaricom,true,Rhoda Bradley,15-03-1990,lannister,15,demoOption1,27580564196,PIQDUwsU,yes,BMI,16-05-1990,84530716,84530716 +82035281970,nl,ironBank,true,Lelia Potter,30-11-2000,stark,85,demoOption2,68034469565,jJqFAPmg,yes,tFa,16-05-1990,57611219,57611219 +13672350358,nl,Safaricom,true,Danny Santos,15-03-1990,stark,16,demoOption2,29472775548,ngqzXCnt,no,fdq,27-10-2002,26015755,26015755 +76281258182,nl,Safaricom,true,Erik Sharp,15-03-1990,lannister,83,demoOption1,39266151505,sSKGZDYq,no,HtW,16-05-1990,21943852,21943852 +95555622866,en,gringotts,true,Charlotte Gardner,15-03-1990,lannister,88,demoOption1,89710764643,HdLYUWQu,yes,zes,16-05-1990,97446202,97446202 +73786093180,nl,Safaricom,true,Lenora Cox,30-11-2000,lannister,6,demoOption3,62605731518,UKwQrAbg,yes,sVc,16-05-1990,99694904,99694904 +26291298727,nl,Safaricom,false,Michael Lindsey,15-03-1990,stark,71,demoOption1,28671693919,hcfnmaQY,no,DWl,16-05-1990,39125910,39125910 +16496564931,en,Safaricom,true,Alejandro Morales,30-11-2000,lannister,72,demoOption3,73929869498,ykSklmDj,yes,vXX,16-05-1990,55045078,55045078 +17005668194,en,Safaricom,true,Helen Burton,30-11-2000,lannister,9,demoOption3,98763598914,EUPRnVbN,no,teH,27-10-2002,90778609,90778609 +61113257892,nl,Safaricom,false,Janie Haynes,30-11-2000,stark,69,demoOption1,99136756077,SrzFgDUG,no,aEn,27-10-2002,52758012,52758012 +86177673485,en,Intersolve-voucher-whatsapp,false,Addie Wagner,15-03-1990,lannister,78,demoOption1,14138226082,eAIxLEFV,yes,bDB,16-05-1990,57567256,57567256 +44079768641,nl,Safaricom,true,William Park,30-11-2000,stark,70,demoOption3,50168832485,ouiwcTMe,no,Arz,27-10-2002,44149899,44149899 +19311879944,nl,Safaricom,false,Helen Davis,15-03-1990,lannister,41,demoOption1,56043222454,mKOnizMt,no,Mbk,27-10-2002,22249055,22249055 +6574574522,nl,Safaricom,true,Lydia Mason,30-11-2000,stark,50,demoOption3,62067630439,jjBOQxLJ,yes,vde,27-10-2002,92837280,92837280 +90753941145,en,Safaricom,false,Hallie Reyes,30-11-2000,greyjoy,92,demoOption1,55095082706,nfkZhDwQ,no,IAJ,27-10-2002,16497325,16497325 +29779561496,nl,Safaricom,true,Katie Carter,30-11-2000,stark,70,demoOption1,56675146724,QLdPGZXa,yes,SRC,27-10-2002,43895532,43895532 +90272761258,nl,Intersolve-voucher-whatsapp,false,Inez Hall,15-03-1990,stark,13,demoOption1,76695340261,kGlHrbrz,yes,cgK,16-05-1990,8155531,8155531 +65436462495,en,Safaricom,false,Dennis Davidson,30-11-2000,lannister,14,demoOption3,18891081554,SFSQduYW,no,Wwk,27-10-2002,95655029,95655029 +4274251394,en,Safaricom,true,Zachary Romero,15-03-1990,stark,88,demoOption2,62561768832,DAdsPQDc,yes,btn,27-10-2002,70611871,70611871 +42132706268,en,gringotts,false,Timothy Bryant,30-11-2000,stark,59,demoOption2,92551268395,lUikwbVO,yes,jdi,16-05-1990,5151301,5151301 +79666757528,en,Safaricom,false,Jesse Patrick,30-11-2000,stark,76,demoOption2,83603052634,QXlLtgjs,yes,Iou,16-05-1990,4016921,4016921 +20236770527,en,Intersolve-voucher-whatsapp,false,Inez King,15-03-1990,greyjoy,61,demoOption2,65001097481,vLKydnEC,no,hwV,27-10-2002,25254523,25254523 +87886888012,nl,Safaricom,false,Douglas Ryan,15-03-1990,stark,67,demoOption1,81934911130,ZDswkJIe,no,uXX,16-05-1990,96958092,96958092 +49857364921,nl,Intersolve-voucher-whatsapp,true,Elizabeth Malone,15-03-1990,lannister,18,demoOption1,34091781002,ngMvnTMx,no,qWo,16-05-1990,82234945,82234945 +44423831630,en,Safaricom,false,Nicholas Wright,15-03-1990,lannister,28,demoOption3,38534797871,NYbnYgzm,no,blw,27-10-2002,64332954,64332954 +72269757928,nl,Safaricom,false,Bessie Hanson,30-11-2000,greyjoy,32,demoOption3,14312380874,LAzrvNHm,yes,kOi,27-10-2002,13487176,13487176 +18031534180,nl,Safaricom,false,Justin Gregory,15-03-1990,stark,77,demoOption1,96702517376,mMVcnqmH,no,VkV,16-05-1990,42597916,42597916 +43412833495,en,gringotts,true,Rhoda Bryan,15-03-1990,greyjoy,61,demoOption1,56509042002,MtXpdprp,no,FWR,27-10-2002,42888412,42888412 +46309508536,nl,ironBank,true,Ricky Coleman,30-11-2000,stark,51,demoOption2,61905065682,tBIGbIww,yes,tYq,27-10-2002,31243837,31243837 +29374832579,nl,ironBank,true,Caroline Goodman,15-03-1990,greyjoy,16,demoOption1,14064370909,XbZiuoWw,no,bBI,27-10-2002,84017336,84017336 +88009877626,en,Intersolve-voucher-whatsapp,true,Philip Bowen,30-11-2000,greyjoy,41,demoOption1,95377482122,cwworCSd,yes,Sfq,16-05-1990,99711938,99711938 +6175772162,nl,Safaricom,true,Vincent Collier,15-03-1990,lannister,14,demoOption2,56149191183,pwUWAiKi,yes,HCD,27-10-2002,27750156,27750156 +83461248070,en,Safaricom,true,Elmer Lloyd,15-03-1990,stark,49,demoOption3,42335959759,qFzuFsQq,no,bYa,16-05-1990,81365269,81365269 +8485285042,en,gringotts,true,Francis Tucker,30-11-2000,stark,6,demoOption1,62269898391,loEbrquT,no,ZlO,16-05-1990,71856621,71856621 +20712464388,en,Safaricom,true,Milton Ruiz,15-03-1990,stark,94,demoOption2,5568762719,HDkIKtIf,no,Xad,16-05-1990,43979610,43979610 +212050739,en,gringotts,false,Bryan Mills,15-03-1990,greyjoy,70,demoOption3,65294514107,nQBZSspl,no,ssU,27-10-2002,81253002,81253002 +39484474644,en,Safaricom,true,Christopher Griffin,30-11-2000,stark,31,demoOption2,72829154973,xQzzrzqi,no,exo,16-05-1990,96537659,96537659 +61871137925,en,Safaricom,false,Betty Day,30-11-2000,stark,34,demoOption1,8307763997,uVIRtPCg,yes,Lto,27-10-2002,95966563,95966563 +33910141924,en,Safaricom,false,Charlotte McCarthy,15-03-1990,stark,57,demoOption2,63053976553,EOVdHHMi,yes,cnw,16-05-1990,45067191,45067191 +55068551465,nl,Safaricom,false,Mathilda Stewart,30-11-2000,greyjoy,83,demoOption2,95726020805,KreeJBnb,yes,Kfj,27-10-2002,78246692,78246692 +44269586280,nl,Intersolve-voucher-whatsapp,true,Rosalie Lynch,15-03-1990,lannister,7,demoOption1,71067184460,bnLtosJw,no,ooC,16-05-1990,61676437,61676437 +67797718915,nl,Safaricom,false,Hester Robbins,15-03-1990,greyjoy,21,demoOption2,74934063648,rjdDENNx,yes,INh,27-10-2002,64672590,64672590 +84888380441,nl,Safaricom,false,Lilly McKenzie,15-03-1990,stark,72,demoOption2,55287932132,CMLtBDEj,no,OFM,27-10-2002,85594612,85594612 +66301178168,en,Safaricom,false,Daisy Figueroa,15-03-1990,greyjoy,93,demoOption2,85877032141,lFGRYQQv,no,otF,27-10-2002,24586653,24586653 +12143631641,nl,Safaricom,true,Russell Schultz,30-11-2000,greyjoy,98,demoOption1,1212422116,gPNXSKUK,yes,AFA,27-10-2002,26342831,26342831 +5140170897,nl,ironBank,false,Agnes Barnett,15-03-1990,lannister,9,demoOption2,70531448422,dMuzvFOM,no,xdc,27-10-2002,82215762,82215762 +99911010606,en,Safaricom,false,Ethan Hoffman,15-03-1990,lannister,18,demoOption2,5925143729,JMVMxSKQ,yes,aTi,16-05-1990,29878798,29878798 +44704824766,en,Safaricom,true,Ollie Hunter,30-11-2000,lannister,87,demoOption3,50214614714,GfXPjpDm,yes,cud,16-05-1990,10496266,10496266 +65180839555,nl,Safaricom,false,Ophelia Dixon,30-11-2000,greyjoy,46,demoOption1,15459938791,opAkUJAx,no,SGC,27-10-2002,75879035,75879035 +30706405573,nl,Safaricom,false,Garrett Hall,30-11-2000,stark,51,demoOption3,7009805421,sXaJyiOL,yes,ZKP,27-10-2002,16128433,16128433 +11209153958,en,Safaricom,true,Theodore Aguilar,30-11-2000,stark,48,demoOption2,25516558580,deWKHStr,yes,Icd,16-05-1990,45656851,45656851 +90149929796,en,Intersolve-voucher-whatsapp,true,Lillie Kelley,15-03-1990,lannister,89,demoOption2,20152290460,nBjeVKiQ,no,ZkC,27-10-2002,72330557,72330557 +67703856618,en,Safaricom,false,Mattie Clayton,30-11-2000,lannister,7,demoOption1,64921297609,hyDIYsug,yes,mWk,16-05-1990,72644177,72644177 +21248681351,en,Safaricom,false,Gussie Schmidt,15-03-1990,lannister,33,demoOption2,72901989673,QXFOZXrS,no,YSR,27-10-2002,36679211,36679211 +33722839723,nl,Safaricom,true,Mayme Austin,30-11-2000,greyjoy,95,demoOption2,86727519243,MWjMvNbX,yes,IRx,27-10-2002,3244859,3244859 +89286935497,en,Intersolve-voucher-whatsapp,true,Loretta Park,15-03-1990,greyjoy,0,demoOption3,77720943327,jGDNlvZx,no,CVi,27-10-2002,25871055,25871055 +14150154952,nl,Safaricom,false,Lucas Leonard,30-11-2000,stark,74,demoOption1,61357894384,kceGkorj,yes,Kpc,16-05-1990,26978235,26978235 +91407915043,nl,Intersolve-voucher-whatsapp,false,Theodore Steele,30-11-2000,lannister,37,demoOption3,75326695301,WpXUDZiV,no,cRC,16-05-1990,16431826,16431826 +87522472369,nl,Safaricom,true,Isaac Mathis,15-03-1990,stark,16,demoOption2,57778199048,ifnBvVWl,no,QRW,16-05-1990,23100498,23100498 +61522502310,en,Safaricom,true,Philip Hart,15-03-1990,greyjoy,88,demoOption3,76799587686,kMmskHgW,no,Apj,27-10-2002,35292626,35292626 +39152986818,nl,Intersolve-voucher-whatsapp,false,Victoria Nash,30-11-2000,greyjoy,0,demoOption1,82185512009,laOQrgOx,no,UNz,27-10-2002,744515,744515 +21222261859,nl,ironBank,true,Ellen Harrington,30-11-2000,stark,71,demoOption3,36939429212,NNXFuFyT,no,naR,27-10-2002,44514145,44514145 +45028953305,nl,Safaricom,false,Lucy Peterson,15-03-1990,greyjoy,31,demoOption3,34303751830,WauyvMIi,no,zgm,16-05-1990,49693038,49693038 +69220477656,en,Intersolve-voucher-whatsapp,true,Sue Harper,30-11-2000,lannister,98,demoOption2,77529358645,uXlguNck,yes,kdN,16-05-1990,25127957,25127957 +3617881552,nl,Safaricom,false,Chris Holt,30-11-2000,greyjoy,64,demoOption2,16986091713,YjLMhtiP,no,VKG,27-10-2002,81499374,81499374 +95449320522,nl,Safaricom,false,Jordan Medina,30-11-2000,greyjoy,98,demoOption3,67035806166,DDtZXieX,no,Obe,27-10-2002,32203601,32203601 +78505036724,nl,ironBank,true,Lloyd Reed,30-11-2000,greyjoy,15,demoOption2,60318988062,SbmQOaZT,no,iUt,27-10-2002,68837901,68837901 +19026584170,en,gringotts,true,Violet Blair,30-11-2000,lannister,90,demoOption1,81508579671,ZmiaQwuB,yes,EnY,27-10-2002,85936253,85936253 +8309644250,en,gringotts,false,Abbie Morton,15-03-1990,lannister,31,demoOption1,20015879787,rGigNWhN,yes,BZo,16-05-1990,29034733,29034733 +5996498612,en,gringotts,false,Violet Wade,30-11-2000,stark,17,demoOption3,59620462323,YatrxTXB,no,wLO,16-05-1990,67142391,67142391 +80097198251,en,Safaricom,false,Jayden Brown,30-11-2000,stark,62,demoOption2,23948122181,VpVVpAOT,no,oWu,16-05-1990,32910516,32910516 +68393046001,nl,ironBank,true,Alberta Evans,15-03-1990,lannister,97,demoOption3,11871508113,lbhzAgUp,no,IdF,27-10-2002,63174276,63174276 +57675846085,nl,Safaricom,true,Micheal Campbell,30-11-2000,greyjoy,23,demoOption2,78761814267,xNBeLeQx,yes,FSa,16-05-1990,63845375,63845375 +2900942393,nl,Safaricom,true,Hester Phillips,15-03-1990,greyjoy,13,demoOption1,51683053650,ttbaKXdh,no,Awt,16-05-1990,25751116,25751116 +17011537364,nl,Safaricom,true,Wesley Bailey,30-11-2000,greyjoy,20,demoOption1,60708161192,xCtLOtgF,no,Fbg,16-05-1990,90291393,90291393 +52965678386,en,Safaricom,false,Lettie Parsons,30-11-2000,greyjoy,6,demoOption2,26855478894,ayXNcRte,yes,Zta,27-10-2002,98324754,98324754 +20396769971,en,Safaricom,true,Derrick Peterson,30-11-2000,lannister,5,demoOption1,52710684361,htmHOVZs,yes,uTf,27-10-2002,53187069,53187069 +16312551604,en,gringotts,true,Katherine Hines,15-03-1990,greyjoy,68,demoOption2,68165279560,nkrYDPbn,yes,yGF,16-05-1990,48273078,48273078 +39184274611,en,Safaricom,true,Delia Reynolds,30-11-2000,lannister,97,demoOption3,67650846919,lxFQvMZU,yes,qoJ,16-05-1990,95198905,95198905 +84606173263,nl,Safaricom,true,Martin White,15-03-1990,lannister,83,demoOption3,73510865681,eeJGjQya,yes,RCe,27-10-2002,19958815,19958815 +84243201081,nl,Safaricom,true,Nathan Ramos,30-11-2000,greyjoy,48,demoOption3,38252016799,TkqgEAZB,no,GoW,16-05-1990,97152726,97152726 +91817613480,nl,Safaricom,true,Harold Ellis,15-03-1990,stark,77,demoOption2,84942626539,hVCikDvt,yes,hZW,16-05-1990,85133752,85133752 +94487393080,en,Safaricom,true,Sophia Hill,15-03-1990,lannister,66,demoOption1,81579472479,CBKardPk,no,yQO,16-05-1990,53643491,53643491 +8340201591,nl,Safaricom,true,Gregory Walton,30-11-2000,stark,39,demoOption2,35453181322,eEGyvJCL,yes,ueG,27-10-2002,78442377,78442377 +66063703405,en,Safaricom,false,Clayton Perry,15-03-1990,stark,35,demoOption2,52344340165,MFaXqrcv,yes,FMC,16-05-1990,2503316,2503316 +97899933800,nl,Safaricom,false,Tillie Ramirez,15-03-1990,lannister,43,demoOption1,78604654714,qkgKkydD,yes,DHg,27-10-2002,35555356,35555356 +43086812029,nl,Safaricom,false,Johnny McKenzie,30-11-2000,lannister,74,demoOption1,84250281221,jVwEsjlk,no,TMe,16-05-1990,20685674,20685674 +75219022641,nl,Safaricom,false,John Barnes,30-11-2000,stark,87,demoOption1,94638284678,bLmRjoXY,yes,GFZ,27-10-2002,43966438,43966438 +87395057364,en,Safaricom,false,Alex Simpson,30-11-2000,stark,78,demoOption3,70846836198,duxgVfiD,yes,cNv,16-05-1990,3686287,3686287 +29993336619,nl,ironBank,false,Eula Blair,15-03-1990,stark,43,demoOption3,82571975783,JOPnmvrI,no,aCx,16-05-1990,72585201,72585201 +9609063594,en,Safaricom,true,Luke Singleton,15-03-1990,greyjoy,58,demoOption1,25883640584,QxmFsvNV,yes,uyI,27-10-2002,88892590,88892590 +85680765987,en,Safaricom,true,Ryan Holmes,15-03-1990,lannister,25,demoOption2,28345284119,teNecvYS,yes,iUz,27-10-2002,20361034,20361034 +57614226399,en,gringotts,false,Brent Pratt,15-03-1990,lannister,37,demoOption1,73685256267,jXZoptHX,yes,HjR,27-10-2002,71579580,71579580 +51920388654,en,Intersolve-voucher-whatsapp,true,Carolyn Vargas,15-03-1990,lannister,91,demoOption1,67157403099,MDEpOosg,yes,gkn,27-10-2002,40201952,40201952 +65065965879,nl,Safaricom,true,Leah Boone,30-11-2000,lannister,22,demoOption1,6343447219,AqWpdhxb,no,BOV,16-05-1990,27886319,27886319 +9575038267,nl,Safaricom,true,Carl Sandoval,30-11-2000,lannister,28,demoOption1,59212364154,nZZVTPTF,yes,akb,16-05-1990,70103669,70103669 +61553955294,nl,ironBank,true,Daisy Bishop,15-03-1990,stark,31,demoOption3,20225199120,ZKEJIFdW,no,Gkw,16-05-1990,98816690,98816690 +87825529176,en,Safaricom,false,Ruth McBride,15-03-1990,stark,78,demoOption2,83005364643,fSTwBYNH,yes,Uma,16-05-1990,81310855,81310855 +31541086530,en,Safaricom,false,Connor Mendoza,15-03-1990,stark,49,demoOption2,30152301212,sNPMQzUP,no,MER,27-10-2002,6685020,6685020 +51842139158,en,Safaricom,false,Mario Hopkins,30-11-2000,greyjoy,69,demoOption3,76643814399,NtvrkAUZ,no,nxb,16-05-1990,3135111,3135111 +14205752964,en,Safaricom,false,Milton Reed,15-03-1990,stark,78,demoOption3,12444193709,XGRIRqjf,yes,yaw,16-05-1990,37037164,37037164 +20743409582,nl,Safaricom,true,Vera Stevenson,30-11-2000,lannister,73,demoOption1,59196563790,hgRitJwQ,yes,cVq,16-05-1990,15048230,15048230 +4060248961,en,gringotts,false,Linnie Watts,15-03-1990,greyjoy,51,demoOption1,69206701237,opSQCGma,no,vYG,16-05-1990,60116550,60116550 +84611304505,nl,Safaricom,true,Patrick Cunningham,30-11-2000,lannister,18,demoOption1,10320535416,tSRWaVld,no,NBn,16-05-1990,92878994,92878994 +57415284192,en,Safaricom,false,Noah Francis,15-03-1990,stark,25,demoOption3,51318210121,GWdZjppa,yes,qUE,27-10-2002,43308908,43308908 +90584892876,nl,ironBank,true,Derrick Hines,30-11-2000,lannister,34,demoOption2,71904584972,aGsXpAKB,yes,rhx,16-05-1990,62128380,62128380 +22651714848,en,Safaricom,false,Dominic Ray,15-03-1990,stark,87,demoOption3,95610085725,sBgnNMbx,yes,oPd,27-10-2002,48356095,48356095 +49063321330,nl,ironBank,true,Ralph Perkins,15-03-1990,stark,26,demoOption3,72617726744,vzOmAzQQ,yes,mOr,16-05-1990,77419077,77419077 +76696337196,nl,Safaricom,true,Fannie Schmidt,15-03-1990,greyjoy,64,demoOption2,38808543993,yJOWZDIt,no,BkH,16-05-1990,50085288,50085288 +92630606438,nl,Safaricom,true,Jennie Singleton,15-03-1990,greyjoy,74,demoOption3,38748608223,uRRCEICJ,yes,bSi,27-10-2002,50344166,50344166 +30039879377,en,Safaricom,false,Lettie Terry,30-11-2000,lannister,45,demoOption1,59034709477,XAXgcaCv,yes,oQy,27-10-2002,90532766,90532766 +17018590054,en,Intersolve-voucher-whatsapp,true,Lloyd Nguyen,30-11-2000,stark,95,demoOption1,78159992567,NkUVLcMN,no,nJx,16-05-1990,47743179,47743179 +42374924792,nl,Intersolve-voucher-whatsapp,true,Leroy Chambers,30-11-2000,greyjoy,27,demoOption1,89616059114,PitNGEek,yes,SVQ,27-10-2002,55013801,55013801 +61644391214,nl,ironBank,true,Vera Chavez,15-03-1990,stark,94,demoOption2,77734637890,kkMPmaOP,yes,gWg,16-05-1990,37221849,37221849 +19248893373,en,gringotts,false,Mabel Alvarado,30-11-2000,greyjoy,48,demoOption1,67938330125,Ogeozhto,no,DRG,16-05-1990,99436924,99436924 +10387605974,nl,Safaricom,true,Craig Thornton,30-11-2000,stark,5,demoOption1,99417951069,UsTZugpJ,no,Yuo,16-05-1990,78276144,78276144 +38165981837,en,Safaricom,true,Walter Larson,15-03-1990,lannister,21,demoOption3,54850767225,IKDYwHFe,no,qkS,16-05-1990,44587016,44587016 +94540668990,en,Safaricom,true,Irene Harrison,30-11-2000,stark,97,demoOption3,47433301669,dvyJyRbE,no,SWN,16-05-1990,85390877,85390877 +78476264317,en,Safaricom,false,Matilda Miller,15-03-1990,stark,73,demoOption2,46177052395,ghZIHASZ,yes,uNY,27-10-2002,47293307,47293307 +58442810151,nl,Intersolve-voucher-whatsapp,false,Adrian Colon,15-03-1990,stark,8,demoOption3,87155061733,tTfXDHVX,yes,SMJ,27-10-2002,98202114,98202114 +76000038255,en,Safaricom,true,Anthony Owen,30-11-2000,lannister,22,demoOption3,50867833340,GFNpUPRd,no,aAj,27-10-2002,2970365,2970365 +65810844205,en,Safaricom,false,Wayne Sullivan,15-03-1990,lannister,46,demoOption3,15159318597,pbRJtJDB,yes,qnw,16-05-1990,52516745,52516745 +6956924307,en,Safaricom,true,Duane Silva,15-03-1990,greyjoy,43,demoOption3,20191985090,ilQWKdlO,no,Lri,27-10-2002,84388560,84388560 +6594592158,nl,Safaricom,true,Mina Wong,30-11-2000,lannister,77,demoOption3,88921772879,nNxvhFNO,yes,hNI,16-05-1990,56774432,56774432 +17364456240,nl,Intersolve-voucher-whatsapp,true,Beatrice Leonard,30-11-2000,lannister,31,demoOption2,79773033684,idnQhVED,yes,qaP,27-10-2002,83795712,83795712 +79759936148,en,Safaricom,true,Joe Pena,15-03-1990,lannister,68,demoOption1,99392817195,qxcndFaA,no,PsC,16-05-1990,6883277,6883277 +76604554578,en,gringotts,false,Scott Campbell,15-03-1990,stark,93,demoOption1,78119721903,HUnSLuvn,no,dwZ,27-10-2002,35436543,35436543 +51426810829,nl,Safaricom,false,Margaret Carlson,30-11-2000,greyjoy,98,demoOption2,62676034591,dSizTqBA,yes,Dva,27-10-2002,46378703,46378703 +53536040833,nl,Safaricom,false,Olga Clayton,30-11-2000,lannister,89,demoOption3,24040886370,TlBwwMuK,no,FOJ,27-10-2002,95964915,95964915 +51169655265,en,Safaricom,true,Belle Cole,30-11-2000,greyjoy,66,demoOption3,52520689985,kHlsZfhw,yes,bOa,27-10-2002,96318149,96318149 +34981114454,nl,Safaricom,true,Hester Haynes,15-03-1990,greyjoy,72,demoOption1,49063821386,iwnNihPz,no,LJI,16-05-1990,83406846,83406846 +21142150140,en,gringotts,true,Maurice Barker,15-03-1990,stark,23,demoOption3,29094019274,HIAlOSCi,no,UbI,27-10-2002,789747,789747 +83435690634,en,Safaricom,true,Lettie Ryan,30-11-2000,greyjoy,20,demoOption2,10596812884,jVYtSzdS,yes,oSK,16-05-1990,52375653,52375653 +14485155645,en,Intersolve-voucher-whatsapp,false,Katie Bass,15-03-1990,stark,86,demoOption1,10141100520,WuFqqhId,yes,dYe,16-05-1990,46425656,46425656 +52325056996,en,Intersolve-voucher-whatsapp,true,Virgie Gonzalez,15-03-1990,greyjoy,42,demoOption3,90208560311,VzrozIVj,yes,Rxc,27-10-2002,76111344,76111344 +72329828858,nl,Safaricom,false,Alan Peterson,30-11-2000,stark,78,demoOption1,20587743237,AdgbgApA,yes,gGY,16-05-1990,41124950,41124950 +63256152478,nl,Safaricom,true,Ruby Castro,15-03-1990,lannister,94,demoOption2,60892246450,VENrhkkq,no,Grz,16-05-1990,33259416,33259416 +13697055766,en,Safaricom,true,Leroy Nash,30-11-2000,lannister,32,demoOption1,31686771313,GUmMohLH,no,kWm,16-05-1990,63903904,63903904 +71120102317,nl,Safaricom,true,Dennis Vasquez,15-03-1990,stark,33,demoOption2,50771685308,GBXnwgcA,yes,EVO,16-05-1990,2662056,2662056 +38288755067,en,Safaricom,false,Virginia Park,15-03-1990,lannister,66,demoOption3,51039865822,AYGjmLIT,no,huh,16-05-1990,69292371,69292371 +70769927089,en,gringotts,true,Bertha McKenzie,30-11-2000,greyjoy,63,demoOption3,23999636120,KgDizFNi,yes,CKG,16-05-1990,93189179,93189179 +10700883747,en,Safaricom,false,Marion Tucker,15-03-1990,lannister,65,demoOption2,54118270516,oVIcHpaJ,no,mTC,16-05-1990,70900455,70900455 +70190667839,nl,ironBank,true,Steve Obrien,15-03-1990,lannister,50,demoOption1,37602707227,YAIeedsu,yes,VUy,16-05-1990,74272089,74272089 +41191398158,en,Safaricom,true,Isaac Perez,15-03-1990,greyjoy,87,demoOption1,53684020121,OXBhPThV,no,ZGK,16-05-1990,23963385,23963385 +48216946224,nl,Safaricom,true,Mary Byrd,30-11-2000,stark,61,demoOption1,90546896168,LIqmVrIY,yes,pef,16-05-1990,43016553,43016553 +21387039755,nl,ironBank,false,Lora Castro,15-03-1990,lannister,66,demoOption2,12376615040,mPuJBcAc,yes,hpE,27-10-2002,23248019,23248019 +34553669280,en,Safaricom,false,Philip Webb,30-11-2000,lannister,35,demoOption3,57203414822,vhRzsmIg,no,IgI,27-10-2002,15557060,15557060 +51125219877,nl,Safaricom,false,Florence Norton,15-03-1990,greyjoy,84,demoOption2,89430675494,NvCvHqgC,yes,edJ,27-10-2002,93395145,93395145 +98755387549,nl,ironBank,false,Dollie Hayes,30-11-2000,greyjoy,51,demoOption1,16374530194,WFpCMDFN,yes,zws,27-10-2002,60283155,60283155 +42870336279,en,Safaricom,false,Jerry Bush,30-11-2000,stark,87,demoOption3,30611947340,ASLdLlty,no,SmH,16-05-1990,68287991,68287991 +5460480845,nl,Safaricom,true,Polly Henry,30-11-2000,greyjoy,39,demoOption1,40410700718,bQUbPjVs,no,HXe,27-10-2002,53316380,53316380 +80841807256,nl,ironBank,false,Lucas Malone,15-03-1990,stark,62,demoOption3,87974412192,zGdgFKyN,no,sow,27-10-2002,35853454,35853454 +38498529292,en,Safaricom,true,Stanley Frazier,30-11-2000,greyjoy,86,demoOption1,91758421758,NduwEjWo,yes,Cdv,16-05-1990,70106811,70106811 +75050356268,en,Safaricom,true,Benjamin Nichols,30-11-2000,lannister,67,demoOption3,21734397017,gYLVZQvi,yes,NxM,27-10-2002,13272273,13272273 +69164066969,en,Safaricom,true,Marian Berry,15-03-1990,greyjoy,54,demoOption3,59500667789,pgeDPmvG,yes,ZKx,27-10-2002,99569305,99569305 +55839502198,en,Safaricom,true,Herman Swanson,15-03-1990,stark,64,demoOption2,75263812109,NWIQDroL,no,kNB,27-10-2002,86832258,86832258 +33865776857,en,Safaricom,true,Matthew Baker,15-03-1990,stark,45,demoOption3,41748935256,lswToAxw,no,otl,27-10-2002,8791442,8791442 +57995472485,en,gringotts,false,Emily Hardy,15-03-1990,greyjoy,50,demoOption2,76530492623,AySGgYpL,yes,Vji,27-10-2002,58890270,58890270 +28314364018,nl,Safaricom,false,Johanna Wilson,30-11-2000,stark,84,demoOption2,20045438351,frkvTBpy,yes,kYU,16-05-1990,71565795,71565795 +31443774745,en,gringotts,true,Ruby Vasquez,30-11-2000,greyjoy,12,demoOption2,89154221723,VSIXNhxT,no,nWH,16-05-1990,634722,634722 +37983683705,nl,Safaricom,false,Isaac White,30-11-2000,stark,14,demoOption1,17413838353,iObXxqcB,no,xhY,27-10-2002,76881337,76881337 +46715867487,nl,ironBank,true,Jesus McCormick,30-11-2000,lannister,97,demoOption2,33932628515,QAkOnenT,yes,xEb,16-05-1990,30271755,30271755 +71588961319,en,Intersolve-voucher-whatsapp,false,Lelia Logan,15-03-1990,stark,57,demoOption3,98679402120,JDGkFVoK,yes,vGO,16-05-1990,45689400,45689400 +22054693128,nl,Safaricom,false,Bertha Bishop,30-11-2000,lannister,27,demoOption1,39371964246,BjsxXSDg,no,cLX,16-05-1990,59551981,59551981 +71876768115,nl,ironBank,false,Dennis Sutton,15-03-1990,lannister,11,demoOption3,2525262806,yHYpiBzQ,no,yDo,27-10-2002,27411282,27411282 +10471220772,en,Safaricom,true,Eric Pena,30-11-2000,greyjoy,99,demoOption3,29532211262,UmRokKNt,yes,GSe,27-10-2002,27331334,27331334 +74979498090,nl,ironBank,true,Lucas Pratt,30-11-2000,greyjoy,99,demoOption1,66779975707,LRZydCGP,no,GTY,27-10-2002,40854395,40854395 +27160242829,en,Safaricom,false,Brett Luna,15-03-1990,lannister,53,demoOption1,17529208639,nuicIAMI,yes,BOl,16-05-1990,49526872,49526872 +86190465788,en,Safaricom,true,Julian Santiago,30-11-2000,stark,48,demoOption1,45946424457,BnZLTyJM,no,GGB,27-10-2002,6724880,6724880 +48076066820,en,Intersolve-voucher-whatsapp,true,Jane Reyes,30-11-2000,stark,10,demoOption3,49968633474,fnUyBPeo,yes,LEH,16-05-1990,41636052,41636052 +51227356535,nl,ironBank,false,Lily Vasquez,30-11-2000,greyjoy,1,demoOption3,24246685856,QrqnOpZi,yes,KYr,27-10-2002,62463325,62463325 +63508275814,en,gringotts,false,Nathan Ramos,15-03-1990,greyjoy,25,demoOption2,52687273770,EKCiRONY,no,exP,27-10-2002,88028547,88028547 +6877383764,nl,Safaricom,false,Dominic Watts,15-03-1990,lannister,58,demoOption1,98685607024,pHLFFYlg,no,lRu,16-05-1990,8624383,8624383 +74456307182,en,gringotts,false,Elmer Harrison,30-11-2000,lannister,86,demoOption1,29256634106,RXanMcjr,no,XqX,16-05-1990,26799167,26799167 +46155884851,nl,Intersolve-voucher-whatsapp,false,Sophia Bell,15-03-1990,greyjoy,12,demoOption2,68027855603,IJEtGsOg,no,WaM,16-05-1990,27743027,27743027 +3814962270,en,gringotts,true,Lizzie Reynolds,15-03-1990,greyjoy,46,demoOption3,11176230479,fWTuOJsO,no,aRn,27-10-2002,31078760,31078760 +94777264225,en,gringotts,false,Catherine Barker,15-03-1990,lannister,46,demoOption1,84216779859,zjfVTTQa,yes,Kua,27-10-2002,95912266,95912266 +64457667687,nl,Safaricom,true,Shane Oliver,15-03-1990,greyjoy,65,demoOption2,47328468867,dMtRLcrP,yes,yCT,16-05-1990,89506199,89506199 +99760726605,en,Intersolve-voucher-whatsapp,true,Maggie Harrington,15-03-1990,greyjoy,3,demoOption2,51774685587,WPQGRtJh,yes,oGc,27-10-2002,26689665,26689665 +17413412429,nl,Safaricom,false,Nora Howard,15-03-1990,stark,62,demoOption2,50146324400,fsxPxeuM,yes,ZVQ,16-05-1990,3774172,3774172 +94298915487,nl,Intersolve-voucher-whatsapp,false,Earl Joseph,15-03-1990,greyjoy,95,demoOption3,41373282884,qzghngmw,no,nTB,27-10-2002,63741776,63741776 +65863334408,en,Safaricom,true,Beatrice Riley,15-03-1990,greyjoy,57,demoOption1,20566942282,JDYHxIdW,no,cAd,27-10-2002,30968002,30968002 +11586444651,nl,Intersolve-voucher-whatsapp,true,Abbie Chandler,15-03-1990,lannister,92,demoOption1,97175054015,tpCWCrsP,yes,VnZ,27-10-2002,36335578,36335578 +47655785844,en,Intersolve-voucher-whatsapp,false,Johnny Chapman,30-11-2000,greyjoy,86,demoOption1,96093631428,TeevoIbc,no,xwi,16-05-1990,22603703,22603703 +57019630827,nl,ironBank,true,Matilda Reynolds,15-03-1990,stark,1,demoOption3,82097510187,LMbYaHiu,no,btr,16-05-1990,480272,480272 +98241195513,nl,Safaricom,false,Mitchell Sparks,30-11-2000,stark,84,demoOption2,93543117223,WhzRsHUJ,no,PAF,16-05-1990,82561941,82561941 +88884250981,en,Safaricom,false,Olga Hart,30-11-2000,stark,74,demoOption1,34407217332,CbivTuhi,yes,KFG,27-10-2002,98487742,98487742 +65997362116,en,Safaricom,true,Olga Harvey,15-03-1990,stark,70,demoOption1,79211174295,VkjHoYcq,no,UsM,16-05-1990,99922776,99922776 +34162176197,nl,ironBank,true,Nancy Pierce,30-11-2000,stark,19,demoOption1,74026277144,oISUrXLQ,yes,dky,16-05-1990,36099038,36099038 +11894153337,en,gringotts,true,Amelia Hunter,30-11-2000,greyjoy,16,demoOption3,67761409433,RWinpiWt,yes,CJa,27-10-2002,20661272,20661272 +96270626784,nl,ironBank,true,Andrew Klein,15-03-1990,lannister,7,demoOption2,10721868840,ptsFQuRW,yes,nEa,27-10-2002,62740045,62740045 +5182727797,en,Safaricom,true,Kenneth Taylor,15-03-1990,greyjoy,29,demoOption1,14590602132,IjMfJAHF,yes,vww,27-10-2002,41394529,41394529 +16761330492,en,Safaricom,true,Aiden Carson,30-11-2000,lannister,6,demoOption3,60114749592,duJbhmdG,yes,Rvm,27-10-2002,40230931,40230931 +55824397632,en,Intersolve-voucher-whatsapp,true,Gertrude Mason,30-11-2000,stark,18,demoOption1,94219887484,pwjkbLze,yes,RzD,27-10-2002,1193998,1193998 +50601174408,nl,ironBank,false,Gussie Roberts,15-03-1990,greyjoy,65,demoOption3,36531387487,lojpwVnC,no,Vde,16-05-1990,89080951,89080951 +32758965517,nl,ironBank,true,Mary Herrera,15-03-1990,lannister,13,demoOption2,48162723643,vvfGhvTe,yes,prG,16-05-1990,67203658,67203658 +23433097913,en,Safaricom,false,Addie Caldwell,15-03-1990,lannister,15,demoOption3,96294749257,ZIEuElSF,no,hMf,16-05-1990,85065874,85065874 +95720196183,en,Safaricom,true,Gregory Buchanan,15-03-1990,greyjoy,1,demoOption1,24168988890,DDjeFeKf,yes,Qfd,16-05-1990,72174562,72174562 +75616224735,nl,ironBank,true,Lela Cohen,30-11-2000,lannister,7,demoOption3,1015020364,azdeXcDZ,no,IJQ,27-10-2002,64487845,64487845 +96128641475,nl,Safaricom,false,Lawrence Spencer,15-03-1990,lannister,44,demoOption2,94746809829,BUnuiWZl,no,rwD,16-05-1990,45003960,45003960 +33633783071,en,Safaricom,true,James Dixon,15-03-1990,greyjoy,14,demoOption2,11353958728,lLTlGKIm,no,ANA,27-10-2002,98588694,98588694 +92944232552,nl,Safaricom,true,Jordan Hayes,15-03-1990,lannister,74,demoOption3,93474191,cHvNXdEr,yes,sfH,27-10-2002,72175677,72175677 +74537684769,nl,ironBank,false,Hilda Buchanan,15-03-1990,stark,19,demoOption1,80594552146,UVfpeVpw,no,Xpv,16-05-1990,46779546,46779546 +68426092863,nl,Safaricom,true,Chris Glover,30-11-2000,lannister,90,demoOption1,51058705787,HCFdbKWK,yes,Cqq,27-10-2002,77915774,77915774 +74688663001,nl,Safaricom,true,Evan Willis,30-11-2000,stark,18,demoOption3,85450260668,uIsIoSaS,yes,cOi,16-05-1990,31655053,31655053 +11097899169,en,Safaricom,true,Fred Christensen,15-03-1990,lannister,75,demoOption3,58883688002,APbChKga,yes,wYs,27-10-2002,21453832,21453832 +45255203426,en,gringotts,false,Rachel Dunn,30-11-2000,stark,95,demoOption3,15677128839,UUQtHHaw,no,pGC,27-10-2002,32859281,32859281 +3436738333,nl,Safaricom,true,Hettie Stokes,15-03-1990,lannister,71,demoOption1,73086534326,jENQggEG,yes,MEy,16-05-1990,88696889,88696889 +92091078721,en,Intersolve-voucher-whatsapp,true,Abbie Joseph,30-11-2000,stark,37,demoOption1,86599058210,GzIrhcUO,no,uaq,16-05-1990,59961067,59961067 +86680742699,en,gringotts,false,Bessie Murphy,15-03-1990,lannister,93,demoOption2,77891364336,deOPyKho,yes,SQv,27-10-2002,38016206,38016206 +42584212274,nl,Safaricom,false,Winifred Lucas,30-11-2000,lannister,8,demoOption3,31069828121,VQNrHkLL,no,Vzn,27-10-2002,30935567,30935567 +25549085370,en,gringotts,true,Earl Ferguson,30-11-2000,greyjoy,76,demoOption1,90522340904,vmdZEhci,yes,vmz,27-10-2002,385217,385217 +66720285997,en,gringotts,false,Austin Fields,30-11-2000,greyjoy,13,demoOption2,12631412491,CWlJeAnZ,yes,xKQ,16-05-1990,24786737,24786737 +25346274734,en,Safaricom,true,Chase Munoz,15-03-1990,greyjoy,34,demoOption3,21286040095,NanpAqep,no,kaS,27-10-2002,15673270,15673270 +28238946817,nl,Intersolve-voucher-whatsapp,true,Darrell Cobb,15-03-1990,greyjoy,39,demoOption3,60885598563,KeBhoLgy,no,CVQ,16-05-1990,1902568,1902568 +56008757827,nl,Safaricom,false,Caroline Bailey,30-11-2000,lannister,98,demoOption1,60561834352,cjFcAcwc,yes,OOi,27-10-2002,29870280,29870280 +93491206462,nl,Safaricom,false,Maria Vargas,15-03-1990,lannister,28,demoOption1,20737214247,kuFbDODn,no,CAQ,27-10-2002,37195139,37195139 +70725657113,en,Intersolve-voucher-whatsapp,false,Albert Daniel,30-11-2000,greyjoy,50,demoOption1,50586222282,HNWZRsHa,yes,Awa,16-05-1990,41924067,41924067 +41969185730,nl,Safaricom,false,Hattie Harper,15-03-1990,greyjoy,28,demoOption1,70479031519,QWRURGVH,no,WOg,16-05-1990,35777531,35777531 +80555726552,en,Safaricom,true,Adam Garrett,15-03-1990,lannister,42,demoOption2,81604211586,FJdujgXy,yes,tKD,16-05-1990,68094261,68094261 +26364030217,en,Safaricom,false,Jonathan Hunt,15-03-1990,greyjoy,70,demoOption2,33071130847,RhILoXNx,yes,liJ,16-05-1990,8268410,8268410 +76281440938,nl,ironBank,true,Jennie Caldwell,30-11-2000,lannister,40,demoOption1,75586581896,zuQxuHzK,yes,vvP,16-05-1990,50211573,50211573 +92259104235,en,Safaricom,true,Ryan Jefferson,15-03-1990,greyjoy,38,demoOption3,20532014697,HUJvmkyj,no,FIA,16-05-1990,1512509,1512509 +71579119523,nl,ironBank,false,Lettie Logan,30-11-2000,lannister,85,demoOption1,8212793277,FkZupvTe,yes,eKY,16-05-1990,44121244,44121244 +46868146396,en,Safaricom,true,Joseph Henry,30-11-2000,stark,30,demoOption1,33473476597,JJaoNvVQ,yes,FZz,27-10-2002,64327253,64327253 +57807489749,en,Intersolve-voucher-whatsapp,true,Adele Roberts,15-03-1990,stark,95,demoOption3,79324852916,rYdBAqKL,no,FJI,16-05-1990,35638952,35638952 +21084018749,nl,Safaricom,true,Rose Nelson,15-03-1990,greyjoy,15,demoOption2,48046197901,iWkoljVf,yes,eIv,27-10-2002,42627212,42627212 +18363625309,nl,Safaricom,true,Elizabeth Wilkerson,15-03-1990,stark,19,demoOption1,97986497215,KFgUasgd,no,Nzd,16-05-1990,14375980,14375980 +89164012329,nl,ironBank,true,Josephine Garza,30-11-2000,greyjoy,51,demoOption1,65211079263,YwpMeueK,no,Jeb,16-05-1990,65268582,65268582 +52775795261,en,Safaricom,false,Christopher Frazier,15-03-1990,greyjoy,20,demoOption2,22870245975,zDcxnhXd,yes,uWg,16-05-1990,16764022,16764022 +44856428558,en,Safaricom,true,Lillian Bell,30-11-2000,stark,82,demoOption3,55989697653,TDQdVPod,yes,Psj,16-05-1990,7883270,7883270 +16473596431,nl,ironBank,true,Gary Miller,15-03-1990,greyjoy,6,demoOption3,26675966814,djXNIwNy,no,cOu,27-10-2002,87362180,87362180 +87215965246,nl,Intersolve-voucher-whatsapp,false,Mason Klein,15-03-1990,greyjoy,23,demoOption1,32849589235,DpikdDYz,no,pSX,27-10-2002,27790343,27790343 +72318505528,nl,Intersolve-voucher-whatsapp,false,Rodney Strickland,30-11-2000,lannister,4,demoOption2,60418708440,yoCPoBhj,yes,JHt,16-05-1990,5701279,5701279 +47334872604,en,Intersolve-voucher-whatsapp,false,Ruby Ramos,15-03-1990,stark,97,demoOption1,47596689419,EdByxHDi,yes,Zby,27-10-2002,80325998,80325998 +57349871949,nl,ironBank,true,Jordan Shaw,15-03-1990,greyjoy,10,demoOption3,77985663937,wvCbrQIj,yes,tUB,16-05-1990,43510787,43510787 +39080257133,en,Intersolve-voucher-whatsapp,true,Brent Harvey,15-03-1990,greyjoy,65,demoOption3,52495562034,tPuehiuh,no,Pnf,16-05-1990,74820896,74820896 +3094093495,en,Safaricom,false,Mattie Perkins,30-11-2000,stark,32,demoOption1,83893445923,zfeoxMLL,yes,mYb,16-05-1990,69566833,69566833 +12244077023,en,Intersolve-voucher-whatsapp,true,Fred Lindsey,15-03-1990,lannister,3,demoOption2,37975812603,khfxBVCC,no,gjy,27-10-2002,15065480,15065480 +63106503350,nl,Intersolve-voucher-whatsapp,false,Amelia Delgado,15-03-1990,greyjoy,3,demoOption3,26248370480,FpeNBBcd,no,XdI,16-05-1990,5098457,5098457 +9345930852,en,Safaricom,false,Lela Ortiz,30-11-2000,stark,84,demoOption2,2746436152,dcfAQNPC,yes,mbb,27-10-2002,17160747,17160747 +80919211322,en,Safaricom,true,Nathaniel Park,30-11-2000,greyjoy,31,demoOption3,18821864357,SWMXnOLN,yes,yWl,27-10-2002,8007155,8007155 +8404772626,en,Safaricom,true,Inez Evans,30-11-2000,stark,21,demoOption1,15076225709,SGtLMEZT,yes,lWB,16-05-1990,38153968,38153968 +7417051385,nl,Safaricom,true,Rosetta Russell,15-03-1990,greyjoy,55,demoOption1,33783534816,NUCAbZPp,no,xgr,16-05-1990,24207267,24207267 +4360486447,nl,Safaricom,true,Craig Warren,15-03-1990,lannister,61,demoOption2,76195796791,XhVVVUva,yes,Lwp,16-05-1990,60278427,60278427 +63454887278,en,Intersolve-voucher-whatsapp,true,Blake Wise,15-03-1990,stark,94,demoOption3,79172136610,FpaCUsMk,no,iFd,27-10-2002,83221926,83221926 +56019854694,nl,Safaricom,true,Oscar Rose,30-11-2000,lannister,47,demoOption2,17536355187,fmyZRAjT,no,Tml,16-05-1990,10638330,10638330 +89486908811,en,Safaricom,false,Francis Munoz,15-03-1990,lannister,9,demoOption1,85822980992,uSeBusMu,yes,RtK,27-10-2002,20813727,20813727 +50991083152,nl,ironBank,false,Teresa Hansen,15-03-1990,greyjoy,78,demoOption2,10404714051,RRIcaYJR,no,tHY,16-05-1990,19004426,19004426 +38941025190,nl,Safaricom,false,Elsie Hodges,30-11-2000,lannister,71,demoOption2,12329349527,rNhjmeCP,yes,QDR,16-05-1990,16731896,16731896 +99744840750,en,gringotts,true,Adele Norman,15-03-1990,lannister,75,demoOption2,43604046290,vNKwDeYz,no,aFX,27-10-2002,86068239,86068239 +53238730897,en,gringotts,false,Nathaniel Abbott,30-11-2000,lannister,50,demoOption3,82600768077,UThwzFgI,yes,ejC,16-05-1990,64226077,64226077 +37829509258,en,Safaricom,false,Mike Ballard,30-11-2000,stark,1,demoOption3,79289324107,XWhBGUIF,yes,aZc,27-10-2002,30129276,30129276 +70533879017,en,Safaricom,true,Gussie Conner,15-03-1990,stark,52,demoOption3,84387378820,AveYusAh,yes,LAu,16-05-1990,25395266,25395266 +28698299394,nl,Safaricom,false,Bill Brady,30-11-2000,stark,42,demoOption2,18123096291,aXnxnKoF,yes,TkK,16-05-1990,49149665,49149665 +36437800762,en,Intersolve-voucher-whatsapp,false,Mina Powell,30-11-2000,lannister,39,demoOption1,13930331373,gxGgkQdo,no,NOn,16-05-1990,14101704,14101704 +49421270872,nl,Safaricom,false,Beatrice Sanchez,30-11-2000,greyjoy,29,demoOption1,40996654967,bQNagohG,no,xOd,16-05-1990,65450577,65450577 +63364564099,en,Safaricom,false,Gordon Wilson,30-11-2000,lannister,96,demoOption3,82431874389,LdYaBSEe,yes,cLq,27-10-2002,40367330,40367330 +26126178628,nl,Safaricom,false,Estelle Cox,30-11-2000,stark,48,demoOption1,18600126533,NoXtioRe,yes,wwz,27-10-2002,5927650,5927650 +99600064538,nl,Safaricom,true,Sadie Jimenez,15-03-1990,lannister,98,demoOption3,10243585343,ruDbAsFJ,no,Hlu,27-10-2002,25835697,25835697 +39326795763,en,Safaricom,true,Carrie Burke,30-11-2000,greyjoy,47,demoOption1,71776897612,YrVIanNq,yes,ZVw,16-05-1990,76472476,76472476 +91386266066,nl,Safaricom,true,Walter Jefferson,30-11-2000,lannister,38,demoOption1,77028086460,JRiZyUTL,yes,wzm,27-10-2002,58510199,58510199 +99731194703,en,Safaricom,true,Loretta Wong,30-11-2000,lannister,80,demoOption3,53164814587,AQxedOew,no,WzD,27-10-2002,94416688,94416688 +52163485292,en,Safaricom,true,Theresa Pittman,15-03-1990,stark,23,demoOption2,98668152544,kGwhZzil,no,ubJ,27-10-2002,35323280,35323280 +5112693636,en,Safaricom,true,Jane Hardy,30-11-2000,lannister,74,demoOption1,70625372745,ULSpBcQp,no,bfR,16-05-1990,30868353,30868353 +90833288682,en,Safaricom,true,Isabella Clarke,30-11-2000,stark,15,demoOption3,95294809298,OyTTzSUk,no,FCm,16-05-1990,44198804,44198804 +5612630729,en,gringotts,false,Rodney May,15-03-1990,greyjoy,21,demoOption3,50762345833,qYbMHsYu,no,qmP,27-10-2002,75888720,75888720 +96301646898,en,gringotts,false,Mayme Blair,15-03-1990,stark,78,demoOption3,96472324836,TYwwUEUg,no,ZVe,27-10-2002,20364624,20364624 +46897886418,en,gringotts,false,Daniel Thornton,30-11-2000,lannister,11,demoOption2,12649392443,hStUSvfo,yes,yfl,16-05-1990,65521169,65521169 +20943679443,nl,ironBank,true,Jerry Bailey,30-11-2000,greyjoy,58,demoOption1,29654241953,koGpTVnk,no,Thh,27-10-2002,9784959,9784959 +96829214991,en,gringotts,true,Daisy Floyd,30-11-2000,lannister,76,demoOption1,21833322904,YUzfTyDi,yes,lkF,27-10-2002,22781731,22781731 +6972270004,en,Intersolve-voucher-whatsapp,true,Louisa Ryan,15-03-1990,stark,31,demoOption1,52045994411,ExyOIbgK,no,wiP,27-10-2002,5172399,5172399 +67486466788,nl,Intersolve-voucher-whatsapp,true,Keith Mills,30-11-2000,lannister,30,demoOption1,92672983644,JthvKzHd,no,ZME,16-05-1990,33529842,33529842 +26336555654,nl,ironBank,false,Dora Stanley,15-03-1990,lannister,50,demoOption2,31908625455,BCkLtoDH,no,iZp,27-10-2002,72568661,72568661 +83242831467,en,Safaricom,true,Lela Sullivan,30-11-2000,greyjoy,6,demoOption1,79007347178,MCSbqwsz,yes,XiL,27-10-2002,74325417,74325417 +57225597127,nl,Intersolve-voucher-whatsapp,true,Danny Gibson,15-03-1990,greyjoy,52,demoOption3,25000382906,oaQHklqi,yes,ZoN,27-10-2002,60379765,60379765 +26863988248,en,Safaricom,false,Brian Haynes,30-11-2000,greyjoy,68,demoOption3,47230764793,ElyLRKVe,no,Kha,27-10-2002,4276445,4276445 +5824013954,nl,Intersolve-voucher-whatsapp,false,Francisco Conner,30-11-2000,stark,40,demoOption2,47209300324,jLFsVyrp,no,HRe,16-05-1990,52848980,52848980 +40092618219,nl,Safaricom,true,Frank Patterson,30-11-2000,greyjoy,63,demoOption2,27918620970,svlfUIjM,no,kuc,27-10-2002,48363471,48363471 +21334829670,en,Safaricom,false,Philip Cruz,15-03-1990,greyjoy,49,demoOption3,38533811804,dFfmvclo,yes,lJu,27-10-2002,75329581,75329581 +94213203511,nl,Safaricom,true,Alexander Delgado,30-11-2000,greyjoy,47,demoOption2,12806414323,LxdIHIfo,no,pol,16-05-1990,64839898,64839898 +71370412384,en,Safaricom,true,Cameron Lamb,15-03-1990,lannister,15,demoOption3,42319621587,mIfLUvaZ,no,CBp,27-10-2002,62957898,62957898 +11191401426,en,Intersolve-voucher-whatsapp,true,Alex Hernandez,30-11-2000,lannister,61,demoOption1,28273129931,usoAAuKH,yes,DCj,16-05-1990,78692031,78692031 +26056176661,nl,Safaricom,true,Rosie Alvarado,15-03-1990,stark,85,demoOption1,96768419039,hQcovZbX,yes,ykR,16-05-1990,7167360,7167360 +62176676089,en,Safaricom,false,Jose Morrison,30-11-2000,greyjoy,50,demoOption3,29855848295,QlkDHDDV,yes,YEx,16-05-1990,1233532,1233532 +79187488315,nl,ironBank,false,Hilda Stevenson,15-03-1990,lannister,84,demoOption2,26502138993,aaymVWFV,no,LyO,16-05-1990,87562039,87562039 +77240201078,nl,ironBank,true,Elnora Cook,15-03-1990,greyjoy,98,demoOption3,82850353485,eJVahWzF,yes,SoG,27-10-2002,64629971,64629971 +68611717423,en,gringotts,false,Grace Hogan,15-03-1990,greyjoy,6,demoOption2,3899264141,ssQSNNyg,yes,geq,27-10-2002,77185602,77185602 +85207493952,en,Safaricom,true,Carl Pittman,15-03-1990,greyjoy,65,demoOption2,27873119583,FXictwNu,yes,kmQ,16-05-1990,74465025,74465025 +26798925557,nl,Safaricom,true,Miguel Jensen,30-11-2000,greyjoy,39,demoOption3,27147041262,XKAYlhec,no,MSL,27-10-2002,34182064,34182064 +21359257526,nl,Safaricom,true,Kevin Harmon,30-11-2000,greyjoy,94,demoOption1,69791212783,krWtbeuA,yes,qRg,27-10-2002,77872063,77872063 +35774027091,nl,Safaricom,false,Sarah Glover,30-11-2000,stark,91,demoOption1,60323878690,vjmHcYez,no,XcD,16-05-1990,98834848,98834848 +58180405341,en,Safaricom,false,Virgie Higgins,30-11-2000,stark,66,demoOption2,33396805715,nQXKUUeF,no,kmB,16-05-1990,43524208,43524208 +71105975632,en,Intersolve-voucher-whatsapp,true,Sylvia Holmes,15-03-1990,lannister,91,demoOption1,73492396408,BUbaYDrc,yes,NHx,16-05-1990,91158008,91158008 +28659777894,nl,Intersolve-voucher-whatsapp,true,Francis Spencer,30-11-2000,greyjoy,78,demoOption1,64311209887,zrkWQSFn,yes,taS,16-05-1990,23425824,23425824 +80594162331,nl,Safaricom,true,Sue Peters,30-11-2000,stark,23,demoOption2,61387060046,oiAKxLGJ,no,bLo,16-05-1990,95271912,95271912 +54700483585,en,Safaricom,true,Leah Black,15-03-1990,greyjoy,42,demoOption1,13912624543,GbDLzvhh,no,Tzy,27-10-2002,27133786,27133786 +13129636355,en,Safaricom,false,Mabelle Tyler,30-11-2000,greyjoy,35,demoOption3,24019371668,rxdETYed,no,DaB,27-10-2002,45448017,45448017 +25176550609,nl,Safaricom,false,Jerome Abbott,15-03-1990,greyjoy,75,demoOption1,49530166819,bjixjFkl,yes,VdY,27-10-2002,53926284,53926284 +66544322667,nl,Safaricom,true,Dylan Hall,15-03-1990,greyjoy,61,demoOption3,23696122332,NYFhwfXO,yes,vdp,16-05-1990,67876731,67876731 +86286585706,en,gringotts,false,Eric Sherman,30-11-2000,stark,45,demoOption1,3439977666,uBKIVNxA,yes,wqV,27-10-2002,39383113,39383113 +33422301167,nl,Safaricom,false,Carlos Stone,30-11-2000,lannister,6,demoOption1,96673068871,JywpImdF,no,zby,27-10-2002,57842231,57842231 +88282266917,en,Safaricom,true,Stella Smith,15-03-1990,stark,33,demoOption1,38995316486,XMwtBGfJ,yes,sHE,27-10-2002,8870083,8870083 +80452611442,nl,ironBank,false,Tony Atkins,15-03-1990,stark,88,demoOption1,98862159712,BLvvpJCB,yes,ush,16-05-1990,20979281,20979281 +19078005257,en,Safaricom,true,Mina Mann,30-11-2000,stark,37,demoOption3,8857294576,dWJKznff,yes,HWQ,16-05-1990,54693357,54693357 +75961480975,nl,ironBank,false,Benjamin Potter,15-03-1990,lannister,17,demoOption2,77692749265,uumTEiPJ,no,LBF,16-05-1990,74376285,74376285 +64817034038,en,Safaricom,true,Belle Sutton,30-11-2000,greyjoy,91,demoOption2,16930402002,sMHCCxSV,yes,kkx,16-05-1990,60978020,60978020 +31726561099,en,Safaricom,false,Nathaniel Brady,30-11-2000,stark,3,demoOption2,55440384603,VHZJEfhN,yes,uUH,27-10-2002,14633166,14633166 +99120000838,en,Safaricom,true,Chester Wallace,15-03-1990,lannister,28,demoOption1,77919399698,JZKDYgMz,yes,YTg,16-05-1990,90936694,90936694 +78634798660,nl,Safaricom,false,Darrell Torres,15-03-1990,stark,50,demoOption1,37813514847,xvmIJdjt,yes,CpF,16-05-1990,87999029,87999029 +40916001283,nl,Safaricom,true,Dale Brady,30-11-2000,stark,89,demoOption2,75208956905,phLmuZKd,yes,Axs,27-10-2002,22019902,22019902 +65974059849,en,Safaricom,false,Mary Bryant,15-03-1990,stark,44,demoOption3,14536865565,vuGQNSwW,yes,wfv,16-05-1990,83256392,83256392 +90163775370,nl,ironBank,true,Hattie Wheeler,30-11-2000,greyjoy,66,demoOption3,53065283769,GgTrssOk,yes,Vjh,16-05-1990,36665706,36665706 +4739745577,en,Safaricom,true,Tom Blair,30-11-2000,lannister,5,demoOption1,55302595441,TjyrRIRo,yes,BjU,16-05-1990,62467832,62467832 +883351548,nl,Safaricom,false,Justin Bush,15-03-1990,greyjoy,13,demoOption1,39921307216,cVLpyBEX,no,QxB,27-10-2002,41597356,41597356 +14197922779,nl,Safaricom,false,George Chandler,30-11-2000,lannister,21,demoOption1,98282978498,EdxDVDKv,yes,iZL,16-05-1990,21440079,21440079 +13536581035,nl,Safaricom,true,Agnes Garrett,15-03-1990,stark,10,demoOption1,92390637061,OviPezZa,no,siC,27-10-2002,72858535,72858535 +44625085296,en,Safaricom,false,Maria Bush,30-11-2000,stark,82,demoOption2,46264308647,bMVwHMPi,no,ikK,27-10-2002,71469861,71469861 +39377295418,en,Safaricom,false,Eugenia Parker,30-11-2000,stark,54,demoOption1,2905601298,YlzFzkyj,no,bYq,27-10-2002,19968522,19968522 +34063923774,nl,Safaricom,false,Manuel Jones,15-03-1990,stark,48,demoOption3,72303906161,yQIZwuyk,yes,AGd,27-10-2002,7100178,7100178 +10014824253,nl,Safaricom,false,Marc Warren,30-11-2000,stark,42,demoOption3,31064624135,qCyvydPg,yes,Qng,27-10-2002,28965746,28965746 +93893788728,en,Safaricom,false,Fred Simon,30-11-2000,greyjoy,14,demoOption3,31959848095,LpZilUsw,yes,syw,27-10-2002,48922554,48922554 +53505421978,en,Safaricom,false,Matthew Guerrero,15-03-1990,greyjoy,1,demoOption3,57440833885,fIvopbHv,yes,avB,16-05-1990,80868146,80868146 +75131734760,nl,Safaricom,true,Bruce Ellis,15-03-1990,greyjoy,51,demoOption3,76737746717,OQzJWQoz,yes,SVr,27-10-2002,69883316,69883316 +42434273610,en,Safaricom,true,Jeremy Mathis,15-03-1990,greyjoy,35,demoOption2,31126284775,pOeFMsKB,yes,Cui,16-05-1990,904046,904046 +68562246122,en,Safaricom,true,Bobby Stevenson,30-11-2000,greyjoy,97,demoOption1,17344592048,yXUDAoux,yes,eif,16-05-1990,11961608,11961608 +54851642006,en,Intersolve-voucher-whatsapp,false,Tom Miles,15-03-1990,stark,8,demoOption2,36589494129,qPfINWjo,no,gDt,16-05-1990,22353289,22353289 +2462557312,en,Safaricom,false,Lela Henderson,30-11-2000,lannister,24,demoOption2,9067073559,JsVYnjxX,no,WWj,16-05-1990,99206644,99206644 +31714186937,nl,Safaricom,false,Caroline Bowen,15-03-1990,greyjoy,11,demoOption1,11405487303,RgMJNMpN,no,tas,27-10-2002,12294950,12294950 +44002832228,en,gringotts,true,Maurice Morrison,15-03-1990,stark,50,demoOption2,22964988164,HzZQTbqY,no,iFs,27-10-2002,34741498,34741498 +27084427180,en,Intersolve-voucher-whatsapp,true,Eula Sims,15-03-1990,lannister,95,demoOption1,37061646184,uYqpBLsQ,no,EIW,27-10-2002,24491786,24491786 +54407420653,en,Safaricom,false,Lenora Elliott,30-11-2000,lannister,73,demoOption2,17256297475,kYPMfqqG,yes,mzn,27-10-2002,64019447,64019447 +80456235880,en,Safaricom,false,Alex Davidson,30-11-2000,stark,53,demoOption2,5202610172,DcZFAZSh,no,dYO,16-05-1990,43447974,43447974 +89841495755,en,Intersolve-voucher-whatsapp,true,Augusta Rowe,15-03-1990,lannister,19,demoOption2,5305876793,bXnIpNHV,yes,LPv,27-10-2002,93567505,93567505 +52782716570,en,Safaricom,false,Christian Fowler,30-11-2000,stark,18,demoOption1,47039332054,NjQBTUQT,yes,gcQ,27-10-2002,32800775,32800775 +71419770090,nl,Safaricom,false,Ola Gilbert,15-03-1990,lannister,72,demoOption3,86545811988,erMCmrDX,yes,flc,16-05-1990,55882378,55882378 +35915506763,nl,Intersolve-voucher-whatsapp,true,Todd Bass,30-11-2000,lannister,95,demoOption3,19562195318,oASnHhzr,yes,yoM,27-10-2002,3724276,3724276 +83786462780,nl,Safaricom,false,Myra Long,30-11-2000,lannister,44,demoOption2,33385452462,fpAEimZe,yes,XKK,16-05-1990,84747240,84747240 +69785900086,en,gringotts,true,Rena Powers,30-11-2000,greyjoy,11,demoOption2,78605514326,LLEzkgun,yes,YPe,27-10-2002,59684985,59684985 +22842635852,en,Intersolve-voucher-whatsapp,false,Bruce Dean,30-11-2000,lannister,5,demoOption1,53193455957,NxJAXpOI,no,TXb,16-05-1990,43325607,43325607 +54141362981,nl,Safaricom,false,Brent McCoy,30-11-2000,stark,65,demoOption3,25126173110,bHdNJHkB,no,GZI,16-05-1990,86894125,86894125 +20552266409,en,gringotts,true,Jorge Gill,30-11-2000,lannister,55,demoOption1,56080660023,LPqGOQSv,no,YRS,27-10-2002,17121344,17121344 +27758928667,en,gringotts,false,Mamie McCarthy,30-11-2000,greyjoy,2,demoOption1,63988213536,IOKOQcXP,no,day,27-10-2002,73098155,73098155 +26610145441,nl,Safaricom,false,Georgie Guzman,15-03-1990,greyjoy,27,demoOption2,57381268781,WXSidtKE,yes,WFC,27-10-2002,16347128,16347128 +57798988418,en,Intersolve-voucher-whatsapp,true,Roger Saunders,15-03-1990,lannister,46,demoOption3,62944849954,mUXzRkes,no,qfA,16-05-1990,47160567,47160567 +3742844751,nl,Safaricom,false,Blake Davis,30-11-2000,stark,68,demoOption3,1777685110,gcljpXHU,yes,Ywq,27-10-2002,85396034,85396034 +10721619588,nl,ironBank,true,Lola Nguyen,30-11-2000,greyjoy,9,demoOption3,75117154157,ryiSyykR,no,QxB,16-05-1990,90487496,90487496 +92674910087,en,gringotts,false,Jerry Wolfe,15-03-1990,stark,86,demoOption1,2128974347,KIvjzFqu,no,gmg,16-05-1990,721350,721350 +78335193454,en,Intersolve-voucher-whatsapp,true,Ricky Jensen,15-03-1990,greyjoy,82,demoOption1,55643438162,fyhfawHk,no,fGY,16-05-1990,64577877,64577877 +89088386702,nl,Safaricom,true,Virginia Palmer,15-03-1990,greyjoy,63,demoOption3,58234098519,yKuXAZDU,no,QJy,27-10-2002,30130384,30130384 +84192859808,en,Intersolve-voucher-whatsapp,true,Ella Oliver,15-03-1990,stark,7,demoOption2,21782939278,IUqRezbC,no,Ftn,27-10-2002,33626239,33626239 +41011467772,nl,Safaricom,true,Nelle Davidson,15-03-1990,greyjoy,71,demoOption2,49121530730,nNPeHVpi,yes,Dol,27-10-2002,92461809,92461809 +25314153207,nl,Intersolve-voucher-whatsapp,true,Tommy Rice,30-11-2000,greyjoy,11,demoOption2,40286932881,HkRpHOVi,no,hCc,16-05-1990,17942499,17942499 +49506659313,en,Safaricom,false,Birdie Stephens,30-11-2000,greyjoy,61,demoOption1,96329200433,ocClSFyY,yes,lAL,27-10-2002,43479423,43479423 +96364437306,en,Safaricom,true,Douglas Soto,15-03-1990,stark,69,demoOption3,55014497388,AuBdwnWl,yes,JZP,16-05-1990,84510699,84510699 +74425010217,nl,Safaricom,true,Roger Fitzgerald,15-03-1990,greyjoy,14,demoOption2,43222263924,UVcCfqFN,no,ZNK,16-05-1990,59831794,59831794 +26118593717,nl,ironBank,false,Leroy Austin,15-03-1990,greyjoy,50,demoOption3,14829297891,xqozeMjC,no,GgH,16-05-1990,49088396,49088396 +33014580750,en,Safaricom,true,Mitchell McDaniel,15-03-1990,greyjoy,4,demoOption3,50976794938,aDpttxAg,yes,BXL,16-05-1990,44656324,44656324 +69493500870,en,Safaricom,true,Maurice Sparks,30-11-2000,greyjoy,57,demoOption3,31758877223,HtDdkomo,yes,THn,16-05-1990,23018354,23018354 +28727084388,en,Safaricom,false,Beulah Figueroa,30-11-2000,lannister,76,demoOption1,14278347161,QmpCJStD,no,PUK,16-05-1990,89560149,89560149 +72494078662,en,Safaricom,false,Pearl Francis,30-11-2000,lannister,59,demoOption3,47134627800,BDieFAia,yes,vHj,16-05-1990,85567994,85567994 +45188705826,en,gringotts,true,Steven Morrison,30-11-2000,greyjoy,80,demoOption1,97885597222,CrXPawBW,yes,XdF,16-05-1990,41637648,41637648 +65376105092,nl,Intersolve-voucher-whatsapp,true,Tom Dixon,30-11-2000,greyjoy,42,demoOption2,10164684767,isoodnjw,yes,IDD,16-05-1990,55162201,55162201 +99614732628,nl,Safaricom,true,Timothy Griffith,30-11-2000,stark,70,demoOption3,19511471154,alUHjFSe,yes,Yvc,27-10-2002,55108115,55108115 +90776244533,nl,Safaricom,true,John Sharp,30-11-2000,greyjoy,72,demoOption1,51682830451,SAsViWvy,yes,iQy,16-05-1990,5054681,5054681 +46857024937,en,Intersolve-voucher-whatsapp,false,Glenn Stevens,15-03-1990,greyjoy,75,demoOption1,7768093502,lMXEyMBL,no,VOa,16-05-1990,61970173,61970173 +18866799564,nl,Safaricom,false,Gerald Curtis,15-03-1990,greyjoy,74,demoOption1,11222438655,VkkyQJTI,yes,LUy,16-05-1990,46575726,46575726 +70072014963,nl,Safaricom,false,Joe Dunn,30-11-2000,lannister,32,demoOption1,7317394663,YMdRoMAL,yes,lro,27-10-2002,1988182,1988182 +5184102674,nl,Safaricom,true,Ethel Watkins,30-11-2000,greyjoy,46,demoOption1,20718118717,QvDOPRIq,no,Kvy,27-10-2002,16266028,16266028 +68036525372,nl,Safaricom,false,Logan Olson,30-11-2000,lannister,49,demoOption1,47687755275,pwrACExP,no,eeq,16-05-1990,64781008,64781008 +57369919915,en,gringotts,true,Mabel Cannon,15-03-1990,greyjoy,81,demoOption1,6233328357,XKPjqaZa,yes,zpZ,27-10-2002,60963242,60963242 +70949414051,nl,Safaricom,true,Stanley Griffith,30-11-2000,stark,44,demoOption3,95717579067,obsiCPqk,yes,Qhq,27-10-2002,79507057,79507057 +69507117885,nl,Safaricom,false,Nellie Reynolds,30-11-2000,greyjoy,5,demoOption1,35849910388,iGZrVJey,no,ECs,27-10-2002,9498025,9498025 +97778761402,nl,Intersolve-voucher-whatsapp,true,Scott Campbell,15-03-1990,stark,57,demoOption1,73584851448,OMYAVvMj,no,JIj,16-05-1990,81854281,81854281 +16970365715,nl,ironBank,true,Ernest Thornton,30-11-2000,greyjoy,84,demoOption2,34096001403,SFgYSnQg,yes,FZu,27-10-2002,24226295,24226295 +7700960418,nl,Safaricom,true,Edna McCoy,30-11-2000,greyjoy,34,demoOption3,92246964500,giMGGjap,yes,FOU,27-10-2002,43778569,43778569 +60916158580,en,Safaricom,true,Herman Ramirez,30-11-2000,stark,53,demoOption3,24000337937,PocylpEv,yes,EIS,16-05-1990,50502707,50502707 +37223412689,nl,Safaricom,false,Hunter Pratt,30-11-2000,lannister,61,demoOption2,82417237171,WLxbUXFm,no,Xdy,27-10-2002,75864860,75864860 +8384179955,nl,Safaricom,true,Philip Adkins,30-11-2000,stark,58,demoOption3,1841604627,yRmMlijX,yes,GzY,27-10-2002,75454402,75454402 +38523594321,en,Safaricom,true,Genevieve Floyd,15-03-1990,stark,11,demoOption3,2503956761,lDBhuEod,yes,aLP,27-10-2002,71572578,71572578 +63363902654,en,Safaricom,true,Harry Chambers,15-03-1990,greyjoy,77,demoOption3,11155945278,crUuqQLj,no,Prr,27-10-2002,14549461,14549461 +41405612893,nl,Safaricom,true,Janie Quinn,15-03-1990,greyjoy,64,demoOption2,27471937346,ONNzbwSl,yes,Trq,27-10-2002,14665840,14665840 +18219629922,nl,ironBank,true,Jeffery Daniels,30-11-2000,stark,75,demoOption3,42313972308,CUpKZRaS,no,hWa,27-10-2002,8383028,8383028 +43961421323,nl,Safaricom,false,Clarence Duncan,15-03-1990,greyjoy,76,demoOption1,66933592742,jsCNKBBL,no,lYK,27-10-2002,74220525,74220525 +69868553576,en,Safaricom,false,Johnny Lamb,15-03-1990,stark,28,demoOption3,3608843472,zcfEgSfh,yes,pQw,27-10-2002,72044196,72044196 +29866904799,en,Intersolve-voucher-whatsapp,true,Lucille Bowers,30-11-2000,lannister,69,demoOption2,84822333532,BYPcvOEG,yes,zsQ,27-10-2002,48917736,48917736 +44249566697,nl,Safaricom,false,Logan Steele,15-03-1990,stark,47,demoOption2,30311759388,ZATMjKOq,no,vmR,16-05-1990,32143591,32143591 +73022390333,nl,ironBank,true,Anne Welch,30-11-2000,greyjoy,8,demoOption2,60346835634,LSvDCffo,no,CBN,27-10-2002,36879491,36879491 +4975775025,en,Safaricom,false,Scott Tran,15-03-1990,greyjoy,97,demoOption3,8967444999,bEsxQxBR,yes,vLu,27-10-2002,38526686,38526686 +36020743385,en,gringotts,false,Lillie Hubbard,30-11-2000,greyjoy,68,demoOption3,91679524440,gxNGtaPu,yes,Hyq,16-05-1990,66780395,66780395 +53467224747,en,Safaricom,true,Anne Moreno,30-11-2000,lannister,46,demoOption3,96152639945,PskmEhdi,yes,Qgf,27-10-2002,19914535,19914535 +32227754213,en,Intersolve-voucher-whatsapp,false,Mittie Little,30-11-2000,lannister,85,demoOption3,86083198963,vHChAngE,no,AQD,27-10-2002,17481224,17481224 +71100733957,nl,Safaricom,true,Mark Sims,30-11-2000,greyjoy,83,demoOption3,3195474884,JaAoeAkb,yes,RnM,16-05-1990,49811173,49811173 +99359423165,nl,Safaricom,true,Ernest Hudson,30-11-2000,lannister,33,demoOption3,57705435542,KJSkpHsA,no,YOG,27-10-2002,58403912,58403912 +50179500028,nl,ironBank,false,Mittie McKenzie,15-03-1990,stark,4,demoOption1,59718385013,DJBJmMTL,yes,jGV,27-10-2002,33077638,33077638 +65033949549,nl,ironBank,false,Raymond Pratt,15-03-1990,stark,18,demoOption2,53918522960,OjNvMdab,yes,UXO,27-10-2002,37108095,37108095 +91865902006,nl,Safaricom,true,Hulda Barker,15-03-1990,stark,70,demoOption1,83552737094,rMdqdQHb,yes,HvP,27-10-2002,9341257,9341257 +72167059697,nl,Intersolve-voucher-whatsapp,true,Hulda Adkins,15-03-1990,greyjoy,74,demoOption1,61229539409,aMurQrpV,yes,bqZ,16-05-1990,25733478,25733478 +44654177188,nl,Safaricom,false,Carl Jefferson,15-03-1990,greyjoy,82,demoOption1,91967610321,ZRjVojsO,no,GlA,27-10-2002,50296973,50296973 +77471224004,nl,Safaricom,true,Derrick Wallace,30-11-2000,stark,34,demoOption1,79873690252,ZQcbWqNW,no,srB,27-10-2002,9874848,9874848 +81350723407,en,Safaricom,true,Laura Reed,30-11-2000,greyjoy,48,demoOption1,26706612300,ubGElpTk,no,JUF,16-05-1990,71722902,71722902 +22723212382,nl,Safaricom,true,Ollie Maldonado,15-03-1990,greyjoy,53,demoOption3,27118235781,pJeqmgxP,yes,ahB,27-10-2002,39315036,39315036 +58097392488,en,gringotts,true,Bernard Adkins,30-11-2000,greyjoy,62,demoOption1,68851038365,YHGMjBzZ,yes,Jrs,16-05-1990,97414645,97414645 +53252038659,en,gringotts,true,Christian Greene,15-03-1990,greyjoy,23,demoOption3,59037291483,LvZTgrSz,yes,lXW,27-10-2002,23434632,23434632 +21926816499,en,Safaricom,false,Ricky Hayes,15-03-1990,greyjoy,55,demoOption1,67795966097,sFhgUHvW,no,Vil,27-10-2002,13966227,13966227 +10999287966,en,Safaricom,false,Celia Clark,15-03-1990,greyjoy,25,demoOption2,53679056807,dIRomgbZ,yes,Xug,16-05-1990,64188364,64188364 +38150426801,nl,Safaricom,false,Lura Perry,30-11-2000,greyjoy,15,demoOption2,47828163057,ZCyUyltw,yes,khK,16-05-1990,85884375,85884375 +82295256858,nl,Intersolve-voucher-whatsapp,false,Martha Drake,30-11-2000,stark,12,demoOption1,54396111133,uUxHthAL,yes,pXm,27-10-2002,39453036,39453036 +55928553585,nl,Safaricom,false,Mittie Wilkins,30-11-2000,stark,37,demoOption1,38364728622,tbqGQbgD,no,RPt,16-05-1990,96283552,96283552 +43318497007,nl,Intersolve-voucher-whatsapp,false,Allen Sutton,30-11-2000,stark,40,demoOption1,32778038363,wNkpFJJf,yes,Cia,16-05-1990,96368962,96368962 +17277232664,nl,ironBank,true,Mattie Gordon,15-03-1990,greyjoy,43,demoOption3,40621769985,XyfBVtRn,no,Lrt,16-05-1990,52987035,52987035 +50569701427,nl,ironBank,false,Steven Harrison,30-11-2000,lannister,26,demoOption2,71158366567,IMBjaugV,yes,ZQQ,16-05-1990,98557142,98557142 +28461471637,en,Safaricom,true,Flora Brock,15-03-1990,lannister,11,demoOption2,6304551748,OSQNwSbU,yes,aEB,27-10-2002,4915957,4915957 +29186100102,en,Safaricom,true,Inez Goodwin,15-03-1990,stark,93,demoOption1,2824597064,yhnmGERc,no,gvZ,16-05-1990,74112318,74112318 +9049890853,nl,Safaricom,false,Oscar Lloyd,30-11-2000,greyjoy,34,demoOption3,40920220421,KxvGbSum,yes,UNc,27-10-2002,70103993,70103993 +33368875684,en,Safaricom,false,Randy Dawson,30-11-2000,greyjoy,93,demoOption2,98621232736,YjkjBjOY,no,kzK,16-05-1990,26589715,26589715 +60178483429,en,Safaricom,false,Elijah Boone,30-11-2000,lannister,4,demoOption2,41291080994,YFJwgQhl,yes,gZa,16-05-1990,90882852,90882852 +52290151230,nl,Safaricom,false,Barry Butler,15-03-1990,lannister,27,demoOption2,57816223707,xTvseXke,no,OMr,16-05-1990,3137898,3137898 +33418735722,nl,Intersolve-voucher-whatsapp,true,Russell Rios,15-03-1990,greyjoy,35,demoOption2,79645599756,ehYvFODz,yes,tXd,27-10-2002,81970187,81970187 +98821084773,nl,Safaricom,true,Caleb Tucker,30-11-2000,greyjoy,13,demoOption3,85166937147,TqYYyGIj,no,dJp,16-05-1990,55026212,55026212 +18597593772,en,gringotts,false,Adeline White,30-11-2000,greyjoy,21,demoOption2,12216482785,hbTaimOe,yes,nLT,27-10-2002,23848695,23848695 +16927125121,en,Safaricom,false,Jacob Taylor,30-11-2000,lannister,35,demoOption3,45497425614,XbtGIGZV,yes,KQQ,16-05-1990,53125026,53125026 +79786658329,nl,Safaricom,true,Winifred Owens,30-11-2000,stark,27,demoOption2,87967585069,malZdJLV,no,LTG,16-05-1990,3601295,3601295 +30715655130,nl,Safaricom,false,Myrtie Burns,30-11-2000,stark,12,demoOption1,65502620900,UOgeoByR,yes,Xqj,16-05-1990,87328252,87328252 +14535116204,nl,Safaricom,false,Oscar Frazier,15-03-1990,greyjoy,85,demoOption2,39241083248,rhUlhmvc,no,llw,27-10-2002,22139557,22139557 +48476706235,nl,Safaricom,false,Inez Tucker,30-11-2000,lannister,29,demoOption1,31290450120,ibtVPhBi,no,xTt,27-10-2002,27401839,27401839 +61019099002,en,Safaricom,true,Etta Bryant,30-11-2000,stark,96,demoOption1,40857566530,WfUgJAiG,yes,zgJ,16-05-1990,51161307,51161307 +89755970940,nl,Safaricom,false,Craig Barker,30-11-2000,stark,75,demoOption3,99407912476,QihgFcPv,yes,aQU,27-10-2002,33273367,33273367 +21262252907,en,Safaricom,true,Violet Medina,15-03-1990,lannister,51,demoOption1,9160841685,dGLMPgqA,yes,HkH,16-05-1990,46944897,46944897 +46962970098,nl,Safaricom,false,Chad Chavez,15-03-1990,greyjoy,60,demoOption2,94614270589,tmtgbwtC,yes,Nzh,27-10-2002,91423047,91423047 +26649131444,nl,Safaricom,true,Gertrude Richardson,15-03-1990,stark,98,demoOption3,81260558411,iBRuWbkc,no,dvf,27-10-2002,30445330,30445330 +83010769916,en,Safaricom,false,Clarence Cruz,15-03-1990,lannister,95,demoOption1,93612667928,vkuKlIxB,no,wnK,27-10-2002,65574538,65574538 +22636223490,en,gringotts,false,Dominic Silva,30-11-2000,stark,16,demoOption2,89424410293,eprYVFJI,yes,sEZ,16-05-1990,93282604,93282604 +74984242844,nl,Safaricom,false,Anne Marshall,15-03-1990,greyjoy,85,demoOption1,90757642669,rBHDTdkD,yes,WyE,16-05-1990,55407872,55407872 +24924196872,nl,ironBank,true,Delia Wilkins,30-11-2000,greyjoy,15,demoOption2,38193117764,eZkOeFBx,yes,ePT,27-10-2002,61242587,61242587 +5711702996,en,Intersolve-voucher-whatsapp,false,Gertrude Anderson,15-03-1990,lannister,47,demoOption1,931780555,ZkKqNyeV,yes,ngX,27-10-2002,9547769,9547769 +63623134494,nl,Intersolve-voucher-whatsapp,false,Lida Thomas,15-03-1990,stark,79,demoOption1,29226784304,QVFwTPLT,no,VmJ,27-10-2002,72565149,72565149 +55908876429,en,Safaricom,false,Frank Salazar,15-03-1990,stark,8,demoOption2,72292273041,jqofcMxx,no,wMN,16-05-1990,52688993,52688993 +77320522890,en,Safaricom,false,Emilie Martin,30-11-2000,lannister,91,demoOption2,866505112,mDbYdiYy,no,Xwh,16-05-1990,97029866,97029866 +13815770082,en,Safaricom,false,Joseph Zimmerman,15-03-1990,lannister,3,demoOption3,89211043576,kUfNajEa,no,RfF,27-10-2002,17756385,17756385 +85411743863,nl,Safaricom,false,Bill Dennis,15-03-1990,stark,52,demoOption2,95404424333,OXTvZMtS,no,Hkw,27-10-2002,12403171,12403171 +64182650315,en,Safaricom,false,Harry Luna,30-11-2000,greyjoy,85,demoOption1,66522358278,XmfYJCki,yes,wUZ,27-10-2002,30566934,30566934 +77155372082,en,Intersolve-voucher-whatsapp,false,Jared Patrick,15-03-1990,lannister,37,demoOption2,82983315162,CaMIJAde,no,zSK,16-05-1990,61153768,61153768 +38723277025,en,Safaricom,false,Barbara Nelson,15-03-1990,greyjoy,45,demoOption2,64606097945,HVsgxzmB,yes,MAj,16-05-1990,87184628,87184628 +44712213744,nl,Safaricom,true,Teresa Morton,30-11-2000,greyjoy,97,demoOption1,44659693458,bUssMQjx,yes,cnm,16-05-1990,65244400,65244400 +78329797404,nl,Safaricom,true,Viola Graham,15-03-1990,lannister,18,demoOption1,43814429656,TBcKLIAj,yes,mCX,27-10-2002,76117849,76117849 +29880613471,en,Safaricom,false,Mollie Parsons,30-11-2000,greyjoy,26,demoOption3,10246098514,ZTLNGEIg,no,BrA,16-05-1990,53538994,53538994 +21409248667,nl,Intersolve-voucher-whatsapp,true,Alberta Burgess,15-03-1990,greyjoy,71,demoOption3,6860766593,gIdBfhOV,yes,eqr,27-10-2002,36154786,36154786 +32850744703,en,Safaricom,false,Joel Underwood,30-11-2000,lannister,17,demoOption3,2856638758,EFPaEEbo,no,qIm,16-05-1990,7792893,7792893 +88280939643,nl,Safaricom,false,Myrtie Graham,15-03-1990,greyjoy,6,demoOption3,6742434785,nMykviRL,no,JxT,27-10-2002,19270186,19270186 +70192388760,nl,ironBank,false,Barry Doyle,30-11-2000,stark,16,demoOption2,15341061874,GmqWtQmm,yes,bYd,27-10-2002,99351687,99351687 +28864517633,nl,Safaricom,false,Antonio Chandler,30-11-2000,greyjoy,7,demoOption2,2691664017,NdYSjfVf,no,qRJ,27-10-2002,59349198,59349198 +45945667655,en,Safaricom,false,Peter Neal,15-03-1990,stark,50,demoOption3,44756650871,xKrKGVaA,no,lxV,16-05-1990,18714066,18714066 +45705301796,en,gringotts,true,Lela Warner,30-11-2000,stark,11,demoOption2,21419598673,COSTNwPG,no,Sho,16-05-1990,38630903,38630903 +52961770260,nl,ironBank,true,Dean Harrington,30-11-2000,lannister,70,demoOption2,18075991611,wqWBuDNP,no,ryf,27-10-2002,75338289,75338289 +95003213912,en,Safaricom,true,Agnes Coleman,30-11-2000,stark,11,demoOption2,54076245946,qZKONExL,no,xQZ,27-10-2002,68730398,68730398 +25929854446,nl,Safaricom,true,Alma Rice,30-11-2000,stark,82,demoOption1,58461460024,xAnZXbMv,yes,RPz,16-05-1990,73521364,73521364 +58935400449,en,Intersolve-voucher-whatsapp,false,Mayme Vargas,30-11-2000,stark,82,demoOption1,31297202928,IoExnkdv,no,Jjh,16-05-1990,96484299,96484299 +60107043062,nl,Safaricom,false,Arthur Kennedy,15-03-1990,greyjoy,2,demoOption3,96388231837,VXwvATVP,no,RDV,27-10-2002,96601961,96601961 +66486649213,nl,Safaricom,true,Maggie Jordan,15-03-1990,greyjoy,44,demoOption3,56581563661,qcysCLYI,yes,iit,16-05-1990,36647372,36647372 +57838088272,nl,Safaricom,false,Leila Walton,15-03-1990,stark,17,demoOption3,23836781502,kDXtrkov,no,PmM,27-10-2002,33966036,33966036 +43087998544,en,gringotts,true,Travis Gross,15-03-1990,stark,9,demoOption1,11625167088,bOyBKBuW,no,fhd,16-05-1990,8362432,8362432 +12993718890,en,Intersolve-voucher-whatsapp,true,Cody Morton,30-11-2000,lannister,64,demoOption1,30123853157,hCyexsBP,yes,yyb,27-10-2002,8506796,8506796 +86923410303,en,gringotts,false,Dorothy Rhodes,15-03-1990,lannister,9,demoOption2,42876712339,RzFjujHh,no,Cvg,16-05-1990,41902274,41902274 +50303816707,en,Safaricom,false,Tommy Lane,30-11-2000,lannister,45,demoOption3,91826830787,mjHhUdvY,no,EFP,27-10-2002,49337356,49337356 +10539817591,en,Safaricom,false,Clyde Moreno,30-11-2000,lannister,13,demoOption1,21867568530,JEtGOkqs,yes,Bnb,27-10-2002,11314917,11314917 +31086244747,en,Safaricom,true,Sean Richardson,15-03-1990,lannister,9,demoOption1,32969543264,tWSZBbae,yes,AIH,16-05-1990,97857488,97857488 +65337118254,nl,Safaricom,false,Luke Shaw,30-11-2000,lannister,86,demoOption1,83669040622,ntkCbCdY,no,RbM,27-10-2002,32242738,32242738 +86035803916,nl,ironBank,true,Adelaide Cruz,15-03-1990,greyjoy,72,demoOption1,89500363815,wdyuLxcs,no,xEM,27-10-2002,84025373,84025373 +47023936967,nl,Intersolve-voucher-whatsapp,false,Blake Sparks,30-11-2000,lannister,46,demoOption3,47249635306,rawffTcU,no,qlU,27-10-2002,65597747,65597747 +84876668941,en,gringotts,false,Ruth Parks,30-11-2000,lannister,60,demoOption1,3842765858,aIBSppJT,yes,Tqg,27-10-2002,53872452,53872452 +61466224437,en,Intersolve-voucher-whatsapp,true,Ella George,30-11-2000,greyjoy,22,demoOption1,1693664723,kfggptXL,no,wbL,27-10-2002,10007872,10007872 +61623148853,nl,Intersolve-voucher-whatsapp,true,Essie Morgan,15-03-1990,stark,15,demoOption3,56628515257,XZldwnHv,no,lsb,16-05-1990,37007275,37007275 +37252431386,en,Safaricom,false,Birdie Hampton,15-03-1990,stark,50,demoOption1,12020798579,JknrQxzP,no,Ydr,27-10-2002,73635745,73635745 +65519843521,en,gringotts,true,Charlotte Campbell,30-11-2000,greyjoy,19,demoOption2,82966224017,gtEVsfNz,no,JMZ,16-05-1990,75173212,75173212 +99325424774,nl,Safaricom,false,Henry Benson,30-11-2000,greyjoy,22,demoOption1,73836299834,uTAmgxYH,yes,ReW,16-05-1990,89763628,89763628 +45099540735,nl,Intersolve-voucher-whatsapp,true,Bertie Cole,15-03-1990,stark,7,demoOption3,55966306230,OVsdcaIh,no,eaC,16-05-1990,7651807,7651807 +36239964167,en,gringotts,true,William Jensen,30-11-2000,greyjoy,42,demoOption1,23639692492,lVMkmFxL,yes,Pbw,27-10-2002,87080389,87080389 +86834466376,en,gringotts,false,Mina Bradley,30-11-2000,greyjoy,50,demoOption3,97742523290,WkdQeZgS,yes,xQM,16-05-1990,18189114,18189114 +31746913464,nl,Intersolve-voucher-whatsapp,true,Floyd Baldwin,15-03-1990,lannister,41,demoOption2,38630943219,viGVqDxE,yes,ctv,27-10-2002,95567885,95567885 +31502474837,nl,ironBank,false,Richard Santiago,15-03-1990,lannister,23,demoOption3,58699933239,ULEbBJup,yes,hZa,16-05-1990,28749710,28749710 +24905476742,nl,Intersolve-voucher-whatsapp,true,Rebecca Pierce,30-11-2000,greyjoy,54,demoOption1,54468691221,jOeDWgUl,yes,nIV,27-10-2002,98748769,98748769 +60113540308,en,Safaricom,true,Devin Rodriguez,30-11-2000,stark,68,demoOption2,79796283693,xXodoCwp,no,VLd,27-10-2002,71143252,71143252 +36980793229,en,gringotts,true,Stella Paul,15-03-1990,stark,28,demoOption1,36960900764,ONGyLzaw,yes,Dwo,27-10-2002,12607614,12607614 +35769362349,nl,Safaricom,false,Josephine Norton,30-11-2000,lannister,44,demoOption1,37766906049,VhmMADjo,no,sQx,16-05-1990,56393470,56393470 +63270004795,nl,Safaricom,false,Ricky Evans,15-03-1990,stark,64,demoOption1,79873960394,kYipgTAE,yes,hoa,27-10-2002,5315092,5315092 +51965322293,nl,Safaricom,true,Lilly Gibson,30-11-2000,stark,86,demoOption3,36957181555,ujLhtrpx,yes,Rtm,27-10-2002,13566775,13566775 +89068652070,en,Intersolve-voucher-whatsapp,false,Corey Rodgers,15-03-1990,lannister,43,demoOption1,21608144322,ilnjFArS,no,zpz,16-05-1990,9422767,9422767 +6497125219,nl,ironBank,true,Stella Hansen,30-11-2000,greyjoy,46,demoOption1,33390301304,ACKnBHTr,yes,PuM,27-10-2002,50535197,50535197 +71887903922,en,Safaricom,true,Mason Gibbs,15-03-1990,lannister,59,demoOption3,27981802655,hswZZZmA,no,xZT,16-05-1990,45968118,45968118 +53184327504,en,Safaricom,true,Beulah Powell,15-03-1990,stark,11,demoOption3,6641345077,UtQTOZMq,yes,AJd,27-10-2002,89002672,89002672 +74505035547,nl,ironBank,true,Elnora Ward,15-03-1990,lannister,87,demoOption1,55107958385,qJoUFJYV,yes,rXL,16-05-1990,12863014,12863014 +80470209197,en,Safaricom,false,Vernon Keller,15-03-1990,greyjoy,45,demoOption2,29025446396,EXljJCGr,no,cNJ,27-10-2002,49940322,49940322 +65263081540,en,Intersolve-voucher-whatsapp,false,Jennie Hawkins,30-11-2000,lannister,25,demoOption2,81315066560,NdDSZzsV,no,VET,27-10-2002,51077433,51077433 +17265135232,nl,Intersolve-voucher-whatsapp,true,Johanna McCarthy,15-03-1990,lannister,23,demoOption2,67662815213,oDAEfJCP,no,mFx,16-05-1990,55990685,55990685 +75724630924,nl,Safaricom,false,Nathaniel Silva,15-03-1990,lannister,94,demoOption2,36471826143,rRhovEIn,yes,aCc,16-05-1990,38712413,38712413 +63629369536,en,Safaricom,false,Rosa Newton,30-11-2000,greyjoy,34,demoOption3,46612743751,umjtFRon,no,ZhS,16-05-1990,53363007,53363007 +57258868332,en,Safaricom,true,Paul Bowman,15-03-1990,lannister,70,demoOption3,81139595022,DToDIqiT,yes,GtT,27-10-2002,47937690,47937690 +7839623151,en,Safaricom,false,Catherine Vasquez,30-11-2000,greyjoy,87,demoOption1,31757643641,wKQsBnCt,yes,HHf,27-10-2002,99239566,99239566 +99664118682,en,Intersolve-voucher-whatsapp,true,Lela Johnson,15-03-1990,stark,59,demoOption1,87194544430,PwaVSHiO,yes,mjq,27-10-2002,52940784,52940784 +6734182409,en,Intersolve-voucher-whatsapp,false,Daisy Holt,15-03-1990,lannister,9,demoOption3,57328208033,pJNKTnqe,yes,HyG,16-05-1990,21567289,21567289 +3913456460,nl,Safaricom,false,Jay Cruz,15-03-1990,stark,14,demoOption1,8961753234,ByUwxgyf,no,CTv,27-10-2002,38146429,38146429 +5297355640,nl,Safaricom,true,Carrie Hoffman,15-03-1990,lannister,93,demoOption2,41570749237,VhsqQHqZ,no,pEb,16-05-1990,1188154,1188154 +30127436968,en,Intersolve-voucher-whatsapp,true,Alberta Padilla,15-03-1990,lannister,24,demoOption3,26405484296,BaBcVeBq,no,GYa,27-10-2002,44398238,44398238 +78321833583,en,Safaricom,true,Kate Clayton,30-11-2000,stark,80,demoOption1,43039306189,bqBjjOol,no,Sql,16-05-1990,68034638,68034638 +13284750053,nl,ironBank,false,Emma Wright,15-03-1990,lannister,92,demoOption1,52917970663,SmrWjGCc,no,Bse,27-10-2002,88335242,88335242 +30412640563,nl,Intersolve-voucher-whatsapp,true,Mathilda Walton,15-03-1990,greyjoy,28,demoOption2,77583296690,dfyeBiio,yes,SEN,27-10-2002,58529084,58529084 +22880365881,en,Safaricom,false,Harry Newton,15-03-1990,lannister,17,demoOption3,40046695691,pHoDDbBk,no,lQc,16-05-1990,91750194,91750194 +22771756122,en,Safaricom,true,Henrietta Wells,30-11-2000,lannister,68,demoOption3,70013150338,jplkxCXL,yes,Bgz,27-10-2002,65682640,65682640 +40616158925,nl,ironBank,false,Bernard Cannon,15-03-1990,stark,27,demoOption2,74680802840,TcvWHkvL,no,JVI,16-05-1990,74902542,74902542 +56987183962,en,Intersolve-voucher-whatsapp,true,Gene Bryan,15-03-1990,lannister,98,demoOption3,29182263225,dCoVoXvq,no,snD,16-05-1990,5474595,5474595 +57423372658,nl,Safaricom,false,Irene Harris,15-03-1990,lannister,96,demoOption1,74143381568,HggCzHuw,no,aDu,16-05-1990,85958498,85958498 +41648266493,en,gringotts,false,Leah Peters,15-03-1990,lannister,93,demoOption2,74116979113,kRqyBoUP,no,yup,27-10-2002,54142876,54142876 +99949588636,en,Safaricom,true,Marc Webster,30-11-2000,lannister,38,demoOption2,15283517175,FpAKkCfB,no,ebr,16-05-1990,80289235,80289235 +26092552640,nl,Safaricom,true,Ian Ramos,30-11-2000,stark,94,demoOption2,68761323792,QGjsyflu,yes,gGK,27-10-2002,66552188,66552188 +90525384128,nl,ironBank,true,Gussie Munoz,30-11-2000,greyjoy,83,demoOption1,66868273667,hocPVLot,yes,qKP,16-05-1990,1242470,1242470 +79163180236,en,gringotts,false,Eula Bennett,30-11-2000,lannister,60,demoOption1,28340293265,AAzZybXN,yes,Kes,16-05-1990,73707890,73707890 +54860399722,en,gringotts,true,Brian Sims,15-03-1990,greyjoy,83,demoOption3,60230313148,sDFuKdXN,yes,WAM,27-10-2002,97886475,97886475 +41559523368,en,gringotts,true,Warren Reyes,30-11-2000,stark,54,demoOption1,62388882343,yNKijkvP,yes,alB,27-10-2002,79325738,79325738 +61720145813,nl,Safaricom,true,Nina Schneider,30-11-2000,lannister,66,demoOption3,61142953273,JfJAfqbr,yes,ZaP,27-10-2002,21939206,21939206 +26668851536,en,Intersolve-voucher-whatsapp,true,Annie Guerrero,15-03-1990,lannister,55,demoOption3,20078223631,kzKejjGT,yes,jgj,27-10-2002,10280926,10280926 +27306769773,nl,Intersolve-voucher-whatsapp,false,Adele Gonzalez,30-11-2000,greyjoy,8,demoOption2,17367299060,RIjsmhNi,yes,BOO,16-05-1990,34494528,34494528 +91863486189,en,Safaricom,true,Olivia Dixon,30-11-2000,lannister,12,demoOption2,45597017443,qzmGbRnd,no,Ian,27-10-2002,15550598,15550598 +26098503114,nl,Safaricom,true,Harvey Taylor,30-11-2000,stark,30,demoOption3,81261251844,lDDSTdrB,yes,ZBz,27-10-2002,93583432,93583432 +50208286680,nl,Safaricom,false,Sophie Elliott,30-11-2000,lannister,78,demoOption3,68243379836,oltgQCPH,yes,uUe,27-10-2002,92743039,92743039 +46213597609,nl,ironBank,false,Mabelle Page,30-11-2000,lannister,42,demoOption2,30765367626,NrzpKDtu,yes,CFa,16-05-1990,68266791,68266791 +89243270481,en,Safaricom,false,Kathryn Moore,30-11-2000,stark,49,demoOption2,82888468381,htDCGGxi,yes,Egg,16-05-1990,94251970,94251970 +72609971227,nl,ironBank,false,Mitchell Oliver,30-11-2000,greyjoy,55,demoOption1,83949602481,osYwsIsF,no,WAX,16-05-1990,42718553,42718553 +49644752882,nl,Safaricom,true,Ricardo Maldonado,15-03-1990,stark,42,demoOption2,44888037001,PvXJPMQZ,yes,jik,16-05-1990,11446640,11446640 +52508878377,en,Safaricom,false,Rose Flores,30-11-2000,greyjoy,13,demoOption1,21389927218,VlSCBiCv,no,FFY,16-05-1990,62410334,62410334 +11966629990,en,Safaricom,true,Louise Welch,15-03-1990,stark,52,demoOption2,2024781313,SLczsgAQ,no,hSs,27-10-2002,66261988,66261988 +15204741815,en,Safaricom,false,John Saunders,15-03-1990,lannister,76,demoOption1,59188016321,eDTweCcC,no,uKX,27-10-2002,38758300,38758300 +26761436086,nl,Safaricom,false,Joshua Johnston,30-11-2000,lannister,70,demoOption3,76473345974,tthFlITx,yes,gNf,27-10-2002,25365798,25365798 +69303524019,nl,Intersolve-voucher-whatsapp,true,Brian Patterson,15-03-1990,greyjoy,55,demoOption2,14945351138,DTiFuJlw,yes,fxP,16-05-1990,35056332,35056332 +96915009530,en,Intersolve-voucher-whatsapp,false,Marguerite Guerrero,15-03-1990,lannister,56,demoOption2,9381260369,gWyjaQoY,yes,hIp,27-10-2002,51256224,51256224 +64306622154,nl,Safaricom,false,Estella Sutton,30-11-2000,lannister,73,demoOption1,30594176049,djqdEOtg,yes,Wrd,16-05-1990,11380010,11380010 +16714023536,nl,ironBank,true,Maude Ramirez,15-03-1990,greyjoy,50,demoOption1,28174453126,xzNcHnVr,yes,HKQ,16-05-1990,41886498,41886498 +23208843907,en,Safaricom,false,Ann Barnett,15-03-1990,stark,92,demoOption1,42189903873,HIIIsYSI,yes,SZt,27-10-2002,52945127,52945127 +85282596473,en,Intersolve-voucher-whatsapp,true,Ricky Hernandez,15-03-1990,lannister,28,demoOption3,35496015596,kHXteDZp,no,VEy,27-10-2002,97313596,97313596 +89172977606,en,Intersolve-voucher-whatsapp,true,Ida Pope,30-11-2000,greyjoy,31,demoOption3,98679064942,ejanzynQ,yes,Gqo,27-10-2002,99177665,99177665 +24414904436,nl,ironBank,true,Lois Long,15-03-1990,lannister,73,demoOption2,52475279158,ckLjmTuG,yes,Ehq,16-05-1990,63301679,63301679 +72992742863,nl,Safaricom,false,Bill Carr,30-11-2000,greyjoy,31,demoOption1,39332915376,gEFwVapG,no,ILB,16-05-1990,87160182,87160182 +4039025838,en,Safaricom,true,Lillie McKinney,30-11-2000,greyjoy,2,demoOption3,75457967556,gqnEAuxU,yes,TYy,16-05-1990,56625867,56625867 +94553968212,nl,ironBank,false,Garrett Maldonado,30-11-2000,greyjoy,31,demoOption3,86508845369,izsIkHfM,yes,pNT,16-05-1990,79203938,79203938 +61908145985,en,Intersolve-voucher-whatsapp,true,Belle Hines,15-03-1990,greyjoy,65,demoOption3,43065541024,oPWwQpZl,yes,SLd,27-10-2002,82401614,82401614 +91341722496,en,Intersolve-voucher-whatsapp,true,Fred Diaz,15-03-1990,lannister,73,demoOption3,96036954407,aUyjECDY,no,Doi,27-10-2002,13792910,13792910 +46635590798,nl,Safaricom,false,Lula Blake,30-11-2000,stark,70,demoOption2,97958187838,vSwzPLkK,no,JuW,16-05-1990,65732079,65732079 +85247445897,en,Safaricom,false,Tommy Paul,30-11-2000,greyjoy,88,demoOption3,96523328715,TCInuYnQ,no,kHK,16-05-1990,47965514,47965514 +70293141934,nl,Safaricom,false,Mae Pope,30-11-2000,lannister,26,demoOption3,27805434504,ezAbVYUp,no,Ufx,27-10-2002,69459876,69459876 +89772772766,nl,ironBank,false,Clifford Ferguson,30-11-2000,lannister,99,demoOption2,17888331988,qDxlzkvk,no,WGq,27-10-2002,29826125,29826125 +84329687232,nl,Safaricom,false,Alexander Cole,15-03-1990,greyjoy,48,demoOption2,28055201491,rZpHhksr,no,FyS,16-05-1990,92932538,92932538 +81683330390,nl,Intersolve-voucher-whatsapp,false,Linnie Alvarez,15-03-1990,stark,76,demoOption2,17203927857,QWaKjEyX,no,lys,16-05-1990,78013717,78013717 +62696741350,nl,Safaricom,true,Henry Peters,30-11-2000,stark,10,demoOption3,39179558952,ZYhTdfub,yes,TFc,16-05-1990,13335247,13335247 +51548730306,nl,Intersolve-voucher-whatsapp,false,Cora Hogan,15-03-1990,stark,46,demoOption2,28330351573,WCsiaAkF,yes,HLv,27-10-2002,74325594,74325594 +2808750475,nl,Safaricom,false,Alma Sandoval,15-03-1990,stark,29,demoOption2,61647033262,ybcgoizc,no,UVf,16-05-1990,30746583,30746583 +57949608201,en,gringotts,false,Bess Mason,15-03-1990,lannister,7,demoOption2,7486879373,AMtBaYtq,yes,hnW,16-05-1990,45810678,45810678 +84746656417,en,Intersolve-voucher-whatsapp,false,Frederick Pratt,30-11-2000,lannister,98,demoOption3,99258964983,hrOXBYaG,yes,MoG,27-10-2002,64961703,64961703 +5839569016,en,Intersolve-voucher-whatsapp,true,Lilly Reeves,30-11-2000,stark,32,demoOption2,97557278508,fLApbxKo,yes,qGz,27-10-2002,4607575,4607575 +53599405543,en,Safaricom,true,Alejandro Marshall,30-11-2000,greyjoy,25,demoOption1,68961665708,vZhNGHON,yes,vTK,27-10-2002,25038827,25038827 +62882687019,en,Safaricom,false,Delia Wise,30-11-2000,stark,9,demoOption3,45528287575,eixEKtmY,yes,PsT,16-05-1990,85791099,85791099 +65282353578,nl,Safaricom,true,Jay Larson,30-11-2000,stark,91,demoOption2,15382721825,mZAdmgVB,yes,Woy,16-05-1990,57107304,57107304 +34630230028,en,Safaricom,true,Clyde Keller,15-03-1990,lannister,15,demoOption3,98332439448,MfIitTEo,no,yxR,27-10-2002,61609680,61609680 +38338205808,en,Safaricom,false,Eva Snyder,15-03-1990,stark,7,demoOption1,58345798755,yAerLsof,yes,UPU,16-05-1990,72770123,72770123 +27043987635,nl,Safaricom,false,Rachel Rose,30-11-2000,lannister,65,demoOption2,82422236592,hGYRjHsh,no,Sri,16-05-1990,31969448,31969448 +62970111546,nl,Safaricom,false,Lura Jefferson,15-03-1990,lannister,45,demoOption3,40735513043,mYmfbyMd,no,ZNi,16-05-1990,75386852,75386852 +28481264448,nl,ironBank,true,Eunice Turner,30-11-2000,lannister,96,demoOption3,30274111461,NmdRiAdN,yes,VYw,27-10-2002,81522967,81522967 +55798117942,en,Safaricom,false,Edna Daniel,30-11-2000,lannister,53,demoOption1,12931989415,bmDucJEY,no,Vrf,27-10-2002,12609447,12609447 +29649622558,nl,Safaricom,false,Cory Chandler,30-11-2000,lannister,6,demoOption2,44081692175,osJWguQu,no,dkZ,27-10-2002,27631253,27631253 +95298559445,nl,Intersolve-voucher-whatsapp,true,Ricky Sharp,30-11-2000,greyjoy,62,demoOption2,22410751279,scTmvZGi,no,vma,27-10-2002,93672761,93672761 +79933020354,en,Safaricom,false,Charlotte Newman,15-03-1990,stark,40,demoOption3,8721859258,QnoCjJdu,no,CRC,16-05-1990,55488315,55488315 +94142344952,nl,Safaricom,false,John Peters,15-03-1990,greyjoy,0,demoOption1,5268774619,AvejvhdH,yes,vVP,16-05-1990,77028667,77028667 +62087284134,en,Safaricom,true,Aaron Warner,30-11-2000,lannister,63,demoOption3,68008273497,SoIfkOoy,yes,zVK,16-05-1990,62649380,62649380 +1938662886,nl,ironBank,false,Beulah Steele,30-11-2000,greyjoy,32,demoOption2,68009188128,iSGlsVsa,yes,qAU,27-10-2002,44485898,44485898 +99122327667,nl,Safaricom,false,Virgie Evans,15-03-1990,stark,3,demoOption1,52538602580,TDLNTvMk,no,XMr,27-10-2002,70235505,70235505 +12401274449,en,Safaricom,false,Emilie Brewer,30-11-2000,lannister,45,demoOption2,49129757570,urOUeLwX,no,ocR,16-05-1990,13167977,13167977 +61995676537,nl,ironBank,true,Kevin Pittman,15-03-1990,lannister,92,demoOption2,66216106996,kmCxnwMq,yes,guA,27-10-2002,18267781,18267781 +17229896674,nl,Safaricom,false,Eunice Ray,15-03-1990,stark,72,demoOption3,83247648564,gobPKmCh,no,BKR,16-05-1990,53405489,53405489 +20604955953,nl,Safaricom,false,Derek Gutierrez,30-11-2000,greyjoy,63,demoOption3,10218073180,aLzsFYqv,no,VOL,16-05-1990,74144475,74144475 +66112298340,nl,ironBank,false,Max Bailey,30-11-2000,stark,32,demoOption2,72258020595,hLUWkcmA,yes,bGP,16-05-1990,76683438,76683438 +74145446970,en,gringotts,false,Alvin Vaughn,15-03-1990,stark,73,demoOption2,40525079401,zzvAsrNn,no,AQs,27-10-2002,99707420,99707420 +84141371093,en,Intersolve-voucher-whatsapp,false,Leonard Banks,30-11-2000,lannister,59,demoOption1,75234248479,GKPFSXNR,no,MQW,27-10-2002,75852287,75852287 +28778278736,en,Safaricom,false,Carlos Crawford,30-11-2000,stark,79,demoOption1,86100290325,YgUqdJvM,no,Xnh,27-10-2002,82595017,82595017 +97003060150,nl,Intersolve-voucher-whatsapp,false,Ella Figueroa,15-03-1990,lannister,53,demoOption2,71811635695,qyYReUwv,yes,Ifx,27-10-2002,74549707,74549707 +13248806707,en,Intersolve-voucher-whatsapp,true,Amelia Richardson,30-11-2000,greyjoy,9,demoOption2,68633515850,ysYRopoT,yes,usm,16-05-1990,64846660,64846660 +88151683423,nl,Safaricom,true,Effie Kelly,15-03-1990,lannister,32,demoOption1,41983867072,kxxZuQup,no,JSY,27-10-2002,43272626,43272626 +3965081935,nl,Safaricom,true,Kevin McKinney,30-11-2000,greyjoy,17,demoOption3,81390395574,iNkSHpwC,no,RHO,16-05-1990,36113665,36113665 +41295310874,en,gringotts,false,Jeffery Mason,30-11-2000,lannister,69,demoOption1,92926917519,noSUGaat,no,YtD,16-05-1990,65120102,65120102 +99185589862,en,Safaricom,true,Leila White,30-11-2000,greyjoy,14,demoOption3,81470593031,jQRJTvoh,no,Zbf,16-05-1990,96192113,96192113 +8701430845,en,Intersolve-voucher-whatsapp,false,Alta Shelton,15-03-1990,stark,65,demoOption2,75268678119,WgmkpELB,yes,Tdu,27-10-2002,61704390,61704390 +14626866355,nl,Safaricom,true,Rena May,30-11-2000,greyjoy,3,demoOption1,14091368268,NXaqpUnx,no,hjt,27-10-2002,38117299,38117299 +56788309267,en,Intersolve-voucher-whatsapp,false,Harriett Beck,15-03-1990,greyjoy,88,demoOption1,2434174492,oyUgKGQj,yes,GNa,27-10-2002,35268120,35268120 +47189850188,en,gringotts,true,Wesley Erickson,15-03-1990,greyjoy,61,demoOption3,41892827372,ARAWgvop,no,vIK,27-10-2002,56922055,56922055 +84993783013,en,gringotts,true,Mike Little,30-11-2000,greyjoy,50,demoOption3,48465226969,hpkznwtc,no,osp,27-10-2002,62064950,62064950 +91259576203,nl,Safaricom,true,Maria Delgado,15-03-1990,greyjoy,51,demoOption2,30947098892,gSvSrnyb,no,BMi,16-05-1990,60275422,60275422 +8319336938,en,Safaricom,true,Ida Osborne,30-11-2000,lannister,79,demoOption3,95324004683,eUmOxJZs,no,euz,27-10-2002,40890099,40890099 +41358647563,en,Safaricom,true,Marvin Lawrence,30-11-2000,greyjoy,32,demoOption3,97805346734,KyDZzpMZ,yes,BCH,16-05-1990,79048854,79048854 +17612532912,nl,Intersolve-voucher-whatsapp,false,Shane Joseph,15-03-1990,greyjoy,99,demoOption2,94749532957,GcGCqenC,yes,OMR,27-10-2002,16666901,16666901 +13740091546,en,Safaricom,true,Max Delgado,30-11-2000,lannister,90,demoOption2,88541182046,MdgXKkNz,yes,pCI,27-10-2002,41869049,41869049 +75147177161,nl,Safaricom,false,Danny Cummings,30-11-2000,greyjoy,44,demoOption1,8291740152,zQSjUqzO,yes,vwg,27-10-2002,7854282,7854282 +78637905780,nl,ironBank,false,Willie Bryant,30-11-2000,stark,76,demoOption2,35101200508,BkxunDnn,yes,UBv,27-10-2002,56394614,56394614 +29261595674,nl,Safaricom,false,Bruce Diaz,15-03-1990,lannister,11,demoOption2,50025960327,EEUzofwr,yes,ztl,27-10-2002,62515299,62515299 +32059524948,en,Safaricom,true,Joel Greer,15-03-1990,stark,71,demoOption1,52826386789,RajsNphQ,no,oku,27-10-2002,99046651,99046651 +10214738893,nl,Intersolve-voucher-whatsapp,true,Lillian Pratt,15-03-1990,lannister,13,demoOption2,2777321247,htAIERIx,yes,NLe,16-05-1990,77869517,77869517 +13197872815,en,Intersolve-voucher-whatsapp,false,Linnie Warren,30-11-2000,greyjoy,63,demoOption1,79893515000,dJJOcYui,yes,DDX,16-05-1990,44112783,44112783 +48116638365,en,Safaricom,true,Seth McCoy,15-03-1990,lannister,67,demoOption1,89233941048,mJPyReDV,yes,yZR,27-10-2002,46543031,46543031 +5018786704,en,Safaricom,true,Todd Hodges,30-11-2000,greyjoy,92,demoOption3,63698851833,vLkZqWgL,no,aJF,16-05-1990,23303051,23303051 +97595659330,en,Safaricom,false,Lou Todd,30-11-2000,lannister,70,demoOption2,13289276,ScqDXULK,no,OVm,27-10-2002,82129878,82129878 +79300524440,en,Safaricom,false,Lela Burns,15-03-1990,lannister,8,demoOption2,77110105563,tkadDkAL,no,iTr,27-10-2002,76109956,76109956 +60715825077,en,Intersolve-voucher-whatsapp,true,Francis Thompson,15-03-1990,greyjoy,34,demoOption2,34304990921,cxDQaNRC,no,cVX,16-05-1990,37108526,37108526 +22273422184,nl,Safaricom,true,Minnie Mack,30-11-2000,greyjoy,94,demoOption1,97026381390,galVluLe,yes,NVk,16-05-1990,24627131,24627131 +59291109121,nl,Safaricom,true,Nina Frazier,30-11-2000,lannister,80,demoOption2,65011149708,WBNQmFxr,no,QRw,27-10-2002,60728969,60728969 +74233973565,en,Intersolve-voucher-whatsapp,false,Ian Townsend,15-03-1990,greyjoy,26,demoOption3,19664125323,bZBykCZn,yes,idQ,27-10-2002,51602042,51602042 +96206560913,en,Safaricom,true,Ann Boone,30-11-2000,greyjoy,57,demoOption2,8211146428,bNPHCSjB,yes,msu,27-10-2002,60984325,60984325 +57601351245,nl,Safaricom,true,Vernon Taylor,30-11-2000,stark,64,demoOption2,30727650741,VALiIOzq,no,ACw,16-05-1990,54423823,54423823 +59444513541,nl,Intersolve-voucher-whatsapp,false,Inez Glover,30-11-2000,lannister,40,demoOption1,9957825082,mKxAVpdu,no,HmW,27-10-2002,62255423,62255423 +64017373149,en,Safaricom,true,Victoria Wallace,30-11-2000,greyjoy,32,demoOption1,53256420784,MxOiHpef,no,QTe,16-05-1990,86597895,86597895 +49282341974,nl,ironBank,false,Dorothy Aguilar,30-11-2000,stark,85,demoOption1,26454734892,NnaFLqpE,no,MfF,16-05-1990,31087068,31087068 +41708843536,nl,Safaricom,false,Blake Underwood,15-03-1990,lannister,8,demoOption2,53315897662,XrZARtOt,yes,bFe,16-05-1990,58739719,58739719 +4572734098,nl,ironBank,false,Roy Hill,30-11-2000,stark,86,demoOption3,6807218193,DcysHbbN,no,mwm,16-05-1990,29975169,29975169 +5699106589,en,Safaricom,true,Shawn Wheeler,15-03-1990,stark,63,demoOption1,82462474207,gjUmHDCF,yes,tuL,27-10-2002,28878173,28878173 +46407765937,nl,Intersolve-voucher-whatsapp,false,Theresa Dunn,30-11-2000,lannister,45,demoOption3,50362161098,DTwAvWxm,no,SKs,27-10-2002,3259160,3259160 +83181759183,en,gringotts,false,Mattie Neal,15-03-1990,lannister,72,demoOption1,72489354928,nabWQWhO,no,fVf,27-10-2002,3930832,3930832 +50773550417,en,gringotts,false,Nina Conner,30-11-2000,stark,24,demoOption3,74708077288,kOZMiLRJ,no,RtA,27-10-2002,56521080,56521080 +38812840418,nl,Safaricom,true,Edith Webster,15-03-1990,stark,20,demoOption1,32093265298,RdaYtdnr,yes,CTo,27-10-2002,40512977,40512977 +36967492512,en,Safaricom,true,Myrtie Herrera,15-03-1990,greyjoy,57,demoOption2,36670266231,mQppQxCz,no,nxy,16-05-1990,84313291,84313291 +50000034756,nl,Safaricom,true,Clifford West,15-03-1990,stark,93,demoOption2,8370530351,aJDEdJWb,yes,XOA,27-10-2002,49931854,49931854 +68622970716,en,gringotts,true,Stella Green,30-11-2000,lannister,75,demoOption2,26979466849,QUkINaKy,no,yEi,16-05-1990,42647959,42647959 +71928683115,nl,ironBank,false,Hunter Foster,30-11-2000,lannister,41,demoOption1,10028778145,QdJMgHzy,yes,rdq,27-10-2002,64888264,64888264 +74396921360,en,Safaricom,true,Jason Patton,30-11-2000,greyjoy,98,demoOption3,1857650098,ELlqZvkz,no,Oys,16-05-1990,92830579,92830579 +59233708807,en,Safaricom,false,Oscar Baldwin,15-03-1990,lannister,95,demoOption1,79653249165,GHGmBNfH,no,Mdt,27-10-2002,88943361,88943361 +16410477235,nl,Safaricom,false,Elmer Coleman,30-11-2000,greyjoy,90,demoOption1,30731221490,ITtBzgCw,no,LfT,27-10-2002,86099294,86099294 +89253486972,en,Safaricom,false,Louise Vega,15-03-1990,lannister,53,demoOption3,17129085889,CoumLdje,no,IbE,16-05-1990,89236904,89236904 +92147272012,en,Safaricom,true,Winnie Boyd,15-03-1990,lannister,55,demoOption1,9327050688,rLUQIwUJ,yes,aDX,27-10-2002,75886207,75886207 +23857406687,nl,Intersolve-voucher-whatsapp,true,Rose Washington,30-11-2000,stark,53,demoOption3,51737549725,GbCcOEhj,yes,AtO,16-05-1990,26715157,26715157 +50487616800,nl,Safaricom,true,Estella Burke,15-03-1990,greyjoy,70,demoOption3,30853676620,GbNhMJjs,yes,Dks,27-10-2002,87007432,87007432 +23493626706,nl,Safaricom,true,Clara Francis,30-11-2000,greyjoy,43,demoOption2,32886930172,zLYoXWrq,no,WSW,27-10-2002,39415081,39415081 +25323004367,nl,Safaricom,true,Emily McKenzie,30-11-2000,greyjoy,91,demoOption3,28522439870,fYARiPOA,yes,yue,27-10-2002,28723350,28723350 +96815037247,en,gringotts,false,Bertha Allison,30-11-2000,greyjoy,87,demoOption1,62460833853,SMRnKCfl,yes,pem,16-05-1990,13649810,13649810 +7629094041,en,gringotts,true,Amanda Holmes,15-03-1990,greyjoy,67,demoOption1,675785861,MFvGcoYQ,no,yMr,16-05-1990,26511233,26511233 +96936150980,en,Safaricom,true,Thomas Casey,15-03-1990,greyjoy,1,demoOption1,11684555198,xWNrtfFQ,no,jQt,27-10-2002,65754515,65754515 +47309469664,nl,ironBank,false,Jeanette Walton,15-03-1990,greyjoy,27,demoOption1,79860691750,OiBLrBBd,yes,xNE,16-05-1990,52764247,52764247 +2340848718,en,Safaricom,true,Estella McBride,30-11-2000,lannister,51,demoOption3,87067694720,gIRCZSue,yes,sLt,27-10-2002,81714709,81714709 +35637281032,en,Safaricom,false,Ivan Blake,15-03-1990,lannister,55,demoOption1,73495314360,bhteeJXQ,no,abR,27-10-2002,70818842,70818842 +67313321361,nl,Intersolve-voucher-whatsapp,true,Milton Munoz,30-11-2000,greyjoy,16,demoOption2,19043125213,PJaNGgIe,yes,iaJ,16-05-1990,75800941,75800941 +5210841697,en,Safaricom,true,Mathilda Higgins,30-11-2000,greyjoy,38,demoOption1,81967496905,HrscTyFI,no,fZQ,27-10-2002,64594387,64594387 +7070930739,en,Intersolve-voucher-whatsapp,true,Thomas Frank,15-03-1990,greyjoy,17,demoOption1,75692108868,PiVRYAxa,yes,Jdu,27-10-2002,84818529,84818529 +73381333620,nl,ironBank,true,Kate Black,30-11-2000,lannister,27,demoOption2,61675450171,ERTNJvZk,no,cFv,27-10-2002,80393500,80393500 +84993006516,nl,Safaricom,false,Hettie Powers,15-03-1990,lannister,38,demoOption1,37345821309,sOfUEftS,yes,XoD,16-05-1990,12560307,12560307 +5817640931,nl,Intersolve-voucher-whatsapp,true,Gerald Bryant,30-11-2000,lannister,14,demoOption1,61747775094,CtYuysef,no,deg,27-10-2002,87998031,87998031 +41676236943,en,Safaricom,true,Marguerite Ortiz,30-11-2000,stark,52,demoOption3,93962425835,bHgVUxaU,yes,MlD,27-10-2002,38488673,38488673 +56424464206,nl,ironBank,true,Elsie Chapman,30-11-2000,lannister,62,demoOption2,96367027396,ixcnBJVg,yes,KTX,27-10-2002,30836386,30836386 +84279128429,en,Intersolve-voucher-whatsapp,false,Harry Neal,15-03-1990,lannister,20,demoOption3,91417162676,qidEFGjf,no,Pzd,16-05-1990,94248811,94248811 +18683752181,nl,Safaricom,false,Ida McKinney,30-11-2000,lannister,69,demoOption2,98322833711,xOCLlJEH,no,AAY,27-10-2002,83598001,83598001 +86054410279,nl,Safaricom,true,Minerva Pena,15-03-1990,greyjoy,93,demoOption2,52781085237,OENvuUhZ,no,Ish,27-10-2002,79300611,79300611 +28447739455,nl,Safaricom,false,Jimmy Chandler,30-11-2000,lannister,44,demoOption2,69965243363,JSRMsWqx,yes,vJu,27-10-2002,12644326,12644326 +19093251359,en,gringotts,true,Stanley Blake,30-11-2000,greyjoy,77,demoOption2,56667886349,vhxEqizW,yes,AAI,27-10-2002,27491613,27491613 +8101796923,en,Intersolve-voucher-whatsapp,false,Cornelia Bennett,15-03-1990,greyjoy,51,demoOption2,57860970716,TZhrPCWO,no,UsI,16-05-1990,71295754,71295754 +86005614971,nl,Intersolve-voucher-whatsapp,true,Lenora Buchanan,15-03-1990,greyjoy,24,demoOption2,53016858101,BulbxEuo,no,EgC,27-10-2002,90732874,90732874 +20928641968,en,Safaricom,true,Mason Richards,15-03-1990,lannister,17,demoOption1,48881758823,ALKTqPmF,no,tEq,27-10-2002,41399437,41399437 diff --git a/e2e/test-registration-data/test-registrations-westeros-20.csv b/e2e/test-registration-data/test-registrations-westeros-20.csv index 61c9bd63bb..dbd8ccd0f7 100644 --- a/e2e/test-registration-data/test-registrations-westeros-20.csv +++ b/e2e/test-registration-data/test-registrations-westeros-20.csv @@ -1,21 +1,21 @@ -referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,firstName,lastName,phoneNumber,fspName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity,house -00dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -01dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -02dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -03dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -04dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -05dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -06dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -07dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -08dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -09dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -10dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -11dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -12dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -13dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -14dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238876,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -15dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238856,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -16dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -17dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -18dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark -19dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,Test,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag,stark +referenceId,preferredLanguage,maxPayments,firstName,lastName,phoneNumber,programFinancialServiceProviderConfigurationName,whatsappPhoneNumber,house,dragon,knowsNothing,motto,personalId,date,accountId,openAnswer,fixedChoice,healthArea +00dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +01dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +02dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +03dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +04dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +05dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +06dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +07dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +08dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +09dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,ironBank,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +10dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +11dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +12dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +13dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +14dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238876,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +15dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238856,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +16dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +17dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +18dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName +19dc9451-1273-484c-b2e8-ae21b51a96ab,en,,Test,succeed,14155238886,gringotts,14155238886,stark,0,1,Winter is coming,id1,30-06-1994,1,agreed,yes,HealthAreaName diff --git a/e2e/tests/121-Portal/EditInfoPersonAffected/OpenPopUpViewAndEdit.spec.ts b/e2e/tests/121-Portal/EditInfoPersonAffected/OpenPopUpViewAndEdit.spec.ts index 61e159da19..fed4258851 100644 --- a/e2e/tests/121-Portal/EditInfoPersonAffected/OpenPopUpViewAndEdit.spec.ts +++ b/e2e/tests/121-Portal/EditInfoPersonAffected/OpenPopUpViewAndEdit.spec.ts @@ -3,7 +3,6 @@ import { test } from '@playwright/test'; import { AppRoutes } from '@121-portal/src/app/app-routes.enum'; import englishTranslations from '@121-portal/src/assets/i18n/en.json'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import fspIntersolveVisa from '@121-service/src/seed-data/fsp/fsp-intersolve-visa.json'; import NLRCProgram from '@121-service/src/seed-data/program/program-nlrc-ocw.json'; import { seedPaidRegistrations } from '@121-service/test/helpers/registration.helper'; import { resetDB } from '@121-service/test/helpers/utility.helper'; @@ -15,7 +14,10 @@ import PersonalInformationPopUp from '@121-e2e/pages/PersonalInformationPopUp/Pe import TableModule from '@121-e2e/pages/Table/TableModule'; const nlrcOcwProgrammeTitle = NLRCProgram.titlePortal.en; -const whatsappLabel = fspIntersolveVisa.questions[5].label.en; +const whatsappLabel = NLRCProgram.programRegistrationAttributes.find( + (attribute) => attribute.name === 'whatsappPhoneNumber', +)!.label.en; + const save = englishTranslations.common.save; test.beforeEach(async ({ page }) => { diff --git a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateCustomAttributesSuccessfully.spec.ts b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateCustomAttributesSuccessfully.spec.ts index d9b89359ea..d0091d14b2 100644 --- a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateCustomAttributesSuccessfully.spec.ts +++ b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateCustomAttributesSuccessfully.spec.ts @@ -38,7 +38,9 @@ test.beforeEach(async ({ page }) => { ); }); -test('[28043] Update custom attributes successfully', async ({ page }) => { +test('[28043] Update registration attributes successfully', async ({ + page, +}) => { const table = new TableModule(page); const homePage = new HomePage(page); const registration = new RegistrationDetails(page); diff --git a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateFinancialServiceProvider.spec.ts b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateFinancialServiceProvider.spec.ts index 642cebccb9..eb4e15bc79 100644 --- a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateFinancialServiceProvider.spec.ts +++ b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdateFinancialServiceProvider.spec.ts @@ -2,10 +2,11 @@ import { test } from '@playwright/test'; import { AppRoutes } from '@121-portal/src/app/app-routes.enum'; import englishTranslations from '@121-portal/src/assets/i18n/en.json'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { getFinancialServiceProviderSettingByNameOrThrow } from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import visaFspIntersolve from '@121-service/src/seed-data/fsp/fsp-intersolve-visa.json'; -import fspIntersolveVoucher from '@121-service/src/seed-data/fsp/fsp-intersolve-voucher-whatsapp.json'; import NLRCProgram from '@121-service/src/seed-data/program/program-nlrc-ocw.json'; +import programOcw from '@121-service/src/seed-data/program/program-nlrc-ocw.json'; import { seedPaidRegistrations } from '@121-service/test/helpers/registration.helper'; import { resetDB } from '@121-service/test/helpers/utility.helper'; import { registrationsOCW } from '@121-service/test/registrations/pagination/pagination-data'; @@ -18,8 +19,32 @@ import TableModule from '@121-e2e/pages/Table/TableModule'; const nlrcOcwProgrammeTitle = NLRCProgram.titlePortal.en; const save = englishTranslations.common.save; const ok = englishTranslations.common.ok; -const voucherFspName = fspIntersolveVoucher.displayName.en; -const visaFspName = visaFspIntersolve.displayName.en; +const voucherFspName = getFinancialServiceProviderSettingByNameOrThrow( + FinancialServiceProviders.intersolveVoucherWhatsapp, +).defaultLabel.en; +const visaFspName = getFinancialServiceProviderSettingByNameOrThrow( + FinancialServiceProviders.intersolveVisa, +).defaultLabel.en; +const visaQuestionStreet = programOcw.programRegistrationAttributes.find( + (attribute) => attribute.name === 'addressStreet', +)!.label.en; + +const visaQuestionHouseNumberAddition = + programOcw.programRegistrationAttributes.find( + (attribute) => attribute.name === 'addressHouseNumberAddition', + )!.label.en; + +const visaQuestionPostalCode = programOcw.programRegistrationAttributes.find( + (attribute) => attribute.name === 'addressPostalCode', +)!.label.en; + +const visaQuestionCity = programOcw.programRegistrationAttributes.find( + (attribute) => attribute.name === 'addressCity', +)!.label.en; + +const visaQuestionHouseNumber = programOcw.programRegistrationAttributes.find( + (attribute) => attribute.name === 'addressHouseNumber', +)!.label.en; test.beforeEach(async ({ page }) => { await resetDB(SeedScript.nlrcMultiple); @@ -55,17 +80,24 @@ test('[28048] Update chosen Finacial service provider', async ({ page }) => { // Need to be fixed await test.step('Update Finacial service provider from Voucher whatsapp to Visa debit card', async () => { await piiPopUp.updatefinancialServiceProvider({ - fspNewName: visaFspName, - fspOldName: voucherFspName, + fspNewName: visaFspName!, + fspOldName: voucherFspName!, saveButtonName: save, okButtonName: ok, + newAttributes: [ + { labelText: visaQuestionHouseNumber, newValue: '3' }, + { labelText: visaQuestionStreet, newValue: 'Nieuwe straat' }, + { labelText: visaQuestionHouseNumberAddition, newValue: 'D' }, + { labelText: visaQuestionPostalCode, newValue: '1234AB' }, + { labelText: visaQuestionCity, newValue: 'Amsterdam' }, + ], }); }); await test.step('Validate Finacial service provider be updated', async () => { await table.validateFspCell({ rowNumber, - fspName: visaFspName, + fspName: visaFspName!, }); }); }); diff --git a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePaymentMultiplierInvalid.spec.ts b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePaymentMultiplierInvalid.spec.ts index 6c6d7a1949..5c74f0790b 100644 --- a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePaymentMultiplierInvalid.spec.ts +++ b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePaymentMultiplierInvalid.spec.ts @@ -49,18 +49,6 @@ test('[28040] Update paymentAmountMultiplier with invalid value', async ({ await table.selectFspPaPii({ shouldSelectVisa: true }); }); - await test.step('Update payment amount multiplier with empty string', async () => { - await piiPopUp.updatepaymentAmountMultiplier({ - amount: '', - saveButtonName: save, - okButtonName: ok, - alert: alertPattern.replace( - '{{error}}', - 'paymentAmountMultiplier must be a positive number', - ), - }); - }); - await test.step('Update payment amount multiplier with negative number', async () => { await piiPopUp.updatepaymentAmountMultiplier({ amount: '-1', @@ -68,7 +56,7 @@ test('[28040] Update paymentAmountMultiplier with invalid value', async ({ okButtonName: ok, alert: alertPattern.replace( '{{error}}', - 'paymentAmountMultiplier must be a positive number', + 'paymentAmountMultiplier: this field must be a positive number', ), }); }); diff --git a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePhoneNumberInvalid.spec.ts b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePhoneNumberInvalid.spec.ts index 43f0ff3383..6deab7e69d 100644 --- a/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePhoneNumberInvalid.spec.ts +++ b/e2e/tests/121-Portal/EditInfoPersonAffected/UpdatePhoneNumberInvalid.spec.ts @@ -16,10 +16,6 @@ import TableModule from '@121-e2e/pages/Table/TableModule'; const nlrcOcwProgrammeTitle = NLRCProgram.titlePortal.en; const save = englishTranslations.common.save; const ok = englishTranslations.common.ok; -const noneEmptyPhoneNumberAlert = - englishTranslations['page'].program['program-people-affected'][ - 'edit-person-affected-popup' - ].properties.error['not-empty']; const alertPattern = englishTranslations.common['error-with-message']; test.beforeEach(async ({ page }) => { @@ -43,8 +39,8 @@ test('[28045] Update phoneNumber with invalid value', async ({ page }) => { const homePage = new HomePage(page); const piiPopUp = new PersonalInformationPopUp(page); - function createAlertMessage(pattern: string, phoneNumber: string): string { - const error = `The value '${phoneNumber}' given for the attribute 'phoneNumber' does not have the correct format for type 'tel'`; + function createAlertMessage(pattern: string): string { + const error = `phoneNumber: This value is not a valid phonenumber according to Twilio lookup`; return pattern.replace('{{error}}', error); } @@ -56,30 +52,9 @@ test('[28045] Update phoneNumber with invalid value', async ({ page }) => { await table.selectFspPaPii({ shouldSelectVisa: true }); }); - await test.step('Update phone number with empty string', async () => { - const phoneNumber = ''; - await piiPopUp.updatePhoneNumber({ - phoneNumber, - saveButtonName: save, - okButtonName: ok, - alert: alertPattern.replace('{{error}}', noneEmptyPhoneNumberAlert), - }); - }); - - await test.step('Update phone number with longer(18 digit) number', async () => { - const phoneNumber = '123456789012345678'; - const alertMessage = createAlertMessage(alertPattern, phoneNumber); - await piiPopUp.updatePhoneNumber({ - phoneNumber, - saveButtonName: save, - okButtonName: ok, - alert: alertMessage, - }); - }); - - await test.step('Update phone number with shorter(7 digit) number', async () => { - const phoneNumber = '1234567'; - const alertMessage = createAlertMessage(alertPattern, phoneNumber); + await test.step('Update phone number with invalid lookup number', async () => { + const phoneNumber = '16005550005'; + const alertMessage = createAlertMessage(alertPattern); await piiPopUp.updatePhoneNumber({ phoneNumber, saveButtonName: save, diff --git a/e2e/tests/121-Portal/MakeNewPayment/Safaricom/MissingNationalIdErrorSafaricom.spec.ts b/e2e/tests/121-Portal/MakeNewPayment/Safaricom/MakeFailedPaymentSafaricom.spec.ts similarity index 84% rename from e2e/tests/121-Portal/MakeNewPayment/Safaricom/MissingNationalIdErrorSafaricom.spec.ts rename to e2e/tests/121-Portal/MakeNewPayment/Safaricom/MakeFailedPaymentSafaricom.spec.ts index 93ac42bdca..09ffa76617 100644 --- a/e2e/tests/121-Portal/MakeNewPayment/Safaricom/MissingNationalIdErrorSafaricom.spec.ts +++ b/e2e/tests/121-Portal/MakeNewPayment/Safaricom/MakeFailedPaymentSafaricom.spec.ts @@ -1,5 +1,6 @@ import { test } from '@playwright/test'; +import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import KRCSProgram from '@121-service/src/seed-data/program/program-krcs-turkana.json'; import { seedIncludedRegistrations } from '@121-service/test/helpers/registration.helper'; @@ -25,17 +26,22 @@ const paymentStatus = englishTranslations.entity.payment.status.error; const paymentFilter = englishTranslations['registration-details']['activity-overview'].filters .payment; -const paymentErrorMessages = 'Property idNumber is undefined'; +const paymentErrorMessages = + 'Error Occurred - Invalid Access Token - mocked_access_token'; +const registrationsSafaricomFailed = structuredClone( + registrationsSafaricom, +) as unknown as MappedPaginatedRegistrationDto[]; +registrationsSafaricomFailed[0].phoneNumber = '254000000000'; test.beforeEach(async ({ page }) => { await resetDB(SeedScript.krcsMultiple); const programIdBHA = 2; const bhaProgramId = programIdBHA; - registrationsSafaricom[0].nationalId = ''; + // make shallow copy const accessToken = await getAccessToken(); await seedIncludedRegistrations( - registrationsSafaricom, + registrationsSafaricomFailed, bhaProgramId, accessToken, ); @@ -49,18 +55,16 @@ test.beforeEach(async ({ page }) => { ); }); -test('[30262] Safaricom: Error because of missing National ID', async ({ - page, -}) => { +test('[30262] Safaricom: Make failed payment', async ({ page }) => { const table = new TableModule(page); const navigationModule = new NavigationModule(page); const homePage = new HomePage(page); const registrationPage = new RegistrationDetails(page); const paymentsPage = new PaymentsPage(page); - const numberOfPas = registrationsSafaricom.length; + const numberOfPas = registrationsSafaricomFailed.length; const defaultTransferValue = KRCSProgram.fixedTransferValue; - const defaultMaxTransferValue = registrationsSafaricom.reduce( + const defaultMaxTransferValue = registrationsSafaricomFailed.reduce( (output, pa) => { return output + pa.paymentAmountMultiplier * defaultTransferValue; }, diff --git a/e2e/tests/121-Portal/MakeNewPayment/Safaricom/RetryPaymentSafaricom.spec.ts b/e2e/tests/121-Portal/MakeNewPayment/Safaricom/RetryPaymentSafaricom.spec.ts index 0d73a4d721..fd87c3dbb2 100644 --- a/e2e/tests/121-Portal/MakeNewPayment/Safaricom/RetryPaymentSafaricom.spec.ts +++ b/e2e/tests/121-Portal/MakeNewPayment/Safaricom/RetryPaymentSafaricom.spec.ts @@ -35,15 +35,18 @@ const paymentErrorMessages = ]['fix-error']; const programIdBHA = 2; const bhaProgramId = programIdBHA; +const registrationsSafaricomRetry = JSON.parse( + JSON.stringify(registrationsSafaricom), +); test.beforeEach(async ({ page }) => { await resetDB(SeedScript.krcsMultiple); - registrationsSafaricom[0].phoneNumber = '254000000000'; + registrationsSafaricomRetry[0].phoneNumber = '254000000000'; const accessToken = await getAccessToken(); await seedIncludedRegistrations( - registrationsSafaricom, + registrationsSafaricomRetry, bhaProgramId, accessToken, ); @@ -64,7 +67,7 @@ test('[30279] Safaricom: Retry failed payment', async ({ page }) => { const registrationPage = new RegistrationDetails(page); const paymentsPage = new PaymentsPage(page); - const numberOfPas = registrationsSafaricom.length; + const numberOfPas = registrationsSafaricomRetry.length; const defaultTransferValue = KRCSProgram.fixedTransferValue; const defaultMaxTransferValue = registrationsSafaricom.reduce( (output, pa) => { @@ -120,7 +123,7 @@ test('[30279] Safaricom: Retry failed payment', async ({ page }) => { await updateRegistration( bhaProgramId, - registrationsSafaricom[0].referenceId, + registrationsSafaricomRetry[0].referenceId, { phoneNumber: '254708374149', }, diff --git a/e2e/tests/121-Portal/ViewPaProfilePage/ValidateFspInPaPopUp.spec.ts b/e2e/tests/121-Portal/ViewPaProfilePage/ValidateFspInPaPopUp.spec.ts index e5b3a277bc..30474b1b44 100644 --- a/e2e/tests/121-Portal/ViewPaProfilePage/ValidateFspInPaPopUp.spec.ts +++ b/e2e/tests/121-Portal/ViewPaProfilePage/ValidateFspInPaPopUp.spec.ts @@ -2,8 +2,9 @@ import { test } from '@playwright/test'; import { AppRoutes } from '@121-portal/src/app/app-routes.enum'; import englishTranslations from '@121-portal/src/assets/i18n/en.json'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { getFinancialServiceProviderSettingByNameOrThrow } from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import visaFspIntersolve from '@121-service/src/seed-data/fsp/fsp-intersolve-visa.json'; import NLRCProgram from '@121-service/src/seed-data/program/program-nlrc-ocw.json'; import { seedPaidRegistrations } from '@121-service/test/helpers/registration.helper'; import { resetDB } from '@121-service/test/helpers/utility.helper'; @@ -19,7 +20,9 @@ import TableModule from '../../../pages/Table/TableModule'; const nlrcOcwProgrammeTitle = NLRCProgram.titlePortal.en; const pageTitle = englishTranslations['registration-details'].pageTitle; -const visaFspName = visaFspIntersolve.displayName.en; +const visaFspName = getFinancialServiceProviderSettingByNameOrThrow( + FinancialServiceProviders.intersolveVisa, +).defaultLabel.en; test.beforeEach(async ({ page }) => { await resetDB(SeedScript.nlrcMultiple); @@ -59,6 +62,6 @@ test('[27659][27611] Open the edit PA popup', async ({ page }) => { await registration.validateHeaderToContainText(pageTitle); await registration.openEditPaPopUp(); await registration.validateEditPaPopUpOpened(); - await piiPopUp.validateFspNamePresentInEditPopUp(visaFspName); + await piiPopUp.validateFspNamePresentInEditPopUp(visaFspName!); }); }); diff --git a/e2e/tests/121-Portal/ViewPaProfilePage/ViewActivityFspOverview.spec.ts b/e2e/tests/121-Portal/ViewPaProfilePage/ViewActivityFspOverview.spec.ts index 25fd9bc254..a388b2e501 100644 --- a/e2e/tests/121-Portal/ViewPaProfilePage/ViewActivityFspOverview.spec.ts +++ b/e2e/tests/121-Portal/ViewPaProfilePage/ViewActivityFspOverview.spec.ts @@ -1,8 +1,8 @@ import { test } from '@playwright/test'; import { AppRoutes } from '@121-portal/src/app/app-routes.enum'; -import FspName from '@121-portal/src/app/enums/fsp-name.enum'; import englishTranslations from '@121-portal/src/assets/i18n/en.json'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; @@ -16,12 +16,12 @@ import NLRCProgram from '@121-service/src/seed-data/program/program-nlrc-ocw.jso import { waitFor } from '@121-service/src/utils/waitFor.helper'; import { doPayment, - updateFinancialServiceProvider, waitForPaymentTransactionsToComplete, } from '@121-service/test/helpers/program.helper'; import { awaitChangePaStatus, importRegistrations, + updateRegistration, } from '@121-service/test/helpers/registration.helper'; import { getAccessToken, @@ -89,17 +89,15 @@ test.beforeEach(async ({ page }) => { Object.values(TransactionStatusEnum), ); - await updateFinancialServiceProvider( + await updateRegistration( programIdVisa, + registrationVisa.referenceId, + { + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, + }, + 'automated test', accessToken, - paymentReferenceIds, - FspName.intersolveVoucherPaper, - '31600000000', - 'a', - '2', - '3', - '1234CH', - 'Waddinxveen', ); // Login diff --git a/e2e/tests/121-Portal/ViewPaProfilePage/ViewPersonalInformationTable.spec.ts b/e2e/tests/121-Portal/ViewPaProfilePage/ViewPersonalInformationTable.spec.ts index cddda500ac..ef4487932a 100644 --- a/e2e/tests/121-Portal/ViewPaProfilePage/ViewPersonalInformationTable.spec.ts +++ b/e2e/tests/121-Portal/ViewPaProfilePage/ViewPersonalInformationTable.spec.ts @@ -2,8 +2,9 @@ import { test } from '@playwright/test'; import { AppRoutes } from '@121-portal/src/app/app-routes.enum'; import englishTranslations from '@121-portal/src/assets/i18n/en.json'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { getFinancialServiceProviderSettingByNameOrThrow } from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import visaFspTranslations from '@121-service/src/seed-data/fsp/fsp-intersolve-visa.json'; import NLRCProgram from '@121-service/src/seed-data/program/program-nlrc-ocw.json'; import { seedPaidRegistrations } from '@121-service/test/helpers/registration.helper'; import { resetDB } from '@121-service/test/helpers/utility.helper'; @@ -20,7 +21,9 @@ const pageTitle = englishTranslations['registration-details'].pageTitle; const status = englishTranslations.entity.registration.status.included; const language = englishTranslations.page.program['program-people-affected'].language.nl; -const visaFsp = visaFspTranslations.displayName.en; +const visaFsp = getFinancialServiceProviderSettingByNameOrThrow( + FinancialServiceProviders.intersolveVisa, +).defaultLabel.en; test.beforeEach(async ({ page }) => { await resetDB(SeedScript.nlrcMultiple); @@ -62,7 +65,7 @@ test('[27492] View Personal information table', async ({ page }) => { await Helpers.getTodaysDate(), language, '+14155235555', - visaFsp, + visaFsp!, ); }); }); diff --git a/features/121-Portal/Change_password.feature b/features/121-Portal/Change_password.feature deleted file mode 100644 index 5b8b75e221..0000000000 --- a/features/121-Portal/Change_password.feature +++ /dev/null @@ -1,37 +0,0 @@ -@portal - -Feature: Change password - - Background: - Given a logged-in user - And "Change password" page is open - - Scenario: Change password successfully - Given "Change password" page is open - And the "Current password" field appears purple when active - And the field turns red when left empty - And the user has entered current password correctly - And the user enters new password - And the new password is at least 8 characters one capital letter, one lowercase letter and one number - And the new password field turns green when active - And the user enters same password in "Confirm password" field - And the confirm password field turns green when active - And the in both fields password match - And the "Update" button turns black - When the user clicks on the "Update" button - Then the password is changed - And "Password changed" is displayed in a green field above the "Current password" field - And the user is navigated to the home page after 3 seconds - - Scenario: Change password unsuccessfully (Non-matching passwords) - Given the user has entered their current password correctly - And the user enters a new password (which complies with the rules mentioned in Scenario: Change password successfully) - When the user enters a different password in "Confirm password" field than in "New password" field - Then an error is displayed "Passwords do not match" - - Scenario: Change password unsuccessfully (Current password incorrect) - Given the user has entered their current password incorrectly - And the user enters a new password (which complies with the rules mentioned in Scenario: Change password successfully) - And the user enters the same password in the "Confirm password" field - When the user clicks on the "Update" button - Then an error is displayed "Failed attempt: current password incorrect" diff --git a/features/121-Portal/Delete_people_affected.feature b/features/121-Portal/Delete_people_affected.feature deleted file mode 100644 index c89d5cf76f..0000000000 --- a/features/121-Portal/Delete_people_affected.feature +++ /dev/null @@ -1,25 +0,0 @@ -@portal -Feature: Delete people affected (extension of View_and_Manage_people_affected.feature) - - Background: - Given a logged-in user with "RegistrationDELETE" permission - And the "active phase" is "registration" or "inclusion" - - Scenario: Use bulk-action "delete PA" - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When user selects the "delete PA" action - Then all rows except PAs with status "included", "completed", or "paused" should be eligible - - Scenario: Confirm "delete PA" action - Given the generic "confirm apply action" scenario (see View_and_Manage_people_affected.feature) - When the "bulk action" is "delete PA" - Then the "Pop up" to confirm will open - And it mentions the selected number of PAs to delete - And it mentions an additional explanation sentence - When the user confirms - Then the PA will be switched to status "deleted" - And this means it will no longer be visible in any of the phases, also not through "filter by status: all" - And the selected registrations will be anonymized in the database (field "phoneNumber") - And some related entities will be deleted: "note", "registration_data", "people_affected_app_data", "latest_message", "twilio_message", "whatsapp_pending_message", "try_whatsapp" - And some related entities will be anonymized: "intersolve_voucher" (field "whatsappPhoneNumber"), "safaricom_request" - And some related entities will be untouched as they contain no PII: e.g. "transactions", "registration-status-changes", "imagecode-export-vouchers", "imagecode" diff --git a/features/121-Portal/Edit_Info_Person_Affected.feature b/features/121-Portal/Edit_Info_Person_Affected.feature deleted file mode 100644 index 77d9ea307e..0000000000 --- a/features/121-Portal/Edit_Info_Person_Affected.feature +++ /dev/null @@ -1,139 +0,0 @@ -@portal -Feature: Edit information on Person Affected - - Background: - Given the "selected phase" is one of the phases with the "People Affected table" - - Scenario: View information icon in table - Given a logged-in user with "RegistrationPersonalREAD" permission - When the user views the "People Affected Table" - Then the user sees an "information icon" in each row at the beginning of the "Person Affected" column - When the user hovers over it - Then it is highlighted - - Scenario: No permission to view information - Given the user does not have the "RegistrationPersonalREAD" permission - When the user views the "People Affected Table" - Then the user does not see the information-icon, but only the 'PA#X' text - - Scenario: Open the popup to view and edit information - Given a logged-in user with "RegistrationPersonalREAD" permission - When the user click the "information icon" - Then a popup opens - And in the title the ID-number of the Person Affected is mentioned - And an input-field for the "paymentAmountMultiplier" and "phoneNumber" and "preferredLanguage" is shown - And an input-field for each Custom Attribute is shown - And an input-field for each FSP-attribute (such as "whatsappPhoneNumber") is shown - And a dropdown-list with the current chosen FSP is shown - And all input-fields have an accompanying "save" button which is disabled - And there is an explanation, including PII-warning - And there is a "save" button - - Scenario: Update paymentAmountMultiplier successfully - Given no automatic calculation of paymentAmountMultiplier is configured for the program - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the "paymentAmountMultiplier" is shown - When the user changes the value and presses the save-button - Then the 'Reason for update' popup opens - When the user fills in the reason and presses the 'save' button - Then the save-button changes into a progress indicator - And the value of the input-field is written to the database - And the progress indicator changes into the save-button again - And the "paymentAmountMultiplier" is updated - And the data change is logged in the Profile page Activity overview > See 'View_PA_profile_page.feature' for details - - Scenario: Update paymentAmountMultiplier with invalid value - Given no automatic calculation of paymentAmountMultiplier is configured for the program - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the "paymentAmountMultiplier" is shown - When the user changes the value to "" or a negative number and presses the update-button - Then the update-button changes into a progress indicator - And a feedback message with the specific requirements of the value is shown - And the progress indicator changes into the update-button again - - Scenario: Update preferredLanguage successfully - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the "preferredLanguage" is shown - When the user changes the value and presses the save-button - Then the 'Reason for update' popup opens - When the user fills in the reason and presses the 'save' button - Then the save-button changes into a progress indicator - When the user selects a different option from the dropdown and presses the update-button - Then the update-button changes into a progress indicator - And the value of the input-field is written to the database - And the progress indicator changes into the update-button again - And the "preferredLanguage" is updated - And the data change is logged in the Profile page Activity overview > See 'View_PA_profile_page.feature' for details - - Scenario: Update custom attributes successfully - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the is shown - When the user changes the value to something invalid and presses the update-button - Then the update-button changes into a progress indicator - And a feedback message with the specific requirements of the value is shown - And the progress indicator changes into the update-button again - And - if configured for the program - the "paymentAmountMultiplier" is recalculated based on formula - And the data change is logged in the Profile page Activity overview > See 'View_PA_profile_page.feature' for details - - Scenario: Update 'numeric' answer with invalid value - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the is shown - When the user tries to enter a non-numeric number - Then nothing happens - - Scenario: Update 'phonenumber' answer with invalid value - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the is shown - When the user changes the phonenumber to a non-existent phone-number and presses the update-button - Then the update-button changes into a progress indicator - And a feedback message saying the value is invalid appears - And the progress indicator changes into the update-button again - - Scenario: Update 'phonenumber' answer with empty value - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the is shown - Given that the program-'setting' "allowEmptyPhoneNumber" is set to true - When the user empties out the input-field and presses the update-button - Then the update-button changes into a progress indicator - And a feedback message saying the value is updated - And the progress indicator changes into the update-button again - - Scenario: Update 'date' answer with invalid value - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the is shown - When the user changes the date to an invalid date and presses the update-button - Then the update-button changes into a progress indicator - And a feedback message with the specific requirements of the value is shown - And the progress indicator changes into the update-button again - - Scenario: Update chosen FSP - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user has opened the popup to edit information - Given an input-field for the current "FSP" is shown - When the user changes the chosen FSP - Then a message appears that attributes relating to the current FSP will be deleted and it lists these attributes - And for each attribute of the new FSP a new input field appears - - When the user has filled in values for each new input field - Then the 'update' button of the chosen 'FSP' gets enabled - - When the user clicks the 'update' button - Then the request is made - And response is given whether it was successful or not (if not, likely due to validation errors on the input-fields) - - When the user closes and re-opens the pop-update - Then the input-fields for attributes of the old FSP are gone - And input-fields for attributes of the new FSP are shown - And the new FSP shows as the current selected value of the dropdown - - And the FSP-change is logged in the Profile page Activity overview - And the data changes related to the FSP-change are also logged in the Activity overview with reason 'Financial service provider change' - diff --git a/features/121-Portal/Export_Intersolve_Visa_cards.feature b/features/121-Portal/Export_Intersolve_Visa_cards.feature deleted file mode 100644 index 0ba81fbffd..0000000000 --- a/features/121-Portal/Export_Intersolve_Visa_cards.feature +++ /dev/null @@ -1,19 +0,0 @@ -@portal -Feature: Export Intersolve Visa cards - - Background: - Given a logged-in user with "FspDebitCardEXPORT" permission - Given a program with FSP 'Intersolve Visa debit card' - Given the user is on the "payment" page - - Scenario: Exporting cards - When the user views the "payment" page - Then the user sees an "Export debit card usage" button - When the user clicks the button - Then a confirmation popup opens with description - And it includes a "last done on" timestamp if available - When the user confirms - Then an Excel is downloaded - And it contains all past and current debit cards for the given program only - And it contains columns: paId, referenceId, registrationStatus, cardNumber, issuedDate, lastUsedDate, balance, cardStatus - diff --git a/features/121-Portal/Export_PA_data_changes.feature b/features/121-Portal/Export_PA_data_changes.feature deleted file mode 100644 index 551f108a66..0000000000 --- a/features/121-Portal/Export_PA_data_changes.feature +++ /dev/null @@ -1,20 +0,0 @@ -@portal -Feature: Export all People Affected data changes - - Background: - Given a logged-in user with "RegistrationPersonalEXPORT" permissions - Given the user views a page with "PA-table" - - Scenario: Export all People Affected data changes - When the user clicks the "Export" button - Then the user sees "Export data changes" as one of the options - When the user clicks it - Then a confirmation popup opens with a start-date and an end-date - When the user fills in start and/or end date (if not, then no min and max is used) and clicks 'confirm' - Then an Excel is downloaded - And it contains all FSP and data changes to People Affected data for the given program and within the provided dates (if any) - And it contains PA information (paId, referenceId) - And it contains data change details (type, fieldName, newValue, oldValue, reason, changedBy, changedAt) - And for data changes related to FSP-change the reason shows 'Financial service provider change' - And if scope is enabled for this program, the resulting Excel only contains data changes for the logged-in user's scope - diff --git a/features/121-Portal/Export_Payment_Details.feature b/features/121-Portal/Export_Payment_Details.feature deleted file mode 100644 index 411d06088c..0000000000 --- a/features/121-Portal/Export_Payment_Details.feature +++ /dev/null @@ -1,41 +0,0 @@ -@portal -Feature: Export payment data - - Scenario: Viewing the export options - Given a logged-in user with "RegistrationPersonalEXPORT", "PaymentREAD" and "PaymentTransactionREAD" permissions - When the user views the "payment" page - Then the user sees an "Export payment data" component - And the dropdown contains an option for all closed payments, and a list of all payments, with number, "open" or "closed" and date - - Scenario: Export payment report before payment - Given a logged-in user with "RegistrationPersonalEXPORT", "PaymentREAD" and "PaymentTransactionREAD" permissions - Given the payment has not taken place - When the user selects the first "open" payment from the dropdown-list - Then the "export report" button is changed to the "export included people affected" button - When the user clicks the "export included people affected" button - Then an Excel-file is downloaded - And it shows a list of the registrations that are "included" - And - if program and user have scope - then it shows only the PAs within the scope of the user - And it shows the "name" and other program-attributes to be able to identify people - And it shows the dates at which the person reached each status, to be able to assess the trajectory towards inclusion - And it shows all program questions which have "included" as "export" attribute - And it shows all program custom attributes which have "included" as "export" attribute - And the "export inclusion list" button remains enabled, so the action can be repeated infinitely - And if no "included" registrations then an alert is shown that "no data can be downloaded" - - Scenario: Export payment report after payment - Given a logged-in user with "RegistrationPersonalEXPORT", "PaymentREAD" and "PaymentTransactionREAD" permissions - Given the payment has taken place - When the user selects a "closed" payment from the dropdown-list - Then the "export report" button is enabled - When the user clicks the "export report" button - Then an Excel-file is dowloaded - And it shows a list of the registrations that have been paid out - And - if program and user have scope - then it shows only the PAs within the scope of the user - And "transaction" data: "payment", "timestamp", "amount" (multiplication of "paymentAmountMultiplier" and "transfer value"), "status", "errorMessage", "financialServiceProvider" - And "registration" data: "referenceId", "phoneNumber" and any defined program-questions, fsp-questions and program-custom-attributes that have "payment" as part of the "export" attribute - - Scenario: Viewing the export options without permission - Given a logged-in user without "RegistrationPersonalEXPORT", "PaymentREAD" and "PaymentTransactionREAD" permissions - When the user views the "payment" page - Then the "Export payment data" component is not visible diff --git a/features/121-Portal/Export_duplicate_people_affected_list.feature b/features/121-Portal/Export_duplicate_people_affected_list.feature deleted file mode 100644 index 8d5346e1ba..0000000000 --- a/features/121-Portal/Export_duplicate_people_affected_list.feature +++ /dev/null @@ -1,20 +0,0 @@ -@portal -Feature: Export duplicate people affected list - - Background: - Given a logged-in user with "RegistrationPersonalEXPORT" permission - Given the "selected phase" is the "registration" phase - - Scenario: Export duplicate people affected list - When the user clicks the "export duplicate people affected" and confirms the confirm prompt - Then an Excel-file is downloaded - And it shows a list of the registrations that have any of the columns marked with "duplicateCheck" in common - And - if program and user have scope - then only duplications among registrations within the scope of the user are found and returned - And it shows the "name" and other attributes to be able to identify people - And the "export duplicate people affected" button remains enabled, so the action can be repeated - And if no "duplicate people affected" registrations are found then an alert is shown that "no data can be downloaded" - - Scenario: Viewing the export options without permission - Given a logged-in does not have the "RegistrationPersonalEXPORT" permission - When the user views the "registration" page - Then the export list button is not visible diff --git a/features/121-Portal/Export_people_affected.feature b/features/121-Portal/Export_people_affected.feature deleted file mode 100644 index fecb2c9e89..0000000000 --- a/features/121-Portal/Export_people_affected.feature +++ /dev/null @@ -1,28 +0,0 @@ -@portal -Feature: Export People Affected list - - Background: - Given a logged-in user with "RegistrationPersonalEXPORT" permission - And the "selected phase" is the "registration" phase - - Scenario: Export People Affected list - When the user clicks the "export people affected" button and confirms the confirm prompt - Then an Excel-file is downloaded - And it shows a list of all People Affected - And - if program and user have scope - then it contains only registrations within the scope of the user - And it shows the "name" and other dynamic program-attributes, that are also in the PA-table - And it shows "id" and other generic attributes, that are also in the PA-table - And it does not show any attributes that are not directly visible in the PA-table, such as "note" - And it shows all program questions which have "all-people-affected" as "export" attribute - And it shows all program custom attributes which have "all-people-affected" as "export" attribute - And any columns that only contain null-values are automatically filtered out - - Scenario: Export inclusion list with 15000 PAs - Given there are 15000 PAs in the system - When the user clicks the "export people affected" and confirms the confirm prompt - Then an Excel-file is downloaded as in the scenario above quickly and without problem - - Scenario: Viewing the export options without permission - Given a logged-in user does not have the "RegistrationPersonalEXPORT" permission - When the user views the "registration" page - Then the export list button is not visible diff --git a/features/121-Portal/Export_unused_vouchers.feature b/features/121-Portal/Export_unused_vouchers.feature deleted file mode 100644 index 3ff6d5c919..0000000000 --- a/features/121-Portal/Export_unused_vouchers.feature +++ /dev/null @@ -1,19 +0,0 @@ -@portal -Feature: Export unused vouchers - - Background: - Given a selected program with "Intersolve" FSP - Given a logged-in user with "RegistrationPersonalEXPORT" permissions - - Scenario: Exporting unused vouchers - When the user views the "payment" page - Then the user sees an "Export list of unused vouchers" button - When the user clicks the button - Then a confirmation popup opens with description - And it includes a "last done on" timestamp if available - When the user confirms - Then an Excel is downloaded - And it contains all currently unused vouchers for the given program only - And - if program and user have scope - then it contains only vouchers of PAs within the scope of the user - And it contains columns: payment, issueDate, whatsappPhoneNumber, phoneNumber, lastExternalUpdate, name - diff --git a/features/121-Portal/Import_registrations.feature b/features/121-Portal/Import_registrations.feature deleted file mode 100644 index e983605428..0000000000 --- a/features/121-Portal/Import_registrations.feature +++ /dev/null @@ -1,52 +0,0 @@ -@portal -Feature: Import registrations with status registered - - Background: - Given a logged-in user with the "RegistrationCREATE" and "RegistrationImportTemplateREAD" permissions - Given the "selected phase" is "registrationValidation" - Given the user clicks the "Import registrations" button - - Scenario: Download template for import registrations - When the user clicks the "Download template CSV-file" button - Then a CSV-file is downloaded - And it contains 1 row of column names - And it contains the generic column names "phoneNumber", "preferredLanguage", "fspName", "paymentAmountMultiplier" - And it has dynamic "programCustomAttributes" of that program - And it has the dynamic columns for programQuestions of that program - - When the program is not configured with a paymentAmountMultiplierFormula - Then it contains the column "paymentAmountMultiplier" after the "fspQuestions" - - When the program has scope enabled - Then it contains the column scope - - Scenario: Successfully import registrations via CSV - Given a valid import CSV file is prepared based on the template - Given - if program and user have a scope - the file only contains records within the scope of the user - And it has generic columns "preferredLanguage", "phoneNumber", "fspName" - And it has the dynamic columns for programQuestions of that program - And it has the dynamic "programCustomAttributes" of that program - And it has as delimiter ";" or "," - And it has "X" rows - And the input of each cell is valid - When the user clicks "OK" to confirm the import - Then it shows the number "X" of successfully imported registrations - And the PA-table in the Portal shows "X" new rows of PAs - And they have status "Registered" - And all other columns are filled as if a real registration was done - And - if configured for the program - the "paymentAmountMultiplier" is calculated based on formula - And no SMS is sent to the PA - - Scenario: Unsuccessfully import registrations via CSV - Given an invalid import CSV file because of - - wrong file format/extension - - wrong column names - - disallowed values - - registrations outside the scope of the user - - empty phonenumber while the program disallows that - - duplicate referenceIds in import-file - - referenceId already exists - - etc. - When the user selects this file and clicks "OK" to confirm the import - Then feedback is given that something went wrong and it gives details on where the error is, mainly if in a generic column - And there is no input validation on the dynamic columns diff --git a/features/121-Portal/Include_people_affected.feature b/features/121-Portal/Include_people_affected.feature deleted file mode 100644 index 09c21f74e8..0000000000 --- a/features/121-Portal/Include_people_affected.feature +++ /dev/null @@ -1,33 +0,0 @@ -@portal -Feature: Include people affected - - Background: - Given a logged-in user with the "RegistrationStatusIncludedUPDATE" permission - Given the "selected phase" is the "inclusion" or "payment" phase - - Scenario: View "include in program" action - When opening the "action dropdown" - Then the "include in program" action is visible - - Scenario: Use bulk-action "include in program" - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When user selects the "include in program" action - Then the eligible rows are those with status "registered", "selected for validation", "validated", "rejected", "inclusion ended" and "paused" (in "inclusion phase") or "completed" (in "payment" phase) - - Scenario: Confirm "include in program" action - Given the generic "confirm apply action" scenario (see View_and_Manage_people_affected.feature) - When the "bulk action" is "include in program" - Then the "included" PAs are no longer visible in the "inclusion" page the user is on now, as "included" PAs are viewable in the "payment" page only - And the "status" is updated to "Included" - And if a templated message is present or the custom SMS option is used, an SMS is sent to the PA (see View_and_Manage_people_affected.feature) - - Scenario: Include 10000 PAs - Given there are 10000 "registered" PAs in the system - When the user uses and confirms the "include in program" action on all 10000 PAs - Then this is all processed as in the scenario above, quickly and without problem - - - - - - diff --git a/features/121-Portal/Make_new_payment.feature b/features/121-Portal/Make_new_payment.feature deleted file mode 100644 index f976e210b9..0000000000 --- a/features/121-Portal/Make_new_payment.feature +++ /dev/null @@ -1,319 +0,0 @@ -@portal -Feature: Make a new payment - - Background: - Given a logged-in user with the "PaymentCREATE" permission - And the user views the "payment" page - - Scenario: Show maximum total amount - Given a new payment is possible on the program - Given the number of "PA included" is more then "0" - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When the user selects the "Do payment" action - Then a "row checkbox" appears in the "select" column for eligible rows - When the user selects 1 or more PA's - Then the "apply action" button is enabled - When clicking the "apply action" button - Then the pop-up "Do payment" is shown - And the "Transfer Value" is filled in with the program's default value - And the pop-up shows the number of PAs to pay out to - And it shows the maximum total amount to pay out - And this total amount reflects that some PAs may receive more than the supplied "Transfer Value" because of a "paymentAmountMultiplier" greater than 1 - And this total amount is a maximum because some transactions might fail - - Scenario: Send payment instructions with changed transfer value - Given the "Do payment" prompt is open - Given the user changes the Transfer value to "20" - When the user clicks the button "start payout now" - Then the total maximum amount reflects the changed amount per PA - When the user clicks the button "OK" - Then the payment instructions list is sent to the Financial Service Provider - And the payment instructions for each PA contain the transfer value "20" times the PA's "paymentAmountMultiplier" - And a popup shows for how many PAs the instructions were successfully sent - - Scenario: Send payment instructions - Given this is not the last payment for the program - When the user confirms the payout - Then the payment instructions list is sent to the Financial Service Provider - And the payment instructions for each PA contain the transfer value times the PA's "paymentAmountMultiplier" - And the message "Payout request successfully sent to X PAs" is shown - And it shows an "OK" button - When the users presses "OK" - Then the page refreshes - And the "export payment data" component now shows that the payment is "closed" - And the "export payment data" component now has the next payment enabled - And the "bulk action" dropdown list now shows the next available payment to do - When clicking "Payment history" - Then the payment history popup opens - And it shows the payment number, the payment state, the payment date+time and the transaction amount - And the payment state shows 'Success' when the payment went through - And it shows 'Failed' for failed transactions - And it shows 'Waiting' for waiting transactions - And - for successful transactions - the PA receives (notification about) voucher/cash depending on the FSP - And the 'Export people affected' in the 'Registration' phase now contains 3 new columns for the new payment: status, amount, timestamp - - Scenario: Send payment instructions for 10000 PAs - Given there are 10000 PAs in the system - And they are included (see e.g. Portal/Include_people_affected_Run_Program_role.feature) - Then the user selects the "Do payment" action - And the user selects all 10000 PAs - And the user clicks the "Apply action" button and the "Do payment" popup shows up - When the user clicks the "start payout now" button - Then the message "Payout request successfully sent to X PAs" is shown - And it mentions that it can take some time (very rough estimation: 0.5 seconds per PA) - And it shows an "OK" button - When the users presses "OK" - Then the page refreshes - And the "payout" button is now disabled - And it mentions that a payout is in progress - And it shows a refresh icon - When the user clicks the refresh icon - Then the "Payment History" column will start showing more and more PA's with status waiting - When the user clicks the refresh icon again - Then the "Payment History" column will upgrade more and more PA's from 'waiting' to 'success' or 'error' - When the user clicks the refresh icon again (given the payment has finished) - Then the payout-in-progress message is gone - And the "payout" button for the next payment is enabled again - And the "Payment History" column may still contain PA's on 'waiting', as the status-callbacks go on longer then the request-loop - When the user refreshes the page again - Then eventually all 'waiting' PAs have upgraded to 'success' or 'error' (unless some status callback fails for some reason) - - Scenario: Retry failed payment for 1 PA - Given the payment has failed for a PA - When the user clicks the "Payment #x failed" button for this PA - Then the "Payment History" popup appears - And it shows all payments for this PA - And the failed payment button shows the date and is red - When the user clicks the button a new popup shows - And it contains the error message and a retry-button - Then the user clicks the retry-button - And a normal payment scenario is started for this 1 PA only (see other scenario) - - Scenario: Retry payment for all failed payments of PAs - Given the payment has failed for more than 1 PA - Then the user sees the "Retry all failed" button above the bulk action dropdown - When the user clicks it - Then a popup appears - And it shows the number of PAs for which the payment will be retried - When the user clicks 'OK' - And a normal payment scenario is started for this all the PAs (see other scenario) - - -- "Intersolve-voucher" - - Scenario: Send payment instructions to a Person Affected with Financial Service Provider "Intersolve-voucher" - Given the Person Affected has chosen the option "receive voucher via whatsApp" - When payment instructions are successfully sent (see scenario: Send payment instructions with at least 1 successful transaction) - Then the Person Affected receives a whatsApp message - And it explains the Person Affected to reply 'yes' to receive the voucher - When the Person Affected replies 'yes' (or anything else) - Then the Person Affected receives a voucher image - And it is accompanied by text that explains what is sent - And a separate "explanation" image is sent that explains how to use the voucher in the store (only if instruction-image is uploaded) - And a separate voucher image is sent for any old uncollected vouchers or for any other registrations on the same "whatsappPhoneNumber" - - -- "Intersolve-visa" - - Scenario: Send first payment instructions to a Person Affected with SMS created with registrations import endpoint with Financial Service Provider "Intersolve-visa" - Given the Person Affected has been created directly via registrations import endpoint as registered with Financial Service Provider "Intersolve-visa" - And the PA has correctly filled "firstName", "lastName", "phoneNumber", "addressStreet", "addressHouseNumber", "addressPostalCode", "addressCity" - And the PA has status "included" - When payment instructions are successfully sent (see scenario: Send payment instructions with at least 1 successful transaction) - Then the Person Affected receives 1 notification on SMS via generic send message feature "./Send_message_to_people_affected.feature" - And the notification is about receiving their Visa card - - Scenario: Send first payment instructions to a Person Affected with Whatsapp created registrations import endpoint with Financial Service Provider "Intersolve-visa" - Given the Person Affected has been created directly via registrations import endpoint as registered with Financial Service Provider "Intersolve-visa" - And the PA has correctly filled "firstName", "lastName", "phoneNumber", "whatsappPhoneNumber", "addressStreet", "addressHouseNumber", "addressPostalCode", "addressCity" - And the PA has status "included" - When payment instructions are successfully sent (see scenario: Send payment instructions with at least 1 successful transaction) - Then the Person Affected receives 1 notification on WhatsApp via generic send message feature "./Send_message_to_people_affected.feature" - And the notification is about receiving their Visa card - - Scenario: Send first payment instructions to a Person Affected who changed from Intersolve Financial Service Provider "Intersolve-voucher" to Financial Service Provider "Intersolve-visa" - Given the Person Affected has been updated having Intersolve Financial Service Provider "Intersolve-voucher" to having with Financial Service Provider "Intersolve-visa" - When payment instructions are successfully sent (see scenario: Send payment instructions with at least 1 successful transaction) - Then the Person Affected receives 1 notification on SMS via generic send message feature "./Send_message_to_people_affected.feature" - And the notification is about receiving their Visa card - - - Scenario: Send 2nd or higher payment instructions to a Person Affected with Financial Service Provider "Intersolve-visa" - Given the Person Affected has successfully completed send first payment with Financial Service Provider "Intersolve-visa" - # Set Visa Card to ACTIVE using Intersolve's Swagger UI (https://service-integration.intersolve.nl/pointofsale/swagger/index.html) - And the Visa Debit Card of the Person Affected is "ACTIVE" - When payment instructions are successfully sent (see scenario: Send payment instructions with at least 1 successful transaction) - Then the Person Affected receives 1 notification (WhatsApp or SMS) via generic send message feature "./Send_message_to_people_affected.feature" - And the notification is about the topup of their Visa card - - # TODO: Remove this scenario at some point when we feel it is no longer necessary to test, as it is pretty specificically oriented at a bug which is now solved - Scenario: Send payment instructions in parts to a People Affected with Financial Service Provider "Intersolve-visa" - Given PAs are registered with test-file '121-import-test-registrations-OCW.csv' - And PAs have status "included" - And a payment has been done for part of the PAs already - And after that the payment amount multiplier has been updated for some of those PAs - And the payment is then executed for the rest of the PAs - And it fails for some of those PAs - When then using 'retry all' - Then all PAs who have correct registration data for receiving a payment have been paid the correct amount - And all PAs have received a notification about the top-up their Visa card - - Scenario: Unsuccessfully send payment instructions for Person Affected with inactive card with Financial Service Provider "Intersolve-visa" - Given PAs are registerd with test-file '121-import-test-registrations-OCW.csv' - And 1 PA with correct registration data for payment has status "included" - And this PA has received their first payment for Financial Service Prodiver "Intersolve-visa" - And this PA has not yet activated their card - When executing the next payment for this PA - Then the transaction of this payment for this PA will show as "failed" with an error message that the card is "inactive" - - Scenario: Limit top-up (calculated amount > 0) with Financial Service Provider "Intersolve-visa" - Given 1 PA with correct registration data for payment has status "included" - And the PA has already received a payment with Financial Service Provider "Intersolve-visa" - When executing a payment for a PA with amount 140 euros - Then the service will check what the maximum amount is that can be topped up - And the payment instructions will be successfully sent with either the calculated amount or the maximum amount (whichever is lower) - - Scenario: Limit top-up (calculated amount =< 0) with Financial Service Provider "Intersolve-visa" - Given 1 PA with correct registration data for payment has status "included" - And the PA has already received a payment with Financial Service Provider "Intersolve-visa" - When executing a payment for a PA with amount 140 euros - Then the service will check what the maximum amount is that can be topped up - And no API call will be made to Intersolve - And a succesfull transaction will be created with amount 0 - - Scenario: Successfully retry payment after correcting registration data for PA with Financial Service Provider "Intersolve-visa" - # TODO: Test with other types of missing data? (phone number, lastName, ...) - Given 1 PA with missing "addressCity" and has status "included" - When executing a payment for a PA - Then the payment fails because of a INVALID_PARAMETERS error - When updating the PA with a lastName and retrying the payment - Then the payment shows with status "success" - - Scenario: Send first payment instructions to a Person Affected with incorrect (SMS) phoneNumer with Financial Service Provider "Intersolve-visa" - Given a Person Affected has been registered with Financial Service Provider "Intersolve-visa" - And the PA has correctly filled "firstName", "lastName", "addressStreet", "addressHouseNumber", "addressPostalCode", "addressCity" - And the PA has a non-existing phone number in field "phoneNumber" - And the PA has no "whatsappPhoneNumber" - And the PA has status "included" - When payment instructions are successfully sent (see scenario: Send payment instructions with at least 1 successful transaction) - Then the payment shows as "success" in the Portal - And the notification shows as "failed" in the Portal - - Scenario: Send 2nd or higher payment instructions to a Person Affected with invalid whatsappPhoneNumber with Financial Service Provider "Intersolve-visa" - Given a Person Affected has been registered with Financial Service Provider "Intersolve-visa" - And the PA has correctly filled "firstName", "lastName", "phoneNumber", "addressStreet", "addressHouseNumber", "addressPostalCode", "addressCity" - And the PA has a "whatsappPhoneNumber" without a Whatsapp account on it - And the PA has status "included" - And the PA already received a payment with Financial Service Provider "Intersolve-visa" - When payment instructions are successfully sent (see scenario: Send payment instructions with at least 1 successful transaction) - Then the payment shows as "success" in the Portal - And the notification shows as "failed" in the Portal - - # TODO: Add scenarios for successful retry send payment after unsuccessful send paymen due to: - # 1. 121 Platform sends correct data, but create customer endpoint fails (assumption that Intersolve provides way to test this) - # 2. Create wallet endpoint fails (assumption that Intersolve provides way to test this) - # 3. Link customer to wallet endpoint fails (assumption that Intersolve provides way to test this) - # 4. Create debit card endpoint fails (assumption that Intersolve provides way to test this) - # 5. Load balance endpoint fails (assumption that Intersolve provides way to test this) - - --"Safaricom Kenya" - - Scenario: Successfully make a payment to a Person Affected with Financial Service provider "Safaricom" - Given the Person Affected has been imported as registered - Given all fields have correctly filled ("phoneNumber", "National ID number") - Given age is not under 18 - Given PA requests valid transaction value - When payment is successfully requested - Then a successful transaction appears in the payment column and the payment history popup - - Scenario: Unsuccessfully make a payment to a Person Affected with Financial Service provider "Safaricom" with missing data - Given the Person Affected has been imported as registered and included - Given an obligatory field is missing ("phoneNumber", "National ID number") - Given requested value is not valid - When payment is requested - Then a failed transaction appears for the PA with the missing data - - Scenario: Unsuccessfully make a payment to a Person Affected with Financial Service provider "Safaricom" by making a wrong request - # Trigger this in mock by using phoneNumber=254000000001 - Given the Person Affected has been imported as registered and included - Given the request cannot be processed correctly - When payment is requested - Then a failed transaction appears for the PA with an error message of Invalid Access Token - - Scenario: Unsuccessfully make a payment to a Person Affected with Financial Service provider "Safaricom" by making a successful request, but the PA cannot be paid out - # Trigger this in mock by using phoneNumber=254000000000 - Given the Person Affected has been imported as registered and included - Given the request can be processed correctly, but the PA cannot be paid out - When payment is requested - Then a failed transaction appears for the PA with an error message of 'The phone number does not have M-PESA' - - Scenario: Unsuccessfully make a payment to a Person Affected with Financial Service provider "Safaricom" by making a successful request, but the PA cannot be paid out due to timeout on safaricom side - # Trigger this in mock by using phoneNumber=254000000002 - Given the Person Affected has been imported as registered and included - Given the request can be processed correctly, but the PA cannot be paid out due to timeout on safaricom - When payment is requested - Then a failed transaction appears for the PA with an error message of 'Transfer timed out' - - Scenario: Unsuccessfully make a duplicate payment to a Person Affected with Financial Service provider "Safaricom" because of a unintended Redis job re-attempt - # This is not easily triggered in mock currently, but can be triggered by providing a static originatorConversationId in payments.service.ts and manually retrying a failed transaction - Given the Person Affected has been imported as registered and included - Given a failed transaction has been done already for the PA - When the retry payment is requested - Then a failed transaction appears for the PA with an error message indicating 'Payout with originatorConversationId=abc already exists & processed before.' - - - - --"Commercial Bank of Ethiopia" - - Scenario: Successfully make a payment to a Person Affected with Financial Service provider "Commercial Bank of Ethiopia" - Given the Person Affected has been imported as registered - Given all PAs have correctly filled ("FullName", "Age", "FamilyMembers", "phoneNumber", "National ID number", "Bank Account Number") - Given age is not under 18 - Given PA requests valid transaction value - When payment is successfully requested - Then a successful payment appears in the payment column and the payment history popup - And payment details are displayed with accordion open - - Scenario: Unsuccessfully make a payment to a Person Affected with Financial Service provider "Commercial Bank of Ethiopia" with missing data - - Given the Person Affected has been imported as registered - Given an obligatory field is missing ("FullName", "Age", "FamilyMembers", "phoneNumber", "National ID number", "Bank Account Number") - Given requested value is not valid - When payment is requested - Then a failed payment appears for the PA with the missing data - And error is displayed - - - """ - ------------------------------------------------------------------------------------------------------------------------------------ - - See the 'Send payments instructions diagram' at './wiki/Send-payment-instructions' for more info, oriented at Financial Service Provider: "Intersolve-voucher". - - ------------------------------------------------------------------------------------------------------------------------------------ - - One or multiple registrations with the same payment-address(phone-number) - - 1. There is maximum one registration per payment-address. - - Payment succeeds for all People Affected - - Person Affected receives initial WhatsApp message about receiving one voucher (+ any older uncollected vouchers) - - If replied "_yes_", the Person Affected receives: - * a WhatsApp message about receiving one voucher (incl. the voucher image) - * ... + any older uncollected vouchers (without text) - * ... + one explanation image - - 2. There is 1 rejected and 1 included registration on one payment-address. - - Works exactly as (1) - - 3. There are 2 or more _included_ registrations on one payment-address. - - Payment succeeds - - All Persons Affected receive (on the same phonenumber) the same initial WhatsApp message about receiving one voucher for this week + also any older uncollected vouchers - - If replied 'yes' just once, all Persons Affected receive together: - * a WhatsApp message about receiving multiple vouchers (incl. the first voucher image) - * ... + any additional vouchers for this week (without text) - * ... + any older uncollected vouchers (without text) - * ... + one explanation image per Person Affected (without text) - - 4. There are 2 (or more) included registrations on one payment address at moment of payout. But before "_yes_" reply, 1 (or more) are rejected. - - Works exactly as (3) - - The status at moment of payout is relevant, not the status at the moment of the "_yes_" reply. - - This specifically enables to immediately end inclusion for People Affected after their last payout, without having to wait for their reply. - - """ diff --git a/features/121-Portal/Manage_Intersolve_Visa_card.feature b/features/121-Portal/Manage_Intersolve_Visa_card.feature deleted file mode 100644 index a93e3b5fc7..0000000000 --- a/features/121-Portal/Manage_Intersolve_Visa_card.feature +++ /dev/null @@ -1,88 +0,0 @@ -@portal -Feature: Manage Intersolve Visa card - - Background: - Given a logged-in user with "FspDebitCardREAD" permission - Given a PA with FSP 'Intersolve Visa debit card' - Given the PA has at least 1 Visa debit card (typically through at least 1 payment with this FSP) - Given the user is seeing the debit card section in the PA profile page (see 'View_PA_profile_page.feature') - - Scenario: View Visa debit card details - When clicking one row in the Visa debit card table - Then a popup opens - And it shows the card number in the title - And it shows the card Status again - And it shows the Current balance - And it shows the amount that was spend this month - And it shows when the card was issued - And it shows the last used date - And it shows a button to block/unblock the card depending on the current status of the card - And is has red text and outline in both cases - And it shows a button to issue a new card - - Scenario: Succesfully pause Visa debit card - Given the user has opened the Visa debit card details popup - Given the card is currently not paused - Given the user has the "FspDebitCardBLOCK" permission and thereby the 'pause' button is enabled - When the user clicks on the "Pause card" button - Then the card is paused/blocked with Intersolve and in 121 database - And an automatic message is sent to the PA that the card is paused - And a success alert is shown - And - after closing the alert and subsequent refresh - the card's status in the table is "Paused" - - Scenario: Succesfully unpause Visa debit card - Given the user has opened the Visa debit card details popup - Given the card is currently paused - Given the user has the "FspDebitCardUNBLOCK" permission and thereby the 'unpause' button is enabled - When the user clicks on the "Unpause card" button - Then the card is unpaused/unblocked with Intersolve and in 121 database - And an automatic message is sent to the PA that the card is unpaused - And a success alert is shown - And - after closing the alert and subsequent refresh - the card's status in the table is now no longer "Paused" but back to its previous status - - Scenario: Unsuccesfully pause Visa debit card - Given the user has opened the Visa debit card details popup - Given the user has the "FspDebitCardBLOCK" permission and thereby the 'pause' button is enabled - Given the card is currently not blocked with Intersolve but is somehow marked as blocked in the 121 database - When the user clicks on the "Pause card" button - Then the call to Intersolve fails - And an error alert is shown that the token is already blocked - And the blocked status in the 121 database is updated so the situation is aligned again - - Scenario: Unsuccesfully unpause Visa debit card - >> Similar to "Unsuccesfully pause Visa debit card" - - Scenario: Successfully issue new card - Given the user has opened the Visa debit card details popup - Given the user has the "FspDebitCardCREATE" permission and thereby the 'Issue new card' button is enabled - Given the old wallet is either "Blocked" or "Active" or "Inactive" (so status does not matter) - Given potentially changed personal details (address, phone number) - When the user clicks on the "Issue new card" button - Then an 'are you sure' prompt is shown - When clicking OK - Then Intersolve creates a new wallet, links it to the customer, issues a new card with balance same as the old wallet, unloads balance from the old wallet and blocks the old wallet - And updated address fields and phone number are used - And an automatic message is sent to the PA that a new card has been issued - And a success alert is shown - And an extra card appears in the Debit card table on top - And it has status 'Issued' - And the old card is still there with status 'Blocked' - And any older cards also have status 'Blocked' - And when opening the details of the new card, it shows a balance of 0 as it is still 'Inactive' and after it is activated it shows the balance of the old card - And when opening the details of the old card, it shows a balance of 0 - - Scenario: Unsuccessfully issue new card - When the user clicks on the "Issue new card" button but one of the calls to Intersolve fails for whatever reasons - Then an error alert is shown - And it mentions the step that failed and the specific error message - And the user will be instructed to make adjustments if possible and retry - And if the new wallet was already created, it is removed again in the 121 data - And except if the very last step of blocking the old card, then the new wallet is not removed, and it will appear in the table with status 'Inactive' - And the user will still need to retry >> TO DO: change funtionality? - - Scenario: Successfully retry issue new card - Given the same assumptions as above - Given the previous 'issue new card' attempt failed for whatever reason - Given the user potentially has made adjustments based on the error message - When the user clicks on the "Issue new card" button - Then the retry flow should result in the same as the successful flow above diff --git a/features/121-Portal/Manage_Users.feature b/features/121-Portal/Manage_Users.feature deleted file mode 100644 index 73993dcd6e..0000000000 --- a/features/121-Portal/Manage_Users.feature +++ /dev/null @@ -1,21 +0,0 @@ - @portal - - Background: - Given user is on Users page - - Scenario: View "Users" page - Given user is on Users page - Given Users tab is open - When Details are displayed in table in order (Name, User Type, Status, Last login) - Then under Name user emails are displayed - And possible User Types are Admin and Regular - And status can be Active - And last login is displayed in format (dd/mm/yyyy) or empty - And user clicks on a column users are sorted alphabetically or numerically - And above users table on the left side filtering field is displayed - And on the right side on top of user table "Add new user" button is displayed - - Scenario: View roles permissions tab - When User clicks on "Roles and permissions" tab - Then Admin and Regular roles are displayed - And Under admin "Admin can view and edit data, add new user and create new project" diff --git a/features/121-Portal/Manage_payment_via_import_and_export.feature b/features/121-Portal/Manage_payment_via_import_and_export.feature deleted file mode 100644 index bf11edcabb..0000000000 --- a/features/121-Portal/Manage_payment_via_import_and_export.feature +++ /dev/null @@ -1,50 +0,0 @@ -@portal -Feature: Manage payment via import and export - - Background: - Given an FSP of integration type "csv" - Given at least 1 payment is done for the program - - Scenario: Export payment instructions for FSP without reconciliation - Given an FSP with "hasReconciliation=false" - Given a logged-in user with "PaymentFspInstructionREAD" permissions - When the user selects a "closed" payment from the dropdown-list - Then the "export payment instructions" button is enabled - When the user clicks the "export payment instructions" button - Then an confirm popup opens with a description - And it shows an 'action done last on' timestamp if available - When the user confirms - Then an Excel-file is dowloaded - And it shows a list of the registrations that were "included" for this payment - And "transaction" information where the "amount" is the multiplication of the PA's "paymentAmountMultiplier" and the supplied "transfer value" - And all data as programmed for this FSP - - Scenario: Export payment instructions for FSP with reconciliation - Given an FSP with "hasReconciliation=true" - Given everything the same as in previous scenario - When the user clicks the "export payment instructions" button - Then a difference is that only 'waiting' transactions are included, instead of all - And this is also explained in the export popup - And an Excel file is downloaded - And - if FSP='Excel' - then see [wiki](/~https://github.com/global-121/121-platform/wiki/Excel-payment-instructions-FSP) for details on which columns are exported - - Scenario: Successfully import payment reconciliation data - Given a logged-in user with "PaymentCREATE" permissions - Given a correct input file (CSV for 'Excel') - When the user selects a "closed" payment from the dropdown-list - Then the "import payment reconciliation data" button is enabled (except for the last payment if still in progress) - When the user clicks the button - Then a filepicker popup opens - And it shows an 'action done last on' timestamp if available - When an input-file is chosen and confirmed - Then the file is processed - And an popup appears which confirms the total number of updated 'waiting' transactions - And splits them out in 'to success' and 'to failed' - And it mentions 'not found' records, if any. - And - if FSP='Excel' - then see [wiki](/~https://github.com/global-121/121-platform/wiki/Excel-payment-instructions-FSP) for details on import format - - Scenario: Unsuccessfully import payment reconciliation data - Given an incorrect input-file - When the file is chosen and confirmed - Then a popup appears that 'something went wrong' and it includes a description of the error if possible - diff --git a/features/121-Portal/Manage_team_members.feature b/features/121-Portal/Manage_team_members.feature deleted file mode 100644 index 228b63323b..0000000000 --- a/features/121-Portal/Manage_team_members.feature +++ /dev/null @@ -1,72 +0,0 @@ - @portal - - Background: - Given the user has the "AidWorkerProgramREAD" permission - - Scenario: View "Team Members" - When user is on Team Members page - Then details are displayed in table in order (Name, Role, Status, Scope - if enabled -, Last login) - And possible Roles can be configured per program - And status can be Active - And last login is displayed in format (dd/mm/yyyy) or empty - And each row has a 3 dot icon on the right side - And when user clicks on a sortable column users are sorted alphabetically or numerically - And on the right side on top of user table "Add team member" button is displayed - - Scenario: Add New Team member - Given the user has the "AidWorkerProgramUPDATE" permission - When User clicks on "Add team member" button - Then a pop-up is displayed - When the user enters team members email - And Selects Role for team member by clicking on check boxes - And - if Scope is enabled - fills in the Scope - Then Clicks on "Add" butto - And Notification with "You've succsessfully added a team member" message is displayed - And user clicks on "X" on popup - And Popup is closed - - Scenario: No permission to add New Team member - Given the user does not have the "AidWorkerProgramUPDATE" permission - When user is on Team Members page - Then the user sees a disabled "Add new user" menu option they cannot click - - Scenario: Edit Team members - Given There is a team member on the list - Given the user has the "AidWorkerProgramUPDATE" permission - When User clicks on Three dot icon on the right side of row where team member is displayed - And Meatball menu is displayed - And User clicks on "Edit" - Then pop-up is displayed - And user is not able to edit email - And User changes Role of team member by clicking on checkboxes - And - if Scope is enabled - user updates the Scope - And clicks "Save" button - Then "You've succsessfully edited the role(s) of this user" - And user clicks on "X" on popup - And Popup is closed - - Scenario: Remove team member - Given There is a team member on the list - Given the user has the "AidWorkerProgramUPDATE" permission - When User clicks on Three dot icon on the right side of row where team member is displayed - And Meatball menu is displayed - And User clicks on "Remove user" - And warning message is displayed - And user clicks on "Remove" - Then "You've succsessfully removed this team member" message is displayed - And user clicks on "X" on popup - And Popup is closed - - Scenario: No permission to edit and remove team member - Given the user does not have the "AidWorkerProgramUPDATE" permission - When There is a team member in the list - Then the user sees a disabled 3 dot icon on the right side of the row they cannot click - - Scenario: View error messages - Given user clicks on Add user button - And popup window is displayed - When user that has already been added - Then error message "This user is already a team member." is displayed - And when email is changed to valid open - And user does not assign at least one role - Then error message "Please assign at least one role." is displayed diff --git a/features/121-Portal/Mark_as_no_longer_eligible.feature b/features/121-Portal/Mark_as_no_longer_eligible.feature deleted file mode 100644 index eb233287de..0000000000 --- a/features/121-Portal/Mark_as_no_longer_eligible.feature +++ /dev/null @@ -1,17 +0,0 @@ -@portal -Feature: Mark Person Affected as no longer eligible (extension of View_and_Manage_people_affected.feature) - - Background: - Given a logged-in user with "RegistrationStatusNoLongerEligibleUPDATE" permission - And the "active phase" is "registration & validation" - - Scenario: Use bulk-action "mark as no longer eligible" - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When user selects the "mark as no longer eligible" action - Then the eligible rows are those with status "Imported" and "Invited" - - Scenario: Confirm "mark as no longer eligible" action - Given the generic "confirm apply action" scenario (see View_and_Manage_people_affected.feature) - When the "bulk action" is "mark as no longer eligible" - Then the "changed data" is that the "no longer eligible" timestamp is filled for the selected rows - And the "status" is updated to "No longer eligible" diff --git a/features/121-Portal/Navigate_home_and_main_menu.feature b/features/121-Portal/Navigate_home_and_main_menu.feature deleted file mode 100644 index 34e6a2b86d..0000000000 --- a/features/121-Portal/Navigate_home_and_main_menu.feature +++ /dev/null @@ -1,33 +0,0 @@ -@portal -Feature: Navigate home page and main menu - - Background: - Given a logged-in user - - Scenario: View home screen - When the user visits the Portal URL - Then the "home" page is shown - And how many (assigned) programs are running is shown in the page-header - And a "program card" for all programs that are assigned to this user is shown - - Scenario: Open a program-page - Given the user views the "home" page - When the user clicks on a program-card in the list - Then a program-specific page of the currently active program-phase opens - And the header shows the "portal title" of the program - - Scenario: Go back to home-page - Given the user views a program-specific page - When the user clicks the "home" option from the main-menu - Then the "home" page is shown - And each "program card" is showing the latest status - - Scenario: Go to help-page - Given the user views any page - When the user clicks the "help" option from the main-menu - Then the "help" page is shown - - Scenario: Logout - When the user clicks the "logout" option - Then the user is logged out - And the "login" page is shown diff --git a/features/121-Portal/Navigate_program_menu.feature b/features/121-Portal/Navigate_program_menu.feature deleted file mode 100644 index 749a4bffd0..0000000000 --- a/features/121-Portal/Navigate_program_menu.feature +++ /dev/null @@ -1,69 +0,0 @@ -@portal -Feature: Navigate program menu - - Background: - Given a logged-in user - - Scenario: See dashboard page - Given the user views a "program" page that is not the "Dashboard" page - When the user clicks the "Dashboard" menu option - Then the user sees the "Dashboard" page - - Scenario: See team page - Given the user views a "program" page that is not the "Team" page - Given the user has the "AidWorkerProgramREAD" permission - When the user clicks the "Team" menu option - Then the user sees the "Team" page - - Scenario: No permission to see team page - Given the user does not have the "AidWorkerProgramREAD" permission - When the user views a "program" page that is not the "Team" page - Then the user sees a disabled "Team" menu option they cannot click - - Scenario: See current phase of the program - When the user views a "program" page - Then the user sees a "phase-navigation-bar" with "five" possible phases of the program in the header - And sees the "selected phase" highlighted - And sees that future phases are disabled - And sees that past phases are enabled - - Scenario: View past phase - Given the user views a "program" page - When the user clicks one of the past phases in the "phase-navigation-bar" - Then the color of the selected phase changes and reflects the "selected phase" - And sees that the "move-to-next-phase"-button is disabled, reflecting the read-only mode of past phases. - And sees - depending on which state - that certain program-components in the page will (dis)appear or will be dis/enabled. - - Scenario: Option to advance to next phase - Given the user has the "ProgramPhaseUPDATE" permission - Given the "current program phase" is the "selected phase" - When the user views a "program" page - Then the "move-to-next-phase" button is shown - And the "move-to-next-phase" button is enabled - - Scenario: Option to advance to next phase not available - Given the user does not have the "ProgramPhaseUPDATE" permission - When the user views a "program" page - Then the "move-to-next-phase" button is not shown - - Scenario: Advancing to next phase - Given the user has the "ProgramPhaseUPDATE" permission - Given user views a "program" page - Given "selected phase" is equal to "current program phase" - When user clicks the "move-to-next-phase"-button - Then the program page corresponding to the new "program phase" is shown as the "selected phase" - And sees - depending on which state - that certain program-components in the page will (dis)appear. - - Scenario: Opening a program for registration ("publishing") - Given user views a "program" page - Given the "current program phase" is "Design" - Given "selected phase" is "Design" - When user clicks the "Open for registration"-button - Then the program will advance to the next phase (see scenario: "Advancing to next phase") - - Scenario: Open/Close a program for registration ("publishing"/"un-publishing") - Given user views a "program" page - Given the "current program phase" is "Registration" - Given "selected phase" is "Registration" - When user clicks the "Allow new registrations"-toggle button - Then the program will update to the "published/unpublished" state diff --git a/features/121-Portal/Navigate_program_phases.feature b/features/121-Portal/Navigate_program_phases.feature deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/121-Portal/Reject_or_End_inclusion_people_affected.feature b/features/121-Portal/Reject_or_End_inclusion_people_affected.feature deleted file mode 100644 index 221b9a8f43..0000000000 --- a/features/121-Portal/Reject_or_End_inclusion_people_affected.feature +++ /dev/null @@ -1,64 +0,0 @@ -@portal -Feature: Reject or end inclusion of people affected (extension of View_and_Manage_people_affected.feature) - - Background: - Given a logged-in user with "RegistrationStatusRejectedUPDATE" permission - Given the "selected phase" is the "inclusion" or "payment" phase - - Scenario: View "reject from program" action - When opening the "action dropdown" - Then the "reject from program" action is visible - - Scenario: Use bulk-action "reject from program" - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When user selects the "reject from program" action - Then the eligible rows are those with status "included", "registered", "selected for validation", "validated", "no longer eligible" and "registered while no longer eligible" - - Scenario: Confirm "reject from program" action - Given the generic "confirm apply action" scenario (see View_and_Manage_people_affected.feature) - When the "bulk action" is "reject from program" - Then the PA is from now on only viewable in the "inclusion" page - And "rejected" timestamp is filled for the selected rows - And the "included" column remains filled - And the "status" is updated to "Rejected" - And if a templated message is present or if the custom SMS option is used, an SMS is sent to the PA (see View_and_Manage_people_affected.feature) - - ------------------------------------------ - - Background: - Given a logged-in user with "RegistrationStatusInclusionEndedUPDATE" permission - Given the "selected phase" is the "payment" phase - - Scenario: View "end inclusion in program" action - When opening the "action dropdown" - Then the "end inclusion in program" action is visible - - Scenario: Use bulk-action "end inclusion in program" - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When user selects the "end inclusion in program" action - Then the eligible rows are those with status "included" - - Scenario: Confirm "end inclusion in program" action - Given the generic "confirm apply action" scenario (see View_and_Manage_people_affected.feature) - When the "bulk action" is "end inclusion in program" - Then the PA is from now on only seeable in the "inclusion" page - And the "end inclusion" timestamp is filled for the selected rows - And the "included" column remains filled - And the "status" is updated to "Inclusion ended" - And if a templated message is present or if the custom SMS option is used, an SMS is sent to the PA (see View_and_Manage_people_affected.feature) - - ------------------------------------------ - - Scenario: Reject or End inclusion for 100000 PAs - Given there are 100000 PAs in the system - And they are included (see Include_people_affected_Run_Program_role.feature) - When the user uses and confirms the "reject from program" or "end inclusion in program" action on all 100000 PAs - Then this is all processed as in the scenarios above, quickly and without problem - - -------------------------------------------- - - Scenario: Identify People Affected to Reject based on "Registered while no longer eligible" - Given there are People Affected which are marked as no longer eligible - And some of these have completed a registration - When the user searches in the PA-table for status "Registered while no longer eligible" (either through filtering or sorting + scrolling) - Then these people can subsequently be selected for rejection (see scenarios: 'Use bulk-action "reject from program"' and 'Confirm "reject from program" action') diff --git a/features/121-Portal/Send_message_to_people_affected.feature b/features/121-Portal/Send_message_to_people_affected.feature deleted file mode 100644 index f390e3b76d..0000000000 --- a/features/121-Portal/Send_message_to_people_affected.feature +++ /dev/null @@ -1,43 +0,0 @@ -@portal -Feature: Send message to people affected (extension of View_and_Manage_people_affected.feature) - - Background: - Given a logged-in user with "RegistrationREAD" permission - And the "selected phase" is the "registrationValidation" or "inclusion" or "payment" phase - - Scenario: Use bulk-action "Send message to PAs" - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When user selects the "Send message to PAs" action - Then the eligible rows are only those with a phone number - - Scenario: Configure a custom message in popoup - Given the generic "select bulk action" scenario (see View_and_Manage_people_affected.feature) - When user selects the "Send message to PAs" action - Then the popup shows an empty message - And the user can edit the message - And the user can add placeholders for (Program question, Program attributes, Payment multiplier, Fsp diplayname and Max payments) - And the placeholders are replaced by the preview values of the first PA in the popup - - Scenario: Confirm "Send message to PAs" action - Given the generic "confirm apply action" scenario (see View_and_Manage_people_affected.feature) - When the "bulk action" is "Send message to PAs" - Then a message is sent to the selected rows - And if "whatsappPhoneNumber" is known then the generic Whatsapp template is sent and otherwise dirctly the actual message via SMS - And the latest message columm shows this message type and also the latest status (In twilio-mock: READ for whatsapp and SENT for SMS) - And if placeholders were used then they are replaced by the actual values of the PA - And if the WhatsApp is being replied to (in twilio-mock automatic) then also the follow-up WhatsApp message is sent - - # You can use 15005550001 in combination with Twilio Mock to test a failed message - Scenario: Unsuccessful "Send message to PAs" - Given the generic "confirm apply action" scenario (see View_and_Manage_people_affected.feature) - And the message somehow cannot arrive successfully - When the "bulk action" is "Send message to PAs" - Then no message is sent (WhatsApp or SMS) - And the "Messages" column shows "UNDELIVERED or FAILED" as status - - Scenario: Send message only to PAs with last message 'failed' - Given the "Send message to PAs" bulk action has been used - And this action has failed for one or more PAs - When the filter "Last message" is used with the value "failed" - Then only the PAs for which the last message failed, appear in the PA table - And they can be selected for another "Send message to PAs" bulk action diff --git a/features/121-Portal/View_PA_profile_page.feature b/features/121-Portal/View_PA_profile_page.feature deleted file mode 100644 index baaf5a1e0e..0000000000 --- a/features/121-Portal/View_PA_profile_page.feature +++ /dev/null @@ -1,93 +0,0 @@ -@portal -Feature: View PA profile page - - Background: - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user is viewing the PA table - Given 1 or more PAs with at least status "created" - Given the user sees the PA number - - Scenario: Open profile page - When the user clicks on the PA number - Then the profile page for that PA is opened - - Scenario: View Personal information table - Given the user has opened the PA profile page - Then the user sees the "Personal information" table - And it mentions the name of the PA - And it shows the current status of the PA and the date the status was changed - And it shows the PA's primary language, phone number and FSP - And if there are program attributes, it shows the WhatsApp phone number and the Partner Organization - And - if program has scope - then it shows PAs scope - - Scenario: Open the edit PA popup - Given the user sees the "Personal Information" table - Given the user sees the "Show All" button - When the user clicks on the "Show All" button - Then the edit PA popup opens - - Scenario: View Visa debit cards table - Given the PA has FSP 'Intersolve Visa debit card' - Given the PA has at least 1 Visa debit card (typically through at least 1 payment with this FSP) - Given the user has the "FspDebitCardREAD" permission - When the user opens the PA profile page - Then the user sees the "Visa debit cards" table on the left below the "Personal information" table - And it shows a row for each Visa debit card - And this should contain multiple rows only if a PA has a reissued card for some reason - And it shows per card the card number and the status of the card - And older cards should have status "Blocked" - And the 1 current card can have status "Inactive", "Active" or "Blocked" - >> See 'Manage_Intersolve_Visa_card.feature' for more scenarios related to this table - - Scenario: View Activity overview - Given the user sees the "Activity overview" table - And the user sees the "All" tab and the list of all available updates in the table - Given a logged-in user with "RegistrationPersonalREAD" permission - Then the user sees the "Status history" tab - When the user clicks on the "Status history" tab - Then the user sees the list of status changes in the table - Given a logged-in user with "RegistrationPersonalREAD" permission - Then the user sees the "Data Changes" tab - When the user clicks on the "Data Changes" tab - Then the user sees the list of data and FSP changes in the table - And this information includes the date and time of the change, the user who made the change, the field that was changed, the old value, the new value and the reason - Given a logged-in user with "RegistrationNotificationREAD" permission - Then the user sees the "Messages" tab - When the user clicks on the "Messages" tab - Then the user sees the list of sent messages in the table - Given a logged-in user with "PaymentREAD" and "PaymentTransactionREAD" permissions - Then the user sees the "Payments" tab - When the user clicks on the "Payments" tab - Then the user sees the the list of payments in the table - And all the tabs have a count of updates next to them - - Scenario: Successfully add note - Given a logged-in user with "RegistrationPersonalUPDATE" permission - Given the user sees the "Activity overview" table - And the user sees the "Actions" button in the top right corner - When the user clicks on the "Actions" button - Then the user sees the "Add note" option - When the user clicks on the "Add note" option - Then the user sees the "Add note" popup - When the user types some text in the 'Type note:' field - Then the user sees the 'OK' button enable - When the user clicks on the 'OK' button - Then the user sees a feedback message that the note was added successfully - And the page refreshes - And the user sees the updated note in the "Activity overview" table - - Scenario: Unsuccessfully update note - Given the same assumptions as in the 'successfully add note' scenario - Given something goes wrong for some reason (which cannot be simulated by the tester) - When the user follows the same steps as in the 'successfully add note' scenario and clicks on the 'OK' button - Then a feedback message that something went wrong is given - And it gives the basic error type if possible, e.g. "Bad Request" - - Scenario: Successfully view note(s) - Given a logged-in user with "RegistrationPersonalREAD" permission - Given the user sees the "Activity overview" table - Given the user sees the "All" tab and the list of all available updates in the table - Given the user sees the "Notes" tab - When the user clicks on the "Notes" tab - Then the user sees the list of notes in the table - And this information includes the date and time of the note and the user who made the note diff --git a/features/121-Portal/View_and_Manage_people_affected.feature b/features/121-Portal/View_and_Manage_people_affected.feature deleted file mode 100644 index be79c8b60e..0000000000 --- a/features/121-Portal/View_and_Manage_people_affected.feature +++ /dev/null @@ -1,222 +0,0 @@ -@portal -Feature: View and manage people affected (generic features) - - Background: - Given a logged-in user with "RegistrationREAD" permission - Given a chosen "program" - - Scenario: View People Affected table - When the user views a page with the "PA table" - Then a table with all PAs of that program is shown - And - if the program and user have a scope - then only PAs within that scope are shown - And depending on the "selected phase" only current people affected with given "PA statuses" are shown (see Scenario: Filter rows of PA-table by People Affected status) - And for each person the "Select" column is empty - And for each person a "PA identifier" is shown and this is an auto-increment number per registration starting from 1 per program - And it has a clickable "i" button in front of it, which opens a popup - And depending on the "selected phase" other columns are shown (see detailed scenarios below) - And above the table a list of "bulk actions" is shown - And next to it an "apply action" button is shown and it is "disabled" - And above the table a free text "filter field" and a dropdown for "status filter" are shown - - Scenario: View columns of table WITHOUT access to personal data - When the user views the PA-table - Then the users sees the columns mentioned in the previous scenario - And for each person a "status" is shown - And all above columns are fixed when scrolling horizontally - And "transfer value" column is shown - And "inclusion score" column is shown (if "validation" is configured for the program) - And "financial service provider" column is shown - And "payment history" is shown (in "payment" page only) - And "last message" is shown and it shows the channel (SMS/Whatsapp) and status of the last message sent to the PA - - Scenario: View Message History - When the user click the "last message" column - Then a popup appears with the message history of the PA - And it shows per message the type, the channel, the status, the date, the message text, the error code (if applicable) - - Scenario: View payment history column and popup - >> See View_payment_history_popup.feature - - Scenario: View columns of table WITH access to personal data - Given the logged-in user also has "RegistrationPersonalREAD" permission - When the user views the PA-table - Then the user sees all columns available in previous scenario - And the "i" button in front of the "PA identifier" - And for each person the columns that make up the "name" are shown - And for each person a "phone number" is shown - And all above columns are fixed when scrolling horizontally - And "custom attribute" columns are shown if configured to be showing for that phase - And "program questions" are shown if configured to be showing for that phase - And "fsp questions" are shown if configured to be showing for that phase - - Scenario: Edit boolean custom attributes in PA table - Given the logged-in user also has "RegistrationAttributeUPDATE" permission - When the user clicks one of the "custom attribute" columns with type 'boolean' - Then the clicked "custom attribute" is updated - And a popup with 'update successful' appears - - Scenario: Filter rows of PA-table by People Affected status - Given the table with all "people affected" relevant to the selected program phase is shown - When the user clicks on the "status" button above the table - Then a dropdown with all the possible statuses appears - And only the statuses relevant to the current phase are selected - - "registration": imported, invited, created, registered, selected for validation, no longer eligible, registered while no longer eligible - - "inclusion": registered, selected for validation, validated, rejected, inclusion ended - - "payment": included - When the user makes a different selection of statuses - And the user clicks on "apply" - Then the table immediately updates to show only rows that match the status selection - When the user clicks on "cancel" - Then the table keeps the row that matched the previous status selection - - --- Filter PA-table START --- - - Scenario: Filter rows of PA-table by one filterable column - Given the table with all "people affected" relevant to the selected program phase is shown - When the user clicks on the "select column" filter dropdown above the table - Then a dropdown with all the filterable columns appears - And the dropdown contains a search bar to search the columns to filter - When the user makes a selection - Then a text field appears on the right of the dropdown - And if its a "numeric" field then only numeric characters are allowed, otherwise all is allowed - When the user writes the value of the filter - Then the "apply filter" button is enabled - When the user clicks on "apply filter" or presses the "enter" button on the keyboard - Then the value is used on the selected column to filter the table - And a text showing the number of "fitered recipients" appears below the dropdown - And a chip showing the selected column and the filter value appears next to it on the right - And on the right side of the page the "clear all filters" button appears - And the dropdown is reset to an empty selection - And the search bar and the "apply filter" button disappear - - Scenario: Filter rows of PA-table by multiple filterable columns - Given the table has been filtered for one column already - When the user selects another column and applies the filter - Then the already filtered table is additionally filtered by this second filter - And the "filtered recipients" count updates according to the two filters applied - And a second chip showing column and value appears next to the previous one - And the filter row above is reset again - - Scenario: Remove one column filter from the PA-table - Given there is at least one column filter applied to the table - When the user clicks on the "x" inside one of the applied filter chips - Then the filter is removed from the table - And the chip disappears - And if there is still one column filter applied, the "filtered recipients" count is updated - - Scenario: Remove all column filters from the PA-table - Given there is at least one column filter applied to the table - When the user clicks "x" on the last chip remaining or clicks on "clear all filters" - Then all filters are removed from the table - And all chips disappear - And the "filtered recipient" count disappears - - --- Filter PA-table END --- - - Scenario: Show People Affected of all phases - Given the table with all "people affected" relevant to the selected program phase is shown - And the user has opened the "status" dropdown filter - And an option to select "all" is on top of the list - And the option is not checked - When the user selects "all" - Then all the statuses are selected - When the user clicks on "apply" - Then the table will now show all "people affected", also those from other phases - And - if done while the filter text field contains text - then the filtered text keeps being applied - When the user deselect "all" - Then the table will not show any "people affected" - - Scenario: View available actions - When the user opens up the "choose action" dropdown - Then a list appears with available "bulk actions" - And it is dependent on permissions of the currently logged-in "user" - And it is dependent on "selected phase" of the program - - registration: invite / mark as no longer eligible / select for validation / send message / delete PA - - inclusion: include / reject / send message / delete PA - - payment: include / reject / end inclusion / pause / send message / do payment - - Scenario: Select "bulk action" while rows eligible - Given at least 1 person is eligible for the "bulk action" - When the user selects a "bulk action" - Then the dropdown now shows the name of the "bulk action" instead of "choose action" - And the "apply action" button is "enabled" - And a "row checkbox" appears in the "select" column for eligible rows - And a "header checkbox" appears in the "select" column - - Scenario: Select "bulk action" while no rows eligible - Given no people are eligible for the "bulk action" - When the user selects a "bulk action" from the dropdown - And there are no visible checkboxes in the table - And the user selects the "selet-all" checkbox - Then a popup with the message "no people are eligible" is shown - And the dropdown is reset to the default "choose action" option - - Scenario: Sort people enrolled in a program by property - When the user clicks a column-header - Then the rows show in "ascending or descending" order - - Scenario: Select a row - Given a "bulk action" is selected - And "row checkboxes" have appeared for eligible rows - And all "row checkboxes" are unchecked - When the "user" clicks on the checkbox - Then the 'Apply action' button becomes enabled - - Scenario: Select all rows given no row selection - Given a "bulk action" is selected - And the "header checkbox" has appeared - And the "header checkbox" is unchecked - When the "user" checks the "header checkbox" - Then all "row checkboxes" are selected and the "header checkbox" is selected - - Scenario: Deselect all rows given full selection - Given current "full" selection - When unchecking "header checkbox" - Then all "row checkboxes" are unchecked - - Scenario: Select all rows given partial selection - Given 1 "row checkbox" is selected - When user checks "header checkbox" - Then all "row checkboxes" are checked - - Scenario: Apply action without message-option - Given a "bulk action" is selected - And 0 or more rows are selected - And there is no custom-SMS option for this action - When "user" clicks "apply action" - Then a popup appears which lists which "bulk action" will be applied to "how many" people affected - And it has a "confirm" button and a "cancel" button - - Scenario: Apply action with message-option - Given a "bulk action" is selected - And 0 or more rows are selected - And there is a custom-SMS option for this action - When "user" clicks "apply action" - Then a popup appears which lists which "bulk action" will be applied to "how many" people affected - And it has a checkbox that allows to choose whether to send a Custom SMS with this action - And it is checked by default or not based on the action - - default yes for: invite, reject, end inclusion, send message - - default no for: include - - SMS not an option for: select for validation, mark as no longer eligible, delete PA - And - if checked by default or manually - it shows a templated message if one is present otherwise it shows a free text field to enter the message - And it shows a character counter - And it has a "confirm" button, which is disabled if checkbox is checked AND the entered text is below 20 characters - And it has a "cancel" button - - Scenario: Confirm apply action - Given the confirm popup has appeared - When "user" clicks confirm - Then the popup disappears - And the "changed data" is reflected in the table - And the "action" dropdown is reset - And the "apply action" is "disabled" again - And all "row checkboxes" and "header checkbox" disappear - And - if the action has an SMS-action and it is used - an SMS is sent to the PA - - Scenario: View and Filter PA-table with 100000 PAs - Given there are 100000 PAs in the system - When the user clicks through the PA-table - Then this goes quickly and without problem - When the user uses the text or status filter functions - Then the PA-table updates to only filtered rows quickly and without problem - diff --git a/features/121-Portal/View_dashboard_page.feature b/features/121-Portal/View_dashboard_page.feature deleted file mode 100644 index c81f7de720..0000000000 --- a/features/121-Portal/View_dashboard_page.feature +++ /dev/null @@ -1,69 +0,0 @@ -@portal -Feature: View dashboard page - - Background: - Given a logged-in user with the "ProgramMetricsREAD" permissions - Given the user views the "dashboard"-page for a given "program" - Given the "program" is not configured with a "monitoringDashboardUrl" - Given the "program" has actual data and not generated mock data - - Scenario: View PA-status metrics table successfully - When the user views the "dashboard"-page - Then the PA-status metrics table is showing on the top right - And a date for "Last updated" is shown with a refresh button - And the "most recent payment" is selected in the 'Payment #' row - if a payment has been done - And the "most recent calendar month" is selected in the 'Calendar month' row - And there is a column for each possible PA status, including "deleted" - And for all rows and columns in the table an info-icon is shown - And for all rows in the table numbers are shown reflecting the chosen program - - Scenario: View PA-status metrics table with SCOPE - Given the program has scope enabled - Given the user has a scope for the given program - Given only part of the users have the right scope - When the user views the table - Then it only shows numbers reflecting the PAs within the scope of the user - - Scenario: View PA-status metrics for specific payment - When the user clicks the "Choose payment"-list - Then a list of all past payments with their dates is shown - When the user makes a selection from the list - Then metrics are requested from the API for that "payment" - And the most recent values for all metrics are shown - And a new date for "Last updated" is shown - - Scenario: View PA-status metrics for specific month - Given the user views the "dashboard"-page - When the user clicks the "Choose month"-list - Then a list of all past months where payments where done - When the user makes a selection from the list - Then metrics are requested from the API for that "year" + "month" - And the most recent values for all metrics are shown - And a new date for "Last updated" is shown - - Scenario: Export PA-status metrics as a CSV file - Given the user views the "dashboard"-page - When the user clicks the "export as csv"-button - Then a CSV file is downloaded - And it contains the translated headers and the data shown in the page - - Scenario: View PAs helped to date - When the user views the "dashboard" page - Then total number of PA's helped is shown on the right below the "PA-status metrics table" - And it shows the total number of PA's helped to date for that program - And it also includes PAs with only failed and waiting transactions - And it also still includes "deleted" PAs - And a date for "Last updated" is shown with a refresh button - And an info icon is shown - And - if program and user have scope - then only the PAs within the scope of the user are counted - - Scenario: Refresh any element of the dashboard page - Given the user has opened the "dashboard page" before - When the user clicks the "update"-button on an element - Then the most recent values for all metrics are shown - And a new date for "Last updated" is shown - - - - - diff --git a/features/121-Portal/View_last_payment_overview.feature b/features/121-Portal/View_last_payment_overview.feature deleted file mode 100644 index a28913271e..0000000000 --- a/features/121-Portal/View_last_payment_overview.feature +++ /dev/null @@ -1,21 +0,0 @@ -@portal -Feature: View last payment overview - - Background: - Given a logged-in user with the "PaymentREAD" and "TransactionREAD" permissions - - Scenario: View last payment overview when a payment is done - Given at least 1 payment has been done - When the user views the "payment" page - Then right above the "People affected table" an overview of the last payment is shown - And it mentions "Last payment: #" - And it shows the amount of successful, waiting, and failed payments - And - if program and user have scope - then these numbers are filtered to only those PAs within the scope of the user - And also if no PAs fall within the scope, then the overview still shows with 0 for all numbers - And it shows a "Retry all" button only if there are failed payments and the user also has the "PaymentCREATE" permission - >> For using "Retry all": see "Retry payment for all failed payments of PAs" in "Make_new_payment.feature" - - Scenario: Do not see last payment overview when no payment is done yet - Given no payment has been done - When the user views the "payment" page - Then no overview is shown \ No newline at end of file diff --git a/features/121-Portal/View_messages.feature b/features/121-Portal/View_messages.feature deleted file mode 100644 index bc04bfaecf..0000000000 --- a/features/121-Portal/View_messages.feature +++ /dev/null @@ -1,43 +0,0 @@ -@portal -Feature: View Messages - - Background: - Given a logged-in user with "RegistrationPersonalREAD" permission - - Scenario: View Last message column - Given the user is on the "registration", "inclusion" or "payment" page - Given at least 1 PA is shown in the PA Table with at least 1 "message" - When the user views the "Last message" column - Then it shows a button with the "message type" and "message status" of the latest "message" - - Scenario: View messages from message history pop-up - Given the user is on the "registration", "inclusion" or "payment" page - Given at least 1 PA is shown in the PA Table with at least 1 "message" - When the user clicks the button in the "Last message" column - Then it shows a pop-up with a list of all "Messages" for this PA - - Scenario: View messages from Activity overview - Given the user has opened the PA profile page - Given the PA has received at least 1 message - When the user has selected the "All" or "Messages" tab - Then the user sees a list with "Messages" - - Scenario: View message - Given the user has opened the Acivity overview on the PA profile page or the message history pop-up - Given the PA has received at least 1 message - When the user sees a card with "Message" - Then the user sees the "Mail icon" of "Message" - And the user sees the "Template Type" of "Message" - And the user sees if "Message" was sent via "SMS" or "WhatsApp" - And the user sees a green "Status" badge of "Sent", "Delivered", or "Read" for "Message" - And the user sees the "Date" of "Message" - And the user sees the "Text" of the "Message" - - Scenario: View failed message - Given the user has opened the Acivity overview on the PA profile page or the message history pop-up - Given the PA was sent at least 1 "Message" that failed - When the user sees a card with "Message" - Then the user sees a red "Status" badge of "Failed" for "Message" - And the user sees the "Date" of "Message" - And the user sees the "Text" of the "Message" - And the user sees the text "Twilio error code" with a hyperlink to the Twilio error code diff --git a/features/121-Portal/View_payment_history_popup.feature b/features/121-Portal/View_payment_history_popup.feature deleted file mode 100644 index 228bbff270..0000000000 --- a/features/121-Portal/View_payment_history_popup.feature +++ /dev/null @@ -1,57 +0,0 @@ -@portal -Feature: View payment history column and popup - - Background: - Given a logged-in user with "RegistrationREAD" permission - Given the user is on the "payment" page - Given 1 or more PAs with at least status "included" - - Scenario: View payment history column - When the user views the "payment history" column - Then it shows a button which says 'Payments' - And the button has purple text and light grey outline - - Scenario: View payment history popup for a PA - Given a payment is done for the PA - When the user clicks on the button in the payment history column - Then the payment history popup opens - And it mentions the name of the PA - And below it a row for each payment that is done for that PA or for which a single payment is possible for that PA - And each row starts with a money icon - And then Payment #X is mentioned - And - for payments that are done for the PA - on the right side of the payment number the distribution date is displayed in format DD-MM-YYYY, hh:mm - And under the payment number it mentions the status Successful/Waiting/Failed - And the status text and outline is green if Successful - And the status text and outline is yellow if Waiting - And the status text and outline is red if Failed - And - for payments for which a single payment is possible - it mentions 'Not yet sent' in yellow - And if the FSP has voucher support and the status is 'Success' then an 'Open voucher' button is displayed - And if status is 'Failed' or 'Waiting' then a 'Details' button is displayed - And if payment is 'Not yet sent' then a 'Send payment' button is displayed - And the user is able to open an accordion for each payment - And when the user opens the accordion payment details are displayed in two columns - And first column details contains the "sent datetime" and "amount" - And second column details contains the "FSP" at time of payment - And it contains any custom FSP-specific attributes (currently only card ID if FSP is Visa) - And if any of the attributes are not completely visible, hovering over it will show the full value - And User is able to close accordion by clicking on the "^" button - And popup is closed when user clicks on "X" button - - Scenario: Do single payment - Given the user has opened the payment history popup - Given a single payment is possible for a payment - - PA = included - - payment has been done for at least 1 other PA - - payment has not been done for this PA - - only for last 5 payments - When the user clicks the 'Send payment' button - Then the 'do single payment' popup appears - And it contains an editable transfer amount field - And it contains a 'start payout now' button - - When the user clicks 'start payout now' - Then an 'Are you sure?' popup appears - And it contains the total amount to be paid out - - When the user confirms - Then a payment is executed for this PA only (identical to payments done in: Make_new_payment.feature) diff --git a/features/Admin-user/Add_And_Update_program_custom_attribute.feature b/features/Admin-user/Add_And_Update_program_custom_attribute.feature deleted file mode 100644 index b6c93e663a..0000000000 --- a/features/Admin-user/Add_And_Update_program_custom_attribute.feature +++ /dev/null @@ -1,21 +0,0 @@ -@swagger-ui -Feature: Add and Update Program Custom Attribute - - Background: - Given a logged-in "admin" user in the Swagger UI - - Scenario: Successfully add custom attributes of program - Given the "programId" is provided - Given the required properties "name", "type", "label" (and unrequired "phases") for this "Custom Attribute" have been provided - Given the provided combination of "name" and "programId" does not exist in the database - When the user fills in the body properties - And calls the "POST /programs/{programId}/custom-attributes" endpoint - Then a "Custom Attribute" with all its attributes is created and returned as response - - Scenario: Successfully update custom attributes of program - Given the "programId" and "attributeId" are provided - Given the provided combination of "name" and "programId" does not already exist in the database - When the user fills in the "name" and a subset of other attributes "type", "label" and "phases" - And calls the "PATCH /programs/{programId}/custom-attributes/{attributeId}" endpoint - Then the existing "Custom Attribute" is updated only with the provided values - And the response only contains the updated properties diff --git a/features/Admin-user/Export_vouchers_to_cancel.feature b/features/Admin-user/Export_vouchers_to_cancel.feature deleted file mode 100644 index c608d12133..0000000000 --- a/features/Admin-user/Export_vouchers_to_cancel.feature +++ /dev/null @@ -1,12 +0,0 @@ -@admin -Feature: Export vouchers to cancel (see WORKFLOWS.md for background) - - Background: - Given 1 or more programs with "Intersolve-voucher-whatsapp" FSP - Given a logged-in "admin" user on Swagger - - Scenario: Exporting vouchers to cancel - When the user calls the '/metrics/to-cancel-vouchers' endpoint - Then it returns an array - And it contains vouchers to cancel for all programs in that instance - diff --git a/features/Admin-user/Load_seed_data.feature b/features/Admin-user/Load_seed_data.feature deleted file mode 100644 index 13c955edb6..0000000000 --- a/features/Admin-user/Load_seed_data.feature +++ /dev/null @@ -1,22 +0,0 @@ -@swagger-ui -Feature: Load seed data - - Background: - Given a logged-in "admin" user in the Swagger UI - - Scenario: Reset instance and program data sucessfully - Given the "secret" is provided - Given the "script" is provided - When the user calls the "/api/scripts/reset" endpoint - Then status code 202 is returned - And 1 or mor programs show in the Portal, depending on the chosen "script" - - Scenario: Reset instance and program data with mock registration data sucessfully - Given the "secret" is provided - Given the provided "script" is "nlrc-multiple-mock-data" - Given "mockPv" or "mockOcw" or both are true - Given positive numbers are provided for "mockPowerNumberRegistrations", "mockNumberPayments" and "mockPowerNumberMessages" - When the user calls the "/api/scripts/reset" endpoint - Then status code 202 is returned - And both programs show in the Portal - And registrations, payments and messages show in the Portal \ No newline at end of file diff --git a/features/Admin-user/Manage_Roles.feature b/features/Admin-user/Manage_Roles.feature deleted file mode 100644 index 0e6bda3572..0000000000 --- a/features/Admin-user/Manage_Roles.feature +++ /dev/null @@ -1,48 +0,0 @@ -@swagger-ui -Feature: View, Add and Edit roles - - Background: - Given a logged-in "admin" user in the Swagger UI - - Scenario: View all roles with permissions in current program - When the user calls the GET "/roles" endpoint - Then a list of roles with correspondig permissions is returned - - Scenario: Successfully add role - Given a "role" that does not exist yet - Given the new "Role" is provided - Given the required attributes "label" and "permissions" for this "Role" have been provided - When the user fills in the "role" and the "label" and the "permissions" in as body - And calls the POST "/roles" endpoint - Then a "Role" is created and returend as response - - Scenario: Unsuccessfully try to add role that already exists - Given a "role" that already exists - Given the new "Role" is provided - Given the required "attributes" for this "Role" have been provided - When the user fills in the "role" and the "label" and the "permissions" in as body - And calls the POST "/roles" endpoint - Then a status 400 is returned with a message that "Role exists already" - - Scenario: Successfully update role - Given an existing roleId - Given the required "attributes" for this "Role" have been provided - When the user fills in the "label" and the "permissions" in as body - And calls the PUT "/roles/:roleId" endpoint - Then a "Role" with all its attributes is returned - - Scenario: Unsuccessfully try to update unknown role - Given an unkonwn roleId - When the user fills in the "label" and the "permissions" in as body - And calls the PUT "/roles/:roleId" endpoint - Then a 404 response is returned that the role is not found - - Scenario: Successfully delete role - Given the "Role" id is provided - When the user calls the DELETE "/roles/:roleId" endpoint - Then a success response (200) is returned - - Scenario: Unsuccessfully try to delete unknown role - Given an unkonwn roleId - And calls the DELETE "/roles/:roleId" endpoint - Then a 404 response is returned that the role is not found diff --git a/features/Admin-user/Sync_Intersolve_Visa_Customer.feature b/features/Admin-user/Sync_Intersolve_Visa_Customer.feature deleted file mode 100644 index 6620b25467..0000000000 --- a/features/Admin-user/Sync_Intersolve_Visa_Customer.feature +++ /dev/null @@ -1,14 +0,0 @@ -@swagger-ui -Feature: Synchronize Intersolve Visa customer data to 121 data - - Background: - Given a logged-in "admin" user in the Swagger UI - Given a PA with FSP "Intersolve Visa" - - Scenario: Successfully synchronize Intersolve customer data with 121 data - Given the "programId" is provided - And the "referenceId" is provided - And potentially phone number and/or address fields are updated in the 121-Platform (or not, this endpoint just syncs) - When the user calls the "/api/programs/{programId}/financial-service-providers/intersolve-visa/customers/{referenceId}" endpoint - Then the phone number and the address fields of the Intersolve Customer are updated at Intersolve to be the same as in the 121 registration - And - if non-mock - this could be checked by accessing the Intersolve customer API directly (integration environment only) diff --git a/features/Admin-user/Update_program.feature b/features/Admin-user/Update_program.feature deleted file mode 100644 index 9fe3356c9a..0000000000 --- a/features/Admin-user/Update_program.feature +++ /dev/null @@ -1,26 +0,0 @@ -@swagger-ui -Feature: Update Program - - Background: - Given a logged-in "admin" user in the Swagger UI - - Scenario: Successfully update properties of program - Given the "programId" is provided - When user calls the "PATCH /programs/{programId}" endpoint - And 1 or more other program attributes are provided - Then only the provided properties of the "program" are updated - And the whole "program" object is returned - - Scenario: Successfully add Financial Service Provider to a program - Given the "programId" is provided - When user calls the "PATCH /programs/{programId}" endpoint - And 1 or more FSPs are provided that are not yet configured for this program - Then these FSPs are added to the program - And the whole "program" object is returned - - Scenario: Unsuccessfully add non-existing Financial Service Provider to a program - Given the "programId" is provided - When user calls the "PATCH /programs/{programId}" endpoint - And 1 or more FSPs are provided that do not exist in the instance - Then a 400 Bad Request response is returned with a message with more information about the error - diff --git a/features/Admin-user/Update_program_question.feature b/features/Admin-user/Update_program_question.feature deleted file mode 100644 index b4753c4300..0000000000 --- a/features/Admin-user/Update_program_question.feature +++ /dev/null @@ -1,20 +0,0 @@ -@swagger-ui -Feature: Update Program question - - Background: - Given a logged-in "admin" user in the Swagger UI - - Scenario: Successfully update properties of program question - Given the "programId" is provided - Given the required property "name" is provided and is existing in the database - Given 0 or more other attributes are provided - When the user fills in the body properties - And calls the "PATCH /programs/{programId}/program-questions/${questionId}" endpoint - Then only the provided properties of the existing "program question" are updated - And the whole "program question" object is returned - - Scenario: Unsuccessfully update unknown program question - Given the required property "name" is provided and is not existing in the database - When the user fills in the body properties - And calls the "PATCH /programs/{programId}/program-questions/${questionId}" endpoint - Then a 404 response is returned with a message that the "program question" is not found diff --git a/features/Automated/Send_reminder_on_uncollected_voucher.feature b/features/Automated/Send_reminder_on_uncollected_voucher.feature deleted file mode 100644 index 8af98326cb..0000000000 --- a/features/Automated/Send_reminder_on_uncollected_voucher.feature +++ /dev/null @@ -1,24 +0,0 @@ -@cronjob -Feature: Send reminder message for unclaimed vouchers (use phonenumber: 16005550002 & change the cronjob to run every minute and change the time filter to test this) - - Background: - Given a program with "Intersolve" FSP - Given a PA has chosen FSP "Intersolve-voucher-whatsapp" - Given a payment has been made to this PA - Given the PA has received the initial WhatsApp-message - - Scenario: PA does not send anything to initial WhatsApp-message - When the PA does not send anything back - Then a reminder is sent out the next day at noon - When the PA does not send anything back - Then a reminder is sent out the next day at noon - And a PA without WhatsApp should not get a reminder - And a PA should not be reminded more than 3 times - - Scenario: PA does not send anything to initial WhatsApp-message with other programs in the instance - Given there is another program that has 3+ more payments - Given the PA is only ever included into the one program - When the PA does not send anything - Then a reminder is sent out the next day at noon irrespective of how many payments any other programs have - And this basically means that the counting of 'sending maximum 3 latest vouchers' is done per program, not overall - And the PA should not be reminded more than 3 times diff --git a/features/Other/Claim_digital_voucher.feature b/features/Other/Claim_digital_voucher.feature deleted file mode 100644 index 985c55d468..0000000000 --- a/features/Other/Claim_digital_voucher.feature +++ /dev/null @@ -1,53 +0,0 @@ -@send-whatsapp-message -Feature: Claim digital vouchers - - Background: - Given a program with "Intersolve" FSP - And a PA has chosen FSP "Intersolve-voucher-whatsapp" - And a payment has been made to this PA - And the PA has received the initial WhatsApp-message - - Scenario: Send 'yes' reply to initial WhatsApp-message when uncollected vouchers available - When the PA replies 'yes' to initial WhatsApp-message - Then a WhatsApp-message is sent to the PA - And it includes the voucher image - And it includes an accompanying explanation text - And it includes a second image which explains how to use the voucher in the store - And it includes additional images for older uncollected vouchers, but only of the 2 payments prior to the current one - And all sent vouchers are marked as 'claimed' in the database, and cannot be sent again in the future - And the status of the transaction in the PA-table updates to 'success' if not already the case - And the transaction in the PA-table now shows that the voucher is sent (through a cash-icon) and at which date - - Scenario: Send 'yes' reply when no uncollected vouchers available - When the PA replies 'yes' to initial WhatsApp-message (or sends 'yes' at any moment) - Then a WhatsApp-message is sent to the PA - And it mentions that this is an automated response and to contact the help-desk - - Scenario: Send anything else besides 'yes' - When the PA sends anything else then 'yes' - Then the same scenarios as above are followed, as it does not matter what text is exactly sent - - Scenario: PA does not send anything to initial WhatsApp-message - When the PA does not send anything - Then a reminder is send out the next day at noon - And the same scenarios as above are followed - And a PA without WhatsApp should not get a reminder - And a PA should not be reminded more than 3 times - - Scenario: PA claims digital voucher with other programs that have more payments - Given there is another program that has 3+ more payments - Given the PA is only ever included into the one program - When the PA replies 'yes' to initial WhatsApp-message - Then the PA receives all vouchers for their program that were not already sent up to 3 payments back irrespective of how many payments any other programs have - And this basically means that the counting of 'sending maximum 3 latest vouchers' is done per program, not overall - - Scenario: Template error occurs when sending WhatsApp-message - TODO: This scenario is not specifically about claiming vouchers, but about sending WhatsApp follow-up messages in general - Given a PA has phone number 16005550003 (and Twilio mock is enabled) - And the PA has received the initial WhatsApp-message - When the PA replies 'yes' to initial WhatsApp-message - Then an error occurs with code 63016 - When the system gets the error code 63016 - Then the system tries to send the message again - And the system will retry this 3 times with 30 seconds in between - diff --git a/features/README.md b/features/README.md deleted file mode 100644 index b82014fffb..0000000000 --- a/features/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# Features - - - -- [Features](#features) - - [All features / scenario's](#all-features--scenarios) - - [For Aid Workers](#for-aid-workers) - - [Using 121-Portal](#using-121-portal) - - [Using 3rd party systems](#using-3rd-party-systems) - - [For Person/People Affected](#for-personpeople-affected) - - [Using external tools/applications](#using-external-toolsapplications) - - [For Admin-user](#for-admin-user) - - [Using Swagger UI](#using-swagger-ui) - - [Automated processes (121-service)](#automated-processes-121-service) - - [Reference](#reference) - - [Tools](#tools) - - [How to describe features / define scenarios](#how-to-describe-features--define-scenarios) - ---- - -## All features / scenario's - -Features of the 121-platform are described in this folder in a standardized way using the [Gherkin-language](https://cucumber.io/docs/gherkin/). - -### For Aid Workers - -#### Using 121-Portal - -- [View dashboard page](121-Portal/View_dashboard_page.feature) -- [Manage team members](121-Portal/Manage_team_members.feature) -- [View and Manage people affected](121-Portal/View_and_Manage_people_affected.feature) -- [View payment history popup](121-Portal/View_payment_history_popup.feature) -- [Edit information of Person Affected](121-Portal/Edit_Info_Person_Affected.feature) -- [Import registrations as registered](121-Portal/Import_as_registered.feature) -- [Import registrations as imported](121-Portal/Import_as_imported.feature) -- [Invite people affected](121-Portal/Invite_people_affected.feature) -- [Delete people affected](121-Portal/Delete_people_affected.feature) -- [Mark as no longer eligible](121-Portal/Mark_as_no_longer_eligible.feature) -- [Import registered people affected](121-Portal/Import_people_affected.feature#L83) -- [Export People Affected](121-Portal/Export_people_affected.feature) -- [Export PA data changes](121-Portal/Export_PA_data_changes.feature) -- [Include people affected](121-Portal/Include_people_affected.feature) -- [Reject or End Inclusion of people affected](121-Portal/Reject_or_End_inclusion_people_affected.feature) -- [Export duplicate People Affected list](121-Portal/Export_duplicate_people_affected_list.feature) -- [Make a new payment](121-Portal/Make_new_payment.feature) -- [View last payment overview](121-Portal/View_last_payment_overview.feature) -- [Export payment details](121-Portal/Export_Payment_Details.feature) -- [Manage payment via import and export](121-Portal/Manage_payment_via_import_and_export.feature) -- [Export unused vouchers](121-Portal/Export_unused_vouchers.feature) -- [Export Intersolve Visa cards](121-Portal/Export_Intersolve_Visa_cards.feature) -- Get voucher balance -- View/Download/Print voucher -- [View PA profile page](121-Portal/View_PA_profile_page.feature) -- [Manage Intersolve Visa card](121-Portal/Manage_Intersolve_Visa_card.feature) -- Generic 121-Portal components/features - - Login - - Logout - - [Change password] (121-Portal/Change_password.feature) - - [Navigate home and main menu](121-Portal/Navigate_home_and_main_menu.feature) - - [Navigate program menu](121-Portal/Navigate_program_menu.feature) - -#### Using 3rd party systems - -Using Redline WhatsApp Helpdesk - -- View iframe with PA details based on phone number - -Using application such as EspoCRM to call our API - -- Create/import registration: The general 'update PA' flow is automatically tested via [API-test](..\services\121-service\test\registrations\import-registration.test.ts) -- Update PA attribute: The general 'update PA' flow is automatically tested via [API-test](..\services\121-service\test\registrations\update-registration.test.ts) -- Delete PA: The general 'delete PA' flow is automatically tested via [API-test](..\services\121-service\test\registrations\delete-registration.test.ts) -- Update FSP of PA: Not tested yet. - -### For Person/People Affected - -#### Using external tools/applications - -- Send a WhatsApp message - - [Claim digital voucher](Other/Claim_digital_voucher.feature) - - Claim pending message - -### For Admin-user - -#### Using Swagger UI - -- [Manage user roles](Admin-user/Manage_Roles.feature) -- [Add and Update program custom attribute](Admin-user/Add_And_Update_program_custom_attribute.feature) -- [Update program question](Admin-user/Update_program_question.feature) -- [Update program](Admin-user/Update_program.feature) -- [Export Intersolve vouchers to cancel](Admin-user/Export_vouchers_to_cancel.feature) -- [Sync Intersolve Visa Customer](Admin-user/Sync_Intersolve_Visa_Customer.feature) -- [Load seed data](Admin-user/Load_seed_data.feature) -- Update Financial Service Provider (not chosen FSP, but entity itself) -- Create/Update/Delete FSP attributes -- Update organization -- Add/update Intersolve instructions image -- Create new aidworker user and manage assignment to program -- Delete user - -### Automated processes (121-service) - -- [Send reminder on uncollected vouchers](Automated/Send_reminder_on_uncollected_voucher.feature) - ---- - -## Reference - -- The complete definition of the Gherkin syntax: -- A comprehensive guide on BDD by Automation Panda: - - [The Gherkin Language](https://automationpanda.com/2017/01/26/bdd-101-the-gherkin-language/) - - [Gherkin by example](https://automationpanda.com/2017/01/27/bdd-101-gherkin-by-example/) - - [Writing good Gherkin](https://automationpanda.com/2017/01/30/bdd-101-writing-good-gherkin/) - -## Tools - -- [BDD Editor](http://www.bddeditor.com/editor): A 'wizard'-like interface to create feature-files in a browser. -- [AssertThat Gherkin editor](https://www.assertthat.com/gherkin_editor): An editor, syntax-highlighting and validator in a browser. -- VSCode-extension: [Cucumber (Gherkin) Full Support](https://marketplace.visualstudio.com/items?itemName=alexkrechik.cucumberautocomplete) - -## How to describe features / define scenarios - -Features can be added to this folder by: - -- Create a `.feature`-file, named after its title with `_` for spaces; - i.e. `View_PA_profile_page.feature` -- Add a reference to the list above at the appropriate _actor_. -- Tag the whole feature or each scenario with the components involved. - i.e: `@portal`, etc. (all lowercase) diff --git a/interfaces/Portal/src/app/components/registration-activity-overview/registration-activity-overview.component.ts b/interfaces/Portal/src/app/components/registration-activity-overview/registration-activity-overview.component.ts index 9f6076323f..9bc7a8a9e1 100644 --- a/interfaces/Portal/src/app/components/registration-activity-overview/registration-activity-overview.component.ts +++ b/interfaces/Portal/src/app/components/registration-activity-overview/registration-activity-overview.component.ts @@ -9,6 +9,7 @@ import { RegistrationActivity, RegistrationActivityType, } from 'src/app/models/registration-activity.model'; +import { TranslatableString } from 'src/app/models/translatable-string.model'; import { PastPaymentsService } from 'src/app/services/past-payments.service'; import { RegistrationActivityService } from 'src/app/services/registration-activity.service'; import { PaymentUtils } from 'src/app/shared/payment.utils'; @@ -17,8 +18,8 @@ import { AuthService } from '../../auth/auth.service'; import Permission from '../../auth/permission.enum'; import EventType from '../../enums/event-type.enum'; import { Attribute } from '../../models/attribute.model'; -import { AnswerType } from '../../models/fsp.model'; import { Person } from '../../models/person.model'; +import { RegistrationAttributeType } from '../../models/registration-attribute.model'; import { RegistrationActivityDetailAccordionComponent } from '../../program/registration-activity-detail-accordion/registration-activity-detail-accordion.component'; import { EnumService } from '../../services/enum.service'; import { MessagesService } from '../../services/messages.service'; @@ -199,13 +200,13 @@ export class RegistrationActivityOverviewComponent implements OnInit { (attr) => attr.name === change.attributes.fieldName, ); - if (attribute?.type === AnswerType.Boolean) { + if (attribute?.type === RegistrationAttributeType.Boolean) { const booleanLabel = { true: this.translate.instant( - 'page.program.program-people-affected.column.custom-attribute-true', + 'page.program.program-people-affected.column.registration-attribute-true', ), false: this.translate.instant( - 'page.program.program-people-affected.column.custom-attribute-false', + 'page.program.program-people-affected.column.registration-attribute-false', ), }; oldValue = booleanLabel[oldValue]; @@ -261,29 +262,17 @@ export class RegistrationActivityOverviewComponent implements OnInit { if (change.type === EventType.financialServiceProviderChange) { if (description.oldValue) { - try { - description.oldValue = JSON.parse(description.oldValue); - description.oldValue = this.translatableString.get( - description.oldValue, - ); - } catch (error) { - description.oldValue = this.translatableString.get( - description.oldValue, - ); - } + const label = this.getLabelForFspConfigChange(description.oldValue); + description.oldValue = label + ? this.translatableString.get(label) + : this.translatableString.get(description.oldValue); } if (description.newValue) { - try { - description.newValue = JSON.parse(description.newValue); - description.newValue = this.translatableString.get( - description.newValue, - ); - } catch (error) { - description.newValue = this.translatableString.get( - description.newValue, - ); - } + const label = this.getLabelForFspConfigChange(description.newValue); + description.newValue = label + ? this.translatableString.get(label) + : this.translatableString.get(description.newValue); } this.activityOverview.push({ @@ -312,6 +301,14 @@ export class RegistrationActivityOverviewComponent implements OnInit { this.activityOverview.sort((a, b) => (b.date > a.date ? 1 : -1)); } + private getLabelForFspConfigChange( + name: string, + ): string | TranslatableString { + return this.program.financialServiceProviderConfigurations.find( + (fspConfig) => fspConfig.name === name, + )?.label; + } + private getSubLabelText(change: any, attribute: Attribute): string { const translationKey = `page.program.program-people-affected.column.${change.attributes.fieldName}`; const translation = this.translate.instant(translationKey); diff --git a/interfaces/Portal/src/app/components/registration-personal-information/registration-personal-information.component.ts b/interfaces/Portal/src/app/components/registration-personal-information/registration-personal-information.component.ts index 3278b83785..8a50a82747 100644 --- a/interfaces/Portal/src/app/components/registration-personal-information/registration-personal-information.component.ts +++ b/interfaces/Portal/src/app/components/registration-personal-information/registration-personal-information.component.ts @@ -52,7 +52,7 @@ export class RegistrationPersonalInformationComponent implements OnInit { ]; private canUpdatePaData: boolean; - private canUpdatePaFsp: boolean; + private canUpdatePaProgramFspConfig: boolean; private canViewPersonalData: boolean; private canUpdateRegistrationAttributeFinancial: boolean; private canUpdatePersonalData: boolean; @@ -161,15 +161,13 @@ export class RegistrationPersonalInformationComponent implements OnInit { } if ( - this.person.financialServiceProvider && - this.program.financialServiceProviders + this.person.financialServiceProviderName && + this.program.financialServiceProviderConfigurations ) { this.personalInfoTable.push({ label: this.getLabel('fsp'), value: this.translatableString.get( - this.program.financialServiceProviders.find( - (i) => i.fsp === this.person.financialServiceProvider, - )?.displayName, + this.person.programFinancialServiceProviderConfigurationLabel, ), }); } @@ -193,7 +191,7 @@ export class RegistrationPersonalInformationComponent implements OnInit { canUpdateRegistrationAttributeFinancial: this.canUpdateRegistrationAttributeFinancial, canUpdatePersonalData: this.canUpdatePersonalData, - canUpdatePaFsp: this.canUpdatePaFsp, + canUpdatePaProgramFspConfig: this.canUpdatePaProgramFspConfig, canViewMessageHistory: this.canViewMessageHistory, canViewPaymentData: this.canViewPaymentData, }, @@ -206,9 +204,10 @@ export class RegistrationPersonalInformationComponent implements OnInit { this.canUpdatePaData = this.authService.hasAllPermissions(this.program.id, [ Permission.RegistrationAttributeUPDATE, ]); - this.canUpdatePaFsp = this.authService.hasAllPermissions(this.program.id, [ - Permission.RegistrationFspUPDATE, - ]); + this.canUpdatePaProgramFspConfig = this.authService.hasAllPermissions( + this.program.id, + [Permission.RegistrationFspConfigUPDATE], + ); this.canViewPersonalData = this.authService.hasAllPermissions( this.program.id, [Permission.RegistrationPersonalREAD], diff --git a/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.html b/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.html index 52b7eda117..f4b9839d86 100644 --- a/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.html +++ b/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.html @@ -20,7 +20,7 @@
diff --git a/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.ts b/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.ts index a9e21a5a1c..ddb54d8704 100644 --- a/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.ts +++ b/interfaces/Portal/src/app/components/registration-profile/registration-profile.component.ts @@ -65,7 +65,7 @@ export class RegistrationProfileComponent implements OnInit { } public fspHasPhysicalCardSupport( - fspName: Person['financialServiceProvider'], + fspName: Person['financialServiceProviderName'], ): boolean { return PaymentUtils.hasPhysicalCardSupport(fspName); } diff --git a/interfaces/Portal/src/app/components/update-fsp/update-fsp.component.html b/interfaces/Portal/src/app/components/update-fsp/update-fsp.component.html index c76769484c..c3cfaef69a 100644 --- a/interfaces/Portal/src/app/components/update-fsp/update-fsp.component.html +++ b/interfaces/Portal/src/app/components/update-fsp/update-fsp.component.html @@ -11,7 +11,7 @@
- {{ fspItem.displayName }} + {{ fspConfigItem.translatedLabel }} - - - {{ - 'page.program.program-people-affected.edit-person-affected-popup.fspChangeWarning' - | translate - }}
-
-
    -
  • - {{ attr.label | translate }} -
  • -
-
-
-

- {{ - 'page.program.program-people-affected.edit-person-affected-popup.fspNewAttributesExplanation' - | translate - }} -

-
- -
-
{ @@ -113,75 +117,27 @@ export class UpdateFspComponent implements OnInit { ); } - public onFspChange({ detail }) { - this.getFspAttributes(detail.value); - this.checkAttributesCorrectlyFilled(); + public onFspSelectionChange({ detail }) { + this.setSelectedFspAndPrepareDropdown(detail.value); + this.enableUpdateBtn = this.startingFspName !== this.selectedFspName; } - public getFspAttributes(fspString: FspName) { - this.selectedFspAttributes = []; - this.attributesToSave = {}; - if (this.fspList) { - this.fspList = this.fspList.map((fspItem) => ({ - ...fspItem, - displayName: this.translatableString.get(fspItem.displayName), - })); - - const selectedFsp = this.fspList.find( - (fspItem) => fspItem.fsp === fspString, + public setSelectedFspAndPrepareDropdown(fspString: string) { + if (this.programFspConfigList) { + this.programFspConfigList = this.programFspConfigList.map( + (programFspConfig) => ({ + ...programFspConfig, + translatedLabel: this.translatableString.get(programFspConfig.label), + }), ); - if (selectedFsp) { - this.selectedFspName = selectedFsp.fsp; - this.selectedFspAttributes = selectedFsp.editableAttributes.map( - (attr) => ({ - ...attr, - label: this.translatableString.get(attr.label), - }), - ); - - // Preload attributesToSave .. - this.attributesToSave = this.selectedFspAttributes.reduce( - (obj, key) => { - obj[key.name] = - //.. with prefilled value if available - this.attributeValues[key.name] || - // .. and with empty string / null otherwise - (key.type === AnswerType.Text ? '' : null); - return obj; - }, - {}, - ); - } - - this.attributeDifference = this.startingAttributes.filter( - (attr) => !this.selectedFspAttributes.includes(attr), + const selectedFsp = this.programFspConfigList.find( + (fspItem) => fspItem.name === fspString, ); - } - } - - public onAttributeChange(attrName, { detail }) { - this.attributesToSave = { - ...this.attributesToSave, - [attrName]: detail.value.trim(), - }; - this.checkAttributesCorrectlyFilled(); - } - - private checkAttributesCorrectlyFilled() { - for (const attr of this.selectedFspAttributes) { - if ( - !CheckAttributeInputUtils.isAttributeCorrectlyFilled( - attr.type, - attr.pattern, - this.attributesToSave[attr.name], - ) - ) { - this.enableUpdateBtn = false; - return; + if (selectedFsp) { + this.selectedFspName = selectedFsp.name; } } - this.enableUpdateBtn = true; } } diff --git a/interfaces/Portal/src/app/components/update-property-item/update-property-item.component.html b/interfaces/Portal/src/app/components/update-property-item/update-property-item.component.html index 4a0fb6936b..f19ba57675 100644 --- a/interfaces/Portal/src/app/components/update-property-item/update-property-item.component.html +++ b/interfaces/Portal/src/app/components/update-property-item/update-property-item.component.html @@ -9,10 +9,10 @@ interface="popover" class="ion-margin-top" [disabled]="isDisabled" - [multiple]="type === answerType.MultiSelect" + [multiple]="type === registrationAttributeType.MultiSelect" > {{ option.label }} @@ -36,7 +36,7 @@ - + - + - + - + - + ({ + option, + label: this.translate.get(label), + })); + return translatedOptions; + } + + private addEmptyOption(options: ProgramRegistrationAttributeOption[]) { + if (options.length > 0 && options[0].option !== '_null_') { + options.unshift({ + option: '_null_', // Ionic select does not support null properly so using '_null_' instead as a workaround + label: { en: '-' }, // Add an string '-' for every language + }); + } + return options; + } + public doUpdate(reasonInput?: string) { if (this.type === 'date') { if (!this.isValidDate()) { @@ -101,18 +140,25 @@ export class UpdatePropertyItemComponent implements OnInit { return; } } + let updatedValue = this.propertyModel; - this.updated.emit({ value: this.propertyModel, reason: reasonInput }); - } + if ( + this.type === RegistrationAttributeType.Enum && + this.propertyModel === '_null_' + ) { + updatedValue = null; + } - public translatedOptions() { - return this.options.map(({ option, label }) => ({ - option, - label: this.translate.get(label), - })); + this.updated.emit({ value: updatedValue, reason: reasonInput }); } private isValidDate(): boolean { + if ( + !this.isRequired && + (this.propertyModel === '' || this.propertyModel == null) + ) { + return true; + } const dateInput = this.propertyModel; const regex = /^\d{2}-\d{2}-\d{4}$/; @@ -144,6 +190,7 @@ export class UpdatePropertyItemComponent implements OnInit { this.type, this.pattern, this.propertyModel, + this.isRequired, ) ); } diff --git a/interfaces/Portal/src/app/enums/fsp-name.enum.ts b/interfaces/Portal/src/app/enums/fsp-name.enum.ts index 6acdb405ac..bd5bf47a35 100644 --- a/interfaces/Portal/src/app/enums/fsp-name.enum.ts +++ b/interfaces/Portal/src/app/enums/fsp-name.enum.ts @@ -1,8 +1,8 @@ -import { FinancialServiceProviders as FinancialServiceProviderNameEnum } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; /** * Note: Names-definition explicitly used from back-end, to prevent repetition. */ -const FspName = FinancialServiceProviderNameEnum; -type FspName = FinancialServiceProviderNameEnum; +const FspName = FinancialServiceProviders; +type FspName = FinancialServiceProviders; export default FspName; diff --git a/interfaces/Portal/src/app/mocks/api.programs.mock.ts b/interfaces/Portal/src/app/mocks/api.programs.mock.ts index 21205cf8af..04a489b4ff 100644 --- a/interfaces/Portal/src/app/mocks/api.programs.mock.ts +++ b/interfaces/Portal/src/app/mocks/api.programs.mock.ts @@ -19,22 +19,11 @@ const programsArray: Program[] = [ author: {}, published: true, notifications: { en: 'Notification text' }, - programCustomAttributes: [ - { - id: 1, - name: 'namePartnerOrganization', - type: 'string', - programId: 1, - label: { - en: 'Partner Organization', - }, - }, - ], - programQuestions: [ + programRegistrationAttributes: [ { id: 1, name: 'phoneNumber', - answerType: 'tel', + registrationAttributeType: 'tel', label: { en: 'Phone Number', }, @@ -46,7 +35,7 @@ const programsArray: Program[] = [ enableMaxPayments: true, enableScope: true, allowEmptyPhoneNumber: false, - financialServiceProviders: [], + financialServiceProviderConfigurations: [], aidworkerAssignments: [], fullnameNamingConvention: [], paTableAttributes: [], diff --git a/interfaces/Portal/src/app/models/actions.model.ts b/interfaces/Portal/src/app/models/actions.model.ts index 61d1d2d2e6..843a4246d5 100644 --- a/interfaces/Portal/src/app/models/actions.model.ts +++ b/interfaces/Portal/src/app/models/actions.model.ts @@ -1,5 +1,4 @@ export enum ActionType { - importPeopleAffected = 'import-people-affected', importRegistrations = 'import-registrations', paymentFinished = 'payment-finished', paymentStarted = 'payment-started', diff --git a/interfaces/Portal/src/app/models/attribute.model.ts b/interfaces/Portal/src/app/models/attribute.model.ts index 13a37a0529..ce74e89ad8 100644 --- a/interfaces/Portal/src/app/models/attribute.model.ts +++ b/interfaces/Portal/src/app/models/attribute.model.ts @@ -1,9 +1,10 @@ -import { AnswerType } from './fsp.model'; +import { RegistrationAttributeType } from './registration-attribute.model'; import { TranslatableString } from './translatable-string.model'; export class Attribute { name: string; - type: AnswerType; + type: RegistrationAttributeType; label: TranslatableString; + isRequired: boolean; pattern?: string; } diff --git a/interfaces/Portal/src/app/models/bulk-actions.models.ts b/interfaces/Portal/src/app/models/bulk-actions.models.ts index 4803988a4b..28912c61b0 100644 --- a/interfaces/Portal/src/app/models/bulk-actions.models.ts +++ b/interfaces/Portal/src/app/models/bulk-actions.models.ts @@ -39,5 +39,5 @@ export class BulkActionResult { public readonly applicableCount: number; public readonly nonApplicableCount: number; public readonly sumPaymentAmountMultiplier?: number; - public readonly fspsInPayment?: string[]; + public readonly programFinancialServiceProviderConfigurationNames?: string[]; } diff --git a/interfaces/Portal/src/app/models/fsp.model.ts b/interfaces/Portal/src/app/models/fsp.model.ts index f944de8ca9..2e9a557a02 100644 --- a/interfaces/Portal/src/app/models/fsp.model.ts +++ b/interfaces/Portal/src/app/models/fsp.model.ts @@ -2,40 +2,19 @@ import FspName from '../enums/fsp-name.enum'; import { Attribute } from './attribute.model'; import { TranslatableString } from './translatable-string.model'; -export class Fsp { - id: number; - fsp: FspName; - displayName: TranslatableString | string; - integrationType: FspIntegrationType; - hasReconciliation: boolean; - questions?: FspQuestion[]; - editableAttributes?: Attribute[]; -} - -export enum AnswerType { - // Translate the types used in the API to internal, proper types: - Number = 'numeric', - Text = 'text', - Date = 'date', - Enum = 'dropdown', - PhoneNumber = 'tel', - Email = 'email', - Boolean = 'boolean', - MultiSelect = 'multi-select', -} -export class FspQuestion { +export class FinancialServiceProviderConfiguration { id: number; name: string; - answerType: AnswerType; - label: TranslatableString; - placeholder?: TranslatableString; - options: FspAttributeOption[] | null; - duplicateCheck: boolean; + label: TranslatableString | string; + editableAttributes?: Attribute[]; + financialServiceProviderName: FspName; + financialServiceProvider: FinancialServiceProvider; } -export class FspAttributeOption { - option: string; - label: TranslatableString; +export class FinancialServiceProvider { + integrationType: FspIntegrationType; + notifyOnTransaction: boolean; + attributes: { name: string; required: boolean }[]; } export enum FspIntegrationType { diff --git a/interfaces/Portal/src/app/models/payment.model.ts b/interfaces/Portal/src/app/models/payment.model.ts index f62f401520..bf08d22727 100644 --- a/interfaces/Portal/src/app/models/payment.model.ts +++ b/interfaces/Portal/src/app/models/payment.model.ts @@ -26,7 +26,8 @@ export class PaymentRowDetail { transaction?: Transaction; errorMessage?: string; waiting?: boolean; - fsp?: FspName; + programFinancialServiceProviderConfigurationTranslatedLabel?: string; + financialServiceProviderName?: FspName; status?: string; paymentDate?: string; } diff --git a/interfaces/Portal/src/app/models/person.model.ts b/interfaces/Portal/src/app/models/person.model.ts index 35f9195a40..7ae59f3778 100644 --- a/interfaces/Portal/src/app/models/person.model.ts +++ b/interfaces/Portal/src/app/models/person.model.ts @@ -14,8 +14,11 @@ export class Person { registrationCreated?: string; status: RegistrationStatus; note?: string; - financialServiceProvider?: FspName; - fspDisplayName?: string | TranslatableString; + financialServiceProviderName: FspName; + programFinancialServiceProviderConfigurationName: string; + programFinancialServiceProviderConfigurationLabel: + | string + | TranslatableString; paymentAmountMultiplier?: number; maxPayments?: number; preferredLanguage?: LanguageEnum; diff --git a/interfaces/Portal/src/app/models/program.model.ts b/interfaces/Portal/src/app/models/program.model.ts index ec8f4e933c..769a0582a4 100644 --- a/interfaces/Portal/src/app/models/program.model.ts +++ b/interfaces/Portal/src/app/models/program.model.ts @@ -1,6 +1,6 @@ import { FilterOperator } from '../enums/filters.enum'; import { Attribute } from './attribute.model'; -import { Fsp } from './fsp.model'; +import { FinancialServiceProviderConfiguration } from './fsp.model'; import { LanguageEnum } from './person.model'; import { TranslatableString } from './translatable-string.model'; @@ -18,14 +18,13 @@ export class Program { targetNrRegistrations?: number; distributionDuration: number; distributionFrequency: DistributionFrequency; - financialServiceProviders?: Fsp[]; + financialServiceProviderConfigurations?: FinancialServiceProviderConfiguration[]; aidworkerAssignments?: any[]; created: string; updated: string; validation: boolean; published: boolean; - programCustomAttributes: ProgramCustomAttribute[]; - programQuestions: ProgramQuestion[]; + programRegistrationAttributes: ProgramRegistrationAttribute[]; editableAttributes?: Attribute[]; notifications: string | TranslatableString; languages: LanguageEnum[]; @@ -78,28 +77,21 @@ export class AidWorker { created: string | Date; } -export class ProgramCustomAttribute { - id: number; - programId: number; - name: string; - type: string; - label?: TranslatableString; -} - export class PaTableAttribute extends Attribute {} -export class ProgramQuestion { +export class ProgramRegistrationAttribute { id: number; name: string; - answerType: string; + registrationAttributeType: string; label: TranslatableString; placeholder?: TranslatableString; pattern?: string; // Remember to escape the special characters in the string! - options: null | ProgramQuestionOption[]; + options: null | ProgramRegistrationAttributeOption[]; duplicateCheck: boolean; + scoring: Record; } -export class ProgramQuestionOption { +export class ProgramRegistrationAttributeOption { option: string; label: TranslatableString; } diff --git a/interfaces/Portal/src/app/models/registration-attribute.model.ts b/interfaces/Portal/src/app/models/registration-attribute.model.ts new file mode 100644 index 0000000000..67a5e164cc --- /dev/null +++ b/interfaces/Portal/src/app/models/registration-attribute.model.ts @@ -0,0 +1,11 @@ +export enum RegistrationAttributeType { + // Translate the types used in the API to internal, proper types: + Number = 'numeric', + Text = 'text', + Date = 'date', + Enum = 'dropdown', + PhoneNumber = 'tel', + Email = 'email', + Boolean = 'boolean', + MultiSelect = 'multi-select', +} diff --git a/interfaces/Portal/src/app/models/transaction.model.ts b/interfaces/Portal/src/app/models/transaction.model.ts index da48c01a11..9919538070 100644 --- a/interfaces/Portal/src/app/models/transaction.model.ts +++ b/interfaces/Portal/src/app/models/transaction.model.ts @@ -1,6 +1,7 @@ import FspName from '../enums/fsp-name.enum'; import { StatusEnum } from './status.enum'; import { IntersolvePayoutStatus } from './transaction-custom-data'; +import { TranslatableString } from './translatable-string.model'; export class Transaction { id: number; @@ -16,7 +17,10 @@ export class Transaction { IntersolvePayoutStatus: IntersolvePayoutStatus; } | any; - fspName: string; + financialServiceProviderName: FspName; + programFinancialServiceProviderConfigurationTranslatedLabel: string; + programFinancialServiceProviderConfigurationLabel: TranslatableString; + programFinancialServiceProviderConfigurationName: string; fsp: FspName; user: { id: number; diff --git a/interfaces/Portal/src/app/program/bulk-import/bulk-import.component.html b/interfaces/Portal/src/app/program/bulk-import/bulk-import.component.html index 2eda78109c..f0a915c362 100644 --- a/interfaces/Portal/src/app/program/bulk-import/bulk-import.component.html +++ b/interfaces/Portal/src/app/program/bulk-import/bulk-import.component.html @@ -2,7 +2,7 @@
{ - let action = ActionType.importPeopleAffected; - - if (type === RegistrationStatus.registered) { - action = ActionType.importRegistrations; + if (type !== RegistrationStatus.registered) { + return ''; } + const action = ActionType.importRegistrations; + const latestAction = await this.programsService.retrieveLatestActions( action, this.programId, diff --git a/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.html b/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.html index 9befab884a..b6b5893ba0 100644 --- a/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.html +++ b/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.html @@ -59,6 +59,7 @@

{{ person?.name }}

| translate " [value]="attributeValues?.paymentAmountMultiplier" + [isRequired]="true" (updated)=" updatePaAttribute( 'paymentAmountMultiplier', @@ -99,6 +100,7 @@

{{ person?.name }}

| translate " [value]="attributeValues?.maxPayments" + [isRequired]="false" (updated)=" updatePaAttribute( 'maxPayments', @@ -129,6 +131,7 @@

{{ person?.name }}

| translate " [value]="attributeValues?.phoneNumber" + [isRequired]="!program.allowEmptyPhoneNumber" (updated)=" updatePaAttribute( 'phoneNumber', @@ -156,6 +159,7 @@

{{ person?.name }}

| translate " [value]="attributeValues?.scope" + [isRequired]="false" (updated)=" updatePaAttribute( 'scope', @@ -180,6 +184,7 @@

{{ person?.name }}

| translate " [value]="attributeValues.preferredLanguage" + [isRequired]="true" [options]="availableLanguages" (updated)=" updatePaAttribute( @@ -198,33 +203,34 @@

{{ person?.name }}

{{ - 'page.program.program-people-affected.column.fspDisplayName' + 'page.program.program-people-affected.column.programFinancialServiceProviderConfigurationLabel' | translate }}:
- {{ person?.fspDisplayName }} + {{ person?.programFinancialServiceProviderConfigurationLabel }}
{{ person?.name }} [showSubmit]="canUpdatePaData && canUpdatePersonalData" [options]="paTableAttribute.options" [pattern]="paTableAttribute.pattern" + [isRequired]="paTableAttribute.isRequired" [explanation]="paTableAttribute.explanation" (updated)=" updatePaAttribute( diff --git a/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.ts b/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.ts index ff8a20b939..d6bb578b96 100644 --- a/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.ts +++ b/interfaces/Portal/src/app/program/edit-person-affected-popup/edit-person-affected-popup.component.ts @@ -2,18 +2,14 @@ import { Component, Input, OnInit } from '@angular/core'; import { AlertController, ModalController } from '@ionic/angular'; import { TranslateService } from '@ngx-translate/core'; import { DateFormat } from 'src/app/enums/date-format.enum'; -import { - AnswerType, - Fsp, - FspAttributeOption, - FspQuestion, -} from 'src/app/models/fsp.model'; +import { FinancialServiceProviderConfiguration } from 'src/app/models/fsp.model'; import { Person, PersonDefaultAttributes } from 'src/app/models/person.model'; import { Program, - ProgramQuestion, - ProgramQuestionOption, + ProgramRegistrationAttribute, + ProgramRegistrationAttributeOption, } from 'src/app/models/program.model'; +import { RegistrationAttributeType } from 'src/app/models/registration-attribute.model'; import { ProgramsServiceApiService } from 'src/app/services/programs-service-api.service'; import { PubSubEvent, PubSubService } from 'src/app/services/pub-sub.service'; import { TranslatableStringService } from 'src/app/services/translatable-string.service'; @@ -47,7 +43,7 @@ export class EditPersonAffectedPopupComponent implements OnInit { public canUpdatePersonalData = false; @Input({ required: true }) - public canUpdatePaFsp = false; + public canUpdatePaProgramFspConfig = false; @Input({ required: true }) public canViewMessageHistory = false; @@ -67,9 +63,9 @@ export class EditPersonAffectedPopupComponent implements OnInit { public paTableAttributes: Attribute[] = []; private paTableAttributesInput: Program['editableAttributes']; - public fspList: Fsp[] = []; + public programFspConfigList: FinancialServiceProviderConfiguration[] = []; public programFspLength = 0; - public personFsp: Fsp; + public personFsp: FinancialServiceProviderConfiguration; public availableLanguages = []; @@ -93,16 +89,9 @@ export class EditPersonAffectedPopupComponent implements OnInit { this.program = await this.programsService.getProgramById(this.programId); this.availableLanguages = this.getAvailableLanguages(); - if (this.program && this.program.financialServiceProviders) { - for (const fsp of this.program.financialServiceProviders) { - const fspDetails = await this.programsService.getFspById(fsp.id); - fspDetails.displayName = Object.assign( - {}, - fspDetails.displayName, - fsp.displayName, - ); - this.fspList.push(fspDetails); - } + if (this.program && this.program.financialServiceProviderConfigurations) { + this.programFspConfigList = + this.program.financialServiceProviderConfigurations; } this.person = ( @@ -124,16 +113,10 @@ export class EditPersonAffectedPopupComponent implements OnInit { this.attributeValues.preferredLanguage = this.person?.preferredLanguage; if (this.program && this.program.editableAttributes) { - this.paTableAttributesInput = this.program.editableAttributes; - - const fspObject = this.fspList.find( - (f) => f.fsp === this.person?.financialServiceProvider, + // Filter out the phoneNumber attribute here, because it always added to the edit PA popup, regardless of the program configuration as it is also a field in the RegistrationEntity + this.paTableAttributesInput = this.program.editableAttributes.filter( + (attribute) => attribute.name !== 'phoneNumber', ); - if (fspObject && fspObject.editableAttributes) { - this.paTableAttributesInput = fspObject.editableAttributes.concat( - this.paTableAttributesInput, - ); - } } if (this.canViewPersonalData) { @@ -145,10 +128,11 @@ export class EditPersonAffectedPopupComponent implements OnInit { this.attributeValues.scope = this.person?.scope; } - if (this.person?.fspDisplayName) { - this.person.fspDisplayName = this.translatableString.get( - this.person.fspDisplayName, - ); + if (this.person?.programFinancialServiceProviderConfigurationLabel) { + this.person.programFinancialServiceProviderConfigurationLabel = + this.translatableString.get( + this.person.programFinancialServiceProviderConfigurationLabel, + ); } this.loading = false; @@ -158,15 +142,16 @@ export class EditPersonAffectedPopupComponent implements OnInit { attribute: string, value: string | number | string[], reason: string, - isPaTableAttribute: boolean, + isProgramRegistrationAttribute: boolean, ): Promise { let valueToStore: string | number | string[]; valueToStore = value; - if (isPaTableAttribute && !Array.isArray(value)) { - valueToStore = String(value); + if (isProgramRegistrationAttribute && value === '') { + valueToStore = null; } + this.inProgress[attribute] = true; if (attribute === PersonDefaultAttributes.paymentAmountMultiplier) { @@ -219,10 +204,6 @@ export class EditPersonAffectedPopupComponent implements OnInit { reason, ) .then((response: Person) => { - console.log( - '🚀 ~ file: edit-person-affected-popup.component.ts:197 ~ EditPersonAffectedPopupComponent ~ .then ~ response:', - response, - ); this.inProgress[attribute] = false; this.attributeValues[attribute] = valueToStore; this.attributeValues.paymentAmountMultiplier = @@ -265,9 +246,9 @@ export class EditPersonAffectedPopupComponent implements OnInit { } private fillPaTableAttributes() { - this.programFspLength = this.fspList.length; - for (const fspItem of this.fspList) { - if (fspItem.fsp === this.person.financialServiceProvider) { + this.programFspLength = this.programFspConfigList.length; + for (const fspItem of this.programFspConfigList) { + if (fspItem.name === this.person.financialServiceProviderName) { this.personFsp = fspItem; } } @@ -279,8 +260,8 @@ export class EditPersonAffectedPopupComponent implements OnInit { let options = null; if ( - paTableAttribute.type === AnswerType.Enum || - paTableAttribute.type === AnswerType.MultiSelect + paTableAttribute.type === RegistrationAttributeType.Enum || + paTableAttribute.type === RegistrationAttributeType.MultiSelect ) { options = this.getDropdownOptions(paTableAttribute); } @@ -292,6 +273,7 @@ export class EditPersonAffectedPopupComponent implements OnInit { return { name: paTableAttribute.name, type: paTableAttribute.type, + isRequired: paTableAttribute.isRequired, label, value: this.person[paTableAttribute.name], options, @@ -302,30 +284,18 @@ export class EditPersonAffectedPopupComponent implements OnInit { ); } - private isFspAttribute(paTableAttribute: Attribute): boolean { - if (!this.personFsp || !this.personFsp.questions) { - return false; - } - return this.personFsp.questions.some( - (attr) => attr.name === paTableAttribute.name, - ); - } - private getDropdownOptions( paTableAttribute: Attribute, - ): FspAttributeOption[] | ProgramQuestionOption[] { - if (this.isFspAttribute(paTableAttribute)) { - const fspQuestion = this.personFsp.questions.find( - (attr: FspQuestion) => attr.name === paTableAttribute.name, + ): ProgramRegistrationAttributeOption[] { + const programRegistrationAttribute = + this.program.programRegistrationAttributes.find( + (attribute: ProgramRegistrationAttribute) => + attribute.name === paTableAttribute.name, ); - return fspQuestion.options ? fspQuestion.options : []; - } - - const programQuestion = this.program.programQuestions.find( - (question: ProgramQuestion) => question.name === paTableAttribute.name, - ); - return programQuestion.options ? programQuestion.options : []; + return programRegistrationAttribute.options + ? programRegistrationAttribute.options + : []; } public closeModal() { diff --git a/interfaces/Portal/src/app/program/export-fsp-instructions/export-fsp-instructions.component.ts b/interfaces/Portal/src/app/program/export-fsp-instructions/export-fsp-instructions.component.ts index 89816fe34f..f961ff0d7c 100644 --- a/interfaces/Portal/src/app/program/export-fsp-instructions/export-fsp-instructions.component.ts +++ b/interfaces/Portal/src/app/program/export-fsp-instructions/export-fsp-instructions.component.ts @@ -25,9 +25,6 @@ export class ExportFspInstructionsComponent implements OnChanges, OnInit { @Input() public lastPaymentId: number; - @Input() - private hasFspWithReconciliation: boolean; - @Input() public paymentInProgress: boolean; @@ -62,11 +59,8 @@ export class ExportFspInstructionsComponent implements OnChanges, OnInit { this.subHeader = this.translate.instant( 'page.program.export-fsp-intructions.confirm-message', ); - // This shows the 'reconciliation' variant if there is at least one FSP with reconciliation (not 100% correct, but good enough for now) this.message = this.translate.instant( - this.hasFspWithReconciliation - ? 'page.program.export-fsp-intructions.sub-message.reconciliation' - : 'page.program.export-fsp-intructions.sub-message.no-reconciliation', + 'page.program.export-fsp-intructions.sub-message.reconciliation', ); if ( await this.authService.hasPermission( @@ -107,7 +101,7 @@ export class ExportFspInstructionsComponent implements OnChanges, OnInit { .then( (res) => { this.isInProgress = false; - if (!res.data || res.data.length === 0) { + if (!res) { actionResult( this.alertController, this.translate, @@ -115,9 +109,13 @@ export class ExportFspInstructionsComponent implements OnChanges, OnInit { ); return; } - const exportFileName = `payment#${this.payment}-fsp-instructions`; - - downloadAsXlsx(res.data, exportFileName); + for (const fspInstructionPerProgramFspConfig of res) { + const exportFileName = `payment#${this.payment}-${fspInstructionPerProgramFspConfig.fileNamePrefix}-fsp-instructions`; + downloadAsXlsx( + fspInstructionPerProgramFspConfig.data, + exportFileName, + ); + } this.updateSubHeader(); }, diff --git a/interfaces/Portal/src/app/program/make-payment/make-payment.component.ts b/interfaces/Portal/src/app/program/make-payment/make-payment.component.ts index 1e05ea8366..35e00eb92d 100644 --- a/interfaces/Portal/src/app/program/make-payment/make-payment.component.ts +++ b/interfaces/Portal/src/app/program/make-payment/make-payment.component.ts @@ -178,8 +178,10 @@ export class MakePaymentComponent implements OnInit, OnDestroy { if (response) { const fspIntegrationType = getFspIntegrationType( - response.fspsInPayment || - this.program.financialServiceProviders.map((fsp) => fsp.fsp), + response.programFinancialServiceProviderConfigurationNames || + this.program.financialServiceProviderConfigurations.map( + (fspConfig) => fspConfig.name, + ), this.program, ); message += getPaymentResultText( @@ -192,8 +194,12 @@ export class MakePaymentComponent implements OnInit, OnDestroy { } private onPaymentError(error) { - if (error && error.error && error.error.errors) { - actionResult(this.alertController, this.translate, error.error.errors); + if (error && error.error && (error.error.errors || error.error.message)) { + actionResult( + this.alertController, + this.translate, + error.error.errors || error.error.message, + ); } else { actionResult( this.alertController, diff --git a/interfaces/Portal/src/app/program/metrics/metrics.component.ts b/interfaces/Portal/src/app/program/metrics/metrics.component.ts index 43afccf319..2e7c7b464b 100644 --- a/interfaces/Portal/src/app/program/metrics/metrics.component.ts +++ b/interfaces/Portal/src/app/program/metrics/metrics.component.ts @@ -108,7 +108,7 @@ export class MetricsComponent implements OnInit { icon: 'card', label: 'page.program.program-details.financialServiceProviders', value: getValueOrEmpty( - this.program.financialServiceProviders, + this.program.financialServiceProviderConfigurations, (value) => value.length, ), }); diff --git a/interfaces/Portal/src/app/program/payment-status-popup/payment-status-popup.component.ts b/interfaces/Portal/src/app/program/payment-status-popup/payment-status-popup.component.ts index 9d908ddcaf..4fa6c4ced3 100644 --- a/interfaces/Portal/src/app/program/payment-status-popup/payment-status-popup.component.ts +++ b/interfaces/Portal/src/app/program/payment-status-popup/payment-status-popup.component.ts @@ -181,8 +181,10 @@ export class PaymentStatusPopupComponent implements OnInit { if (response) { const fspIntegrationType = getFspIntegrationType( - response.fspsInPayment || - this.program.financialServiceProviders.map((fsp) => fsp.fsp), + response.programFinancialServiceProviderConfigurationNames || + this.program.financialServiceProviderConfigurations.map( + (fspConfig) => fspConfig.name, + ), this.program, ); message += getPaymentResultText( diff --git a/interfaces/Portal/src/app/program/program-payout/program-payout.component.html b/interfaces/Portal/src/app/program/program-payout/program-payout.component.html index 1ae0483e59..64bcb94d7e 100644 --- a/interfaces/Portal/src/app/program/program-payout/program-payout.component.html +++ b/interfaces/Portal/src/app/program/program-payout/program-payout.component.html @@ -60,11 +60,10 @@

{{ 'page.program.program-payout.make-export-import' | translate }}

[programId]="programId" [payment]="exportPaymentId" [lastPaymentId]="lastPaymentId" - [hasFspWithReconciliation]="hasFspWithReconciliation" [paymentInProgress]="paymentInProgress" > fsp.integrationType === FspIntegrationType.csv, + this.program.financialServiceProviderConfigurations.some( + (fspConfig) => + fspConfig.financialServiceProvider.integrationType === + FspIntegrationType.csv, ); - this.hasFspWithReconciliation = this.program.financialServiceProviders.some( - (fsp) => fsp.hasReconciliation, - ); this.canMakePayment = this.checkCanMakePayment(); this.canViewPayment = this.checkCanViewPayment(); @@ -152,8 +150,8 @@ export class ProgramPayoutComponent implements OnInit { } private checkCanExportCardBalances(): boolean { - const visaFsp = this.program?.financialServiceProviders?.some((fsp) => - PaymentUtils.hasPhysicalCardSupport(fsp.fsp), + const visaFsp = this.program?.financialServiceProviderConfigurations?.some( + (fsp) => PaymentUtils.hasPhysicalCardSupport(fsp.name), ); const hasPermission = this.authService.hasAllPermissions(this.program.id, [ @@ -179,9 +177,10 @@ export class ProgramPayoutComponent implements OnInit { } async checkShowCbeValidation(): Promise { - const hasCbeProvider = this.program?.financialServiceProviders?.some( - (fsp) => fsp.fsp === FinancialServiceProviders.commercialBankEthiopia, - ); + const hasCbeProvider = + this.program?.financialServiceProviderConfigurations?.some( + (fsp) => fsp.name === FinancialServiceProviders.commercialBankEthiopia, + ); const hasPermission = await this.authService.hasPermission( this.program.id, Permission.PaymentFspInstructionREAD, @@ -334,10 +333,13 @@ export class ProgramPayoutComponent implements OnInit { } private checkProgramHasVoucherSupport( - fsps: Program['financialServiceProviders'], + fspConfigs: Program['financialServiceProviderConfigurations'], ): boolean { - for (const fsp of fsps || []) { - if (fsp && PaymentUtils.hasVoucherSupport(fsp.fsp)) { + for (const fspConfig of fspConfigs || []) { + if ( + fspConfig && + PaymentUtils.hasVoucherSupport(fspConfig.financialServiceProviderName) + ) { return true; } } diff --git a/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.html b/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.html index 21e576ff87..6cf87cba5a 100644 --- a/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.html +++ b/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.html @@ -267,9 +267,9 @@ diff --git a/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.ts b/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.ts index bff4912543..faefb4c7b8 100644 --- a/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.ts +++ b/interfaces/Portal/src/app/program/program-people-affected/program-people-affected.component.ts @@ -126,7 +126,7 @@ export class ProgramPeopleAffectedComponent implements OnDestroy { public canUpdateRegistrationAttributeFinancial: boolean; private canViewMessageHistory: boolean; private canUpdatePaData: boolean; - private canUpdatePaFsp: boolean; + private canUpdatePaProgramFspConfig: boolean; private canUpdatePersonalData: boolean; private canViewPaymentData: boolean; private canViewVouchers: boolean; @@ -317,9 +317,10 @@ export class ProgramPeopleAffectedComponent implements OnDestroy { this.canUpdatePaData = this.authService.hasAllPermissions(this.programId, [ Permission.RegistrationAttributeUPDATE, ]); - this.canUpdatePaFsp = this.authService.hasAllPermissions(this.programId, [ - Permission.RegistrationFspUPDATE, - ]); + this.canUpdatePaProgramFspConfig = this.authService.hasAllPermissions( + this.programId, + [Permission.RegistrationFspConfigUPDATE], + ); this.canViewPersonalData = this.authService.hasAllPermissions( this.programId, [Permission.RegistrationPersonalREAD], @@ -562,11 +563,9 @@ export class ProgramPeopleAffectedComponent implements OnDestroy { : '' }` : '', - fsp: person.financialServiceProvider, + fsp: person.financialServiceProviderName, financialServiceProvider: this.translatableStringService.get( - this.program?.financialServiceProviders?.find( - (p) => p.fsp === person?.financialServiceProvider, - )?.displayName, + person.programFinancialServiceProviderConfigurationLabel, ), lastMessageStatus: person.lastMessageStatus, hasNote: !!person.note, @@ -576,7 +575,7 @@ export class ProgramPeopleAffectedComponent implements OnDestroy { personRow = this.fillPaymentHistoryColumn(personRow); } - // Custom attributes can be personal data or not personal data + // Program registration attributes can be personal data or not personal data // for now only users that view custom data can see it if (this.canViewPersonalData) { personRow = this.fillPaTableAttributeRows(person, personRow); @@ -621,12 +620,9 @@ export class ProgramPeopleAffectedComponent implements OnDestroy { public showInclusionScore(): boolean { let show = false; - if (this.program?.programQuestions) { - for (const question of this.program.programQuestions) { - if ( - question['scoring'] && - Object.keys(question['scoring']).length > 0 - ) { + if (this.program?.programRegistrationAttributes) { + for (const attribute of this.program.programRegistrationAttributes) { + if (attribute.scoring && Object.keys(attribute.scoring).length > 0) { show = true; break; } @@ -646,7 +642,7 @@ export class ProgramPeopleAffectedComponent implements OnDestroy { canUpdateRegistrationAttributeFinancial: this.canUpdateRegistrationAttributeFinancial, canUpdatePersonalData: this.canUpdatePersonalData, - canUpdatePaFsp: this.canUpdatePaFsp, + canUpdatePaProgramFspConfig: this.canUpdatePaProgramFspConfig, canViewMessageHistory: this.canViewMessageHistory, canViewPaymentData: this.canViewPaymentData, }, diff --git a/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.html b/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.html index 9cbc0aebca..e43da69367 100644 --- a/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.html +++ b/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.html @@ -30,7 +30,8 @@ activity.paymentRowDetail.transaction && !hasErrorCheck(activity.paymentRowDetail) && hasVoucherSupportCheck( - activity.paymentRowDetail.transaction.fsp + activity.paymentRowDetail.transaction + .financialServiceProviderName ) " shape="round" @@ -136,7 +137,10 @@ id="hover-trigger" *ngIf="activity.paymentRowDetail.transaction?.status" > - {{ activity.paymentRowDetail.transaction?.fspName }} + {{ + activity.paymentRowDetail.transaction + ?.programFinancialServiceProviderConfigurationTranslatedLabel + }}
diff --git a/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.ts b/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.ts index 3d8c5a0924..6217db0378 100644 --- a/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.ts +++ b/interfaces/Portal/src/app/program/registration-activity-detail-accordion/registration-activity-detail-accordion.component.ts @@ -92,7 +92,9 @@ export class RegistrationActivityDetailAccordionComponent implements OnInit { ); if ( - !PaymentUtils.hasVoucherSupport(paymentRow.fsp) && + !PaymentUtils.hasVoucherSupport( + paymentRow.financialServiceProviderName, + ) && !hasError && !isSinglePayment ) { @@ -114,7 +116,7 @@ export class RegistrationActivityDetailAccordionComponent implements OnInit { if ( this.canViewVouchers && - PaymentUtils.hasVoucherSupport(paymentRow.fsp) && + PaymentUtils.hasVoucherSupport(paymentRow.financialServiceProviderName) && !!paymentRow.transaction ) { await this.programsService diff --git a/interfaces/Portal/src/app/services/past-payments.service.ts b/interfaces/Portal/src/app/services/past-payments.service.ts index 3e02dc541e..a54b55fe98 100644 --- a/interfaces/Portal/src/app/services/past-payments.service.ts +++ b/interfaces/Portal/src/app/services/past-payments.service.ts @@ -124,7 +124,10 @@ export class PastPaymentsService { ); paymentRowValue.status = StatusEnum.notYetSent; } else { - transaction.fspName = this.translatableString.get(transaction.fspName); + transaction.programFinancialServiceProviderConfigurationTranslatedLabel = + this.translatableString.get( + transaction.programFinancialServiceProviderConfigurationLabel, + ); paymentRowValue = PaymentUtils.getPaymentRowInfo( transaction, program, diff --git a/interfaces/Portal/src/app/services/programs-service-api.service.ts b/interfaces/Portal/src/app/services/programs-service-api.service.ts index ef25847a8c..5caa76f021 100644 --- a/interfaces/Portal/src/app/services/programs-service-api.service.ts +++ b/interfaces/Portal/src/app/services/programs-service-api.service.ts @@ -1,3 +1,4 @@ +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { saveAs } from 'file-saver'; @@ -13,7 +14,7 @@ import RegistrationStatus from '../enums/registration-status.enum'; import { ActionType, LatestAction } from '../models/actions.model'; import { Event } from '../models/event.model'; import { ExportType } from '../models/export-type.model'; -import { Fsp } from '../models/fsp.model'; +import { FinancialServiceProviderConfiguration } from '../models/fsp.model'; import { ImportType } from '../models/import-type.enum'; import { Wallet } from '../models/intersolve-visa-wallet.model'; import { Message, MessageTemplate } from '../models/message.model'; @@ -127,18 +128,14 @@ export class ProgramsServiceApiService { getPaTableAttributes( programId: number | string, options?: { - includeCustomAttributes?: boolean; - includeProgramQuestions?: boolean; - includeFspQuestions?: boolean; + includeProgramRegistrationAttributes?: boolean; includeTemplateDefaultAttributes?: boolean; filterShowInPeopleAffectedTable?: boolean; }, ): Promise { let params = new HttpParams(); const defaultOptions = { - includeCustomAttributes: true, - includeProgramQuestions: true, - includeFspQuestions: true, + includeProgramRegistrationAttributes: true, includeTemplateDefaultAttributes: false, filterShowInPeopleAffectedTable: true, }; @@ -304,7 +301,7 @@ export class ProgramsServiceApiService { ): Promise { const downloadData: string[] = await this.apiService.get( environment.url_121_service_api, - `/programs/${programId}/registrations/import-template`, + `/programs/${programId}/registrations/import/template`, false, ); @@ -320,7 +317,7 @@ export class ProgramsServiceApiService { import(programId: number, file: File): Promise { const formData = new FormData(); formData.append('file', file); - const path = `/programs/${programId}/registrations/import-registrations`; + const path = `/programs/${programId}/registrations/import`; return new Promise((resolve, reject) => { this.apiService @@ -348,17 +345,20 @@ export class ProgramsServiceApiService { programId: number, type: ImportType, ): Promise { - const downloadData: string[] = await this.apiService.get( - environment.url_121_service_api, - `/programs/${programId}/payments/fsp-reconciliation/import-template`, - ); + const templates: { name: string; template: string[] }[] = + await this.apiService.get( + environment.url_121_service_api, + `/programs/${programId}/payments/fsp-reconciliation/import-template`, + ); - const csvContents = downloadData.join(';') + '\r\n'; + for (const template of templates) { + const csvContents = template.template.join(';') + '\r\n'; - saveAs( - new Blob([csvContents], { type: 'text/csv' }), - `${type}-TEMPLATE.csv`, - ); + saveAs( + new Blob([csvContents], { type: 'text/csv' }), + `${type}-${template.name}-TEMPLATE.csv`, + ); + } return; } @@ -809,23 +809,12 @@ export class ProgramsServiceApiService { ); } - getFspById(fspId: number): Promise { + getFspByName( + fspName: FinancialServiceProviders, + ): Promise { return this.apiService.get( environment.url_121_service_api, - '/financial-service-providers/' + fspId, - ); - } - - updateChosenFsp( - referenceId: string, - programId: number, - newFspName: string, - newFspAttributes?: object, - ): Promise { - return this.apiService.put( - environment.url_121_service_api, - `/programs/${programId}/registrations/${referenceId}/fsp`, - { newFspName, newFspAttributes }, + '/financial-service-providers/' + fspName, ); } @@ -839,17 +828,8 @@ export class ProgramsServiceApiService { async getDuplicateCheckAttributes(programId: number): Promise { const program = await this.getProgramById(programId); - const fspAttributes = program.financialServiceProviders - .filter((fsp) => !!fsp.questions) - .map((fsp) => fsp.questions) - .flat(); - const attributeNames: string[] = [] - .concat( - program.programQuestions, - program.programCustomAttributes, - fspAttributes, - ) + .concat(program.programRegistrationAttributes) .filter((attribute) => attribute.duplicateCheck === true) .map((attribute) => attribute.name); diff --git a/interfaces/Portal/src/app/services/table.service.ts b/interfaces/Portal/src/app/services/table.service.ts index c161871634..4eedcae630 100644 --- a/interfaces/Portal/src/app/services/table.service.ts +++ b/interfaces/Portal/src/app/services/table.service.ts @@ -3,12 +3,12 @@ import { Platform } from '@ionic/angular'; import { TranslateService } from '@ngx-translate/core'; import { AuthService } from '../auth/auth.service'; import Permission from '../auth/permission.enum'; -import { AnswerType } from '../models/fsp.model'; import { PersonDefaultAttributes, PersonTableColumn, } from '../models/person.model'; import { Program } from '../models/program.model'; +import { RegistrationAttributeType } from '../models/registration-attribute.model'; import { TranslatableString } from '../models/translatable-string.model'; import { ProgramsServiceApiService } from './programs-service-api.service'; import { TranslatableStringService } from './translatable-string.service'; @@ -18,14 +18,14 @@ import { TranslatableStringService } from './translatable-string.service'; }) export class TableService { private columnWidthPerType = { - [AnswerType.Number]: 90, - [AnswerType.Date]: 180, - [AnswerType.PhoneNumber]: 130, - [AnswerType.Text]: 150, - [AnswerType.Enum]: 160, - [AnswerType.Email]: 180, - [AnswerType.Boolean]: 90, - [AnswerType.MultiSelect]: 180, + [RegistrationAttributeType.Number]: 90, + [RegistrationAttributeType.Date]: 180, + [RegistrationAttributeType.PhoneNumber]: 130, + [RegistrationAttributeType.Text]: 150, + [RegistrationAttributeType.Enum]: 160, + [RegistrationAttributeType.Email]: 180, + [RegistrationAttributeType.Boolean]: 90, + [RegistrationAttributeType.MultiSelect]: 180, }; private columnsWithSpecialFormatting = [ @@ -73,8 +73,9 @@ export class TableService { ...this.getColumnDefaults(), frozenLeft: this.platform.width() > 1280, permissions: [Permission.RegistrationPersonalREAD], - minWidth: this.columnWidthPerType[AnswerType.PhoneNumber], - width: this.columnWidthPerType[AnswerType.PhoneNumber], + minWidth: + this.columnWidthPerType[RegistrationAttributeType.PhoneNumber], + width: this.columnWidthPerType[RegistrationAttributeType.PhoneNumber], }, { prop: 'preferredLanguage', @@ -84,8 +85,8 @@ export class TableService { ...this.getColumnDefaults(), sortable: false, // TODO: disabled, because sorting in the backend is does on values (nl/en) instead of frontend labels (Dutch/English) permissions: [Permission.RegistrationPersonalREAD], - minWidth: this.columnWidthPerType[AnswerType.Text], - width: this.columnWidthPerType[AnswerType.Text], + minWidth: this.columnWidthPerType[RegistrationAttributeType.Text], + width: this.columnWidthPerType[RegistrationAttributeType.Text], }, { prop: 'status', @@ -103,8 +104,8 @@ export class TableService { 'page.program.program-people-affected.column.registrationCreated', ), ...this.getColumnDefaults(), - minWidth: this.columnWidthPerType[AnswerType.Date], - width: this.columnWidthPerType[AnswerType.Date], + minWidth: this.columnWidthPerType[RegistrationAttributeType.Date], + width: this.columnWidthPerType[RegistrationAttributeType.Date], }, { prop: 'paymentAmountMultiplier', @@ -113,8 +114,8 @@ export class TableService { ), ...this.getColumnDefaults(), comparator: this.paComparator.bind(this), - minWidth: this.columnWidthPerType[AnswerType.Number], - width: this.columnWidthPerType[AnswerType.Number], + minWidth: this.columnWidthPerType[RegistrationAttributeType.Number], + width: this.columnWidthPerType[RegistrationAttributeType.Number], }, { prop: 'maxPayments', @@ -128,7 +129,7 @@ export class TableService { { prop: 'financialServiceProvider', name: this.translate.instant( - 'page.program.program-people-affected.column.fspDisplayName', + 'page.program.program-people-affected.column.programFinancialServiceProviderConfigurationLabel', ), ...this.getColumnDefaults(), minWidth: 220, @@ -227,23 +228,21 @@ export class TableService { if (canViewPersonalData) { for (const nameColumn of program.fullnameNamingConvention) { - const searchableColumns = [ - ...program.programQuestions, - ...program.programCustomAttributes, - ]; + const searchableColumns = program.programRegistrationAttributes; - const nameQuestion = searchableColumns.find( - (question) => question.name === nameColumn, + const nameAttribute = searchableColumns.find( + (attribute) => attribute.name === nameColumn, ); - if (nameQuestion) { + if (nameAttribute) { const addCol = { prop: nameColumn, - name: this.translatableStringService.get(nameQuestion.label), + name: this.translatableStringService.get(nameAttribute.label), ...this.getColumnDefaults(), frozenLeft: this.platform.width() > 768, permissions: [Permission.RegistrationPersonalREAD], - minWidth: this.getColumnWidthPerType()[AnswerType.Text], - width: this.getColumnWidthPerType()[AnswerType.Text], + minWidth: + this.getColumnWidthPerType()[RegistrationAttributeType.Text], + width: this.getColumnWidthPerType()[RegistrationAttributeType.Text], }; columns.push(addCol); } diff --git a/interfaces/Portal/src/app/shared/message-editor/message-editor.component.ts b/interfaces/Portal/src/app/shared/message-editor/message-editor.component.ts index cd83abf3e0..71d45f4441 100644 --- a/interfaces/Portal/src/app/shared/message-editor/message-editor.component.ts +++ b/interfaces/Portal/src/app/shared/message-editor/message-editor.component.ts @@ -120,7 +120,6 @@ export class MessageEditorComponent implements AfterViewInit, OnInit { this.attributes = await this.programsService.getPaTableAttributes( this.inputProps.programId, { - includeFspQuestions: false, includeTemplateDefaultAttributes: true, filterShowInPeopleAffectedTable: false, }, diff --git a/interfaces/Portal/src/app/shared/payment-result.ts b/interfaces/Portal/src/app/shared/payment-result.ts index f4eb5536d7..e802e9f9e7 100644 --- a/interfaces/Portal/src/app/shared/payment-result.ts +++ b/interfaces/Portal/src/app/shared/payment-result.ts @@ -1,21 +1,25 @@ import { TranslateService } from '@ngx-translate/core'; -import FspName from '../enums/fsp-name.enum'; import { FspIntegrationType } from '../models/fsp.model'; import { Program } from '../models/program.model'; export function getFspIntegrationType( - fspsInPayment: FspName[], + fspConfigNamesInPayment: string[], program: Program, ) { // In case of multiple FSPs default integrationType to API, but overwrite if any FSP has a different integration type // This variable is only used to return different UX copy on doPayment result let fspIntegrationType = FspIntegrationType.api; - for (const fsp of fspsInPayment) { - const programFsp = program.financialServiceProviders.find( - (f) => f.fsp === fsp, - ); - if (programFsp.integrationType === FspIntegrationType.csv) { - fspIntegrationType = programFsp.integrationType; + for (const fspConfigName of fspConfigNamesInPayment) { + const programFspConfig = + program.financialServiceProviderConfigurations.find( + (f) => f.name === fspConfigName, + ); + if ( + programFspConfig.financialServiceProvider.integrationType === + FspIntegrationType.csv + ) { + fspIntegrationType = + programFspConfig.financialServiceProvider.integrationType; return fspIntegrationType; } } diff --git a/interfaces/Portal/src/app/shared/payment.utils.ts b/interfaces/Portal/src/app/shared/payment.utils.ts index 25c8f3742b..d8e3900e35 100644 --- a/interfaces/Portal/src/app/shared/payment.utils.ts +++ b/interfaces/Portal/src/app/shared/payment.utils.ts @@ -24,7 +24,9 @@ export class PaymentUtils { transaction, amount: transaction.amount, currency: program?.currency, - fsp: transaction.fsp as FspName, + financialServiceProviderName: transaction.financialServiceProviderName, + programFinancialServiceProviderConfigurationTranslatedLabel: + transaction.programFinancialServiceProviderConfigurationTranslatedLabel, sentDate: transaction.paymentDate, paymentDate: transaction.paymentDate, }; diff --git a/interfaces/Portal/src/app/shared/utils/check-attribute-input.utils.ts b/interfaces/Portal/src/app/shared/utils/check-attribute-input.utils.ts index 2532a1e767..65ff2f37fc 100644 --- a/interfaces/Portal/src/app/shared/utils/check-attribute-input.utils.ts +++ b/interfaces/Portal/src/app/shared/utils/check-attribute-input.utils.ts @@ -1,12 +1,20 @@ -import { AnswerType } from '../../models/fsp.model'; +import { RegistrationAttributeType } from '../../models/registration-attribute.model'; export class CheckAttributeInputUtils { static isAttributeCorrectlyFilled( - type: AnswerType, + type: RegistrationAttributeType, pattern: string, value: string, + isRequired: boolean, ): boolean { - if (type === AnswerType.Text) { + if (value == null || value === '') { + if (isRequired) { + return false; + } + return true; + } + + if (type === RegistrationAttributeType.Text) { if (pattern) { if (new RegExp(pattern).test(value || '')) { // text with pattern, and matched: correct @@ -17,7 +25,7 @@ export class CheckAttributeInputUtils { } // text without pattern: correct return true; - } else if (type === AnswerType.Number) { + } else if (type === RegistrationAttributeType.Number) { if (value) { // number filled: correct return true; diff --git a/interfaces/Portal/src/assets/i18n/ar.json b/interfaces/Portal/src/assets/i18n/ar.json index eb15b430b8..38906f5609 100644 --- a/interfaces/Portal/src/assets/i18n/ar.json +++ b/interfaces/Portal/src/assets/i18n/ar.json @@ -157,7 +157,6 @@ "btn-text": "تعليمات دفع الصادرات", "confirm-message": "سيؤدي هذا إلى تنزيل ملف يحتوي على تعليمات الدفع لمزود الخدمة المالية.", "sub-message": { - "no-reconciliation": "يجب عليك استخدام هذا الملف مرة واحدة فقط لكل دفعة في بوابة مزود الخدمة المالية.", "reconciliation": "سيتضمن هذا التصدير فقط الأشخاص المتأثرين الذين تنتظرهم حالة المعاملة الحالية." }, "timestamp": "آخر تصدير تم في: {{dateTime}}" @@ -362,11 +361,8 @@ }, "choose-action": "اختر الإجراء...", "column": { - "custom-attribute-false": "لا", - "custom-attribute-true": "نعم", "dob": "تاريخ الميلاد", "failedPayment": "فشل الدفع #", - "fspDisplayName": "مزود الخدمات المالية", "hasNote": "لديه ملاحظة", "inclusionScore": "درجة التضمين", "lastMessageStatus": "آخر رسالة", @@ -381,7 +377,10 @@ "personAffectedSequence": "الشخص المتضرر", "phoneNumber": "رقم الهاتف", "preferredLanguage": "اللغة المفضلة", + "programFinancialServiceProviderConfigurationLabel": "مزود الخدمات المالية", "referenceId": "الرقم المرجعي", + "registration-attribute-false": "لا", + "registration-attribute-true": "نعم", "registrationCreated": "تم إنشاء التسجيل", "registrationCreatedDate": "تاريخ إنشاء التسجيل", "select": "اختار", @@ -395,8 +394,6 @@ "error-alert": { "invalid-date": "تنسيق التاريخ الذي أدخلته غير صالح. مثال تنسيق التاريخ الصالح هو 31-12-1970 والذي يتبع اليوم والشهر والسنة ، DD-MM-YYYY" }, - "fspChangeWarning": "سيؤدي تحديد FSP هذا إلى حذف السمات التالية المتعلقة ب FSP القديم:", - "fspNewAttributesExplanation": "بالنسبة ل FSP هذا ، تكون السمات التالية مطلوبة:", "has-notes-title": "الملاحظات المتاحة", "popup-title": "{{pa}} معلومات مفصلة", "properties": { diff --git a/interfaces/Portal/src/assets/i18n/en.json b/interfaces/Portal/src/assets/i18n/en.json index 142ac84361..c1efd3a956 100644 --- a/interfaces/Portal/src/assets/i18n/en.json +++ b/interfaces/Portal/src/assets/i18n/en.json @@ -174,7 +174,6 @@ "btn-text": "Export Payment Instructions", "confirm-message": "This will download a file with payment instructions for the Financial Service Provider.", "sub-message": { - "no-reconciliation": "You should only use this file once per payment in the portal of the Financial Service Provider.", "reconciliation": "This export will only include People Affected for which the current transaction status is waiting." }, "timestamp": "Latest export has been done on: {{dateTime}}" @@ -341,12 +340,9 @@ }, "choose-action": "Choose action...", "column": { - "custom-attribute-false": "No", - "custom-attribute-true": "Yes", "dob": "Date of birth", "failedPayment": "Failed for payment #", "financialServiceProvider": "Financial Service Provider", - "fspDisplayName": "Financial Service Provider", "hasNote": "Has Note", "inclusionScore": "Inclusion score", "lastMessageStatus": "Last message", @@ -361,7 +357,10 @@ "personAffectedSequence": "Person Affected", "phoneNumber": "Phonenumber", "preferredLanguage": "Preferred language", + "programFinancialServiceProviderConfigurationLabel": "Financial Service Provider", "referenceId": "Reference ID", + "registration-attribute-false": "No", + "registration-attribute-true": "Yes", "registrationCreated": "Registration created", "registrationCreatedDate": "Registration created date", "select": "Select", @@ -375,8 +374,6 @@ "error-alert": { "invalid-date": "The date format you inputted is not valid. A valid date format example is 31-12-1970 which follows day-month-year, DD-MM-YYYY" }, - "fspChangeWarning": "Selecting this FSP will delete the following attributes related to the old FSP:", - "fspNewAttributesExplanation": "For this FSP the following attributes are required:", "has-notes-title": "Notes available", "popup-title": "{{pa}} detailed info", "properties": { diff --git a/interfaces/Portal/src/assets/i18n/es.json b/interfaces/Portal/src/assets/i18n/es.json index bea30c5ff9..0f3c8cc558 100644 --- a/interfaces/Portal/src/assets/i18n/es.json +++ b/interfaces/Portal/src/assets/i18n/es.json @@ -157,7 +157,6 @@ "btn-text": "Instrucciones de pago de exportación", "confirm-message": "Esto descargará un archivo con instrucciones de pago para el Proveedor de Servicios Financieros.", "sub-message": { - "no-reconciliation": "Solo debe utilizar este archivo una vez por pago en el portal del Proveedor de Servicios Financieros.", "reconciliation": "Esta exportación solo incluirá a las personas afectadas para las que se esté esperando el estado actual de la transacción." }, "timestamp": "La última exportación se ha realizado el: {{dateTime}}" @@ -362,11 +361,8 @@ }, "choose-action": "Elige acción...", "column": { - "custom-attribute-false": "No", - "custom-attribute-true": "Sí", "dob": "Fecha de nacimiento", "failedPayment": "Error en el pago #", - "fspDisplayName": "Proveedor de servicios financieros", "hasNote": "Tiene nota", "inclusionScore": "Puntuación de inclusión", "lastMessageStatus": "Último mensaje", @@ -381,7 +377,10 @@ "personAffectedSequence": "Persona afectada", "phoneNumber": "Número de teléfono", "preferredLanguage": "Idioma preferido", + "programFinancialServiceProviderConfigurationLabel": "Proveedor de servicios financieros", "referenceId": "ID de referencia", + "registration-attribute-false": "No", + "registration-attribute-true": "Sí", "registrationCreated": "Registro creado", "registrationCreatedDate": "Fecha de creación de la inscripción", "select": "Escoger", @@ -395,8 +394,6 @@ "error-alert": { "invalid-date": "El formato de fecha que ingresó no es válido. Un ejemplo de formato de fecha válido es 31-12-1970 que sigue día-mes-año, DD-MM-AAAA" }, - "fspChangeWarning": "Al seleccionar este FSP, se eliminarán los siguientes atributos relacionados con el FSP anterior:", - "fspNewAttributesExplanation": "Para este FSP se requieren los siguientes atributos:", "has-notes-title": "Notas disponibles", "popup-title": "{{pa}} información detallada", "properties": { diff --git a/interfaces/Portal/src/assets/i18n/fr.json b/interfaces/Portal/src/assets/i18n/fr.json index 4535560e02..03b73615d6 100644 --- a/interfaces/Portal/src/assets/i18n/fr.json +++ b/interfaces/Portal/src/assets/i18n/fr.json @@ -178,7 +178,6 @@ "btn-text": "Exporter les instructions de paiement", "confirm-message": "Cela téléchargera un fichier contenant les instructions de paiement pour le prestataire de services financiers.", "sub-message": { - "no-reconciliation": "Vous ne devez utiliser ce fichier qu’une seule fois par paiement sur le portail du prestataire de services financiers.", "reconciliation": "Cet export n’inclura que les Personnes Affectées pour lesquelles le statut actuel de la transaction est en attente." }, "timestamp": "Dernier export effectué le : {{dateTime}}" @@ -372,12 +371,9 @@ }, "choose-action": "Sélectionner une action...", "column": { - "custom-attribute-false": "Non", - "custom-attribute-true": "Oui", "dob": "Date de naissance", "failedPayment": "Échec du paiement #", "financialServiceProvider": "Prestataire de Services Financiers", - "fspDisplayName": "Prestataire de Services Financiers", "hasNote": "A indiqué", "inclusionScore": "Score d’inclusion", "lastMessageStatus": "Dernier message", @@ -392,7 +388,10 @@ "personAffectedSequence": "Personne Affectée", "phoneNumber": "Numéro de téléphone", "preferredLanguage": "Langue préférée", + "programFinancialServiceProviderConfigurationLabel": "Prestataire de Services Financiers", "referenceId": "Identifiant de référence", + "registration-attribute-false": "Non", + "registration-attribute-true": "Oui", "registrationCreated": "Création d’un enregistrement", "registrationCreatedDate": "Date de création de l'enregistrement", "select": "Sélectionner", @@ -406,8 +405,6 @@ "error-alert": { "invalid-date": "Le format de date que vous avez saisi n’est pas valide. Un exemple de format de date valide est 31-12-1970 qui suit jour-mois-année, JJ-MM-AAAA" }, - "fspChangeWarning": "La sélection de ce PSF supprimera les attributs suivants liés à l’ancien PSF :", - "fspNewAttributesExplanation": "Pour ce PSF, les attributs suivants sont requis :", "has-notes-title": "Notes disponibles", "popup-title": "{{pa}} info détaillée", "properties": { diff --git a/interfaces/Portal/src/assets/i18n/nl.json b/interfaces/Portal/src/assets/i18n/nl.json index 3ef50e7f48..2d5d42df0c 100644 --- a/interfaces/Portal/src/assets/i18n/nl.json +++ b/interfaces/Portal/src/assets/i18n/nl.json @@ -165,7 +165,6 @@ "btn-text": "Betaalinstructies exporteren", "confirm-message": "Hiermee downloadt u een bestand met betalingsinstructies voor de financiële dienstverlener.", "sub-message": { - "no-reconciliation": "U dient dit bestand slechts één keer per betaling te gebruiken in het portaal van de Financiële Dienstverlener.", "reconciliation": "Deze export omvat alleen hulpvragers waarvoor de huidige transactiestatus wachtend is." }, "timestamp": "Laatste export is uitgevoerd op: {{dateTime}}" @@ -376,12 +375,9 @@ }, "choose-action": "Kies actie...", "column": { - "custom-attribute-false": "Nee", - "custom-attribute-true": "Ja", "dob": "Geboortedatum", "failedPayment": "Mislukt bij betaling #", "financialServiceProvider": "Type hulp", - "fspDisplayName": "Type hulp", "hasNote": "Heeft notitie", "inclusionScore": "Inclusiescore", "lastMessageStatus": "Laatste bericht", @@ -396,7 +392,10 @@ "personAffectedSequence": "Hulpvrager", "phoneNumber": "Telefoonnummer", "preferredLanguage": "Voorkeurstaal", + "programFinancialServiceProviderConfigurationLabel": "Type hulp", "referenceId": "Referentie nummer", + "registration-attribute-false": "Nee", + "registration-attribute-true": "Ja", "registrationCreated": "Aanmaakdatum", "registrationCreatedDate": "Aanmaakdatum registratie", "select": "Selecteer", @@ -410,8 +409,6 @@ "error-alert": { "invalid-date": "De datumnotatie die u hebt ingevoerd, is niet geldig. Een geldig voorbeeld van een datumnotatie is 31-12-1970. Dus: dag-maand-jaar, DD-MM-JJJJ" }, - "fspChangeWarning": "Als u deze FSP selecteert, wordt de volgende informatie verwijderd die verband houden met de oude FSP:", - "fspNewAttributesExplanation": "Voor deze FSP is de volgende informatie vereist:", "has-notes-title": "Notities beschikbaar", "popup-title": "{{pa}} meer info", "properties": { diff --git a/interfaces/Portalicious/src/app/components/page-layout/components/registration-menu/registration-menu.component.ts b/interfaces/Portalicious/src/app/components/page-layout/components/registration-menu/registration-menu.component.ts index e07df945e9..0f6771d282 100644 --- a/interfaces/Portalicious/src/app/components/page-layout/components/registration-menu/registration-menu.component.ts +++ b/interfaces/Portalicious/src/app/components/page-layout/components/registration-menu/registration-menu.component.ts @@ -53,7 +53,7 @@ export class RegistrationMenuComponent { routerLink: `/${AppRoutes.project}/${this.projectId().toString()}/${AppRoutes.projectRegistrations}/${this.registrationId().toString()}/${AppRoutes.projectRegistrationDebitCards}`, icon: 'pi pi-credit-card', visible: - this.registration.data()?.financialServiceProvider === + this.registration.data()?.financialServiceProviderName === FinancialServiceProviders.intersolveVisa, }, ]); diff --git a/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts b/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts index ea27c1c890..63922d6ac8 100644 --- a/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts +++ b/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts @@ -134,14 +134,15 @@ export class RegistrationsTableComponent { getChipDataByRegistrationStatus(registration.status), }, { - field: 'financialServiceProvider', + field: 'programFinancialServiceProviderConfigurationName', header: $localize`FSP`, type: QueryTableColumnType.MULTISELECT, - options: this.project.data().financialServiceProviders.map((fsp) => ({ - label: - this.translatableStringService.translate(fsp.displayName) ?? '', - value: fsp.fsp, - })), + options: this.project + .data() + .programFinancialServiceProviderConfigurations.map((config) => ({ + label: this.translatableStringService.translate(config.label) ?? '', + value: config.name, + })), }, { field: 'registrationCreated', diff --git a/interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.api.service.ts b/interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.api.service.ts new file mode 100644 index 0000000000..c8d2c80091 --- /dev/null +++ b/interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.api.service.ts @@ -0,0 +1,21 @@ +import { Injectable, Signal } from '@angular/core'; + +import { DomainApiService } from '~/domains/domain-api.service'; +import { FinancialServiceProviderConfiguration } from '~/domains/financial-service-provider-configuration/financial-service-provider-configuration.model'; + +const BASE_ENDPOINT = (projectId: Signal) => [ + 'programs', + projectId, + 'financial-service-provider-configurations', +]; + +@Injectable({ + providedIn: 'root', +}) +export class FinancialServiceProviderConfigurationApiService extends DomainApiService { + getFinancialServiceProviderConfigurations(projectId: Signal) { + return this.generateQueryOptions({ + path: BASE_ENDPOINT(projectId), + }); + } +} diff --git a/interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.model.ts b/interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.model.ts new file mode 100644 index 0000000000..4560c497de --- /dev/null +++ b/interfaces/Portalicious/src/app/domains/financial-service-provider-configuration/financial-service-provider-configuration.model.ts @@ -0,0 +1,6 @@ +import { ProgramFinancialServiceProviderConfigurationResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto'; + +import { Dto } from '~/utils/dto-type'; + +export type FinancialServiceProviderConfiguration = + Dto; diff --git a/interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.helper.ts b/interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.helper.ts new file mode 100644 index 0000000000..3ecbe27d7f --- /dev/null +++ b/interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.helper.ts @@ -0,0 +1,8 @@ +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; + +export function getFinancialServiceProviderSettingByName( + name: FinancialServiceProviders, +) { + return FINANCIAL_SERVICE_PROVIDER_SETTINGS.find((fsp) => fsp.name === name); +} diff --git a/interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.model.ts b/interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.model.ts index 314cc9e00e..7284196a36 100644 --- a/interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.model.ts +++ b/interfaces/Portalicious/src/app/domains/financial-service-provider/financial-service-provider.model.ts @@ -1,5 +1,5 @@ -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; +import { FinancialServiceProviderDto } from '@121-service/src/financial-service-providers/financial-service-provider.dto'; import { Dto } from '~/utils/dto-type'; -export type FinancialServiceProvider = Dto; +export type FinancialServiceProvider = Dto; diff --git a/interfaces/Portalicious/src/app/domains/project/project.api.service.ts b/interfaces/Portalicious/src/app/domains/project/project.api.service.ts index 5e3576ef43..504128e6d9 100644 --- a/interfaces/Portalicious/src/app/domains/project/project.api.service.ts +++ b/interfaces/Portalicious/src/app/domains/project/project.api.service.ts @@ -79,16 +79,12 @@ export class ProjectApiService extends DomainApiService { getProjectAttributes({ projectId, - includeCustomAttributes = false, - includeProgramQuestions = false, - includeFspQuestions = false, + includeProgramRegistrationAttributes = false, includeTemplateDefaultAttributes = false, filterShowInPeopleAffectedTable = false, }: { projectId: Signal; - includeCustomAttributes?: boolean; - includeProgramQuestions?: boolean; - includeFspQuestions?: boolean; + includeProgramRegistrationAttributes?: boolean; includeTemplateDefaultAttributes?: boolean; filterShowInPeopleAffectedTable?: boolean; }) { @@ -99,9 +95,7 @@ export class ProjectApiService extends DomainApiService { path: [BASE_ENDPOINT, projectId, 'attributes'], requestOptions: { params: { - includeCustomAttributes, - includeProgramQuestions, - includeFspQuestions, + includeProgramRegistrationAttributes, includeTemplateDefaultAttributes, filterShowInPeopleAffectedTable, }, diff --git a/interfaces/Portalicious/src/app/domains/project/project.helper.ts b/interfaces/Portalicious/src/app/domains/project/project.helper.ts index a96ba5cdc9..9d98d7b294 100644 --- a/interfaces/Portalicious/src/app/domains/project/project.helper.ts +++ b/interfaces/Portalicious/src/app/domains/project/project.helper.ts @@ -1,12 +1,9 @@ -import { FinancialServiceProviderIntegrationType } from '@121-service/src/financial-service-providers/enum/financial-service-provider-integration-type.enum'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { - AnswerTypes, - CustomAttributeType, -} from '@121-service/src/registration/enum/custom-data-attributes'; +import { FinancialServiceProviderIntegrationType } from '@121-service/src/financial-service-providers/financial-service-provider-integration-type.enum'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; import { DataListItem } from '~/components/data-list/data-list.component'; +import { getFinancialServiceProviderSettingByName } from '~/domains/financial-service-provider/financial-service-provider.helper'; import { FSPS_WITH_PHYSICAL_CARD_SUPPORT, FSPS_WITH_VOUCHER_SUPPORT, @@ -29,43 +26,41 @@ export const attributeToDataListItem = ( ): DataListItem | undefined => { const label = attribute.label; - // TODO: AB#30519 avoid using "as" here - switch (attribute.type as AnswerTypes | CustomAttributeType) { - case AnswerTypes.multiSelect: + switch (attribute.type) { + case RegistrationAttributeTypes.multiSelect: // TODO: Implement multiSelect when necessary console.log( 'attributeToDataListItem: multiSelect not implemented', value, ); return undefined; - case AnswerTypes.numeric: + case RegistrationAttributeTypes.numeric: return { label, type: 'number', value: value as number, }; - case AnswerTypes.numericNullable: + case RegistrationAttributeTypes.numericNullable: return { label, type: 'number', value: value as null | number, }; - case AnswerTypes.date: + case RegistrationAttributeTypes.date: return { label, type: 'date', value: value as Date, }; - case CustomAttributeType.boolean: + case RegistrationAttributeTypes.boolean: return { label, type: 'boolean', value: value as boolean, }; - case AnswerTypes.dropdown: - case AnswerTypes.tel: - case AnswerTypes.text: - case CustomAttributeType.text: + case RegistrationAttributeTypes.dropdown: + case RegistrationAttributeTypes.tel: + case RegistrationAttributeTypes.text: return { label, type: 'text', @@ -74,32 +69,54 @@ export const attributeToDataListItem = ( } }; -// TODO: AB#31594 all of the below helpers should be changed in the refactor registration data branch -// to use "fsp configurations" instead of "financial service providers" - export function projectHasVoucherSupport(project?: Project) { - return project?.financialServiceProviders.some((fsp) => - FSPS_WITH_VOUCHER_SUPPORT.includes(fsp.fsp), + return project?.programFinancialServiceProviderConfigurations.some((fsp) => + FSPS_WITH_VOUCHER_SUPPORT.includes(fsp.financialServiceProviderName), ); } export function projectHasPhysicalCardSupport(project?: Project) { - return project?.financialServiceProviders.some((fsp) => - FSPS_WITH_PHYSICAL_CARD_SUPPORT.includes(fsp.fsp), + return project?.programFinancialServiceProviderConfigurations.some((fsp) => + FSPS_WITH_PHYSICAL_CARD_SUPPORT.includes(fsp.financialServiceProviderName), ); } export function projectHasFspWithExportFileIntegration(project?: Project) { - return project?.financialServiceProviders.some( + return project?.programFinancialServiceProviderConfigurations.some( (fsp) => - fsp.integrationType === FinancialServiceProviderIntegrationType.csv, + getFinancialServiceProviderSettingByName(fsp.financialServiceProviderName) + ?.integrationType === FinancialServiceProviderIntegrationType.csv, ); } -export function fspsHaveExcelFsp(fsps: FinancialServiceProviders[]) { - return fsps.some((fsp) => fsp === FinancialServiceProviders.excel); -} +export function financialServiceProviderConfigurationNamesHaveIntegrationType({ + project, + financialServiceProviderConfigurationNames, + integrationType, +}: { + project: Project; + financialServiceProviderConfigurationNames: string[]; + integrationType: FinancialServiceProviderIntegrationType; +}) { + const fspSettings = financialServiceProviderConfigurationNames.map( + (financialServiceProviderConfigurationName) => { + const config = project.programFinancialServiceProviderConfigurations.find( + (fsp) => fsp.name === financialServiceProviderConfigurationName, + ); + + if (!config) { + throw new Error( + `Could not find financial service provider configuration with name ${financialServiceProviderConfigurationName}`, + ); + } + + return getFinancialServiceProviderSettingByName( + config.financialServiceProviderName, + ); + }, + ); -export function fspsHaveIntegratedFsp(fsps: FinancialServiceProviders[]) { - return fsps.some((fsp) => fsp !== FinancialServiceProviders.excel); + return fspSettings.some((fspSetting) => { + return fspSetting?.integrationType === integrationType; + }); } diff --git a/interfaces/Portalicious/src/app/domains/project/project.model.ts b/interfaces/Portalicious/src/app/domains/project/project.model.ts index a49ffbd4a9..efcdd4e2bd 100644 --- a/interfaces/Portalicious/src/app/domains/project/project.model.ts +++ b/interfaces/Portalicious/src/app/domains/project/project.model.ts @@ -1,5 +1,5 @@ import { FoundProgramDto } from '@121-service/src/programs/dto/found-program.dto'; -import { Attribute as AttributeFromBackend } from '@121-service/src/registration/enum/custom-data-attributes'; +import { Attribute as AttributeFrom121Service } from '@121-service/src/registration/enum/registration-attribute.enum'; import { GetUserReponseDto } from '@121-service/src/user/dto/get-user-response.dto'; import { AssignmentResponseDTO } from '@121-service/src/user/dto/userrole-response.dto'; @@ -16,7 +16,7 @@ export type ProjectUserWithRolesLabel = { lastLogin?: Date; } & Omit; -export type Attribute = Dto; +export type Attribute = Dto; export type AttributeWithTranslatedLabel = { label: string } & Omit< Attribute, diff --git a/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts b/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts index 8fa385e3e6..c0a2f85ea4 100644 --- a/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts +++ b/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts @@ -68,7 +68,7 @@ export class RegistrationApiService extends DomainApiService { method: 'POST', endpoint: this.pathToQueryKey([ ...BASE_ENDPOINT(projectId), - 'import-registrations', + 'import', ]).join('/'), body: formData, isUpload: true, diff --git a/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts b/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts index 01543e0534..68c2824a16 100644 --- a/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts +++ b/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts @@ -128,10 +128,8 @@ export class ProjectMonitoringPageComponent { }, { label: $localize`FSP(s)`, - value: projectData?.financialServiceProviders - .map((fsp) => - this.translatableStringService.translate(fsp.displayName), - ) + value: projectData?.programFinancialServiceProviderConfigurations + .map((fsp) => this.translatableStringService.translate(fsp.label)) .join(', '), }, { diff --git a/interfaces/Portalicious/src/app/pages/project-payments/components/create-payment/create-payment.component.html b/interfaces/Portalicious/src/app/pages/project-payments/components/create-payment/create-payment.component.html index cf137dec95..62177a27ec 100644 --- a/interfaces/Portalicious/src/app/pages/project-payments/components/create-payment/create-payment.component.html +++ b/interfaces/Portalicious/src/app/pages/project-payments/components/create-payment/create-payment.component.html @@ -57,7 +57,7 @@

@if (currentStep() === 2) {
- @if (hasExcelFsp()) { + @if (paymentHasExcelFsp()) {

Review payment summary and follow the next steps:

  1. @@ -83,7 +83,7 @@

    [data]="paymentSummaryData()" [hideBottomBorder]="true" /> - @if (hasIntegratedFsp()) { + @if (paymentHasIntegratedFsp()) { - fspsHaveIntegratedFsp(this.dryRunResult()?.fspsInPayment ?? []), + private paymentHasIntegrationType( + integrationType: FinancialServiceProviderIntegrationType, + ) { + const project = this.project.data(); + const dryRunResult = this.dryRunResult(); + + if (!project || !dryRunResult) { + return false; + } + + return financialServiceProviderConfigurationNamesHaveIntegrationType({ + project, + financialServiceProviderConfigurationNames: + dryRunResult.programFinancialServiceProviderConfigurationNames, + integrationType, + }); + } + + paymentHasIntegratedFsp = computed(() => + this.paymentHasIntegrationType(FinancialServiceProviderIntegrationType.api), ); - hasExcelFsp = computed(() => - fspsHaveExcelFsp(this.dryRunResult()?.fspsInPayment ?? []), + paymentHasExcelFsp = computed(() => + this.paymentHasIntegrationType(FinancialServiceProviderIntegrationType.csv), ); paymentSummaryData = computed(() => { @@ -223,21 +241,21 @@ export class CreatePaymentComponent { return []; } - const fsps = this.financialServiceProviders.data(); - const listData: DataListItem[] = [ { label: $localize`Financial Service Provider(s)`, - value: dryRunResult.fspsInPayment - .map((fspInPayment) => { - const fsp = fsps?.find((fsp) => fsp.fsp === fspInPayment); + value: dryRunResult.programFinancialServiceProviderConfigurationNames + .map((paymentFspConfigName) => { + const fspConfig = this.financialServiceProviderConfigurations + .data() + ?.find((fspConfig) => fspConfig.name === paymentFspConfigName); return ( - this.translatableStringService.translate(fsp?.displayName) ?? - fspInPayment + this.translatableStringService.translate(fspConfig?.label) ?? + paymentFspConfigName ); }) .join(', '), - loading: this.financialServiceProviders.isPending(), + loading: this.financialServiceProviderConfigurations.isPending(), fullWidth: true, }, { diff --git a/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/activity-log-expanded-row/activity-log-expanded-row.component.ts b/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/activity-log-expanded-row/activity-log-expanded-row.component.ts index c47798381a..80a9e708ae 100644 --- a/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/activity-log-expanded-row/activity-log-expanded-row.component.ts +++ b/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/activity-log-expanded-row/activity-log-expanded-row.component.ts @@ -20,7 +20,6 @@ import { ProjectApiService } from '~/domains/project/project.api.service'; import { REGISTRATION_STATUS_LABELS } from '~/domains/registration/registration.helper'; import { Activity } from '~/domains/registration/registration.model'; import { ActivityLogTableCellContext } from '~/pages/project-registration-activity-log/project-registration-activity-log.page'; - @Component({ selector: 'app-activity-log-expanded-row', standalone: true, @@ -53,7 +52,7 @@ export class ActivityLogExpandedRowComponent return ( activity.type === ActivityTypeEnum.Transaction && - activity.attributes.fsp === + activity.attributes.financialServiceProviderName === FinancialServiceProviders.intersolveVoucherWhatsapp ); }); @@ -113,7 +112,7 @@ export class ActivityLogExpandedRowComponent }, { label: $localize`FSP`, - value: item.attributes.fsp, + value: item.attributes.financialServiceProviderConfigurationLabel, }, { label: $localize`Amount`, diff --git a/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/table-cell-overview.component.ts b/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/table-cell-overview.component.ts index 9996c37f08..d183bdfa82 100644 --- a/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/table-cell-overview.component.ts +++ b/interfaces/Portalicious/src/app/pages/project-registration-activity-log/components/table-cell-overview.component.ts @@ -114,7 +114,7 @@ export class TableCellOverviewComponent if ( item.type !== ActivityTypeEnum.Transaction || - item.attributes.fsp !== + item.attributes.financialServiceProviderName !== FinancialServiceProviders.intersolveVoucherWhatsapp ) { return; diff --git a/interfaces/Portalicious/src/app/pages/project-registration-personal-information/project-registration-personal-information.page.ts b/interfaces/Portalicious/src/app/pages/project-registration-personal-information/project-registration-personal-information.page.ts index d3e082c425..4242cc9451 100644 --- a/interfaces/Portalicious/src/app/pages/project-registration-personal-information/project-registration-personal-information.page.ts +++ b/interfaces/Portalicious/src/app/pages/project-registration-personal-information/project-registration-personal-information.page.ts @@ -50,9 +50,7 @@ export class ProjectRegistrationPersonalInformationPageComponent { projectAttributes = injectQuery( this.projectApiService.getProjectAttributes({ projectId: this.projectId, - includeCustomAttributes: true, - includeFspQuestions: true, - includeProgramQuestions: true, + includeProgramRegistrationAttributes: true, includeTemplateDefaultAttributes: true, }), ); diff --git a/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.ts b/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.ts index 42c9367bb5..1617d19efb 100644 --- a/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.ts +++ b/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.ts @@ -151,8 +151,10 @@ export class ExportRegistrationsComponent { isCBEProject = computed(() => this.project .data() - ?.financialServiceProviders.some( - ({ fsp }) => fsp === FinancialServiceProviders.commercialBankEthiopia, + ?.programFinancialServiceProviderConfigurations.some( + (fsp) => + fsp.financialServiceProviderName === + FinancialServiceProviders.commercialBankEthiopia, ), ); } diff --git a/interfaces/Portalicious/src/app/services/export.service.ts b/interfaces/Portalicious/src/app/services/export.service.ts index 0a4b91bc04..1ba27ba27a 100644 --- a/interfaces/Portalicious/src/app/services/export.service.ts +++ b/interfaces/Portalicious/src/app/services/export.service.ts @@ -165,17 +165,6 @@ export class ExportService { }; } - private toAtributesForDuplicateCheckFilter( - attributes: { - name: string; - duplicateCheck: boolean; - }[], - ) { - return attributes - .filter((attribute) => attribute.duplicateCheck) - .map((attribute) => attribute.name); - } - async getDuplicateCheckAttributes( projectId: Signal, ): Promise { @@ -185,23 +174,13 @@ export class ExportService { this.projectApiService.getProject(projectId)(), ); - const { - programQuestions, - programCustomAttributes, - financialServiceProviders, - } = project; - - const fspAttributes = financialServiceProviders - .map((fsp) => fsp.questions) - .flat(); - - const allAttributeNames: string[] = [ - ...this.toAtributesForDuplicateCheckFilter(programQuestions), - ...this.toAtributesForDuplicateCheckFilter(programCustomAttributes), - ...this.toAtributesForDuplicateCheckFilter(fspAttributes), - ]; + const duplicateCheckAttributes = project.programRegistrationAttributes + .filter((attribute) => attribute.duplicateCheck) + .map((attribute) => attribute.name); - return [...new Set(allAttributeNames)].sort((a, b) => a.localeCompare(b)); + return [...new Set(duplicateCheckAttributes)].sort((a, b) => + a.localeCompare(b), + ); } async exportFspInstructions({ diff --git a/interfaces/Portalicious/src/app/services/messaging.service.ts b/interfaces/Portalicious/src/app/services/messaging.service.ts index 4d21a32f3a..c9e8ac58d6 100644 --- a/interfaces/Portalicious/src/app/services/messaging.service.ts +++ b/interfaces/Portalicious/src/app/services/messaging.service.ts @@ -37,8 +37,7 @@ export class MessagingService { return this.projectApiService.getProjectAttributes({ projectId, // This is the same combo used in the 121-service -> QueueMessageService.getPlaceholdersInMessageText - includeCustomAttributes: true, - includeProgramQuestions: true, + includeProgramRegistrationAttributes: true, includeTemplateDefaultAttributes: true, }); } diff --git a/k6/README.md b/k6/README.md index 2dfc7d6076..f8b4046e16 100644 --- a/k6/README.md +++ b/k6/README.md @@ -37,6 +37,8 @@ Then: npm install ``` +Make sure ENV-variable EXTERNAL_121_SERVICE_URL is set to http://localhost:3000/ (and not to some ngrok address) + **To run the tests:** ```shell diff --git a/k6/helpers/registration-default.data.js b/k6/helpers/registration-default.data.js index 0f50a48e62..6949dbc21a 100644 --- a/k6/helpers/registration-default.data.js +++ b/k6/helpers/registration-default.data.js @@ -9,7 +9,8 @@ export const registrationVisa = { paymentAmountMultiplier: 1, fullName: 'Jane Doe', [CustomDataAttributes.phoneNumber]: '14155238887', - fspName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, addressStreet: 'Teststraat', addressHouseNumber: '1', addressHouseNumberAddition: '', @@ -20,7 +21,8 @@ export const registrationVisa = { export const registrationSafaricom = { referenceId: '01dc9451-1273-484c-b2e8-ae21b51a96ab', - fspName: FinancialServiceProviders.safaricom, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.safaricom, phoneNumber: '254708374149', preferredLanguage: 'en', paymentAmountMultiplier: 1, @@ -39,13 +41,13 @@ export const registrationSafaricom = { county: 'ethiopia', subCounty: 'ethiopia', ward: 'dsa', - location: 21, - subLocation: 2113, + location: '21', + subLocation: '2113', village: 'adis abea', - nearestSchool: 213321, + nearestSchool: '213321', areaType: 'urban', mainSourceLivelihood: 'salary_from_formal_employment', - mainSourceLivelihoodOther: 213, + mainSourceLivelihoodOther: '213', Male05: 1, Female05: 0, Male612: 0, @@ -67,24 +69,24 @@ export const registrationSafaricom = { habitableRooms: 0, tenureStatusOfDwelling: 'Owner occupied', ownerOccupiedState: 'purchased', - ownerOccupiedStateOther: 0, + ownerOccupiedStateOther: '0', rentedFrom: 'individual', - rentedFromOther: 0, + rentedFromOther: '0', constructionMaterialRoof: 'tin', - ifRoofOtherSpecify: 31213, + ifRoofOtherSpecify: '31213', constructionMaterialWall: 'tiles', - ifWallOtherSpecify: 231312, + ifWallOtherSpecify: '231312', constructionMaterialFloor: 'cement', ifFloorOtherSpecify: 'asdsd', dwellingRisk: 'fire', - ifRiskOtherSpecify: 123213, + ifRiskOtherSpecify: '123213', mainSourceOfWater: 'lake', ifWaterOtherSpecify: 'dasdas', pigs: 'no', ifYesPigs: 123123, chicken: 'no', mainModeHumanWasteDisposal: 'septic_tank', - ifHumanWasteOtherSpecify: 31213, + ifHumanWasteOtherSpecify: '31213', cookingFuel: 'electricity', ifFuelOtherSpecify: 'asdsda', Lighting: 'electricity', @@ -107,17 +109,17 @@ export const registrationSafaricom = { howManyDeaths: 0, householdConditions: 'poor', skipMeals: 'no', - receivingBenefits: 0, - ifYesNameProgramme: 0, + receivingBenefits: '0', + ifYesNameProgramme: '0', typeOfBenefit: 'in_kind', - ifOtherBenefit: 2123312, - ifCash: 12312, - ifInKind: 132132, + ifOtherBenefit: '2123312', + ifCash: '12312', + ifInKind: '132132', feedbackOnRespons: 'no', - ifYesFeedback: 312123, + ifYesFeedback: '312123', whoDecidesHowToSpend: 'male_household_head', possibilityForConflicts: 'no', genderedDivision: 'no', ifYesElaborate: 'asddas', - geopoint: 123231, + geopoint: '123231', }; diff --git a/k6/models/programs.js b/k6/models/programs.js index b1c10c0047..2bed187c5d 100644 --- a/k6/models/programs.js +++ b/k6/models/programs.js @@ -20,40 +20,17 @@ export default class ProgramsModel { return res; } - updateCustomAttributes(programId, nameAttribute) { - const url = `${baseUrl}api/programs/${programId}/custom-attributes`; - const payload = JSON.stringify({ - type: 'text', - label: { - en: 'District', - fr: 'Département', - }, - showInPeopleAffectedTable: true, - duplicateCheck: true, - name: `${nameAttribute}`, - }); - const params = { - headers: { - 'Content-Type': 'application/json', - }, - }; - const res = http.post(url, payload, params); - return res; - } - - getProgrammeById(programId) { + getProgramById(programId) { const url = `${baseUrl}api/programs/${programId}`; const res = http.get(url); return res; } - createProgramQuestion(programId, questionName) { - const url = `${baseUrl}api/programs/${programId}/program-questions`; + createProgramRegistrationAttribute(programId, attributeName) { + const url = `${baseUrl}api/programs/${programId}/registration-attributes`; const payload = JSON.stringify({ - name: questionName, options: ['string'], scoring: {}, - persistence: true, pattern: 'string', showInPeopleAffectedTable: false, editableInPortal: true, @@ -62,12 +39,13 @@ export default class ProgramsModel { en: '+31 6 00 00 00 00', }, duplicateCheck: false, + name: attributeName, label: { - en: questionName, + en: attributeName, fr: 'Remplissez votre nom, sil vous plaît:', }, - answerType: 'text', - questionType: 'standard', + type: 'text', + isRequired: false, }); const params = { headers: { diff --git a/k6/models/registrations.js b/k6/models/registrations.js index ad968173be..b547945887 100644 --- a/k6/models/registrations.js +++ b/k6/models/registrations.js @@ -1,3 +1,4 @@ +// import { open } from 'k6'; import http from 'k6/http'; import config from './config.js'; // Import your configuration file @@ -8,7 +9,7 @@ export default class RegistrationsModel { constructor() {} importRegistrations(programId, registrations) { - const url = `${baseUrl}api/programs/${programId}/registrations/import`; + const url = `${baseUrl}api/programs/${programId}/registrations`; const payload = JSON.stringify([registrations]); const params = { headers: { @@ -20,4 +21,18 @@ export default class RegistrationsModel { return res; } + + importRegistrationsCsv(programId, csvFile) { + const url = `${baseUrl}api/programs/${programId}/registrations/import`; + const formData = { + file: http.file(csvFile, 'registrations.csv'), + }; + const params = { + timeout: '1200s', + }; + + const res = http.post(url, formData, params); + + return res; + } } diff --git a/k6/tests/getProgramWithManyAttributes.js b/k6/tests/getProgramWithManyAttributes.js index 8c8cff062c..3232fbfb47 100644 --- a/k6/tests/getProgramWithManyAttributes.js +++ b/k6/tests/getProgramWithManyAttributes.js @@ -42,17 +42,17 @@ export default function () { return r.timings.duration < 200; }, }); - // add 50 program questions to generate a bigger load + // add 50 program registration attributes to generate a bigger load for (let i = 1; i <= 50; i++) { - const questionName = `question${i}`; - const programQuestions = programsPage.createProgramQuestion( - programId, - questionName, - ); - registrationVisa[questionName] = 'bla'; + const attributeName = `attribute${i}`; + const programRegistrationAttributes = + programsPage.createProgramRegistrationAttribute(programId, attributeName); + registrationVisa[attributeName] = 'bla'; - check(programQuestions, { - 'Program questions added successfully status was 201': (r) => { + check(programRegistrationAttributes, { + 'Program registration attributes added successfully status was 201': ( + r, + ) => { if (r.status != 201) { console.log(r.body); } @@ -61,21 +61,6 @@ export default function () { }); } - // add 10 custom attributes to generate bigger load - for (let i = 1; i <= 10; i++) { - const cutstomAttributeName = `nameAttribute${i}`; - const customAttributes = programsPage.updateCustomAttributes( - programId, - cutstomAttributeName, - ); - registrationVisa[cutstomAttributeName] = 'bla'; - - check(customAttributes, { - 'Custom attribute added successful status was 201': (r) => - r.status == 201, - }); - } - // Upload registration const registrationImport = registrationsPage.importRegistrations( programId, @@ -92,9 +77,9 @@ export default function () { 'Duplication successful status was 201': (r) => r.status == 201, }); - // get programme by id and validte load time is less than 200ms - const programme = programsPage.getProgrammeById(2); - check(programme, { + // get program by id and validte load time is less than 200ms + const program = programsPage.getProgramById(2); + check(program, { 'Programme loaded succesfully status was 200': (r) => r.status == 200, 'Programme load time is less than 200ms': (r) => { if (r.timings.duration >= 200) { diff --git a/k6/tests/import1000Registrations.js b/k6/tests/import1000Registrations.js new file mode 100644 index 0000000000..e611b27cbd --- /dev/null +++ b/k6/tests/import1000Registrations.js @@ -0,0 +1,63 @@ +import { check, sleep } from 'k6'; + +import LoginModel from '../models/login.js'; +import RegistrationsModel from '../models/registrations.js'; +import ResetModel from '../models/reset.js'; + +const resetPage = new ResetModel(); +const loginPage = new LoginModel(); +const registrationsPage = new RegistrationsModel(); + +const resetScript = 'test-multiple'; +const programId = 2; + +const csvFilePath = + '../../e2e/test-registration-data/test-registrations-westeros-1000.csv'; + +// Somehow this works, but is not recognized. If importing from k6 above, it will not work. +// eslint-disable-next-line no-undef +const csvFile = open(csvFilePath); // open() only works here in 'init' stage of k6 test +if (!csvFile) { + throw new Error(`File not found: ${csvFilePath}`); +} + +export const options = { + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + }, + vus: 1, + iterations: 1, + // REFACTOR: should we investigate if the duration can be reduced? + duration: '9m', // At the time of writing this test, this test took ~7m both locally and on GH actions. Setting the limit to 9m, so it's below the API timeout limit of10m. Change this value only deliberatedly. If the tests takes longer because of regression effects, it should fail. +}; + +export default function () { + // reset db + const reset = resetPage.resetDB(resetScript); + check(reset, { + 'Reset successful status was 202': (r) => r.status == 202, + }); + + // login + const login = loginPage.login(); + check(login, { + 'Login successful status was 200': (r) => r.status == 201, + 'Login time is less than 200ms': (r) => { + if (r.timings.duration >= 200) { + console.log(`Login time was ${r.timings.duration}ms`); + } + return r.timings.duration < 200; + }, + }); + + // Upload registrations + const registrationImport = registrationsPage.importRegistrationsCsv( + programId, + csvFile, + ); + check(registrationImport, { + 'Import of registration successful status was 201': (r) => r.status == 201, + }); + + sleep(1); +} diff --git a/k6/tests/statusChangePaymentInLargeProgram.js b/k6/tests/statusChangePaymentInLargeProgram.js index 74ddbc947a..606c27fb13 100644 --- a/k6/tests/statusChangePaymentInLargeProgram.js +++ b/k6/tests/statusChangePaymentInLargeProgram.js @@ -30,6 +30,7 @@ export const options = { }; export default function () { + // REFACTOR: this test requires the same setup as getProgramWithManyAttributes.js. Move setup code to shared place. // reset db const reset = resetPage.resetDB(resetScript); check(reset, { @@ -48,17 +49,17 @@ export default function () { }, }); - // add 50 program questions to generate a bigger load + // add 50 program registration attributes to generate a bigger load for (let i = 1; i <= 50; i++) { - const questionName = `question${i}`; - const programQuestions = programsPage.createProgramQuestion( - programId, - questionName, - ); - registrationVisa[questionName] = 'bla'; - - check(programQuestions, { - 'Program questions added successfully status was 201': (r) => { + const attributeName = `attribute${i}`; + const programRegistrationAttributes = + programsPage.createProgramRegistrationAttribute(programId, attributeName); + registrationVisa[attributeName] = 'bla'; + + check(programRegistrationAttributes, { + 'Program registration attributes added successfully status was 201': ( + r, + ) => { if (r.status != 201) { console.log(r.body); } @@ -67,21 +68,6 @@ export default function () { }); } - // add 15 custom attributes to generate bigger load - for (let i = 1; i <= 15; i++) { - const cutstomAttributeName = `nameAttribute${i}`; - const customAttributes = programsPage.updateCustomAttributes( - programId, - cutstomAttributeName, - ); - registrationVisa[cutstomAttributeName] = 'bla'; - - check(customAttributes, { - 'Custom attribute added successful status was 201': (r) => - r.status == 201, - }); - } - // Upload registration const registrationImport = registrationsPage.importRegistrations( programId, @@ -99,7 +85,7 @@ export default function () { }); // get program by id and validate load time is less than 200ms - const program = programsPage.getProgrammeById(programId); + const program = programsPage.getProgramById(programId); check(program, { 'Programme loaded successfully status was 200': (r) => r.status == 200, 'Programme load time is less than 200ms': (r) => { diff --git a/package-lock.json b/package-lock.json index 3ae654f480..5feba45642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "license": "Apache-2.0", "devDependencies": { "@prettier/plugin-xml": "^3.4.1", + "@types/multer": "^1.4.12", "husky": "^9.0.11", "lint-staged": "^15.2.2", "prettier": "3.3.3", @@ -27,6 +28,124 @@ "prettier": "^3.0.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@xml-tools/parser": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", @@ -826,6 +945,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index bbfa5b72a2..7f4c45fd35 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "devDependencies": { "@prettier/plugin-xml": "^3.4.1", + "@types/multer": "^1.4.12", "husky": "^9.0.11", "lint-staged": "^15.2.2", "prettier": "3.3.3", diff --git a/services/.env.example b/services/.env.example index 30ca9429d0..45b459233d 100644 --- a/services/.env.example +++ b/services/.env.example @@ -284,6 +284,8 @@ MOCK_SAFARICOM=TRUE # Third-party FSP: CBE # ---------------- COMMERCIAL_BANK_ETHIOPIA_URL= +COMMERCIAL_BANK_ETHIOPIA_PASSWORD=test-COMMERCIAL_BANK_ETHIOPIA_PASSWORD +COMMERCIAL_BANK_ETHIOPIA_USERNAME=test-COMMERCIAL_BANK_ETHIOPIA_USERNAME # To use a mock version of the COMMERCIAL BANK ETHIOPIA API, use: `TRUE` to enable, leave empty or out to disable. MOCK_COMMERCIAL_BANK_ETHIOPIA=TRUE diff --git a/services/121-service/.knip.json b/services/121-service/.knip.json index 29e07961b2..5f966d5a15 100644 --- a/services/121-service/.knip.json +++ b/services/121-service/.knip.json @@ -6,7 +6,8 @@ "!src/**/*.spec.{js,ts}", "!src/migration/**/*.{js,ts}", "!src/seed-data/**/*.{js,ts}", - "!src/datasource-manage-migrations.ts" + "!src/datasource-manage-migrations.ts", + "!src/payments/fsp-integration/intersolve-visa/entities/intersolve-visa-wallet.entity.ts" ], "rules": { "dependencies": "warn", diff --git a/services/121-service/module-dependencies.md b/services/121-service/module-dependencies.md index 2d617b6239..4d2f8ebf9e 100644 --- a/services/121-service/module-dependencies.md +++ b/services/121-service/module-dependencies.md @@ -16,6 +16,18 @@ graph LR ProgramModule-->ProgramAttributesModule ProgramModule-->KoboConnectModule ProgramModule-->ProgramFinancialServiceProviderConfigurationsModule + ProgramFinancialServiceProviderConfigurationsModule-->TransactionsModule + TransactionsModule-->UserModule + TransactionsModule-->ActionsModule + TransactionsModule-->MessageQueuesModule + MessageQueuesModule-->ProgramAttributesModule + MessageQueuesModule-->RegistrationDataModule + MessageQueuesModule-->QueueRegistryModule + TransactionsModule-->MessageTemplateModule + TransactionsModule-->RegistrationUtilsModule + RegistrationUtilsModule-->RegistrationDataModule + TransactionsModule-->EventsModule + EventsModule-->UserModule ProgramModule-->IntersolveVisaModule IntersolveVisaModule-->UserModule OrganizationModule-->UserModule @@ -25,21 +37,10 @@ graph LR WhatsappModule-->MessageTemplateModule MessageModule-->SmsModule MessageModule-->MessageQueuesModule - MessageQueuesModule-->ProgramAttributesModule - MessageQueuesModule-->RegistrationDataModule - MessageQueuesModule-->QueueRegistryModule MessageModule-->IntersolveVoucherModule IntersolveVoucherModule-->ImageCodeModule IntersolveVoucherModule-->UserModule IntersolveVoucherModule-->TransactionsModule - TransactionsModule-->UserModule - TransactionsModule-->ActionsModule - TransactionsModule-->MessageQueuesModule - TransactionsModule-->MessageTemplateModule - TransactionsModule-->RegistrationUtilsModule - RegistrationUtilsModule-->RegistrationDataModule - TransactionsModule-->EventsModule - EventsModule-->UserModule IntersolveVoucherModule-->MessageQueuesModule IntersolveVoucherModule-->MessageTemplateModule IntersolveVoucherModule-->RegistrationDataModule @@ -95,7 +96,6 @@ graph LR MetricsModule-->IntersolveVoucherModule MetricsModule-->EventsModule MetricsModule-->RegistrationDataModule - MigrateVisaModule-->UserModule MessageIncomingModule-->ImageCodeModule MessageIncomingModule-->UserModule MessageIncomingModule-->IntersolveVoucherModule diff --git a/services/121-service/package-lock.json b/services/121-service/package-lock.json index 6d3ab5258b..6a37d864a0 100644 --- a/services/121-service/package-lock.json +++ b/services/121-service/package-lock.json @@ -60,6 +60,7 @@ "@types/cookie-parser": "^1.4.7", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.13", + "@types/multer": "^1.4.12", "@types/node": "^20.x", "@types/passport-azure-ad": "^4.3.6", "@types/passport-jwt": "^4.0.1", @@ -6462,6 +6463,16 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/mysql": { "version": "2.15.26", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", diff --git a/services/121-service/package.json b/services/121-service/package.json index 6aeefa7820..eef7e44d7d 100644 --- a/services/121-service/package.json +++ b/services/121-service/package.json @@ -93,6 +93,7 @@ "@types/cookie-parser": "^1.4.7", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.13", + "@types/multer": "^1.4.12", "@types/node": "^20.x", "@types/passport-azure-ad": "^4.3.6", "@types/passport-jwt": "^4.0.1", diff --git a/services/121-service/src/actions/action.entity.ts b/services/121-service/src/actions/action.entity.ts index 589b8020b0..3bed3e35bc 100644 --- a/services/121-service/src/actions/action.entity.ts +++ b/services/121-service/src/actions/action.entity.ts @@ -18,7 +18,6 @@ export class ActionEntity extends Base121AuditedEntity { } export enum AdditionalActionType { - importPeopleAffected = 'import-people-affected', importRegistrations = 'import-registrations', paymentFinished = 'payment-finished', paymentStarted = 'payment-started', diff --git a/services/121-service/src/actions/utils/action.mapper.spec.ts b/services/121-service/src/actions/utils/action.mapper.spec.ts index 19f14af09a..e9a0694f60 100644 --- a/services/121-service/src/actions/utils/action.mapper.spec.ts +++ b/services/121-service/src/actions/utils/action.mapper.spec.ts @@ -42,7 +42,7 @@ describe('Action mapper', () => { }; const actionEntity: ActionEntity = { id: actionId, - actionType: AdditionalActionType.importPeopleAffected, + actionType: AdditionalActionType.importRegistrations, user, program: {} as ProgramEntity, userId, @@ -56,7 +56,7 @@ describe('Action mapper', () => { }; const expectedResult: ActionReturnDto = { id: actionId, - actionType: AdditionalActionType.importPeopleAffected, + actionType: AdditionalActionType.importRegistrations, user: expectedUserOwnerResult, created: createdDate, }; diff --git a/services/121-service/src/activities/activities.mapper.ts b/services/121-service/src/activities/activities.mapper.ts index f79d79119d..9632e3a2db 100644 --- a/services/121-service/src/activities/activities.mapper.ts +++ b/services/121-service/src/activities/activities.mapper.ts @@ -116,8 +116,11 @@ export class ActivitiesMapper { status: transaction.status, amount: transaction.amount, paymentDate: transaction.paymentDate, - fsp: transaction.fsp, - fspName: transaction.fspName, + financialServiceProviderName: transaction.financialServiceProviderName, + financialServiceProviderConfigurationLabel: + transaction.programFinancialServiceProviderConfigurationLabel, + financialServiceProviderConfigurationName: + transaction.programFinancialServiceProviderConfigurationName, errorMessage: transaction.errorMessage, }, })); diff --git a/services/121-service/src/activities/activities.service.ts b/services/121-service/src/activities/activities.service.ts index 8f986708d8..c4b190a209 100644 --- a/services/121-service/src/activities/activities.service.ts +++ b/services/121-service/src/activities/activities.service.ts @@ -48,7 +48,7 @@ export class ActivitiesService { availableTypes.push(ActivityTypeEnum.Transaction); transactions = - await this.transactionScopedRepository.getManyByRegistrationIdAndProgramId( + await this.transactionScopedRepository.getLatestTransactionsByRegistrationIdAndProgramId( registrationId, programId, ); diff --git a/services/121-service/src/activities/interfaces/transaction-activity.interface.ts b/services/121-service/src/activities/interfaces/transaction-activity.interface.ts index 296ea21252..923aef6282 100644 --- a/services/121-service/src/activities/interfaces/transaction-activity.interface.ts +++ b/services/121-service/src/activities/interfaces/transaction-activity.interface.ts @@ -11,8 +11,9 @@ export interface TransactionActivity extends BaseActivity { status: TransactionStatusEnum; amount: number; paymentDate: Date; - fsp: FinancialServiceProviders; - fspName: LocalizedString; + financialServiceProviderName: FinancialServiceProviders; + financialServiceProviderConfigurationLabel: LocalizedString; + financialServiceProviderConfigurationName: string; errorMessage?: string; }; } diff --git a/services/121-service/src/app.module.ts b/services/121-service/src/app.module.ts index 908ab91d43..133dd63607 100644 --- a/services/121-service/src/app.module.ts +++ b/services/121-service/src/app.module.ts @@ -1,6 +1,6 @@ import { BullModule } from '@nestjs/bull'; import { Module, OnApplicationBootstrap } from '@nestjs/common'; -import { APP_GUARD } from '@nestjs/core'; +import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { MulterModule } from '@nestjs/platform-express'; import { ScheduleModule } from '@nestjs/schedule'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; @@ -16,13 +16,14 @@ import { EmailsModule } from '@121-service/src/emails/emails.module'; import { FinancialServiceProviderCallbackJobProcessorsModule } from '@121-service/src/financial-service-provider-callback-job-processors/financial-service-provider-callback-job-processors.module'; import { HealthModule } from '@121-service/src/health/health.module'; import { MetricsModule } from '@121-service/src/metrics/metrics.module'; -import { MigrateVisaModule } from '@121-service/src/migrate-visa/migrate-visa.module'; import { NoteModule } from '@121-service/src/notes/notes.module'; import { MessageModule } from '@121-service/src/notifications/message.module'; import { MessageIncomingModule } from '@121-service/src/notifications/message-incoming/message-incoming.module'; import { OrganizationModule } from '@121-service/src/organization/organization.module'; import { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity'; +import { ProgramModule } from '@121-service/src/programs/programs.module'; import { ScriptsModule } from '@121-service/src/scripts/scripts.module'; +import { ProgramExistenceInterceptor } from '@121-service/src/shared/interceptors/program-existence.interceptor'; import { TransactionJobProcessorsModule } from '@121-service/src/transaction-job-processors/transaction-job-processors.module'; import { TransactionQueuesModule } from '@121-service/src/transaction-queues/transaction-queues.module'; import { TypeOrmModule } from '@121-service/src/typeorm.module'; @@ -36,9 +37,9 @@ import { TypeOrmModule } from '@121-service/src/typeorm.module'; CronjobModule, ScriptsModule, OrganizationModule, + ProgramModule, MessageModule, MetricsModule, - MigrateVisaModule, MessageIncomingModule, NoteModule, EmailsModule, @@ -77,6 +78,10 @@ import { TypeOrmModule } from '@121-service/src/typeorm.module'; provide: APP_GUARD, useClass: ThrottlerGuard, }, + { + provide: APP_INTERCEPTOR, + useClass: ProgramExistenceInterceptor, + }, ], }) export class ApplicationModule implements OnApplicationBootstrap { diff --git a/services/121-service/src/events/events.service.spec.ts b/services/121-service/src/events/events.service.spec.ts index 935814d70f..644a1c645c 100644 --- a/services/121-service/src/events/events.service.spec.ts +++ b/services/121-service/src/events/events.service.spec.ts @@ -4,6 +4,7 @@ import { EventEntity } from '@121-service/src/events/entities/event.entity'; import { EventEnum } from '@121-service/src/events/enum/event.enum'; import { EventScopedRepository } from '@121-service/src/events/event.repository'; import { EventsService } from '@121-service/src/events/events.service'; +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; @@ -26,7 +27,7 @@ const attributeEntityNewValue = { const attributeEntityFieldName = { key: 'fieldName', - value: 'whatsappPhoneNumber', + value: FinancialServiceProviderAttributes.whatsappPhoneNumber, }; const mockFindEventResult: EventEntity[] = [ @@ -66,8 +67,11 @@ function getViewRegistration(): RegistrationViewEntity { preferredLanguage: LanguageEnum.en, inclusionScore: 0, paymentAmountMultiplier: 1, - financialServiceProvider: FinancialServiceProviders.intersolveVisa, - fspDisplayName: { en: 'Visa debit card' }, + financialServiceProviderName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: 'Intersolve-Visa', + programFinancialServiceProviderConfigurationLabel: { + en: 'Visa debit card', + }, registrationProgramId: 2, personAffectedSequence: 'PA #2', maxPayments: null, @@ -194,18 +198,28 @@ describe('EventsService', () => { it('should log an FSP change of intersolve visa to voucher whatsapp', async () => { // Changes that should be logged - newViewRegistration['whatsappPhoneNumber'] = '1234567890'; - newViewRegistration['fspDisplayName'] = { + newViewRegistration[ + FinancialServiceProviderAttributes.whatsappPhoneNumber + ] = '1234567890'; + newViewRegistration['programFinancialServiceProviderConfigurationLabel'] = { en: 'Albert Heijn voucher WhatsApp', }; - delete newViewRegistration['addressCity']; - delete newViewRegistration['addressPostalCode']; - delete newViewRegistration['addressHouseNumberAddition']; - delete newViewRegistration['addressHouseNumber']; - delete newViewRegistration['addressStreet']; + delete newViewRegistration[FinancialServiceProviderAttributes.addressCity]; + delete newViewRegistration[ + FinancialServiceProviderAttributes.addressPostalCode + ]; + delete newViewRegistration[ + FinancialServiceProviderAttributes.addressHouseNumberAddition + ]; + delete newViewRegistration[ + FinancialServiceProviderAttributes.addressHouseNumber + ]; + delete newViewRegistration[ + FinancialServiceProviderAttributes.addressStreet + ]; // Changes that should not be logged - newViewRegistration.financialServiceProvider = + newViewRegistration.programFinancialServiceProviderConfigurationName = FinancialServiceProviders.intersolveVoucherWhatsapp; // Act @@ -220,11 +234,17 @@ describe('EventsService', () => { attributes: [ { key: 'oldValue', - value: oldViewRegistration['fspDisplayName'], + value: + oldViewRegistration[ + 'programFinancialServiceProviderConfigurationLabel' + ], }, { key: 'newValue', - value: newViewRegistration['fspDisplayName'], + value: + newViewRegistration[ + 'programFinancialServiceProviderConfigurationLabel' + ], }, ], userId: 2, @@ -235,13 +255,22 @@ describe('EventsService', () => { attributes: [ { key: 'oldValue', - value: oldViewRegistration['whatsappPhoneNumber'], + value: + oldViewRegistration[ + FinancialServiceProviderAttributes.whatsappPhoneNumber + ], }, { key: 'newValue', - value: newViewRegistration['whatsappPhoneNumber'], + value: + newViewRegistration[ + FinancialServiceProviderAttributes.whatsappPhoneNumber + ], + }, + { + key: 'fieldName', + value: FinancialServiceProviderAttributes.whatsappPhoneNumber, }, - { key: 'fieldName', value: 'whatsappPhoneNumber' }, ], userId: 2, }, @@ -249,8 +278,17 @@ describe('EventsService', () => { registrationId: oldViewRegistration.id, type: EventEnum.registrationDataChange, attributes: [ - { key: 'oldValue', value: oldViewRegistration['addressCity'] }, - { key: 'fieldName', value: 'addressCity' }, + { + key: 'oldValue', + value: + oldViewRegistration[ + FinancialServiceProviderAttributes.addressCity + ], + }, + { + key: 'fieldName', + value: FinancialServiceProviderAttributes.addressCity, + }, ], userId: 2, }, @@ -258,23 +296,47 @@ describe('EventsService', () => { registrationId: oldViewRegistration.id, type: EventEnum.registrationDataChange, attributes: [ - { key: 'oldValue', value: oldViewRegistration['addressPostalCode'] }, - { key: 'fieldName', value: 'addressPostalCode' }, + { + key: 'oldValue', + value: + oldViewRegistration[ + FinancialServiceProviderAttributes.addressPostalCode + ], + }, + { + key: 'fieldName', + value: FinancialServiceProviderAttributes.addressPostalCode, + }, ], userId: 2, }, { registrationId: oldViewRegistration.id, type: EventEnum.registrationDataChange, - attributes: [{ key: 'fieldName', value: 'addressHouseNumberAddition' }], + attributes: [ + { + key: 'fieldName', + value: + FinancialServiceProviderAttributes.addressHouseNumberAddition, + }, + ], userId: 2, }, { registrationId: oldViewRegistration.id, type: EventEnum.registrationDataChange, attributes: [ - { key: 'oldValue', value: oldViewRegistration['addressHouseNumber'] }, - { key: 'fieldName', value: 'addressHouseNumber' }, + { + key: 'oldValue', + value: + oldViewRegistration[ + FinancialServiceProviderAttributes.addressHouseNumber + ], + }, + { + key: 'fieldName', + value: FinancialServiceProviderAttributes.addressHouseNumber, + }, ], userId: 2, }, @@ -282,8 +344,17 @@ describe('EventsService', () => { registrationId: oldViewRegistration.id, type: EventEnum.registrationDataChange, attributes: [ - { key: 'oldValue', value: oldViewRegistration['addressStreet'] }, - { key: 'fieldName', value: 'addressStreet' }, + { + key: 'oldValue', + value: + oldViewRegistration[ + FinancialServiceProviderAttributes.addressStreet + ], + }, + { + key: 'fieldName', + value: FinancialServiceProviderAttributes.addressStreet, + }, ], userId: 2, }, diff --git a/services/121-service/src/events/events.service.ts b/services/121-service/src/events/events.service.ts index 9b4402ff84..671afafcb8 100644 --- a/services/121-service/src/events/events.service.ts +++ b/services/121-service/src/events/events.service.ts @@ -321,7 +321,9 @@ export class EventsService { 'programId', 'registrationCreated', 'registrationCreatedDate', - 'financialServiceProvider', + 'financialServiceProviderName', + 'programFinancialServiceProviderConfigurationId', + 'programFinancialServiceProviderConfigurationLabel', 'registrationProgramId', 'personAffectedSequence', 'lastMessageStatus', @@ -335,7 +337,7 @@ export class EventsService { private getEventType(key: string): EventEnum { const financialServiceProviderKey: keyof RegistrationViewEntity = - 'fspDisplayName'; + 'programFinancialServiceProviderConfigurationName'; if (key === financialServiceProviderKey) { return EventEnum.financialServiceProviderChange; } diff --git a/services/121-service/src/financial-service-providers/dto/update-financial-service-provider.dto.ts b/services/121-service/src/financial-service-providers/dto/update-financial-service-provider.dto.ts deleted file mode 100644 index c4b16bc908..0000000000 --- a/services/121-service/src/financial-service-providers/dto/update-financial-service-provider.dto.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; - -import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; -import { QuestionOption } from '@121-service/src/shared/enum/question.enums'; -import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; - -export class UpdateFspAttributeDto { - @ApiProperty({ example: { en: 'attribute label' } }) - @IsOptional() - public label?: LocalizedString; - - @ApiProperty({ example: { en: 'attribute placeholder' } }) - @IsOptional() - public placeholder?: LocalizedString; - - @ApiProperty({ - example: [ - { - option: 'true', - label: { - en: 'Yes', - }, - }, - { - option: 'false', - label: { - en: 'No', - }, - }, - ], - }) - @IsOptional() - public options?: QuestionOption[]; - - @ApiProperty({ - example: [ExportType.allPeopleAffected, ExportType.included], - }) - @IsOptional() - public export?: ExportType[]; - - @ApiProperty() - @IsOptional() - public answerType?: string; - - @ApiProperty({ - example: false, - }) - @IsOptional() - public showInPeopleAffectedTable: boolean; -} - -export class CreateFspAttributeDto extends UpdateFspAttributeDto { - @ApiProperty() - @IsString() - @IsNotEmpty() - public readonly name: string; -} - -export class UpdateFinancialServiceProviderDto { - @ApiProperty({ - example: { en: 'FSP display name', nl: 'FSP weergavenaam' }, - }) - @IsOptional() - public readonly displayName?: LocalizedString; -} diff --git a/services/121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum.ts b/services/121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum.ts new file mode 100644 index 0000000000..a65485996e --- /dev/null +++ b/services/121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum.ts @@ -0,0 +1,12 @@ +export enum FinancialServiceProviderAttributes { + phoneNumber = 'phoneNumber', + nationalId = 'nationalId', + fullName = 'fullName', + addressStreet = 'addressStreet', + addressHouseNumber = 'addressHouseNumber', + addressHouseNumberAddition = 'addressHouseNumberAddition', + addressPostalCode = 'addressPostalCode', + addressCity = 'addressCity', + bankAccountNumber = 'bankAccountNumber', + whatsappPhoneNumber = 'whatsappPhoneNumber', +} diff --git a/services/121-service/src/financial-service-providers/enum/financial-service-provider-name.enum.ts b/services/121-service/src/financial-service-providers/enum/financial-service-provider-name.enum.ts index 98673416ae..bc28ceb33e 100644 --- a/services/121-service/src/financial-service-providers/enum/financial-service-provider-name.enum.ts +++ b/services/121-service/src/financial-service-providers/enum/financial-service-provider-name.enum.ts @@ -7,74 +7,12 @@ export enum FinancialServiceProviders { excel = 'Excel', } -export enum FinancialServiceProviderConfigurationEnum { +export enum FinancialServiceProviderConfigurationProperties { password = 'password', username = 'username', columnsToExport = 'columnsToExport', columnToMatch = 'columnToMatch', brandCode = 'brandCode', - displayName = 'displayName', coverLetterCode = 'coverLetterCode', fundingTokenCode = 'fundingTokenCode', } - -export const FinancialServiceProviderConfigurationMapping: Record< - FinancialServiceProviders, - FinancialServiceProviderConfigurationEnum[] -> = { - [FinancialServiceProviders.intersolveVoucherWhatsapp]: [ - FinancialServiceProviderConfigurationEnum.password, - FinancialServiceProviderConfigurationEnum.username, - FinancialServiceProviderConfigurationEnum.displayName, - ], - [FinancialServiceProviders.intersolveVoucherPaper]: [ - FinancialServiceProviderConfigurationEnum.password, - FinancialServiceProviderConfigurationEnum.username, - FinancialServiceProviderConfigurationEnum.displayName, - ], - [FinancialServiceProviders.intersolveVisa]: [ - FinancialServiceProviderConfigurationEnum.brandCode, - FinancialServiceProviderConfigurationEnum.coverLetterCode, - FinancialServiceProviderConfigurationEnum.displayName, - FinancialServiceProviderConfigurationEnum.fundingTokenCode, - ], - [FinancialServiceProviders.safaricom]: [ - FinancialServiceProviderConfigurationEnum.displayName, - ], - [FinancialServiceProviders.commercialBankEthiopia]: [ - FinancialServiceProviderConfigurationEnum.password, - FinancialServiceProviderConfigurationEnum.username, - FinancialServiceProviderConfigurationEnum.displayName, - ], - [FinancialServiceProviders.excel]: [ - FinancialServiceProviderConfigurationEnum.columnsToExport, - FinancialServiceProviderConfigurationEnum.columnToMatch, - FinancialServiceProviderConfigurationEnum.displayName, - ], -}; - -export const RequiredFinancialServiceProviderConfigurations: Partial< - Record -> = { - [FinancialServiceProviders.intersolveVoucherWhatsapp]: [ - FinancialServiceProviderConfigurationEnum.password, - FinancialServiceProviderConfigurationEnum.username, - ], - [FinancialServiceProviders.intersolveVoucherPaper]: [ - FinancialServiceProviderConfigurationEnum.password, - FinancialServiceProviderConfigurationEnum.username, - ], - [FinancialServiceProviders.intersolveVisa]: [ - FinancialServiceProviderConfigurationEnum.brandCode, - FinancialServiceProviderConfigurationEnum.coverLetterCode, - FinancialServiceProviderConfigurationEnum.fundingTokenCode, - ], - [FinancialServiceProviders.commercialBankEthiopia]: [ - FinancialServiceProviderConfigurationEnum.password, - FinancialServiceProviderConfigurationEnum.username, - ], - [FinancialServiceProviders.excel]: [ - FinancialServiceProviderConfigurationEnum.columnsToExport, - FinancialServiceProviderConfigurationEnum.columnToMatch, - ], -}; diff --git a/services/121-service/src/financial-service-providers/financial-service-provider-integration-type.enum.ts b/services/121-service/src/financial-service-providers/financial-service-provider-integration-type.enum.ts new file mode 100644 index 0000000000..ab7cf98661 --- /dev/null +++ b/services/121-service/src/financial-service-providers/financial-service-provider-integration-type.enum.ts @@ -0,0 +1,4 @@ +export enum FinancialServiceProviderIntegrationType { + csv = 'csv', + api = 'api', +} diff --git a/services/121-service/src/financial-service-providers/financial-service-provider-settings.helpers.ts b/services/121-service/src/financial-service-providers/financial-service-provider-settings.helpers.ts new file mode 100644 index 0000000000..4725abb86e --- /dev/null +++ b/services/121-service/src/financial-service-providers/financial-service-provider-settings.helpers.ts @@ -0,0 +1,36 @@ +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviderDto } from '@121-service/src/financial-service-providers/financial-service-provider.dto'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; + +export function getFinancialServiceProviderSettingByNameOrThrow( + name: string, +): FinancialServiceProviderDto { + const foundFsp = FINANCIAL_SERVICE_PROVIDER_SETTINGS.find( + (fsp) => fsp.name === name, + ); + if (!foundFsp) { + throw new Error(`Financial service provider with name ${name} not found`); + } else { + return foundFsp; + } +} + +export function getFinancialServiceProviderConfigurationProperties( + financialServiceProviderName: FinancialServiceProviders, +): string[] { + const foundFsp = getFinancialServiceProviderSettingByNameOrThrow( + financialServiceProviderName, + ); + return foundFsp.configurationProperties.map((property) => property.name); +} + +export function getFinancialServiceProviderConfigurationRequiredProperties( + financialServiceProviderName: FinancialServiceProviders, +): string[] { + const foundFsp = getFinancialServiceProviderSettingByNameOrThrow( + financialServiceProviderName, + ); + return foundFsp.configurationProperties + .filter((property) => property.isRequired) + .map((property) => property.name); +} diff --git a/services/121-service/src/financial-service-providers/financial-service-provider.controller.ts b/services/121-service/src/financial-service-providers/financial-service-provider.controller.ts index 3b472a01be..d131d6e35c 100644 --- a/services/121-service/src/financial-service-providers/financial-service-provider.controller.ts +++ b/services/121-service/src/financial-service-providers/financial-service-provider.controller.ts @@ -1,25 +1,8 @@ -import { - Body, - Controller, - Delete, - Get, - HttpStatus, - Param, - ParseIntPipe, - Patch, - Post, - UseGuards, -} from '@nestjs/common'; +import { Controller, Get, HttpStatus, Param, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { - CreateFspAttributeDto, - UpdateFinancialServiceProviderDto, - UpdateFspAttributeDto, -} from '@121-service/src/financial-service-providers/dto/update-financial-service-provider.dto'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; +import { FinancialServiceProviderDto } from '@121-service/src/financial-service-providers/financial-service-provider.dto'; import { FinancialServiceProvidersService } from '@121-service/src/financial-service-providers/financial-service-provider.service'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { AuthenticatedUser } from '@121-service/src/guards/authenticated-user.decorator'; import { AuthenticatedUserGuard } from '@121-service/src/guards/authenticated-user.guard'; @@ -36,123 +19,29 @@ export class FinancialServiceProvidersController { @ApiResponse({ status: HttpStatus.OK, description: 'All Financial Service Providers with attributes', - type: [FinancialServiceProviderEntity], + type: FinancialServiceProviderDto, }) @Get() - public async getAllFsps(): Promise { + public async getAllFsps(): Promise { return await this.fspService.getAllFsps(); } - @ApiOperation({ summary: 'Get Financial Service Provider (FSP) by fspId.' }) - @ApiParam({ name: 'fspId', required: true, type: 'integer' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Fsp with attributes', - type: FinancialServiceProviderEntity, + @ApiOperation({ summary: 'Get Financial Service Provider (FSP) by name.' }) + @ApiParam({ + name: 'financialServiceProviderName', + required: true, + type: 'string', }) - @Get(':fspId') - public async getFspById( - @Param('fspId', ParseIntPipe) - fspId: number, - ): Promise { - return await this.fspService.getFspById(fspId); - } - - @AuthenticatedUser({ isAdmin: true }) - @ApiOperation({ summary: 'Update Financial Service Provider' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Financial Service Provicer updated', - type: FinancialServiceProviderEntity, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'No Financial Service Provicer found with given id', - }) - @ApiParam({ name: 'fspId', required: true, type: 'integer' }) - @Patch(':fspId') - public async updateFsp( - @Param('fspId', ParseIntPipe) - fspId: number, - @Body() updateFspDto: UpdateFinancialServiceProviderDto, - ): Promise { - return await this.fspService.updateFsp(fspId, updateFspDto); - } - - @AuthenticatedUser({ isAdmin: true }) - @ApiOperation({ summary: 'Update FSP attribute' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'FSP attribute updated', - type: FspQuestionEntity, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: - 'No attribute with given name found in Financial Service Provicer with given id', - }) - @ApiParam({ name: 'fspId', required: true, type: 'integer' }) - @ApiParam({ name: 'attributeName', required: true, type: 'string' }) - @Patch(':fspId/attribute/:attributeName') - public async updateFspAttribute( - @Param() params, - @Param('fspId', ParseIntPipe) - fspId: number, - @Body() updateFspAttributeDto: UpdateFspAttributeDto, - ): Promise { - return await this.fspService.updateFspAttribute( - fspId, - params.attributeName, - updateFspAttributeDto, - ); - } - - @AuthenticatedUser({ isAdmin: true }) - @ApiOperation({ summary: 'Create FSP attribute' }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: 'FSP attribute created', - type: FspQuestionEntity, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'No Financial Service Provicer found with given id', - }) - @ApiParam({ name: 'fspId', required: true, type: 'integer' }) - @Post(':fspId/attribute') - public async createFspAttribute( - @Param('fspId', ParseIntPipe) - fspId: number, - @Body() createFspAttributeDto: CreateFspAttributeDto, - ): Promise { - return await this.fspService.createFspAttribute( - fspId, - createFspAttributeDto, - ); - } - - @AuthenticatedUser({ isAdmin: true }) - @ApiOperation({ summary: 'Delete FSP attribute' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'FSP attribute deleted', - type: FspQuestionEntity, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'No attribut with given name found for given fspId', - }) - @ApiParam({ name: 'fspId', required: true, type: 'integer' }) - @ApiParam({ name: 'attributeName', required: true, type: 'string' }) - @Delete(':fspId/attribute/:attributeName') - public async deleteFspAttribute( - @Param() params, - @Param('fspId', ParseIntPipe) - fspId: number, - ): Promise { - return await this.fspService.deleteFspAttribute( - fspId, - params.attributeName, - ); + description: 'Fsp with attributes', + type: FinancialServiceProviderDto, + }) + @Get(':financialServiceProviderName') + public async getFspByName( + @Param('financialServiceProviderName') + financialServiceProviderName: string, + ): Promise { + return await this.fspService.getFspByName(financialServiceProviderName); } } diff --git a/services/121-service/src/financial-service-providers/financial-service-provider.dto.ts b/services/121-service/src/financial-service-providers/financial-service-provider.dto.ts new file mode 100644 index 0000000000..5ae128558b --- /dev/null +++ b/services/121-service/src/financial-service-providers/financial-service-provider.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviderIntegrationType } from '@121-service/src/financial-service-providers/financial-service-provider-integration-type.enum'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; +import { WrapperType } from '@121-service/src/wrapper.type'; + +export class FinancialServiceProviderDto { + @ApiProperty({ example: 'fspName' }) + readonly name: WrapperType; + + @ApiProperty({ example: FinancialServiceProviderIntegrationType.api }) + readonly integrationType: WrapperType; + + @ApiProperty({ example: { en: 'default label' } }) + readonly defaultLabel: LocalizedString; + + @ApiProperty({ example: true }) + readonly notifyOnTransaction: boolean; + + @ApiProperty({ + example: [ + { name: 'houseNumber', isRequired: true }, + { name: 'houseNumberAddition', isRequired: false }, + ], + }) + readonly attributes: { + name: FinancialServiceProviderAttributes; + isRequired: boolean; + }[]; + + @ApiProperty({ + example: [ + { name: 'columnsToExport', isRequired: true }, + { name: 'columnToMatch', isRequired: true }, + ], + }) + readonly configurationProperties: { name: string; isRequired: boolean }[]; +} diff --git a/services/121-service/src/financial-service-providers/financial-service-provider.entity.ts b/services/121-service/src/financial-service-providers/financial-service-provider.entity.ts deleted file mode 100644 index f7a7631918..0000000000 --- a/services/121-service/src/financial-service-providers/financial-service-provider.entity.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity, ManyToMany, OneToMany, Relation } from 'typeorm'; - -import { CascadeDeleteEntity } from '@121-service/src/base.entity'; -import { FinancialServiceProviderIntegrationType } from '@121-service/src/financial-service-providers/enum/financial-service-provider-integration-type.enum'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { Attribute } from '@121-service/src/registration/enum/custom-data-attributes'; -import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; -import { getEnumValue, WrapperType } from '@121-service/src/wrapper.type'; - -@Entity('financial_service_provider') -export class FinancialServiceProviderEntity extends CascadeDeleteEntity { - @Column({ type: 'character varying', unique: true }) - @ApiProperty({ example: 'fspName' }) - public fsp: WrapperType; - - @Column('json', { nullable: true }) - @ApiProperty({ example: { en: 'FSP display name' } }) - public displayName: LocalizedString | null; - - @Column({ - default: FinancialServiceProviderIntegrationType.api, - type: 'character varying', - }) - @ApiProperty({ - example: getEnumValue(FinancialServiceProviderIntegrationType.api), - }) - public integrationType: WrapperType; - - @Column({ default: false }) - @ApiProperty({ - example: false, - description: 'Only relevant for integrationType=csv', - }) - public hasReconciliation: boolean; - - @Column({ default: false }) - @ApiProperty({ example: false }) - public notifyOnTransaction: boolean; - - @OneToMany((_type) => FspQuestionEntity, (questions) => questions.fsp) - public questions: Relation; - - @ManyToMany( - (_type) => ProgramEntity, - (program) => program.financialServiceProviders, - ) - public program: Relation; - - @OneToMany( - (_type) => TransactionEntity, - (transactions) => transactions.financialServiceProvider, - ) - public transactions: Relation; - - @OneToMany( - (_type) => ProgramFinancialServiceProviderConfigurationEntity, - (programFspConfiguration) => programFspConfiguration.fsp, - ) - public configuration: Relation< - ProgramFinancialServiceProviderConfigurationEntity[] - >; - - public editableAttributes?: Attribute[]; -} diff --git a/services/121-service/src/financial-service-providers/financial-service-provider.module.ts b/services/121-service/src/financial-service-providers/financial-service-provider.module.ts index a42d9df656..58f1b856f9 100644 --- a/services/121-service/src/financial-service-providers/financial-service-provider.module.ts +++ b/services/121-service/src/financial-service-providers/financial-service-provider.module.ts @@ -1,34 +1,14 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { FinancialServiceProvidersController } from '@121-service/src/financial-service-providers/financial-service-provider.controller'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; import { FinancialServiceProvidersService } from '@121-service/src/financial-service-providers/financial-service-provider.service'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { FinancialServiceProviderRepository } from '@121-service/src/financial-service-providers/repositories/financial-service-provider.repository'; -import { FinancialServiceProviderQuestionRepository } from '@121-service/src/financial-service-providers/repositories/financial-service-provider-question.repository'; import { UserModule } from '@121-service/src/user/user.module'; @Module({ - imports: [ - HttpModule, - UserModule, - TypeOrmModule.forFeature([ - FinancialServiceProviderEntity, - FspQuestionEntity, - ]), - ], - providers: [ - FinancialServiceProvidersService, - FinancialServiceProviderQuestionRepository, - FinancialServiceProviderRepository, - ], + imports: [HttpModule, UserModule], + providers: [FinancialServiceProvidersService], controllers: [FinancialServiceProvidersController], - exports: [ - FinancialServiceProvidersService, - FinancialServiceProviderQuestionRepository, - FinancialServiceProviderRepository, - ], + exports: [FinancialServiceProvidersService], }) export class FinancialServiceProvidersModule {} diff --git a/services/121-service/src/financial-service-providers/financial-service-provider.service.ts b/services/121-service/src/financial-service-providers/financial-service-provider.service.ts index 707543ac53..8632b18daa 100644 --- a/services/121-service/src/financial-service-providers/financial-service-provider.service.ts +++ b/services/121-service/src/financial-service-providers/financial-service-provider.service.ts @@ -1,154 +1,29 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Equal, Repository } from 'typeorm'; -import { - CreateFspAttributeDto, - UpdateFinancialServiceProviderDto, - UpdateFspAttributeDto, -} from '@121-service/src/financial-service-providers/dto/update-financial-service-provider.dto'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { Attribute } from '@121-service/src/registration/enum/custom-data-attributes'; +import { FinancialServiceProviderDto } from '@121-service/src/financial-service-providers/financial-service-provider.dto'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; @Injectable() export class FinancialServiceProvidersService { - @InjectRepository(FinancialServiceProviderEntity) - private financialServiceProviderRepository: Repository; - @InjectRepository(FspQuestionEntity) - public fspAttributeRepository: Repository; - - public async getFspById(id: number): Promise { - const fsp = await this.financialServiceProviderRepository.findOneOrFail({ - where: { id: Equal(id) }, - relations: ['questions'], - }); - if (fsp) { - fsp.editableAttributes = await this.getPaEditableAttributesFsp(fsp.id); - } - return fsp; - } - - public async getAllFsps(): Promise { - const fsps = await this.financialServiceProviderRepository.find({ - relations: ['questions'], - }); - for (const fsp of fsps) { - fsp.editableAttributes = await this.getPaEditableAttributesFsp(fsp.id); - } - return fsps; - } - - private async getPaEditableAttributesFsp(fspId): Promise { - const fspAttributes = await this.fspAttributeRepository.find({ - where: { fspId: Equal(fspId) }, - }); - - const attrs = fspAttributes.map((c) => { - return { - name: c.name, - type: c.answerType, - label: c.label, - options: c.options, - pattern: c.pattern, - }; - }); - - return attrs; - } - - public async updateFsp( - fspId: number, - updateFspDto: UpdateFinancialServiceProviderDto, - ): Promise { - const fsp = await this.financialServiceProviderRepository.findOne({ - where: { id: Equal(fspId) }, - }); + public async getFspByName( + name: string, + ): Promise { + const fsp = FINANCIAL_SERVICE_PROVIDER_SETTINGS.find( + (fsp) => fsp.name === name, + ); if (!fsp) { - const errors = `No fsp found with id ${fspId}`; - throw new HttpException({ errors }, HttpStatus.NOT_FOUND); + const availableFsps = FINANCIAL_SERVICE_PROVIDER_SETTINGS.map( + (fsp) => fsp.name, + ).join(', '); + throw new HttpException( + `Financial Service Provider not found. Available FSPs: ${availableFsps}`, + HttpStatus.NOT_FOUND, + ); } - - for (const key in updateFspDto) { - if (key !== 'fsp') { - fsp[key] = updateFspDto[key]; - } - } - - await this.financialServiceProviderRepository.save(fsp); return fsp; } - public async updateFspAttribute( - fspId: number, - attributeName: string, - fspAttributeDto: UpdateFspAttributeDto, - ): Promise { - const fspAttributes = await this.fspAttributeRepository.find({ - where: { name: Equal(attributeName) }, - relations: ['fsp'], - }); - // Filter out the right fsp, if fsp-attribute name occurs across multiple fsp's - const fspAttribute = fspAttributes.find((a) => a.fsp.id === fspId); - if (!fspAttribute) { - const errors = `No fspAttribute found with name ${attributeName} in fsp with id ${fspId}`; - throw new HttpException({ errors }, HttpStatus.NOT_FOUND); - } - - for (const key in fspAttributeDto) { - fspAttribute[key] = fspAttributeDto[key]; - } - - await this.fspAttributeRepository.save(fspAttribute); - return fspAttribute; - } - - public async createFspAttribute( - fspId: number, - fspAttributeDto: CreateFspAttributeDto, - ): Promise { - const fsp = await this.financialServiceProviderRepository.findOne({ - where: { id: Equal(fspId) }, - }); - if (!fsp) { - const errors = `Fsp with id '${fspId}' not found.'`; - throw new HttpException({ errors }, HttpStatus.NOT_FOUND); - } - - const fspAttributes = await this.fspAttributeRepository.find({ - where: { name: Equal(fspAttributeDto.name) }, - relations: ['fsp'], - }); - // Filter out the right fsp, if fsp-attribute name occurs across multiple fsp's - const oldFspAttribute = fspAttributes.find((a) => a.fsp.id === fspId); - if (oldFspAttribute) { - const errors = `Attribute with name ${fspAttributeDto.name} already exists for fsp with id ${fspId}`; - throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); - } - const fspAttribute = new FspQuestionEntity(); - for (const key in fspAttributeDto) { - fspAttribute[key] = fspAttributeDto[key]; - } - fspAttribute.fsp = fsp; - await this.fspAttributeRepository.save(fspAttribute); - return fspAttribute; - } - - public async deleteFspAttribute( - fspId: number, - attributeName: string, - ): Promise { - const fspAttribute = await this.fspAttributeRepository.findOne({ - where: { - name: Equal(attributeName), - fsp: { id: Equal(fspId) }, - }, - relations: ['fsp'], - }); - if (!fspAttribute) { - const errors = `Attribute with name: '${attributeName}' not found for fsp with id ${fspId}.'`; - throw new HttpException({ errors }, HttpStatus.NOT_FOUND); - } - return await this.fspAttributeRepository.remove(fspAttribute); + public async getAllFsps(): Promise { + return FINANCIAL_SERVICE_PROVIDER_SETTINGS; } } diff --git a/services/121-service/src/financial-service-providers/financial-service-providers-settings.const.ts b/services/121-service/src/financial-service-providers/financial-service-providers-settings.const.ts new file mode 100644 index 0000000000..91db6efb76 --- /dev/null +++ b/services/121-service/src/financial-service-providers/financial-service-providers-settings.const.ts @@ -0,0 +1,171 @@ +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviderDto } from '@121-service/src/financial-service-providers/financial-service-provider.dto'; +import { FinancialServiceProviderIntegrationType } from '@121-service/src/financial-service-providers/financial-service-provider-integration-type.enum'; + +// Attributes are the programRegistrationAttributes that are required for a regisration to have a program financial service provider configuration with the financial service provider +// Configuration properties are the program finacial service configuration properties that are required for the financial service provider to be able to send a payment +export const FINANCIAL_SERVICE_PROVIDER_SETTINGS: FinancialServiceProviderDto[] = + [ + { + name: FinancialServiceProviders.excel, + integrationType: FinancialServiceProviderIntegrationType.csv, + defaultLabel: { + en: 'Excel Payment Instructions', + }, + notifyOnTransaction: false, + attributes: [], + configurationProperties: [ + { + name: FinancialServiceProviderConfigurationProperties.columnsToExport, + isRequired: false, + }, + { + name: FinancialServiceProviderConfigurationProperties.columnToMatch, + isRequired: true, + }, + ], + }, + + { + name: FinancialServiceProviders.intersolveVisa, + integrationType: FinancialServiceProviderIntegrationType.api, + defaultLabel: { + en: 'Visa debit card', + }, + notifyOnTransaction: true, + attributes: [ + { + name: FinancialServiceProviderAttributes.fullName, + isRequired: true, + }, + { + name: FinancialServiceProviderAttributes.addressCity, + isRequired: true, + }, + { + name: FinancialServiceProviderAttributes.addressHouseNumber, + isRequired: true, + }, + { + name: FinancialServiceProviderAttributes.addressHouseNumberAddition, + isRequired: false, + }, + { + name: FinancialServiceProviderAttributes.addressPostalCode, + isRequired: true, + }, + { + name: FinancialServiceProviderAttributes.addressStreet, + isRequired: true, + }, + { + name: FinancialServiceProviderAttributes.phoneNumber, + isRequired: true, + }, + ], + configurationProperties: [ + { + name: FinancialServiceProviderConfigurationProperties.brandCode, + isRequired: true, + }, + { + name: FinancialServiceProviderConfigurationProperties.coverLetterCode, + isRequired: true, + }, + { + name: FinancialServiceProviderConfigurationProperties.fundingTokenCode, + isRequired: true, + }, + ], + }, + { + name: FinancialServiceProviders.intersolveVoucherWhatsapp, + integrationType: FinancialServiceProviderIntegrationType.api, + defaultLabel: { + en: 'Albert Heijn voucher WhatsApp', + }, + notifyOnTransaction: false, + attributes: [ + { + name: FinancialServiceProviderAttributes.whatsappPhoneNumber, + isRequired: true, + }, + ], + configurationProperties: [ + { + name: FinancialServiceProviderConfigurationProperties.password, + isRequired: true, + }, + { + name: FinancialServiceProviderConfigurationProperties.username, + isRequired: true, + }, + ], + }, + { + name: FinancialServiceProviders.intersolveVoucherPaper, + integrationType: FinancialServiceProviderIntegrationType.api, + defaultLabel: { + en: 'Albert Heijn voucher paper', + }, + notifyOnTransaction: false, + attributes: [], + configurationProperties: [ + { + name: FinancialServiceProviderConfigurationProperties.password, + isRequired: true, + }, + { + name: FinancialServiceProviderConfigurationProperties.username, + isRequired: true, + }, + ], + }, + { + name: FinancialServiceProviders.safaricom, + integrationType: FinancialServiceProviderIntegrationType.api, + defaultLabel: { + en: 'Safaricom', + }, + notifyOnTransaction: false, + attributes: [ + { + name: FinancialServiceProviderAttributes.phoneNumber, + isRequired: true, + }, + { + name: FinancialServiceProviderAttributes.nationalId, + isRequired: true, + }, + ], + configurationProperties: [], + }, + { + name: FinancialServiceProviders.commercialBankEthiopia, + integrationType: FinancialServiceProviderIntegrationType.api, + defaultLabel: { + en: 'Commercial Bank of Ethiopia', + }, + notifyOnTransaction: false, + attributes: [ + { + name: FinancialServiceProviderAttributes.bankAccountNumber, + isRequired: true, + }, + ], + configurationProperties: [ + { + name: FinancialServiceProviderConfigurationProperties.password, + isRequired: true, + }, + { + name: FinancialServiceProviderConfigurationProperties.username, + isRequired: true, + }, + ], + }, + ]; diff --git a/services/121-service/src/financial-service-providers/fsp-question.entity.ts b/services/121-service/src/financial-service-providers/fsp-question.entity.ts deleted file mode 100644 index 90e43f73a4..0000000000 --- a/services/121-service/src/financial-service-providers/fsp-question.entity.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - Check, - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - Relation, - Unique, -} from 'typeorm'; - -import { Base121Entity } from '@121-service/src/base.entity'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; -import { NameConstraintQuestions } from '@121-service/src/shared/const'; -import { QuestionOption } from '@121-service/src/shared/enum/question.enums'; -import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; - -@Unique('fspQuestionUnique', ['name', 'fspId']) -@Entity('financial_service_provider_question') -@Check(`"name" NOT IN (${NameConstraintQuestions})`) -export class FspQuestionEntity extends Base121Entity { - @Column() - @ApiProperty({ example: 'name' }) - public name: string; - - @Column('json') - @ApiProperty({ example: { en: 'label' } }) - public label: LocalizedString; - - @Column('json', { nullable: true }) - @ApiProperty({ example: { en: 'placeholder' } }) - public placeholder: LocalizedString | null; - - @Column('json', { nullable: true }) - @ApiProperty({ example: [] }) - public options: QuestionOption[] | null; - - @Column('json', { - default: [ExportType.allPeopleAffected, ExportType.included], - }) - @ApiProperty({ example: [] }) - public export: ExportType[]; - - @Column({ type: 'character varying', nullable: true }) - @ApiProperty({ example: 'pattern' }) - public pattern: string | null; - - @Column() - @ApiProperty({ example: 'tel' }) - public answerType: string; - - @Column({ default: false }) - @ApiProperty({ example: false }) - public duplicateCheck: boolean; - - @Column({ default: false }) - @ApiProperty({ example: false }) - public showInPeopleAffectedTable: boolean; - - @ManyToOne((_type) => FinancialServiceProviderEntity, (fsp) => fsp.questions) - @JoinColumn({ name: 'fspId' }) - public fsp: Relation; - @Column() - public fspId: number; - - @OneToMany( - () => RegistrationDataEntity, - (registrationData) => registrationData.fspQuestion, - ) - public registrationData: Relation; -} diff --git a/services/121-service/src/financial-service-providers/repositories/financial-service-provider-question.repository.ts b/services/121-service/src/financial-service-providers/repositories/financial-service-provider-question.repository.ts deleted file mode 100644 index 25dbeccf40..0000000000 --- a/services/121-service/src/financial-service-providers/repositories/financial-service-provider-question.repository.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { InjectRepository } from '@nestjs/typeorm'; -import { Equal, Repository } from 'typeorm'; - -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; - -export class FinancialServiceProviderQuestionRepository extends Repository { - constructor( - @InjectRepository(FspQuestionEntity) - private baseRepository: Repository, - ) { - super( - baseRepository.target, - baseRepository.manager, - baseRepository.queryRunner, - ); - } - public async getQuestionsByFspName( - fspName: FinancialServiceProviders, - ): Promise { - return await this.baseRepository.find({ - where: { fsp: { fsp: Equal(fspName) } }, - }); - } -} diff --git a/services/121-service/src/financial-service-providers/repositories/financial-service-provider.repository.ts b/services/121-service/src/financial-service-providers/repositories/financial-service-provider.repository.ts deleted file mode 100644 index 0c972f3647..0000000000 --- a/services/121-service/src/financial-service-providers/repositories/financial-service-provider.repository.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { InjectRepository } from '@nestjs/typeorm'; -import { Equal, Repository } from 'typeorm'; - -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; - -export class FinancialServiceProviderRepository extends Repository { - constructor( - @InjectRepository(FinancialServiceProviderEntity) - private baseRepository: Repository, - ) { - super( - baseRepository.target, - baseRepository.manager, - baseRepository.queryRunner, - ); - } - public async getByName( - name: FinancialServiceProviders, - ): Promise { - return await this.baseRepository.findOne({ - where: { fsp: Equal(name) }, - }); - } -} diff --git a/services/121-service/src/metrics/dto/registration-type.dto.ts b/services/121-service/src/metrics/dto/registration-type.dto.ts deleted file mode 100644 index c2d586616e..0000000000 --- a/services/121-service/src/metrics/dto/registration-type.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class RegistrationType { - id: number; - fsp: string | undefined; // Assuming fsp is a string, adjust if necessary - [key: string]: unknown; // Add any other properties that `registration` might have -} diff --git a/services/121-service/src/metrics/metrics.module.ts b/services/121-service/src/metrics/metrics.module.ts index 54c4eccf0b..58477ed04b 100644 --- a/services/121-service/src/metrics/metrics.module.ts +++ b/services/121-service/src/metrics/metrics.module.ts @@ -3,7 +3,6 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ActionsModule } from '@121-service/src/actions/actions.module'; import { EventsModule } from '@121-service/src/events/events.module'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { MetricsController } from '@121-service/src/metrics/metrics.controller'; import { MetricsService } from '@121-service/src/metrics/metrics.service'; import { IntersolveVisaModule } from '@121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.module'; @@ -12,8 +11,7 @@ import { SafaricomTransferEntity } from '@121-service/src/payments/fsp-integrati import { PaymentsModule } from '@121-service/src/payments/payments.module'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { RegistrationDataModule } from '@121-service/src/registration/modules/registration-data/registration-data.module'; import { RegistrationsModule } from '@121-service/src/registration/registrations.module'; import { RegistrationScopedRepository } from '@121-service/src/registration/repositories/registration-scoped.repository'; @@ -25,9 +23,7 @@ import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/cre @Module({ imports: [ TypeOrmModule.forFeature([ - ProgramQuestionEntity, - ProgramCustomAttributeEntity, - FspQuestionEntity, + ProgramRegistrationAttributeEntity, ProgramEntity, SafaricomTransferEntity, ]), diff --git a/services/121-service/src/metrics/metrics.service.ts b/services/121-service/src/metrics/metrics.service.ts index 238939636a..89a7594107 100644 --- a/services/121-service/src/metrics/metrics.service.ts +++ b/services/121-service/src/metrics/metrics.service.ts @@ -7,11 +7,9 @@ import { v4 as uuid } from 'uuid'; import { ActionsService } from '@121-service/src/actions/actions.service'; import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { FileDto } from '@121-service/src/metrics/dto/file.dto'; import { PaymentStateSumDto } from '@121-service/src/metrics/dto/payment-state-sum.dto'; import { ProgramStats } from '@121-service/src/metrics/dto/program-stats.dto'; -import { RegistrationType } from '@121-service/src/metrics/dto/registration-type.dto'; import { RegistrationStatusStats } from '@121-service/src/metrics/dto/registrationstatus-stats.dto'; import { RowType } from '@121-service/src/metrics/dto/rolo-type.dto'; import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; @@ -23,19 +21,18 @@ import { SafaricomTransferEntity } from '@121-service/src/payments/fsp-integrati import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { getFspDisplayNameMapping } from '@121-service/src/programs/utils/overwrite-fsp-display-name.helper'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { PaginationFilter } from '@121-service/src/registration/dto/filter-attribute.dto'; +import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; import { RegistrationDataOptions, RegistrationDataRelation, } from '@121-service/src/registration/dto/registration-data-relation.model'; import { - AnswerTypes, - CustomDataAttributes, - GenericAttributes, -} from '@121-service/src/registration/enum/custom-data-attributes'; + DefaultRegistrationDataAttributeNames, + GenericRegistrationAttributes, + RegistrationAttributeTypes, +} from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { RegistrationDataScopedRepository } from '@121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository'; import { RegistrationsService } from '@121-service/src/registration/registrations.service'; @@ -48,7 +45,6 @@ import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; import { UserService } from '@121-service/src/user/user.service'; import { RegistrationDataScopedQueryService } from '@121-service/src/utils/registration-data-query/registration-data-query.service'; import { getScopedRepositoryProviderName } from '@121-service/src/utils/scope/createScopedRepositoryProvider.helper'; - const MAX_NUMBER_OF_PAYMENTS_TO_EXPORT = 5; const userPermissionMapByExportType = { [ExportType.allPeopleAffected]: [PermissionEnum.RegistrationPersonalEXPORT], @@ -64,12 +60,8 @@ const userPermissionMapByExportType = { export class MetricsService { @InjectRepository(ProgramEntity) private readonly programRepository: Repository; - @InjectRepository(ProgramQuestionEntity) - private readonly programQuestionRepository: Repository; - @InjectRepository(ProgramCustomAttributeEntity) - private readonly programCustomAttributeRepository: Repository; - @InjectRepository(FspQuestionEntity) - private readonly fspQuestionRepository: Repository; + @InjectRepository(ProgramRegistrationAttributeEntity) + private readonly programRegistrationAttributeRepository: Repository; public constructor( private readonly registrationScopedRepository: RegistrationScopedRepository, @@ -217,7 +209,7 @@ export class MetricsService { programId, ExportType.included, ); - const pastPaymentDetails = await this.getPastPaymentDetails( + const pastPaymentDetails = await this.getPaymentDetailsPayment( programId, minPaymentId, maxPaymentId, @@ -267,13 +259,13 @@ export class MetricsService { row['id'] = row['registrationProgramId'] ?? null; const preferredLanguage = 'en'; - row['fspDisplayName'] = - typeof row['fspDisplayName'] === 'object' - ? ((row['fspDisplayName']?.[preferredLanguage] as - | string - | undefined) ?? '') - : (row['fspDisplayName'] ?? ''); - + row['programFinancialServiceProviderConfigurationLabel'] = + typeof row['programFinancialServiceProviderConfigurationLabel'] === + 'object' + ? ((row['programFinancialServiceProviderConfigurationLabel']?.[ + preferredLanguage + ] as string | undefined) ?? '') + : (row['programFinancialServiceProviderConfigurationLabel'] ?? ''); delete row['registrationProgramId']; } await this.replaceValueWithDropdownLabel(rows, relationOptions); @@ -306,77 +298,38 @@ export class MetricsService { const relationOptions: RegistrationDataOptions[] = []; const program = await this.programRepository.findOneOrFail({ where: { id: Equal(programId) }, - relations: [ - 'programQuestions', - 'programCustomAttributes', - 'financialServiceProviders', - 'financialServiceProviders.questions', - ], + relations: ['programRegistrationAttributes'], }); - for (const programCustomAttr of program.programCustomAttributes) { - const name = programCustomAttr.name; - const relation = { - programCustomAttributeId: programCustomAttr.id, - }; - relationOptions.push({ name, relation }); - } - for (const programQuestion of program.programQuestions) { + for (const programRegistrationAttribute of program.programRegistrationAttributes) { if ( - JSON.parse(JSON.stringify(programQuestion.export)).includes( - exportType, - ) && - programQuestion.name !== CustomDataAttributes.phoneNumber // Phonenumber is exclude because it is already a registration entity attribute + JSON.parse( + JSON.stringify(programRegistrationAttribute.export), + ).includes(exportType) && + programRegistrationAttribute.name !== + DefaultRegistrationDataAttributeNames.phoneNumber // Phonenumber is exclude because it is already a registration entity attribute ) { - const name = programQuestion.name; + const name = programRegistrationAttribute.name; const relation = { - programQuestionId: programQuestion.id, + programRegistrationAttributeId: programRegistrationAttribute.id, }; relationOptions.push({ name, relation }); } } - let fspQuestions: FspQuestionEntity[] = []; - for (const fsp of program.financialServiceProviders) { - fspQuestions = fspQuestions.concat(fsp.questions); - } - for (const fspQuestion of fspQuestions) { - if ( - JSON.parse(JSON.stringify(fspQuestion.export)).includes(exportType) && - fspQuestion.name !== CustomDataAttributes.phoneNumber // Phonenumber is exclude because it is already a registration entity attribute - ) { - const name = fspQuestion.name; - const relation = { fspQuestionId: fspQuestion.id }; - relationOptions.push({ name, relation }); - } - } return relationOptions; } private getRelationOptionsForDuplicates( - programQuestions: ProgramQuestionEntity[], - programCustomAttributes: ProgramCustomAttributeEntity[], - fspQuestions: FspQuestionEntity[], + programRegistrationAttributes: ProgramRegistrationAttributeEntity[], ): RegistrationDataOptions[] { const relationOptions: RegistrationDataOptions[] = []; - for (const programQuestion of programQuestions) { - const name = programQuestion.name; - const relation = { programQuestionId: programQuestion.id }; - relationOptions.push({ name, relation }); - } - - for (const programCustomAttribute of programCustomAttributes) { - const name = programCustomAttribute.name; + for (const programRegistrationAttribute of programRegistrationAttributes) { + const name = programRegistrationAttribute.name; const relation = { - programCustomAttributeId: programCustomAttribute.id, + programRegistrationAttributeId: programRegistrationAttribute.id, }; relationOptions.push({ name, relation }); } - - for (const fspQuestion of fspQuestions) { - const name = fspQuestion.name; - const relation = { fspQuestionId: fspQuestion.id }; - relationOptions.push({ name, relation }); - } return relationOptions; } @@ -436,15 +389,15 @@ export class MetricsService { } const defaultSelect = [ - GenericAttributes.referenceId, - GenericAttributes.registrationProgramId, - GenericAttributes.status, - GenericAttributes.phoneNumber, - GenericAttributes.preferredLanguage, - GenericAttributes.paymentAmountMultiplier, - GenericAttributes.registrationCreatedDate, - GenericAttributes.fspDisplayName, - GenericAttributes.paymentCount, + GenericRegistrationAttributes.referenceId, + GenericRegistrationAttributes.registrationProgramId, + GenericRegistrationAttributes.status, + GenericRegistrationAttributes.phoneNumber, + GenericRegistrationAttributes.preferredLanguage, + GenericRegistrationAttributes.paymentAmountMultiplier, + GenericRegistrationAttributes.registrationCreatedDate, + GenericRegistrationAttributes.programFinancialServiceProviderConfigurationLabel, + GenericRegistrationAttributes.paymentCount, ] as string[]; const program = await this.programRepository.findOneByOrFail({ @@ -452,18 +405,19 @@ export class MetricsService { }); if (program.enableMaxPayments) { - defaultSelect.push(GenericAttributes.maxPayments); + defaultSelect.push(GenericRegistrationAttributes.maxPayments); } if (program.enableScope) { - defaultSelect.push(GenericAttributes.scope); + defaultSelect.push(GenericRegistrationAttributes.scope); } const registrationDataNamesProgram = relationOptions .map((r) => r.name) .filter( (r): r is string => - r !== undefined && r !== CustomDataAttributes.phoneNumber, + r !== undefined && + r !== DefaultRegistrationDataAttributeNames.phoneNumber, ); const chunkSize = 10000; @@ -490,17 +444,21 @@ export class MetricsService { programId: number, relationOptions: RegistrationDataOptions[], registrationIds: number[], - ): Promise { + ): Promise { const query = this.registrationScopedRepository .createQueryBuilder('registration') - .leftJoin('registration.fsp', 'fsp') + .leftJoin( + 'registration.programFinancialServiceProviderConfiguration', + 'fspconfig', + ) .select([ `registration."referenceId" AS "referenceId"`, `registration."registrationProgramId" AS "id"`, `registration."registrationStatus" AS status`, - `fsp."fsp" AS fsp`, + `fspconfig."financialServiceProviderName" AS "financialServiceProviderName"`, + `fspconfig."label" AS "programFinancialServiceProviderConfigurationLabel"`, 'registration."scope" AS scope', - `registration."${GenericAttributes.phoneNumber}"`, + `registration."${GenericRegistrationAttributes.phoneNumber}"`, ]) .andWhere({ programId }) .andWhere( @@ -533,32 +491,18 @@ export class MetricsService { }[] = []; for (const option of relationOptions) { - if (option.relation?.programQuestionId) { - const dropdownProgramQuestion = - await this.programQuestionRepository.findOne({ + if (option.relation?.programRegistrationAttributeId) { + const dropdownProgramRegistrationAttribute = + await this.programRegistrationAttributeRepository.findOne({ where: { - id: Equal(option.relation.programQuestionId), - answerType: Equal(AnswerTypes.dropdown), + id: Equal(option.relation.programRegistrationAttributeId), + type: Equal(RegistrationAttributeTypes.dropdown), }, }); - if (dropdownProgramQuestion) { + if (dropdownProgramRegistrationAttribute) { valueOptionMappings.push({ - questionName: dropdownProgramQuestion.name, - options: dropdownProgramQuestion.options ?? undefined, - }); - } - } - if (option.relation?.fspQuestionId) { - const dropdownFspQuestion = await this.fspQuestionRepository.findOne({ - where: { - id: Equal(option.relation.fspQuestionId), - answerType: Equal(AnswerTypes.dropdown), - }, - }); - if (dropdownFspQuestion) { - valueOptionMappings.push({ - questionName: dropdownFspQuestion.name, - options: dropdownFspQuestion.options ?? undefined, + questionName: dropdownProgramRegistrationAttribute.name, + options: dropdownProgramRegistrationAttribute.options ?? undefined, }); } } @@ -601,34 +545,23 @@ export class MetricsService { programId: number, ): Promise { const program = await this.programRepository.findOneOrFail({ - relations: ['programQuestions', 'programCustomAttributes'], + relations: ['programRegistrationAttributes'], where: { id: Equal(programId), }, }); const relationOptions: RegistrationDataOptions[] = []; - const combinedArray = [ - ...program.programQuestions, - ...program.programCustomAttributes, - ]; - for (const entry of combinedArray) { + for (const entry of program.programRegistrationAttributes) { if ( JSON.parse(JSON.stringify(program.fullnameNamingConvention)).includes( entry.name, ) ) { const name = entry.name; - let relation: RegistrationDataRelation | undefined; - if (entry instanceof ProgramCustomAttributeEntity) { - relation = { - programCustomAttributeId: entry.id, - }; - } - if (entry instanceof ProgramQuestionEntity) { - relation = { - programQuestionId: entry.id, - }; - } + const relation: RegistrationDataRelation = { + programRegistrationAttributeId: entry.id, + }; + relationOptions.push({ name, relation }); } } @@ -642,19 +575,8 @@ export class MetricsService { const duplicatesMap = new Map(); const uniqueRegistrationIds = new Set(); - const programQuestions = await this.programQuestionRepository.find({ - where: { - program: { - id: Equal(programId), - }, - duplicateCheck: Equal(true), - }, - }); - const programQuestionIds = programQuestions.map((question) => { - return question.id; - }); - const programCustomAttributes = - await this.programCustomAttributeRepository.find({ + const programRegistrationAttributes = + await this.programRegistrationAttributeRepository.find({ where: { program: { id: Equal(programId), @@ -662,46 +584,29 @@ export class MetricsService { duplicateCheck: Equal(true), }, }); - const programCustomAttributeIds = programCustomAttributes.map((att) => { - return att.id; - }); - const fspQuestions = await this.fspQuestionRepository.find({ - relations: ['fsp', 'fsp.program'], - where: { - duplicateCheck: Equal(true), - fsp: { program: { id: Equal(programId) } }, + const programRegistrationAttributeIds = programRegistrationAttributes.map( + (question) => { + return question.id; }, - }); - - const fspQuestionIds = fspQuestions.map((fspQuestion) => { - return fspQuestion.id; - }); + ); const program = await this.programRepository.findOneOrFail({ where: { id: Equal(programId) }, - relations: ['financialServiceProviders', 'programFspConfiguration'], + relations: ['programFinancialServiceProviderConfigurations'], }); const nameRelations = await this.getNameRelationsByProgram(programId); const duplicateRelationOptions = this.getRelationOptionsForDuplicates( - programQuestions, - programCustomAttributes, - fspQuestions, + programRegistrationAttributes, ); const relationOptions = [...nameRelations, ...duplicateRelationOptions]; const whereOptions: Record>[] = []; - if (programQuestionIds.length > 0) { - whereOptions.push({ programQuestionId: In(programQuestionIds) }); - } - if (programCustomAttributeIds.length > 0) { + if (programRegistrationAttributeIds.length > 0) { whereOptions.push({ - programCustomAttributeId: In(programCustomAttributeIds), + programRegistrationAttributeId: In(programRegistrationAttributeIds), }); } - if (fspQuestionIds.length > 0) { - whereOptions.push({ fspQuestionId: In(fspQuestionIds) }); - } const query = this.registrationDataScopedRepository .createQueryBuilder('registration_data') @@ -756,7 +661,6 @@ export class MetricsService { return this.getRegisrationsForDuplicates( duplicatesMap, uniqueRegistrationIds, - fspQuestions, relationOptions, program, ); @@ -765,7 +669,6 @@ export class MetricsService { private async getRegisrationsForDuplicates( duplicatesMap: Map, uniqueRegistrationProgramIds: Set, - fspQuestions: FspQuestionEntity[], relationOptions: RegistrationDataOptions[], program: ProgramEntity, ): Promise<{ @@ -779,80 +682,53 @@ export class MetricsService { ]), programId: Equal(program.id), }, - select: ['registrationProgramId', 'fspId'], + select: [ + 'registrationProgramId', + 'programFinancialServiceProviderConfigurationId', + ], }); - - // Create an object to group registrations by fspId - const groupedRegistrations: Record< - string, // Keeping fspId as a string - { registrationProgramId: number; fspId: number }[] - > = {}; - registrationAndFspId.forEach((registration) => { - if (!registration.fspId) { - return; - } - const { registrationProgramId, fspId } = registration; - const fspIdStr = fspId.toString(); // Convert fspId to string for indexing - if (!groupedRegistrations[fspIdStr]) { - groupedRegistrations[fspIdStr] = []; - } - groupedRegistrations[fspIdStr].push({ registrationProgramId, fspId }); + const registrationIds = registrationAndFspId.map((r) => { + return { + registrationProgramId: r.registrationProgramId, + fspId: r.programFinancialServiceProviderConfigurationId, + }; }); - // Create an object to group relation options per FSP - const relationOptionsPerFsp = this.getRelationOptionsPerFsp( - relationOptions, - program, - fspQuestions, - ); - - const relationOptionNoFsp = relationOptions.filter( - (o) => !o.relation?.fspQuestionId, - ); - - let allRegistrations: RegistrationType[] = []; - for (const [fspIdStr, registrationIds] of Object.entries( - groupedRegistrations, - )) { - const fspId = Number(fspIdStr); // Convert fspIdStr back to number for processing - const registrationsWithSameFspId = - (await this.getRegistrationsFieldsForDuplicates( - program.id, - relationOptionsPerFsp[fspId] - ? relationOptionsPerFsp[fspId] - : relationOptionNoFsp, - registrationIds.map((r) => r.registrationProgramId), - )) as RegistrationType[]; - - allRegistrations = allRegistrations.concat(registrationsWithSameFspId); - } + const allRegistrations: MappedPaginatedRegistrationDto[] = + await this.getRegistrationsFieldsForDuplicates( + program.id, + relationOptions, + registrationIds.map((r) => r.registrationProgramId), + ); - const fspDisplayNameMapping = getFspDisplayNameMapping(program); const preferredLanguage = 'en'; - const result = allRegistrations.map((registration: RegistrationType) => { - // Ensure registration is of type RegistrationType - registration = - this.registrationsService.transformRegistrationByNamingConvention( - JSON.parse(JSON.stringify(program.fullnameNamingConvention)), - registration as RegistrationType, - ) as RegistrationType; - - // If a mapping exists, get the display name for the preferred language else use the FSP name - const fspDisplayNameForRegistrationFsp = - fspDisplayNameMapping[ - registration['fsp'] as keyof typeof fspDisplayNameMapping - ]; - - registration['fsp'] = fspDisplayNameForRegistrationFsp - ? fspDisplayNameForRegistrationFsp[preferredLanguage] - : (registration['fsp'] as string); + const result = allRegistrations.map( + (registration: MappedPaginatedRegistrationDto) => { + // Ensure registration is of type RegistrationType + registration = + this.registrationsService.transformRegistrationByNamingConvention( + JSON.parse(JSON.stringify(program.fullnameNamingConvention)), + registration, + ); - return { - ...registration, - duplicateWithIds: uniq(duplicatesMap.get(registration['id'])).join(','), - }; - }); + // If a mapping exists, get the display name for the preferred language else use the FSP name + // TODO: Destructuring object to prevent type errors, should be refactored + const { + programFinancialServiceProviderConfigurationLabel, + ...registrationCopy + } = registration; + registrationCopy['programFinancialServiceProviderConfigurationLabel'] = + programFinancialServiceProviderConfigurationLabel[preferredLanguage]; + + return { + ...registrationCopy, + duplicateWithIds: uniq(duplicatesMap.get(registration['id'])).join( + ',', + ), + }; + }, + ); return { fileName: ExportType.duplicates, @@ -860,40 +736,7 @@ export class MetricsService { }; } - private getRelationOptionsPerFsp( - relationOptions: RegistrationDataOptions[], - program: ProgramEntity, - fspQuestions: FspQuestionEntity[], - ): Record { - const relationOptionsPerFsp: Record = {}; - - // Filter relation options for specific FSP - for (const fsp of program.financialServiceProviders) { - relationOptionsPerFsp[fsp.id] = relationOptions.filter( - (o) => !o.relation?.fspQuestionId, - ); - - // Get all questions for specific FSP - const fspQuestionsPerFsp = fspQuestions.filter( - (question) => question.fsp.id === fsp.id, - ); - - for (const question of fspQuestionsPerFsp) { - const fspQuestionRelation = relationOptions.find( - (o) => o.relation?.fspQuestionId === question.id, - ); - - // Make sure to handle the case where fspQuestionRelation could be undefined - if (fspQuestionRelation) { - relationOptionsPerFsp[fsp.id].push(fspQuestionRelation); - } - } - } - - return relationOptionsPerFsp; - } - - private async getPastPaymentDetails( + private async getPaymentDetailsPayment( programId: number, minPaymentId: number, maxPaymentId: number, @@ -928,7 +771,7 @@ export class MetricsService { 'registration.paymentAmountMultiplier as "paymentAmountMultiplier"', 'transaction.amount as "amount"', 'SUBSTRING(transaction."errorMessage", 1, 32000) as "errorMessage"', - 'fsp.fsp AS financialServiceProvider', + 'fspConfig.name AS financialServiceProvider', ]) .innerJoin( '(' + latestTransactionPerPa.getQuery() + ')', @@ -937,7 +780,10 @@ export class MetricsService { ) .setParameters(latestTransactionPerPa.getParameters()) .leftJoin('transaction.registration', 'registration') - .leftJoin('transaction.financialServiceProvider', 'fsp'); + .leftJoin( + 'transaction.programFinancialServiceProviderConfiguration', + 'fspConfig', + ); const additionalFspExportFields = await this.getAdditionalFspExportFields(programId); @@ -1023,14 +869,18 @@ export class MetricsService { > { const program = await this.programRepository.findOneOrFail({ where: { id: Equal(programId) }, - relations: ['financialServiceProviders'], + relations: ['programFinancialServiceProviderConfigurations'], }); let fields: { entityJoinedToTransaction: EntityClass; attribute: string; }[] = []; - for (const fsp of program.financialServiceProviders) { - if (fsp.fsp === FinancialServiceProviders.safaricom) { + + for (const fspConfig of program.programFinancialServiceProviderConfigurations) { + if ( + fspConfig.financialServiceProviderName === + FinancialServiceProviders.safaricom + ) { fields = [ ...fields, ...[ diff --git a/services/121-service/src/migrate-visa/migrate-visa.controller.ts b/services/121-service/src/migrate-visa/migrate-visa.controller.ts deleted file mode 100644 index d8309133ab..0000000000 --- a/services/121-service/src/migrate-visa/migrate-visa.controller.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - Controller, - Post, - Query, - UploadedFile, - UseGuards, - UseInterceptors, -} from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { - ApiBody, - ApiConsumes, - ApiOperation, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; - -import { AuthenticatedUser } from '@121-service/src/guards/authenticated-user.decorator'; -import { AuthenticatedUserGuard } from '@121-service/src/guards/authenticated-user.guard'; -import { MigrateVisaService } from '@121-service/src/migrate-visa/migrate-visa.service'; -import { FILE_UPLOAD_API_FORMAT } from '@121-service/src/shared/file-upload-api-format'; - -@UseGuards(AuthenticatedUserGuard) -@ApiTags('migrate') -@Controller('migrate-visa') -export class MigrateVisaController { - public constructor(private readonly migrateVisaService: MigrateVisaService) {} - - @AuthenticatedUser({ isAdmin: true }) - @ApiQuery({ - name: 'limit', - required: false, - type: 'integer', - description: 'Optional limit for the number of records to migrate', - }) - @ApiOperation({ summary: 'Migrate visa data. One time use' }) - @ApiConsumes('multipart/form-data') - @ApiBody(FILE_UPLOAD_API_FORMAT) - @UseInterceptors(FileInterceptor('file')) - @Post('visa') - public async migrateVisaData( - @Query('limit') limit: number, - @UploadedFile() csvFileWithPreActivationValues: Blob, - ): Promise { - await this.migrateVisaService.migrateData( - limit, - csvFileWithPreActivationValues, - ); - } -} diff --git a/services/121-service/src/migrate-visa/migrate-visa.module.ts b/services/121-service/src/migrate-visa/migrate-visa.module.ts deleted file mode 100644 index 90256dfbcf..0000000000 --- a/services/121-service/src/migrate-visa/migrate-visa.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { MigrateVisaController } from '@121-service/src/migrate-visa/migrate-visa.controller'; -import { MigrateVisaService } from '@121-service/src/migrate-visa/migrate-visa.service'; -import { CustomHttpService } from '@121-service/src/shared/services/custom-http.service'; -import { UserModule } from '@121-service/src/user/user.module'; -import { FileImportService } from '@121-service/src/utils/file-import/file-import.service'; - -@Module({ - imports: [TypeOrmModule.forFeature(), UserModule, HttpModule], - providers: [MigrateVisaService, CustomHttpService, FileImportService], - controllers: [MigrateVisaController], - exports: [MigrateVisaService], -}) -export class MigrateVisaModule {} diff --git a/services/121-service/src/migrate-visa/migrate-visa.service.ts b/services/121-service/src/migrate-visa/migrate-visa.service.ts deleted file mode 100644 index 16467e2d42..0000000000 --- a/services/121-service/src/migrate-visa/migrate-visa.service.ts +++ /dev/null @@ -1,729 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Issuer, TokenSet } from 'openid-client'; -import { DataSource, QueryRunner } from 'typeorm'; -import { v4 as uuid } from 'uuid'; - -import { - FinancialServiceProviderConfigurationEnum, - FinancialServiceProviders, -} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { IssueTokenRequestIntersolveApiDto } from '@121-service/src/payments/fsp-integration/intersolve-visa/dtos/intersolve-api/issue-token-request-intersolve-api.dto'; -import { IssueTokenResponseIntersolveApiDto } from '@121-service/src/payments/fsp-integration/intersolve-visa/dtos/intersolve-api/issue-token-response-intersolve-api.dto'; -import { ErrorsInResponseIntersolveApi } from '@121-service/src/payments/fsp-integration/intersolve-visa/dtos/intersolve-api/partials/error-in-response-intersolve-api'; -import { IntersolveVisaChildWalletEntity } from '@121-service/src/payments/fsp-integration/intersolve-visa/entities/intersolve-visa-child-wallet.entity'; -import { IntersolveVisaCustomerEntity } from '@121-service/src/payments/fsp-integration/intersolve-visa/entities/intersolve-visa-customer.entity'; -import { IntersolveVisaParentWalletEntity } from '@121-service/src/payments/fsp-integration/intersolve-visa/entities/intersolve-visa-parent-wallet.entity'; -import { - IntersolveVisaWalletEntity, - IntersolveVisaWalletStatus, -} from '@121-service/src/payments/fsp-integration/intersolve-visa/entities/intersolve-visa-wallet.entity'; -import { IntersolveBlockTokenReasonCodeEnum } from '@121-service/src/payments/fsp-integration/intersolve-visa/enums/intersolve-block-token-reason-code.enum'; -import { IntersolveVisaTokenStatus } from '@121-service/src/payments/fsp-integration/intersolve-visa/enums/intersolve-visa-token-status.enum'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; -import { CustomHttpService } from '@121-service/src/shared/services/custom-http.service'; -import { FileImportService } from '@121-service/src/utils/file-import/file-import.service'; - -const intersolveVisaApiUrl = process.env.MOCK_INTERSOLVE - ? `${process.env.MOCK_SERVICE_URL}api/fsp/intersolve-visa` - : process.env.INTERSOLVE_VISA_API_URL; - -interface VisaCustomerWithReferenceId extends IntersolveVisaCustomerEntity { - referenceId: string; -} - -interface PreActivationValueRecord { - CardCode: string; - PreActivationValue: number; -} - -@Injectable() -export class MigrateVisaService { - constructor( - private readonly httpService: CustomHttpService, - private readonly fileImportService: FileImportService, - private readonly dataSource: DataSource, - ) {} - name = 'VisaMigrateChildParentWallet1717505534921'; - public tokenSet: TokenSet; - - public async migrateData( - limit: number, - csvFileWithPreActivationValues: Blob, - ): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await this.migrationTemplateData(queryRunner); - await this.insertProgramConfiguration(); - // await this.COMMENT_OUT_THIS_FUNCTION_FOR_TESTING_ONLY_CLEAR_DATA( - // queryRunner, - // ); - const programIds = await this.selectProgramIdsForInstanceWithVisa(); - for (const programId of programIds) { - await this.migrateProgramData( - queryRunner, - programId, - limit, - csvFileWithPreActivationValues, - ); - } - } - - private async insertProgramConfiguration(): Promise { - // this should not be set on production so it should not run on production - if (process.env.INTERSOLVE_VISA_FUNDINGTOKEN_CODE) { - // Insert program configuration - const fspVisaId = await this.dataSource.query( - `SELECT id FROM "121-service"."financial_service_provider" WHERE "fsp" = 'Intersolve-visa'`, - ); - const config2 = new ProgramFinancialServiceProviderConfigurationEntity(); - config2.fspId = fspVisaId[0].id; - config2.programId = 2; - config2.name = FinancialServiceProviderConfigurationEnum.fundingTokenCode; - config2.value = process.env.INTERSOLVE_VISA_FUNDINGTOKEN_CODE; - const config3 = new ProgramFinancialServiceProviderConfigurationEntity(); - config3.fspId = fspVisaId[0].id; - config3.programId = 3; - config3.name = FinancialServiceProviderConfigurationEnum.fundingTokenCode; - config3.value = process.env.INTERSOLVE_VISA_FUNDINGTOKEN_CODE; - - // save - try { - await this.dataSource.manager.save(config2); - } catch (e) { - if (e.code === '23505') { - console.log('Funding token already set for program 2'); - } else { - throw e; // Re-throw the error if it's not the specific one we're handling - } - } - try { - await this.dataSource.manager.save(config3); - } catch (e) { - if (e.code === '23505') { - console.log('Funding token already set for program 3'); - } else { - throw e; // Re-throw the error if it's not the specific one we're handling - } - } - } - } - - private async COMMENT_OUT_THIS_FUNCTION_FOR_TESTING_ONLY_CLEAR_DATA( - q: QueryRunner, - ): Promise { - // truncate - await q.query( - `TRUNCATE TABLE "121-service"."intersolve_visa_parent_wallet" CASCADE`, - ); - await q.query( - `TRUNCATE TABLE "121-service"."intersolve_visa_child_wallet" CASCADE`, - ); - await q.query( - `TRUNCATE TABLE "121-service"."intersolve_migration_progress"`, - ); - } - - private async migrationTemplateData(q: QueryRunner): Promise { - await q.query( - `UPDATE "121-service"."message_template" SET type = 'pauseVisaCard' WHERE type = 'blockVisaCard'`, - ); - await q.query( - `UPDATE "121-service"."message_template" SET type = 'unpauseVisaCard' WHERE type = 'unblockVisaCard'`, - ); - } - - private async createMigrationProgressTableAndIndex(queryRunner: QueryRunner) { - await queryRunner.query( - `CREATE TABLE IF NOT EXISTS "121-service"."intersolve_migration_progress" ("id" SERIAL NOT NULL, "referenceId" character varying NOT NULL, "completedOrCaught" boolean NOT NULL DEFAULT false, "error" json, "created" TIMESTAMP NOT NULL DEFAULT now())`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_intersolve_migration_progress" ON "121-service"."intersolve_migration_progress" ("referenceId")`, - ); - console.log('Migration progress table and index created(IF NOT EXISTS)'); - } - - private async migrateProgramData( - queryRunner: QueryRunner, - programId: number, - limit: number, - csvFileWithPreActivationValues: Blob, - ): Promise { - await this.createMigrationProgressTableAndIndex(queryRunner); - - // Process pre-activation values of wallets to be able to transfer them to parent wallets in case of INACTIVE wallets - const preActivationValuesMap = await this.processPreActivationValuesCsvFile( - csvFileWithPreActivationValues, - ); - - console.time(`Migrating program ${programId}`); - const brandCode = await this.getBrandcodeForProgram(queryRunner, programId); - const fundingTokenCode = await this.getFundingTokenCodeForProgram( - queryRunner, - programId, - ); - const visaCustomers = await this.selectVisaCustomers( - programId, - queryRunner, - limit, - ); - console.log( - `Migrating ${visaCustomers.length} customers for program ${programId}`, - ); - for (let i = 0; i < visaCustomers.length; i++) { - const visaCustomer = visaCustomers[i]; - if ((i + 1) % 10 === 0) { - console.log(`Migrating ${i + 1} of ${visaCustomers.length}`); - } - - let error: any; - - try { - await queryRunner.query( - `INSERT INTO "121-service".intersolve_migration_progress - ("referenceId", "completedOrCaught", "error", "created") - VALUES('${visaCustomer.referenceId}', false, NULL, now())`, - ); - - await this.migrateCustomerAndWalletData( - visaCustomer, - brandCode, - queryRunner, - preActivationValuesMap, - fundingTokenCode, - ); - } catch (e) { - // console.log('error:', e); - error = { - message: e.message, - stack: e.stack, - }; - } finally { - const errorString = error - ? JSON.stringify(error).replace(/'/g, "''") - : null; - const query = `UPDATE "121-service"."intersolve_migration_progress" SET "completedOrCaught" = true, "error" = ${errorString ? `'${errorString}'` : 'NULL'} WHERE "referenceId" = '${visaCustomer.referenceId}'`; - await queryRunner.query(query); - } - } - - /** - * When the migration is done, we want to query the migrations table for: - * - 'completedOrCaught' being false -> This means it stopped somewhere in the process, should be the edge case - * - 'completedOrCaught' being true and 'error' being defined -> This means an error occured during the process - * - * Those are the cases that should be investiged. - * - * */ - - console.timeEnd(`Migrating program ${programId}`); - } - - private async processPreActivationValuesCsvFile( - csvFileWithPreActivationValues: Blob, - ): Promise> { - const preActivationValueRecords = (await this.fileImportService.validateCsv( - csvFileWithPreActivationValues, - )) as PreActivationValueRecord[]; - // Convert to map for performance reasons - const preActivationValuesMap = new Map( - preActivationValueRecords.map((item) => [ - item['CardCode'], - item['PreActivationBudget'], - ]), - ); - return preActivationValuesMap; - } - - private async migrateCustomerAndWalletData( - visaCustomer: VisaCustomerWithReferenceId, - brandCode: string, - queryRunner: QueryRunner, - preActivationValuesMap: Map, - fundingTokenCode: string, - ): Promise { - const originalWalletsOfCustomer = await this.selectOriginalWallets( - visaCustomer, - queryRunner, - ); - - if (originalWalletsOfCustomer.length > 0) { - // create parent wallet - const newestOriginalWallet = originalWalletsOfCustomer[0]; - const parentWallet = await this.createParentWallet( - visaCustomer, - newestOriginalWallet, - brandCode, - queryRunner, - ); - - // migrate & link child wallets - await this.migrateAndLinkChildWallets( - originalWalletsOfCustomer, - parentWallet, - queryRunner, - preActivationValuesMap, - fundingTokenCode, - ); - } - } - - private async createParentWallet( - visaCustomer: VisaCustomerWithReferenceId, - newestOriginalWallet: IntersolveVisaWalletEntity, - brandCode: string, - queryRunner: QueryRunner, - ): Promise { - const createWalletReponse = await this.postCreateActiveWallet( - { - reference: uuid(), - activate: true, - }, - brandCode, - ); - if (!createWalletReponse?.data?.success) { - console.log( - 'MigrateVisaService ~ createWalletReponse.data.errors:', - createWalletReponse?.data?.errors, - ); - const errors = createWalletReponse?.data?.errors; - const formattedErrors = this.formatErrors(errors); - - throw new Error( - `Failed to create wallet for customer with referenceId ${visaCustomer.referenceId}. Errors: ${formattedErrors}`, - ); - } - const tokenCode = createWalletReponse.data?.data?.token?.code; - const linkToCustomerReponse = await this.postLinkCustomerToWallet( - { - holderId: visaCustomer.holderId, - }, - tokenCode, - ); - - if (!this.isSuccessResponseStatus(linkToCustomerReponse.status)) { - console.log( - 'MigrateVisaService ~ linkToCustomerReponse.data.errors:', - linkToCustomerReponse?.data?.errors, - ); - const errors = linkToCustomerReponse?.data?.errors; - const formattedErrors = this.formatErrors(errors); - - throw new Error( - `Failed to link wallet to customer with referenceId ${visaCustomer.referenceId}. Errors: ${formattedErrors}`, - ); - } - - const newParentWallet = new IntersolveVisaParentWalletEntity(); - newParentWallet.intersolveVisaCustomerId = visaCustomer.id; - newParentWallet.tokenCode = tokenCode; - newParentWallet.balance = newestOriginalWallet.balance - ? newestOriginalWallet.balance - : 0; // Deals with the factor that the old balance was nullable - newParentWallet.lastExternalUpdate = newestOriginalWallet.lastExternalUpdate - ? newestOriginalWallet.lastExternalUpdate - : new Date(); - newParentWallet.spentThisMonth = newestOriginalWallet.spentThisMonth; - newParentWallet.isLinkedToVisaCustomer = true; - return await queryRunner.manager.save(newParentWallet); - } - - private async migrateAndLinkChildWallets( - originalWallets: IntersolveVisaWalletEntity[], - parentWallet: IntersolveVisaParentWalletEntity, - queryRunner: QueryRunner, - preActivationValuesMap: Map, - fundingTokenCode: string, - ): Promise { - for (const originalWallet of originalWallets) { - const childWallet = new IntersolveVisaChildWalletEntity(); - childWallet.intersolveVisaParentWalletId = parentWallet.id; - childWallet.tokenCode = originalWallet.tokenCode!; // We assume that tokencode is never null in the old data - childWallet.isLinkedToParentWallet = false; - childWallet.isTokenBlocked = originalWallet.tokenBlocked ? true : false; // Deals with the factor that the old isTokenBlocked was nullable - childWallet.isDebitCardCreated = originalWallet.debitCardCreated; - childWallet.walletStatus = originalWallet.walletStatus - ? (originalWallet.walletStatus as unknown as IntersolveVisaTokenStatus) - : IntersolveVisaTokenStatus.Active; // Deals with the factor that the old walletStatus was nullable - childWallet.cardStatus = originalWallet.cardStatus; - childWallet.lastUsedDate = originalWallet.lastUsedDate; - childWallet.lastExternalUpdate = originalWallet.lastExternalUpdate; - childWallet.created = originalWallet.created; - const savedChildWallet = await queryRunner.manager.save(childWallet); - - await this.postToggleBlockWallet( - originalWallet.tokenCode, - { - reasonCode: IntersolveBlockTokenReasonCodeEnum.UNBLOCK_GENERAL, - }, - false, - ); - const postLinkTokenResult = await this.postLinkToken( - savedChildWallet.tokenCode, - parentWallet.tokenCode, - ); - if (!this.isSuccessResponseStatus(postLinkTokenResult.status)) { - const errors = postLinkTokenResult?.data?.errors; - const formattedErrors = this.formatErrors(errors); - - throw new Error( - `Failed to link child wallet ${childWallet.id} to parent wallet ${parentWallet.id}. Errors: ${formattedErrors}`, - ); - } else { - savedChildWallet.isLinkedToParentWallet = true; - await queryRunner.manager.save(savedChildWallet); - } - - if (originalWallet.walletStatus === IntersolveVisaWalletStatus.Inactive) { - // get pre-activation value per child wallet - const preActivationValue = preActivationValuesMap.get( - originalWallet.tokenCode as string, - ); - if (!preActivationValue) { - throw new Error( - `No pre-activation value found for wallet ${originalWallet.id} with tokenCode ${originalWallet.tokenCode}`, - ); - } - - // transfer pre-activation value to parent wallet - const postTransferResult = await this.postTransfer( - parentWallet.tokenCode, - preActivationValue as number, - fundingTokenCode, - ); - - if (!this.isSuccessResponseStatus(postTransferResult.status)) { - const errors = postTransferResult?.data?.errors; - const formattedErrors = this.formatErrors(errors); - throw new Error( - `Failed to transfer pre-activation value of inactive original wallet ${originalWallet.id} to parent wallet ${parentWallet.id}. Errors: ${formattedErrors}`, - ); - } - } - - if (originalWallet.tokenBlocked) { - // Block wallet again if original wallet was blocked - await this.postToggleBlockWallet( - originalWallet.tokenCode, - { - reasonCode: IntersolveBlockTokenReasonCodeEnum.BLOCK_GENERAL, - }, - true, - ); - } - } - } - - //////////////////////////////////////////////////////////////////////////////// - //////////////////////////// QUERY HELPER FUNCTIONS //////////////////////////// - //////////////////////////////////////////////////////////////////////////////// - - private async getFundingTokenCodeForProgram( - queryRunner: QueryRunner, - programId: number, - ): Promise { - const fundingTokenCodeConfig = await queryRunner.query( - ` - SELECT * - FROM "121-service"."program_fsp_configuration" pfc - INNER JOIN "121-service"."financial_service_provider" f ON pfc."fspId" = f."id" - WHERE pfc."programId" = $1 - AND pfc."name" = $2 - AND f."fsp" = $3 - `, - [ - programId, - FinancialServiceProviderConfigurationEnum.fundingTokenCode, - FinancialServiceProviders.intersolveVisa, - ], - ); - - if (!fundingTokenCodeConfig || fundingTokenCodeConfig.length === 0) { - throw new Error( - `No fundingTokenCode found for program ${programId}. Please update the program FSP cofinguration.`, - ); - } - return fundingTokenCodeConfig[0]?.value as string; - } - - private async getBrandcodeForProgram( - queryRunner: QueryRunner, - programId: number, - ): Promise { - const brandCodeConfig = await queryRunner.query( - ` - SELECT * - FROM "121-service"."program_fsp_configuration" pfc - INNER JOIN "121-service"."financial_service_provider" f ON pfc."fspId" = f."id" - WHERE pfc."programId" = $1 - AND pfc."name" = $2 - AND f."fsp" = $3 - `, - [ - programId, - FinancialServiceProviderConfigurationEnum.brandCode, - FinancialServiceProviders.intersolveVisa, - ], - ); - - if (!brandCodeConfig || brandCodeConfig.length === 0) { - throw new Error( - `No brandCode found for program ${programId}. Please update the program FSP cofinguration.`, - ); - } - return brandCodeConfig[0]?.value as string; - } - - private async selectProgramIdsForInstanceWithVisa(): Promise { - // if intersolve_visa is not enabled, return empty array - if (!process.env.INTERSOLVE_VISA_API_URL) { - return []; - } - // We know that program 2 and 3 are the programs with intersolve_visa on the NLRC instance. - return [2, 3]; - } - - private async selectVisaCustomers( - programId: number, - queryRunner: QueryRunner, - limit: number, - ): Promise { - return queryRunner.query( - `select - i.*, - r."referenceId" - from - "121-service"."intersolve_visa_customer" i - left join "121-service".registration r on - r.id = i."registrationId" - LEFT JOIN "121-service"."intersolve_migration_progress" imp ON r."referenceId" = imp."referenceId" - WHERE "programId" = ${programId} AND imp."referenceId" IS NULL - LIMIT ${limit ?? 'ALL'}`, - ); - } - - private async selectOriginalWallets( - visaCustomer: VisaCustomerWithReferenceId, - queryRunner: QueryRunner, - ): Promise { - return queryRunner.query( - `SELECT * FROM "121-service"."intersolve_visa_wallet" WHERE "intersolveVisaCustomerId" = ${visaCustomer.id} order by "created" desc`, - ); - } - - private formatErrors(errors: unknown): string { - return errors - ? Object.values(errors) - .map((value) => { - const code = value.code ? `${value.code}` : ''; - const field = value.field ? `(${value.field}) ` : ''; - const description = value.description - ? `: ${value.description}` - : ''; - return [code, field, description].filter(Boolean).join(' '); - }) - .join(', ') - : 'Unknown error'; - } - - //////////////////////////////////////////////////////////////////////////////// - //////////////////////////// HTTPS HELPER FUNCTIONS //////////////////////////// - //////////////////////////////////////////////////////////////////////////////// - - public async getAuthenticationToken() { - if (process.env.MOCK_INTERSOLVE) { - return 'mocked-token'; - } - if (this.isTokenValid(this.tokenSet)) { - // Return cached token - return this.tokenSet.access_token; - } - // If not valid, request new token - const trustIssuer = await Issuer.discover( - `${process.env.INTERSOLVE_VISA_OIDC_ISSUER}/.well-known/openid-configuration`, - ); - const client = new trustIssuer.Client({ - client_id: process.env.INTERSOLVE_VISA_CLIENT_ID!, - client_secret: process.env.INTERSOLVE_VISA_CLIENT_SECRET!, - }); - const tokenSet = await client.grant({ - grant_type: 'client_credentials', - }); - // Cache tokenSet - this.tokenSet = tokenSet; - return tokenSet.access_token; - } - - private isTokenValid( - tokenSet: TokenSet, - ): tokenSet is TokenSet & Required> { - if (!tokenSet || !tokenSet.expires_at) { - return false; - } - // Convert expires_at to milliseconds - const expiresAtInMs = tokenSet.expires_at * 1000; - const timeLeftBeforeExpire = expiresAtInMs - Date.now(); - // If more than 1 minute left before expiration, the token is considered valid - return timeLeftBeforeExpire > 60000; - } - - public async postCreateActiveWallet( - payload: IssueTokenRequestIntersolveApiDto, - brandCode: string, - ): Promise { - const authToken = await this.getAuthenticationToken(); - - const apiPath = process.env.INTERSOLVE_VISA_PROD - ? 'pointofsale-payments' - : 'pointofsale'; - const url = `${intersolveVisaApiUrl}/${apiPath}/v1/brand-types/${brandCode}/issue-token?includeBalances=true`; - const headers = [ - { name: 'Authorization', value: `Bearer ${authToken}` }, - { name: 'Tenant-ID', value: process.env.INTERSOLVE_VISA_TENANT_ID }, - ]; - return await this.httpService.post( - url, - payload, - headers, - ); - } - - public async postLinkCustomerToWallet( - payload: { - holderId: string | null; - }, - tokenCode: string | null, - ): Promise<{ - data: { - success?: boolean; - errors?: ErrorsInResponseIntersolveApi[]; - code?: string; - correlationId?: string; - }; - status: number; - statusText?: string; - }> { - const authToken = await this.getAuthenticationToken(); - const apiPath = process.env.INTERSOLVE_VISA_PROD - ? 'wallet-payments' - : 'wallet'; - const url = `${intersolveVisaApiUrl}/${apiPath}/v1/tokens/${tokenCode}/register-holder`; - const headers = [ - { name: 'Authorization', value: `Bearer ${authToken}` }, - { name: 'Tenant-ID', value: process.env.INTERSOLVE_VISA_TENANT_ID }, - ]; - // On success this returns a 204 No Content - return await this.httpService.post(url, payload, headers); - } - - private isSuccessResponseStatus(status: number): boolean { - return status >= 200 && status < 300; - } - - public async postToggleBlockWallet( - tokenCode: string | null, - payload: { - reasonCode: IntersolveBlockTokenReasonCodeEnum; - }, - block: boolean, - ): Promise<{ - data: { - success?: boolean; - errors?: ErrorsInResponseIntersolveApi[]; - code?: string; - correlationId?: string; - }; - status: number; - statusText?: string; - }> { - const authToken = await this.getAuthenticationToken(); - const apiPath = process.env.INTERSOLVE_VISA_PROD - ? 'pointofsale-payments' - : 'pointofsale'; - const url = `${intersolveVisaApiUrl}/${apiPath}/v1/tokens/${tokenCode}/${ - block ? 'block' : 'unblock' - }`; - const headers = [ - { name: 'Authorization', value: `Bearer ${authToken}` }, - { name: 'Tenant-ID', value: process.env.INTERSOLVE_VISA_TENANT_ID }, - ]; - const blockResult = await this.httpService.post(url, payload, headers); - const result = { - status: blockResult.status, - statusText: blockResult.statusText, - data: blockResult.data, - }; - return result; - } - - public async postLinkToken( - childTokenCode: string | null, - parentTokenCode: string | null, - ): Promise<{ - data: { - success?: boolean; - errors?: ErrorsInResponseIntersolveApi[]; - code?: string; - correlationId?: string; - }; - status: number; - statusText?: string; - }> { - const authToken = await this.getAuthenticationToken(); - const apiPath = process.env.INTERSOLVE_VISA_PROD - ? 'wallet-payments' - : 'wallet'; - const url = `${intersolveVisaApiUrl}/${apiPath}/v1/tokens/${parentTokenCode}/link-token`; - const headers = [ - { name: 'Authorization', value: `Bearer ${authToken}` }, - { name: 'Tenant-ID', value: process.env.INTERSOLVE_VISA_TENANT_ID }, - ]; - const payload = { - tokenCode: childTokenCode, - }; - const linkResult = await this.httpService.post(url, payload, headers); - return linkResult; - } - - public async postTransfer( - parentTokenCode: string, - amountInCent: number, - fundingTokenCode: string, - ): Promise<{ - data: { - success?: boolean; - errors?: ErrorsInResponseIntersolveApi[]; - code?: string; - correlationId?: string; - }; - status: number; - statusText?: string; - }> { - const authToken = await this.getAuthenticationToken(); - const apiPath = process.env.INTERSOLVE_VISA_PROD - ? 'wallet-payments' - : 'wallet'; - const url = `${intersolveVisaApiUrl}/${apiPath}/v1/tokens/${fundingTokenCode}/transfer`; - const headers = [ - { name: 'Authorization', value: `Bearer ${authToken}` }, - { name: 'Tenant-ID', value: process.env.INTERSOLVE_VISA_TENANT_ID }, - ]; - - const payload = { - quantity: { - value: amountInCent, - assetCode: process.env.INTERSOLVE_VISA_ASSET_CODE!, - }, - creditor: { - tokenCode: parentTokenCode, - }, - reference: `ParentTokenCode=${parentTokenCode}`, // Should be 50 characters or less! - operationReference: uuid(), // Required to pass in a UUID, which needs be unique for all transfers. Is used as idempotency key. - }; - - const transferResult = await this.httpService.post( - url, - payload, - headers, - ); - return transferResult; - } -} diff --git a/services/121-service/src/migration/1643720490970-remove-migrate-name-partner-org.ts b/services/121-service/src/migration/1643720490970-remove-migrate-name-partner-org.ts index ff926d5fb5..c67df7fb08 100755 --- a/services/121-service/src/migration/1643720490970-remove-migrate-name-partner-org.ts +++ b/services/121-service/src/migration/1643720490970-remove-migrate-name-partner-org.ts @@ -1,9 +1,4 @@ -import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm'; - -import { CustomAttributeType } from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class removeMigrateNamePartnerOrg1643720490970 implements MigrationInterface @@ -11,7 +6,6 @@ export class removeMigrateNamePartnerOrg1643720490970 name = 'removeMigrateNamePartnerOrg1643720490970'; public async up(queryRunner: QueryRunner): Promise { - await this.migrateData(queryRunner.manager); await queryRunner.commitTransaction(); await queryRunner.startTransaction(); await queryRunner.query( @@ -30,48 +24,4 @@ export class removeMigrateNamePartnerOrg1643720490970 `ALTER TABLE "121-service"."registration" ADD "namePartnerOrganization" character varying`, ); } - - private async migrateData(manager: EntityManager): Promise { - const programRepository = manager.getRepository(ProgramEntity); - const registrationRepository = manager.getRepository(RegistrationEntity); - const programCustomAttributeRepository = manager.getRepository( - ProgramCustomAttributeEntity, - ); - const regsWithPartnerOrg = await manager - .getRepository(RegistrationEntity) - .createQueryBuilder('registration') - .select('registration.*') - .where('"namePartnerOrganization" is not null') - .getRawMany(); - for (const r of regsWithPartnerOrg) { - r.customData['namePartnerOrganization'] = r['namePartnerOrganization']; - await registrationRepository.save(r); - } - - const programs = await manager - .getRepository(ProgramEntity) - .createQueryBuilder('program') - .leftJoinAndSelect( - 'program.programCustomAttributes', - 'programCustomAttributes', - ) - .select(['program.id']) - .getMany(); - for (const program of programs) { - // Then namePartnerOrganization is part of this programCustomAttributes - if (regsWithPartnerOrg.length > 0) { - const attributeReturn = await programCustomAttributeRepository.save({ - name: 'namePartnerOrganization', - type: CustomAttributeType.text, - label: JSON.parse( - JSON.stringify({ - en: 'Partner Organization', - }), - ), - }); - program.programCustomAttributes.push(attributeReturn); - await programRepository.save(program); - } - } - } } diff --git a/services/121-service/src/migration/1654693178991-phasesAndEditableProperties.ts b/services/121-service/src/migration/1654693178991-phasesAndEditableProperties.ts index a6a59c9f3f..e0fbf4f8ac 100755 --- a/services/121-service/src/migration/1654693178991-phasesAndEditableProperties.ts +++ b/services/121-service/src/migration/1654693178991-phasesAndEditableProperties.ts @@ -1,10 +1,4 @@ -import fs from 'fs'; -import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm'; - -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class PhasesAndEditableProperties1654693178991 implements MigrationInterface @@ -51,85 +45,4 @@ export class PhasesAndEditableProperties1654693178991 `ALTER TABLE "121-service"."fsp_attribute" DROP COLUMN "phases"`, ); } - - private async migrateData(manager: EntityManager): Promise { - let programPilotNL, programPilotNL2, fspIntersolve; - try { - // refactor: if encountering issues with these imports, consider refactoring similarly to what was done in /~https://github.com/global-121/121-platform/pull/5192/files - programPilotNL = JSON.parse( - fs.readFileSync('seed-data/program/program-pilot-nl.json', 'utf8'), - ); - programPilotNL2 = JSON.parse( - fs.readFileSync('seed-data/program/program-pilot-nl-2.json', 'utf8'), - ); - // fspIntersolve = JSON.parse( - // fs.readFileSync('seed-data/fsp/fsp-intersolve.json', 'utf8'), - // ); - } catch { - console.log( - 'NLRC programs not found. Not migrating phases and editable properties for NLRC program', - ); - } - if (programPilotNL && programPilotNL2 && fspIntersolve) { - const programRepo = manager.getRepository(ProgramEntity); - const programQuestionsRepo = manager.getRepository(ProgramQuestionEntity); - const customAttributesRepo = manager.getRepository( - ProgramCustomAttributeEntity, - ); - const program = await programRepo - .createQueryBuilder('program') - .leftJoin('program.programQuestions', 'programQuestion') - .leftJoin('program.programCustomAttributes', 'programCustomAttribute') - .select('program.id') - .addSelect('program.titlePortal') - .addSelect('program.ngo') - .addSelect('programQuestion.id') - .addSelect('programQuestion.name') - .addSelect('programQuestion.phases') - .addSelect('programQuestion.editableInPortal') - .addSelect('programCustomAttribute.id') - .addSelect('programCustomAttribute.name') - .addSelect('programCustomAttribute.phases') - .where(`ngo = 'NLRC'`) - .getOne(); - - if (program) { - let programJson; - if (program.titlePortal!['en'] === programPilotNL.titlePortal.en) { - programJson = programPilotNL; - } - if (program.titlePortal!['en'] === programPilotNL2.titlePortal.en) { - programJson = programPilotNL2; - } - for (const q of program.programQuestions) { - const qJson = programJson.programQuestions.find( - (qJson) => qJson.name === q.name, - ); - q.editableInPortal = qJson.editableInPortal; - await programQuestionsRepo.save(q); - } - for (const ca of program.programCustomAttributes) { - programJson.programCustomAttributes.find( - (caJson) => caJson.name === ca.name, - ); - await customAttributesRepo.save(ca); - } - } - - const fspAttributeRepo = manager.getRepository(FspQuestionEntity); - - const fspAttributes = await fspAttributeRepo - .createQueryBuilder('fspAttribute') - .select('fspAttribute.id') - .addSelect('fspAttribute.name') - .addSelect('fspAttribute.phases') - .getMany(); - - for (const fspAttribute of fspAttributes) { - if (fspAttribute.name === fspIntersolve.attributes[0].name) { - await fspAttributeRepo.save(fspAttribute); - } - } - } - } } diff --git a/services/121-service/src/migration/1656412499569-registrationData.ts b/services/121-service/src/migration/1656412499569-registrationData.ts index 4b21c0a901..dbe9a38ab7 100755 --- a/services/121-service/src/migration/1656412499569-registrationData.ts +++ b/services/121-service/src/migration/1656412499569-registrationData.ts @@ -1,50 +1,6 @@ -import fs from 'fs'; -import { - Check, - Column, - Entity, - EntityManager, - MigrationInterface, - OneToMany, - OneToOne, - QueryRunner, -} from 'typeorm'; - -import { CascadeDeleteEntity } from '@121-service/src/base.entity'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { OrganizationEntity } from '@121-service/src/organization/organization.entity'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; -import { NameConstraintQuestions } from '@121-service/src/shared/const'; +import { MigrationInterface, QueryRunner } from 'typeorm'; // This entity was copied here during the deletion of monitoringQuestions and everything related to it -@Entity('monitoring_question') -@Check(`"name" NOT IN (${NameConstraintQuestions})`) -class MonitoringQuestionEntity extends CascadeDeleteEntity { - @Column() - public name: string; - - @Column('json') - public intro: JSON; - - @Column('json') - public conclusion: JSON; - - @Column('json', { nullable: true }) - public options: JSON | null; - - // @ts-expect-error monitoringQuestion has been removed since - @OneToOne(() => OrganizationEntity, (instance) => instance.monitoringQuestion) - public instance: OrganizationEntity; - - @OneToMany( - () => RegistrationDataEntity, - // @ts-expect-error monitoringQuestion has been removed since - (registrationData) => registrationData.monitoringQuestion, - ) - public registrationData: RegistrationDataEntity[]; -} export class registrationData1656412499569 implements MigrationInterface { name = 'registrationData1656412499569'; @@ -142,147 +98,4 @@ export class registrationData1656412499569 implements MigrationInterface { `ALTER TABLE "121-service"."program" DROP COLUMN "fullnameNamingConvention"`, ); } - - private async migrateData(manager: EntityManager): Promise { - const programRepo = manager.getRepository(ProgramEntity); - const registrationRepo = manager.getRepository(RegistrationEntity); - const fspAttributeRepo = manager.getRepository(FspQuestionEntity); - const registrationDataRepo = manager.getRepository(RegistrationDataEntity); - const instanceRepo = manager.getRepository(OrganizationEntity); - const monQuestionRepo = manager.getRepository(MonitoringQuestionEntity); - - let instancePilotLVV, instancePilotPV; - try { - // refactor: if encountering issues with these imports, consider refactoring similarly to what was done in /~https://github.com/global-121/121-platform/pull/5192/files - instancePilotLVV = JSON.parse( - fs.readFileSync('seed-data/instance/instance-pilot-nl.json', 'utf8'), - ); - instancePilotPV = JSON.parse( - fs.readFileSync('seed-data/instance/instance-pilot-nl-2.json', 'utf8'), - ); - } catch { - console.log( - 'NLRC programs not found. Not migrating phases and editable properties for NLRC program', - ); - } - if (!instancePilotLVV || !instancePilotPV) { - return; - } - - const registrations = await registrationRepo - .createQueryBuilder('registration') - .select('registration.*') - .getRawMany(); - - if (registrations) { - const fspAttributes = await fspAttributeRepo - .createQueryBuilder('fspAttribute') - .select('fspAttribute.id') - .addSelect('fspAttribute.name') - .getMany(); - - const program = await programRepo - .createQueryBuilder('program') - .leftJoin('program.programQuestions', 'programQuestion') - .leftJoin('program.programCustomAttributes', 'programCustomAttribute') - .select('program.id') - .addSelect('program.titlePortal') - .addSelect('program.ngo') - .addSelect('programQuestion.id') - .addSelect('programQuestion.name') - .addSelect('programCustomAttribute.id') - .addSelect('programCustomAttribute.name') - .where(`ngo = 'NLRC'`) - .getOne(); - - const instance = await instanceRepo - .createQueryBuilder('instance') - .select('instance.id') - .getOne(); - - if (fspAttributes && program && instance) { - const programTitle = Object.assign(program.titlePortal!); - const titleString = programTitle['en']; - const monitoringQuestion = new MonitoringQuestionEntity(); - monitoringQuestion.name = 'monitoringAnswer'; - program.fullnameNamingConvention = JSON.parse( - JSON.stringify(['nameFirst', 'nameLast']), - ); - await programRepo.save(program); - if (titleString === 'NLRC Direct Digital Aid (LVV)') { - // Use LVV monitoring question - const monQuestion = instancePilotLVV['monitoringQuestion']; - monitoringQuestion.intro = monQuestion['intro']; - monitoringQuestion.options = monQuestion['options']; - monitoringQuestion.conclusion = monQuestion['conclusion']; - - // @ts-expect-error monitoringQuestion has been removed since - instance.monitoringQuestion = monitoringQuestion; - await monQuestionRepo.save(monitoringQuestion); - await instanceRepo.save(instance); - } else if (titleString === 'NLRC Direct Digital Aid Program (PV)') { - // Use PV monitoring question - const monQuestion = instancePilotPV['monitoringQuestion']; - monitoringQuestion.intro = monQuestion['intro']; - monitoringQuestion.options = monQuestion['options']; - monitoringQuestion.conclusion = monQuestion['conclusion']; - - // @ts-expect-error monitoringQuestion has been removed since - instance.monitoringQuestion = monitoringQuestion; - await monQuestionRepo.save(monitoringQuestion); - await instanceRepo.save(instance); - } - - for (const registration of registrations) { - const customDataObject = Object.assign(registration.customData); - for await (const key of Object.keys(customDataObject)) { - const registrationData = new RegistrationDataEntity(); - registrationData.registrationId = registration.id; - switch (key) { - // Program Questions - case 'nameFirst': - case 'nameLast': - case 'vnumber': - case 'phoneNumber': - const q = program.programQuestions.find((q) => q.name === key); - if (q) { - registrationData.programQuestion = q; - registrationData.value = registration.customData[key]; - } - await registrationDataRepo.save(registrationData); - break; - // FSP attribute(s) - case 'whatsappPhoneNumber': - const attr = fspAttributes.find((att) => att.name === key); - if (attr) { - registrationData.fspQuestion = attr; - registrationData.value = registration.customData[key]; - } - await registrationDataRepo.save(registrationData); - break; - // Custom data - case 'namePartnerOrganization': - const ca = program.programCustomAttributes.find( - (q) => q.name === key, - ); - if (ca) { - registrationData.programCustomAttribute = ca; - registrationData.value = registration.customData[key]; - } - await registrationDataRepo.save(registrationData); - break; - // Monitoring question - case 'monitoringAnswer': - // @ts-expect-error monitoringQuestion has been removed since - registrationData.monitoringQuestion = monitoringQuestion; - registrationData.value = registration.customData[key]; - - await registrationDataRepo.save(registrationData); - break; - } - } - } - } - } - } } diff --git a/services/121-service/src/migration/1708330965061-migrate-data-changes-to-event.ts b/services/121-service/src/migration/1708330965061-migrate-data-changes-to-event.ts index 3a2c9e6f52..57c1c8c3b4 100644 --- a/services/121-service/src/migration/1708330965061-migrate-data-changes-to-event.ts +++ b/services/121-service/src/migration/1708330965061-migrate-data-changes-to-event.ts @@ -31,7 +31,6 @@ export class MigrateDataChangesToEvent1708330965061 await queryRunner.query( `DROP INDEX "121-service"."IDX_5c7356500932acbd5f76a787ce"`, ); - console.time('migrateData'); const manager = queryRunner.manager; const eventRepo = manager.getRepository(EventEntity); const keysToMigrate = ['fieldName', 'oldValue', 'newValue', 'reason']; @@ -70,6 +69,5 @@ export class MigrateDataChangesToEvent1708330965061 await queryRunner.query( `DROP TABLE IF EXISTS "121-service".registration_change_log `, ); - console.timeEnd('migrateData'); } } diff --git a/services/121-service/src/migration/1708330966062-migrate-status-changes-to-event.ts b/services/121-service/src/migration/1708330966062-migrate-status-changes-to-event.ts index a878a0d482..a6337adfb1 100644 --- a/services/121-service/src/migration/1708330966062-migrate-status-changes-to-event.ts +++ b/services/121-service/src/migration/1708330966062-migrate-status-changes-to-event.ts @@ -25,7 +25,6 @@ export class MigrateStatusChangesToEvent1708330966062 public async down(_queryRunner: QueryRunner): Promise {} private async migrateStatusChanges(queryRunner: QueryRunner): Promise { - console.time('migrateStatusChanges'); const manager = queryRunner.manager; const eventRepo = manager.getRepository(EventEntity); const adminUser = await queryRunner.query( @@ -75,6 +74,5 @@ export class MigrateStatusChangesToEvent1708330966062 }); await eventRepo.save(events, { chunk: 300 }); - console.timeEnd('migrateStatusChanges'); } } diff --git a/services/121-service/src/migration/1710080807910-migrate-fsp-display-name-portal-data-to-display-name.ts b/services/121-service/src/migration/1710080807910-migrate-fsp-display-name-portal-data-to-display-name.ts index ea1dc56f73..b090417828 100644 --- a/services/121-service/src/migration/1710080807910-migrate-fsp-display-name-portal-data-to-display-name.ts +++ b/services/121-service/src/migration/1710080807910-migrate-fsp-display-name-portal-data-to-display-name.ts @@ -1,12 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -interface FinancialServiceProvider { - id: number; - fspDisplayNamePortal?: string; - displayName?: Record; -} - export class MigrateFspDisplayNamePortalDataToDisplayName1710080807910 implements MigrationInterface { @@ -16,7 +9,7 @@ export class MigrateFspDisplayNamePortalDataToDisplayName1710080807910 // Commit transaction because the tables are needed before the insert await queryRunner.commitTransaction(); // migrate existing data from fspDisplayNamePaApp / fspDisplayNamePortal to displayName - await this.migrateFspDisplayName(queryRunner); + // await this.migrateFspDisplayName(queryRunner); await queryRunner.startTransaction(); } @@ -25,40 +18,4 @@ export class MigrateFspDisplayNamePortalDataToDisplayName1710080807910 `UPDATE "121-service"."financial_service_provider" SET "displayName" = NULL`, ); } - - private async migrateFspDisplayName(queryRunner: QueryRunner): Promise { - const manager = queryRunner.manager; - const financialServiceProviderRepo = manager.getRepository( - FinancialServiceProviderEntity, - ); - - const existingData: FinancialServiceProvider[] = await queryRunner.query( - `SELECT * FROM "121-service"."financial_service_provider" ORDER BY "id" ASC`, - ); - - const financialServiceProviders = existingData.map( - (financialServiceProvider) => { - if ( - financialServiceProvider?.fspDisplayNamePortal && - typeof financialServiceProvider?.fspDisplayNamePortal === 'string' - ) { - try { - financialServiceProvider.displayName = JSON.parse( - financialServiceProvider?.fspDisplayNamePortal, - ); - } catch (error) { - financialServiceProvider.displayName = { - en: financialServiceProvider?.fspDisplayNamePortal, - }; - } - } - - return financialServiceProvider; - }, - ); - - await financialServiceProviderRepo.save(financialServiceProviders, { - chunk: 300, - }); - } } diff --git a/services/121-service/src/migration/1713363871246-remove-startedRegistration-state.ts b/services/121-service/src/migration/1713363871246-remove-startedRegistration-state.ts index 9454e34b32..72a796b168 100644 --- a/services/121-service/src/migration/1713363871246-remove-startedRegistration-state.ts +++ b/services/121-service/src/migration/1713363871246-remove-startedRegistration-state.ts @@ -8,7 +8,6 @@ export class RemoveStartedRegistrationState1713363871246 name = 'RemoveStartedRegistrationState1713363871246'; public async up(queryRunner: QueryRunner): Promise { - console.time('RemoveStartedRegistrationState1713363871246'); await queryRunner.commitTransaction(); await queryRunner.query(`DELETE FROM "121-service".note WHERE "registrationId" IN @@ -32,7 +31,6 @@ export class RemoveStartedRegistrationState1713363871246 ); await queryRunner.startTransaction(); - console.timeEnd('RemoveStartedRegistrationState1713363871246'); } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/services/121-service/src/migration/1714467563401-remove-deleted-state-from-registration.ts b/services/121-service/src/migration/1714467563401-remove-deleted-state-from-registration.ts index d0e39f1a31..81c790408c 100644 --- a/services/121-service/src/migration/1714467563401-remove-deleted-state-from-registration.ts +++ b/services/121-service/src/migration/1714467563401-remove-deleted-state-from-registration.ts @@ -6,7 +6,6 @@ export class RemoveDeletedStateFromRegistration1714467563401 name = 'RemoveDeletedStateFromRegistration1714467563401'; public async up(queryRunner: QueryRunner): Promise { - console.time('RemoveDeletedStateFromRegistration1714467563401'); await queryRunner.commitTransaction(); const queryCondition = `"fspId" is null AND "registrationStatus" = 'deleted'`; @@ -44,7 +43,6 @@ export class RemoveDeletedStateFromRegistration1714467563401 ); await queryRunner.startTransaction(); - console.timeEnd('RemoveDeletedStateFromRegistration1714467563401'); } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/services/121-service/src/migration/1729605362361-program-registration-attribute-refactor.ts b/services/121-service/src/migration/1729605362361-program-registration-attribute-refactor.ts new file mode 100644 index 0000000000..43c29c8af8 --- /dev/null +++ b/services/121-service/src/migration/1729605362361-program-registration-attribute-refactor.ts @@ -0,0 +1,616 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProgramRegistrationAttributeRefactor1729605362361 + implements MigrationInterface +{ + name = 'ProgramRegistrationAttributeRefactor1729605362361'; + + public async up(queryRunner: QueryRunner): Promise { + console.time('Migration'); + await this.createNewTablesAndViews(queryRunner); + + await this.migrateFspConig(queryRunner); + await this.migrateQuestionsToProgramRegistrationAttributes(queryRunner); + await this.addConstraints(queryRunner); + + await this.checkRegistrationFspConfigMigrations(queryRunner); + await this.checkTransactionFspConfigMigrations(queryRunner); + + await this.adjustPermissions(queryRunner); + + await this.dropOldTablesAndViews(queryRunner); + console.timeEnd('Migration'); + // throw new Error('You shall not pass! Use this to prevent the migration from passing'); + } + private async createNewTablesAndViews( + queryRunner: QueryRunner, + ): Promise { + await queryRunner.query( + `ALTER TABLE "121-service"."registration" DROP CONSTRAINT "FK_9e5a5ef99940e591cad5b25a345"`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."transaction" DROP CONSTRAINT "FK_ba98ea5ca43ebe54f60c5aaabec"`, + ); + await queryRunner.query( + `DROP INDEX "121-service"."IDX_ba98ea5ca43ebe54f60c5aaabe"`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."registration" ADD COLUMN "programFinancialServiceProviderConfigurationId" INTEGER`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."transaction" ADD COLUMN "programFinancialServiceProviderConfigurationId" INTEGER`, + ); + + await queryRunner.query( + `CREATE TABLE "121-service"."program_financial_service_provider_configuration_property" ("id" SERIAL NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "updated" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "value" character varying NOT NULL, "programFinancialServiceProviderConfigurationId" integer NOT NULL, CONSTRAINT "programFinancialServiceProviderConfigurationPropertyUnique" UNIQUE ("programFinancialServiceProviderConfigurationId", "name"), CONSTRAINT "PK_01dfd2b0e5d93a8cf4090254cfc" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_2ea95dd85e592bad75d0278873" ON "121-service"."program_financial_service_provider_configuration_property" ("created") `, + ); + await queryRunner.query( + `CREATE TABLE "121-service"."program_financial_service_provider_configuration" ("id" SERIAL NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "updated" TIMESTAMP NOT NULL DEFAULT now(), "programId" integer NOT NULL, "financialServiceProviderName" character varying NOT NULL, "name" character varying NOT NULL, "label" json NOT NULL, CONSTRAINT "programFinancialServiceProviderConfigurationUnique" UNIQUE ("programId", "name"), CONSTRAINT "PK_bc2d4d99fa94cb01d4566acdffc" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_04aac36fce58b33d30d71b700f" ON "121-service"."program_financial_service_provider_configuration" ("created") `, + ); + await queryRunner.query( + `CREATE TABLE "121-service"."program_registration_attribute" ("id" SERIAL NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "updated" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "label" json NOT NULL, "type" character varying NOT NULL, "isRequired" boolean NOT NULL, "placeholder" json, "options" json, "scoring" json NOT NULL DEFAULT '{}', "programId" integer NOT NULL, "export" json NOT NULL DEFAULT '["all-people-affected","included"]', "pattern" character varying, "duplicateCheck" boolean NOT NULL DEFAULT false, "showInPeopleAffectedTable" boolean NOT NULL DEFAULT false, "editableInPortal" boolean NOT NULL DEFAULT false, CONSTRAINT "programAttributeUnique" UNIQUE ("name", "programId"), CONSTRAINT "CHK_88f5ede846c87b3059ed09f967" CHECK ("name" NOT IN ('id', 'status', 'referenceId', 'preferredLanguage', 'inclusionScore', 'paymentAmountMultiplier', 'financialServiceProvider', 'registrationProgramId', 'maxPayments', 'lastTransactionCreated', 'lastTransactionPaymentNumber', 'lastTransactionStatus', 'lastTransactionAmount', 'lastTransactionErrorMessage', 'lastTransactionCustomData', 'paymentCount', 'paymentCountRemaining', 'registeredDate', 'validationDate', 'inclusionDate', 'deleteDate', 'completedDate', 'lastMessageStatus', 'lastMessageType', 'declinedDate')), CONSTRAINT "PK_b85642d2f95cc2fcc6145e14463" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_1387f030d9f04f7d80c78a60d5" ON "121-service"."program_registration_attribute" ("created") `, + ); + await queryRunner.query( + `CREATE TABLE "121-service"."registration_attribute_data" ("id" SERIAL NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "updated" TIMESTAMP NOT NULL DEFAULT now(), "registrationId" integer NOT NULL, "programRegistrationAttributeId" integer NOT NULL, "value" character varying NOT NULL, CONSTRAINT "registrationProgramAttributeUnique" UNIQUE ("registrationId", "programRegistrationAttributeId"), CONSTRAINT "PK_bef7662581d64d69db3f6405411" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_29cd6ac9bf4002df266d0ba23e" ON "121-service"."registration_attribute_data" ("created") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_8914b71c0e30c44291ab68a9b8" ON "121-service"."registration_attribute_data" ("registrationId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_3037018a626cd41bd16c588170" ON "121-service"."registration_attribute_data" ("value") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_d8a56a1864ef40e1551833430b" ON "121-service"."transaction" ("programFinancialServiceProviderConfigurationId") `, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."program_financial_service_provider_configuration_property" ADD CONSTRAINT "FK_5e40569627925419cd94db0da36" FOREIGN KEY ("programFinancialServiceProviderConfigurationId") REFERENCES "121-service"."program_financial_service_provider_configuration"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."program_financial_service_provider_configuration" ADD CONSTRAINT "FK_f7400125e09c4d8fec5747ec588" FOREIGN KEY ("programId") REFERENCES "121-service"."program"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."program_registration_attribute" ADD CONSTRAINT "FK_8788ebf12909c03049a0d8c377d" FOREIGN KEY ("programId") REFERENCES "121-service"."program"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."registration_attribute_data" ADD CONSTRAINT "FK_8914b71c0e30c44291ab68a9b8a" FOREIGN KEY ("registrationId") REFERENCES "121-service"."registration"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."registration_attribute_data" ADD CONSTRAINT "FK_3bd62b57d06901bcd85e28fd060" FOREIGN KEY ("programRegistrationAttributeId") REFERENCES "121-service"."program_registration_attribute"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."registration" ADD CONSTRAINT "FK_148b6bb5c37ca2d444b01c00c2f" FOREIGN KEY ("programFinancialServiceProviderConfigurationId") REFERENCES "121-service"."program_financial_service_provider_configuration"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."transaction" ADD CONSTRAINT "FK_d8a56a1864ef40e1551833430bb" FOREIGN KEY ("programFinancialServiceProviderConfigurationId") REFERENCES "121-service"."program_financial_service_provider_configuration"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `DELETE FROM "121-service"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'registration_view', '121-service'], + ); + await queryRunner.query(`DROP VIEW "121-service"."registration_view"`); + await queryRunner.query( + `CREATE VIEW "121-service"."registration_view" AS SELECT "registration"."id" AS "id", "registration"."created" AS "registrationCreated", "registration"."programId" AS "programId", "registration"."registrationStatus" AS "status", "registration"."referenceId" AS "referenceId", "registration"."phoneNumber" AS "phoneNumber", "registration"."preferredLanguage" AS "preferredLanguage", "registration"."inclusionScore" AS "inclusionScore", "registration"."paymentAmountMultiplier" AS "paymentAmountMultiplier", "registration"."maxPayments" AS "maxPayments", "registration"."paymentCount" AS "paymentCount", "registration"."scope" AS "scope", "fspconfig"."label" AS "programFinancialServiceProviderConfigurationLabel", CAST(CONCAT('PA #',registration."registrationProgramId") as VARCHAR) AS "personAffectedSequence", registration."registrationProgramId" AS "registrationProgramId", TO_CHAR("registration"."created",'yyyy-mm-dd') AS "registrationCreatedDate", fspconfig."name" AS "programFinancialServiceProviderConfigurationName", fspconfig."id" AS "programFinancialServiceProviderConfigurationId", fspconfig."financialServiceProviderName" AS "financialServiceProviderName", "registration"."maxPayments" - "registration"."paymentCount" AS "paymentCountRemaining", COALESCE("message"."type" || ': ' || "message"."status",'no messages yet') AS "lastMessageStatus" FROM "121-service"."registration" "registration" LEFT JOIN "121-service"."program_financial_service_provider_configuration" "fspconfig" ON "fspconfig"."id"="registration"."programFinancialServiceProviderConfigurationId" LEFT JOIN "121-service"."latest_message" "latestMessage" ON "latestMessage"."registrationId"="registration"."id" LEFT JOIN "121-service"."twilio_message" "message" ON "message"."id"="latestMessage"."messageId" ORDER BY "registration"."registrationProgramId" ASC`, + ); + + await queryRunner.query( + `INSERT INTO "121-service"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + '121-service', + 'VIEW', + 'registration_view', + 'SELECT "registration"."id" AS "id", "registration"."created" AS "registrationCreated", "registration"."programId" AS "programId", "registration"."registrationStatus" AS "status", "registration"."referenceId" AS "referenceId", "registration"."phoneNumber" AS "phoneNumber", "registration"."preferredLanguage" AS "preferredLanguage", "registration"."inclusionScore" AS "inclusionScore", "registration"."paymentAmountMultiplier" AS "paymentAmountMultiplier", "registration"."maxPayments" AS "maxPayments", "registration"."paymentCount" AS "paymentCount", "registration"."scope" AS "scope", "fspconfig"."label" AS "programFinancialServiceProviderConfigurationLabel", CAST(CONCAT(\'PA #\',registration."registrationProgramId") as VARCHAR) AS "personAffectedSequence", registration."registrationProgramId" AS "registrationProgramId", TO_CHAR("registration"."created",\'yyyy-mm-dd\') AS "registrationCreatedDate", fspconfig."name" AS "programFinancialServiceProviderConfigurationName", fspconfig."id" AS "programFinancialServiceProviderConfigurationId", fspconfig."financialServiceProviderName" AS "financialServiceProviderName", "registration"."maxPayments" - "registration"."paymentCount" AS "paymentCountRemaining", COALESCE("message"."type" || \': \' || "message"."status",\'no messages yet\') AS "lastMessageStatus" FROM "121-service"."registration" "registration" LEFT JOIN "121-service"."program_financial_service_provider_configuration" "fspconfig" ON "fspconfig"."id"="registration"."programFinancialServiceProviderConfigurationId" LEFT JOIN "121-service"."latest_message" "latestMessage" ON "latestMessage"."registrationId"="registration"."id" LEFT JOIN "121-service"."twilio_message" "message" ON "message"."id"="latestMessage"."messageId" ORDER BY "registration"."registrationProgramId" ASC', + ], + ); + } + + private async adjustPermissions(queryRunner: QueryRunner) { + // Step 1: Select the IDs of the permissions to be deleted + const permissionIds = await queryRunner.query(` + SELECT id FROM "121-service".permission WHERE "name" IN ('program:question.update', 'program:question.delete', 'program:custom-attribute.update') + `); + + if (permissionIds.length > 0) { + const ids = permissionIds + .map((permission: { id: number }) => permission.id) + .join(','); + + // Step 2: Delete entries from user_role_permissions_permission table + await queryRunner.query(` + DELETE FROM "121-service".user_role_permissions_permission WHERE "permissionId" IN (${ids}) + `); + + // Step 3: Delete the permissions from the permission table + await queryRunner.query(` + DELETE FROM "121-service".permission WHERE "id" IN (${ids}) + `); + } + + // Step 4: Rename the permission 'registration:fsp.update' to 'registration:fsp-config.update' + await queryRunner.query(` + UPDATE "121-service".permission + SET "name" = 'registration:fsp-config.update' + WHERE "name" = 'registration:fsp.update' + `); + } + + private async migrateFspConig(queryRunner: QueryRunner) { + const programFspAssignements = await queryRunner.query(` + SELECT + pfspfsp."programId", + pfspfsp."financialServiceProviderId", + fsp.fsp AS "financialServiceProviderName", + fsp.id AS "financialServiceProviderId", + fsp."displayName" AS "financialServiceProviderNameDisplayName" + FROM + "121-service".program_financial_service_providers_financial_service_provider AS pfspfsp + LEFT JOIN + "121-service".financial_service_provider AS fsp + ON + pfspfsp."financialServiceProviderId" = fsp.id`); + for (const assignemt of programFspAssignements) { + const oldFspConfigDisplayName = await this.getOldFspConfigDisplayName( + queryRunner, + assignemt.programId, + assignemt.financialServiceProviderId, + ); + // write some code to migrate displays names from the old fsp config setup + const newFspConfig = { + created: new Date(), + updated: new Date(), + programId: assignemt.programId, + financialServiceProviderName: assignemt.financialServiceProviderName, + name: assignemt.financialServiceProviderName, + label: + oldFspConfigDisplayName ?? + JSON.stringify({ en: assignemt.financialServiceProviderName }), + }; + // raw query insert + const insterFspConfigResult = await queryRunner.query(` + INSERT INTO "121-service".program_financial_service_provider_configuration ( + created, + updated, + "programId", + "financialServiceProviderName", + name, + label + ) + VALUES ( + now(), + now(), + ${newFspConfig.programId}, + '${newFspConfig.financialServiceProviderName}', + '${newFspConfig.name}', + '${newFspConfig.label}' + ) + RETURNING id + `); + + const insertedId = insterFspConfigResult[0].id; + + // Update the transaction table so transactions are related to program_financial_service_provider_configuration instead of fspId + await queryRunner.query(` + UPDATE "121-service".transaction + SET "programFinancialServiceProviderConfigurationId" = ${insertedId} + WHERE "programId" = ${assignemt.programId} + AND "financialServiceProviderId" = ${assignemt.financialServiceProviderId}`); + // do the same per registration + await queryRunner.query(` + UPDATE "121-service".registration + SET "programFinancialServiceProviderConfigurationId" = ${insertedId} + WHERE "programId" = ${assignemt.programId} + AND "fspId" = ${assignemt.financialServiceProviderId}`); + // migrate old fsp config properties to the new fsp config properties + await this.migrateOldFspConfigToFspConfigProperties( + queryRunner, + insertedId, + assignemt.programId, + assignemt.financialServiceProviderId, + ); + } + + // migrate transactions to relate to the new fsp config instead of fsp + } + + private async migrateOldFspConfigToFspConfigProperties( + queryRunner: QueryRunner, + newFspConfigId: number, + programId: number, + fspId: number, + ): Promise { + const oldFspConfig = await queryRunner.query(` + SELECT + * + FROM + "121-service".program_fsp_configuration + WHERE + "fspId" = ${fspId} + AND + "programId" = ${programId} + AND name != 'displayName'`); + + for (const config of oldFspConfig) { + // insert the old fsp config in the new table + await queryRunner.query(` + INSERT INTO "121-service".program_financial_service_provider_configuration_property ( + name, + value, + "programFinancialServiceProviderConfigurationId" + ) + VALUES ( + '${config.name}', + '${config.value}', + ${newFspConfigId} + )`); + } + } + + private async getOldFspConfigDisplayName( + queryRunner: QueryRunner, + programId: number, + fspId: number, + ): Promise { + const oldFspConfig = await queryRunner.query(` + SELECT + * + FROM + "121-service".program_fsp_configuration + WHERE + "fspId" = ${fspId} + AND + "programId" = ${programId} + AND + "name" = 'displayName'`); + return oldFspConfig[0]?.value; + + // get the display name from the old fsp config + } + + private async migrateQuestionsToProgramRegistrationAttributes( + queryRunner: QueryRunner, + ): Promise { + await queryRunner.query(` + INSERT INTO "121-service".program_registration_attribute ( + created, + updated, + name, + label, + type, + "isRequired", + placeholder, + options, + scoring, + "programId", + export, + pattern, + "duplicateCheck", + "showInPeopleAffectedTable", + "editableInPortal" + ) + SELECT + created, + updated, + name, + label, + type, + false AS "isRequired", -- Set a default value as it's not present in the old table + NULL::json AS placeholder, -- Set to NULL as it's not present in the old table + NULL::json AS options, -- Set to NULL as it's not present in the old table + '{}'::json AS scoring, -- Set default empty JSON as specified in the new table + "programId", + '["all-people-affected","included"]'::json AS export, -- Default value as specified in the new table + NULL::character varying AS pattern, -- Set to NULL as it's not present in the old table + "duplicateCheck", + "showInPeopleAffectedTable", + true AS "editableInPortal" -- Set as true value as it's not present in the old table + FROM + "121-service".program_custom_attribute`); + + await queryRunner.query(` + INSERT INTO "121-service".program_registration_attribute ( + created, + updated, + name, + label, + type, + "isRequired", + placeholder, + options, + scoring, + "programId", + export, + pattern, + "duplicateCheck", + "showInPeopleAffectedTable", + "editableInPortal" + ) + SELECT + created, + updated, + name, + label, + "answerType" as type, + false AS "isRequired", + placeholder, + options, + scoring, + "programId", + export, + pattern, + "duplicateCheck", + "showInPeopleAffectedTable", + "editableInPortal" + FROM + "121-service".program_question + `); + + await queryRunner.query(` + INSERT INTO "121-service".program_registration_attribute ( + created, + updated, + name, + label, + type, + "isRequired", + placeholder, + options, + scoring, + "programId", + export, + pattern, + "duplicateCheck", + "showInPeopleAffectedTable", + "editableInPortal" + ) + SELECT + fspq.created, + fspq.updated, + fspq.name, + fspq.label, + fspq."answerType", + false AS "isRequired", -- Set a default value as it's not present in the old table + fspq.placeholder, + fspq.options, + '{}'::json AS scoring, -- Set default empty JSON as specified in the new table + pfsp."programId", + fspq.export, + fspq.pattern, + fspq."duplicateCheck", + fspq."showInPeopleAffectedTable", + false AS "editableInPortal" -- Set a default value as it's not present in the old table + FROM + "121-service".financial_service_provider_question fspq + JOIN + "121-service".program_financial_service_providers_financial_service_provider pfsp + ON + fspq."fspId" = pfsp."financialServiceProviderId" + ON + CONFLICT (name, "programId") DO NOTHING;`); + + const [ + programRegistrationAttributes, + programQuestions, + programCustomAttributes, + fspQuestions, + ] = await Promise.all([ + queryRunner.query( + `SELECT id, name, "programId" FROM "121-service".program_registration_attribute`, + ), + queryRunner.query( + `SELECT id, name, "programId" FROM "121-service".program_question`, + ), + queryRunner.query( + `SELECT id, name, "programId" FROM "121-service".program_custom_attribute`, + ), + queryRunner.query( + `SELECT id, name FROM "121-service".financial_service_provider_question`, + ), + ]); + for (const programRegistrationAttribute of programRegistrationAttributes) { + const matchingProgramQuestion = programQuestions.find( + (pq) => + pq.name === programRegistrationAttribute.name && + pq.programId === programRegistrationAttribute.programId, + ); + if (matchingProgramQuestion) { + const queryPQData = ` + INSERT INTO "121-service".registration_attribute_data ( + created, + updated, + "registrationId", + "programRegistrationAttributeId", + value + ) + SELECT + rd.created, + rd.updated, + rd."registrationId", + ${programRegistrationAttribute.id}, + rd.value + FROM + "121-service".registration_data rd + WHERE + rd."programQuestionId" = ${matchingProgramQuestion.id}`; + await queryRunner.query(queryPQData); + } + } + + for (const programRegistrationAttribute of programRegistrationAttributes) { + const matchingProgramCustomAttribute = programCustomAttributes.find( + (pca) => + pca.name === programRegistrationAttribute.name && + pca.programId === programRegistrationAttribute.programId, + ); + if (matchingProgramCustomAttribute) { + const queryPQData = ` + INSERT INTO "121-service".registration_attribute_data ( + created, + updated, + "registrationId", + "programRegistrationAttributeId", + value + ) + SELECT + rd.created, + rd.updated, + rd."registrationId", + ${programRegistrationAttribute.id}, + rd.value + FROM + "121-service".registration_data rd + WHERE + rd."programCustomAttributeId" = ${matchingProgramCustomAttribute.id}`; + await queryRunner.query(queryPQData); + } + } + + for (const programRegistrationAttribute of programRegistrationAttributes) { + const matchingFspQs = fspQuestions.filter( + (fspQ) => fspQ.name === programRegistrationAttribute.name, + ); + + if (matchingFspQs.length > 0) { + const fspQuestionIds = matchingFspQs.map((fspQ) => fspQ.id).join(', '); + // Remove registration data related to unmatched fsp questions + // So for example some registrations still have data of jumbo while their fsp is now visa + const deleteRegistrationDataRelatedToUnMatchedFsp = ` + DELETE FROM "121-service".registration_data rd + USING "121-service".registration r, "121-service"."financial_service_provider_question" fq + WHERE rd."registrationId" = r.id + AND rd."fspQuestionId" = fq.id + AND fq."fspId" != r."fspId"; + `; + await queryRunner.query(deleteRegistrationDataRelatedToUnMatchedFsp); + + const queryFQData = ` + INSERT INTO "121-service".registration_attribute_data ( + created, + updated, + "registrationId", + "programRegistrationAttributeId", + value + ) + SELECT + rd.created, + rd.updated, + rd."registrationId", + ${programRegistrationAttribute.id}, + rd.value + FROM + "121-service".registration_data rd + LEFT JOIN + "121-service".registration r ON r.id = rd."registrationId" + WHERE + rd."fspQuestionId" IN (${fspQuestionIds}) AND r."programId" = ${programRegistrationAttribute.programId}`; + + await queryRunner.query(queryFQData); + } + } + + return Promise.resolve(); + } + + private async checkRegistrationFspConfigMigrations( + queryRunner: QueryRunner, + ): Promise { + // check if all the fsp config properties have been migrated + const mismatchedRegistrations = await queryRunner.query(` + SELECT r.* + FROM "121-service".registration r + LEFT JOIN "121-service".program_financial_service_provider_configuration fspConfig + ON r."programFinancialServiceProviderConfigurationId" = fspConfig.id + WHERE r."programId" != fspConfig."programId" + `); + + if (mismatchedRegistrations.length > 0) { + throw new Error( + `Data integrity issue: ${mismatchedRegistrations.length} registrations are linked to an FSP configuration with a different programId.`, + ); + } + } + + private async checkTransactionFspConfigMigrations( + queryRunner: QueryRunner, + ): Promise { + // check if all the fsp config properties have been migrated + const mismatchedTransactions = await queryRunner.query(` + SELECT t.* + FROM "121-service".transaction t + LEFT JOIN "121-service".program_financial_service_provider_configuration fspConfig + ON t."programFinancialServiceProviderConfigurationId" = fspConfig.id + WHERE t."programId" != fspConfig."programId" + `); + + if (mismatchedTransactions.length > 0) { + throw new Error( + `Data integrity issue: ${mismatchedTransactions.length} transactions are linked to an FSP configuration with a different programId.`, + ); + } + } + + private async addConstraints(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE SEQUENCE IF NOT EXISTS "121-service"."program_financial_service_pro_id_seq" OWNED BY "121-service"."program_financial_service_provider_configuration_property"."id"`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."program_financial_service_provider_configuration_property" ALTER COLUMN "id" SET DEFAULT nextval('"121-service"."program_financial_service_pro_id_seq"')`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."program_financial_service_provider_configuration_property" ALTER COLUMN "id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."transaction" DROP CONSTRAINT "FK_d8a56a1864ef40e1551833430bb"`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."transaction" ALTER COLUMN "programFinancialServiceProviderConfigurationId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."transaction" ADD CONSTRAINT "FK_d8a56a1864ef40e1551833430bb" FOREIGN KEY ("programFinancialServiceProviderConfigurationId") REFERENCES "121-service"."program_financial_service_provider_configuration"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."registration" ALTER COLUMN "programFinancialServiceProviderConfigurationId" SET NOT NULL`, + ); + } + + private async dropOldTablesAndViews(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP TABLE "121-service"."registration_data" cascade`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."transaction" DROP COLUMN "financialServiceProviderId"`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."registration" DROP COLUMN "fspId"`, + ); + await queryRunner.query( + `DROP TABLE "121-service"."program_fsp_configuration" cascade`, + ); + await queryRunner.query( + `DROP TABLE "121-service"."program_custom_attribute" cascade`, + ); + await queryRunner.query( + `DROP TABLE "121-service"."program_question" cascade`, + ); + await queryRunner.query( + `DROP TABLE "121-service"."financial_service_provider_question" cascade`, + ); + await queryRunner.query( + `DROP TABLE "121-service"."program_financial_service_providers_financial_service_provider" cascade`, + ); + } + + public async down(_queryRunner: QueryRunner): Promise { + console.log('Not implemented'); + } +} diff --git a/services/121-service/src/migration/1729848646578-missing-program-fsp-confg-properties-seq.ts b/services/121-service/src/migration/1729848646578-missing-program-fsp-confg-properties-seq.ts new file mode 100644 index 0000000000..e8ee8ba98b --- /dev/null +++ b/services/121-service/src/migration/1729848646578-missing-program-fsp-confg-properties-seq.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MissingProgramFspConfgPropertiesSeq1729848646578 + implements MigrationInterface +{ + name = 'MissingProgramFspConfgPropertiesSeq1729848646578'; + + public async up(queryRunner: QueryRunner): Promise { + // These queries are also present in src/migration/1729605362361-program-registration-attribute-refactor.ts for some reason however typeorm still generates + // this migration file. Could not find a way around this. So leaving it as is. + await queryRunner.query( + `CREATE SEQUENCE IF NOT EXISTS "121-service"."program_financial_service_pro_id_seq" OWNED BY "121-service"."program_financial_service_provider_configuration_property"."id"`, + ); + await queryRunner.query( + `ALTER TABLE "121-service"."program_financial_service_provider_configuration_property" ALTER COLUMN "id" SET DEFAULT nextval('"121-service"."program_financial_service_pro_id_seq"')`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "121-service"."program_financial_service_provider_configuration_property" ALTER COLUMN "id" DROP DEFAULT`, + ); + await queryRunner.query( + `DROP SEQUENCE "121-service"."program_financial_service_pro_id_seq"`, + ); + } +} diff --git a/services/121-service/src/notifications/message-incoming/message-incoming.service.ts b/services/121-service/src/notifications/message-incoming/message-incoming.service.ts index 3bb0506d2d..416895d2be 100644 --- a/services/121-service/src/notifications/message-incoming/message-incoming.service.ts +++ b/services/121-service/src/notifications/message-incoming/message-incoming.service.ts @@ -26,7 +26,7 @@ import { ImageCodeService } from '@121-service/src/payments/imagecode/image-code import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { QueueRegistryService } from '@121-service/src/queue-registry/queue-registry.service'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; @@ -299,15 +299,15 @@ export class MessageIncomingService { // This should be refactored later const program = await this.programRepository.findOneOrFail({ where: { id: Equal(tryWhatsapp.registration.programId) }, - relations: ['financialServiceProviders'], + relations: ['programFinancialServiceProviderConfigurations'], }); - const fspIntersolveWhatsapp = program.financialServiceProviders.find( - (fsp) => { - return (fsp.fsp = + const fspConfigWithFspIntersolveWhatsapp = + program.programFinancialServiceProviderConfigurations.find((config) => { + return (config.financialServiceProviderName = FinancialServiceProviders.intersolveVoucherWhatsapp); - }, - )!; - tryWhatsapp.registration.fsp = fspIntersolveWhatsapp; + })!; + tryWhatsapp.registration.programFinancialServiceProviderConfigurationId = + fspConfigWithFspIntersolveWhatsapp.id; const savedRegistration = await this.registrationRepository.save( tryWhatsapp.registration, ); @@ -315,7 +315,7 @@ export class MessageIncomingService { savedRegistration, tryWhatsapp.registration.phoneNumber, { - name: CustomDataAttributes.whatsappPhoneNumber, + name: DefaultRegistrationDataAttributeNames.whatsappPhoneNumber, }, ); await this.tryWhatsappRepository.remove(tryWhatsapp); @@ -334,12 +334,12 @@ export class MessageIncomingService { 'whatsappPendingMessages', ) .leftJoinAndSelect('registration.program', 'program') - .leftJoin('registration_data.fspQuestion', 'fspQuestion') + .leftJoin('registration_data.programRegistrationAttribute', 'attribute') .where('registration_data.value = :whatsappPhoneNumber', { whatsappPhoneNumber: phoneNumber, }) - .andWhere('fspQuestion.name = :name', { - name: CustomDataAttributes.whatsappPhoneNumber, + .andWhere('attribute.name = :name', { + name: DefaultRegistrationDataAttributeNames.whatsappPhoneNumber, }) .orderBy('whatsappPendingMessages.created', 'ASC') .getMany(); diff --git a/services/121-service/src/notifications/message-queues/message-queues.service.spec.ts b/services/121-service/src/notifications/message-queues/message-queues.service.spec.ts index d928bcf9a3..ff1ad9cce7 100644 --- a/services/121-service/src/notifications/message-queues/message-queues.service.spec.ts +++ b/services/121-service/src/notifications/message-queues/message-queues.service.spec.ts @@ -11,6 +11,7 @@ import { MessageQueuesService } from '@121-service/src/notifications/message-que import { MessageTemplateEntity } from '@121-service/src/notifications/message-template/message-template.entity'; import { ProgramAttributesService } from '@121-service/src/program-attributes/program-attributes.service'; import { QueueRegistryService } from '@121-service/src/queue-registry/queue-registry.service'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; @@ -66,7 +67,8 @@ describe('MessageQueuesService', () => { registration.preferredLanguage = LanguageEnum.fr; registration.phoneNumber = '234567891'; registration.programId = 1; - registration['whatsappPhoneNumber'] = '0987654321'; + registration[DefaultRegistrationDataAttributeNames.whatsappPhoneNumber] = + '0987654321'; // Act await queueMessageService.addMessageJob({ @@ -83,7 +85,8 @@ describe('MessageQueuesService', () => { queueRegistryService.createMessageSmallBulkQueue.add, ).toHaveBeenCalledWith(ProcessNameMessage.send, { ...defaultMessageJob, - whatsappPhoneNumber: registration['whatsappPhoneNumber'], + whatsappPhoneNumber: + registration[DefaultRegistrationDataAttributeNames.whatsappPhoneNumber], phoneNumber: registration.phoneNumber, preferredLanguage: registration.preferredLanguage, registrationId: registration.id, diff --git a/services/121-service/src/notifications/message-queues/message-queues.service.ts b/services/121-service/src/notifications/message-queues/message-queues.service.ts index 841b6fe829..bf4a597528 100644 --- a/services/121-service/src/notifications/message-queues/message-queues.service.ts +++ b/services/121-service/src/notifications/message-queues/message-queues.service.ts @@ -19,7 +19,7 @@ import { import { MessageTemplateEntity } from '@121-service/src/notifications/message-template/message-template.entity'; import { ProgramAttributesService } from '@121-service/src/program-attributes/program-attributes.service'; import { QueueRegistryService } from '@121-service/src/queue-registry/queue-registry.service'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; @@ -75,12 +75,13 @@ export class MessageQueuesService { bulksize?: number; userId: number; }): Promise { - let whatsappPhoneNumber = registration['whatsappPhoneNumber']; + let whatsappPhoneNumber = + registration[DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]; if (registration instanceof RegistrationEntity) { whatsappPhoneNumber = await this.registrationDataService.getRegistrationDataValueByName( registration, - CustomDataAttributes.whatsappPhoneNumber, + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber, ); } @@ -136,13 +137,11 @@ export class MessageQueuesService { }); messageText = messageTemplate?.message; } - const placeholders = await this.programAttributesService.getAttributes( + const placeholders = await this.programAttributesService.getAttributes({ programId, - true, - true, - false, - true, - ); + includeProgramRegistrationAttributes: true, + includeTemplateDefaultAttributes: true, + }); const usedPlaceholders: string[] = []; for (const placeholder of placeholders) { const regex = new RegExp(`{{${placeholder.name}}}`, 'g'); diff --git a/services/121-service/src/notifications/message-template/message-template.service.ts b/services/121-service/src/notifications/message-template/message-template.service.ts index 66153bfef4..a293e589db 100644 --- a/services/121-service/src/notifications/message-template/message-template.service.ts +++ b/services/121-service/src/notifications/message-template/message-template.service.ts @@ -125,13 +125,11 @@ export class MessageTemplateService { message: string, ): Promise { const availableAttributes = - await this.programAttributesService.getAttributes( + await this.programAttributesService.getAttributes({ programId, - true, - true, - false, - true, - ); + includeProgramRegistrationAttributes: true, + includeTemplateDefaultAttributes: true, + }); const availablePlaceholders = availableAttributes.map( (a) => `{{${a.name}}}`, ); diff --git a/services/121-service/src/payments/dto/fsp-instructions.dto.ts b/services/121-service/src/payments/dto/fsp-instructions.dto.ts index d7c713e136..a72345362a 100644 --- a/services/121-service/src/payments/dto/fsp-instructions.dto.ts +++ b/services/121-service/src/payments/dto/fsp-instructions.dto.ts @@ -2,4 +2,5 @@ import { ExcelFspInstructions } from '@121-service/src/payments/fsp-integration/ export class FspInstructions { public data: ExcelFspInstructions[]; + public fileNamePrefix: string; } diff --git a/services/121-service/src/payments/dto/get-import-template-response.dto.ts b/services/121-service/src/payments/dto/get-import-template-response.dto.ts new file mode 100644 index 0000000000..4b5128134c --- /dev/null +++ b/services/121-service/src/payments/dto/get-import-template-response.dto.ts @@ -0,0 +1,4 @@ +export class GetImportTemplateResponseDto { + public name: string; + public template: string[]; +} diff --git a/services/121-service/src/payments/dto/import-reconciliation-response.dto.ts b/services/121-service/src/payments/dto/import-reconciliation-response.dto.ts new file mode 100644 index 0000000000..c282b231e4 --- /dev/null +++ b/services/121-service/src/payments/dto/import-reconciliation-response.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { ReconciliationFeedbackDto } from '@121-service/src/payments/dto/reconciliation-feedback.dto'; +import { AggregateImportResultDto } from '@121-service/src/registration/dto/bulk-import.dto'; + +export class ImportReconciliationResponseDto { + @ApiProperty({ + type: [ReconciliationFeedbackDto], + description: 'The import result', + }) + importResult: ReconciliationFeedbackDto[]; + + @ApiProperty({ + type: AggregateImportResultDto, + description: 'The aggregate import result', + }) + aggregateImportResult: AggregateImportResultDto; +} diff --git a/services/121-service/src/payments/dto/pa-payment-data.dto.ts b/services/121-service/src/payments/dto/pa-payment-data.dto.ts index 715b14cb44..121bbc29a8 100644 --- a/services/121-service/src/payments/dto/pa-payment-data.dto.ts +++ b/services/121-service/src/payments/dto/pa-payment-data.dto.ts @@ -3,7 +3,9 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-pr export class PaPaymentDataDto { public referenceId: string; public paymentAddress: string; - public fspName: FinancialServiceProviders; + public programFinancialServiceProviderConfigurationId: number; + // TODO: Do not use the the PaPaymentDataDto in Intersolve voucher & CBE than we we can refactor this to not need the FinancialServiceProviders enum anymore + public financialServiceProviderName: FinancialServiceProviders; public transactionAmount: number; public bulkSize: number; public userId: number; diff --git a/services/121-service/src/payments/dto/pa-payment-retry-data.dto.ts b/services/121-service/src/payments/dto/pa-payment-retry-data.dto.ts new file mode 100644 index 0000000000..c9714611b0 --- /dev/null +++ b/services/121-service/src/payments/dto/pa-payment-retry-data.dto.ts @@ -0,0 +1,5 @@ +import { PaPaymentDataDto } from '@121-service/src/payments/dto/pa-payment-data.dto'; + +export class PaPaymentRetryDataDto extends PaPaymentDataDto { + programFinancialServiceProviderConfigurationName: string; +} diff --git a/services/121-service/src/payments/dto/payment-transaction-result.dto.ts b/services/121-service/src/payments/dto/payment-transaction-result.dto.ts index 11f9f3c05a..63a71b1851 100644 --- a/services/121-service/src/payments/dto/payment-transaction-result.dto.ts +++ b/services/121-service/src/payments/dto/payment-transaction-result.dto.ts @@ -1,6 +1,7 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; +// TODO: This file is up for refactoring: splitting up into multiple files, consistent naming, using interfaces instead of classes and/or using param decorators. export class FspTransactionResultDto { public fspName: FinancialServiceProviders; public paList: PaTransactionResultDto[]; diff --git a/services/121-service/src/payments/dto/reconciliation-feedback.dto.ts b/services/121-service/src/payments/dto/reconciliation-feedback.dto.ts new file mode 100644 index 0000000000..b533e22280 --- /dev/null +++ b/services/121-service/src/payments/dto/reconciliation-feedback.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; +import { ImportStatus } from '@121-service/src/registration/dto/bulk-import.dto'; + +export class ReconciliationFeedbackDto { + // TODO align wigh guidelines: use the dto only for the endpoint response, and don't pass it along multiple methods > create new interfaces for that + split in files where appropriate + @ApiProperty({ example: '1234', description: 'The reference ID' }) + referenceId?: string | null; + + @ApiProperty({ + example: TransactionStatusEnum.success, + enum: TransactionStatusEnum, + }) + status?: TransactionStatusEnum | null; + + @ApiProperty({ example: 'Success', description: 'The message' }) + message?: string | null; + + @ApiProperty({ + example: ImportStatus.imported, + enum: ImportStatus, + }) + importStatus: ImportStatus; + + [key: string]: string | undefined | null | ImportStatus; +} diff --git a/services/121-service/src/payments/dto/transaction-relation-details.dto.ts b/services/121-service/src/payments/dto/transaction-relation-details.dto.ts index 8a06763e4d..42c1f2cd14 100644 --- a/services/121-service/src/payments/dto/transaction-relation-details.dto.ts +++ b/services/121-service/src/payments/dto/transaction-relation-details.dto.ts @@ -2,4 +2,5 @@ export class TransactionRelationDetailsDto { programId: number; paymentNr: number; userId: number; + programFinancialServiceProviderConfigurationId: number; } diff --git a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.api.service.ts b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.api.service.ts index f85dd14805..6fd6e8c834 100644 --- a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.api.service.ts +++ b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.api.service.ts @@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common'; import { CommercialBankEthiopiaMockService } from '@121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.mock'; import { CommercialBankEthiopiaTransferPayload } from '@121-service/src/payments/fsp-integration/commercial-bank-ethiopia/dto/commercial-bank-ethiopia-transfer-payload.dto'; import { CommercialBankEthiopiaSoapElements } from '@121-service/src/payments/fsp-integration/commercial-bank-ethiopia/enum/commercial-bank-ethiopia.enum'; +import { RequiredUsernamePasswordInterface } from '@121-service/src/program-financial-service-provider-configurations/interfaces/required-username-password.interface'; +import { UsernamePasswordInterface } from '@121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface'; import { SoapService } from '@121-service/src/utils/soap/soap.service'; @Injectable() @@ -14,7 +16,7 @@ export class CommercialBankEthiopiaApiService { public async creditTransfer( payment: CommercialBankEthiopiaTransferPayload, - credentials: { username: string; password: string }, + credentials: RequiredUsernamePasswordInterface, ): Promise { const payload = await this.createCreditTransferPayload( payment, @@ -71,7 +73,7 @@ export class CommercialBankEthiopiaApiService { public async createCreditTransferPayload( payment: CommercialBankEthiopiaTransferPayload, - credentials: { username: string; password: string }, + credentials: RequiredUsernamePasswordInterface, ): Promise { // Create the SOAP envelope for credit transfer const payload = await this.soapService.readXmlAsJs( @@ -143,7 +145,7 @@ export class CommercialBankEthiopiaApiService { public async getTransactionStatus( payment: CommercialBankEthiopiaTransferPayload, - credentials: { username: string; password: string }, + credentials: RequiredUsernamePasswordInterface, ): Promise { const payload = await this.createTransactionStatusPayload( payment, @@ -188,7 +190,7 @@ export class CommercialBankEthiopiaApiService { public async createTransactionStatusPayload( payment: CommercialBankEthiopiaTransferPayload, - credentials: { username: string; password: string }, + credentials: RequiredUsernamePasswordInterface, ): Promise { // Create the SOAP envelope for credit transfer const payload = await this.soapService.readXmlAsJs( @@ -241,7 +243,7 @@ export class CommercialBankEthiopiaApiService { public async getValidationStatus( bankAccountNumber: string, - credentials: { username: string; password: string }, + credentials: RequiredUsernamePasswordInterface, ): Promise { const payload = await this.createValidationStatusPayload( bankAccountNumber, @@ -286,7 +288,7 @@ export class CommercialBankEthiopiaApiService { public async createValidationStatusPayload( bankAccountNumber: string, - credentials: { username: string; password: string }, + credentials: UsernamePasswordInterface, ): Promise { // Create the SOAP envelope for credit transfer const payload = await this.soapService.readXmlAsJs( diff --git a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.module.ts b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.module.ts index cc55b1b63a..e8e123040c 100644 --- a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.module.ts +++ b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.module.ts @@ -11,7 +11,8 @@ import { PaymentProcessorCommercialBankEthiopia } from '@121-service/src/payment import { RedisModule } from '@121-service/src/payments/redis/redis.module'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { TransactionsModule } from '@121-service/src/payments/transactions/transactions.module'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { QueueRegistryModule } from '@121-service/src/queue-registry/queue-registry.module'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; @@ -44,6 +45,7 @@ import { SoapService } from '@121-service/src/utils/soap/soap.service'; CommercialBankEthiopiaAccountEnquiriesEntity, ), PaymentProcessorCommercialBankEthiopia, + ProgramFinancialServiceProviderConfigurationRepository, ], controllers: [CommercialBankEthiopiaController], exports: [ diff --git a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.spec.ts b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.spec.ts index 2c6a087130..1d00c2619f 100644 --- a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.spec.ts +++ b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@automock/jest'; import { - FinancialServiceProviderConfigurationEnum, + FinancialServiceProviderConfigurationProperties, FinancialServiceProviders, } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { PaPaymentDataDto } from '@121-service/src/payments/dto/pa-payment-data.dto'; @@ -15,13 +15,14 @@ import { generateMockCreateQueryBuilder } from '@121-service/src/utils/createQue const programId = 3; const paymentNr = 5; const userId = 1; -const mockCredentials = { username: '1234', password: '1234' }; const sendPaymentData: PaPaymentDataDto[] = [ { transactionAmount: 22, referenceId: '3fc92035-78f5-4b40-a44d-c7711b559442', paymentAddress: '14155238886', - fspName: FinancialServiceProviders.commercialBankEthiopia, + programFinancialServiceProviderConfigurationId: 1, + financialServiceProviderName: + FinancialServiceProviders.commercialBankEthiopia, bulkSize: 1, userId, }, @@ -44,7 +45,6 @@ const paymentDetailsResult: CommercialBankEthiopiaJobDto = { paymentNr, programId, payload: payload[0], - credentials: mockCredentials, userId: sendPaymentData[0].userId, }; @@ -73,11 +73,11 @@ describe('CommercialBankEthiopiaService', () => { it('should add payment to queue', async () => { const dbQueryResult = [ { - name: FinancialServiceProviderConfigurationEnum.username, + name: FinancialServiceProviderConfigurationProperties.username, value: '1234', }, { - name: FinancialServiceProviderConfigurationEnum.password, + name: FinancialServiceProviderConfigurationProperties.password, value: '1234', }, ]; diff --git a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.ts b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.ts index 5590d2334b..c449a44c1e 100644 --- a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.ts +++ b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service.ts @@ -1,13 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import Redis from 'ioredis'; import { Equal, Repository } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import { - FinancialServiceProviderConfigurationEnum, - FinancialServiceProviders, -} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { PaPaymentDataDto } from '@121-service/src/payments/dto/pa-payment-data.dto'; import { FspTransactionResultDto, @@ -30,7 +29,9 @@ import { import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { TransactionsService } from '@121-service/src/payments/transactions/transactions.service'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { RequiredUsernamePasswordInterface } from '@121-service/src/program-financial-service-provider-configurations/interfaces/required-username-password.interface'; +import { UsernamePasswordInterface } from '@121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { QueueRegistryService } from '@121-service/src/queue-registry/queue-registry.service'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; @@ -49,8 +50,6 @@ export class CommercialBankEthiopiaService public transactionRepository: Repository; @InjectRepository(ProgramEntity) public programRepository: Repository; - @InjectRepository(ProgramFinancialServiceProviderConfigurationEntity) - public programFspConfigurationRepository: Repository; @Inject( getScopedRepositoryProviderName( CommercialBankEthiopiaAccountEnquiriesEntity, @@ -64,6 +63,7 @@ export class CommercialBankEthiopiaService private readonly transactionsService: TransactionsService, @Inject(REDIS_CLIENT) private readonly redisClient: Redis, + public readonly programFspConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, ) {} public async sendPayment( @@ -71,9 +71,6 @@ export class CommercialBankEthiopiaService programId: number, paymentNr: number, ): Promise { - const credentials: { username: string; password: string } = - await this.getCommercialBankEthiopiaCredentials(programId); - const program = await this.programRepository.findOneByOrFail({ id: programId, }); @@ -88,6 +85,7 @@ export class CommercialBankEthiopiaService ); const registrationData = await this.getRegistrationData(referenceIds); + // TODO Refactor this to get all data in one query instead of per PA for (const paPayment of paPaymentList) { const paRegistrationData = await this.getPaRegistrationData( paPayment, @@ -105,7 +103,6 @@ export class CommercialBankEthiopiaService paymentNr, programId, payload, - credentials, userId: paPayment.userId, }; const job = @@ -121,16 +118,23 @@ export class CommercialBankEthiopiaService async processQueuedPayment( data: CommercialBankEthiopiaJobDto, ): Promise { + const credentials = + await this.programFspConfigurationRepository.getUsernamePasswordProperties( + data.paPaymentData.programFinancialServiceProviderConfigurationId, + ); + const paymentRequestResultPerPa = await this.sendPaymentPerPa( data.payload, data.paPaymentData.referenceId, - data.credentials, + credentials, ); const transactionRelationDetails = { programId: data.programId, paymentNr: data.paymentNr, userId: data.userId, + programFinancialServiceProviderConfigurationId: + data.paPaymentData.programFinancialServiceProviderConfigurationId, }; // Storing the per payment so you can continiously seed updates of transactions in Portal await this.transactionsService.storeTransactionUpdateStatus( @@ -174,20 +178,22 @@ export class CommercialBankEthiopiaService .select([ 'registration.referenceId AS "referenceId"', 'data.value AS value', - 'COALESCE("programQuestion".name, "fspQuestion".name) AS "fieldName"', + '"programRegistrationAttribute".name AS "fieldName"', ]) .where('registration.referenceId IN (:...referenceIds)', { referenceIds, }) - .andWhere( - '(programQuestion.name IN (:...names) OR fspQuestion.name IN (:...names))', - { - names: ['fullName', 'bankAccountNumber'], - }, - ) + .andWhere('(programRegistrationAttribute.name IN (:...names))', { + names: [ + FinancialServiceProviderAttributes.fullName, + FinancialServiceProviderAttributes.bankAccountNumber, + ], + }) .leftJoin('registration.data', 'data') - .leftJoin('data.programQuestion', 'programQuestion') - .leftJoin('data.fspQuestion', 'fspQuestion') + .leftJoin( + 'data.programRegistrationAttribute', + 'programRegistrationAttribute', + ) .getRawMany(); // Filter out properties with null values from each object @@ -218,9 +224,11 @@ export class CommercialBankEthiopiaService let debitTheIrRefRetry; paRegistrationData.forEach((data) => { - if (data.fieldName === 'fullName') { + if (data.fieldName === FinancialServiceProviderAttributes.fullName) { fullName = data.value; - } else if (data.fieldName === 'bankAccountNumber') { + } else if ( + data.fieldName === FinancialServiceProviderAttributes.bankAccountNumber + ) { bankAccountNumber = data.value; } else if ((data.fieldName = 'debitTheIrRef')) { debitTheIrRefRetry = data.value; @@ -246,7 +254,7 @@ export class CommercialBankEthiopiaService public async sendPaymentPerPa( payload: CommercialBankEthiopiaTransferPayload, referenceId: string, - credentials: { username: string; password: string }, + credentials: UsernamePasswordInterface, ): Promise { const paTransactionResult = new PaTransactionResultDto(); paTransactionResult.fspName = @@ -255,42 +263,56 @@ export class CommercialBankEthiopiaService paTransactionResult.date = new Date(); paTransactionResult.calculatedAmount = payload.debitAmount; - let result = await this.commercialBankEthiopiaApiService.creditTransfer( - payload, - credentials, - ); + let requiredCredentials: RequiredUsernamePasswordInterface; + if (credentials.password == null || credentials.username == null) { + paTransactionResult.status = TransactionStatusEnum.error; + paTransactionResult.message = + 'Missing username or password for program financial service provider configuration of the registration'; + return paTransactionResult; + } else { + requiredCredentials = { + username: credentials.username, + password: credentials.password, + }; - if (result && result.resultDescription === 'Transaction is DUPLICATED') { - result = await this.commercialBankEthiopiaApiService.getTransactionStatus( + let result = await this.commercialBankEthiopiaApiService.creditTransfer( payload, - credentials, + requiredCredentials, ); - } - if ( - result && - result.Status && - result.Status.successIndicator && - result.Status.successIndicator._text === 'Success' - ) { - paTransactionResult.status = TransactionStatusEnum.success; - payload.status = TransactionStatusEnum.success; - } else { - paTransactionResult.status = TransactionStatusEnum.error; - paTransactionResult.message = - result.resultDescription || - (result.Status && - result.Status.messages && - (result.Status.messages.length > 0 - ? result.Status.messages[0]._text - : result.Status.messages._text)); - } + if (result && result.resultDescription === 'Transaction is DUPLICATED') { + result = + await this.commercialBankEthiopiaApiService.getTransactionStatus( + payload, + requiredCredentials, + ); + } - paTransactionResult.customData = { - requestResult: payload, - paymentResult: result, - }; - return paTransactionResult; + if ( + result && + result.Status && + result.Status.successIndicator && + result.Status.successIndicator._text === 'Success' + ) { + paTransactionResult.status = TransactionStatusEnum.success; + payload.status = TransactionStatusEnum.success; + } else { + paTransactionResult.status = TransactionStatusEnum.error; + paTransactionResult.message = + result.resultDescription || + (result.Status && + result.Status.messages && + (result.Status.messages.length > 0 + ? result.Status.messages[0]._text + : result.Status.messages._text)); + } + + paTransactionResult.customData = { + requestResult: payload, + paymentResult: result, + }; + return paTransactionResult; + } } public async validateAllPas(): Promise { @@ -301,8 +323,8 @@ export class CommercialBankEthiopiaService } public async validatePasForProgram(programId: number): Promise { - const credentials: { username: string; password: string } = - await this.getCommercialBankEthiopiaCredentials(programId); + const credentials = + await this.getCommercialBankEthiopiaCredentialsOrThrow(programId); const getAllPersonsAffectedData = await this.getAllPersonsAffectedData(programId); @@ -388,21 +410,23 @@ export class CommercialBankEthiopiaService .select([ 'registration.id AS "id"', 'ARRAY_AGG(data.value) AS "values"', - 'ARRAY_AGG(COALESCE("programQuestion".name, "fspQuestion".name)) AS "fieldNames"', + 'ARRAY_AGG("programRegistrationAttribute".name) AS "fieldNames"', ]) .where('registration.programId = :programId', { programId }) - .andWhere( - '(programQuestion.name IN (:...names) OR fspQuestion.name IN (:...names))', - { - names: ['fullName', 'bankAccountNumber'], - }, - ) + .andWhere('(programRegistrationAttribute.name IN (:...names))', { + names: [ + FinancialServiceProviderAttributes.fullName, + FinancialServiceProviderAttributes.bankAccountNumber, + ], + }) .andWhere('registration.registrationStatus NOT IN (:...statusValues)', { statusValues: ['deleted', 'paused'], }) .leftJoin('registration.data', 'data') - .leftJoin('data.programQuestion', 'programQuestion') - .leftJoin('data.fspQuestion', 'fspQuestion') + .leftJoin( + 'data.programRegistrationAttribute', + 'programRegistrationAttribute', + ) .groupBy('registration.id') .getRawMany(); @@ -418,30 +442,28 @@ export class CommercialBankEthiopiaService return formattedData; } - public async getCommercialBankEthiopiaCredentials( - programId: number, - ): Promise<{ username: string; password: string }> { - const config = await this.programFspConfigurationRepository - .createQueryBuilder('fspConfig') - .select('name') - .addSelect('value') - .where('fspConfig.programId = :programId', { programId }) - .andWhere('fsp.fsp = :fspName', { - fspName: FinancialServiceProviders.commercialBankEthiopia, - }) - .leftJoin('fspConfig.fsp', 'fsp') - .getRawMany(); + public async getCommercialBankEthiopiaCredentialsOrThrow( + programFinancialServiceProviderConfigurationId: number, + ): Promise { + const credentials = + await this.programFspConfigurationRepository.getUsernamePasswordProperties( + programFinancialServiceProviderConfigurationId, + ); - const credentials: { username: string; password: string } = { - username: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.username, - )?.value, - password: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.password, - )?.value, + if (credentials.password == null || credentials.username == null) { + throw new HttpException( + 'Missing username or password for program financial service provider configuration of the registration', + HttpStatus.NOT_FOUND, + ); + } + + // added this to prevent a typeerror as: return credentials gives a type error + const requiredCredentials: RequiredUsernamePasswordInterface = { + username: credentials.username, + password: credentials.password, }; - return credentials; + return requiredCredentials; } public async getAllProgramsWithCBE(): Promise { @@ -449,12 +471,15 @@ export class CommercialBankEthiopiaService .createQueryBuilder('program') .select('program.id') .innerJoin( - 'program.financialServiceProviders', - 'financialServiceProviders', + 'program.programFinancialServiceProviderConfigurations', + 'programFinancialServiceProviderConfigurations', + ) + .where( + 'programFinancialServiceProviderConfigurations.financialServiceProviderName = :fsp', + { + fsp: FinancialServiceProviders.commercialBankEthiopia, + }, ) - .where('financialServiceProviders.fsp = :fsp', { - fsp: FinancialServiceProviders.commercialBankEthiopia, - }) .getMany(); return programs; diff --git a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/dto/commercial-bank-ethiopia-job.dto.ts b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/dto/commercial-bank-ethiopia-job.dto.ts index 62a1e61d95..0cd0040e95 100644 --- a/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/dto/commercial-bank-ethiopia-job.dto.ts +++ b/services/121-service/src/payments/fsp-integration/commercial-bank-ethiopia/dto/commercial-bank-ethiopia-job.dto.ts @@ -6,6 +6,5 @@ export class CommercialBankEthiopiaJobDto { paymentNr: number; programId: number; payload: CommercialBankEthiopiaTransferPayload; - credentials: { username: string; password: string }; userId: number; } diff --git a/services/121-service/src/payments/fsp-integration/excel/dto/excel-fsp-instructions.dto.ts b/services/121-service/src/payments/fsp-integration/excel/dto/excel-fsp-instructions.dto.ts index 1be277d625..455882535e 100644 --- a/services/121-service/src/payments/fsp-integration/excel/dto/excel-fsp-instructions.dto.ts +++ b/services/121-service/src/payments/fsp-integration/excel/dto/excel-fsp-instructions.dto.ts @@ -1,12 +1,11 @@ -export class ExcelFspInstructions { - public amount: number; +interface KnownProperties { + amount: number; + id: number; + referenceId: string; } -export class ExcelReconciliationDto { - public id: number; - public referenceId: string; - public amount: number; - // columnToMatch field is added dynamically based on program fsp configuration - // Allow dynamic fields based on program FSP configuration - [key: string]: string | number; -} +type UnknownProperties = Record; + +interface ExcelFspInstructions extends KnownProperties, UnknownProperties {} + +export { ExcelFspInstructions }; diff --git a/services/121-service/src/payments/fsp-integration/excel/excel.module.ts b/services/121-service/src/payments/fsp-integration/excel/excel.module.ts index 9886d72184..4905a9258d 100644 --- a/services/121-service/src/payments/fsp-integration/excel/excel.module.ts +++ b/services/121-service/src/payments/fsp-integration/excel/excel.module.ts @@ -5,17 +5,30 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { LookupService } from '@121-service/src/notifications/lookup/lookup.service'; import { ExcelService } from '@121-service/src/payments/fsp-integration/excel/excel.service'; import { TransactionsModule } from '@121-service/src/payments/transactions/transactions.module'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { RegistrationsModule } from '@121-service/src/registration/registrations.module'; +import { FileImportService } from '@121-service/src/utils/file-import/file-import.service'; @Module({ imports: [ HttpModule, - TypeOrmModule.forFeature([ProgramEntity]), + TypeOrmModule.forFeature([ + ProgramEntity, + ProgramFinancialServiceProviderConfigurationEntity, + ]), + // TODO: Refactor this to not make excel module depedenent TransactionsModule and RegistrationsModule TransactionsModule, RegistrationsModule, ], - providers: [ExcelService, LookupService], + providers: [ + ExcelService, + LookupService, + FileImportService, + // TODO: Refactor this to not make excel module depedenent on program financial service provider configuration + ProgramFinancialServiceProviderConfigurationRepository, + ], controllers: [], exports: [ExcelService], }) diff --git a/services/121-service/src/payments/fsp-integration/excel/excel.service.spec.ts b/services/121-service/src/payments/fsp-integration/excel/excel.service.spec.ts deleted file mode 100644 index 1a3c09f02f..0000000000 --- a/services/121-service/src/payments/fsp-integration/excel/excel.service.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Test } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; - -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { ExcelService } from '@121-service/src/payments/fsp-integration/excel/excel.service'; -import { TransactionReturnDto } from '@121-service/src/payments/transactions/dto/get-transaction.dto'; -import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; -import { TransactionsService } from '@121-service/src/payments/transactions/transactions.service'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; -import { RegistrationViewScopedRepository } from '@121-service/src/registration/repositories/registration-view-scoped.repository'; -import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; - -const mockTransactionService = { - retrieveTransaction: jest.fn(), -}; - -const mockRegistrationsPaginationService = { - retrieveRegistrationsPaginationService: jest.fn(), -}; - -const mockRegistrationViewScopedRepository = { - retrieveRegistrationViewScopedRepository: jest.fn(), -}; - -describe('ExcelService', () => { - let excelService: ExcelService; - - const matchColumn = 'phoneNumber'; - const phoneNumber = '27883373741'; - const referenceid = 'referenceId1234'; - const registrationId = 1; - const transactionStatus = TransactionStatusEnum.success; - const transactionAmount = 25; - const registrationViewEntity = new RegistrationViewEntity(); - registrationViewEntity.phoneNumber = phoneNumber; - registrationViewEntity.id = registrationId; - registrationViewEntity.referenceId = referenceid; - const registrations = [registrationViewEntity]; - const transaction = new TransactionReturnDto(); - transaction.referenceId = referenceid; - transaction.amount = transactionAmount; - const transactions = [transaction]; - - beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [ - ExcelService, - { - provide: getRepositoryToken(ProgramEntity), - useValue: { - createQueryBuilder: jest.fn(() => ({ - leftJoinAndSelect: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - getOne: jest.fn().mockResolvedValue({ - id: 1, - programFspConfiguration: [ - { name: 'columnToMatch', value: 'phoneNumber' }, - ], - }), - })), - }, - }, - { - provide: TransactionsService, - useValue: mockTransactionService, - }, - { - provide: RegistrationsPaginationService, - useValue: mockRegistrationsPaginationService, - }, - { - provide: RegistrationViewScopedRepository, - useValue: mockRegistrationViewScopedRepository, - }, - ], - }).compile(); - - excelService = moduleRef.get(ExcelService); - }); - - it('should find and return the matching reconciliation record for a given registration', async () => { - // Arrange - const importRecords = [ - { [matchColumn]: phoneNumber, status: transactionStatus }, - ]; - - const expectedResult = [ - { - paTransactionResult: { - calculatedAmount: transactionAmount, - fspName: FinancialServiceProviders.excel, - referenceId: referenceid, - registrationId, - status: transactionStatus, - }, - phoneNumber, - status: transactionStatus, - }, - ]; - - // Act - const result = excelService.joinRegistrationsAndImportRecords( - registrations, - importRecords, - matchColumn, - transactions, - ); - - // Assert - expect(result).toEqual(expectedResult); - }); - - it('should return no paTransactionResult when no phone number matches', async () => { - // Arrange - const wrongPhoneNumber = '1234567890'; - const importRecords = [ - { - [matchColumn]: wrongPhoneNumber, - status: transactionStatus, - }, - ]; - - // Act - const result = excelService.joinRegistrationsAndImportRecords( - registrations, - importRecords, - matchColumn, - transactions, - ); - - // Assert - expect(result[0]['paTransactionResult']).toBeUndefined(); - }); - - it('should throw an error when import record lacks a status column', async () => { - // Arrange - const importRecords = [{ [matchColumn]: phoneNumber }]; - - // Act & Assert - try { - excelService.joinRegistrationsAndImportRecords( - registrations, - importRecords, - matchColumn, - transactions, - ); - // eslint-disable-next-line jest/no-jasmine-globals - fail('Expected error to be thrown'); - } catch (error) { - // eslint-disable-next-line jest/no-conditional-expect - expect(error).toBeDefined(); - } - }); -}); diff --git a/services/121-service/src/payments/fsp-integration/excel/excel.service.ts b/services/121-service/src/payments/fsp-integration/excel/excel.service.ts index 8497c2614b..61d4478881 100644 --- a/services/121-service/src/payments/fsp-integration/excel/excel.service.ts +++ b/services/121-service/src/payments/fsp-integration/excel/excel.service.ts @@ -1,9 +1,9 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Equal, Repository } from 'typeorm'; import { - FinancialServiceProviderConfigurationEnum, + FinancialServiceProviderConfigurationProperties, FinancialServiceProviders, } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { PaPaymentDataDto } from '@121-service/src/payments/dto/pa-payment-data.dto'; @@ -11,17 +11,21 @@ import { FspTransactionResultDto, PaTransactionResultDto, } from '@121-service/src/payments/dto/payment-transaction-result.dto'; -import { - ExcelFspInstructions, - ExcelReconciliationDto, -} from '@121-service/src/payments/fsp-integration/excel/dto/excel-fsp-instructions.dto'; +import { ReconciliationFeedbackDto } from '@121-service/src/payments/dto/reconciliation-feedback.dto'; +import { TransactionRelationDetailsDto } from '@121-service/src/payments/dto/transaction-relation-details.dto'; +import { ExcelFspInstructions } from '@121-service/src/payments/fsp-integration/excel/dto/excel-fsp-instructions.dto'; import { FinancialServiceProviderIntegrationInterface } from '@121-service/src/payments/fsp-integration/fsp-integration.interface'; +import { ReconciliationReturnType } from '@121-service/src/payments/interfaces/reconciliation-return-type.interface'; import { TransactionReturnDto } from '@121-service/src/payments/transactions/dto/get-transaction.dto'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { TransactionsService } from '@121-service/src/payments/transactions/transactions.service'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { BulkImportResult } from '@121-service/src/registration/dto/bulk-import.dto'; +import { ImportStatus } from '@121-service/src/registration/dto/bulk-import.dto'; +import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; +import { FileImportService } from '@121-service/src/utils/file-import/file-import.service'; @Injectable() export class ExcelService @@ -35,6 +39,8 @@ export class ExcelService public constructor( private readonly transactionsService: TransactionsService, private readonly registrationsPaginationService: RegistrationsPaginationService, + private readonly programFinancialServiceProviderConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, + private readonly fileImportService: FileImportService, ) {} public async sendPayment( @@ -42,43 +48,72 @@ export class ExcelService programId: number, paymentNr: number, ): Promise { - const fspTransactionResult = new FspTransactionResultDto(); - fspTransactionResult.paList = []; - fspTransactionResult.fspName = FinancialServiceProviders.excel; + const transactionResultObjectList: { + paTransactionResultDto: PaTransactionResultDto; + transactionRelationDetailsDto: TransactionRelationDetailsDto; + }[] = []; + for (const paPayment of paPaymentList) { - const transactionResult = new PaTransactionResultDto(); - transactionResult.calculatedAmount = paPayment.transactionAmount; - transactionResult.fspName = FinancialServiceProviders.excel; - transactionResult.referenceId = paPayment.referenceId; - transactionResult.status = TransactionStatusEnum.waiting; - fspTransactionResult.paList.push(transactionResult); + const paTransactionResultDto = new PaTransactionResultDto(); + paTransactionResultDto.calculatedAmount = paPayment.transactionAmount; + paTransactionResultDto.fspName = FinancialServiceProviders.excel; + paTransactionResultDto.referenceId = paPayment.referenceId; + paTransactionResultDto.status = TransactionStatusEnum.waiting; + + const transactionRelationDetailsDto = { + programId, + paymentNr, + userId: paPaymentList[0].userId, + programFinancialServiceProviderConfigurationId: + paPayment.programFinancialServiceProviderConfigurationId, + }; + + const transactionResultObject = { + paTransactionResultDto, + transactionRelationDetailsDto, + }; + + transactionResultObjectList.push(transactionResultObject); } - const transactionRelationDetails = { - programId, - paymentNr, - userId: paPaymentList[0].userId, - }; + await this.transactionsService.storeAllTransactions( - fspTransactionResult, - transactionRelationDetails, + transactionResultObjectList, ); + const fspTransactionResult = new FspTransactionResultDto(); + fspTransactionResult.fspName = FinancialServiceProviders.excel; + fspTransactionResult.paList = transactionResultObjectList.map( + (transactionResultObject) => + transactionResultObject.paTransactionResultDto, + ); return fspTransactionResult; } - public async getFspInstructions( - transactions: TransactionReturnDto[], - programId: number, - payment: number, - ): Promise { - const exportColumns = await this.getExportColumnsForProgram(programId); - // Creating a new query builder since it is imposssible to do a where in query if there are more than 500000 referenceIds - const qb = this.registrationsPaginationService.getQueryBuilderForFsp( + public async getFspInstructions({ + transactions, + programId, + payment, + programFinancialServiceProviderConfigurationId, + }: { + transactions: TransactionReturnDto[]; + programId: number; + payment: number; + programFinancialServiceProviderConfigurationId: number; + }): Promise { + const exportColumns = await this.getExportColumnsForProgramFspConfig( + programFinancialServiceProviderConfigurationId, programId, - payment, - FinancialServiceProviders.excel, - TransactionStatusEnum.waiting, ); + // TODO: Think about refactoring it's probably better use the transaction ids instead of the referenceIds not sure what the original reasoning was + // Creating a new query builder since it is imposssible to do a where in query if there are more than 500000 referenceIds + // TODO: Also refactor this so that the excel service does not know about transactions, so than this query should be moved to a repository and be called in another service + const qb = + this.registrationsPaginationService.getQueryBuilderForFspInstructions({ + programId, + payment, + programFinancialServiceProviderConfigurationId, + status: TransactionStatusEnum.waiting, + }); const chunkSize = 400000; const registrations = await this.registrationsPaginationService.getRegistrationsChunked( @@ -98,42 +133,40 @@ export class ExcelService ); } - private async getExportColumnsForProgram( + private async getExportColumnsForProgramFspConfig( + programFinancialServiceProviderConfigurationId: number, programId: number, ): Promise { - const programWithConfig = await this.programRepository - .createQueryBuilder('program') - .leftJoinAndSelect('program.programQuestions', 'programQuestions') - .leftJoinAndSelect( - 'program.programCustomAttributes', - 'programCustomAttributes', - ) - .leftJoinAndSelect( - 'program.programFspConfiguration', - 'programFspConfiguration', - 'programFspConfiguration.name = :configName', + const columnsToExportConfig = + await this.programFinancialServiceProviderConfigurationRepository.getPropertyValueByName( { - configName: FinancialServiceProviderConfigurationEnum.columnsToExport, + programFinancialServiceProviderConfigurationId, + name: FinancialServiceProviderConfigurationProperties.columnsToExport, }, - ) - .andWhere('program.id = :programId', { - programId, - }) - .getOneOrFail(); + ); - let exportColumns: string[]; - const columnsToExportConfig = - programWithConfig.programFspConfiguration[0]?.value; if (columnsToExportConfig) { - exportColumns = columnsToExportConfig as string[]; - } else { - // Default to using all program questions & attributes names if columnsToExport is not specified - // So generic fields must be specified in the programFspConfiguration - exportColumns = programWithConfig.programQuestions - .map((q) => q.name) - .concat(programWithConfig.programCustomAttributes.map((q) => q.name)); + // check if columnsToExportConfig is a string array or throw an error + if (!Array.isArray(columnsToExportConfig)) { + throw new HttpException( + { + errors: `FinancialServiceProviderConfigurationProperty ${FinancialServiceProviderConfigurationProperties.columnsToExport} must be an array, but received ${typeof columnsToExportConfig}`, + }, + HttpStatus.NOT_FOUND, + ); + } + return columnsToExportConfig; } - return exportColumns; + + const programWithAttributes = await this.programRepository.findOneOrFail({ + where: { id: Equal(programId) }, + relations: ['programRegistrationAttributes'], + }); + // Default to using all program registration attributes names if columnsToExport is not specified + // So generic fields must be specified in the programFspConfiguration + return programWithAttributes.programRegistrationAttributes.map( + (q) => q.name, + ); } private joinRegistrationsAndTransactions( @@ -157,7 +190,11 @@ export class ExcelService ); let j = 0; const excelFspInstructions = orderedRegistrations.map((registration) => { - const fspInstructions = new ExcelFspInstructions(); + const fspInstructions: ExcelFspInstructions = { + referenceId: registration.referenceId, + id: registration.id, + amount: 0, // Initialize amount with a default value this value will be overwritten but it is necessary to have a value here + }; for (const col of exportColumns) { fspInstructions[col] = registration[col]; } @@ -184,25 +221,28 @@ export class ExcelService return excelFspInstructions; } - public async getImportMatchColumn(programId: number): Promise { - const programWithConfig = await this.programRepository - .createQueryBuilder('program') - .leftJoinAndSelect( - 'program.programFspConfiguration', - 'programFspConfiguration', - 'programFspConfiguration.name = :configName', - { configName: FinancialServiceProviderConfigurationEnum.columnToMatch }, - ) - .andWhere('program.id = :programId', { - programId, - }) - .getOne(); - const matchColumn: string = programWithConfig?.programFspConfiguration[0] - ?.value as string; + public async getImportMatchColumn( + programFinancialServiceProviderConfigurationId: number, + ): Promise { + const matchColumn = + await this.programFinancialServiceProviderConfigurationRepository.getPropertyValueByName( + { + programFinancialServiceProviderConfigurationId, + name: FinancialServiceProviderConfigurationProperties.columnToMatch, + }, + ); if (!matchColumn) { throw new HttpException( { - errors: `No match column found for FSP 'Excel' and program with id ${programId}`, + errors: `No match column found for FSP 'Excel' and programFinancialServiceProviderConfigurationId with id ${programFinancialServiceProviderConfigurationId}`, + }, + HttpStatus.NOT_FOUND, + ); + } + if (typeof matchColumn !== 'string') { + throw new HttpException( + { + errors: `Match column must be a string, but received ${typeof matchColumn}`, }, HttpStatus.NOT_FOUND, ); @@ -210,16 +250,127 @@ export class ExcelService return matchColumn; } - public async getRegistrationsForReconciliation( - programId: number, - payment: number, - matchColumn: string, - ) { - const qb = this.registrationsPaginationService.getQueryBuilderForFsp( + public async processReconciliationData({ + file, + payment, + programId, + fspConfigs, + }: { + file: Express.Multer.File; + payment: number; + programId: number; + fspConfigs: ProgramFinancialServiceProviderConfigurationEntity[]; + }): Promise { + const maxRecords = 10000; + const validatedExcelImport = await this.fileImportService.validateCsv( + file, + maxRecords, + ); + + // First set up unfilled feedback object based on import rows .. + const crossFspConfigImportResults: ReconciliationReturnType[] = []; + for (const row of validatedExcelImport) { + const resultRow = new ReconciliationReturnType(); + resultRow.feedback = new ReconciliationFeedbackDto(); + resultRow.feedback = { + ...row, + importStatus: ImportStatus.notFound, + referenceId: null, + message: null, + }; + resultRow.programFinancialServiceProviderConfigurationId = undefined; + resultRow.transaction = undefined; + crossFspConfigImportResults.push(resultRow); + } + + // .. then loop over fspConfigs to update rows where matched + for await (const fspConfig of fspConfigs) { + const matchColumn = await this.getImportMatchColumn(fspConfig.id); + const importResultForFspConfig = await this.reconciliatePayments({ + programId, + payment, + validatedExcelImport, + fspConfig, + matchColumn, + }); + // Convert the array into a map for increased performace (hashmap lookup) + const importResultForFspConfigMap = new Map( + importResultForFspConfig.map((item) => [ + item.feedback[matchColumn], + item, + ]), + ); + + // .. then loop over each row of the original import to update if a match has been found with this fspConfig + crossFspConfigImportResults.forEach((row, index) => { + const importResultForFspConfigRow = importResultForFspConfigMap.get( + row.feedback[matchColumn], + ); + if ( + importResultForFspConfigRow?.feedback.importStatus !== + ImportStatus.notFound + ) { + crossFspConfigImportResults[index] = importResultForFspConfigRow!; + } + }); + } + return crossFspConfigImportResults; + } + + private async reconciliatePayments({ + programId, + payment, + validatedExcelImport, + fspConfig, + matchColumn, + }: { + programId: number; + payment: number; + validatedExcelImport: object[]; + fspConfig: ProgramFinancialServiceProviderConfigurationEntity; + matchColumn: string; + }): Promise { + const registrationsForReconciliation = + await this.getRegistrationsForReconciliation( + programId, + payment, + matchColumn, + fspConfig.id, + ); + if (!registrationsForReconciliation?.length) { + return []; + } + const lastTransactions = await this.transactionsService.getLastTransactions( programId, payment, - FinancialServiceProviders.excel, + undefined, + undefined, + fspConfig.id, + ); + // Join registration data with the imported CSV records + return this.joinRegistrationsAndImportRecords( + registrationsForReconciliation, + validatedExcelImport, + matchColumn, + lastTransactions, + fspConfig.id, ); + } + + private async getRegistrationsForReconciliation( + programId: number, + payment: number, + matchColumn: string, + programFinancialServiceProviderConfigurationId: number, + ): Promise { + const qb = + this.registrationsPaginationService.getQueryBuilderForFspInstructions({ + programId, + payment, + programFinancialServiceProviderConfigurationId, + financialServiceProviderName: FinancialServiceProviders.excel, + }); + // log query const chunkSize = 400000; return await this.registrationsPaginationService.getRegistrationsChunked( programId, @@ -232,21 +383,22 @@ export class ExcelService ); } - public joinRegistrationsAndImportRecords( + private joinRegistrationsAndImportRecords( registrations: Awaited< ReturnType >, importRecords: object[], matchColumn: string, - transactions: TransactionReturnDto[], - ): BulkImportResult[] { + existingTransactions: TransactionReturnDto[], + fspConfigId: number, + ): ReconciliationReturnType[] { // First order registrations by referenceId to join amount from transactions const registrationsOrderedByReferenceId = registrations.sort((a, b) => a.referenceId.localeCompare(b.referenceId), ); const registrationsWithAmount = this.joinRegistrationsAndTransactions( registrationsOrderedByReferenceId, - transactions, + existingTransactions, ['id', 'referenceId', matchColumn], ); @@ -255,10 +407,14 @@ export class ExcelService a[matchColumn]?.localeCompare(b[matchColumn]), ); const registrationsOrdered = registrationsWithAmount.sort((a, b) => - a[matchColumn]?.localeCompare(b[matchColumn]), + (a[matchColumn] as string).localeCompare(b[matchColumn] as string), ); - const importResponseRecords = importRecordsOrdered.map((record) => { + const resultFeedback: ReconciliationReturnType[] = []; + for (const record of importRecordsOrdered) { + let transaction: PaTransactionResultDto | null = null; + let importStatus = ImportStatus.notFound; + if ( ![TransactionStatusEnum.success, TransactionStatusEnum.error].includes( record[this.statusColumnName]?.toLowerCase(), @@ -271,36 +427,48 @@ export class ExcelService throw new HttpException({ errors }, HttpStatus.NOT_FOUND); } - const importResponseRecord = record as BulkImportResult; // find registration with matching matchColumn value const matchedRegistration = registrationsOrdered.find( (r) => r[matchColumn] === record[matchColumn], ); if (matchedRegistration) { - importResponseRecord['paTransactionResult'] = - this.createTransactionResult( - matchedRegistration as ExcelReconciliationDto, - record, - ); + transaction = this.createTransactionResult(matchedRegistration, record); + importStatus = + transaction.status === TransactionStatusEnum.success + ? ImportStatus.paymentSuccess + : ImportStatus.paymentFailed; } - return importResponseRecord; - }); + resultFeedback.push({ + feedback: { + referenceId: (matchedRegistration?.referenceId as string) ?? null, + status: transaction?.status ?? null, + message: transaction?.message ?? null, + importStatus, + [matchColumn]: record[matchColumn], + }, + programFinancialServiceProviderConfigurationId: matchedRegistration + ? fspConfigId + : undefined, + transaction: transaction || undefined, + }); + } - return importResponseRecords; + return resultFeedback; } public createTransactionResult( - registrationWithAmount: ExcelReconciliationDto, + registrationWithAmount: ExcelFspInstructions, importResponseRecord: any, ): PaTransactionResultDto { - const paTransactionResult = new PaTransactionResultDto(); - paTransactionResult.referenceId = registrationWithAmount.referenceId; - paTransactionResult.registrationId = registrationWithAmount.id; - paTransactionResult.fspName = FinancialServiceProviders.excel; - paTransactionResult.status = importResponseRecord[ - this.statusColumnName - ]?.toLowerCase() as TransactionStatusEnum; - paTransactionResult.calculatedAmount = registrationWithAmount.amount; - return paTransactionResult; + return { + referenceId: registrationWithAmount.referenceId, + registrationId: registrationWithAmount.id, + fspName: FinancialServiceProviders.excel, + status: importResponseRecord[ + this.statusColumnName + ]?.toLowerCase() as TransactionStatusEnum, + calculatedAmount: registrationWithAmount.amount, + message: null, + }; } } diff --git a/services/121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.service.ts b/services/121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.service.ts index 005184be6c..fd31347e9b 100644 --- a/services/121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.service.ts +++ b/services/121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.service.ts @@ -619,7 +619,9 @@ export class IntersolveVisaService public async hasIntersolveCustomer(registrationId: number): Promise { const count = await this.intersolveVisaCustomerScopedRepository.count({ - where: { registrationId: Equal(registrationId) }, + where: { + registrationId: Equal(registrationId), + }, }); return count > 0; } diff --git a/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-store-voucher-options.dto.ts b/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-store-voucher-options.dto.ts index d783ac16ff..7f4b4fe4fb 100644 --- a/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-store-voucher-options.dto.ts +++ b/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-store-voucher-options.dto.ts @@ -2,4 +2,5 @@ export class IntersolveStoreVoucherOptionsDto { messageSid?: string; intersolveVoucherId?: number; userId?: number; + programFinancialServiceProviderConfigurationId?: number; } diff --git a/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-voucher-job.dto.ts b/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-voucher-job.dto.ts index fede44c583..9f0a465bb6 100644 --- a/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-voucher-job.dto.ts +++ b/services/121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-voucher-job.dto.ts @@ -4,6 +4,5 @@ export class IntersolveVoucherJobDto { paymentInfo: PaPaymentDataDto; useWhatsapp: boolean; payment: number; - credentials: { username: string; password: string }; programId: number; } diff --git a/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.module.ts b/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.module.ts index 632db47c80..c6dfe8ecf7 100644 --- a/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.module.ts +++ b/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.module.ts @@ -17,7 +17,8 @@ import { ImageCodeModule } from '@121-service/src/payments/imagecode/image-code. import { RedisModule } from '@121-service/src/payments/redis/redis.module'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { TransactionsModule } from '@121-service/src/payments/transactions/transactions.module'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity'; import { QueueRegistryModule } from '@121-service/src/queue-registry/queue-registry.module'; @@ -63,6 +64,7 @@ import { SoapService } from '@121-service/src/utils/soap/soap.service'; RegistrationScopedRepository, createScopedRepositoryProvider(IntersolveVoucherEntity), PaymentProcessorIntersolveVoucher, + ProgramFinancialServiceProviderConfigurationRepository, ], controllers: [IntersolveVoucherController], exports: [ diff --git a/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.spec.ts b/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.spec.ts index a2010638b8..e05690855d 100644 --- a/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.spec.ts +++ b/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.spec.ts @@ -1,25 +1,24 @@ import { TestBed } from '@automock/jest'; -import { - FinancialServiceProviderConfigurationEnum, - FinancialServiceProviders, -} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { PaPaymentDataDto } from '@121-service/src/payments/dto/pa-payment-data.dto'; import { IntersolveVoucherJobDto } from '@121-service/src/payments/fsp-integration/intersolve-voucher/dto/intersolve-voucher-job.dto'; import { IntersolveVoucherService } from '@121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service'; import { QueueRegistryService } from '@121-service/src/queue-registry/queue-registry.service'; import { JobNames } from '@121-service/src/shared/enum/job-names.enum'; -import { generateMockCreateQueryBuilder } from '@121-service/src/utils/createQueryBuilderMock.helper'; const programId = 3; const paymentNr = 5; -const mockCredentials = { username: '1234', password: '1234' }; +const usernameValue = '1234'; +const passwordValue = '4567'; const sendPaymentData: PaPaymentDataDto[] = [ { transactionAmount: 22, referenceId: '3fc92035-78f5-4b40-a44d-c7711b559442', paymentAddress: '14155238886', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + financialServiceProviderName: + FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationId: 1, bulkSize: 1, userId: 1, }, @@ -29,7 +28,6 @@ const paymentDetailsResult: IntersolveVoucherJobDto = { paymentInfo: sendPaymentData[0], useWhatsapp: true, payment: paymentNr, - credentials: mockCredentials, programId, }; @@ -59,25 +57,17 @@ describe('IntersolveVoucherService', () => { // Arrange const useWhatsapp = true; - const dbQueryResult = [ - { - name: FinancialServiceProviderConfigurationEnum.password, - value: '1234', - }, - { - name: FinancialServiceProviderConfigurationEnum.username, - value: '1234', - }, - ]; - const createQueryBuilder: any = - generateMockCreateQueryBuilder(dbQueryResult); + const dbQueryResult = { + username: usernameValue, + password: passwordValue, + }; jest .spyOn( intersolveVoucherService.programFspConfigurationRepository, - 'createQueryBuilder', + 'getUsernamePasswordProperties', ) - .mockImplementation(() => createQueryBuilder) as any; + .mockImplementation(() => Promise.resolve(dbQueryResult)); jest .spyOn( diff --git a/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.ts b/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.ts index 389b00b615..f33c8ebd6a 100644 --- a/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.ts +++ b/services/121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service.ts @@ -5,7 +5,7 @@ import Redis from 'ioredis'; import { Equal, Repository } from 'typeorm'; import { - FinancialServiceProviderConfigurationEnum, + FinancialServiceProviderConfigurationProperties, FinancialServiceProviders, } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { MessageContentType } from '@121-service/src/notifications/enum/message-type.enum'; @@ -40,7 +40,8 @@ import { import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { TransactionsService } from '@121-service/src/payments/transactions/transactions.service'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { UsernamePasswordInterface } from '@121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { QueueRegistryService } from '@121-service/src/queue-registry/queue-registry.service'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; @@ -63,8 +64,6 @@ export class IntersolveVoucherService public readonly transactionRepository: Repository; @InjectRepository(ProgramEntity) public readonly programRepository: Repository; - @InjectRepository(ProgramFinancialServiceProviderConfigurationEntity) - public readonly programFspConfigurationRepository: Repository; private readonly fallbackLanguage = LanguageEnum.en; @@ -80,6 +79,8 @@ export class IntersolveVoucherService private readonly queueMessageService: MessageQueuesService, private readonly messageTemplateService: MessageTemplateService, private readonly queueRegistryService: QueueRegistryService, + public readonly programFspConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, + @Inject(REDIS_CLIENT) private readonly redisClient: Redis, ) {} @@ -90,24 +91,6 @@ export class IntersolveVoucherService payment: number, useWhatsapp: boolean, ): Promise { - const config = await this.programFspConfigurationRepository - .createQueryBuilder('fspConfig') - .select('name') - .addSelect('value') - .where('fspConfig.programId = :programId', { programId }) - .andWhere('fsp.fsp = :fspName', { fspName: paPaymentList[0].fspName }) - .leftJoin('fspConfig.fsp', 'fsp') - .getRawMany(); - - const credentials: { username: string; password: string } = { - username: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.username, - )?.value, - password: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.password, - )?.value, - }; - for (const paymentInfo of paPaymentList) { const job = await this.queueRegistryService.transactionJobIntersolveVoucherQueue.add( @@ -116,10 +99,10 @@ export class IntersolveVoucherService paymentInfo, useWhatsapp, payment, - credentials, programId, }, ); + await this.redisClient.sadd(getRedisSetName(job.data.programId), job.id); } } @@ -127,26 +110,24 @@ export class IntersolveVoucherService public async processQueuedPayment( jobData: IntersolveVoucherJobDto, ): Promise { + const credentials = + await this.programFspConfigurationRepository.getUsernamePasswordProperties( + jobData.paymentInfo.programFinancialServiceProviderConfigurationId, + ); const paResult = await this.sendIndividualPayment( jobData.paymentInfo, jobData.useWhatsapp, jobData.paymentInfo.transactionAmount, jobData.payment, - jobData.credentials, + credentials, ); if (!paResult) { return; } - const registration = await this.registrationScopedRepository.findOne({ + const registration = await this.registrationScopedRepository.findOneOrFail({ where: { referenceId: Equal(paResult.referenceId) }, }); - if (!registration) { - throw new HttpException( - 'PA with this referenceId not found (within your scope)', - HttpStatus.NOT_FOUND, - ); - } await this.storeTransactionResult( jobData.payment, jobData.paymentInfo.transactionAmount, @@ -156,6 +137,8 @@ export class IntersolveVoucherService paResult.message ?? null, registration.programId, { + programFinancialServiceProviderConfigurationId: + jobData.paymentInfo.programFinancialServiceProviderConfigurationId, userId: jobData.paymentInfo.userId, }, ); @@ -166,7 +149,7 @@ export class IntersolveVoucherService useWhatsapp: boolean, calculatedAmount: number, payment: number, - credentials: { username: string; password: string }, + credentials: UsernamePasswordInterface, ) { const paResult = new PaTransactionResultDto(); paResult.referenceId = paymentInfo.referenceId; @@ -607,10 +590,12 @@ export class IntersolveVoucherService try { credentials = { username: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.username, + (c) => + c.name === FinancialServiceProviderConfigurationProperties.username, ).value, password: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.password, + (c) => + c.name === FinancialServiceProviderConfigurationProperties.password, ).value, }; } catch (error) { @@ -750,13 +735,20 @@ export class IntersolveVoucherService options.messageSid, ); - let userId: number | undefined; + let userId = options.userId; + let programFinancialServiceProviderConfigurationId = + options.programFinancialServiceProviderConfigurationId; if (transactionStep === 2) { - userId = - (await this.getUserIdForTransactionStep2(registrationId, payment)) ?? - undefined; - } else { - userId = options.userId; + const userFspConfigIdObject = + await this.getUserFspConfigIdForTransactionStep2( + registrationId, + payment, + ); + if (userFspConfigIdObject) { + userId = userFspConfigIdObject.userId; + programFinancialServiceProviderConfigurationId = + userFspConfigIdObject.programFinancialServiceProviderConfigurationId; + } } if (userId === undefined) { @@ -764,11 +756,17 @@ export class IntersolveVoucherService 'Could not find userId for transaction in storeTransactionResult.', ); } + if (programFinancialServiceProviderConfigurationId === undefined) { + throw new Error( + 'Could not find programFinancialServiceProviderConfigurationId for transaction in storeTransactionResult.', + ); + } const transactionRelationDetails = { programId, paymentNr: payment, userId, + programFinancialServiceProviderConfigurationId, }; await this.transactionsService.storeTransactionUpdateStatus( @@ -777,19 +775,22 @@ export class IntersolveVoucherService ); } - private async getUserIdForTransactionStep2( + private async getUserFspConfigIdForTransactionStep2( registrationId: number, payment: number, ) { - const transaction = await this.transactionRepository.findOne({ + const transaction: null | { + userId: number; + programFinancialServiceProviderConfigurationId: number; + } = await this.transactionRepository.findOne({ where: { registrationId: Equal(registrationId), payment: Equal(payment), }, order: { created: 'DESC' }, - select: ['userId'], + select: ['userId', 'programFinancialServiceProviderConfigurationId'], }); - return transaction?.userId; + return transaction; } public async createTransactionResult( @@ -802,7 +803,7 @@ export class IntersolveVoucherService ): Promise { const registration = await this.registrationScopedRepository.findOneOrFail({ where: { id: Equal(registrationId) }, - relations: ['fsp', 'program'], + relations: ['programFinancialServiceProviderConfiguration', 'program'], }); const transactionResult = new PaTransactionResultDto(); @@ -815,8 +816,12 @@ export class IntersolveVoucherService if (messageSid) { transactionResult.messageSid = messageSid; } + + const fspNameOfRegistration = + registration.programFinancialServiceProviderConfiguration + .financialServiceProviderName; if ( - registration.fsp.fsp === + fspNameOfRegistration === FinancialServiceProviders.intersolveVoucherWhatsapp ) { transactionResult.customData['IntersolvePayoutStatus'] = @@ -828,14 +833,14 @@ export class IntersolveVoucherService transactionResult.status = status; if ( - registration.fsp.fsp === + fspNameOfRegistration === FinancialServiceProviders.intersolveVoucherWhatsapp ) { transactionResult.fspName = FinancialServiceProviders.intersolveVoucherWhatsapp; } if ( - registration.fsp.fsp === FinancialServiceProviders.intersolveVoucherPaper + fspNameOfRegistration === FinancialServiceProviders.intersolveVoucherPaper ) { transactionResult.fspName = FinancialServiceProviders.intersolveVoucherPaper; diff --git a/services/121-service/src/payments/fsp-integration/intersolve-voucher/services/intersolve-voucher-cron.service.ts b/services/121-service/src/payments/fsp-integration/intersolve-voucher/services/intersolve-voucher-cron.service.ts index 8a3745c548..9474d93e6d 100644 --- a/services/121-service/src/payments/fsp-integration/intersolve-voucher/services/intersolve-voucher-cron.service.ts +++ b/services/121-service/src/payments/fsp-integration/intersolve-voucher/services/intersolve-voucher-cron.service.ts @@ -2,10 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Between, Equal, Repository } from 'typeorm'; -import { - FinancialServiceProviderConfigurationEnum, - FinancialServiceProviders, -} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { MessageContentType } from '@121-service/src/notifications/enum/message-type.enum'; import { ProgramNotificationEnum } from '@121-service/src/notifications/enum/program-notification.enum'; import { MessageProcessType } from '@121-service/src/notifications/message-job.dto'; @@ -15,9 +12,9 @@ import { IntersolveIssueVoucherRequestEntity } from '@121-service/src/payments/f import { IntersolveVoucherEntity } from '@121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.entity'; import { IntersolveVoucherService } from '@121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; @@ -33,8 +30,6 @@ export class IntersolveVoucherCronService { public transactionRepository: Repository; @InjectRepository(ProgramEntity) public programRepository: Repository; - @InjectRepository(ProgramFinancialServiceProviderConfigurationEntity) - public programFspConfigurationRepository: Repository; private readonly fallbackLanguage = 'en'; @@ -43,6 +38,7 @@ export class IntersolveVoucherCronService { private readonly queueMessageService: MessageQueuesService, private readonly intersolveVoucherService: IntersolveVoucherService, private readonly registrationDataService: RegistrationDataService, + private readonly programFspConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, ) {} public async cacheUnusedVouchers(): Promise { @@ -66,36 +62,40 @@ export class IntersolveVoucherCronService { toCancel: Equal(true), }, }); - - const config = await this.programFspConfigurationRepository - .createQueryBuilder('fspConfig') - .select('name') - .addSelect('value') - .andWhere('fsp.fsp = :fspName', { - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, - }) - .leftJoin('fspConfig.fsp', 'fsp') - .getRawMany(); - - // Instance has no intersolve voucher configuration - if (config.length === 0) { + if (failedIntersolveRquests.length === 0) { return; } - const credentials: { username: string; password: string } = { - username: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.username, - )?.value, - password: config.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.password, - )?.value, - }; + // Get the first intersolve programFinancialServiceProviderConfigurationId that has intersolveVoucherWhatsapp as FSP + // TODO: store the programFspConfigurationId or the usename and password in the intersolveRequest table so we know which credentials to use for the cancelation + // Before the registration data/programFinancialServiceProviderConfiguration this problem already existed... + const configId = await this.programFspConfigurationRepository.findOne({ + where: { + financialServiceProviderName: Equal( + FinancialServiceProviders.intersolveVoucherWhatsapp, + ), + }, + select: ['id'], + }); + + if (!configId) { + return; + } + const usernamePassword = + await this.programFspConfigurationRepository.getUsernamePasswordProperties( + configId.id, + ); + if (!usernamePassword.username || !usernamePassword.password) { + throw new Error( + 'No username or password found for intersolveVoucherWhatsapp in this instance while trying to cancel by refpos', + ); + } for (const intersolveRequest of failedIntersolveRquests) { await this.cancelRequestRefpos( intersolveRequest, - credentials.username, - credentials.password, + usernamePassword.username, + usernamePassword.password, ); } } @@ -171,7 +171,7 @@ export class IntersolveVoucherCronService { const fromNumber = await this.registrationDataService.getRegistrationDataValueByName( registration, - CustomDataAttributes.whatsappPhoneNumber, + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber, ); if (!fromNumber) { // This can represent the case where a PA was switched from AH-voucher-whatsapp to AH-voucher-paper. But also otherwise it makes no sense to continue. diff --git a/services/121-service/src/payments/interfaces/reconciliation-return-type.interface.ts b/services/121-service/src/payments/interfaces/reconciliation-return-type.interface.ts new file mode 100644 index 0000000000..3ec0ca9b03 --- /dev/null +++ b/services/121-service/src/payments/interfaces/reconciliation-return-type.interface.ts @@ -0,0 +1,10 @@ +import { PaTransactionResultDto } from '@121-service/src/payments/dto/payment-transaction-result.dto'; +import { ReconciliationFeedbackDto } from '@121-service/src/payments/dto/reconciliation-feedback.dto'; + +export class ReconciliationReturnType { + feedback: ReconciliationFeedbackDto; + + programFinancialServiceProviderConfigurationId?: number; + + transaction?: PaTransactionResultDto; +} diff --git a/services/121-service/src/payments/payments.controller.ts b/services/121-service/src/payments/payments.controller.ts index 3a68a21411..fd4dceb829 100644 --- a/services/121-service/src/payments/payments.controller.ts +++ b/services/121-service/src/payments/payments.controller.ts @@ -31,8 +31,10 @@ import { AuthenticatedUser } from '@121-service/src/guards/authenticated-user.de import { AuthenticatedUserGuard } from '@121-service/src/guards/authenticated-user.guard'; import { CreatePaymentDto } from '@121-service/src/payments/dto/create-payment.dto'; import { FspInstructions } from '@121-service/src/payments/dto/fsp-instructions.dto'; +import { GetImportTemplateResponseDto } from '@121-service/src/payments/dto/get-import-template-response.dto'; import { GetPaymentAggregationDto } from '@121-service/src/payments/dto/get-payment-aggregration.dto'; import { GetPaymentsDto } from '@121-service/src/payments/dto/get-payments.dto'; +import { ImportReconciliationResponseDto } from '@121-service/src/payments/dto/import-reconciliation-response.dto'; import { ProgramPaymentsStatusDto } from '@121-service/src/payments/dto/program-payments-status.dto'; import { RetryPaymentDto } from '@121-service/src/payments/dto/retry-payment.dto'; import { PaymentsService } from '@121-service/src/payments/payments.service'; @@ -42,7 +44,6 @@ import { BulkActionResultDto, BulkActionResultPaymentDto, } from '@121-service/src/registration/dto/bulk-action-result.dto'; -import { ImportResult } from '@121-service/src/registration/dto/bulk-import.dto'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; import { FILE_UPLOAD_API_FORMAT } from '@121-service/src/shared/file-upload-api-format'; @@ -268,7 +269,7 @@ export class PaymentsController { @Param('payment', ParseIntPipe) payment: number, @Req() req: ScopedUserRequest, - ): Promise { + ): Promise { const userId = RequestHelper.getUserId(req); return await this.paymentsService.getFspInstructions( @@ -294,7 +295,7 @@ export class PaymentsController { public async getImportFspReconciliationTemplate( @Param('programId', ParseIntPipe) programId: number, - ): Promise { + ): Promise { return await this.paymentsService.getImportInstructionsTemplate( Number(programId), ); @@ -316,15 +317,14 @@ export class PaymentsController { @ApiBody(FILE_UPLOAD_API_FORMAT) @UseInterceptors(FileInterceptor('file')) public async importFspReconciliationData( - @UploadedFile() file: Blob, + @UploadedFile() file: Express.Multer.File, @Param('programId', ParseIntPipe) programId: number, @Param('payment', ParseIntPipe) payment: number, - @Req() req: ScopedUserRequest, - ): Promise { + @Req() req, + ): Promise { const userId = RequestHelper.getUserId(req); - return await this.paymentsService.importFspReconciliationData( file, programId, diff --git a/services/121-service/src/payments/payments.module.ts b/services/121-service/src/payments/payments.module.ts index bcc99893ec..b9aa040da4 100644 --- a/services/121-service/src/payments/payments.module.ts +++ b/services/121-service/src/payments/payments.module.ts @@ -3,9 +3,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ActionsModule } from '@121-service/src/actions/actions.module'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; import { FinancialServiceProvidersModule } from '@121-service/src/financial-service-providers/financial-service-provider.module'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { LookupService } from '@121-service/src/notifications/lookup/lookup.service'; import { CommercialBankEthiopiaModule } from '@121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.module'; import { ExcelModule } from '@121-service/src/payments/fsp-integration/excel/excel.module'; @@ -19,20 +17,18 @@ import { TransactionEntity } from '@121-service/src/payments/transactions/transa import { TransactionsModule } from '@121-service/src/payments/transactions/transactions.module'; import { ProgramFinancialServiceProviderConfigurationsModule } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.module'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { ProgramModule } from '@121-service/src/programs/programs.module'; import { RegistrationDataModule } from '@121-service/src/registration/modules/registration-data/registration-data.module'; import { RegistrationUtilsModule } from '@121-service/src/registration/modules/registration-utilts/registration-utils.module'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { RegistrationsModule } from '@121-service/src/registration/registrations.module'; import { RegistrationScopedRepository } from '@121-service/src/registration/repositories/registration-scoped.repository'; import { InclusionScoreService } from '@121-service/src/registration/services/inclusion-score.service'; import { AzureLogService } from '@121-service/src/shared/services/azure-log.service'; import { TransactionQueuesModule } from '@121-service/src/transaction-queues/transaction-queues.module'; import { UserModule } from '@121-service/src/user/user.module'; -import { FileImportService } from '@121-service/src/utils/file-import/file-import.service'; import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/createScopedRepositoryProvider.helper'; @Module({ @@ -41,10 +37,7 @@ import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/cre ProgramEntity, TransactionEntity, RegistrationEntity, - ProgramQuestionEntity, - FinancialServiceProviderEntity, - FspQuestionEntity, - ProgramCustomAttributeEntity, + ProgramRegistrationAttributeEntity, ]), UserModule, HttpModule, @@ -70,9 +63,8 @@ import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/cre LookupService, InclusionScoreService, RegistrationScopedRepository, - FileImportService, AzureLogService, - createScopedRepositoryProvider(RegistrationDataEntity), + createScopedRepositoryProvider(RegistrationAttributeDataEntity), ], controllers: [PaymentsController], exports: [PaymentsService], diff --git a/services/121-service/src/payments/payments.service.ts b/services/121-service/src/payments/payments.service.ts index e1031b82ce..2e158b808a 100644 --- a/services/121-service/src/payments/payments.service.ts +++ b/services/121-service/src/payments/payments.service.ts @@ -3,21 +3,28 @@ import { Inject } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import Redis from 'ioredis'; import { PaginateQuery } from 'nestjs-paginate'; -import { DataSource, Equal, Repository } from 'typeorm'; +import { DataSource, Equal, In, Repository } from 'typeorm'; import { v4 as uuid } from 'uuid'; import { AdditionalActionType } from '@121-service/src/actions/action.entity'; import { ActionsService } from '@121-service/src/actions/actions.service'; +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; import { FinancialServiceProviderIntegrationType } from '@121-service/src/financial-service-providers/enum/financial-service-provider-integration-type.enum'; import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { RequiredFinancialServiceProviderConfigurations } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderQuestionRepository } from '@121-service/src/financial-service-providers/repositories/financial-service-provider-question.repository'; +import { + getFinancialServiceProviderConfigurationRequiredProperties, + getFinancialServiceProviderSettingByNameOrThrow, +} from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; import { FspInstructions } from '@121-service/src/payments/dto/fsp-instructions.dto'; +import { GetImportTemplateResponseDto } from '@121-service/src/payments/dto/get-import-template-response.dto'; import { PaPaymentDataDto } from '@121-service/src/payments/dto/pa-payment-data.dto'; +import { PaPaymentRetryDataDto } from '@121-service/src/payments/dto/pa-payment-retry-data.dto'; +import { PaTransactionResultDto } from '@121-service/src/payments/dto/payment-transaction-result.dto'; import { ProgramPaymentsStatusDto } from '@121-service/src/payments/dto/program-payments-status.dto'; +import { ReconciliationFeedbackDto } from '@121-service/src/payments/dto/reconciliation-feedback.dto'; import { SplitPaymentListDto } from '@121-service/src/payments/dto/split-payment-lists.dto'; import { CommercialBankEthiopiaService } from '@121-service/src/payments/fsp-integration/commercial-bank-ethiopia/commercial-bank-ethiopia.service'; -import { ExcelFspInstructions } from '@121-service/src/payments/fsp-integration/excel/dto/excel-fsp-instructions.dto'; import { ExcelService } from '@121-service/src/payments/fsp-integration/excel/excel.service'; import { FinancialServiceProviderIntegrationInterface } from '@121-service/src/payments/fsp-integration/fsp-integration.interface'; import { IntersolveVisaService } from '@121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.service'; @@ -28,27 +35,28 @@ import { getRedisSetName, REDIS_CLIENT, } from '@121-service/src/payments/redis/redis-client'; -import { PaymentReturnDto } from '@121-service/src/payments/transactions/dto/get-transaction.dto'; +import { + PaymentReturnDto, + TransactionReturnDto, +} from '@121-service/src/payments/transactions/dto/get-transaction.dto'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { TransactionScopedRepository } from '@121-service/src/payments/transactions/transaction.repository'; import { TransactionsService } from '@121-service/src/payments/transactions/transactions.service'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { BulkActionResultPaymentDto, BulkActionResultRetryPaymentDto, } from '@121-service/src/registration/dto/bulk-action-result.dto'; -import { - ImportResult, - ImportStatus, -} from '@121-service/src/registration/dto/bulk-import.dto'; +import { ImportStatus } from '@121-service/src/registration/dto/bulk-import.dto'; import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; import { ReferenceIdsDto } from '@121-service/src/registration/dto/reference-id.dto'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; import { RegistrationScopedRepository } from '@121-service/src/registration/repositories/registration-scoped.repository'; import { RegistrationsBulkService } from '@121-service/src/registration/services/registrations-bulk.service'; @@ -59,7 +67,6 @@ import { IntersolveVisaTransactionJobDto } from '@121-service/src/transaction-qu import { SafaricomTransactionJobDto } from '@121-service/src/transaction-queues/dto/safaricom-transaction-job.dto'; import { TransactionQueuesService } from '@121-service/src/transaction-queues/transaction-queues.service'; import { splitArrayIntoChunks } from '@121-service/src/utils/chunk.helper'; -import { FileImportService } from '@121-service/src/utils/file-import/file-import.service'; @Injectable() export class PaymentsService { @@ -68,17 +75,7 @@ export class PaymentsService { @InjectRepository(TransactionEntity) private readonly transactionRepository: Repository; - private fspWithQueueServiceMapping: Partial< - Record< - FinancialServiceProviders, - | IntersolveVoucherService - | IntersolveVisaService - | SafaricomService - | CommercialBankEthiopiaService - > - >; - - private FinancialServiceProvidersToServiceMap: Record< + private financialServiceProviderNameToServiceMap: Record< FinancialServiceProviders, [FinancialServiceProviderIntegrationInterface, useWhatsapp?: boolean] >; @@ -96,28 +93,14 @@ export class PaymentsService { private readonly excelService: ExcelService, private readonly registrationsBulkService: RegistrationsBulkService, private readonly registrationsPaginationService: RegistrationsPaginationService, - private readonly fileImportService: FileImportService, private readonly dataSource: DataSource, private readonly transactionScopedRepository: TransactionScopedRepository, private readonly transactionQueuesService: TransactionQueuesService, - private readonly financialServiceProviderQuestionRepository: FinancialServiceProviderQuestionRepository, private readonly programFinancialServiceProviderConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, @Inject(REDIS_CLIENT) private readonly redisClient: Redis, ) { - this.fspWithQueueServiceMapping = { - [FinancialServiceProviders.intersolveVisa]: this.intersolveVisaService, - [FinancialServiceProviders.intersolveVoucherPaper]: - this.intersolveVoucherService, - [FinancialServiceProviders.intersolveVoucherWhatsapp]: - this.intersolveVoucherService, - [FinancialServiceProviders.safaricom]: this.safaricomService, - [FinancialServiceProviders.commercialBankEthiopia]: - this.commercialBankEthiopiaService, - // Add more FSP mappings if they work queue-based - }; - - this.FinancialServiceProvidersToServiceMap = { + this.financialServiceProviderNameToServiceMap = { [FinancialServiceProviders.intersolveVoucherWhatsapp]: [ this.intersolveVoucherService, true, @@ -243,12 +226,12 @@ export class PaymentsService { // If amount is not defined do not calculate the totalMultiplierSum // This happens when you call the endpoint with dryRun=true - // happens in pa table to define which registrations are selectable + // Calling with dryrun is true happens in the pa table when you try to do a payment to decide which registrations are selectable if (!amount) { return { ...bulkActionResultDto, sumPaymentAmountMultiplier: 0, - fspsInPayment: [], + programFinancialServiceProviderConfigurationNames: [], }; } @@ -263,32 +246,30 @@ export class PaymentsService { // Calculate the totalMultiplierSum and create an array with all FSPs for this payment // Get the sum of the paymentAmountMultiplier of all registrations to calculate the total amount of money to be paid in frontend let totalMultiplierSum = 0; - const fspsInPayment: FinancialServiceProviders[] = []; // This loop is pretty fast: with 131k registrations it takes ~38ms + for (const registration of registrationsForPayment) { totalMultiplierSum = totalMultiplierSum + registration.paymentAmountMultiplier; - if ( - registration.financialServiceProvider && - !fspsInPayment.includes(registration.financialServiceProvider) - ) { - fspsInPayment.push(registration.financialServiceProvider); - } + // This is only needed in actual doPayment call } - // TODO: REFACTOR: See /~https://github.com/global-121/121-platform/pull/5347#discussion_r1738465704, can be done as part of: https://dev.azure.com/redcrossnl/121%20Platform/_workitems/edit/27393 - for (const fsp of fspsInPayment) { - await this.validateRequiredFinancialServiceProviderConfigurations( - fsp, - programId, - ); - } + // Get unique programFinancialServiceProviderConfigurationNames in payment + // Getting unique programFinancialServiceProviderConfigurationNames is relatively: with 131k registrations it takes ~36ms locally + const programFinancialServiceProviderConfigurationNames = Array.from( + new Set( + registrationsForPayment.map( + (registration) => + registration.programFinancialServiceProviderConfigurationName, + ), + ), + ); // Fill bulkActionResultPaymentDto with bulkActionResultDto and additional payment specific data const bulkActionResultPaymentDto = { ...bulkActionResultDto, sumPaymentAmountMultiplier: totalMultiplierSum, - fspsInPayment, + programFinancialServiceProviderConfigurationNames, }; // Create an array of referenceIds to be paid @@ -297,6 +278,11 @@ export class PaymentsService { ); if (!dryRun && referenceIds.length > 0) { + await this.checkFspConfigurationsOrThrow( + programId, + programFinancialServiceProviderConfigurationNames, + ); + // TODO: REFACTOR: userId not be passed down, but should be available in a context object; registrationsForPayment.length is redundant, as it is the same as referenceIds.length void this.initiatePayment( userId, @@ -321,34 +307,61 @@ export class PaymentsService { return bulkActionResultPaymentDto; } - async validateRequiredFinancialServiceProviderConfigurations( - fsp: FinancialServiceProviders, + private async checkFspConfigurationsOrThrow( programId: number, - ) { - const requiredConfigurations = - RequiredFinancialServiceProviderConfigurations[ - fsp as FinancialServiceProviders - ]; - // Early return for FSP that don't have required configurarions - if (!requiredConfigurations) { - return; + programFinancialServiceProviderConfigurationNames: string[], + ): Promise { + const validationResults = await Promise.all( + programFinancialServiceProviderConfigurationNames.map((name) => + this.validateMissingFspConfigurations(programId, name), + ), + ); + const errorMessages = validationResults.flat(); + if (errorMessages.length > 0) { + throw new HttpException( + `${errorMessages.join(', ')}`, + HttpStatus.BAD_REQUEST, + ); } + } + + private async validateMissingFspConfigurations( + programId: number, + programFinancialServiceProviderConfigurationName: string, + ): Promise { const config = - await this.programFinancialServiceProviderConfigurationRepository.findByProgramIdAndFinancialServiceProviderName( - programId, - fsp as FinancialServiceProviders, + await this.programFinancialServiceProviderConfigurationRepository.findOneOrFail( + { + where: { + name: Equal(programFinancialServiceProviderConfigurationName), + programId: Equal(programId), + }, + relations: ['properties'], + }, + ); + + const requiredConfigurations = + getFinancialServiceProviderConfigurationRequiredProperties( + config.financialServiceProviderName, ); + // Early return for FSP that don't have required configurations + if (!requiredConfigurations) { + return []; + } + + const errorMessages: string[] = []; for (const requiredConfiguration of requiredConfigurations) { - const foundConfig = config.find((c) => c.name === requiredConfiguration); + const foundConfig = config.properties.find( + (c) => c.name === requiredConfiguration, + ); if (!foundConfig) { - throw new HttpException( - { - errors: `Missing required configuration ${requiredConfiguration} for FSP ${fsp}`, - }, - HttpStatus.BAD_REQUEST, + errorMessages.push( + `Missing required configuration ${requiredConfiguration} for FSP ${config.financialServiceProviderName}`, ); } } + + return errorMessages; } private async getRegistrationsForPaymentChunked( @@ -433,7 +446,7 @@ export class PaymentsService { ): Promise { await this.checkPaymentInProgressAndThrow(programId); - await this.getProgramWithFspOrThrow(programId); + await this.getProgramWithFspConfigOrThrow(programId); const paPaymentDataList = await this.getPaymentListForRetry( programId, @@ -465,11 +478,17 @@ export class PaymentsService { ); }); - const fspsInPayment: FinancialServiceProviders[] = []; + const programFinancialServiceProviderConfigurationNames: string[] = []; // This loop is pretty fast: with 131k registrations it takes ~38ms for (const registration of paPaymentDataList) { - if (!fspsInPayment.includes(registration.fspName)) { - fspsInPayment.push(registration.fspName); + if ( + !programFinancialServiceProviderConfigurationNames.includes( + registration.programFinancialServiceProviderConfigurationName, + ) + ) { + programFinancialServiceProviderConfigurationNames.push( + registration.programFinancialServiceProviderConfigurationName, + ); } } @@ -477,16 +496,16 @@ export class PaymentsService { totalFilterCount: paPaymentDataList.length, applicableCount: paPaymentDataList.length, nonApplicableCount: 0, - fspsInPayment, + programFinancialServiceProviderConfigurationNames, }; } - private async getProgramWithFspOrThrow( + private async getProgramWithFspConfigOrThrow( programId: number, ): Promise { const program = await this.programRepository.findOne({ where: { id: Equal(programId) }, - relations: ['financialServiceProviders'], + relations: ['programFinancialServiceProviderConfigurations'], }); if (!program) { const errors = 'Program not found.'; @@ -612,10 +631,10 @@ export class PaymentsService { ): SplitPaymentListDto { return paPaymentDataList.reduce( (acc: SplitPaymentListDto, paPaymentData) => { - if (!acc[paPaymentData.fspName]) { - acc[paPaymentData.fspName] = []; + if (!acc[paPaymentData.financialServiceProviderName]) { + acc[paPaymentData.financialServiceProviderName] = []; } - acc[paPaymentData.fspName]!.push(paPaymentData); + acc[paPaymentData.financialServiceProviderName]!.push(paPaymentData); return acc; }, {}, @@ -678,7 +697,7 @@ export class PaymentsService { } const [paymentService, useWhatsapp] = - this.FinancialServiceProvidersToServiceMap[fsp]; + this.financialServiceProviderNameToServiceMap[fsp]; return await paymentService.sendPayment( paPaymentList, programId, @@ -718,14 +737,17 @@ export class PaymentsService { isRetry: boolean; }): Promise { // TODO: REFACTOR: This 'ugly' code is now also in registrations.service.reissueCardAndSendMessage. This should be refactored when there's a better way of getting registration data. - const intersolveVisaQuestionNames = - await this.getFinancialServiceProviderQuestionNames( + const intersolveVisaAttributes = + getFinancialServiceProviderSettingByNameOrThrow( FinancialServiceProviders.intersolveVisa, - ); + ).attributes; + const intersolveVisaAttributeNames = intersolveVisaAttributes.map( + (q) => q.name, + ); const dataFieldNames = [ - 'fullName', - 'phoneNumber', - ...intersolveVisaQuestionNames, + FinancialServiceProviderAttributes.fullName, + FinancialServiceProviderAttributes.phoneNumber, + ...intersolveVisaAttributeNames, ]; const registrationViews = await this.getRegistrationViews( referenceIdsTransactionAmounts, @@ -749,20 +771,36 @@ export class PaymentsService { userId, paymentNumber, referenceId: registrationView.referenceId, + programFinancialServiceProviderConfigurationId: + registrationView.programFinancialServiceProviderConfigurationId, // Use hashmap to lookup transaction amount for this referenceId (with the 4000 chuncksize this takes less than 1ms) transactionAmountInMajorUnit: transactionAmountsMap.get( registrationView.referenceId, )!, isRetry, bulkSize: referenceIdsTransactionAmounts.length, - name: registrationView['fullName'], - addressStreet: registrationView['addressStreet'], - addressHouseNumber: registrationView['addressHouseNumber'], + name: registrationView[ + FinancialServiceProviderAttributes.fullName + ]!, // Fullname is a required field if a registration has visa as FSP + addressStreet: + registrationView[ + FinancialServiceProviderAttributes.addressStreet + ], + addressHouseNumber: + registrationView[ + FinancialServiceProviderAttributes.addressHouseNumber + ], addressHouseNumberAddition: - registrationView['addressHouseNumberAddition'], - addressPostalCode: registrationView['addressPostalCode'], - addressCity: registrationView['addressCity'], - phoneNumber: registrationView.phoneNumber, + registrationView[ + FinancialServiceProviderAttributes.addressHouseNumberAddition + ], + addressPostalCode: + registrationView[ + FinancialServiceProviderAttributes.addressPostalCode + ], + addressCity: + registrationView[FinancialServiceProviderAttributes.addressCity], + phoneNumber: registrationView.phoneNumber!, // Phonenumber is a required field if a registration has visa as FSP }; }, ); @@ -793,14 +831,13 @@ export class PaymentsService { paymentNumber: number; isRetry: boolean; }): Promise { - const safaricomQuestionNames = - await this.getFinancialServiceProviderQuestionNames( - FinancialServiceProviders.safaricom, - ); - const dataFieldNames = ['nationalId', ...safaricomQuestionNames]; + const safaricomAttributes = getFinancialServiceProviderSettingByNameOrThrow( + FinancialServiceProviders.safaricom, + ).attributes; + const safaricomAttributeNames = safaricomAttributes.map((q) => q.name); const registrationViews = await this.getRegistrationViews( referenceIdsTransactionAmounts, - dataFieldNames, + safaricomAttributeNames, programId, ); @@ -818,14 +855,17 @@ export class PaymentsService { programId, paymentNumber, referenceId: registrationView.referenceId, + programFinancialServiceProviderConfigurationId: + registrationView.programFinancialServiceProviderConfigurationId, transactionAmount: transactionAmountsMap.get( registrationView.referenceId, )!, isRetry, userId, bulkSize: referenceIdsTransactionAmounts.length, - phoneNumber: registrationView.phoneNumber, - idNumber: registrationView['nationalId'], + phoneNumber: registrationView.phoneNumber!, // Phonenumber is a required field if a registration has safaricom as FSP + idNumber: + registrationView[FinancialServiceProviderAttributes.nationalId], originatorConversationId: uuid(), // OriginatorConversationId is not used for reconciliation by clients, so can be any random unique identifier }; }); @@ -834,16 +874,6 @@ export class PaymentsService { ); } - private async getFinancialServiceProviderQuestionNames( - FinancialServiceProviders: FinancialServiceProviders, - ): Promise { - const questions = - await this.financialServiceProviderQuestionRepository.getQuestionsByFspName( - FinancialServiceProviders, - ); - return questions.map((q) => q.name); - } - private async getRegistrationViews( referenceIdsTransactionAmounts: ReferenceIdAndTransactionAmountInterface[], dataFieldNames: string[], @@ -909,18 +939,26 @@ export class PaymentsService { .createQueryBuilder('registration') .select('"referenceId"') .addSelect('registration.id as id') - .addSelect('fsp.fsp as "fspName"') + .addSelect( + '"fspConfig"."financialServiceProviderName" as "financialServiceProviderName"', + ) + .addSelect( + '"fspConfig"."id" as "programFinancialServiceProviderConfigurationId"', + ) .andWhere('registration."programId" = :programId', { programId }) - .leftJoin('registration.fsp', 'fsp'); + .leftJoin( + 'registration.programFinancialServiceProviderConfiguration', + 'fspConfig', + ); q.addSelect((subQuery) => { return subQuery .addSelect('value', 'paymentAddress') - .from(RegistrationDataEntity, 'data') - .leftJoin('data.fspQuestion', 'question') - .andWhere('question.name IN (:...names)', { + .from(RegistrationAttributeDataEntity, 'data') + .leftJoin('data.programRegistrationAttribute', 'attribute') + .andWhere('attribute.name IN (:...names)', { names: [ - CustomDataAttributes.phoneNumber, - CustomDataAttributes.whatsappPhoneNumber, + DefaultRegistrationDataAttributeNames.phoneNumber, + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber, ], }) .andWhere('data.registrationId = registration.id') @@ -935,10 +973,14 @@ export class PaymentsService { payment: number, userId: number, referenceIds?: string[], - ): Promise { + ): Promise { let q = this.getPaymentRegistrationsQuery(programId); q = this.failedTransactionForRegistrationAndPayment(q, payment); + q.addSelect( + '"fspConfig"."name" as "programFinancialServiceProviderConfigurationName"', + ); + // If referenceIds passed, only retry those let rawResult; if (referenceIds && referenceIds.length > 0) { @@ -973,7 +1015,7 @@ export class PaymentsService { }); rawResult = await q.getRawMany(); } - const result: PaPaymentDataDto[] = []; + const result: PaPaymentRetryDataDto[] = []; for (const row of rawResult) { row['userId'] = userId; result.push(row); @@ -998,10 +1040,12 @@ export class PaymentsService { for (const row of result) { const paPaymentData: PaPaymentDataDto = { userId, + programFinancialServiceProviderConfigurationId: + row.programFinancialServiceProviderConfigurationId, transactionAmount: amount * row.paymentAmountMultiplier, referenceId: row.referenceId, paymentAddress: row.paymentAddress, - fspName: row.fspName, + financialServiceProviderName: row.financialServiceProviderName, bulkSize, }; paPaymentDataList.push(paPaymentData); @@ -1011,79 +1055,99 @@ export class PaymentsService { public async getImportInstructionsTemplate( programId: number, - ): Promise { - const programWithReconciliationFsps = await this.programRepository.findOne({ + ): Promise { + const programWithExcelFspConfigs = await this.programRepository.findOne({ where: { id: Equal(programId), - financialServiceProviders: { - fsp: Equal(FinancialServiceProviders.excel), + programFinancialServiceProviderConfigurations: { + financialServiceProviderName: Equal(FinancialServiceProviders.excel), + }, + }, + relations: ['programFinancialServiceProviderConfigurations'], + order: { + programFinancialServiceProviderConfigurations: { + name: 'ASC', }, }, - relations: ['financialServiceProviders'], - select: ['id'], }); - if (!programWithReconciliationFsps) { - throw new HttpException('Program or FSP not found', HttpStatus.NOT_FOUND); + if (!programWithExcelFspConfigs) { + throw new HttpException( + 'No program with `Excel` FSP found', + HttpStatus.NOT_FOUND, + ); + } + + const templates: GetImportTemplateResponseDto[] = []; + for (const fspConfig of programWithExcelFspConfigs.programFinancialServiceProviderConfigurations) { + const matchColumn = await this.excelService.getImportMatchColumn( + fspConfig.id, + ); + templates.push({ + name: fspConfig.name, + template: [matchColumn, 'status'], + }); } - const matchColumn = await this.excelService.getImportMatchColumn(programId); - return [matchColumn, 'status']; + return templates; } public async getFspInstructions( programId: number, payment: number, userId: number, - ): Promise { - const exportPaymentTransactions = ( - await this.transactionsService.getLastTransactions(programId, payment) - ).filter( - (t) => - t.fspIntegrationType !== FinancialServiceProviderIntegrationType.api, + ): Promise { + const transactions = await this.transactionsService.getLastTransactions( + programId, + payment, ); - if (exportPaymentTransactions.length === 0) { + const programFspConfigEntitiesWithFspInstruction = + await this.programFinancialServiceProviderConfigurationRepository.find({ + where: { + programId: Equal(programId), + financialServiceProviderName: In( + this.getFspNamesThatRequireInstructions(), + ), + }, + order: { + name: 'ASC', + }, + }); + + const transactionsWithFspInstruction = + this.filterTransactionsWithFspInstructionBasedOnStatus( + transactions, + programFspConfigEntitiesWithFspInstruction, + ); + + if (transactionsWithFspInstruction.length === 0) { throw new HttpException( 'No transactions found for this payment with FSPs that require to download payment instructions.', HttpStatus.NOT_FOUND, ); } - let excelInstructions: ExcelFspInstructions[] = []; - - // REFACTOR: below code seems to facilitate multiple non-api FSPs in 1 payment, but does not actually handle this correctly. - // REFACTOR: below code should be transformed to paginate-queries instead of per PA, like the Excel-FSP code below - for await (const transaction of exportPaymentTransactions.filter( - (t) => t.fsp !== FinancialServiceProviders.excel, - )) { - const registration = - await this.registrationScopedRepository.findOneOrFail({ - where: { referenceId: Equal(transaction.referenceId) }, - relations: ['fsp'], + /// Seprate transactionsWithFspInstruction based on their programFinancialServiceProviderConfigurationName + const allFspInstructions: FspInstructions[] = []; + for (const fspConfigEntity of programFspConfigEntitiesWithFspInstruction) { + const fspInstructions = + await this.getFspInstructionsPerProgramFspConfiguration({ + programId, + payment, + transactions: transactionsWithFspInstruction.filter( + (t) => + t.programFinancialServiceProviderConfigurationName === + fspConfigEntity.name, + ), + programFinancialServiceProviderConfigurationName: + fspConfigEntity.name, + programFinancialServiceProviderConfigurationId: fspConfigEntity.id, + financialServiceProviderName: + fspConfigEntity.financialServiceProviderName, }); - - if ( - // For fsp's with reconciliation export only export waiting transactions - registration.fsp.hasReconciliation && - transaction.status !== TransactionStatusEnum.waiting - ) { - continue; - } - } - - // It is assumed the Excel FSP is not combined with other non-api FSPs above, and they are overwritten - const excelTransactions = exportPaymentTransactions.filter( - (t) => - t.fsp === FinancialServiceProviders.excel && - t.status === TransactionStatusEnum.waiting, // only 'waiting' given that Excel FSP has reconciliation - ); - if (excelTransactions.length) { - excelInstructions = await this.excelService.getFspInstructions( - excelTransactions, - programId, - payment, - ); + // Should we exclude empty instructions where fspInstructions.data.length is empty, I think it is clearer for the user if they than get an empty file + allFspInstructions.push(fspInstructions); } await this.actionService.saveAction( @@ -1091,100 +1155,142 @@ export class PaymentsService { programId, AdditionalActionType.exportFspInstructions, ); + return allFspInstructions; + } - return { - data: excelInstructions, - }; + private getFspNamesThatRequireInstructions(): string[] { + return FINANCIAL_SERVICE_PROVIDER_SETTINGS.filter((fsp) => + [FinancialServiceProviderIntegrationType.csv].includes( + fsp.integrationType, + ), + ).map((fsp) => fsp.name); } - public async importFspReconciliationData( - file: Blob, - programId: number, - payment: number, - userId: number, - ): Promise { - // REFACTOR: below code seems to facilitate multiple non-api FSPs in 1 payment, but does not actually handle this correctly. - const programWithReconciliationFsps = - await this.programRepository.findOneOrFail({ - where: { - id: Equal(programId), - financialServiceProviders: { hasReconciliation: Equal(true) }, - }, - relations: ['financialServiceProviders'], - }); + private filterTransactionsWithFspInstructionBasedOnStatus( + transactions: TransactionReturnDto[], + programFspConfigEntitiesWithFspInstruction: ProgramFinancialServiceProviderConfigurationEntity[], + ): TransactionReturnDto[] { + const programFspConfigNamesThatRequireInstructions = + programFspConfigEntitiesWithFspInstruction.map((c) => c.name); + + const transactionsWithFspInstruction = transactions.filter((t) => + programFspConfigNamesThatRequireInstructions.includes( + t.programFinancialServiceProviderConfigurationName, + ), + ); - let importResponseRecords: any[] = []; - for await (const fsp of programWithReconciliationFsps.financialServiceProviders) { - if (fsp.fsp === FinancialServiceProviders.excel) { - const maxRecords = 10000; - const matchColumn = - await this.excelService.getImportMatchColumn(programId); - const excelRegistrations = - await this.excelService.getRegistrationsForReconciliation( - programId, - payment, - matchColumn, - ); - if (!excelRegistrations?.length) { - continue; - } - const validatedExcelImport = await this.fileImportService.validateCsv( - file, - maxRecords, - ); - const transactions = await this.transactionsService.getLastTransactions( + const result: TransactionReturnDto[] = []; + for (const transaction of transactionsWithFspInstruction) { + if ( + // Only export waiting transactions, as others have already been reconciliated + transaction.status === TransactionStatusEnum.waiting + ) { + result.push(transaction); + } + } + return result; + } + + private async getFspInstructionsPerProgramFspConfiguration({ + transactions, + programId, + payment, + programFinancialServiceProviderConfigurationName, + programFinancialServiceProviderConfigurationId, + financialServiceProviderName, + }: { + transactions: TransactionReturnDto[]; + programId: number; + payment: number; + programFinancialServiceProviderConfigurationName: string; + programFinancialServiceProviderConfigurationId: number; + financialServiceProviderName: FinancialServiceProviders; + }): Promise { + if (financialServiceProviderName === FinancialServiceProviders.excel) { + return { + data: await this.excelService.getFspInstructions({ + transactions, programId, payment, - undefined, - undefined, - FinancialServiceProviders.excel, - ); - importResponseRecords = - this.excelService.joinRegistrationsAndImportRecords( - excelRegistrations, - validatedExcelImport, - matchColumn, - transactions, - ); - } + programFinancialServiceProviderConfigurationId, + }), + fileNamePrefix: programFinancialServiceProviderConfigurationName, + }; } + // Is this the best way to prevent a typeerror on the return type? + throw new Error( + `FinancialServiceProviderName ${financialServiceProviderName} not supported in fsp export`, + ); + } - let countPaymentSuccess = 0; - let countPaymentFailed = 0; - let countNotFound = 0; - const transactionsToSave: any[] = []; - for (const importResponseRecord of importResponseRecords) { - if (!importResponseRecord.paTransactionResult) { - importResponseRecord.importStatus = ImportStatus.notFound; - countNotFound += 1; - continue; + public async importFspReconciliationData( + file: Express.Multer.File, + programId: number, + payment: number, + userId: number, + ): Promise<{ + importResult: ReconciliationFeedbackDto[]; + aggregateImportResult: { + countPaymentFailed: number; + countPaymentSuccess: number; + countNotFound: number; + }; + }> { + const program = await this.programRepository.findOneOrFail({ + where: { + id: Equal(programId), + }, + relations: ['programFinancialServiceProviderConfigurations'], + }); + const fspConfigsExcel: ProgramFinancialServiceProviderConfigurationEntity[] = + []; + for (const fspConfig of program.programFinancialServiceProviderConfigurations) { + if ( + fspConfig.financialServiceProviderName === + FinancialServiceProviders.excel + ) { + fspConfigsExcel.push(fspConfig); } - - transactionsToSave.push(importResponseRecord.paTransactionResult); - importResponseRecord.importStatus = ImportStatus.imported; - countPaymentSuccess += Number( - importResponseRecord.paTransactionResult.status === - TransactionStatusEnum.success, - ); - countPaymentFailed += Number( - importResponseRecord.paTransactionResult.status === - TransactionStatusEnum.error, + } + if (!fspConfigsExcel.length) { + throw new HttpException( + 'Other reconciliation FSPs than `Excel` are currently not supported.', + HttpStatus.NOT_FOUND, ); - delete importResponseRecord.paTransactionResult; } - if (transactionsToSave.length) { - const transactionRelationDetails = { - programId, - paymentNr: payment, - userId, - }; - await this.transactionsService.storeAllTransactionsBulk( - transactionsToSave, - transactionRelationDetails, + const importResults = await this.excelService.processReconciliationData({ + file, + payment, + programId, + fspConfigs: fspConfigsExcel, + }); + + for (const fspConfig of fspConfigsExcel) { + const transactions = importResults + .filter( + (r) => + r.programFinancialServiceProviderConfigurationId === fspConfig.id, + ) + .map((r) => r.transaction) + .filter((t): t is PaTransactionResultDto => t !== undefined); + + await this.transactionsService.storeReconciliationTransactionsBulk( + transactions, + { + programId, + paymentNr: payment, + userId, + programFinancialServiceProviderConfigurationId: fspConfig.id, + }, ); } + const feedback: ReconciliationFeedbackDto[] = importResults.map( + (r) => r.feedback, + ); + const aggregateImportResult = this.countFeedbackResults(feedback); + await this.actionService.saveAction( userId, programId, @@ -1192,12 +1298,34 @@ export class PaymentsService { ); return { - importResult: importResponseRecords, - aggregateImportResult: { - countPaymentFailed, - countPaymentSuccess, - countNotFound, - }, + importResult: feedback, + aggregateImportResult, }; } + + private countFeedbackResults(feedback: ReconciliationFeedbackDto[]): { + countPaymentSuccess: number; + countPaymentFailed: number; + countNotFound: number; + } { + let countPaymentSuccess = 0; + let countPaymentFailed = 0; + let countNotFound = 0; + + for (const result of feedback) { + if (!result.referenceId) { + countNotFound += 1; + continue; + } + if (result.importStatus === ImportStatus.paymentSuccess) { + countPaymentSuccess += 1; + } else if (result.importStatus === ImportStatus.paymentFailed) { + countPaymentFailed += 1; + } else if (result.importStatus === ImportStatus.notFound) { + countNotFound += 1; + } + } + + return { countPaymentSuccess, countPaymentFailed, countNotFound }; + } } diff --git a/services/121-service/src/payments/transactions/dto/get-audited-transaction.dto.ts b/services/121-service/src/payments/transactions/dto/get-audited-transaction.dto.ts index c0f9f811bc..ccdd1659b0 100644 --- a/services/121-service/src/payments/transactions/dto/get-audited-transaction.dto.ts +++ b/services/121-service/src/payments/transactions/dto/get-audited-transaction.dto.ts @@ -10,8 +10,9 @@ export interface GetAuditedTransactionDto { amount: number; errorMessage?: string; customData?: string; - fspName: LocalizedString; - fsp: FinancialServiceProviders; + programFinancialServiceProviderConfigurationLabel: LocalizedString; + programFinancialServiceProviderConfigurationName: string; + financialServiceProviderName: FinancialServiceProviders; fspIntegrationType: string; userId: number; username: string; diff --git a/services/121-service/src/payments/transactions/dto/get-transaction.dto.ts b/services/121-service/src/payments/transactions/dto/get-transaction.dto.ts index 9b2ee3099f..957323626a 100644 --- a/services/121-service/src/payments/transactions/dto/get-transaction.dto.ts +++ b/services/121-service/src/payments/transactions/dto/get-transaction.dto.ts @@ -1,7 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Relation } from 'typeorm'; import { FinancialServiceProviderIntegrationType } from '@121-service/src/financial-service-providers/enum/financial-service-provider-integration-type.enum'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; import { UserOwnerDto } from '@121-service/src/user/dto/user-owner.dto'; export class TransactionReturnDto { @@ -23,10 +26,13 @@ export class TransactionReturnDto { public errorMessage: string; @ApiProperty() public customData: any; - @ApiProperty({ example: 'Visa debit card', type: 'string' }) - public fspName: string; - @ApiProperty({ example: 'Intersolve-visa', type: 'string' }) - public fsp: string; + // FinancialServiceProviderName is used in the frontend to determine whether a transaction has a voucher + @ApiProperty({ example: FinancialServiceProviders.excel }) + public financialServiceProviderName: Relation; + @ApiProperty({ example: 'ironBank' }) + public programFinancialServiceProviderConfigurationLabel: Relation; + @ApiProperty({ example: { en: 'Iron bank' }, type: 'string' }) + public programFinancialServiceProviderConfigurationName: string; @ApiProperty({ example: FinancialServiceProviderIntegrationType.api, type: 'string', diff --git a/services/121-service/src/payments/transactions/transaction.entity.ts b/services/121-service/src/payments/transactions/transaction.entity.ts index da8b4450bb..c56f1d70fc 100644 --- a/services/121-service/src/payments/transactions/transaction.entity.ts +++ b/services/121-service/src/payments/transactions/transaction.entity.ts @@ -9,8 +9,8 @@ import { } from 'typeorm'; import { Base121AuditedEntity } from '@121-service/src/base-audited.entity'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; import { LatestTransactionEntity } from '@121-service/src/payments/transactions/latest-transaction.entity'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; import { UserEntity } from '@121-service/src/user/user.entity'; @@ -52,14 +52,17 @@ export class TransactionEntity extends Base121AuditedEntity { public transactionStep: number; @ManyToOne( - (_type) => FinancialServiceProviderEntity, - (financialServiceProvider) => financialServiceProvider.transactions, + (_type) => ProgramFinancialServiceProviderConfigurationEntity, + (programFinancialServiceProviderConfiguration) => + programFinancialServiceProviderConfiguration.transactions, ) - @JoinColumn({ name: 'financialServiceProviderId' }) - public financialServiceProvider: Relation; + @JoinColumn({ + name: 'programFinancialServiceProviderConfigurationId', + }) + public programFinancialServiceProviderConfiguration: Relation; @Index() @Column({ type: 'int' }) - public financialServiceProviderId: number; + public programFinancialServiceProviderConfigurationId: number; @ManyToOne( (_type) => RegistrationEntity, diff --git a/services/121-service/src/payments/transactions/transaction.repository.ts b/services/121-service/src/payments/transactions/transaction.repository.ts index 92e0d0db9b..de80ffa0e9 100644 --- a/services/121-service/src/payments/transactions/transaction.repository.ts +++ b/services/121-service/src/payments/transactions/transaction.repository.ts @@ -2,7 +2,6 @@ import { Inject } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { InjectRepository } from '@nestjs/typeorm'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { GetAuditedTransactionDto } from '@121-service/src/payments/transactions/dto/get-audited-transaction.dto'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; @@ -21,7 +20,7 @@ export class TransactionScopedRepository extends ScopedRepository { let transactionQuery = this.createQueryBuilder('transaction') .select([ @@ -60,11 +59,15 @@ export class TransactionScopedRepository extends ScopedRepository; @InjectRepository(LatestTransactionEntity) private readonly latestTransactionRepository: Repository; - @InjectRepository(FinancialServiceProviderEntity) - private readonly financialServiceProviderRepository: Repository; + @InjectRepository(TwilioMessageEntity) private readonly twilioMessageRepository: Repository; @@ -83,7 +81,7 @@ export class TransactionsService { payment?: number, referenceId?: string, status?: TransactionStatusEnum, - fspName?: FinancialServiceProviders, + programFinancialServiceProviderConfigId?: number, ): Promise { return this.transactionScopedRepository .getLastTransactionsQuery({ @@ -91,7 +89,7 @@ export class TransactionsService { payment, referenceId, status, - fspName, + programFinancialServiceProviderConfigId, }) .getRawMany(); } @@ -104,9 +102,7 @@ export class TransactionsService { const program = await this.programRepository.findOneByOrFail({ id: relationDetails.programId, }); - const fsp = await this.financialServiceProviderRepository.findOneOrFail({ - where: { fsp: Equal(transactionResponse.fspName) }, - }); + const registration = await this.registrationScopedRepository.findOneOrFail({ where: { referenceId: Equal(transactionResponse.referenceId) }, }); @@ -115,7 +111,8 @@ export class TransactionsService { transaction.amount = transactionResponse.calculatedAmount; transaction.created = transactionResponse.date || new Date(); transaction.registration = registration; - transaction.financialServiceProvider = fsp; + transaction.programFinancialServiceProviderConfigurationId = + relationDetails.programFinancialServiceProviderConfigurationId; transaction.program = program; transaction.payment = relationDetails.paymentNr; transaction.userId = relationDetails.userId; @@ -137,6 +134,10 @@ export class TransactionsService { ); } + const notifyOnTransaction = getFinancialServiceProviderSettingByNameOrThrow( + transactionResponse.fspName, + ).notifyOnTransaction; + await this.updatePaymentCountRegistration( registration, program.enableMaxPayments, @@ -144,7 +145,7 @@ export class TransactionsService { await this.updateLatestTransaction(transaction); if ( transactionResponse.status === TransactionStatusEnum.success && - fsp.notifyOnTransaction && + notifyOnTransaction && transactionResponse.notificationObjects && transactionResponse.notificationObjects.length > 0 ) { @@ -283,49 +284,56 @@ export class TransactionsService { } public async storeAllTransactions( - transactionResults: { paList: PaTransactionResultDto[] }, - transactionRelationDetails: Required, + transactionResultObjects: { + paTransactionResultDto: PaTransactionResultDto; + transactionRelationDetailsDto: TransactionRelationDetailsDto; + }[], ): Promise { - // Intersolve transactions are now stored during PA-request-loop already - // Align across FSPs in future again - for (const transaction of transactionResults.paList) { + // Currently only used for Excel FSP + for (const transactionResultObject of transactionResultObjects) { await this.storeTransactionUpdateStatus( - transaction, - transactionRelationDetails, + transactionResultObject.paTransactionResultDto, + transactionResultObject.transactionRelationDetailsDto, ); } } - public async storeAllTransactionsBulk( + public async storeReconciliationTransactionsBulk( transactionResults: PaTransactionResultDto[], transactionRelationDetails: TransactionRelationDetailsDto, - transactionStep?: number, ): Promise { // NOTE: this method is currently only used for the import-fsp-reconciliation use case and assumes: - // 1: only 1 FSP + // 1: only 1 program financial service provider id // 2: no notifications to send // 3: no payment count to update (as it is reconciliation of existing payment) // 4: no twilio message to relate to - // 5: registrationId to be known in transactionResults - const program = await this.programRepository.findOneBy({ - id: transactionRelationDetails.programId, - }); - const fsp = await this.financialServiceProviderRepository.findOne({ - where: { fsp: Equal(transactionResults[0].fspName) }, - }); - const transactionsToSave = transactionResults.map( - (transactionResponse) => ({ - amount: transactionResponse.calculatedAmount, - registrationId: transactionResponse.registrationId, - financialServiceProvider: fsp, - program, - payment: transactionRelationDetails.paymentNr, - userId: transactionRelationDetails.userId, - status: transactionResponse.status, - errorMessage: transactionResponse.message, - customData: transactionResponse.customData, - transactionStep: transactionStep || 1, + const transactionsToSave = await Promise.all( + transactionResults.map(async (transactionResponse) => { + // Get registrationId from referenceId if it is not defined + // TODO find out when this is needed it seems to make more sense if the registrationId is always known and than refenceId is not needed + if (!transactionResponse.registrationId) { + const registration = + await this.registrationScopedRepository.findOneOrFail({ + where: { referenceId: Equal(transactionResponse.referenceId) }, + }); + transactionResponse.registrationId = registration.id; + } + + const transaction = new TransactionEntity(); + transaction.amount = transactionResponse.calculatedAmount; + transaction.registrationId = transactionResponse.registrationId; + transaction.programFinancialServiceProviderConfigurationId = + transactionRelationDetails.programFinancialServiceProviderConfigurationId; + transaction.programId = transactionRelationDetails.programId; + transaction.payment = transactionRelationDetails.paymentNr; + transaction.userId = transactionRelationDetails.userId; + transaction.status = transactionResponse.status; + transaction.errorMessage = transactionResponse.message ?? null; + transaction.customData = transactionResponse.customData; + transaction.transactionStep = 1; + // set other properties as needed + return transaction; }), ); @@ -336,16 +344,12 @@ export class TransactionsService { ); for (const chunk of transactionChunks) { - const savedTransactions = await this.transactionScopedRepository - .createQueryBuilder('transaction') - .insert() - .into(TransactionEntity) - .values(chunk as QueryDeepPartialEntity) - .execute(); + const savedTransactions = + await this.transactionScopedRepository.save(chunk); const savedTransactionEntities = await this.transactionScopedRepository.find({ where: { - id: In(savedTransactions.identifiers.map((i) => i.id)), + id: In(savedTransactions.map((i) => i.id)), }, }); diff --git a/services/121-service/src/program-attributes/program-attributes.module.ts b/services/121-service/src/program-attributes/program-attributes.module.ts index 165634220f..7bfc206619 100644 --- a/services/121-service/src/program-attributes/program-attributes.module.ts +++ b/services/121-service/src/program-attributes/program-attributes.module.ts @@ -1,19 +1,15 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { ProgramAttributesService } from '@121-service/src/program-attributes/program-attributes.service'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; @Module({ imports: [ TypeOrmModule.forFeature([ ProgramEntity, - ProgramQuestionEntity, - ProgramCustomAttributeEntity, - FspQuestionEntity, + ProgramRegistrationAttributeEntity, ]), ], providers: [ProgramAttributesService], diff --git a/services/121-service/src/program-attributes/program-attributes.service.spec.ts b/services/121-service/src/program-attributes/program-attributes.service.spec.ts index f8d2a8d768..ec4f33b01b 100644 --- a/services/121-service/src/program-attributes/program-attributes.service.spec.ts +++ b/services/121-service/src/program-attributes/program-attributes.service.spec.ts @@ -3,42 +3,26 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { ProgramAttributesService } from '@121-service/src/program-attributes/program-attributes.service'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { QuestionType } from '@121-service/src/registration/enum/custom-data-attributes'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; +import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; import { generateMockCreateQueryBuilder } from '@121-service/src/utils/createQueryBuilderMock.helper'; describe('ProgramAttributesService', () => { + let programRegistrationAttributeRepository: Repository; let programAttributesService: ProgramAttributesService; - let programCustomAttributeRepository: Repository; - - const programCustomAttributeRepositoryToken: string | Function = - getRepositoryToken(ProgramCustomAttributeEntity); const programRepositoryToken: string | Function = getRepositoryToken(ProgramEntity); - const programQuestionToken: string | Function = getRepositoryToken( - ProgramQuestionEntity, - ); - const fspQuestionToken: string | Function = - getRepositoryToken(FspQuestionEntity); + const programRegistrationAttributeToken: string | Function = + getRepositoryToken(ProgramRegistrationAttributeEntity); beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ProgramAttributesService, { - provide: programCustomAttributeRepositoryToken, - useClass: Repository, - }, - { - provide: fspQuestionToken, - useClass: Repository, - }, - { - provide: programQuestionToken, + provide: programRegistrationAttributeToken, useClass: Repository, }, { @@ -51,14 +35,13 @@ describe('ProgramAttributesService', () => { programAttributesService = module.get( ProgramAttributesService, ); - - programCustomAttributeRepository = module.get< - Repository - >(programCustomAttributeRepositoryToken); + programRegistrationAttributeRepository = module.get< + Repository + >(programRegistrationAttributeToken); }); describe('getAttributes', () => { - it('should return only custom attributes if includeCustomAttributes === true', async () => { + it('should return only program registration attributes if includeProgramRegistrationAttributes === true and includeTemplateDefaultAttributes === false', async () => { const dbQueryResult = [ { name: 'test name #1', @@ -74,27 +57,34 @@ describe('ProgramAttributesService', () => { ); jest - .spyOn(programCustomAttributeRepository, 'createQueryBuilder') + .spyOn(programRegistrationAttributeRepository, 'createQueryBuilder') .mockImplementation(() => createQueryBuilder) as any; - const result = await programAttributesService.getAttributes( - 1, - true, - false, - false, - false, - ); + const result = await programAttributesService.getAttributes({ + programId: 1, + includeProgramRegistrationAttributes: true, + includeTemplateDefaultAttributes: false, + }); + + const includeTemplateDefaultAttributes: (keyof RegistrationViewEntity)[] = + [ + 'paymentAmountMultiplier', + 'programFinancialServiceProviderConfigurationLabel', + 'programFinancialServiceProviderConfigurationLabel', + 'paymentCountRemaining', + ]; - const resultTypeMapping = result.map((r) => r.questionType); + const resultPropertyNames = result.map((r) => r.name); expect(result).toBeDefined(); // Test the mapping expect(result[0].label).toBe(dbQueryResult[0].label); - // Test the type assignment - expect(result[0].questionType).toBe(QuestionType.programCustomAttribute); - // Test no other types are included - expect(resultTypeMapping).not.toContain(QuestionType.programQuestion); - expect(resultTypeMapping).not.toContain(QuestionType.fspQuestion); + expect( + resultPropertyNames.every( + (name) => + !includeTemplateDefaultAttributes.map(String).includes(name), + ), + ).toBe(true); }); }); }); diff --git a/services/121-service/src/program-attributes/program-attributes.service.ts b/services/121-service/src/program-attributes/program-attributes.service.ts index b50a521aae..40580e819b 100644 --- a/services/121-service/src/program-attributes/program-attributes.service.ts +++ b/services/121-service/src/program-attributes/program-attributes.service.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FilterOperator } from 'nestjs-paginate'; -import { Equal, In, Repository } from 'typeorm'; +import { Equal, Repository } from 'typeorm'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { AllowedFilterOperatorsNumber, AllowedFilterOperatorsString, @@ -15,18 +13,16 @@ import { import { FilterAttributeDto } from '@121-service/src/registration/dto/filter-attribute.dto'; import { Attribute, - QuestionType, -} from '@121-service/src/registration/enum/custom-data-attributes'; + GenericRegistrationAttributes, + RegistrationAttributeTypes, +} from '@121-service/src/registration/enum/registration-attribute.enum'; + @Injectable() export class ProgramAttributesService { @InjectRepository(ProgramEntity) private readonly programRepository: Repository; - @InjectRepository(ProgramQuestionEntity) - private readonly programQuestionRepository: Repository; - @InjectRepository(ProgramCustomAttributeEntity) - private readonly programCustomAttributeRepository: Repository; - @InjectRepository(FspQuestionEntity) - private readonly fspQuestionRepository: Repository; + @InjectRepository(ProgramRegistrationAttributeEntity) + private readonly programRegistrationAttributeEntity: Repository; public getFilterableAttributes(program: ProgramEntity) { const genericPaAttributeFilters = [ @@ -110,31 +106,20 @@ export class ProgramAttributesService { return filterableAttributes; } - public async getAttributes( - programId: number, - includeCustomAttributes: boolean, - includeProgramQuestions: boolean, - includeFspQuestions: boolean, - includeTemplateDefaultAttributes: boolean, - filterShowInPeopleAffectedTable?: boolean, - ): Promise { - let customAttributes: Attribute[] = []; - if (includeCustomAttributes) { - customAttributes = await this.getAndMapProgramCustomAttributes( - programId, - filterShowInPeopleAffectedTable, - ); - } - let programQuestions: Attribute[] = []; - if (includeProgramQuestions) { - programQuestions = await this.getAndMapProgramQuestions( - programId, - filterShowInPeopleAffectedTable, - ); - } - let fspQuestions: Attribute[] = []; - if (includeFspQuestions) { - fspQuestions = await this.getAndMapProgramFspQuestions( + public async getAttributes({ + programId, + includeProgramRegistrationAttributes, + includeTemplateDefaultAttributes, + filterShowInPeopleAffectedTable, + }: { + programId: number; + includeProgramRegistrationAttributes: boolean; + includeTemplateDefaultAttributes: boolean; + filterShowInPeopleAffectedTable?: boolean; + }): Promise { + let programAttributes: Attribute[] = []; + if (includeProgramRegistrationAttributes) { + programAttributes = await this.getAndMapProgramRegistrationAttributes( programId, filterShowInPeopleAffectedTable, ); @@ -146,12 +131,7 @@ export class ProgramAttributesService { await this.getMessageTemplateDefaultAttributes(programId); } - return [ - ...customAttributes, - ...programQuestions, - ...fspQuestions, - ...templateDefaultAttributes, - ]; + return [...programAttributes, ...templateDefaultAttributes]; } private async getMessageTemplateDefaultAttributes( @@ -163,25 +143,25 @@ export class ProgramAttributesService { }); const defaultAttributes: Attribute[] = [ { - name: 'paymentAmountMultiplier', - type: 'numeric', + name: GenericRegistrationAttributes.paymentAmountMultiplier, + type: RegistrationAttributeTypes.numeric, label: null, }, { - name: 'fspDisplayName', - type: 'text', + name: GenericRegistrationAttributes.programFinancialServiceProviderConfigurationLabel, + type: RegistrationAttributeTypes.text, label: null, }, ]; if (hasMaxPayments?.enableMaxPayments) { defaultAttributes.push({ - name: 'maxPayments', - type: 'numeric', + name: GenericRegistrationAttributes.maxPayments, + type: RegistrationAttributeTypes.numeric, label: null, }); defaultAttributes.push({ - name: 'paymentCountRemaining', - type: 'numeric', + name: GenericRegistrationAttributes.paymentCountRemaining, + type: RegistrationAttributeTypes.numeric, label: null, }); } @@ -191,8 +171,8 @@ export class ProgramAttributesService { public async getPaEditableAttributes( programId: number, ): Promise { - const customAttributes = ( - await this.programCustomAttributeRepository.find({ + const programRegistrationAttributes = ( + await this.programRegistrationAttributeEntity.find({ where: { program: { id: Equal(programId) } }, }) ).map((c) => { @@ -200,104 +180,35 @@ export class ProgramAttributesService { name: c.name, type: c.type, label: c.label, + isRequired: c.isRequired, }; }); - const programQuestions = ( - await this.programQuestionRepository.find({ - where: { - program: { id: Equal(programId) }, - editableInPortal: Equal(true), - }, - }) - ).map((c) => { - return { - name: c.name, - type: c.answerType, - label: c.label, - }; - }); - - return [...customAttributes, ...programQuestions]; + return programRegistrationAttributes; } - private async getAndMapProgramQuestions( - programId: number, - filterShowInPeopleAffectedTable?: boolean, - ): Promise { - let queryProgramQuestions = this.programQuestionRepository - .createQueryBuilder('programQuestion') - .where({ program: { id: programId } }); - - if (filterShowInPeopleAffectedTable) { - queryProgramQuestions = queryProgramQuestions.andWhere({ - showInPeopleAffectedTable: true, - }); - } - const rawProgramQuestions = await queryProgramQuestions.getMany(); - const programQuestions = rawProgramQuestions.map((c) => { - return { - name: c.name, - type: c.answerType, - label: c.label, - questionType: QuestionType.programQuestion, - }; - }); - - return programQuestions; - } - private async getAndMapProgramCustomAttributes( + private async getAndMapProgramRegistrationAttributes( programId: number, filterShowInPeopleAffectedTable?: boolean, ): Promise { - let queryCustomAttr = this.programCustomAttributeRepository - .createQueryBuilder('programCustomAttribute') + let queryRegistrationAttr = this.programRegistrationAttributeEntity + .createQueryBuilder('programRegistrationAttribute') .where({ program: { id: programId } }); if (filterShowInPeopleAffectedTable) { - queryCustomAttr = queryCustomAttr.andWhere({ + queryRegistrationAttr = queryRegistrationAttr.andWhere({ showInPeopleAffectedTable: true, }); } - const rawCustomAttributes = await queryCustomAttr.getMany(); - const customAttributes = rawCustomAttributes.map((c) => { + const rawProgramAttributes = await queryRegistrationAttr.getMany(); + const programAttributes = rawProgramAttributes.map((c) => { return { name: c.name, type: c.type, label: c.label, - questionType: QuestionType.programCustomAttribute, + isRequired: c.isRequired, }; }); - return customAttributes; - } - private async getAndMapProgramFspQuestions( - programId: number, - filterShowInPeopleAffectedTable?: boolean, - ): Promise { - const program = await this.programRepository.findOneOrFail({ - where: { id: Equal(programId) }, - relations: ['financialServiceProviders'], - }); - const fspIds = program.financialServiceProviders.map((f) => f.id); - - let queryFspAttributes = this.fspQuestionRepository - .createQueryBuilder('fspAttribute') - .where({ fspId: In(fspIds) }); - - if (filterShowInPeopleAffectedTable) { - queryFspAttributes = queryFspAttributes.andWhere({ - showInPeopleAffectedTable: true, - }); - } - const rawFspAttributes = await queryFspAttributes.getMany(); - const fspAttributes = rawFspAttributes.map((c) => { - return { - name: c.name, - type: c.answerType, - label: c.label, - questionType: QuestionType.fspQuestion, - }; - }); - return fspAttributes; + return programAttributes; } } diff --git a/services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto.ts b/services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto.ts new file mode 100644 index 0000000000..729a2b9cb6 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty } from 'class-validator'; +import { v4 as uuid } from 'uuid'; + +import { FinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { WrapperType } from '@121-service/src/wrapper.type'; + +export class CreateProgramFinancialServiceProviderConfigurationPropertyDto { + @ApiProperty({ + example: FinancialServiceProviderConfigurationProperties.username, + }) + @IsNotEmpty() + @IsEnum(FinancialServiceProviderConfigurationProperties) + public readonly name: WrapperType; + + @ApiProperty({ + example: `password-${uuid()}`, + description: `Should be string (for e.g. name=username) or array of strings (for e.g. name=columnsToExport)`, + }) + @IsNotEmpty() + public readonly value: string | string[]; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto.ts b/services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto.ts new file mode 100644 index 0000000000..a61d026681 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsDefined, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { v4 as uuid } from 'uuid'; + +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; + +export class CreateProgramFinancialServiceProviderConfigurationDto { + @ApiProperty({ example: 'VisaDebitCards' }) + @IsNotEmpty() + @IsString() + public readonly name: string; + + @ApiProperty({ + example: { + en: 'Visa Debit Cards', + nl: 'Visa-betaalkaarten', + }, + }) + @IsNotEmpty() + public readonly label: LocalizedString; + + @ApiProperty({ + enum: FinancialServiceProviders, + example: FinancialServiceProviders.intersolveVoucherWhatsapp, + }) + @IsNotEmpty() + @IsEnum(FinancialServiceProviders) + public readonly financialServiceProviderName: FinancialServiceProviders; + + @IsArray() + @ValidateNested() + @IsDefined() + @IsOptional() + @Type(() => CreateProgramFinancialServiceProviderConfigurationPropertyDto) + @ApiProperty({ + example: [ + { name: 'username', value: `username-${uuid()}` }, + { name: 'password', value: `password-${uuid()}` }, + ], + }) + public readonly properties?: CreateProgramFinancialServiceProviderConfigurationPropertyDto[]; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto.ts b/services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto.ts new file mode 100644 index 0000000000..1f2f85eb29 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ProgramFinancialServiceProviderConfigurationPropertyResponseDto { + @ApiProperty({ example: 'username' }) + public readonly name: string; + + @ApiProperty({ example: new Date() }) + public updated: Date; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto.ts b/services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto.ts new file mode 100644 index 0000000000..e1a1a3dea8 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviderDto } from '@121-service/src/financial-service-providers/financial-service-provider.dto'; +import { ProgramFinancialServiceProviderConfigurationPropertyResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; + +type FinancialServiceProviderWithoutConfigProps = Omit< + FinancialServiceProviderDto, + 'configurationProperties' | 'defaultLabel' +>; + +export class ProgramFinancialServiceProviderConfigurationResponseDto { + @ApiProperty({ example: 1, type: 'number' }) + public readonly programId: number; + + @ApiProperty({ enum: FinancialServiceProviders }) + public financialServiceProviderName: FinancialServiceProviders; + + @ApiProperty({ example: 'FSP Name', type: 'string' }) + public readonly name: string; + + @ApiProperty({ example: { en: 'FSP display name' } }) + public readonly label: LocalizedString; + + /// Can sometimes be undefined if the financial service provider has been removed from the codebase + @ApiProperty() + public readonly financialServiceProvider?: FinancialServiceProviderWithoutConfigProps; + + @ApiProperty({ + example: [ + { name: 'password', updated: new Date() }, + { name: 'username', updated: new Date() }, + ], + type: 'array', + description: 'Only property names are returned for security reasons', + }) + public readonly properties: ProgramFinancialServiceProviderConfigurationPropertyResponseDto[]; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto.ts b/services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto.ts new file mode 100644 index 0000000000..259496dc9d --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + +export class UpdateProgramFinancialServiceProviderConfigurationPropertyDto { + @ApiProperty({ + example: 'redcross-user', + description: + 'Should be string (for e.g. name=username) or array of strings (for e.g. name=columnsToExport)', + }) + @IsNotEmpty() + value: string | string[]; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto.ts b/services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto.ts new file mode 100644 index 0000000000..201afe64c5 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsDefined, + IsNotEmpty, + IsOptional, + ValidateNested, +} from 'class-validator'; + +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; + +export class UpdateProgramFinancialServiceProviderConfigurationDto { + @ApiProperty({ example: { en: 'FSP display name' } }) + @IsNotEmpty() + public readonly label: LocalizedString; + + @IsArray() + @ValidateNested() + @IsDefined() + @IsOptional() + // The CreateProgramFinancialServiceProviderConfigurationPropertyDto is used here instead of the update one because properties are first deleted and than created instead of updated + @Type(() => CreateProgramFinancialServiceProviderConfigurationPropertyDto) + public readonly properties?: CreateProgramFinancialServiceProviderConfigurationPropertyDto[]; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity.ts b/services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity.ts new file mode 100644 index 0000000000..be5a66b5d3 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity.ts @@ -0,0 +1,60 @@ +import { isObject } from 'lodash'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + Relation, + Unique, +} from 'typeorm'; + +import { CascadeDeleteEntity } from '@121-service/src/base.entity'; +import { FinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; + +@Unique('programFinancialServiceProviderConfigurationPropertyUnique', [ + 'programFinancialServiceProviderConfigurationId', + 'name', +]) +@Entity('program_financial_service_provider_configuration_property') +export class ProgramFinancialServiceProviderConfigurationPropertyEntity extends CascadeDeleteEntity { + @Column({ type: 'character varying' }) + public name: FinancialServiceProviderConfigurationProperties; + + @Column({ + type: 'varchar', + transformer: { + to: (value: any) => { + if (Array.isArray(value) || isObject(value)) { + return JSON.stringify(value); + } + + return value; + }, + from: (value: any) => { + try { + const parsedValue = JSON.parse(value); + if (Array.isArray(parsedValue) || isObject(parsedValue)) { + return parsedValue; + } + + return value; + } catch (error) { + return value; + } + }, + }, + }) + public value: string | string[] | Record; + + @ManyToOne( + (_type) => ProgramFinancialServiceProviderConfigurationEntity, + (programFinancialServiceProviderConfiguration) => + programFinancialServiceProviderConfiguration.properties, + { cascade: true, onDelete: 'CASCADE' }, + ) + @JoinColumn({ name: 'programFinancialServiceProviderConfigurationId' }) + public programFinancialServiceProviderConfiguration: Relation; + @Column() + public programFinancialServiceProviderConfigurationId: number; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity.ts b/services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity.ts new file mode 100644 index 0000000000..6682f06f26 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity.ts @@ -0,0 +1,56 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + Relation, + Unique, +} from 'typeorm'; + +import { CascadeDeleteEntity } from '@121-service/src/base.entity'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; +import { ProgramEntity } from '@121-service/src/programs/program.entity'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; + +@Unique('programFinancialServiceProviderConfigurationUnique', [ + 'programId', + 'name', +]) +@Entity('program_financial_service_provider_configuration') +export class ProgramFinancialServiceProviderConfigurationEntity extends CascadeDeleteEntity { + @ManyToOne( + (_type) => ProgramEntity, + (program) => program.programFinancialServiceProviderConfigurations, + ) + @JoinColumn({ name: 'programId' }) + @Column() + public programId: number; + + @Column({ type: 'character varying' }) + public financialServiceProviderName: FinancialServiceProviders; + + @Column({ type: 'character varying' }) + public name: string; + + @Column('json') + public label: LocalizedString; + + @OneToMany( + (_type) => ProgramFinancialServiceProviderConfigurationPropertyEntity, + (programFinancialServiceProviderConfigurationProperty) => + programFinancialServiceProviderConfigurationProperty.programFinancialServiceProviderConfiguration, + { cascade: ['insert'] }, + ) + public properties: Relation< + ProgramFinancialServiceProviderConfigurationPropertyEntity[] + >; + + @OneToMany( + (_type) => TransactionEntity, + (transactions) => transactions.programFinancialServiceProviderConfiguration, + ) + public transactions: Relation; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/interfaces/required-username-password.interface.ts b/services/121-service/src/program-financial-service-provider-configurations/interfaces/required-username-password.interface.ts new file mode 100644 index 0000000000..1c78cbbc30 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/interfaces/required-username-password.interface.ts @@ -0,0 +1,4 @@ +export interface RequiredUsernamePasswordInterface { + username: string; + password: string; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface.ts b/services/121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface.ts new file mode 100644 index 0000000000..cde2f6ee8a --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface.ts @@ -0,0 +1,4 @@ +export interface UsernamePasswordInterface { + username: string | null; + password: string | null; +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.spec.ts b/services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.spec.ts new file mode 100644 index 0000000000..fbb61e02eb --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.spec.ts @@ -0,0 +1,189 @@ +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; +import { ProgramFinancialServiceProviderConfigurationMapper } from '@121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper'; + +describe('ProgramFinancialServiceProviderConfigurationMapper', () => { + describe('mapEntitytoDto', () => { + it('should correctly map ProgramFinancialServiceProviderConfigurationEntity to ProgramFinancialServiceProviderConfigurationResponseDto', () => { + // Arrange + const testEntity = + new ProgramFinancialServiceProviderConfigurationEntity(); + testEntity.programId = 1; + testEntity.financialServiceProviderName = + FinancialServiceProviders.intersolveVisa; + testEntity.name = 'Intersolve Visa'; + testEntity.label = { en: 'Visa Debit Card' }; + testEntity.properties = [ + { + name: FinancialServiceProviderConfigurationProperties.brandCode, + updated: new Date('2023-01-01'), + }, + { + name: FinancialServiceProviderConfigurationProperties.coverLetterCode, + updated: new Date('2023-02-01'), + }, + ] as ProgramFinancialServiceProviderConfigurationPropertyEntity[]; + + // Act + const result = + ProgramFinancialServiceProviderConfigurationMapper.mapEntityToDto( + testEntity, + ); + + // Assert + expect(result.programId).toBe(testEntity.programId); + expect(result.financialServiceProviderName).toBe( + testEntity.financialServiceProviderName, + ); + expect(result.name).toBe(testEntity.name); + expect(result.label).toEqual(testEntity.label); + + const expectedFinancialServiceProvider = + FINANCIAL_SERVICE_PROVIDER_SETTINGS.find( + (fsp) => fsp.name === testEntity.financialServiceProviderName, + )!; + // Remove unnecessary properties from the financialServiceProvider object + const { + configurationProperties: _configurationProperties, + defaultLabel: _defaultLabel, + ...expectedFinancialServiceProviderWithoutProps + } = expectedFinancialServiceProvider; + + // Now use expectedFinancialServiceProviderWithoutProps in your test + expect(result.financialServiceProvider).toEqual( + expectedFinancialServiceProviderWithoutProps, + ); + + expect(result.properties).toHaveLength(testEntity.properties.length); + testEntity.properties.forEach((property, index) => { + expect(result.properties[index].name).toBe(property.name); + expect(result.properties[index].updated).toEqual(property.updated); + }); + }); + + it('should handle an entity with no properties', () => { + // Arrange + const testEntity = + new ProgramFinancialServiceProviderConfigurationEntity(); + testEntity.programId = 1; + testEntity.financialServiceProviderName = + FinancialServiceProviders.safaricom; + testEntity.name = 'Safaricom M-Pesa'; + testEntity.label = { en: 'Safaricom' }; + testEntity.properties = []; + + // Act + const result = + ProgramFinancialServiceProviderConfigurationMapper.mapEntityToDto( + testEntity, + ); + + // Assert + expect(result.programId).toBe(testEntity.programId); + expect(result.financialServiceProviderName).toBe( + testEntity.financialServiceProviderName, + ); + expect(result.name).toBe(testEntity.name); + expect(result.label).toEqual(testEntity.label); + const expectedFinancialServiceProvider = + FINANCIAL_SERVICE_PROVIDER_SETTINGS.find( + (fsp) => fsp.name === testEntity.financialServiceProviderName, + )!; + // Remove unnecessary properties from the financialServiceProvider object + const { + configurationProperties: _configurationProperties, + defaultLabel: _defaultLabel, + ...expectedFinancialServiceProviderWithoutProps + } = expectedFinancialServiceProvider; + expect(result.financialServiceProvider).toEqual( + expectedFinancialServiceProviderWithoutProps, + ); + expect(result.properties).toEqual([]); + }); + }); + + describe('mapDtoToEntity', () => { + it('should correctly map CreateProgramFinancialServiceProviderConfigurationDto to ProgramFinancialServiceProviderConfigurationEntity', () => { + // Arrange + const programId = 1; + const dto: CreateProgramFinancialServiceProviderConfigurationDto = { + financialServiceProviderName: FinancialServiceProviders.intersolveVisa, + name: 'Intersolve Visa in program 1', + label: { en: 'Visa Debit Card' }, + }; + + // Act + const entity = + ProgramFinancialServiceProviderConfigurationMapper.mapDtoToEntity( + dto, + programId, + ); + + // Assert + expect(entity.programId).toBe(programId); + expect(entity.financialServiceProviderName).toBe( + dto.financialServiceProviderName, + ); + expect(entity.name).toBe(dto.name); + expect(entity.label).toEqual(dto.label); + }); + }); + + describe('mapPropertyDtoToEntities', () => { + it('should correctly map CreateProgramFinancialServiceProviderConfigurationPropertyDto to ProgramFinancialServiceProviderConfigurationPropertyEntity', () => { + // Arrange + const dto: CreateProgramFinancialServiceProviderConfigurationPropertyDto = + { + name: FinancialServiceProviderConfigurationProperties.brandCode, + value: 'brand123', + }; + const programFinancialServiceProviderConfigurationId = 1; + + // Act + const entities = + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyDtosToEntities( + [dto], + programFinancialServiceProviderConfigurationId, + ); + const entity = entities[0]; + // Assert + expect(entity.name).toBe(dto.name); + expect(entity.programFinancialServiceProviderConfigurationId).toBe( + programFinancialServiceProviderConfigurationId, + ); + expect(entity.value).toBe(dto.value); + }); + + it('should correctly handle columnsToExport as an array and convert to JSON string', () => { + // Arrange + const dto: CreateProgramFinancialServiceProviderConfigurationPropertyDto = + { + name: FinancialServiceProviderConfigurationProperties.columnsToExport, + value: ['column1', 'column2', 'column3'], + }; + const programFinancialServiceProviderConfigurationId = 2; + + // Act + const entities = + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyDtosToEntities( + [dto], + programFinancialServiceProviderConfigurationId, + ); + const entity = entities[0]; + + // Assert + expect(entity.name).toBe(dto.name); + expect(entity.programFinancialServiceProviderConfigurationId).toBe( + programFinancialServiceProviderConfigurationId, + ); + expect(entity.value).toBe(JSON.stringify(dto.value)); // Expect value to be JSON string + }); + }); +}); diff --git a/services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.ts b/services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.ts new file mode 100644 index 0000000000..c79b6b78a4 --- /dev/null +++ b/services/121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper.ts @@ -0,0 +1,123 @@ +import { FinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { getFinancialServiceProviderSettingByNameOrThrow } from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { ProgramFinancialServiceProviderConfigurationPropertyResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto'; +import { ProgramFinancialServiceProviderConfigurationResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; + +export class ProgramFinancialServiceProviderConfigurationMapper { + public static mapEntitiesToDtos( + entities: ProgramFinancialServiceProviderConfigurationEntity[], + ): ProgramFinancialServiceProviderConfigurationResponseDto[] { + return entities.map((entity) => + ProgramFinancialServiceProviderConfigurationMapper.mapEntityToDto(entity), + ); + } + + public static mapEntityToDto( + entity: ProgramFinancialServiceProviderConfigurationEntity, + ): ProgramFinancialServiceProviderConfigurationResponseDto { + // Remove unnecessary properties from the financialServiceProvider object + const { + configurationProperties: _configurationProperties, + defaultLabel: _defaultLabel, + ...financialServiceProvider + } = getFinancialServiceProviderSettingByNameOrThrow( + entity.financialServiceProviderName, + ); + + const dto: ProgramFinancialServiceProviderConfigurationResponseDto = { + programId: entity.programId, + financialServiceProviderName: entity.financialServiceProviderName, + name: entity.name, + label: entity.label, + financialServiceProvider, + properties: + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyEntitiesToDtos( + entity.properties, + ), + }; + return dto; + } + + public static mapDtoToEntity( + dto: CreateProgramFinancialServiceProviderConfigurationDto, + programId: number, + ): ProgramFinancialServiceProviderConfigurationEntity { + const entity = new ProgramFinancialServiceProviderConfigurationEntity(); + entity.programId = programId; + entity.financialServiceProviderName = dto.financialServiceProviderName; + entity.name = dto.name; + entity.label = dto.label; + return entity; + } + + public static mapPropertyEntitiesToDtos( + properties?: ProgramFinancialServiceProviderConfigurationPropertyEntity[], + ): ProgramFinancialServiceProviderConfigurationPropertyResponseDto[] { + if (!properties) { + return []; + } + return properties.map((property) => + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyEntityToDto( + property, + ), + ); + } + + public static mapPropertyEntityToDto( + property: ProgramFinancialServiceProviderConfigurationPropertyEntity, + ): ProgramFinancialServiceProviderConfigurationPropertyResponseDto { + return { + name: property.name, + updated: property.updated, + }; + } + + public static mapPropertyDtosToEntities( + dtos: CreateProgramFinancialServiceProviderConfigurationPropertyDto[], + programFinancialServiceProviderConfigurationId: number, + ): ProgramFinancialServiceProviderConfigurationPropertyEntity[] { + return dtos.map((dto) => + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyDtoToEntity( + dto, + programFinancialServiceProviderConfigurationId, + ), + ); + } + + private static mapPropertyDtoToEntity( + dto: CreateProgramFinancialServiceProviderConfigurationPropertyDto, + programFinancialServiceProviderConfigurationId: number, + ): ProgramFinancialServiceProviderConfigurationPropertyEntity { + const entity = + new ProgramFinancialServiceProviderConfigurationPropertyEntity(); + entity.name = dto.name; + entity.programFinancialServiceProviderConfigurationId = + programFinancialServiceProviderConfigurationId; + // Later we can add a switch case and a type for each property if there are more non-string properties + entity.value = + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyDtoValueToEntityValue( + dto.value, + dto.name, + ); + return entity; + } + + public static mapPropertyDtoValueToEntityValue( + dtoValue: string | string[], + property: FinancialServiceProviderConfigurationProperties, + ): string { + // For now columnsToExport is the only property that is an array + // Later we can add a switch case and a type for each property if there are more non-string properties + if ( + property === + FinancialServiceProviderConfigurationProperties.columnsToExport + ) { + return JSON.stringify(dtoValue); + } + return dtoValue as string; + } +} diff --git a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity.ts b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity.ts deleted file mode 100644 index 2305a963e3..0000000000 --- a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { isObject } from 'lodash'; -import { - Column, - Entity, - JoinColumn, - ManyToOne, - Relation, - Unique, -} from 'typeorm'; - -import { CascadeDeleteEntity } from '@121-service/src/base.entity'; -import { FinancialServiceProviderConfigurationEnum } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { WrapperType } from '@121-service/src/wrapper.type'; - -@Unique('programFspConfigurationUnique', ['programId', 'fspId', 'name']) -@Entity('program_fsp_configuration') -export class ProgramFinancialServiceProviderConfigurationEntity extends CascadeDeleteEntity { - @ManyToOne( - (_type) => ProgramEntity, - (program) => program.programFspConfiguration, - ) - @JoinColumn({ name: 'programId' }) - @Column() - public programId: number; - - @ManyToOne( - (_type) => FinancialServiceProviderEntity, - (fsp) => fsp.configuration, - ) - @JoinColumn({ name: 'fspId' }) - public fsp: Relation; - @Column() - public fspId: number; - - @Column({ type: 'character varying' }) - public name: WrapperType; - - @Column({ - type: 'varchar', - transformer: { - to: (value: any) => { - if (Array.isArray(value) || isObject(value)) { - return JSON.stringify(value); - } - - return value; - }, - from: (value: any) => { - try { - const parsedValue = JSON.parse(value); - if (Array.isArray(parsedValue) || isObject(parsedValue)) { - return parsedValue; - } - - return value; - } catch (error) { - return value; - } - }, - }, - }) - public value: string | string[] | Record; -} diff --git a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.controller.ts b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.controller.ts index 6b832ca0b8..4c7034235e 100644 --- a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.controller.ts +++ b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.controller.ts @@ -1,128 +1,334 @@ import { - Body, Controller, + HttpCode, + ParseArrayPipe, + UseGuards, +} from '@nestjs/common'; +import { + Body, Delete, Get, HttpStatus, Param, ParseIntPipe, + Patch, Post, - Put, - UseGuards, } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { EXTERNAL_API } from '@121-service/src/config'; +import { FinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { AuthenticatedUser } from '@121-service/src/guards/authenticated-user.decorator'; import { AuthenticatedUserGuard } from '@121-service/src/guards/authenticated-user.guard'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { ProgramFinancialServiceProviderConfigurationPropertyResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto'; +import { ProgramFinancialServiceProviderConfigurationResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto'; import { ProgramFinancialServiceProviderConfigurationsService } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service'; -import { CreateProgramFspConfigurationDto } from '@121-service/src/programs/dto/create-program-fsp-configuration.dto'; -import { UpdateProgramFspConfigurationDto } from '@121-service/src/programs/dto/update-program-fsp-configuration.dto'; +import { WrapperType } from '@121-service/src/wrapper.type'; @UseGuards(AuthenticatedUserGuard) -@ApiTags('programs') +@ApiTags('programs/financial-service-provider-configurations') @Controller('programs') -export class ProgramFspConfigurationController { - private readonly programFspConfigurationService: ProgramFinancialServiceProviderConfigurationsService; +export class ProgramFinancialServiceProviderConfigurationsController { public constructor( - programFspConfigurationService: ProgramFinancialServiceProviderConfigurationsService, - ) { - this.programFspConfigurationService = programFspConfigurationService; - } + private readonly programFinancialServiceProviderConfigurationsService: ProgramFinancialServiceProviderConfigurationsService, + ) {} @AuthenticatedUser({ isAdmin: true }) @ApiOperation({ - summary: 'Get all programFspConfigurationEntity for a specific program', + summary: 'Get all Financial Service Provider Configurations for a Program.', }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Return programFspConfigurationEntity by program id.', + description: + 'Return Financial Service Provider Configurations by Program Id.', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Program does not exist', }) - @Get(':programId/fsp-configuration') - public async findByProgramId( + @Get(':programId/financial-service-provider-configurations') + public async getByProgramId( @Param('programId', ParseIntPipe) programId: number, - ): Promise { - return this.programFspConfigurationService.findByProgramId(programId); + ): Promise { + return this.programFinancialServiceProviderConfigurationsService.getByProgramId( + programId, + ); } @AuthenticatedUser({ isAdmin: true }) @ApiOperation({ - summary: 'Create ProgramFspConfigurationEntity for a program', + summary: + 'Create a Financial Service Provider Configuration for a Program. You can also add properties in this API call or you can add them later using /programs/{programId}/financial-service-provider-configurations/{name}/properties', }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) @ApiResponse({ - status: 201, + status: HttpStatus.CREATED, description: - 'The programFspConfigurationEntity has been successfully created.', + 'The Financial Service Provider Configuration has been successfully created.', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Program does not exist', }) - @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Bad request.' }) - @Post(':programId/fsp-configuration') + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Bad request. Body or params are malformed', + }) + @ApiResponse({ + status: HttpStatus.CONFLICT, + description: + 'Program Financial Service Provider Configuration with same name already exists', + }) + @Post(':programId/financial-service-provider-configurations') public async create( - @Body() programFspConfigurationData: CreateProgramFspConfigurationDto, + @Body() + programFspConfigurationData: CreateProgramFinancialServiceProviderConfigurationDto, @Param('programId', ParseIntPipe) programId: number, - ): Promise { - return await this.programFspConfigurationService.create( + ): Promise { + return await this.programFinancialServiceProviderConfigurationsService.create( programId, programFspConfigurationData, ); } @AuthenticatedUser({ isAdmin: true }) - @ApiOperation({ summary: 'Update ProgramFspConfigurationEntity' }) + @ApiOperation({ + summary: + 'Update a Financial Service Provider Configuration for a Program. Can only update the label and properties. Posting an array with properties or an empty array of properties will delete all existing properties and create new ones. If you want to add properties it is therfore recommended to use this endpoint: /programs/{programId}/financial-service-provider-configurations/{name}/properties. Example of how to format properties can also be found there', + }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) @ApiParam({ - name: 'programFspConfigurationId', + name: 'name', required: true, - type: 'integer', + type: 'string', }) @ApiResponse({ status: HttpStatus.OK, description: - 'The programFspConfigurationEntity has been successfully updated.', + 'The Financial Service Provider Configuration has been successfully updated.', }) - @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Bad request.' }) - @Put(':programId/fsp-configuration/:programFspConfigurationId') + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Program does not exist', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Bad request. Body or params are malformed', + }) + @Patch(':programId/financial-service-provider-configurations/:name') public async update( - @Body() programFspConfigurationData: UpdateProgramFspConfigurationDto, + @Body() + programFspConfigurationData: UpdateProgramFinancialServiceProviderConfigurationDto, @Param('programId', ParseIntPipe) programId: number, - @Param('programFspConfigurationId', ParseIntPipe) - programFspConfigurationId: number, - ): Promise { - return await this.programFspConfigurationService.update( + @Param('name') + name: string, + ): Promise { + return await this.programFinancialServiceProviderConfigurationsService.update( programId, - programFspConfigurationId, + name, programFspConfigurationData, ); } @AuthenticatedUser({ isAdmin: true }) - @ApiOperation({ summary: 'Update ProgramFspConfigurationEntity' }) + @ApiOperation({ + summary: + 'Delete a Financial Service Provider Configuration for a Program. Program Financial Service Provider Configurations cannot be deleted if they are associated with any transactions.', + }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @ApiParam({ - name: 'programFspConfigurationId', - required: true, - type: 'integer', + @HttpCode(204) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: + 'The Financial Service Provider Configuration has been successfully deleted.', }) @ApiResponse({ - status: HttpStatus.OK, + status: HttpStatus.NOT_FOUND, description: - 'The programFspConfigurationEntity has been successfully updated.', + 'Program does not exist or Financial Service Provider Configuration does not exist', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Bad request. Body or params are malformed', }) - @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Bad request.' }) - @Delete(':programId/fsp-configuration/:programFspConfigurationId') + @ApiResponse({ + status: HttpStatus.CONFLICT, + description: + 'Program Financial Service Provider Configuration is associated with transactions, so cannot be deleted', + }) + @Delete(':programId/financial-service-provider-configurations/:name') public async delete( @Param('programId', ParseIntPipe) programId: number, - @Param('programFspConfigurationId', ParseIntPipe) - programFspConfigurationId: number, + @Param('name') + name: string, ): Promise { - return await this.programFspConfigurationService.delete( + await this.programFinancialServiceProviderConfigurationsService.delete( programId, - programFspConfigurationId, + name, + ); + } + + @AuthenticatedUser({ isAdmin: true }) + @ApiOperation({ + summary: `Create properties for a Program Financial Service Provider Configuration. See ${EXTERNAL_API.baseApiUrl}/financial-service-providers for allowed properties per financial service provider.`, + }) + @ApiParam({ name: 'programId', required: true, type: 'integer' }) + @ApiParam({ + name: 'name', + required: true, + type: 'string', + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: + 'The Financial Service Provider Configuration properties have been successfully created.', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Bad request. Body or params are malformed', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: + 'Program does not exist or Financial Service Provider Configuration does not exist', + }) + @ApiBody({ + isArray: true, + type: CreateProgramFinancialServiceProviderConfigurationPropertyDto, + }) + @Post(':programId/financial-service-provider-configurations/:name/properties') + public async createProperties( + @Body( + new ParseArrayPipe({ + items: CreateProgramFinancialServiceProviderConfigurationPropertyDto, + }), + ) + properties: CreateProgramFinancialServiceProviderConfigurationPropertyDto[], + @Param('programId', ParseIntPipe) + programId: number, + @Param('name') + name: string, + ): Promise< + ProgramFinancialServiceProviderConfigurationPropertyResponseDto[] + > { + return await this.programFinancialServiceProviderConfigurationsService.createProperties( + { + programId, + name, + properties, + }, + ); + } + + @AuthenticatedUser({ isAdmin: true }) + @ApiOperation({ + summary: `Update a single property for a Program Financial Service Provider Configuration.. See ${EXTERNAL_API.baseApiUrl}/financial-service-providers for allowed properties per financial service provider.`, + }) + @ApiParam({ name: 'programId', required: true, type: 'integer' }) + @ApiParam({ + name: 'name', + required: true, + type: 'string', + }) + @ApiParam({ + name: 'propertyName', + required: true, + type: 'string', + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: + 'The Financial Service Provider Configuration property has been successfully updated.', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Bad request. Body or params are malformed', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: + 'Program does not exist or Financial Service Provider Configuration or propery does not exist', + }) + @Patch( + ':programId/financial-service-provider-configurations/:name/properties/:propertyName', + ) + public async updateProperty( + @Body() + property: UpdateProgramFinancialServiceProviderConfigurationPropertyDto, + @Param('programId', ParseIntPipe) + programId: number, + @Param('name') + name: string, + @Param('propertyName') + propertyName: WrapperType, + ): Promise { + return await this.programFinancialServiceProviderConfigurationsService.updateProperty( + { + programId, + name, + propertyName, + property, + }, + ); + } + + @AuthenticatedUser({ isAdmin: true }) + @ApiOperation({ + summary: `Delete a single Program Financial Service Provider Configuration property. See ${EXTERNAL_API.baseApiUrl}/financial-service-providers for required properties per financial service provider.`, + }) + @ApiParam({ name: 'programId', required: true, type: 'integer' }) + @ApiParam({ + name: 'name', + required: true, + type: 'string', + }) + @ApiParam({ + name: 'propertyName', + required: true, + type: 'string', + }) + @HttpCode(204) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: + 'The Financial Service Provider Configuration property is successfully deleted.', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Bad request. Body or params are malformed', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: + 'Program does not exist or Financial Service Provider Configuration or propery does not exist', + }) + @Delete( + ':programId/financial-service-provider-configurations/:name/properties/:propertyName', + ) + public async deleteProperty( + @Param('programId', ParseIntPipe) + programId: number, + @Param('name') + name: string, + @Param('propertyName') + propertyName: WrapperType, + ): Promise { + await this.programFinancialServiceProviderConfigurationsService.deleteProperty( + { + programId, + name, + propertyName, + }, ); } } diff --git a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.module.ts b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.module.ts index d8213e663d..62018d2a58 100644 --- a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.module.ts +++ b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.module.ts @@ -1,26 +1,26 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; -import { ProgramFspConfigurationController } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.controller'; +import { TransactionsModule } from '@121-service/src/payments/transactions/transactions.module'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; +import { ProgramFinancialServiceProviderConfigurationsController } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.controller'; import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramFinancialServiceProviderConfigurationsService } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service'; @Module({ - // This Module is a "leaf" module at the "outside" of the 121 Service Modules dependency tree. It should not depend on any other 121 Service Modules. imports: [ TypeOrmModule.forFeature([ ProgramFinancialServiceProviderConfigurationEntity, - // TODO: This is needed because the repository thats used to get FSPs is the typeorm variant, not our custom one. - FinancialServiceProviderEntity, + ProgramFinancialServiceProviderConfigurationPropertyEntity, ]), + TransactionsModule, ], providers: [ ProgramFinancialServiceProviderConfigurationsService, ProgramFinancialServiceProviderConfigurationRepository, ], - controllers: [ProgramFspConfigurationController], + controllers: [ProgramFinancialServiceProviderConfigurationsController], exports: [ ProgramFinancialServiceProviderConfigurationRepository, ProgramFinancialServiceProviderConfigurationsService, diff --git a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository.ts b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository.ts index 2a9c61f4aa..08367ec0e6 100644 --- a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository.ts +++ b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository.ts @@ -1,8 +1,12 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Equal, In, Repository } from 'typeorm'; +import { Equal, Repository } from 'typeorm'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { UsernamePasswordInterface } from '@121-service/src/program-financial-service-provider-configurations/interfaces/username-password.interface'; export class ProgramFinancialServiceProviderConfigurationRepository extends Repository { constructor( @@ -16,47 +20,106 @@ export class ProgramFinancialServiceProviderConfigurationRepository extends Repo ); } - public async findByProgramIdAndFinancialServiceProviderName( - programId: number, - financialServiceProviderName: FinancialServiceProviders, - ): Promise { + public async getByProgramIdAndFinancialServiceProviderName({ + programId, + financialServiceProviderName, + }: { + programId: number; + financialServiceProviderName: FinancialServiceProviders; + }): Promise { return await this.baseRepository.find({ where: { programId: Equal(programId), - fsp: { fsp: Equal(financialServiceProviderName) }, + financialServiceProviderName: Equal(financialServiceProviderName), }, + relations: { properties: true }, }); } - public async getValuesByNamesOrThrow({ - programId, - financialServiceProviderName, + public async getUsernamePasswordProperties( + programFinancialServiceProviderConfigurationId: number, + ): Promise { + const properties = await this.getProperties( + programFinancialServiceProviderConfigurationId, + ); + const propertyUsername = properties.find( + (c) => + c.name === FinancialServiceProviderConfigurationProperties.username, + ); + const propertyPassword = properties.find( + (c) => + c.name === FinancialServiceProviderConfigurationProperties.password, + ); + + const response: UsernamePasswordInterface = { + username: null, + password: null, + }; + + if (typeof propertyUsername?.value == 'string') { + response.username = propertyUsername.value; + } + if (typeof propertyPassword?.value == 'string') { + response.password = propertyPassword.value; + } + return response; + } + + // This methods specfically does not throw as it also used to check if the property exists + public async getPropertyValueByName({ + programFinancialServiceProviderConfigurationId, + name, + }: { + programFinancialServiceProviderConfigurationId: number; + name: FinancialServiceProviderConfigurationProperties; + }) { + const configuration = await this.baseRepository + .createQueryBuilder('configuration') + .leftJoinAndSelect('configuration.properties', 'properties') + .where('configuration.id = :id', { + id: programFinancialServiceProviderConfigurationId, + }) + .andWhere('properties.name = :name', { name }) + .getOne(); + return configuration?.properties.find((property) => property.name === name) + ?.value; + } + + public async getPropertiesByNamesOrThrow({ + programFinancialServiceProviderConfigurationId, names, }: { - programId: number; - financialServiceProviderName: FinancialServiceProviders; + programFinancialServiceProviderConfigurationId: number; names: string[]; }) { - const configurations = await this.baseRepository.find({ - where: { - programId: Equal(programId), - fsp: { fsp: Equal(financialServiceProviderName) }, - name: In(names), - }, - }); + const properties = await this.getProperties( + programFinancialServiceProviderConfigurationId, + ); + for (const name of names) { - if ( - !configurations.find((configuration) => configuration.name === name) - ) { + if (!properties.find((property) => property.name === name)) { throw new Error( - `Configuration with name ${name} not found for program ${programId} and FSP ${financialServiceProviderName}`, + `Configuration with name ${name} not found for ProgramFinancialServiceProviderConfigurationEntity with id: ${programFinancialServiceProviderConfigurationId}`, ); } } - return configurations.map((configuration) => ({ - name: configuration.name, - value: configuration.value, + return properties.map((property) => ({ + name: property.name, + value: property.value, })); } + + private async getProperties( + programFinancialServiceProviderConfigurationId: number, + ) { + const configuration = await this.baseRepository.findOne({ + where: { + id: Equal(programFinancialServiceProviderConfigurationId), + }, + relations: ['properties'], + }); + + return configuration ? configuration.properties : []; + } } diff --git a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.spec.ts b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.spec.ts index 5a36a30f77..17e1bf62c5 100644 --- a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.spec.ts +++ b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.spec.ts @@ -1,36 +1,480 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { Equal } from 'typeorm'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { TransactionScopedRepository } from '@121-service/src/payments/transactions/transaction.repository'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; import { ProgramFinancialServiceProviderConfigurationsService } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service'; +const programId = 1; +const mockProgramFspConfigPropertyEntity = + new ProgramFinancialServiceProviderConfigurationPropertyEntity(); +mockProgramFspConfigPropertyEntity.id = 1; +mockProgramFspConfigPropertyEntity.name = + FinancialServiceProviderConfigurationProperties.brandCode; +mockProgramFspConfigPropertyEntity.value = '123'; +mockProgramFspConfigPropertyEntity.programFinancialServiceProviderConfigurationId = 1; + +const configName = 'Config 1'; +const mockProgramFspConfigEntity = + new ProgramFinancialServiceProviderConfigurationEntity(); +mockProgramFspConfigEntity.id = 1; +mockProgramFspConfigEntity.name = configName; +mockProgramFspConfigEntity.programId = 1; +mockProgramFspConfigEntity.financialServiceProviderName = + FinancialServiceProviders.intersolveVisa; +mockProgramFspConfigEntity.label = { en: 'Test Label' }; +mockProgramFspConfigEntity.properties = [mockProgramFspConfigPropertyEntity]; + +const validPropertyDto: CreateProgramFinancialServiceProviderConfigurationPropertyDto = + { + name: FinancialServiceProviderConfigurationProperties.brandCode, + value: '123', + }; + +// Declaring mocks here so they are accesble through all files +let mockProgramFspConfigurationRepository; +let mockProgramFspConfigurationPropertyRepository; +let mockTransactionScopedRepository; + describe('ProgramFinancialServiceProviderConfigurationsService', () => { let service: ProgramFinancialServiceProviderConfigurationsService; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ + mockProgramFspConfigurationRepository = { + find: jest.fn().mockImplementation((criteria) => { + const programIdOfWhere = criteria.where.programId._value; + const nameWhere = criteria.where?.name?._value; + + if (programIdOfWhere === 1 && !nameWhere) { + return [mockProgramFspConfigEntity]; + } else if (programIdOfWhere === 1 && nameWhere === configName) { + return [mockProgramFspConfigEntity]; + } else { + return []; + } + }), + findOne: jest.fn().mockImplementation((criteria) => { + const programIdOfWhere = criteria.where.programId._value; + const nameWhere = criteria.where?.name?._value; + + if (programIdOfWhere === 1 && !nameWhere) { + return mockProgramFspConfigEntity; + } else if (programIdOfWhere === 1 && nameWhere === configName) { + return mockProgramFspConfigEntity; + } else { + return null; + } + }), + save: jest.fn().mockImplementation((entity) => { + return entity; // Return the entity it receives + }), + delete: jest.fn(), + createQueryBuilder: jest.fn().mockReturnValue({ + innerJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getCount: jest.fn().mockResolvedValue(0), + }), + }; + + mockProgramFspConfigurationPropertyRepository = { + delete: jest.fn(), + find: jest.fn().mockResolvedValue([]), + findOne: jest.fn().mockResolvedValue(mockProgramFspConfigPropertyEntity), + save: jest.fn().mockImplementation((entity) => { + return entity; // Return the entity it receives + }), + }; + + mockTransactionScopedRepository = { + count: jest.fn().mockImplementation((criteria) => { + const programFinancialServiceProviderConfigurationIdOfWhere = + criteria.where.programFinancialServiceProviderConfigurationId._value; + + if (programFinancialServiceProviderConfigurationIdOfWhere === 1) { + return 0; + } + return 1; + }), + }; + + const moduleRef = await Test.createTestingModule({ providers: [ ProgramFinancialServiceProviderConfigurationsService, { provide: getRepositoryToken( ProgramFinancialServiceProviderConfigurationEntity, ), - useValue: {}, // provide a mock implementation if needed + useValue: mockProgramFspConfigurationRepository, + }, + { + provide: getRepositoryToken( + ProgramFinancialServiceProviderConfigurationPropertyEntity, + ), + useValue: mockProgramFspConfigurationPropertyRepository, }, { - provide: getRepositoryToken(FinancialServiceProviderEntity), - useValue: {}, // provide a mock implementation if needed + provide: TransactionScopedRepository, + useValue: mockTransactionScopedRepository, }, ], }).compile(); - service = module.get( - ProgramFinancialServiceProviderConfigurationsService, - ); + service = + moduleRef.get( + ProgramFinancialServiceProviderConfigurationsService, + ); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findByProgramId', () => { + it('should return program configurations for a given program ID', async () => { + const result = await service.getByProgramId(programId); + + expect(mockProgramFspConfigurationRepository.find).toHaveBeenCalledWith({ + where: { programId: Equal(programId) }, + relations: ['properties'], + }); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + }); + }); + + describe('validateAndCreate', () => { + const createDto: CreateProgramFinancialServiceProviderConfigurationDto = { + name: 'Test Configuration', + financialServiceProviderName: FinancialServiceProviders.intersolveVisa, + label: { en: 'Test Label' }, + properties: [ + { + name: FinancialServiceProviderConfigurationProperties.brandCode, + value: '123', + }, + ], + }; + + it('should validate successfully if all checks pass', async () => { + await expect(service.create(programId, createDto)).resolves.not.toThrow(); + expect( + mockProgramFspConfigurationRepository.findOne, + ).toHaveBeenCalledWith({ + where: { + name: Equal(createDto.name), + programId: Equal(programId), + }, + }); + expect(mockProgramFspConfigurationRepository.save).toHaveBeenCalled(); + }); + + it('should throw an exception if english is not provided', async () => { + const noEnglishLabelDto = { + ...createDto, + label: { fr: 'Test Label' }, + }; + await expect( + service.create(programId, noEnglishLabelDto), + ).rejects.toThrow(HttpException); + await expect( + service.create(programId, noEnglishLabelDto), + ).rejects.toThrow( + new HttpException( + `Label must have an English translation`, + HttpStatus.BAD_REQUEST, + ), + ); + }); + + it('should throw an exception if a configuration with the same name exists', async () => { + // Mocking the check for existing configuration + const duplivateNameCreateDto = { + ...createDto, + name: configName, + }; + + await expect( + service.create(programId, duplivateNameCreateDto), + ).rejects.toThrow(HttpException); + await expect( + service.create(programId, duplivateNameCreateDto), + ).rejects.toThrow( + new HttpException( + `Program Financial Service Provider with name ${duplivateNameCreateDto.name} already exists`, + HttpStatus.CONFLICT, + ), + ); + }); + + it('should throw an exception if properties contain invalid name for fsp', async () => { + const invalidPropertiesDto = { + ...createDto, + properties: [ + { + name: FinancialServiceProviderConfigurationProperties.password, + value: '123', + }, + ], + }; + await expect( + service.create(programId, invalidPropertiesDto), + ).rejects.toThrow(); + await expect( + service.create(programId, invalidPropertiesDto), + ).rejects.toThrow(new RegExp(`only the following values are allowed`)); + }); + + it('should throw an exception if properties contain a duplicate name', async () => { + const invalidPropertiesDto = { + ...createDto, + properties: [ + { + name: FinancialServiceProviderConfigurationProperties.brandCode, + value: '123', + }, + { + name: FinancialServiceProviderConfigurationProperties.brandCode, + value: 'again another brandcode', + }, + ], + }; + await expect( + service.create(programId, invalidPropertiesDto), + ).rejects.toThrow(); + await expect( + service.create(programId, invalidPropertiesDto), + ).rejects.toThrow(new RegExp(`Duplicate property names are not allowed`)); + }); + }); + + describe('update', () => { + it('should successfully update the configuration', async () => { + const updateDto: UpdateProgramFinancialServiceProviderConfigurationDto = { + label: { en: 'Updated Label' }, + properties: [], + }; + + const result = await service.update(programId, configName, updateDto); + + expect( + mockProgramFspConfigurationRepository.findOne, + ).toHaveBeenCalledWith({ + where: { + name: Equal(configName), + programId: Equal(programId), + }, + }); + expect( + mockProgramFspConfigurationPropertyRepository.delete, + ).toHaveBeenCalledWith({ + programFinancialServiceProviderConfigurationId: Equal( + mockProgramFspConfigPropertyEntity.programFinancialServiceProviderConfigurationId, + ), + }); + expect(mockProgramFspConfigurationRepository.save).toHaveBeenCalled(); + expect(result.label).toEqual(updateDto.label); + }); + + it('should throw an exception if the configuration does not exist', async () => { + const updateDto: UpdateProgramFinancialServiceProviderConfigurationDto = { + label: { en: 'Updated Label' }, + properties: [], + }; + const nonExistingConfigName = 'Non existing config'; + + await expect( + service.update(programId, nonExistingConfigName, updateDto), + ).rejects.toThrow(HttpException); + expect( + mockProgramFspConfigurationPropertyRepository.delete, + ).not.toHaveBeenCalled(); + await expect( + service.update(programId, nonExistingConfigName, updateDto), + ).rejects.toThrow(new HttpException('Not found', HttpStatus.NOT_FOUND)); + }); + }); + + describe('delete', () => { + it('should successfully delete the configuration', async () => { + await service.delete(programId, configName); + + expect( + mockProgramFspConfigurationRepository.findOne, + ).toHaveBeenCalledWith({ + where: { + name: Equal(configName), + programId: Equal(programId), + }, + relations: ['properties'], + }); + expect(mockProgramFspConfigurationRepository.delete).toHaveBeenCalledWith( + { + id: mockProgramFspConfigEntity.id, + }, + ); + }); + + it('should throw an exception if the configuration does not exist', async () => { + // Mocking findOne to return null (configuration not found) + const nonExistingConfigName = 'Non existing config'; + + await expect( + service.delete(programId, nonExistingConfigName), + ).rejects.toThrow(HttpException); + await expect( + service.delete(programId, nonExistingConfigName), + ).rejects.toThrow(new HttpException('Not found', HttpStatus.NOT_FOUND)); + }); + + it('should throw an exception if the configuration has associated transactions', async () => { + mockTransactionScopedRepository.count.mockResolvedValue(1); // Simulating existing transactions + + await expect(service.delete(programId, configName)).rejects.toThrow( + HttpException, + ); + await expect(service.delete(programId, configName)).rejects.toThrow( + new HttpException( + 'Cannot delete Program FSP Configuration because it has related Transactions', + HttpStatus.CONFLICT, + ), + ); + }); + }); + + describe('validateAndCreateProperties', () => { + it('should succesfully map and create properties', async () => { + const result = await service.createProperties({ + programId, + name: configName, + properties: [validPropertyDto], + }); + expect(mockProgramFspConfigurationRepository.findOne).toHaveBeenCalled(); + expect( + mockProgramFspConfigurationPropertyRepository.save, + ).toHaveBeenCalled(); + expect(result[0].name).toEqual(validPropertyDto.name); + }); + + it('should throw an error if configuration does not exist', async () => { + const nonExistingConfigName = 'Non existing config'; + await expect( + service.createProperties({ + programId, + name: nonExistingConfigName, + properties: [validPropertyDto], + }), + ).rejects.toThrow( + new HttpException( + `Program financial service provider configuration with name ${ + nonExistingConfigName + } not found`, + HttpStatus.NOT_FOUND, + ), + ); + expect( + mockProgramFspConfigurationPropertyRepository.save, + ).not.toHaveBeenCalled(); + }); + }); + + describe('updateProperty', () => { + const propertyName = + FinancialServiceProviderConfigurationProperties.brandCode; + const updatedValue = 'UpdatedValue'; + const updatedPropertyDto = { value: updatedValue }; + + it('should successfully update and map the property', async () => { + await service.updateProperty({ + programId, + name: configName, + propertyName, + property: updatedPropertyDto, + }); + + expect( + mockProgramFspConfigurationRepository.findOne, + ).toHaveBeenCalledWith({ + where: { programId: Equal(programId), name: Equal(configName) }, + }); + expect( + mockProgramFspConfigurationPropertyRepository.save, + ).toHaveBeenCalledWith( + expect.objectContaining({ + id: 1, + name: propertyName, + value: updatedValue, + }), + ); + }); + + it('should throw an error if the property does not exist', async () => { + mockProgramFspConfigurationPropertyRepository.findOne.mockResolvedValue( + null, + ); + + await expect( + service.updateProperty({ + programId, + name: configName, + propertyName, + property: updatedPropertyDto, + }), + ).rejects.toThrow( + new HttpException( + `Program financial service provider configuration property with name ${propertyName} not found`, + HttpStatus.NOT_FOUND, + ), + ); + + expect( + mockProgramFspConfigurationPropertyRepository.save, + ).not.toHaveBeenCalled(); + }); + }); + + describe('deleteProperty', () => { + const propertyName = + FinancialServiceProviderConfigurationProperties.brandCode; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should successfully delete the property', async () => { + mockProgramFspConfigurationRepository.findOne.mockResolvedValue({ + id: 10, + }); + mockProgramFspConfigurationPropertyRepository.findOne.mockResolvedValue({ + id: 100, + name: propertyName, + }); + + await service.deleteProperty({ + programId, + name: configName, + propertyName, + }); + + expect( + mockProgramFspConfigurationRepository.findOne, + ).toHaveBeenCalledWith({ + where: { programId: Equal(programId), name: Equal(configName) }, + }); + expect( + mockProgramFspConfigurationPropertyRepository.delete, + ).toHaveBeenCalledWith({ + id: 100, + }); + }); + }); }); diff --git a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.ts b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.ts index 52549552b9..7b36caed4f 100644 --- a/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.ts +++ b/services/121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service.ts @@ -1,124 +1,428 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Equal, Repository } from 'typeorm'; +import { Equal, In, Repository } from 'typeorm'; -import { FinancialServiceProviderConfigurationMapping } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; -import { CreateProgramFspConfigurationDto } from '@121-service/src/programs/dto/create-program-fsp-configuration.dto'; -import { UpdateProgramFspConfigurationDto } from '@121-service/src/programs/dto/update-program-fsp-configuration.dto'; +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { getFinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; +import { TransactionScopedRepository } from '@121-service/src/payments/transactions/transaction.repository'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { ProgramFinancialServiceProviderConfigurationPropertyResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto'; +import { ProgramFinancialServiceProviderConfigurationResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; +import { ProgramFinancialServiceProviderConfigurationMapper } from '@121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper'; @Injectable() export class ProgramFinancialServiceProviderConfigurationsService { @InjectRepository(ProgramFinancialServiceProviderConfigurationEntity) private readonly programFspConfigurationRepository: Repository; - @InjectRepository(FinancialServiceProviderEntity) - public financialServiceProviderRepository: Repository; + @InjectRepository(ProgramFinancialServiceProviderConfigurationPropertyEntity) + private readonly programFspConfigurationPropertyRepository: Repository; - public async findByProgramId( + constructor( + private readonly transactionScopedRepository: TransactionScopedRepository, + ) {} + + public async getByProgramId( programId: number, - ): Promise { + ): Promise { const programFspConfigurations = await this.programFspConfigurationRepository.find({ where: { programId: Equal(programId) }, + relations: ['properties'], }); - return programFspConfigurations; + return ProgramFinancialServiceProviderConfigurationMapper.mapEntitiesToDtos( + programFspConfigurations, + ); } public async create( programId: number, - programFspConfigurationDto: CreateProgramFspConfigurationDto, - ): Promise { - const fsp = await this.financialServiceProviderRepository.findOne({ - where: { id: Equal(programFspConfigurationDto.fspId) }, - }); - if (!fsp) { - throw new HttpException( - `No fsp found with id ${programFspConfigurationDto.fspId}`, - HttpStatus.NOT_FOUND, - ); - } + programFspConfigurationDto: CreateProgramFinancialServiceProviderConfigurationDto, + ): Promise { + await this.validate(programId, programFspConfigurationDto); + return this.createEntity(programId, programFspConfigurationDto); + } - if (FinancialServiceProviderConfigurationMapping[fsp.fsp] === undefined) { + private async validate( + programId: number, + programFspConfigurationDto: CreateProgramFinancialServiceProviderConfigurationDto, + ): Promise { + this.validateLabelHasEnglishTranslation(programFspConfigurationDto.label); + + const existingConfig = await this.programFspConfigurationRepository.findOne( + { + where: { + name: Equal(programFspConfigurationDto.name), + programId: Equal(programId), + }, + }, + ); + + if (existingConfig) { throw new HttpException( - `Fsp ${fsp.fsp} has no fsp config`, - HttpStatus.NOT_FOUND, + `Program Financial Service Provider with name ${programFspConfigurationDto.name} already exists`, + HttpStatus.CONFLICT, ); - } else { - const allowedConfigForFsp = - FinancialServiceProviderConfigurationMapping[fsp.fsp]; - if (!allowedConfigForFsp.includes(programFspConfigurationDto.name)) { - throw new HttpException( - `For fsp ${fsp.fsp} only the following values are allowed ${allowedConfigForFsp}. You tried to add ${programFspConfigurationDto.name}`, - HttpStatus.NOT_FOUND, - ); - } } - const programFspConfiguration = - new ProgramFinancialServiceProviderConfigurationEntity(); - programFspConfiguration.programId = programId; - programFspConfiguration.fspId = programFspConfigurationDto.fspId; - programFspConfiguration.name = programFspConfigurationDto.name; - programFspConfiguration.value = programFspConfigurationDto.value; + if (programFspConfigurationDto.properties) { + await this.validateAllowedPropertyNames({ + propertyNames: programFspConfigurationDto.properties.map((p) => p.name), + financialServiceProviderName: + programFspConfigurationDto.financialServiceProviderName, + }); + } + } - try { - const resProgramFspConfiguration = - await this.programFspConfigurationRepository.save( - programFspConfiguration, - ); - return resProgramFspConfiguration.id; - } catch (error) { - if (error['code'] === '23505') { - throw new HttpException( - `Conflict on unique constraint: ${error['constraint']}`, - HttpStatus.CONFLICT, - ); - } else { - throw new HttpException( - 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + private async createEntity( + programId: number, + programFspConfigurationDto: CreateProgramFinancialServiceProviderConfigurationDto, + ): Promise { + const newConfigEntity = + ProgramFinancialServiceProviderConfigurationMapper.mapDtoToEntity( + programFspConfigurationDto, + programId, + ); + const savedEntity = + await this.programFspConfigurationRepository.save(newConfigEntity); + if (programFspConfigurationDto.properties) { + savedEntity.properties = await this.createPropertyEntities( + savedEntity.id, + programFspConfigurationDto.properties, + ); } + return ProgramFinancialServiceProviderConfigurationMapper.mapEntityToDto( + savedEntity, + ); } public async update( programId: number, - programFspConfigurationId: number, - updateProgramFspConfigurationDto: UpdateProgramFspConfigurationDto, - ): Promise { - const result = await this.programFspConfigurationRepository.findOne({ + name: string, + updateProgramFspConfigurationDto: UpdateProgramFinancialServiceProviderConfigurationDto, + ): Promise { + const config = await this.programFspConfigurationRepository.findOne({ where: { - id: Equal(programFspConfigurationId), + name: Equal(name), programId: Equal(programId), }, }); - if (!result) { + + if (!config) { throw new HttpException('Not found', HttpStatus.NOT_FOUND); } - result.name = updateProgramFspConfigurationDto.name; - result.value = updateProgramFspConfigurationDto.value; - await this.programFspConfigurationRepository.save(result); - return programFspConfigurationId; + + // Only update the label an properties in this API call. I cannot imagine a use case where we would want to update the name or fsp name + // Updating the FSP name would also be more complex as you would need to check if the new properties are valid for the new FSP + this.validateLabelHasEnglishTranslation( + updateProgramFspConfigurationDto.label, + ); + config.label = updateProgramFspConfigurationDto.label; + + if (updateProgramFspConfigurationDto.properties) { + await this.validateAllowedPropertyNames({ + propertyNames: updateProgramFspConfigurationDto.properties.map( + (p) => p.name, + ), + financialServiceProviderName: config.financialServiceProviderName, + }); + } + + const savedEntity = + await this.programFspConfigurationRepository.save(config); + + if (updateProgramFspConfigurationDto.properties) { + savedEntity.properties = await this.overwriteProperties( + savedEntity.id, + updateProgramFspConfigurationDto.properties, + ); + } + + return ProgramFinancialServiceProviderConfigurationMapper.mapEntityToDto( + savedEntity, + ); } - public async delete( - programId: number, - programFspConfigurationId: number, - ): Promise { - const result = await this.programFspConfigurationRepository.findOne({ + public async delete(programId: number, name: string): Promise { + const config = await this.programFspConfigurationRepository.findOne({ where: { - id: Equal(programFspConfigurationId), + name: Equal(name), programId: Equal(programId), }, + relations: ['properties'], }); - if (!result) { + + if (!config) { throw new HttpException('Not found', HttpStatus.NOT_FOUND); } + + const transactionCount = await this.transactionScopedRepository.count({ + where: { + programFinancialServiceProviderConfigurationId: Equal(config.id), + }, + }); + if (transactionCount > 0) { + throw new HttpException( + 'Cannot delete Program FSP Configuration because it has related Transactions', + HttpStatus.CONFLICT, + ); + } + + // Should cascade delete the properties await this.programFspConfigurationRepository.delete({ - id: programFspConfigurationId, + id: config.id, + }); + } + + public async createProperties({ + programId, + name, + properties: inputProperties, + }: { + programId: number; + name: string; + properties: CreateProgramFinancialServiceProviderConfigurationPropertyDto[]; + }): Promise< + ProgramFinancialServiceProviderConfigurationPropertyResponseDto[] + > { + const config = await this.getProgramFspConfigurationOrThrow( + programId, + name, + ); + await this.validateAllowedPropertyNames({ + propertyNames: inputProperties.map((p) => p.name), + financialServiceProviderName: config.financialServiceProviderName, }); + await this.validateNoDuplicateExistingProperties({ + propertyNames: inputProperties.map((p) => p.name), + configIdToCheckForDuplicates: config.id, + }); + const properties = await this.createPropertyEntities( + config.id, + inputProperties, + ); + return ProgramFinancialServiceProviderConfigurationMapper.mapPropertyEntitiesToDtos( + properties, + ); + } + + private async validateAllowedPropertyNames({ + propertyNames, + financialServiceProviderName, + }: { + propertyNames: string[]; + financialServiceProviderName: FinancialServiceProviders; + }): Promise { + const configPropertiesOfFsp = + getFinancialServiceProviderConfigurationProperties( + financialServiceProviderName, + ); + + const errors: string[] = []; + for (const propertyName of propertyNames) { + if ( + configPropertiesOfFsp && + !configPropertiesOfFsp.includes(propertyName) + ) { + errors.push( + `For fsp ${financialServiceProviderName}, only the following values are allowed: ${configPropertiesOfFsp.join(' ')}. You tried to add ${propertyName}.`, + ); + } + } + + // Check if there are duplicate property names in this array + if (propertyNames.length !== new Set(propertyNames).size) { + const duplicateNames = propertyNames.filter( + (name, index) => propertyNames.indexOf(name) !== index, + ); + errors.push( + `Duplicate property names are not allowed. Found the following duplicates: ${duplicateNames.join(', ')}`, + ); + } + + if (errors.length > 0) { + const errorsString = errors.join(' '); + throw new HttpException(errorsString, HttpStatus.BAD_REQUEST); + } + } + + private async validateNoDuplicateExistingProperties({ + propertyNames, + configIdToCheckForDuplicates, + }: { + propertyNames: string[]; + configIdToCheckForDuplicates: number; + }): Promise { + // Check if properties are already present in the database + const errors: string[] = []; + if (configIdToCheckForDuplicates) { + const exisingProperties = + await this.programFspConfigurationPropertyRepository.find({ + where: { + programFinancialServiceProviderConfigurationId: Equal( + configIdToCheckForDuplicates, + ), + name: In(propertyNames), + }, + }); + for (const property of exisingProperties) { + errors.push( + `Property with name ${property.name} already exists for this configuration`, + ); + } + } + if (errors.length > 0) { + const errorsString = errors.join(' '); + throw new HttpException(errorsString, HttpStatus.BAD_REQUEST); + } + } + + public async updateProperty({ + programId, + name: name, + propertyName, + property, + }: { + programId: number; + name: string; + propertyName: FinancialServiceProviderConfigurationProperties; + property: UpdateProgramFinancialServiceProviderConfigurationPropertyDto; + }): Promise { + const config = await this.getProgramFspConfigurationOrThrow( + programId, + name, + ); + const existingProperty = + await this.getProgramFspConfigurationPropertyOrThrow( + config.id, + propertyName, + ); + + existingProperty.value = + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyDtoValueToEntityValue( + property.value, + existingProperty.name, + ); + + const savedProperty = + await this.programFspConfigurationPropertyRepository.save( + existingProperty, + ); + + return ProgramFinancialServiceProviderConfigurationMapper.mapPropertyEntityToDto( + savedProperty, + ); + } + + public async deleteProperty({ + programId, + name: name, + propertyName, + }: { + programId: number; + name: string; + propertyName: FinancialServiceProviderConfigurationProperties; + }): Promise { + const config = await this.getProgramFspConfigurationOrThrow( + programId, + name, + ); + const existingProperty = + await this.getProgramFspConfigurationPropertyOrThrow( + config.id, + propertyName, + ); + await this.programFspConfigurationPropertyRepository.delete({ + id: existingProperty.id, + }); + } + + private async createPropertyEntities( + programFspConfigurationId: number, + inputProperties: CreateProgramFinancialServiceProviderConfigurationPropertyDto[], + ): Promise { + const propertiesToSave = + ProgramFinancialServiceProviderConfigurationMapper.mapPropertyDtosToEntities( + inputProperties, + programFspConfigurationId, + ); + return this.programFspConfigurationPropertyRepository.save( + propertiesToSave, + ); + } + + private async overwriteProperties( + programFspConfigurationId: number, + properties: CreateProgramFinancialServiceProviderConfigurationPropertyDto[], + ): Promise { + // delete all properties + await this.programFspConfigurationPropertyRepository.delete({ + programFinancialServiceProviderConfigurationId: Equal( + programFspConfigurationId, + ), + }); + // create new properties + return await this.createPropertyEntities( + programFspConfigurationId, + properties, + ); + } + + private validateLabelHasEnglishTranslation(label: any): void { + if (!label.en) { + throw new HttpException( + `Label must have an English translation`, + HttpStatus.BAD_REQUEST, + ); + } + } + + private async getProgramFspConfigurationOrThrow( + programId: number, + name: string, + ): Promise { + const config = await this.programFspConfigurationRepository.findOne({ + where: { + name: Equal(name), + programId: Equal(programId), + }, + }); + if (!config) { + throw new HttpException( + `Program financial service provider configuration with name ${name} not found`, + HttpStatus.NOT_FOUND, + ); + } + return config; + } + + private async getProgramFspConfigurationPropertyOrThrow( + programFspConfigurationId: number, + propertyName: FinancialServiceProviderConfigurationProperties, + ): Promise { + const property = + await this.programFspConfigurationPropertyRepository.findOne({ + where: { + programFinancialServiceProviderConfigurationId: Equal( + programFspConfigurationId, + ), + name: Equal(propertyName), + }, + }); + if (!property) { + throw new HttpException( + `Program financial service provider configuration property with name ${propertyName} not found`, + HttpStatus.NOT_FOUND, + ); + } + return property; } } diff --git a/services/121-service/src/programs/dto/create-program-custom-attribute.dto.ts b/services/121-service/src/programs/dto/create-program-custom-attribute.dto.ts deleted file mode 100644 index c66435e17b..0000000000 --- a/services/121-service/src/programs/dto/create-program-custom-attribute.dto.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - IsBoolean, - IsEnum, - IsNotEmpty, - IsOptional, - IsString, -} from 'class-validator'; - -import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; -import { WrapperType } from '@121-service/src/wrapper.type'; - -export enum CustomAttributeType { - text = 'text', - boolean = 'boolean', - tel = 'tel', -} - -export class UpdateProgramCustomAttributeDto { - @ApiProperty({ example: 'text' }) - @IsNotEmpty() - @IsString() - @IsEnum(CustomAttributeType) - public readonly type: WrapperType; - - @ApiProperty({ - example: { - en: 'District', - fr: 'Département', - }, - }) - @IsNotEmpty() - public label: LocalizedString; - - @ApiProperty({ - example: true, - }) - @IsNotEmpty() - public showInPeopleAffectedTable: boolean; - - @ApiProperty({ example: true, required: false }) - @IsOptional() - @IsBoolean() - public duplicateCheck?: boolean; -} - -export class CreateProgramCustomAttributeDto extends UpdateProgramCustomAttributeDto { - @ApiProperty({ example: 'district' }) - @IsNotEmpty() - @IsString() - public readonly name: string; -} diff --git a/services/121-service/src/programs/dto/create-program-fsp-configuration.dto.ts b/services/121-service/src/programs/dto/create-program-fsp-configuration.dto.ts deleted file mode 100644 index d74cb33762..0000000000 --- a/services/121-service/src/programs/dto/create-program-fsp-configuration.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsNumber } from 'class-validator'; - -import { FinancialServiceProviderConfigurationEnum } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { WrapperType } from '@121-service/src/wrapper.type'; - -export class CreateProgramFspConfigurationDto { - @ApiProperty({ example: 1 }) - @IsNumber() - @IsNotEmpty() - fspId: number; - - @ApiProperty({ example: FinancialServiceProviderConfigurationEnum.username }) - @IsNotEmpty() - @IsEnum(FinancialServiceProviderConfigurationEnum) - name: WrapperType; - - @ApiProperty({ - example: 'test_account', - description: - 'Should be string (for e.g. name=username) or array of strings (for e.g. name=columnsToExport) or JSON object (for e.g name=displayName)', - }) - @IsNotEmpty() - value: string | string[] | Record; -} diff --git a/services/121-service/src/programs/dto/create-program.dto.ts b/services/121-service/src/programs/dto/create-program.dto.ts index 96687f6fef..dee81ecd3d 100644 --- a/services/121-service/src/programs/dto/create-program.dto.ts +++ b/services/121-service/src/programs/dto/create-program.dto.ts @@ -14,20 +14,86 @@ import { ValidateNested, } from 'class-validator'; -import { - FinancialServiceProviderConfigurationEnum, - FinancialServiceProviders, -} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; -import { - CreateProgramCustomAttributeDto, - CustomAttributeType, -} from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; -import { CreateProgramQuestionDto } from '@121-service/src/programs/dto/program-question.dto'; +import { ProgramRegistrationAttributeDto } from '@121-service/src/programs/dto/program-registration-attribute.dto'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; import { WrapperType } from '@121-service/src/wrapper.type'; +// This declared at the top of the file because it is used in the CreateProgramDto and else it is not defined yet +// It's not defined inline because typing works more convient here +const exampleAttributes: ProgramRegistrationAttributeDto[] = [ + { + name: 'nameFirst', + type: RegistrationAttributeTypes.text, + options: undefined, + export: [ExportType.allPeopleAffected, ExportType.included], + scoring: {}, + showInPeopleAffectedTable: true, + editableInPortal: false, + label: { + en: 'First Name', + }, + }, + { + name: 'nameLast', + type: RegistrationAttributeTypes.text, + options: undefined, + export: [ExportType.allPeopleAffected, ExportType.included], + scoring: {}, + showInPeopleAffectedTable: true, + editableInPortal: false, + label: { + en: 'Last Name', + }, + }, + { + name: 'nr_of_children', + label: { + en: 'How many children do you have?', + }, + type: RegistrationAttributeTypes.numeric, + options: undefined, + scoring: { + '0-18': 999, + '19-65': 0, + '65>': 6, + }, + showInPeopleAffectedTable: false, + editableInPortal: false, + isRequired: true, + }, + { + name: 'roof_type', + label: { + en: 'What type is your roof?', + }, + type: RegistrationAttributeTypes.dropdown, + options: [ + { + option: 'steel', + label: { + en: 'Steel', + }, + }, + { + option: 'tiles', + label: { + en: 'Tiles', + }, + }, + ], + scoring: { + '0': 3, + '1': 6, + }, + showInPeopleAffectedTable: false, + editableInPortal: true, + }, +]; + export class ProgramFinancialServiceProviderDto { @ApiProperty() @IsEnum(FinancialServiceProviders) @@ -37,7 +103,7 @@ export class ProgramFinancialServiceProviderDto { @IsArray() @IsOptional() configuration?: { - name: WrapperType; + name: WrapperType; value: string | string[] | Record; }[]; } @@ -103,24 +169,6 @@ export class CreateProgramDto { @IsString() public readonly paymentAmountMultiplierFormula?: string; - @ApiProperty({ - example: [ - { - fsp: FinancialServiceProviders.intersolveVoucherWhatsapp, - }, - { - fsp: FinancialServiceProviders.intersolveVoucherPaper, - }, - ], - description: - 'Use the GET /financial-service-providers endpoint to find valid fspNames.', - }) - @IsArray() - @ValidateNested() - @IsDefined() - @Type(() => ProgramFinancialServiceProviderDto) - public readonly financialServiceProviders: ProgramFinancialServiceProviderDto[]; - @ApiProperty({ example: 250 }) @IsNumber() public readonly targetNrRegistrations: number; @@ -130,120 +178,13 @@ export class CreateProgramDto { public readonly tryWhatsAppFirst: boolean; @ApiProperty({ - example: [ - { - name: 'nameParterOrganization', - type: CustomAttributeType.text, - label: { en: 'Name partner organization' }, - export: [ - ExportType.allPeopleAffected, - ExportType.included, - ExportType.payment, - ], - showInPeopleAffectedTable: true, - }, - { - name: 'exampleBoolean', - type: CustomAttributeType.boolean, - label: { en: 'Example boolean' }, - export: [ - ExportType.allPeopleAffected, - ExportType.included, - ExportType.payment, - ], - showInPeopleAffectedTable: true, - }, - ], + example: exampleAttributes, }) @IsArray() @ValidateNested() @IsDefined() - @Type(() => CreateProgramCustomAttributeDto) - public readonly programCustomAttributes: CreateProgramCustomAttributeDto[]; - - @ApiProperty({ - example: [ - { - name: 'nameFirst', - answerType: 'text', - questionType: 'standard', - options: null, - persistence: true, - export: [ExportType.allPeopleAffected, ExportType.included], - scoring: {}, - showInPeopleAffectedTable: true, - editableInPortal: false, - label: { - en: 'First Name', - }, - }, - { - name: 'nameLast', - answerType: 'text', - questionType: 'standard', - options: null, - persistence: true, - export: [ExportType.allPeopleAffected, ExportType.included], - scoring: {}, - showInPeopleAffectedTable: true, - editableInPortal: false, - label: { - en: 'Last Name', - }, - }, - { - name: 'nr_of_children', - label: { - en: 'How many children do you have?', - }, - answerType: 'numeric', - questionType: 'standard', - options: null, - scoring: { - '0-18': 999, - '19-65': 0, - '65>': 6, - }, - showInPeopleAffectedTable: false, - editableInPortal: false, - }, - { - name: 'roof_type', - label: { - en: 'What type is your roof?', - }, - answerType: 'dropdown', - questionType: 'standard', - options: [ - { - id: 0, - option: 'steel', - label: { - en: 'Steel', - }, - }, - { - id: 1, - option: 'tiles', - label: { - en: 'Tiles', - }, - }, - ], - scoring: { - '0': 3, - '1': 6, - }, - showInPeopleAffectedTable: false, - editableInPortal: true, - }, - ], - }) - @IsArray() - @ValidateNested() - @IsDefined() - @Type(() => CreateProgramQuestionDto) - public readonly programQuestions: CreateProgramQuestionDto[]; + @Type(() => ProgramRegistrationAttributeDto) + public readonly programRegistrationAttributes: ProgramRegistrationAttributeDto[]; @ApiProperty({ example: { en: 'about program' } }) @IsNotEmpty() @@ -251,7 +192,8 @@ export class CreateProgramDto { @ApiProperty({ example: ['nameFirst', 'nameLast'], - description: 'Should be array of name-related program-questions.', + description: + 'Should be array of name-related program-registration-attributes.', }) @IsArray() public readonly fullnameNamingConvention: string[]; diff --git a/services/121-service/src/programs/dto/found-program.dto.ts b/services/121-service/src/programs/dto/found-program.dto.ts index 767bb38a69..9f500e709d 100644 --- a/services/121-service/src/programs/dto/found-program.dto.ts +++ b/services/121-service/src/programs/dto/found-program.dto.ts @@ -6,6 +6,4 @@ export interface FoundProgramDto ProgramEntity, 'monitoringDashboardUrl' | 'programFspConfiguration' >, - Partial< - Pick - > {} + Partial> {} diff --git a/services/121-service/src/programs/dto/program-question.dto.ts b/services/121-service/src/programs/dto/program-registration-attribute.dto.ts similarity index 62% rename from services/121-service/src/programs/dto/program-question.dto.ts rename to services/121-service/src/programs/dto/program-registration-attribute.dto.ts index e7b2abfbe5..439a46d87f 100644 --- a/services/121-service/src/programs/dto/program-question.dto.ts +++ b/services/121-service/src/programs/dto/program-registration-attribute.dto.ts @@ -13,41 +13,37 @@ import { import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; import { CreateOptionsDto } from '@121-service/src/programs/dto/create-options.dto'; -import { AnswerTypes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { QuestionOption } from '@121-service/src/shared/enum/question.enums'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; import { WrapperType } from '@121-service/src/wrapper.type'; -class BaseProgramQuestionDto { - @ApiProperty({}) - @IsNotEmpty() - @IsString() - public readonly name: string; - +class BaseProgramRegistrationAttributeDto { @ApiProperty({ required: false }) - @ValidateIf((o) => o.answerType === AnswerTypes.dropdown) + @ValidateIf((o) => o.type === RegistrationAttributeTypes.dropdown) @ValidateNested() @IsOptional() @Type(() => CreateOptionsDto) public readonly options?: QuestionOption[]; + @ApiProperty() @IsOptional() public readonly scoring?: Record; - @ApiProperty({ required: false }) - @IsOptional() - @IsBoolean() - public readonly persistence?: boolean; + @ApiProperty({ required: false }) @IsOptional() public pattern?: string; + @ApiProperty({ example: false, }) @IsOptional() - public showInPeopleAffectedTable: boolean; + public showInPeopleAffectedTable?: boolean; + @ApiProperty({ example: true }) @IsOptional() public readonly editableInPortal?: boolean; + @ApiProperty({ example: [ExportType.allPeopleAffected, ExportType.included], required: false, @@ -55,6 +51,7 @@ class BaseProgramQuestionDto { @IsOptional() @IsEnum(ExportType, { each: true }) // Use @IsEnum decorator to validate each element public readonly export?: WrapperType; + @ApiProperty({ example: { en: '+31 6 00 00 00 00', @@ -63,6 +60,7 @@ class BaseProgramQuestionDto { }) @IsOptional() public placeholder?: LocalizedString; + @ApiProperty({ example: false, required: false, @@ -71,7 +69,12 @@ class BaseProgramQuestionDto { public duplicateCheck?: boolean; } -export class CreateProgramQuestionDto extends BaseProgramQuestionDto { +export class ProgramRegistrationAttributeDto extends BaseProgramRegistrationAttributeDto { + @ApiProperty({}) + @IsNotEmpty() + @IsString() + public readonly name: string; + @ApiProperty({ example: { en: 'Please enter your last name:', @@ -81,24 +84,28 @@ export class CreateProgramQuestionDto extends BaseProgramQuestionDto { public readonly label: LocalizedString; @ApiProperty({ - example: AnswerTypes.text, + example: RegistrationAttributeTypes.text, }) @IsString() @IsIn([ - AnswerTypes.numeric, - AnswerTypes.dropdown, - AnswerTypes.tel, - AnswerTypes.text, - AnswerTypes.date, + RegistrationAttributeTypes.numeric, + RegistrationAttributeTypes.dropdown, + RegistrationAttributeTypes.tel, + RegistrationAttributeTypes.text, + RegistrationAttributeTypes.date, ]) - public readonly answerType: string; + public readonly type: WrapperType; - @ApiProperty({ example: 'standard' }) - @IsIn(['standard', 'custom']) - public readonly questionType: string; + @ApiProperty({ + example: false, + required: false, + }) + @IsBoolean() + @IsOptional() + public readonly isRequired?: boolean; } -export class UpdateProgramQuestionDto extends BaseProgramQuestionDto { +export class UpdateProgramRegistrationAttributeDto extends BaseProgramRegistrationAttributeDto { @ApiProperty({ example: { en: 'Please enter your last name:', @@ -107,25 +114,28 @@ export class UpdateProgramQuestionDto extends BaseProgramQuestionDto { required: false, }) @IsOptional() - public readonly label?: LocalizedString; + public readonly label?: WrapperType; @ApiProperty({ - example: AnswerTypes.numeric, + example: RegistrationAttributeTypes.numeric, required: false, }) @IsOptional() @IsString() @IsIn([ - AnswerTypes.numeric, - AnswerTypes.dropdown, - AnswerTypes.tel, - AnswerTypes.text, - AnswerTypes.date, + RegistrationAttributeTypes.numeric, + RegistrationAttributeTypes.dropdown, + RegistrationAttributeTypes.tel, + RegistrationAttributeTypes.text, + RegistrationAttributeTypes.date, ]) - public readonly answerType?: WrapperType; + public readonly type?: WrapperType; - @ApiProperty({ example: 'standard', required: false }) - @IsIn(['standard', 'custom']) + @ApiProperty({ + example: false, + required: false, + }) @IsOptional() - public readonly questionType?: string; + @IsBoolean() + public readonly isRequired?: boolean; } diff --git a/services/121-service/src/programs/dto/program-return.dto.ts b/services/121-service/src/programs/dto/program-return.dto.ts index b3050d8927..9e999ac0a2 100644 --- a/services/121-service/src/programs/dto/program-return.dto.ts +++ b/services/121-service/src/programs/dto/program-return.dto.ts @@ -13,18 +13,84 @@ import { ValidateNested, } from 'class-validator'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; -import { ProgramFinancialServiceProviderDto } from '@121-service/src/programs/dto/create-program.dto'; -import { - CreateProgramCustomAttributeDto, - CustomAttributeType, -} from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; -import { CreateProgramQuestionDto } from '@121-service/src/programs/dto/program-question.dto'; +import { ProgramFinancialServiceProviderConfigurationResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto'; +import { ProgramRegistrationAttributeDto } from '@121-service/src/programs/dto/program-registration-attribute.dto'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; import { WrapperType } from '@121-service/src/wrapper.type'; +// This declared at the top of the file because it is used in the dto class and else it is not defined yet +// It's not defined inline because typing works more convient here +const exampleAttributesReturn: ProgramRegistrationAttributeDto[] = [ + { + name: 'nameFirst', + type: RegistrationAttributeTypes.text, + options: undefined, + export: [ExportType.allPeopleAffected, ExportType.included], + scoring: {}, + showInPeopleAffectedTable: true, + editableInPortal: false, + label: { + en: 'First Name', + }, + }, + { + name: 'nameLast', + type: RegistrationAttributeTypes.text, + options: undefined, + export: [ExportType.allPeopleAffected, ExportType.included], + scoring: {}, + showInPeopleAffectedTable: true, + editableInPortal: false, + label: { + en: 'Last Name', + }, + }, + { + name: 'nr_of_children', + label: { + en: 'How many children do you have?', + }, + type: RegistrationAttributeTypes.numeric, + options: undefined, + scoring: { + '0-18': 999, + '19-65': 0, + '65>': 6, + }, + showInPeopleAffectedTable: true, + editableInPortal: false, + }, + { + name: 'roof_type', + label: { + en: 'What type is your roof?', + }, + type: RegistrationAttributeTypes.dropdown, + options: [ + { + option: 'steel', + label: { + en: 'Steel', + }, + }, + { + option: 'tiles', + label: { + en: 'Tiles', + }, + }, + ], + scoring: { + '0': 3, + '1': 6, + }, + showInPeopleAffectedTable: true, + editableInPortal: true, + }, +]; export class ProgramReturnDto { @ApiProperty({ example: 1, type: 'number' }) id: number; @@ -98,24 +164,6 @@ export class ProgramReturnDto { @IsString() public readonly paymentAmountMultiplierFormula?: string; - @ApiProperty({ - example: [ - { - fsp: FinancialServiceProviders.intersolveVoucherWhatsapp, - }, - { - fsp: FinancialServiceProviders.intersolveVoucherPaper, - }, - ], - description: - 'Use the GET /financial-service-providers endpoint to find valid fspNames.', - }) - @IsArray() - @ValidateNested() - @IsDefined() - @Type(() => ProgramFinancialServiceProviderDto) - public readonly financialServiceProviders: ProgramFinancialServiceProviderDto[]; - @ApiProperty({ example: 250 }) @IsNumber() @IsOptional() @@ -126,120 +174,13 @@ export class ProgramReturnDto { public readonly tryWhatsAppFirst: boolean; @ApiProperty({ - example: [ - { - name: 'nameParterOrganization', - type: CustomAttributeType.text, - label: { en: 'Name partner organization' }, - export: [ - ExportType.allPeopleAffected, - ExportType.included, - ExportType.payment, - ], - showInPeopleAffectedTable: true, - }, - { - name: 'exampleBoolean', - type: CustomAttributeType.boolean, - label: { en: 'Example boolean' }, - export: [ - ExportType.allPeopleAffected, - ExportType.included, - ExportType.payment, - ], - showInPeopleAffectedTable: true, - }, - ], + example: exampleAttributesReturn, }) @IsArray() @ValidateNested() @IsDefined() - @Type(() => CreateProgramCustomAttributeDto) - public readonly programCustomAttributes: CreateProgramCustomAttributeDto[]; - - @ApiProperty({ - example: [ - { - name: 'nameFirst', - answerType: 'text', - questionType: 'standard', - options: null, - persistence: true, - export: [ExportType.allPeopleAffected, ExportType.included], - scoring: {}, - showInPeopleAffectedTable: true, - editableInPortal: false, - label: { - en: 'First Name', - }, - }, - { - name: 'nameLast', - answerType: 'text', - questionType: 'standard', - options: null, - persistence: true, - export: [ExportType.allPeopleAffected, ExportType.included], - scoring: {}, - showInPeopleAffectedTable: true, - editableInPortal: false, - label: { - en: 'Last Name', - }, - }, - { - name: 'nr_of_children', - label: { - en: 'How many children do you have?', - }, - answerType: 'numeric', - questionType: 'standard', - options: null, - scoring: { - '0-18': 999, - '19-65': 0, - '65>': 6, - }, - showInPeopleAffectedTable: true, - editableInPortal: false, - }, - { - name: 'roof_type', - label: { - en: 'What type is your roof?', - }, - answerType: 'dropdown', - questionType: 'standard', - options: [ - { - id: 0, - option: 'steel', - label: { - en: 'Steel', - }, - }, - { - id: 1, - option: 'tiles', - label: { - en: 'Tiles', - }, - }, - ], - scoring: { - '0': 3, - '1': 6, - }, - showInPeopleAffectedTable: true, - editableInPortal: true, - }, - ], - }) - @IsArray() - @ValidateNested() - @IsDefined() - @Type(() => CreateProgramQuestionDto) - public readonly programQuestions: CreateProgramQuestionDto[]; + @Type(() => ProgramRegistrationAttributeDto) + public readonly programRegistrationAttributes: ProgramRegistrationAttributeDto[]; @ApiProperty({ example: { en: 'about program' } }) @IsNotEmpty() @@ -248,7 +189,8 @@ export class ProgramReturnDto { @ApiProperty({ example: ['nameFirst', 'nameLast'], - description: 'Should be array of name-related program-questions.', + description: + 'Should be array of name-related program-registration-attributes.', }) @IsArray() @IsOptional() @@ -270,6 +212,10 @@ export class ProgramReturnDto { @IsBoolean() public readonly allowEmptyPhoneNumber: boolean; + @ApiProperty() + @IsArray() + public readonly financialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationResponseDto[]; + @ApiProperty({ example: 'example.org' }) @IsOptional() public monitoringDashboardUrl?: string; diff --git a/services/121-service/src/programs/dto/update-program-fsp-configuration.dto.ts b/services/121-service/src/programs/dto/update-program-fsp-configuration.dto.ts deleted file mode 100644 index 494fcc53f9..0000000000 --- a/services/121-service/src/programs/dto/update-program-fsp-configuration.dto.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty } from 'class-validator'; - -import { FinancialServiceProviderConfigurationEnum } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { WrapperType } from '@121-service/src/wrapper.type'; - -export class UpdateProgramFspConfigurationDto { - @ApiProperty({ - example: FinancialServiceProviderConfigurationEnum.displayName, - }) - @IsNotEmpty() - @IsEnum(FinancialServiceProviderConfigurationEnum) - name: WrapperType; - - @ApiProperty({ - example: { en: 'FSP display name' }, - description: - 'Should be string (for e.g. name=username) or array of strings (for e.g. name=columnsToExport) or JSON object (for e.g name=displayName)', - }) - @IsNotEmpty() - value: string | string[] | Record; -} diff --git a/services/121-service/src/programs/dto/validation-info.dto.ts b/services/121-service/src/programs/dto/validation-info.dto.ts deleted file mode 100644 index 73ed0a1fa3..0000000000 --- a/services/121-service/src/programs/dto/validation-info.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CustomAttributeType } from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; -import { AnswerTypes } from '@121-service/src/registration/enum/custom-data-attributes'; - -export class ValidationInfo { - public readonly type?: AnswerTypes | CustomAttributeType; - public readonly options?: any[]; -} diff --git a/services/121-service/src/programs/mappers/program-registration-attribute.mapper.ts b/services/121-service/src/programs/mappers/program-registration-attribute.mapper.ts new file mode 100644 index 0000000000..9d71392b37 --- /dev/null +++ b/services/121-service/src/programs/mappers/program-registration-attribute.mapper.ts @@ -0,0 +1,55 @@ +import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; +import { ProgramRegistrationAttributeDto } from '@121-service/src/programs/dto/program-registration-attribute.dto'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; + +export class ProgramRegistrationAttributeMapper { + public static entitiesToDtos( + entities: ProgramRegistrationAttributeEntity[], + ): ProgramRegistrationAttributeDto[] { + return entities.map((entity) => this.entityToDto(entity)); + } + + public static entityToDto( + entity: ProgramRegistrationAttributeEntity, + ): ProgramRegistrationAttributeDto { + return { + name: entity.name, + label: entity.label, + type: entity.type, + isRequired: entity.isRequired, + options: entity.options ?? undefined, + scoring: entity.scoring, + pattern: entity.pattern ?? undefined, + showInPeopleAffectedTable: entity.showInPeopleAffectedTable, + editableInPortal: entity.editableInPortal, + export: entity.export as unknown as ExportType[], + duplicateCheck: entity.duplicateCheck, + placeholder: entity.placeholder ?? undefined, + }; + } + + public static dtosToEntities( + attributes: ProgramRegistrationAttributeDto[], + ): Partial[] { + return attributes.map((attribute) => this.dtoToEntity(attribute)); + } + + private static dtoToEntity( + attribute: ProgramRegistrationAttributeDto, + ): Partial { + return { + name: attribute.name, + label: attribute.label, + type: attribute.type, + isRequired: attribute.isRequired, + options: attribute.options ?? null, + scoring: attribute.scoring, + pattern: attribute.pattern ?? null, + showInPeopleAffectedTable: attribute.showInPeopleAffectedTable, + editableInPortal: attribute.editableInPortal, + export: attribute.export as unknown as ExportType[], + duplicateCheck: attribute.duplicateCheck, + placeholder: attribute.placeholder ?? null, + }; + } +} diff --git a/services/121-service/src/programs/program-custom-attribute.entity.ts b/services/121-service/src/programs/program-custom-attribute.entity.ts deleted file mode 100644 index 8c50ee2541..0000000000 --- a/services/121-service/src/programs/program-custom-attribute.entity.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - BeforeRemove, - Check, - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - Relation, - Unique, -} from 'typeorm'; - -import { CascadeDeleteEntity } from '@121-service/src/base.entity'; -import { CustomAttributeType } from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; -import { NameConstraintQuestions } from '@121-service/src/shared/const'; -import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; - -@Unique('programCustomAttributeUnique', ['name', 'programId']) -@Entity('program_custom_attribute') -@Check(`"name" NOT IN (${NameConstraintQuestions})`) -export class ProgramCustomAttributeEntity extends CascadeDeleteEntity { - @Column() - public name: string; - - @Column({ type: 'character varying' }) - public type: CustomAttributeType; - - @Column('json') - public label: LocalizedString; - - @Column({ default: false }) - public showInPeopleAffectedTable: boolean; - - @Column({ default: false }) - public duplicateCheck: boolean; - - @ManyToOne( - (_type) => ProgramEntity, - (program) => program.programCustomAttributes, - ) - @JoinColumn({ name: 'programId' }) - public program: Relation; - - @Column() - public programId: number; - - @OneToMany( - () => RegistrationDataEntity, - (registrationData) => registrationData.programCustomAttribute, - ) - public registrationData: Relation; - - @BeforeRemove() - public async cascadeDelete(): Promise { - await this.deleteAllOneToMany([ - { - entityClass: RegistrationDataEntity, - columnName: 'programCustomAttributeId', - }, - ]); - } -} diff --git a/services/121-service/src/programs/program-question.entity.ts b/services/121-service/src/programs/program-registration-attribute.entity.ts similarity index 56% rename from services/121-service/src/programs/program-question.entity.ts rename to services/121-service/src/programs/program-registration-attribute.entity.ts index 19d8f92896..9b859f6d74 100644 --- a/services/121-service/src/programs/program-question.entity.ts +++ b/services/121-service/src/programs/program-registration-attribute.entity.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from '@nestjs/swagger'; import { BeforeRemove, Check, @@ -14,87 +13,76 @@ import { import { CascadeDeleteEntity } from '@121-service/src/base.entity'; import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { NameConstraintQuestions } from '@121-service/src/shared/const'; import { QuestionOption } from '@121-service/src/shared/enum/question.enums'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; -@Unique('programQuestionUnique', ['name', 'programId']) +@Unique('programAttributeUnique', ['name', 'programId']) @Check(`"name" NOT IN (${NameConstraintQuestions})`) -@Entity('program_question') -export class ProgramQuestionEntity extends CascadeDeleteEntity { +@Entity('program_registration_attribute') +export class ProgramRegistrationAttributeEntity extends CascadeDeleteEntity { @Column() - @ApiProperty({ example: 'question1' }) public name: string; @Column('json') - @ApiProperty({ example: { en: 'label' } }) public label: LocalizedString; + @Column({ type: 'character varying' }) + public type: RegistrationAttributeTypes; + @Column() - @ApiProperty({ example: 'tel' }) - public answerType: string; + public isRequired: boolean; @Column('json', { nullable: true }) - @ApiProperty({ example: { en: 'placeholder' } }) public placeholder: LocalizedString | null; - @Column() - @ApiProperty({ example: 'standard' }) - public questionType: string; - @Column('json', { nullable: true }) - @ApiProperty({ example: [] }) public options: QuestionOption[] | null; - @Column('json') - @ApiProperty({ example: {} }) + @Column('json', { default: {} }) public scoring: Record; - @ManyToOne((_type) => ProgramEntity, (program) => program.programQuestions) + @ManyToOne( + (_type) => ProgramEntity, + (program) => program.programRegistrationAttributes, + ) @JoinColumn({ name: 'programId' }) public program: Relation; @Column() public programId: number; - @Column({ default: true }) - @ApiProperty({ example: true }) - public persistence: boolean; - @Column('json', { default: [ExportType.allPeopleAffected, ExportType.included], }) - @ApiProperty({ example: [] }) public export: ExportType[]; @Column({ type: 'character varying', nullable: true }) - @ApiProperty({ example: 'pattern' }) public pattern: string | null; @Column({ default: false }) - @ApiProperty({ example: false }) public duplicateCheck: boolean; @Column({ default: false }) - @ApiProperty({ example: false }) public showInPeopleAffectedTable: boolean; @OneToMany( - () => RegistrationDataEntity, - (registrationData) => registrationData.programQuestion, + () => RegistrationAttributeDataEntity, + (registrationAttributeData) => + registrationAttributeData.programRegistrationAttribute, ) - public registrationData: Relation; + public registrationAttributeData: Relation; @Column({ default: false }) - @ApiProperty({ example: false }) public editableInPortal: boolean; @BeforeRemove() public async cascadeDelete(): Promise { await this.deleteAllOneToMany([ { - entityClass: RegistrationDataEntity, - columnName: 'programQuestionId', + entityClass: RegistrationAttributeDataEntity, + columnName: 'programRegistrationAttributeId', }, ]); } diff --git a/services/121-service/src/programs/program.entity.ts b/services/121-service/src/programs/program.entity.ts index 3ce7454190..970a2c07ad 100644 --- a/services/121-service/src/programs/program.entity.ts +++ b/services/121-service/src/programs/program.entity.ts @@ -1,30 +1,13 @@ -import { - BeforeRemove, - Column, - Entity, - JoinTable, - ManyToMany, - OneToMany, - Relation, -} from 'typeorm'; +import { BeforeRemove, Column, Entity, OneToMany, Relation } from 'typeorm'; import { ActionEntity } from '@121-service/src/actions/action.entity'; -import { AppDataSource } from '@121-service/src/appdatasource'; import { CascadeDeleteEntity } from '@121-service/src/base.entity'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; import { MessageTemplateEntity } from '@121-service/src/notifications/message-template/message-template.entity'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; -import { ValidationInfo } from '@121-service/src/programs/dto/validation-info.dto'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; import { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { Attributes } from '@121-service/src/registration/dto/update-registration.dto'; -import { - AnswerTypes, - Attribute, - CustomAttributeType, -} from '@121-service/src/registration/enum/custom-data-attributes'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; +import { Attribute } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; @@ -61,13 +44,6 @@ export class ProgramEntity extends CascadeDeleteEntity { @Column({ type: 'character varying', nullable: true }) public paymentAmountMultiplierFormula: string | null; - @ManyToMany( - () => FinancialServiceProviderEntity, - (financialServiceProviders) => financialServiceProviders.program, - ) - @JoinTable() - public financialServiceProviders: Relation; - @Column({ type: 'integer', nullable: true }) public targetNrRegistrations: number | null; @@ -95,16 +71,12 @@ export class ProgramEntity extends CascadeDeleteEntity { public actions: ActionEntity[]; @OneToMany( - () => ProgramQuestionEntity, - (programQuestions) => programQuestions.program, - ) - public programQuestions: Relation; - - @OneToMany( - () => ProgramCustomAttributeEntity, - (programCustomAttributes) => programCustomAttributes.program, + () => ProgramRegistrationAttributeEntity, + (programRegistrationAttributes) => programRegistrationAttributes.program, ) - public programCustomAttributes: Relation; + public programRegistrationAttributes: Relation< + ProgramRegistrationAttributeEntity[] + >; @OneToMany(() => TransactionEntity, (transactions) => transactions.program) public transactions: Relation; @@ -116,7 +88,7 @@ export class ProgramEntity extends CascadeDeleteEntity { public tryWhatsAppFirst: boolean; // TODO: This can be refactored into 'nameField' so that this can be 1 field name that maps to the 'Name' column in the Portal. - // This is an array of ProgramQuestionEntity names that build up the full name of a PA. + // This is an array of ProgramRegistrationAttributeEntity names that build up the full name of a PA. @Column('json', { nullable: true }) public fullnameNamingConvention: string[] | null; @@ -136,7 +108,7 @@ export class ProgramEntity extends CascadeDeleteEntity { () => ProgramFinancialServiceProviderConfigurationEntity, (programFspConfiguration) => programFspConfiguration.programId, ) - public programFspConfiguration: Relation< + public programFinancialServiceProviderConfigurations: Relation< ProgramFinancialServiceProviderConfigurationEntity[] >; @@ -163,14 +135,6 @@ export class ProgramEntity extends CascadeDeleteEntity { entityClass: ActionEntity, columnName: 'program', }, - { - entityClass: ProgramQuestionEntity, - columnName: 'program', - }, - { - entityClass: ProgramCustomAttributeEntity, - columnName: 'program', - }, { entityClass: RegistrationEntity, columnName: 'program', @@ -185,94 +149,4 @@ export class ProgramEntity extends CascadeDeleteEntity { }, ]); } - - public async getValidationInfoForQuestionName( - name: string, - ): Promise { - if (name === Attributes.paymentAmountMultiplier) { - return { type: AnswerTypes.numeric }; - } else if (name === Attributes.maxPayments) { - return { type: AnswerTypes.numericNullable }; - } else if (name === Attributes.referenceId) { - return { type: AnswerTypes.text }; - } else if (name === Attributes.phoneNumber) { - return { type: AnswerTypes.tel }; - } else if (name === Attributes.preferredLanguage) { - return { - type: AnswerTypes.dropdown, - options: await this.getPreferredLanguageOptions(), - }; - } else if (name === Attributes.scope) { - return { type: AnswerTypes.text }; - } - - const repo = AppDataSource.getRepository(ProgramEntity); - const resultProgramQuestion = await repo - .createQueryBuilder('program') - .leftJoin('program.programQuestions', 'programQuestion') - .where('program.id = :programId', { programId: this.id }) - .andWhere('programQuestion.name = :name', { name }) - .select('"programQuestion"."answerType"', 'type') - .addSelect('"programQuestion"."options"', 'options') - .getRawOne(); - - const resultFspQuestion = await repo - .createQueryBuilder('program') - .leftJoin('program.financialServiceProviders', 'fsp') - .leftJoin('fsp.questions', 'question') - .where('program.id = :programId', { programId: this.id }) - .andWhere('question.name = :name', { name }) - .select('"question"."answerType"', 'type') - .addSelect('"question"."options"', 'options') - .getRawOne(); - - const resultProgramCustomAttribute = await repo - .createQueryBuilder('program') - .leftJoin('program.programCustomAttributes', 'programCustomAttribute') - .where('program.id = :programId', { programId: this.id }) - .andWhere('programCustomAttribute.name = :name', { name }) - .select('"programCustomAttribute".type', 'type') - .getRawOne(); - - if ( - Number(!!resultProgramQuestion) + - Number(!!resultFspQuestion) + - Number(!!resultProgramCustomAttribute) > - 1 - ) { - throw new Error( - 'Found more than one fsp question, program question or with the same name for program', - ); - } else if (resultProgramQuestion && resultProgramQuestion.type) { - return { - type: resultProgramQuestion.type as AnswerTypes, - options: resultProgramQuestion.options, - }; - } else if (resultFspQuestion && resultFspQuestion.type) { - return { - type: resultFspQuestion.type as AnswerTypes, - options: resultFspQuestion.options, - }; - } else if ( - resultProgramCustomAttribute && - resultProgramCustomAttribute.type - ) { - return { - type: resultProgramCustomAttribute.type as CustomAttributeType, - }; - } else { - return new ValidationInfo(); - } - } - - private async getPreferredLanguageOptions(): Promise { - const repo = AppDataSource.getRepository(ProgramEntity); - const program = await repo.findOneBy({ id: this.id }); - - return JSON.parse(JSON.stringify(program?.languages)).map((key: string) => { - return { - option: key, - }; - }); - } } diff --git a/services/121-service/src/programs/programs.controller.ts b/services/121-service/src/programs/programs.controller.ts index 029e441424..e97aaed6e5 100644 --- a/services/121-service/src/programs/programs.controller.ts +++ b/services/121-service/src/programs/programs.controller.ts @@ -31,18 +31,16 @@ import { AuthenticatedUserGuard } from '@121-service/src/guards/authenticated-us import { KoboConnectService } from '@121-service/src/kobo-connect/kobo-connect.service'; import { ProgramAttributesService } from '@121-service/src/program-attributes/program-attributes.service'; import { CreateProgramDto } from '@121-service/src/programs/dto/create-program.dto'; -import { CreateProgramCustomAttributeDto } from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; import { - CreateProgramQuestionDto, - UpdateProgramQuestionDto, -} from '@121-service/src/programs/dto/program-question.dto'; + ProgramRegistrationAttributeDto, + UpdateProgramRegistrationAttributeDto, +} from '@121-service/src/programs/dto/program-registration-attribute.dto'; import { ProgramReturnDto } from '@121-service/src/programs/dto/program-return.dto'; import { UpdateProgramDto } from '@121-service/src/programs/dto/update-program.dto'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { ProgramService } from '@121-service/src/programs/programs.service'; -import { Attribute } from '@121-service/src/registration/enum/custom-data-attributes'; +import { Attribute } from '@121-service/src/registration/enum/registration-attribute.enum'; import { SecretDto } from '@121-service/src/scripts/scripts.controller'; import { ScopedUserRequest } from '@121-service/src/shared/scoped-user-request'; import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; @@ -231,107 +229,78 @@ You can also leave the body empty.`, } @AuthenticatedUser({ permissions: [PermissionEnum.ProgramUPDATE] }) - @ApiOperation({ summary: 'Create program question' }) + @ApiOperation({ summary: 'Create registration attribute' }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @Post(':programId/program-questions') - public async createProgramQuestion( - @Body() updateProgramQuestionDto: CreateProgramQuestionDto, + @Post(':programId/registration-attributes') + public async createProgramRegistrationAttribute( + @Body() programRegistrationAttribute: ProgramRegistrationAttributeDto, @Param('programId', ParseIntPipe) programId: number, - ): Promise { - return await this.programService.createProgramQuestion( + ): Promise { + return await this.programService.createProgramRegistrationAttribute({ programId, - updateProgramQuestionDto, - ); + createProgramRegistrationAttributeDto: programRegistrationAttribute, + }); } - @AuthenticatedUser({ permissions: [PermissionEnum.ProgramQuestionUPDATE] }) - @ApiOperation({ summary: 'Update program question' }) + @AuthenticatedUser({ + permissions: [PermissionEnum.ProgramUPDATE], + }) + @ApiOperation({ summary: 'Update program registration attribute' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Return program question', - type: ProgramQuestionEntity, + description: 'Return program registration attribute', + type: ProgramRegistrationAttributeEntity, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Providede program registration attribute name not found', }) - @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @ApiParam({ name: 'programQuestionId', required: true, type: 'integer' }) - @Patch(':programId/program-questions/:programQuestionId') - public async updateProgramQuestion( - @Body() updateProgramQuestionDto: UpdateProgramQuestionDto, - @Param('programId', ParseIntPipe) - programId: number, - @Param('programQuestionId', ParseIntPipe) - programQuestionId: number, - ): Promise { - return await this.programService.updateProgramQuestion( - programId, - programQuestionId, - updateProgramQuestionDto, - ); - } - - @AuthenticatedUser({ permissions: [PermissionEnum.ProgramQuestionDELETE] }) - @ApiOperation({ summary: 'Delete program question AND related answers' }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) @ApiParam({ - name: 'programQuestionId', + name: 'programRegistrationAttributeName', required: true, - type: 'integer', + type: 'string', }) - @Delete(':programId/program-questions/:programQuestionId') - public async deleteProgramQuestion( - @Param('programId', ParseIntPipe) - programId: number, - @Param('programQuestionId', ParseIntPipe) - programQuestionId: number, - ): Promise { - return await this.programService.deleteProgramQuestion( - programId, - programQuestionId, - ); - } - - @AuthenticatedUser({ permissions: [PermissionEnum.ProgramUPDATE] }) - @ApiOperation({ summary: 'Create program custom attribute' }) - @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @Post(':programId/custom-attributes') - public async createProgramCustomAttribute( - @Body() createProgramQuestionDto: CreateProgramCustomAttributeDto, + @Patch(':programId/registration-attributes/:programRegistrationAttributeName') + public async updateProgramRegistrationAttribute( + @Body() + updateProgramRegistrationAttributeDto: UpdateProgramRegistrationAttributeDto, @Param('programId', ParseIntPipe) programId: number, - ): Promise { - return await this.programService.createProgramCustomAttribute( + @Param('programRegistrationAttributeName') + programRegistrationAttributeName: string, + ): Promise { + return await this.programService.updateProgramRegistrationAttribute( programId, - createProgramQuestionDto, + programRegistrationAttributeName, + updateProgramRegistrationAttributeDto, ); } @AuthenticatedUser({ - permissions: [PermissionEnum.ProgramCustomAttributeUPDATE], + permissions: [PermissionEnum.ProgramUPDATE], }) - @ApiOperation({ summary: 'Update program custom attributes' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Return program custom attributes', - type: ProgramCustomAttributeEntity, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'No attribute found for given program and custom attribute id', + @ApiOperation({ + summary: + 'Delete Registration Attribute for a Program. Also deletes the data of this Attribute for the Registrations in this Program.', }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @ApiParam({ name: 'customAttributeId', required: true, type: 'integer' }) - @Patch(':programId/custom-attributes/:customAttributeId') - public async updateProgramCustomAttributes( + @ApiParam({ + name: 'programRegistrationAttributeId', + required: true, + type: 'integer', + }) + @Delete(':programId/registration-attributes/:programRegistrationAttributeId') + public async deleteProgramRegistrationAttribute( @Param('programId', ParseIntPipe) programId: number, - @Param('customAttributeId', ParseIntPipe) - customAttributeId: number, - @Body() createProgramCustomAttributeDto: CreateProgramCustomAttributeDto, - ): Promise { - return await this.programService.updateProgramCustomAttributes( + @Param('programRegistrationAttributeId', ParseIntPipe) + programRegistrationAttributeId: number, + ): Promise { + return await this.programService.deleteProgramRegistrationAttribute( programId, - customAttributeId, - createProgramCustomAttributeDto, + programRegistrationAttributeId, ); } @@ -348,17 +317,7 @@ You can also leave the body empty.`, type: 'boolean', }) @ApiQuery({ - name: 'includeProgramQuestions', - required: false, - type: 'boolean', - }) - @ApiQuery({ - name: 'includeCustomAttributes', - required: false, - type: 'boolean', - }) - @ApiQuery({ - name: 'includeFspQuestions', + name: 'includeProgramRegistrationAttributes', required: false, type: 'boolean', }) @@ -371,12 +330,11 @@ You can also leave the body empty.`, public async getAttributes( @Param('programId', ParseIntPipe) programId: number, - @Query('includeCustomAttributes', new ParseBoolPipe({ optional: true })) - includeCustomAttributes: boolean, - @Query('includeProgramQuestions', new ParseBoolPipe({ optional: true })) - includeProgramQuestions: boolean, - @Query('includeFspQuestions', new ParseBoolPipe({ optional: true })) - includeFspQuestions: boolean, + @Query( + 'includeProgramRegistrationAttributes', + new ParseBoolPipe({ optional: true }), + ) + includeProgramRegistrationAttributes: boolean, @Query( 'includeTemplateDefaultAttributes', new ParseBoolPipe({ optional: true }), @@ -403,14 +361,13 @@ You can also leave the body empty.`, return []; } } - return await this.programAttributesService.getAttributes( + const attr = await this.programAttributesService.getAttributes({ programId, - includeCustomAttributes, - includeProgramQuestions, - includeFspQuestions, + includeProgramRegistrationAttributes, includeTemplateDefaultAttributes, filterShowInPeopleAffectedTable, - ); + }); + return attr; } // TODO: REFACTOR: This endpoint's return is not typed as a DTO, so it is not clear what the response structure is in Swagger UI. See guidelines. diff --git a/services/121-service/src/programs/programs.module.ts b/services/121-service/src/programs/programs.module.ts index 6a1164059d..f74c277599 100644 --- a/services/121-service/src/programs/programs.module.ts +++ b/services/121-service/src/programs/programs.module.ts @@ -4,32 +4,27 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ActionEntity } from '@121-service/src/actions/action.entity'; import { ActionsModule } from '@121-service/src/actions/actions.module'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; import { FinancialServiceProvidersModule } from '@121-service/src/financial-service-providers/financial-service-provider.module'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { KoboConnectModule } from '@121-service/src/kobo-connect/kobo-connect.module'; import { LookupModule } from '@121-service/src/notifications/lookup/lookup.module'; import { IntersolveVisaModule } from '@121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.module'; import { ProgramAttributesModule } from '@121-service/src/program-attributes/program-attributes.module'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; import { ProgramFinancialServiceProviderConfigurationsModule } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.module'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { ProgramController } from '@121-service/src/programs/programs.controller'; import { ProgramService } from '@121-service/src/programs/programs.service'; import { ProgramRepository } from '@121-service/src/programs/repositories/program.repository'; +import { ProgramExistenceInterceptor } from '@121-service/src/shared/interceptors/program-existence.interceptor'; import { UserModule } from '@121-service/src/user/user.module'; @Module({ imports: [ TypeOrmModule.forFeature([ ProgramEntity, - FinancialServiceProviderEntity, + ProgramRegistrationAttributeEntity, ActionEntity, - FspQuestionEntity, - ProgramQuestionEntity, - ProgramCustomAttributeEntity, ProgramFinancialServiceProviderConfigurationEntity, ]), ActionsModule, @@ -43,7 +38,7 @@ import { UserModule } from '@121-service/src/user/user.module'; ProgramFinancialServiceProviderConfigurationsModule, IntersolveVisaModule, ], - providers: [ProgramService, ProgramRepository], + providers: [ProgramService, ProgramRepository, ProgramExistenceInterceptor], controllers: [ProgramController], exports: [ProgramService, ProgramRepository], }) diff --git a/services/121-service/src/programs/programs.service.ts b/services/121-service/src/programs/programs.service.ts index 429a0f1d6f..6655da37e2 100644 --- a/services/121-service/src/programs/programs.service.ts +++ b/services/121-service/src/programs/programs.service.ts @@ -4,33 +4,27 @@ import { DataSource, Equal, QueryFailedError, Repository } from 'typeorm'; import { ActionEntity } from '@121-service/src/actions/action.entity'; import { - FinancialServiceProviderConfigurationEnum, + FinancialServiceProviderConfigurationProperties, FinancialServiceProviders, } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; +import { GetTokenReturnType } from '@121-service/src/payments/fsp-integration/intersolve-visa/interfaces/get-token-return-type.interface'; import { IntersolveVisaService } from '@121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa.service'; import { ProgramAttributesService } from '@121-service/src/program-attributes/program-attributes.service'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; +import { ProgramFinancialServiceProviderConfigurationMapper } from '@121-service/src/program-financial-service-provider-configurations/mappers/program-financial-service-provider-configuration.mapper'; import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramFinancialServiceProviderConfigurationsService } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service'; import { CreateProgramDto } from '@121-service/src/programs/dto/create-program.dto'; -import { - CreateProgramCustomAttributeDto, - UpdateProgramCustomAttributeDto, -} from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; import { FoundProgramDto } from '@121-service/src/programs/dto/found-program.dto'; import { - CreateProgramQuestionDto, - UpdateProgramQuestionDto, -} from '@121-service/src/programs/dto/program-question.dto'; + ProgramRegistrationAttributeDto, + UpdateProgramRegistrationAttributeDto, +} from '@121-service/src/programs/dto/program-registration-attribute.dto'; import { ProgramReturnDto } from '@121-service/src/programs/dto/program-return.dto'; import { UpdateProgramDto } from '@121-service/src/programs/dto/update-program.dto'; +import { ProgramRegistrationAttributeMapper } from '@121-service/src/programs/mappers/program-registration-attribute.mapper'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { overwriteProgramFspDisplayName } from '@121-service/src/programs/utils/overwrite-fsp-display-name.helper'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { RegistrationDataInfo } from '@121-service/src/registration/dto/registration-data-relation.model'; import { nameConstraintQuestionsArray } from '@121-service/src/shared/const'; import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; @@ -41,14 +35,8 @@ import { DefaultUserRole } from '@121-service/src/user/user-role.enum'; export class ProgramService { @InjectRepository(ProgramEntity) private readonly programRepository: Repository; - @InjectRepository(ProgramQuestionEntity) - private readonly programQuestionRepository: Repository; - @InjectRepository(ProgramCustomAttributeEntity) - private readonly programCustomAttributeRepository: Repository; - @InjectRepository(FspQuestionEntity) - private readonly fspAttributeRepository: Repository; - @InjectRepository(FinancialServiceProviderEntity) - public financialServiceProviderRepository: Repository; + @InjectRepository(ProgramRegistrationAttributeEntity) + private readonly programRegistrationAttributeRepository: Repository; @InjectRepository(ActionEntity) public actionRepository: Repository; @@ -74,11 +62,7 @@ export class ProgramService { ); } - const relations = [ - 'financialServiceProviders', - 'financialServiceProviders.questions', - 'programFspConfiguration', - ]; + const relations = ['programFinancialServiceProviderConfigurations']; const program = await this.programRepository.findOne({ where: { id: Equal(programId) }, @@ -89,47 +73,34 @@ export class ProgramService { throw new HttpException({ errors }, HttpStatus.NOT_FOUND); } - // Program attributes and questions are queried separately because the performance is bad when using relations - program.programCustomAttributes = - await this.programCustomAttributeRepository.find({ + program.programRegistrationAttributes = + await this.programRegistrationAttributeRepository.find({ where: { program: { id: Equal(programId) } }, }); - program.programQuestions = await this.programQuestionRepository.find({ - where: { program: { id: Equal(programId) } }, - }); program.editableAttributes = await this.programAttributesService.getPaEditableAttributes(program.id); program['paTableAttributes'] = - await this.programAttributesService.getAttributes( - program.id, - true, - true, - true, - false, - ); + await this.programAttributesService.getAttributes({ + programId: program.id, + includeProgramRegistrationAttributes: true, + includeTemplateDefaultAttributes: false, + }); // TODO: Get these attributes from some enum or something program['filterableAttributes'] = this.programAttributesService.getFilterableAttributes(program); + program['financialServiceProviderConfigurations'] = + ProgramFinancialServiceProviderConfigurationMapper.mapEntitiesToDtos( + program.programFinancialServiceProviderConfigurations, + ); const outputProgram: FoundProgramDto = program; + // TODO: REFACTOR: use DTO to define (stable) structure of data to return (not sure if transformation should be done here or in controller) if (!includeMetricsUrl) { delete outputProgram.monitoringDashboardUrl; } - - // over write fsp displayname by program specific displayName - if (outputProgram.financialServiceProviders?.length > 0) { - outputProgram.financialServiceProviders = overwriteProgramFspDisplayName( - outputProgram.financialServiceProviders, - outputProgram.programFspConfiguration ?? [], - ); - - delete outputProgram.programFspConfiguration; - } - - // TODO: REFACTOR: use DTO to define (stable) structure of data to return (not sure if transformation should be done here or in controller) return outputProgram; } @@ -150,53 +121,32 @@ export class ProgramService { private async validateProgram(programData: CreateProgramDto): Promise { if ( - !programData.financialServiceProviders || - !programData.programQuestions || - !programData.programCustomAttributes || + !programData.programRegistrationAttributes || !programData.fullnameNamingConvention ) { const errors = - 'Required properties missing: `financialServiceProviders`, `programQuestions`, `programCustomAttributes` or `fullnameNamingConvention`'; + 'Required properties missing: `programRegistrationAttributes` or `fullnameNamingConvention`'; throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); } - const fspAttributeNames: string[] = []; - for (const fsp of programData.financialServiceProviders) { - const fspEntity = - await this.financialServiceProviderRepository.findOneOrFail({ - where: { fsp: Equal(fsp.fsp) }, - relations: ['questions'], - }); - for (const question of fspEntity.questions) { - fspAttributeNames.push(question.name); - } - } - - const programQuestionNames = programData.programQuestions.map( - (q) => q.name, - ); - const customAttributeNames = programData.programCustomAttributes.map( + const programAttributeNames = programData.programRegistrationAttributes.map( (ca) => ca.name, ); - const allAttributeNames = programQuestionNames.concat( - customAttributeNames, - [...new Set(fspAttributeNames)], - ); + for (const name of Object.values(programData.fullnameNamingConvention)) { - if (!allAttributeNames.includes(name)) { - const errors = `Element '${name}' of fullnameNamingConvention is not found in program questions or custom attributes`; + if (!programAttributeNames.includes(name)) { + const errors = `Element '${name}' of fullnameNamingConvention is not found in program registration attributes`; throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); } } - - // Check if allAttributeNames has duplicate values - const duplicateNames = allAttributeNames.filter( - (item, index) => allAttributeNames.indexOf(item) !== index, + // Check if programAttributeNames has duplicate values + const duplicateNames = programAttributeNames.filter( + (item, index) => programAttributeNames.indexOf(item) !== index, ); if (duplicateNames.length > 0) { - const errors = `The names ${duplicateNames.join( + const errors = `The following names: '${duplicateNames.join( ', ', - )} are used more than once program question, custom attribute or fsp attribute`; + )}' are used more than once program registration attributes`; throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); } } @@ -240,47 +190,24 @@ export class ProgramService { // Make sure to use these repositories in this transaction else save will be part of another transacion // This can lead to duplication of data const programRepository = queryRunner.manager.getRepository(ProgramEntity); - const programQuestionRepository = queryRunner.manager.getRepository( - ProgramQuestionEntity, - ); - const programCustomAttributeRepository = queryRunner.manager.getRepository( - ProgramCustomAttributeEntity, - ); + const programRegistrationAttributeRepository = + queryRunner.manager.getRepository(ProgramRegistrationAttributeEntity); - let savedProgram; + let savedProgram: ProgramEntity; try { savedProgram = await programRepository.save(program); - savedProgram.programCustomAttributes = []; - for (const customAttribute of programData.programCustomAttributes) { - customAttribute['programId'] = savedProgram.id; - const customAttributeReturn = - await programCustomAttributeRepository.save(customAttribute); - savedProgram.programCustomAttributes.push(customAttributeReturn); - } - - savedProgram.programQuestions = []; - for (const programQuestion of programData.programQuestions) { - const programQuestionEntity = - this.programQuestionDtoToEntity(programQuestion); - programQuestionEntity['programId'] = savedProgram.id; - const programQuestionReturn = await programQuestionRepository.save( - programQuestionEntity, - ); - savedProgram.programQuestions.push(programQuestionReturn); - } - - savedProgram.financialServiceProviders = []; - for (const fspItem of programData.financialServiceProviders) { - const fsp = await this.financialServiceProviderRepository.findOne({ - where: { fsp: Equal(fspItem.fsp) }, - }); - if (!fsp) { - const errors = `Create program error: No fsp found with name ${fspItem.fsp}`; - await queryRunner.rollbackTransaction(); - throw new HttpException({ errors }, HttpStatus.NOT_FOUND); + savedProgram.programRegistrationAttributes = []; + for (const programRegistrationAttribute of programData.programRegistrationAttributes) { + const attributeReturn = + await this.createProgramRegistrationAttributeEntity({ + programId: savedProgram.id, + createProgramRegistrationAttributeDto: programRegistrationAttribute, + repository: programRegistrationAttributeRepository, + }); + if (attributeReturn) { + savedProgram.programRegistrationAttributes.push(attributeReturn); } - savedProgram.financialServiceProviders.push(fsp); } newProgram = await programRepository.save(savedProgram); @@ -288,29 +215,18 @@ export class ProgramService { } catch (err) { console.log('Error creating new program ', err); await queryRunner.rollbackTransaction(); - throw new HttpException( - 'Error creating new program', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + 'Error creating new program', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } finally { await queryRunner.release(); } - // Loop through FSPs again to store config, which can only be done after program is saved - for (const fspItem of programData.financialServiceProviders) { - if (fspItem.configuration && fspItem.configuration?.length > 0) { - for (const config of fspItem.configuration) { - await this.programFspConfigurationService.create(newProgram.id, { - fspId: savedProgram.financialServiceProviders.find( - (f) => f.fsp === fspItem.fsp, - ).id, - name: config.name, - value: config.value, - }); - } - } - } - await this.userService.assignAidworkerToProgram(newProgram.id, userId, { roles: [DefaultUserRole.Admin], scope: undefined, @@ -340,31 +256,11 @@ export class ProgramService { // Overwrite any non-nested attributes of the program with the new supplued values. for (const attribute in updateProgramDto) { // Skip attribute financialServiceProviders, or all configured FSPs will be deleted. See processing of financialServiceProviders below. - if (attribute !== 'financialServiceProviders') { + if (attribute !== 'programFinancialServiceProviderConfigurations') { program[attribute] = updateProgramDto[attribute]; } } - // Add newly supplied FSPs to the program. - if (updateProgramDto.financialServiceProviders) { - for (const fspItem of updateProgramDto.financialServiceProviders) { - if ( - !program.financialServiceProviders.some( - (fsp) => fsp.fsp === fspItem.fsp, - ) - ) { - const fsp = await this.financialServiceProviderRepository.findOne({ - where: { fsp: Equal(fspItem.fsp) }, - }); - if (!fsp) { - const errors = `Update program error: No fsp found with name ${fspItem.fsp}`; - throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); - } - program.financialServiceProviders.push(fsp); - } - } - } - let savedProgram: ProgramEntity; try { savedProgram = await this.programRepository.save(program); @@ -399,44 +295,17 @@ export class ProgramService { fixedTransferValue: program.fixedTransferValue ?? undefined, paymentAmountMultiplierFormula: program.paymentAmountMultiplierFormula ?? undefined, - financialServiceProviders: program.financialServiceProviders.map( - ({ fsp, configuration }) => ({ - fsp, - configuration, - }), - ), + financialServiceProviderConfigurations: + ProgramFinancialServiceProviderConfigurationMapper.mapEntitiesToDtos( + program.programFinancialServiceProviderConfigurations, + ), targetNrRegistrations: program.targetNrRegistrations ?? undefined, tryWhatsAppFirst: program.tryWhatsAppFirst, budget: program.budget ?? undefined, - programCustomAttributes: program.programCustomAttributes.map( - (programCustomAttribute) => { - return { - name: programCustomAttribute.name, - type: programCustomAttribute.type, - label: programCustomAttribute.label, - showInPeopleAffectedTable: - programCustomAttribute.showInPeopleAffectedTable, - duplicateCheck: programCustomAttribute.duplicateCheck, - }; - }, - ), - programQuestions: program.programQuestions.map((programQuestion) => { - return { - name: programQuestion.name, - label: programQuestion.label, - answerType: programQuestion.answerType, - questionType: programQuestion.questionType, - options: programQuestion.options ?? undefined, - scoring: programQuestion.scoring, - persistence: programQuestion.persistence, - pattern: programQuestion.pattern ?? undefined, - showInPeopleAffectedTable: programQuestion.showInPeopleAffectedTable, - editableInPortal: programQuestion.editableInPortal, - export: programQuestion.export as unknown as ExportType[], - duplicateCheck: programQuestion.duplicateCheck, - placeholder: programQuestion.placeholder ?? undefined, - }; - }), + programRegistrationAttributes: + ProgramRegistrationAttributeMapper.entitiesToDtos( + program.programRegistrationAttributes, + ), aboutProgram: program.aboutProgram ?? undefined, fullnameNamingConvention: program.fullnameNamingConvention ?? undefined, languages: program.languages, @@ -451,222 +320,172 @@ export class ProgramService { return programDto; } - public async updateProgramCustomAttributes( - programId: number, - customAttributeId: number, - updateProgramCustomAttributeDto: UpdateProgramCustomAttributeDto, - ): Promise { - const customAttribute = await this.programCustomAttributeRepository.findOne( - { - where: { - id: Equal(customAttributeId), - programId: Equal(programId), - }, - }, - ); - if (!customAttribute) { - const errors = `No program custom attribute found with id ${customAttributeId} for program ${programId}`; - throw new HttpException({ errors }, HttpStatus.NOT_FOUND); - } - - for (const property of Object.keys(updateProgramCustomAttributeDto)) { - customAttribute[property] = updateProgramCustomAttributeDto[property]; - } - return await this.programCustomAttributeRepository.save(customAttribute); - } - private async validateAttributeName( programId: number, name: string, ): Promise { const existingAttributes = - await this.programAttributesService.getAttributes( + await this.programAttributesService.getAttributes({ programId, - true, - true, - true, - false, - ); + includeProgramRegistrationAttributes: true, + includeTemplateDefaultAttributes: false, + }); const existingNames = existingAttributes.map((attr) => { return attr.name; }); if (existingNames.includes(name)) { - const errors = `Unable to create program question/attribute with name ${name}. The names ${existingNames.join( + const errors = `Unable to create program registration attribute with name ${name}. The names ${existingNames.join( ', ', )} are already in use`; throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); } if (nameConstraintQuestionsArray.includes(name)) { - const errors = `Unable to create program question/attribute with name ${name}. The names ${nameConstraintQuestionsArray.join( + const errors = `Unable to create program registration attribute with name ${name}. The names ${nameConstraintQuestionsArray.join( ', ', )} are forbidden to use`; throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); } } - public async createProgramCustomAttribute( - programId: number, - createProgramAttributeDto: CreateProgramCustomAttributeDto, - ) { - await this.validateAttributeName(programId, createProgramAttributeDto.name); - const programCustomAttribute = this.programCustomAttributeDtoToEntity( - createProgramAttributeDto, - ); - programCustomAttribute.programId = programId; - try { - return await this.programCustomAttributeRepository.save( - programCustomAttribute, - ); - } catch (error) { - if (error instanceof QueryFailedError) { - const errorMessage = error.message; // Get the error message from QueryFailedError - throw new HttpException(errorMessage, HttpStatus.BAD_REQUEST); - } - } - return; - } - - private programCustomAttributeDtoToEntity( - dto: CreateProgramCustomAttributeDto, - ): ProgramCustomAttributeEntity { - const programCustomAttribute = new ProgramCustomAttributeEntity(); - programCustomAttribute.name = dto.name; - programCustomAttribute.type = dto.type; - programCustomAttribute.label = dto.label; - programCustomAttribute.duplicateCheck = dto.duplicateCheck ?? false; - return programCustomAttribute; + public async createProgramRegistrationAttribute({ + programId, + createProgramRegistrationAttributeDto, + }: { + programId: number; + createProgramRegistrationAttributeDto: ProgramRegistrationAttributeDto; + }): Promise { + const entity = await this.createProgramRegistrationAttributeEntity({ + programId, + createProgramRegistrationAttributeDto, + }); + return ProgramRegistrationAttributeMapper.entityToDto(entity); } - public async createProgramQuestion( - programId: number, - createProgramQuestionDto: CreateProgramQuestionDto, - ) { - await this.validateAttributeName(programId, createProgramQuestionDto.name); - const programQuestion = this.programQuestionDtoToEntity( - createProgramQuestionDto, + private async createProgramRegistrationAttributeEntity({ + programId, + createProgramRegistrationAttributeDto, + repository, + }: { + programId: number; + createProgramRegistrationAttributeDto: ProgramRegistrationAttributeDto; + repository?: Repository; + }): Promise { + await this.validateAttributeName( + programId, + createProgramRegistrationAttributeDto.name, ); - programQuestion.programId = programId; + const programRegistrationAttribute = + this.programRegistrationAttributeDtoToEntity( + createProgramRegistrationAttributeDto, + ); + programRegistrationAttribute.programId = programId; try { - return await this.programQuestionRepository.save(programQuestion); + if (repository) { + return await repository.save(programRegistrationAttribute); + } else { + return await this.programRegistrationAttributeRepository.save( + programRegistrationAttribute, + ); + } } catch (error) { if (error instanceof QueryFailedError) { const errorMessage = error.message; // Get the error message from QueryFailedError throw new HttpException(errorMessage, HttpStatus.BAD_REQUEST); } + // Unexpected error + throw error; } - return; } - private programQuestionDtoToEntity( - dto: CreateProgramQuestionDto, - ): ProgramQuestionEntity { - const programQuestion = new ProgramQuestionEntity(); - programQuestion.name = dto.name; - programQuestion.label = dto.label; - programQuestion.answerType = dto.answerType; - programQuestion.questionType = dto.questionType; - programQuestion.options = dto.options ?? null; - programQuestion.scoring = dto.scoring ?? {}; - programQuestion.persistence = dto.persistence ?? false; - programQuestion.pattern = dto.pattern ?? null; - programQuestion.editableInPortal = dto.editableInPortal ?? false; - programQuestion.export = dto.export ?? []; - programQuestion.duplicateCheck = dto.duplicateCheck ?? false; - programQuestion.placeholder = dto.placeholder ?? null; - return programQuestion; + private programRegistrationAttributeDtoToEntity( + dto: ProgramRegistrationAttributeDto, + ): ProgramRegistrationAttributeEntity { + const programRegistrationAttribute = + new ProgramRegistrationAttributeEntity(); + programRegistrationAttribute.name = dto.name; + programRegistrationAttribute.label = dto.label; + programRegistrationAttribute.type = dto.type; + programRegistrationAttribute.options = dto.options ?? null; + programRegistrationAttribute.scoring = dto.scoring ?? {}; + programRegistrationAttribute.pattern = dto.pattern ?? null; + programRegistrationAttribute.editableInPortal = + dto.editableInPortal ?? false; + programRegistrationAttribute.export = dto.export ?? []; + programRegistrationAttribute.duplicateCheck = dto.duplicateCheck ?? false; + programRegistrationAttribute.placeholder = dto.placeholder ?? null; + programRegistrationAttribute.isRequired = dto.isRequired ?? false; + programRegistrationAttribute.showInPeopleAffectedTable = + dto.showInPeopleAffectedTable ?? false; + return programRegistrationAttribute; } - public async updateProgramQuestion( + public async updateProgramRegistrationAttribute( programId: number, - programQuestionId: number, - updateProgramQuestionDto: UpdateProgramQuestionDto, - ): Promise { - const programQuestion = await this.programQuestionRepository.findOne({ - where: { - id: Equal(programQuestionId), - programId: Equal(programId), - }, - }); - if (!programQuestion) { - const errors = `No programQuestion found with id ${programQuestionId} for program ${programId}`; + programRegistrationAttributeName: string, + updateProgramRegistrationAttribute: UpdateProgramRegistrationAttributeDto, + ): Promise { + const programRegistrationAttribute = + await this.programRegistrationAttributeRepository.findOne({ + where: { + name: Equal(programRegistrationAttributeName), + programId: Equal(programId), + }, + }); + if (!programRegistrationAttribute) { + const errors = `No programRegistrationAttribute found with name ${programRegistrationAttributeName} for program ${programId}`; throw new HttpException({ errors }, HttpStatus.NOT_FOUND); } - for (const attribute in updateProgramQuestionDto) { - programQuestion[attribute] = updateProgramQuestionDto[attribute]; + for (const attribute in updateProgramRegistrationAttribute) { + programRegistrationAttribute[attribute] = + updateProgramRegistrationAttribute[attribute]; } - await this.programQuestionRepository.save(programQuestion); - return programQuestion; + await this.programRegistrationAttributeRepository.save( + programRegistrationAttribute, + ); + return programRegistrationAttribute; } - public async deleteProgramQuestion( + public async deleteProgramRegistrationAttribute( programId: number, - programQuestionId: number, - ): Promise { + programRegistrationAttributeId: number, + ): Promise { await this.findProgramOrThrow(programId); - const programQuestion = await this.programQuestionRepository.findOne({ - where: { id: Number(programQuestionId) }, - }); - if (!programQuestion) { - const errors = `Program question with id: '${programQuestionId}' not found.'`; + const programRegistrationAttribute = + await this.programRegistrationAttributeRepository.findOne({ + where: { id: Number(programRegistrationAttributeId) }, + }); + if (!programRegistrationAttribute) { + const errors = `Program registration attribute with id: '${programRegistrationAttributeId}' not found.'`; throw new HttpException({ errors }, HttpStatus.NOT_FOUND); } - return await this.programQuestionRepository.remove(programQuestion); + return await this.programRegistrationAttributeRepository.remove( + programRegistrationAttribute, + ); } public async getAllRelationProgram( programId: number, ): Promise { const relations: RegistrationDataInfo[] = []; - const programCustomAttributes = - await this.programCustomAttributeRepository.find({ + + const programRegistrationAttributes = + await this.programRegistrationAttributeRepository.find({ where: { program: { id: Equal(programId) } }, }); - for (const attribute of programCustomAttributes) { + for (const attribute of programRegistrationAttributes) { relations.push({ name: attribute.name, type: attribute.type, relation: { - programCustomAttributeId: attribute.id, - }, - }); - } - - const programQuestions = await this.programQuestionRepository.find({ - where: { program: { id: Equal(programId) } }, - }); - - for (const question of programQuestions) { - relations.push({ - name: question.name, - type: question.answerType, - relation: { - programQuestionId: question.id, + programRegistrationAttributeId: attribute.id, }, }); } - const fspAttributes = await this.fspAttributeRepository.find({ - relations: ['fsp', 'fsp.program'], - }); - const programFspAttributes = fspAttributes.filter((a) => - a.fsp.program.map((p) => p.id).includes(programId), - ); - - for (const attribute of programFspAttributes) { - relations.push({ - name: attribute.name, - type: attribute.answerType, - relation: { - fspQuestionId: attribute.id, - }, - fspId: attribute.fspId, - }); - } - return relations; } @@ -681,27 +500,15 @@ export class ProgramService { ); } - public async getFspConfigurations( - programId: number, - configName: string[], - ): Promise { - let programFspConfigurations = - await this.programFspConfigurationService.findByProgramId(programId); - if (configName.length > 0) { - programFspConfigurations = programFspConfigurations.filter( - (programFspConfiguration) => - configName.includes(programFspConfiguration.name), - ); - } - - return programFspConfigurations; - } - public async getFundingWallet(programId: number) { + // TODO: Refactor ensure this works with the new structure of FSP configuration properties const programFspConfigurations = - await this.programFinancialServiceProviderConfigurationRepository.findByProgramIdAndFinancialServiceProviderName( - programId, - FinancialServiceProviders.intersolveVisa, + await this.programFinancialServiceProviderConfigurationRepository.getByProgramIdAndFinancialServiceProviderName( + { + programId, + financialServiceProviderName: + FinancialServiceProviders.intersolveVisa, + }, ); if (!programFspConfigurations) { throw new HttpException( @@ -710,20 +517,41 @@ export class ProgramService { ); } - const fundingTokenConfiguration = programFspConfigurations.find( + // add all properties to a single array + const properties: ProgramFinancialServiceProviderConfigurationPropertyEntity[] = + []; + for (const programFspConfiguration of programFspConfigurations) { + properties.push(...programFspConfiguration.properties); + } + + const fundingTokenConfigurationProperties = properties.filter( (config) => config.name === - FinancialServiceProviderConfigurationEnum.fundingTokenCode, + FinancialServiceProviderConfigurationProperties.fundingTokenCode, ); - if (!fundingTokenConfiguration) { + if ( + !fundingTokenConfigurationProperties || + fundingTokenConfigurationProperties.length === 0 + ) { throw new HttpException( - 'Funding token configuration not found', + 'Funding token configuration property not found', HttpStatus.NOT_FOUND, ); } - return await this.intersolveVisaService.getWallet( - fundingTokenConfiguration.value as string, - ); + // loop over all properties and return all wallets as an array + const wallets: GetTokenReturnType[] = []; + for (const property of properties) { + if ( + property.name === + FinancialServiceProviderConfigurationProperties.fundingTokenCode + ) { + const wallet = await this.intersolveVisaService.getWallet( + property.value as string, + ); + wallets.push(wallet); + } + } + return wallets; } } diff --git a/services/121-service/src/programs/utils/overwrite-fsp-display-name.helper.ts b/services/121-service/src/programs/utils/overwrite-fsp-display-name.helper.ts deleted file mode 100644 index 64edee4abf..0000000000 --- a/services/121-service/src/programs/utils/overwrite-fsp-display-name.helper.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { isArray, isObject } from 'lodash'; - -import { FinancialServiceProviderConfigurationEnum } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configuration.entity'; -import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; -import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; - -export function overwriteProgramFspDisplayName( - programFinancialServiceProviders: FinancialServiceProviderEntity[], - programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[], -): FinancialServiceProviderEntity[] { - let overwrittenProgramFinancialServiceProviders: FinancialServiceProviderEntity[] = - []; - - if (programFinancialServiceProviders.length > 0) { - overwrittenProgramFinancialServiceProviders = - programFinancialServiceProviders.map((financialServiceProvider) => { - const displayNameConfig = - programFinancialServiceProviderConfigurations.filter( - (programFinancialServiceProviderConfiguration) => - programFinancialServiceProviderConfiguration.fspId === - financialServiceProvider.id && - programFinancialServiceProviderConfiguration.name === - FinancialServiceProviderConfigurationEnum.displayName, - ); - - if (displayNameConfig.length > 0) { - // TODO: there should be a cleaner way to handle things here - // should "value" really have the capability of being all of these things? - if ( - isObject(displayNameConfig[0].value) && - !isArray(displayNameConfig[0].value) - ) { - financialServiceProvider.displayName = displayNameConfig[0] - .value as LocalizedString; - } - } - - return financialServiceProvider; - }); - } - - return overwrittenProgramFinancialServiceProviders; -} - -export function getFspDisplayNameMapping( - program: ProgramEntity, -): Record { - if (!program.financialServiceProviders || !program.programFspConfiguration) { - throw new Error( - `getFspDisplayNameMapping: Should be used with a program entity relations ['financialServiceProviders', 'programFspConfiguration']`, - ); - } - const map = {}; - if (program.financialServiceProviders.length > 0) { - program.financialServiceProviders = overwriteProgramFspDisplayName( - program.financialServiceProviders, - program.programFspConfiguration, - ); - } - for (const fsp of program.financialServiceProviders) { - map[fsp.fsp] = fsp.displayName; - } - return map; -} - -export function overwriteFspDisplayName( - financialServiceProvider: RegistrationViewEntity['financialServiceProvider'], - fspDisplayNameMapping?: Record, -): LocalizedString | undefined { - if (!financialServiceProvider || !fspDisplayNameMapping) { - return undefined; - } - return fspDisplayNameMapping[financialServiceProvider]; -} diff --git a/services/121-service/src/registration/const/filter-operation.const.ts b/services/121-service/src/registration/const/filter-operation.const.ts index b277622f7d..557c4cc6a1 100644 --- a/services/121-service/src/registration/const/filter-operation.const.ts +++ b/services/121-service/src/registration/const/filter-operation.const.ts @@ -29,7 +29,8 @@ const basePaginateConfigRegistrationView: PaginateConfig 'preferredLanguage', 'inclusionScore', 'paymentAmountMultiplier', - 'financialServiceProvider', + 'financialServiceProviderName', + 'programFinancialServiceProviderConfigurationName', 'registrationProgramId', 'personAffectedSequence', 'maxPayments', @@ -47,8 +48,11 @@ const basePaginateConfigRegistrationView: PaginateConfig preferredLanguage: AllowedFilterOperatorsString, inclusionScore: AllowedFilterOperatorsNumber, paymentAmountMultiplier: AllowedFilterOperatorsNumber, - financialServiceProvider: AllowedFilterOperatorsString, - fspDisplayName: AllowedFilterOperatorsString, + financialServiceProviderName: AllowedFilterOperatorsString, + programFinancialServiceProviderConfigurationName: + AllowedFilterOperatorsString, + programFinancialServiceProviderConfigurationLabel: + AllowedFilterOperatorsString, registrationProgramId: AllowedFilterOperatorsNumber, maxPayments: AllowedFilterOperatorsNumber, paymentCount: AllowedFilterOperatorsNumber, diff --git a/services/121-service/src/registration/dto/bulk-action-result.dto.ts b/services/121-service/src/registration/dto/bulk-action-result.dto.ts index bd7e986cf5..1ecdbcb6ee 100644 --- a/services/121-service/src/registration/dto/bulk-action-result.dto.ts +++ b/services/121-service/src/registration/dto/bulk-action-result.dto.ts @@ -1,7 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; - export class BulkActionResultDto { @ApiProperty({ example: 10, @@ -17,12 +15,9 @@ export class BulkActionResultDto { export class BulkActionResultRetryPaymentDto extends BulkActionResultDto { @ApiProperty({ - example: [ - FinancialServiceProviders.intersolveVisa, - FinancialServiceProviders.excel, - ], + example: ['Intersolve-voucher-whatsapp', 'Intersolve-voucher-paper'], }) - public readonly fspsInPayment: FinancialServiceProviders[]; + public readonly programFinancialServiceProviderConfigurationNames: string[]; } export class BulkActionResultPaymentDto extends BulkActionResultRetryPaymentDto { diff --git a/services/121-service/src/registration/dto/bulk-import.dto.ts b/services/121-service/src/registration/dto/bulk-import.dto.ts index a9bf3f476d..335fe4efda 100644 --- a/services/121-service/src/registration/dto/bulk-import.dto.ts +++ b/services/121-service/src/registration/dto/bulk-import.dto.ts @@ -1,7 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, - IsIn, IsInt, IsNumber, IsOptional, @@ -26,7 +25,7 @@ const fspArray = Object.values(FinancialServiceProviders).map((item) => String(item), ); const languageArray = Object.values(LanguageEnum).map((item) => String(item)); -export class BulkImportDto { +class BulkImportDto { @ApiProperty() @IsString() @IsOptional() @@ -59,17 +58,17 @@ export class BulkImportDto { public scope?: string; } -export class BulkImportResult extends BulkImportDto { +class BulkImportResult extends BulkImportDto { public importStatus: ImportStatus; public registrationStatus: RegistrationStatusEnum | string; } export class ImportResult { - public aggregateImportResult: AggregateImportResult; + public aggregateImportResult: AggregateImportResultDto; public importResult?: BulkImportResult[]; } -class AggregateImportResult { +export class AggregateImportResultDto { public countImported?: number; public countExistingPhoneNr?: number; public countInvalidPhoneNr?: number; @@ -79,11 +78,13 @@ class AggregateImportResult { } export class ImportRegistrationsDto extends BulkImportDto { @ApiProperty({ - enum: fspArray, example: fspArray.join(' | '), }) - @IsIn(fspArray) - public fspName: FinancialServiceProviders; + @IsString() + // Should we change this to a more specific name? + // It could also be programFinancialServiceProviderConfigurationName (which is a good name for us programmers) + // However this name is also used by users in the csv file, so it should be a name that is understandable for them + public programFinancialServiceProviderConfigurationName: string; @ApiProperty() @IsOptional() diff --git a/services/121-service/src/registration/dto/bulk-update.dto.ts b/services/121-service/src/registration/dto/bulk-update.dto.ts deleted file mode 100644 index ffaeddd9b1..0000000000 --- a/services/121-service/src/registration/dto/bulk-update.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; - -import { BulkImportDto } from '@121-service/src/registration/dto/bulk-import.dto'; - -export class BulkUpdateDto extends BulkImportDto { - @ApiProperty() - @IsString() - public referenceId: string; - - @ApiProperty() - @IsOptional() - public declare scope?: string; -} diff --git a/services/121-service/src/registration/dto/custom-data.dto.ts b/services/121-service/src/registration/dto/custom-data.dto.ts deleted file mode 100644 index d19b649ac2..0000000000 --- a/services/121-service/src/registration/dto/custom-data.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, Length } from 'class-validator'; - -import { IsRegistrationDataValidType } from '@121-service/src/registration/validators/registration-data-type.class.validator'; - -export class CustomDataDto { - @ApiProperty({ example: '910c50be-f131-4b53-b06b-6506a40a2734' }) - @Length(5, 200) - public readonly referenceId: string; - @ApiProperty({ example: 'whatsappPhoneNumber' }) - @IsNotEmpty() - @IsString() - public readonly key: string; - @ApiProperty({ example: '31600000000' }) - @IsNotEmpty() - @IsRegistrationDataValidType({ - referenceId: 'referenceId', - attribute: 'key', - }) - public readonly value: string | string[]; -} diff --git a/services/121-service/src/registration/dto/registration-data-relation.model.ts b/services/121-service/src/registration/dto/registration-data-relation.model.ts index eeac3d1315..3de3e4ae85 100644 --- a/services/121-service/src/registration/dto/registration-data-relation.model.ts +++ b/services/121-service/src/registration/dto/registration-data-relation.model.ts @@ -1,7 +1,5 @@ export class RegistrationDataRelation { - public programQuestionId?: number; - public fspQuestionId?: number; - public programCustomAttributeId?: number; + public programRegistrationAttributeId: number; } interface RegistrationDataOpionsWithRequiredRelation { @@ -20,5 +18,4 @@ export class RegistrationDataInfo { public name: string; public relation: RegistrationDataRelation; public type: string; - public fspId?: number; } diff --git a/services/121-service/src/registration/dto/registration-update-job.dto.ts b/services/121-service/src/registration/dto/registration-update-job.dto.ts index 5840e96c5b..dd3701850f 100644 --- a/services/121-service/src/registration/dto/registration-update-job.dto.ts +++ b/services/121-service/src/registration/dto/registration-update-job.dto.ts @@ -1,7 +1,7 @@ export class RegistrationsUpdateJobDto { referenceId: string; programId: number; - data: Record; - request?: { userId?: number; scope?: string }; + data: Record; + request: { userId: number; scope?: string }; reason: string; } diff --git a/services/121-service/src/registration/dto/set-fsp.dto.ts b/services/121-service/src/registration/dto/set-fsp.dto.ts deleted file mode 100644 index 51a6720056..0000000000 --- a/services/121-service/src/registration/dto/set-fsp.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsIn, IsOptional } from 'class-validator'; - -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; - -const fspArray = Object.values(FinancialServiceProviders).map((item) => - String(item), -); - -export class UpdateChosenFspDto { - @ApiProperty({ - enum: fspArray, - example: fspArray.join(' | '), - }) - @IsIn(fspArray) - public readonly newFspName: FinancialServiceProviders; - @ApiProperty({ - example: { - whatsappPhoneNumber: '31600000000', - }, - }) - @IsOptional() - public readonly newFspAttributes?: Record; -} diff --git a/services/121-service/src/registration/dto/update-registration.dto.ts b/services/121-service/src/registration/dto/update-registration.dto.ts index 70c4a47127..813ad746b6 100644 --- a/services/121-service/src/registration/dto/update-registration.dto.ts +++ b/services/121-service/src/registration/dto/update-registration.dto.ts @@ -1,8 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString, Length } from 'class-validator'; +import { IsOptional, IsString } from 'class-validator'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; -import { IsRegistrationDataValidType } from '@121-service/src/registration/validators/registration-data-type.class.validator'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; export enum AdditionalAttributes { paymentAmountMultiplier = 'paymentAmountMultiplier', @@ -10,37 +9,22 @@ export enum AdditionalAttributes { maxPayments = 'maxPayments', referenceId = 'referenceId', scope = 'scope', + programFinancialServiceProviderConfigurationName = 'programFinancialServiceProviderConfigurationName', } -export const Attributes = { ...AdditionalAttributes, ...CustomDataAttributes }; -export type Attributes = AdditionalAttributes | CustomDataAttributes; - -const attributesArray = Object.values(Attributes).map((item) => String(item)); - -export class UpdateAttributeDto { - @ApiProperty({ example: '910c50be-f131-4b53-b06b-6506a40a2734' }) - @Length(5, 200) - public readonly referenceId: string; - @ApiProperty({ - enum: attributesArray, - example: attributesArray.join(' | '), - }) - public readonly attribute: Attributes | string; - @ApiProperty({ example: 'new value' }) - @IsRegistrationDataValidType({ - referenceId: 'referenceId', - attribute: 'attribute', - }) - public readonly value: string | number | string[]; - - public readonly userId: number; -} +export const Attributes = { + ...AdditionalAttributes, + ...DefaultRegistrationDataAttributeNames, +}; +export type Attributes = + | AdditionalAttributes + | DefaultRegistrationDataAttributeNames; export class UpdateRegistrationDto { @ApiProperty({ description: `Key value pairs of the registration object.`, example: `{ "phoneNumber" : "1234567890" }`, }) - public data: Record; + public data: Record; @ApiProperty({ description: `Reason is the same for all provided attributes in one API-call`, diff --git a/services/121-service/src/registration/dto/validate-registration-config.dto.ts b/services/121-service/src/registration/dto/validate-registration-config.dto.ts deleted file mode 100644 index 9a9fe95638..0000000000 --- a/services/121-service/src/registration/dto/validate-registration-config.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class ValidationConfigDto { - validateUniqueReferenceId = true; - validatePreferredLanguage = true; - validateExistingReferenceId = true; - validateScope = true; - validatePhoneNumberEmpty = true; - validatePhoneNumberLookup = true; - validateClassValidator = true; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} diff --git a/services/121-service/src/registration/dto/validate-registration-error-object.dto.ts b/services/121-service/src/registration/dto/validate-registration-error-object.dto.ts deleted file mode 100644 index 9b3c9ce352..0000000000 --- a/services/121-service/src/registration/dto/validate-registration-error-object.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class ValidateRegistrationErrorObjectDto { - public lineNumber: number; - public column: string; - public value: string; - public error: string; -} diff --git a/services/121-service/src/registration/enum/custom-data-attributes.ts b/services/121-service/src/registration/enum/custom-data-attributes.ts deleted file mode 100644 index c6d84da071..0000000000 --- a/services/121-service/src/registration/enum/custom-data-attributes.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { QuestionOption } from '@121-service/src/shared/enum/question.enums'; -import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; - -export enum CustomDataAttributes { - phoneNumber = 'phoneNumber', - whatsappPhoneNumber = 'whatsappPhoneNumber', - name = 'name', - nameFirst = 'nameFirst', - nameLast = 'nameLast', - firstName = 'firstName', - lastName = 'lastName', - fathersName = 'fathersName', - namePartnerOrganization = 'namePartnerOrganization', - address = 'address', - addressNoPostalIndex = 'addressNoPostalIndex', - oblast = 'oblast', - raion = 'raion', - postalIndex = 'postalIndex', - city = 'city', - street = 'street', - house = 'house', - apartmentOrOffice = 'apartmentOrOffice', - taxId = 'taxId', - addressStreet = 'addressStreet', - addressHouseNumber = 'addressHouseNumber', - addressHouseNumberAddition = 'addressHouseNumberAddition', - addressPostalCode = 'addressPostalCode', - addressCity = 'addressCity', -} - -export enum GenericAttributes { - referenceId = 'referenceId', - phoneNumber = 'phoneNumber', - preferredLanguage = 'preferredLanguage', - paymentAmountMultiplier = 'paymentAmountMultiplier', - fspName = 'fspName', - maxPayments = 'maxPayments', - paymentCount = 'paymentCount', - scope = 'scope', - status = 'status', - registrationProgramId = 'registrationProgramId', - fspDisplayName = 'fspDisplayName', - registrationCreatedDate = 'registrationCreatedDate', -} - -export class Attribute { - public id?: number; - public name: string; - // TODO: AB#30519 type should not be "string" after the refactor - // public type: AnswerTypes; - public type: string; - public label: LocalizedString | null; - public options?: QuestionOption[] | null; - public questionType?: QuestionType; // TODO: remove this in after implementing pagination - public fspNames?: FinancialServiceProviders[]; - public questionTypes?: QuestionType[]; - public pattern?: string | null; -} - -export type AttributeWithOptionalLabel = Omit & - Partial>; - -export enum QuestionType { - programQuestion = 'programQuestion', - fspQuestion = 'fspQuestion', - programCustomAttribute = 'programCustomAttribute', -} - -export enum AnswerTypes { - tel = 'tel', - dropdown = 'dropdown', - numeric = 'numeric', - numericNullable = 'numeric-nullable', - text = 'text', - date = 'date', - multiSelect = 'multi-select', -} - -export enum CustomAttributeType { - boolean = 'boolean', - text = 'text', -} diff --git a/services/121-service/src/registration/enum/registration-attribute.enum.ts b/services/121-service/src/registration/enum/registration-attribute.enum.ts new file mode 100644 index 0000000000..122031e387 --- /dev/null +++ b/services/121-service/src/registration/enum/registration-attribute.enum.ts @@ -0,0 +1,48 @@ +import { QuestionOption } from '@121-service/src/shared/enum/question.enums'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; + +export enum DefaultRegistrationDataAttributeNames { + phoneNumber = 'phoneNumber', + whatsappPhoneNumber = 'whatsappPhoneNumber', + name = 'name', +} + +export enum GenericRegistrationAttributes { + referenceId = 'referenceId', + phoneNumber = 'phoneNumber', + preferredLanguage = 'preferredLanguage', + paymentAmountMultiplier = 'paymentAmountMultiplier', + programFinancialServiceProviderConfigurationName = 'programFinancialServiceProviderConfigurationName', + programFinancialServiceProviderConfigurationLabel = 'programFinancialServiceProviderConfigurationLabel', + maxPayments = 'maxPayments', + paymentCount = 'paymentCount', + paymentCountRemaining = 'paymentCountRemaining', + scope = 'scope', + status = 'status', + registrationProgramId = 'registrationProgramId', + registrationCreatedDate = 'registrationCreatedDate', +} + +export class Attribute { + public id?: number; + public name: string; + public type: RegistrationAttributeTypes; + public isRequired?: boolean; + public label: LocalizedString | null; + public options?: QuestionOption[] | null; + public pattern?: string | null; +} + +export type AttributeWithOptionalLabel = Omit & + Partial>; + +export enum RegistrationAttributeTypes { + tel = 'tel', + dropdown = 'dropdown', + numeric = 'numeric', + numericNullable = 'numeric-nullable', + text = 'text', + date = 'date', + multiSelect = 'multi-select', + boolean = 'boolean', +} diff --git a/services/121-service/src/registration/enum/registration-csv-validation.enum.ts b/services/121-service/src/registration/enum/registration-csv-validation.enum.ts deleted file mode 100644 index 03c1f28a76..0000000000 --- a/services/121-service/src/registration/enum/registration-csv-validation.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum RegistrationCsvValidationEnum { - importAsRegistered = 'importAsRegistered', - bulkUpdate = 'bulkUpdate', -} diff --git a/services/121-service/src/registration/enum/registration-validation-input-type.enum.ts b/services/121-service/src/registration/enum/registration-validation-input-type.enum.ts new file mode 100644 index 0000000000..27e20df753 --- /dev/null +++ b/services/121-service/src/registration/enum/registration-validation-input-type.enum.ts @@ -0,0 +1,4 @@ +export enum RegistrationValidationInputType { + create = 'create', + update = 'update', +} diff --git a/services/121-service/src/registration/interfaces/validate-registration-config.interface.ts b/services/121-service/src/registration/interfaces/validate-registration-config.interface.ts new file mode 100644 index 0000000000..1c26531234 --- /dev/null +++ b/services/121-service/src/registration/interfaces/validate-registration-config.interface.ts @@ -0,0 +1,5 @@ +export interface ValidationRegistrationConfig { + readonly validateUniqueReferenceId: boolean; + readonly validateExistingReferenceId: boolean; + readonly validatePhoneNumberLookup: boolean; +} diff --git a/services/121-service/src/registration/interfaces/validate-registration-error-object.interface.ts b/services/121-service/src/registration/interfaces/validate-registration-error-object.interface.ts new file mode 100644 index 0000000000..1a75f8ffab --- /dev/null +++ b/services/121-service/src/registration/interfaces/validate-registration-error-object.interface.ts @@ -0,0 +1,6 @@ +export interface ValidateRegistrationErrorObject { + readonly lineNumber: number; + readonly column: string; + readonly error: string; + readonly value: string | number | undefined | boolean; +} diff --git a/services/121-service/src/registration/interfaces/validated-registration-input.interface.ts b/services/121-service/src/registration/interfaces/validated-registration-input.interface.ts new file mode 100644 index 0000000000..d51c078580 --- /dev/null +++ b/services/121-service/src/registration/interfaces/validated-registration-input.interface.ts @@ -0,0 +1,23 @@ +import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; + +export interface ValidatedRegistrationInput + extends RegistrationEntityProperties { + programFinancialServiceProviderConfigurationName?: string; + data: Record; +} + +// Had to use a type here for using Pick +type RegistrationEntityProperties = Partial< + Pick< + InstanceType, + | 'programId' + | 'registrationStatus' + | 'referenceId' + | 'phoneNumber' + | 'preferredLanguage' + | 'inclusionScore' + | 'paymentAmountMultiplier' + | 'maxPayments' + | 'scope' + > +>; diff --git a/services/121-service/src/registration/modules/queue-registrations-update/queue-registrations-update.service.ts b/services/121-service/src/registration/modules/queue-registrations-update/queue-registrations-update.service.ts index 22f3de4764..0d48083e83 100644 --- a/services/121-service/src/registration/modules/queue-registrations-update/queue-registrations-update.service.ts +++ b/services/121-service/src/registration/modules/queue-registrations-update/queue-registrations-update.service.ts @@ -16,9 +16,16 @@ export class QueueRegistrationUpdateService { public async addRegistrationUpdateToQueue( job: RegistrationsUpdateJobDto, ): Promise { + // UsedId has to be defined, else there would have been an auth error + if (!this.request.user || !this.request.user.id) { + throw new Error( + 'User information is missing when processing registration update', + ); + } + job.request = { - userId: this.request.user?.id, - scope: this.request.user?.scope, + userId: this.request.user.id, + scope: this.request.user.scope, }; await this.queueRegistryService.updateRegistrationQueue.add( ProcessNameRegistration.update, diff --git a/services/121-service/src/registration/modules/registration-data/registration-data.module.ts b/services/121-service/src/registration/modules/registration-data/registration-data.module.ts index 596fd46e56..c47b873f8e 100644 --- a/services/121-service/src/registration/modules/registration-data/registration-data.module.ts +++ b/services/121-service/src/registration/modules/registration-data/registration-data.module.ts @@ -5,7 +5,7 @@ import { OrganizationEntity } from '@121-service/src/organization/organization.e import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; import { RegistrationDataScopedRepository } from '@121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { RegistrationScopedRepository } from '@121-service/src/registration/repositories/registration-scoped.repository'; import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/createScopedRepositoryProvider.helper'; @@ -13,7 +13,7 @@ import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/cre imports: [TypeOrmModule.forFeature([ProgramEntity, OrganizationEntity])], providers: [ RegistrationDataService, - createScopedRepositoryProvider(RegistrationDataEntity), + createScopedRepositoryProvider(RegistrationAttributeDataEntity), RegistrationScopedRepository, RegistrationDataScopedRepository, ], diff --git a/services/121-service/src/registration/modules/registration-data/registration-data.service.ts b/services/121-service/src/registration/modules/registration-data/registration-data.service.ts index 8babf9ebef..ccc5e5265c 100644 --- a/services/121-service/src/registration/modules/registration-data/registration-data.service.ts +++ b/services/121-service/src/registration/modules/registration-data/registration-data.service.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Brackets, Equal, Repository, SelectQueryBuilder } from 'typeorm'; +import { Equal, Repository, SelectQueryBuilder } from 'typeorm'; -import { AppDataSource } from '@121-service/src/appdatasource'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { RegistrationDataByNameDto } from '@121-service/src/registration/dto/registration-data-by-name.dto'; import { @@ -12,10 +11,11 @@ import { import { RegistrationDataError } from '@121-service/src/registration/errors/registration-data.error'; import { RegistrationDataScopedRepository } from '@121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; import { RegistrationScopedRepository } from '@121-service/src/registration/repositories/registration-scoped.repository'; +// ## TODO see what be refactored in this service to live in a repository file @Injectable() export class RegistrationDataService { @InjectRepository(ProgramEntity) @@ -59,32 +59,16 @@ export class RegistrationDataService { private getRegistrationDataQuery( registration: RegistrationEntity, name: string, - ): SelectQueryBuilder { + ): SelectQueryBuilder { return this.registrationDataScopedRepository .createQueryBuilder('registrationData') .leftJoin('registrationData.registration', 'registration') - .leftJoin('registrationData.programQuestion', 'programQuestion') - .leftJoin('registrationData.fspQuestion', 'fspQuestion') .leftJoin( - 'registrationData.programCustomAttribute', - 'programCustomAttribute', + 'registrationData.programRegistrationAttribute', + 'programRegistrationAttribute', ) .andWhere('registration.id = :id', { id: registration.id }) - .andWhere( - new Brackets((qb) => { - qb.andWhere(`programQuestion.name = :name`, { name }) - .orWhere( - `(fspQuestion.name = :name AND "fspQuestion"."fspId" = :fsp)`, - { - name, - fsp: registration.fspId, - }, - ) - .orWhere(`programCustomAttribute.name = :name`, { - name, - }); - }), - ); + .andWhere(`programRegistrationAttribute.name = :name`, { name }); } public async getRegistrationDataByName( @@ -92,14 +76,11 @@ export class RegistrationDataService { name: string, ): Promise { const query = this.getRegistrationDataQuery(registration, name); - const queryWithSelect = query.select( - `CASE - WHEN ("programQuestion"."name" is not NULL) THEN "programQuestion"."name" - WHEN ("fspQuestion"."name" is not NULL) THEN "fspQuestion"."name" - WHEN ("programCustomAttribute"."name" is not NULL) THEN "programCustomAttribute"."name" - END as name, - value, "registrationData".id`, - ); + const queryWithSelect = query.select([ + 'programRegistrationAttribute.name as name', + 'registrationData.value as value', + 'registrationData.id as id', + ]); const result = queryWithSelect.getRawOne(); return result; } @@ -107,7 +88,7 @@ export class RegistrationDataService { public async getRegistrationDataEntityByName( registration: RegistrationEntity, name: string, - ): Promise { + ): Promise { const query = this.getRegistrationDataQuery(registration, name); return query.getOne(); } @@ -119,49 +100,25 @@ export class RegistrationDataService { const result = new RegistrationDataRelation(); const query = this.programRepository .createQueryBuilder('program') - .leftJoin('program.programQuestions', 'programQuestion') + .leftJoin( + 'program.programRegistrationAttributes', + 'programRegistrationAttributes', + ) .andWhere('program.id = :programId', { programId: registration.programId, }) - .andWhere('programQuestion.name = :name', { name }) - .select('"programQuestion".id', 'id'); + .andWhere('programRegistrationAttributes.name = :name', { name }) + .select('"programRegistrationAttributes".id', 'id'); - const resultProgramQuestion = await query.getRawOne(); + const resultProgramRegistrationAttribute = await query.getRawOne(); - if (resultProgramQuestion) { - result.programQuestionId = resultProgramQuestion.id; + if (resultProgramRegistrationAttribute) { + result.programRegistrationAttributeId = + resultProgramRegistrationAttribute.id; return result; } - const resultFspQuestion = await this.registrationScopedRepository - .createQueryBuilder('registration') - .leftJoin('registration.fsp', 'fsp') - .leftJoin('fsp.questions', 'question') - .andWhere('registration.id = :registration', { - registration: registration.id, - }) - .andWhere('question.name = :name', { name }) - .andWhere('question."fspId" = fsp.id') - .select('"question".id', 'id') - .getRawOne(); - if (resultFspQuestion) { - result.fspQuestionId = resultFspQuestion.id; - return result; - } - const resultProgramCustomAttribute = await this.programRepository - .createQueryBuilder('program') - .leftJoin('program.programCustomAttributes', 'programCustomAttribute') - .andWhere('program.id = :programId', { - programId: registration.programId, - }) - .andWhere('programCustomAttribute.name = :name', { name }) - .select('"programCustomAttribute".id', 'id') - .getRawOne(); - if (resultProgramCustomAttribute) { - result.programCustomAttributeId = resultProgramCustomAttribute.id; - return result; - } - const errorMessage = `Cannot find registration data, name: '${name}' not found (In program questions, fsp questions, and program custom attributes)`; + const errorMessage = `Cannot find registration data, name: '${name}' not found (In program registration attributes)`; throw new RegistrationDataError(errorMessage); } @@ -200,25 +157,11 @@ export class RegistrationDataService { ): Promise { value = value === undefined || value === null ? '' : String(value); - if (relation.programQuestionId) { - await this.saveProgramQuestionData( + if (relation.programRegistrationAttributeId) { + await this.saveProgramRegistrationAttributeData( registration, value, - relation.programQuestionId, - ); - } - if (relation.fspQuestionId) { - await this.saveFspQuestionData( - registration, - value, - relation.fspQuestionId, - ); - } - if (relation.programCustomAttributeId) { - await this.saveProgramCustomAttributeData( - registration, - value, - relation.programCustomAttributeId, + relation.programRegistrationAttributeId, ); } } @@ -228,30 +171,16 @@ export class RegistrationDataService { value: string[], relation: RegistrationDataRelation, ): Promise { - if (relation.programQuestionId) { - await this.saveProgramQuestionDataMultiSelect( - registration, - value, - relation.programQuestionId, - ); - } - if (relation.fspQuestionId) { - await this.saveFspQuestionDataMultiSelect( + if (relation.programRegistrationAttributeId) { + await this.saveProgramRegistrationAttributeDataMultiSelect( registration, value, - relation.fspQuestionId, - ); - } - if (relation.programCustomAttributeId) { - await this.saveProgramCustomAttributeDataMultiSelect( - registration, - value, - relation.programCustomAttributeId, + relation.programRegistrationAttributeId, ); } } - private async saveProgramQuestionData( + private async saveProgramRegistrationAttributeData( registration: RegistrationEntity | RegistrationViewEntity, value: string, id: number, @@ -259,131 +188,62 @@ export class RegistrationDataService { const existingEntry = await this.registrationDataScopedRepository .createQueryBuilder('registrationData') .andWhere('"registrationId" = :regId', { regId: registration.id }) - .leftJoin('registrationData.programQuestion', 'programQuestion') - .andWhere('programQuestion.id = :id', { id }) + .leftJoin( + 'registrationData.programRegistrationAttribute', + 'programRegistrationAttribute', + ) + .andWhere('programRegistrationAttribute.id = :id', { id }) .getOne(); if (existingEntry) { existingEntry.value = value; await this.registrationDataScopedRepository.save(existingEntry); } else { - const newRegistrationData = new RegistrationDataEntity(); + const newRegistrationData = new RegistrationAttributeDataEntity(); newRegistrationData.registrationId = registration.id; newRegistrationData.value = value; - newRegistrationData.programQuestionId = id; + newRegistrationData.programRegistrationAttributeId = id; await this.registrationDataScopedRepository.save(newRegistrationData); } } - private async saveProgramQuestionDataMultiSelect( - registration: RegistrationEntity | RegistrationViewEntity, - values: string[], - id: number, - ): Promise { - const repoRegistrationData = AppDataSource.getRepository( - RegistrationDataEntity, - ); - - await repoRegistrationData.delete({ - registration: { id: registration.id }, - programQuestion: { id }, - }); - - for await (const value of values) { - const newRegistrationData = new RegistrationDataEntity(); - newRegistrationData.registrationId = registration.id; - newRegistrationData.value = value; - newRegistrationData.programQuestionId = id; - await repoRegistrationData.save(newRegistrationData); - } - } - - private async saveFspQuestionData( - registration: RegistrationEntity | RegistrationViewEntity, - value: string, - id: number, - ): Promise { - const repoRegistrationData = AppDataSource.getRepository( - RegistrationDataEntity, - ); - const existingEntry = await repoRegistrationData - .createQueryBuilder('registrationData') - .andWhere('"registrationId" = :regId', { regId: registration.id }) - .leftJoin('registrationData.fspQuestion', 'fspQuestion') - .andWhere('fspQuestion.id = :id', { id }) - .getOne(); - if (existingEntry) { - existingEntry.value = value; - await repoRegistrationData.save(existingEntry); - } else { - const newRegistrationData = new RegistrationDataEntity(); - newRegistrationData.registrationId = registration.id; - newRegistrationData.value = value; - newRegistrationData.fspQuestionId = id; - await repoRegistrationData.save(newRegistrationData); - } - } - - private async saveFspQuestionDataMultiSelect( + private async saveProgramRegistrationAttributeDataMultiSelect( registration: RegistrationEntity | RegistrationViewEntity, values: string[], id: number, ): Promise { await this.registrationDataScopedRepository.deleteUnscoped({ registration: { id: registration.id }, - fspQuestion: { id }, + programRegistrationAttribute: { id }, }); for await (const value of values) { - const newRegistrationData = new RegistrationDataEntity(); + const newRegistrationData = new RegistrationAttributeDataEntity(); newRegistrationData.registrationId = registration.id; newRegistrationData.value = value; - newRegistrationData.fspQuestionId = id; + newRegistrationData.programRegistrationAttributeId = id; await this.registrationDataScopedRepository.save(newRegistrationData); } } - private async saveProgramCustomAttributeData( - registration: RegistrationEntity | RegistrationViewEntity, - value: string, - id: number, - ): Promise { - const existingEntry = await this.registrationDataScopedRepository - .createQueryBuilder('registrationData') - .andWhere('"registrationId" = :regId', { regId: registration.id }) - .leftJoin( - 'registrationData.programCustomAttribute', - 'programCustomAttribute', - ) - .andWhere('programCustomAttribute.id = :id', { id }) - .getOne(); - if (existingEntry) { - existingEntry.value = value; - await this.registrationDataScopedRepository.save(existingEntry); - } else { - const newRegistrationData = new RegistrationDataEntity(); - newRegistrationData.registrationId = registration.id; - newRegistrationData.value = value; - newRegistrationData.programCustomAttributeId = id; - await this.registrationDataScopedRepository.save(newRegistrationData); + public async deleteProgramRegistrationAttributeData( + registration: RegistrationEntity, + options: RegistrationDataOptions, + ) { + let { relation } = options; + if (!relation && !options.name) { + const errors = `Cannot delete registration data, need either a dataRelation or a name`; + throw new Error(errors); } - } - - private async saveProgramCustomAttributeDataMultiSelect( - registration: RegistrationEntity | RegistrationViewEntity, - values: string[], - id: number, - ): Promise { - await this.registrationDataScopedRepository.deleteUnscoped({ - registration: { id: registration.id }, - programCustomAttribute: { id }, - }); - - for await (const value of values) { - const newRegistrationData = new RegistrationDataEntity(); - newRegistrationData.registrationId = registration.id; - newRegistrationData.value = value; - newRegistrationData.programCustomAttributeId = id; - await this.registrationDataScopedRepository.save(newRegistrationData); + if (!relation) { + relation = await this.getRelationForName(registration, options.name!); + } + if (relation.programRegistrationAttributeId) { + await this.registrationDataScopedRepository.deleteUnscoped({ + registrationId: Equal(registration.id), + programRegistrationAttribute: { + id: relation.programRegistrationAttributeId, + }, + }); } } } diff --git a/services/121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository.ts b/services/121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository.ts index e7aed3b0e3..72915719c3 100644 --- a/services/121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository.ts +++ b/services/121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository.ts @@ -1,19 +1,19 @@ import { Inject } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; +import { SelectQueryBuilder } from 'typeorm'; import { RegistrationDataByNameDto } from '@121-service/src/registration/dto/registration-data-by-name.dto'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { ScopedRepository } from '@121-service/src/scoped.repository'; import { ScopedUserRequest } from '@121-service/src/shared/scoped-user-request'; import { getScopedRepositoryProviderName } from '@121-service/src/utils/scope/createScopedRepositoryProvider.helper'; -export class RegistrationDataScopedRepository extends ScopedRepository { +export class RegistrationDataScopedRepository extends ScopedRepository { constructor( @Inject(REQUEST) request: ScopedUserRequest, - @Inject(getScopedRepositoryProviderName(RegistrationDataEntity)) - scopedRepository: ScopedRepository, + @Inject(getScopedRepositoryProviderName(RegistrationAttributeDataEntity)) + scopedRepository: ScopedRepository, ) { super(request, scopedRepository); } @@ -24,11 +24,7 @@ export class RegistrationDataScopedRepository extends ScopedRepository { const query = this.getRegistrationDataArrayQuery(registration, names); const queryWithSelect = query.select( - `CASE - WHEN ("programQuestion"."name" is not NULL) THEN "programQuestion"."name" - WHEN ("fspQuestion"."name" is not NULL) THEN "fspQuestion"."name" - WHEN ("programCustomAttribute"."name" is not NULL) THEN "programCustomAttribute"."name" - END as name, + ` "programRegistrationAttribute"."name" as name, value, "registrationData".id`, ); const result = await queryWithSelect.getRawMany(); @@ -38,30 +34,14 @@ export class RegistrationDataScopedRepository extends ScopedRepository { + ): SelectQueryBuilder { return this.createQueryBuilder('registrationData') .leftJoin('registrationData.registration', 'registration') - .leftJoin('registrationData.programQuestion', 'programQuestion') - .leftJoin('registrationData.fspQuestion', 'fspQuestion') .leftJoin( - 'registrationData.programCustomAttribute', - 'programCustomAttribute', + 'registrationData.programRegistrationAttribute', + 'programRegistrationAttribute', ) .andWhere('registration.id = :id', { id: registration.id }) - .andWhere( - new Brackets((qb) => { - qb.andWhere(`programQuestion.name IN (:...names)`, { names }) - .orWhere( - `(fspQuestion.name IN (:...names) AND "fspQuestion"."fspId" = :fsp)`, - { - names, - fsp: registration.fspId, - }, - ) - .orWhere(`programCustomAttribute.name IN (:...names)`, { - names, - }); - }), - ); + .andWhere(`programRegistrationAttribute.name IN (:...names)`, { names }); } } diff --git a/services/121-service/src/registration/processsors/registrations-update.processor.ts b/services/121-service/src/registration/processsors/registrations-update.processor.ts index 08db3ea9b9..adc7e40e23 100644 --- a/services/121-service/src/registration/processsors/registrations-update.processor.ts +++ b/services/121-service/src/registration/processsors/registrations-update.processor.ts @@ -23,10 +23,11 @@ export class RegistrationUpdateProcessor { data: jobData.data, reason: jobData.reason, }; - await this.registrationsService.updateRegistration( - jobData.programId, - jobData.referenceId, - dto, - ); + await this.registrationsService.validateInputAndUpdateRegistration({ + programId: jobData.programId, + referenceId: jobData.referenceId, + updateRegistrationDto: dto, + userId: jobData.request.userId, + }); } } diff --git a/services/121-service/src/registration/registration-attribute-data.entity.ts b/services/121-service/src/registration/registration-attribute-data.entity.ts new file mode 100644 index 0000000000..c71475f83b --- /dev/null +++ b/services/121-service/src/registration/registration-attribute-data.entity.ts @@ -0,0 +1,54 @@ +import { + Column, + Entity, + Equal, + Index, + JoinColumn, + ManyToOne, + Relation, + Unique, +} from 'typeorm'; + +import { AppDataSource } from '@121-service/src/appdatasource'; +import { Base121Entity } from '@121-service/src/base.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; +import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; + +@Unique('registrationProgramAttributeUnique', [ + 'registrationId', + 'programRegistrationAttributeId', +]) +@Entity('registration_attribute_data') +export class RegistrationAttributeDataEntity extends Base121Entity { + @ManyToOne((_type) => RegistrationEntity, (registration) => registration.data) + @JoinColumn({ name: 'registrationId' }) + public registration: Relation; + @Index() + @Column() + public registrationId: number; + + @ManyToOne( + (_type) => ProgramRegistrationAttributeEntity, + (programRegistrationAttribute) => + programRegistrationAttribute.registrationAttributeData, + ) + @JoinColumn({ name: 'programRegistrationAttributeId' }) + public programRegistrationAttribute: Relation; + @Column({ type: 'integer' }) + public programRegistrationAttributeId: number; + + @Index() + @Column() + public value: string; + + public async getDataName(): Promise { + const repo = AppDataSource.getRepository(RegistrationAttributeDataEntity); + const dataWithRelations = await repo.findOneOrFail({ + where: { id: Equal(this.id) }, + relations: ['programRegistrationAttribute'], + }); + if (dataWithRelations.programRegistrationAttribute) { + return dataWithRelations.programRegistrationAttribute.name; + } + } +} diff --git a/services/121-service/src/registration/registration-data.entity.ts b/services/121-service/src/registration/registration-data.entity.ts deleted file mode 100644 index 64a15e2d06..0000000000 --- a/services/121-service/src/registration/registration-data.entity.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - Column, - Entity, - Equal, - Index, - JoinColumn, - ManyToOne, - Relation, - Unique, -} from 'typeorm'; - -import { AppDataSource } from '@121-service/src/appdatasource'; -import { Base121Entity } from '@121-service/src/base.entity'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; - -@Unique('registrationProgramQuestionUnique', [ - 'registrationId', - 'programQuestionId', - 'value', -]) -@Unique('registrationFspQuestionUnique', [ - 'registrationId', - 'fspQuestionId', - 'value', -]) -@Unique('registrationProgramCustomAttributeUnique', [ - 'registrationId', - 'programCustomAttributeId', -]) -@Entity('registration_data') -export class RegistrationDataEntity extends Base121Entity { - @ManyToOne((_type) => RegistrationEntity, (registration) => registration.data) - @JoinColumn({ name: 'registrationId' }) - public registration: Relation; - @Index() - @Column() - public registrationId: number; - - @ManyToOne( - (_type) => ProgramQuestionEntity, - (programQuestion) => programQuestion.registrationData, - ) - @JoinColumn({ name: 'programQuestionId' }) - public programQuestion: ProgramQuestionEntity; - @Column({ type: 'integer', nullable: true }) - public programQuestionId: number | null; - - @ManyToOne( - (_type) => FspQuestionEntity, - (fspQuestion) => fspQuestion.registrationData, - ) - @JoinColumn({ name: 'fspQuestionId' }) - public fspQuestion: Relation; - @Column({ type: 'integer', nullable: true }) - public fspQuestionId: number | null; - - @ManyToOne( - (_type) => ProgramCustomAttributeEntity, - (programCustomAttribute) => programCustomAttribute.registrationData, - ) - @JoinColumn({ name: 'programCustomAttributeId' }) - public programCustomAttribute: ProgramCustomAttributeEntity; - @Column({ type: 'integer', nullable: true }) - public programCustomAttributeId: number | null; - - @Index() - @Column() - public value: string; - - public async getDataName(): Promise { - const repo = AppDataSource.getRepository(RegistrationDataEntity); - const dataWithRelations = await repo.findOneOrFail({ - where: { id: Equal(this.id) }, - relations: ['programQuestion', 'fspQuestion', 'programCustomAttribute'], - }); - if (dataWithRelations.programQuestion) { - return dataWithRelations.programQuestion.name; - } - if (dataWithRelations.fspQuestion) { - return dataWithRelations.fspQuestion.name; - } - if (dataWithRelations.programCustomAttribute) { - return dataWithRelations.programCustomAttribute.name; - } - } -} diff --git a/services/121-service/src/registration/registration-view.entity.ts b/services/121-service/src/registration/registration-view.entity.ts index 5688e09bb3..8aa418d02e 100644 --- a/services/121-service/src/registration/registration-view.entity.ts +++ b/services/121-service/src/registration/registration-view.entity.ts @@ -14,7 +14,7 @@ import { LatestTransactionEntity } from '@121-service/src/payments/transactions/ import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; @@ -44,8 +44,22 @@ import { LocalizedString } from '@121-service/src/shared/types/localized-string. .addSelect('registration.programId', 'programId') .addSelect('registration.preferredLanguage', 'preferredLanguage') .addSelect('registration.inclusionScore', 'inclusionScore') - .addSelect('fsp.fsp', 'financialServiceProvider') - .addSelect('fsp.displayName', 'fspDisplayName') + .addSelect( + 'fspconfig."name"', + 'programFinancialServiceProviderConfigurationName', + ) + .addSelect( + 'fspconfig."id"', + 'programFinancialServiceProviderConfigurationId', + ) + .addSelect( + 'fspconfig."financialServiceProviderName"', + 'financialServiceProviderName', + ) + .addSelect( + 'fspconfig.label', + 'programFinancialServiceProviderConfigurationLabel', + ) .addSelect('registration.paymentCount', 'paymentCount') .addSelect( 'registration.maxPayments - registration.paymentCount', @@ -58,7 +72,10 @@ import { LocalizedString } from '@121-service/src/shared/types/localized-string. .addSelect('registration.maxPayments', 'maxPayments') .addSelect('registration.phoneNumber', 'phoneNumber') .addSelect('registration.scope', 'scope') - .leftJoin('registration.fsp', 'fsp') + .leftJoin( + 'registration.programFinancialServiceProviderConfiguration', + 'fspconfig', + ) .leftJoin('registration.latestMessage', 'latestMessage') .leftJoin('latestMessage.message', 'message') .addSelect( @@ -102,10 +119,16 @@ export class RegistrationViewEntity { public paymentAmountMultiplier: number; @ViewColumn() - public financialServiceProvider?: FinancialServiceProviders; + public financialServiceProviderName?: FinancialServiceProviders; + + @ViewColumn() + public programFinancialServiceProviderConfigurationId: number; + + @ViewColumn() + public programFinancialServiceProviderConfigurationName: string; @ViewColumn() - public fspDisplayName: LocalizedString; + public programFinancialServiceProviderConfigurationLabel: LocalizedString; /** This is an "auto" incrementing field with a registration ID per program. */ @ViewColumn() @@ -130,16 +153,16 @@ export class RegistrationViewEntity { public scope: string; @OneToMany( - () => RegistrationDataEntity, + () => RegistrationAttributeDataEntity, (registrationData) => registrationData.registration, ) - public data: RegistrationDataEntity[]; + public data: RegistrationAttributeDataEntity[]; @OneToMany( - () => RegistrationDataEntity, + () => RegistrationAttributeDataEntity, (registrationData) => registrationData.registration, ) - public dataSearchBy: RegistrationDataEntity[]; + public dataSearchBy: RegistrationAttributeDataEntity[]; @OneToMany( () => LatestTransactionEntity, diff --git a/services/121-service/src/registration/registration.entity.ts b/services/121-service/src/registration/registration.entity.ts index afb9c7a84d..4bac2c5d5f 100644 --- a/services/121-service/src/registration/registration.entity.ts +++ b/services/121-service/src/registration/registration.entity.ts @@ -22,7 +22,6 @@ import { import { CascadeDeleteEntity } from '@121-service/src/base.entity'; import { EventEntity } from '@121-service/src/events/entities/event.entity'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; import { NoteEntity } from '@121-service/src/notes/note.entity'; import { LatestMessageEntity } from '@121-service/src/notifications/latest-message.entity'; import { TwilioMessageEntity } from '@121-service/src/notifications/twilio.entity'; @@ -33,9 +32,10 @@ import { IntersolveVisaCustomerEntity } from '@121-service/src/payments/fsp-inte import { ImageCodeExportVouchersEntity } from '@121-service/src/payments/imagecode/image-code-export-vouchers.entity'; import { LatestTransactionEntity } from '@121-service/src/payments/transactions/latest-transaction.entity'; import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { ReferenceIdConstraints } from '@121-service/src/shared/const'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { UserEntity } from '@121-service/src/user/user.entity'; @@ -62,8 +62,8 @@ export class RegistrationEntity extends CascadeDeleteEntity { @Column() public referenceId: string; - @OneToMany(() => RegistrationDataEntity, (data) => data.registration) - public data: Relation; + @OneToMany(() => RegistrationAttributeDataEntity, (data) => data.registration) + public data: Relation; @Column({ type: 'character varying', nullable: true }) public phoneNumber: string | null; @@ -76,11 +76,13 @@ export class RegistrationEntity extends CascadeDeleteEntity { @Column({ type: 'integer', nullable: true }) public inclusionScore: number | null; - @ManyToOne((_type) => FinancialServiceProviderEntity) - @JoinColumn({ name: 'fspId' }) - public fsp: FinancialServiceProviderEntity; - @Column({ type: 'integer', nullable: true }) - public fspId: number | null; + @ManyToOne((_type) => ProgramFinancialServiceProviderConfigurationEntity) + @JoinColumn({ + name: 'programFinancialServiceProviderConfigurationId', + }) + public programFinancialServiceProviderConfiguration: ProgramFinancialServiceProviderConfigurationEntity; + @Column({ type: 'integer', nullable: false }) + public programFinancialServiceProviderConfigurationId: number; @Column({ nullable: false, default: 1 }) @IsInt() @@ -193,7 +195,7 @@ export class RegistrationEntity extends CascadeDeleteEntity { columnName: 'registration', }, { - entityClass: RegistrationDataEntity, + entityClass: RegistrationAttributeDataEntity, columnName: 'registration', }, { diff --git a/services/121-service/src/registration/registrations.controller.ts b/services/121-service/src/registration/registrations.controller.ts index 50ee182a68..3c5f50a615 100644 --- a/services/121-service/src/registration/registrations.controller.ts +++ b/services/121-service/src/registration/registrations.controller.ts @@ -12,7 +12,6 @@ import { ParseIntPipe, Patch, Post, - Put, Query, Req, UploadedFile, @@ -50,8 +49,8 @@ import { MessageHistoryDto } from '@121-service/src/registration/dto/message-his import { ReferenceIdDto } from '@121-service/src/registration/dto/reference-id.dto'; import { RegistrationStatusPatchDto } from '@121-service/src/registration/dto/registration-status-patch.dto'; import { SendCustomTextDto } from '@121-service/src/registration/dto/send-custom-text.dto'; -import { UpdateChosenFspDto } from '@121-service/src/registration/dto/set-fsp.dto'; import { UpdateRegistrationDto } from '@121-service/src/registration/dto/update-registration.dto'; +import { GenericRegistrationAttributes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; @@ -84,19 +83,18 @@ export class RegistrationsController { summary: 'Import set of registered PAs, from CSV', }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @Post('programs/:programId/registrations/import-registrations') + @Post('programs/:programId/registrations/import') @ApiConsumes('multipart/form-data') @ApiBody(FILE_UPLOAD_API_FORMAT) @UseInterceptors(FileInterceptor('file')) - public async importRegistrations( - @UploadedFile() csvFile: Blob, + public async importRegistrationsFromCsv( + @UploadedFile() csvFile: Express.Multer.File, @Param('programId', ParseIntPipe) programId: number, @Req() req: ScopedUserRequest, ): Promise { const userId = RequestHelper.getUserId(req); - - return await this.registrationsService.importRegistrations( + return await this.registrationsService.importRegistrationsFromCsv( csvFile, programId, userId, @@ -112,7 +110,7 @@ export class RegistrationsController { }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) @ApiBody({ isArray: true, type: ImportRegistrationsDto }) - @Post('programs/:programId/registrations/import') + @Post('programs/:programId/registrations') public async importRegistrationsJSON( @Body(new ParseArrayPipe({ items: ImportRegistrationsDto })) data: ImportRegistrationsDto[], @@ -121,8 +119,8 @@ export class RegistrationsController { @Req() req: ScopedUserRequest, ): Promise { const userId = RequestHelper.getUserId(req); - return await this.registrationsService.importRegistrationFromJson( - data, + return await this.registrationsService.importRegistrationsFromJson( + data as unknown as Record[], programId, userId, ); @@ -132,7 +130,7 @@ export class RegistrationsController { @AuthenticatedUser({ permissions: [PermissionEnum.RegistrationREAD] }) @ApiOperation({ summary: - '[SCOPED] Get paginated registrations. Below you will find all the default paginate options, including filtering on any generic fields. NOTE: additionally you can filter on program-specific fields, like program questions, fsp questions, and custom attributes, even though not specified in the Swagger Docs.', + '[SCOPED] Get paginated registrations. Below you will find all the default paginate options, including filtering on any generic fields. NOTE: additionally you can filter on program registration attributes, even though not specified in the Swagger Docs.', }) @ApiParam({ name: 'programId', @@ -183,7 +181,7 @@ export class RegistrationsController { @ApiBody(FILE_UPLOAD_WITH_REASON_API_FORMAT) @UseInterceptors(FileInterceptor('file')) public async patchRegistrations( - @UploadedFile() csvFile: Blob, + @UploadedFile() csvFile: Express.Multer.File, @Body('reason') reason: string, @Param('programId', ParseIntPipe) programId: number, @Req() req: ScopedUserRequest, @@ -206,7 +204,7 @@ export class RegistrationsController { summary: 'Get a CSV template for importing registrations', }) @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @Get('programs/:programId/registrations/import-template') + @Get('programs/:programId/registrations/import/template') public async getImportRegistrationsTemplate( @Param('programId', ParseIntPipe) programId: number, ): Promise { @@ -362,12 +360,12 @@ export class RegistrationsController { public async updateRegistration( @Param('programId', new ParseIntPipe()) programId: number, @Param('referenceId') referenceId: string, - @Body() updateRegistrationDataDto: UpdateRegistrationDto, + @Body() updateRegistrationDto: UpdateRegistrationDto, @Req() req: ScopedUserRequest, ) { const userId = RequestHelper.getUserId(req); - const hasRegistrationUpdatePermission = + const hasUpdateRegistrationPermission = await this.registrationsPaginateService.userHasPermissionForProgram( userId, programId, @@ -379,54 +377,54 @@ export class RegistrationsController { programId, PermissionEnum.RegistrationAttributeFinancialUPDATE, ); + const hasUpdateFspConfigPermission = + await this.registrationsPaginateService.userHasPermissionForProgram( + userId, + programId, + PermissionEnum.RegistrationFspConfigUPDATE, + ); - if (!hasRegistrationUpdatePermission && !hasUpdateFinancialPermission) { + if ( + !hasUpdateRegistrationPermission && + !hasUpdateFinancialPermission && + !hasUpdateFspConfigPermission + ) { const errors = `User does not have permission to update attributes`; throw new HttpException({ errors }, HttpStatus.FORBIDDEN); } - const partialRegistration = updateRegistrationDataDto.data; + const partialRegistration = updateRegistrationDto.data; - if (!hasUpdateFinancialPermission && hasRegistrationUpdatePermission) { - for (const attributeKey of Object.keys(partialRegistration)) { - if ( - FinancialAttributes.includes(attributeKey as keyof RegistrationEntity) - ) { + for (const attributeKey of Object.keys(partialRegistration)) { + if ( + FinancialAttributes.includes(attributeKey as keyof RegistrationEntity) + ) { + if (!hasUpdateFinancialPermission) { const errors = `User does not have permission to update financial attributes`; throw new HttpException({ errors }, HttpStatus.FORBIDDEN); } - } - } - - if (hasUpdateFinancialPermission && !hasRegistrationUpdatePermission) { - for (const attributeKey of Object.keys(partialRegistration)) { - if ( - !FinancialAttributes.includes( - attributeKey as keyof RegistrationEntity, - ) - ) { + } else if ( + attributeKey === + GenericRegistrationAttributes.programFinancialServiceProviderConfigurationName + ) { + if (!hasUpdateFspConfigPermission) { + const errors = `User does not have permission to update chosen program financial service provider configuration`; + throw new HttpException({ errors }, HttpStatus.FORBIDDEN); + } + } else { + if (!hasUpdateRegistrationPermission) { const errors = `User does not have permission to update attributes`; throw new HttpException({ errors }, HttpStatus.FORBIDDEN); } } } - // first validate all attributes and return error if any - for (const attributeKey of Object.keys(partialRegistration)) { - await this.registrationsService.validateAttribute( - referenceId, - attributeKey, - partialRegistration[attributeKey], - userId, - ); - } - - // if all valid, process update - return await this.registrationsService.updateRegistration( + return await this.registrationsService.validateInputAndUpdateRegistration({ programId, referenceId, - updateRegistrationDataDto, - ); + updateRegistrationDto, + userId, + }); } @AuthenticatedUser() @@ -465,35 +463,6 @@ export class RegistrationsController { ); } - @ApiTags('programs/registrations') - @AuthenticatedUser({ permissions: [PermissionEnum.RegistrationFspUPDATE] }) - @ApiOperation({ - summary: - '[SCOPED] [EXTERNALLY USED] Update chosen FSP and attributes. This will delete any custom data field related to the old FSP!', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: - 'Updated fsp and attributes - NOTE: this endpoint is scoped, depending on program configuration it only returns/modifies data the logged in user has access to.', - }) - @ApiParam({ name: 'referenceId', required: true, type: 'string' }) - @ApiParam({ name: 'programId', required: true, type: 'integer' }) - @Put('programs/:programId/registrations/:referenceId/fsp') - public async updateChosenFsp( - @Param() params: { referenceId: string; programId: number }, - @Body() data: UpdateChosenFspDto, - @Req() req: ScopedUserRequest, - ) { - const userId = RequestHelper.getUserId(req); - - return await this.registrationsService.updateChosenFsp({ - referenceId: params.referenceId, - newFspName: data.newFspName, - newFspAttributesRaw: data.newFspAttributes, - userId, - }); - } - @ApiTags('programs/registrations') @ApiResponse({ status: HttpStatus.OK, diff --git a/services/121-service/src/registration/registrations.module.ts b/services/121-service/src/registration/registrations.module.ts index e2706f4a60..0a307bbc3c 100644 --- a/services/121-service/src/registration/registrations.module.ts +++ b/services/121-service/src/registration/registrations.module.ts @@ -5,9 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ActionsModule } from '@121-service/src/actions/actions.module'; import { EventEntity } from '@121-service/src/events/entities/event.entity'; import { EventsModule } from '@121-service/src/events/events.module'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; import { FinancialServiceProvidersModule } from '@121-service/src/financial-service-providers/financial-service-provider.module'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; import { NoteEntity } from '@121-service/src/notes/note.entity'; import { LastMessageStatusService } from '@121-service/src/notifications/last-message-status.service'; import { LatestMessageEntity } from '@121-service/src/notifications/latest-message.entity'; @@ -22,15 +20,14 @@ import { IntersolveVoucherEntity } from '@121-service/src/payments/fsp-integrati import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { ProgramFinancialServiceProviderConfigurationsModule } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.module'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { ProgramModule } from '@121-service/src/programs/programs.module'; import { QueueRegistrationUpdateModule } from '@121-service/src/registration/modules/queue-registrations-update/queue-registrations-update.module'; import { RegistrationDataModule } from '@121-service/src/registration/modules/registration-data/registration-data.module'; import { RegistrationUtilsModule } from '@121-service/src/registration/modules/registration-utilts/registration-utils.module'; import { RegistrationUpdateProcessor } from '@121-service/src/registration/processsors/registrations-update.processor'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { RegistrationsController } from '@121-service/src/registration/registrations.controller'; import { RegistrationsService } from '@121-service/src/registration/registrations.service'; import { RegistrationScopedRepository } from '@121-service/src/registration/repositories/registration-scoped.repository'; @@ -51,15 +48,12 @@ import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/cre TypeOrmModule.forFeature([ UserEntity, ProgramEntity, - ProgramQuestionEntity, - FinancialServiceProviderEntity, - FspQuestionEntity, TryWhatsappEntity, - ProgramCustomAttributeEntity, RegistrationEntity, LatestMessageEntity, WhatsappPendingMessageEntity, MessageTemplateEntity, + ProgramRegistrationAttributeEntity, ]), UserModule, HttpModule, @@ -90,7 +84,7 @@ import { createScopedRepositoryProvider } from '@121-service/src/utils/scope/cre RegistrationsInputValidator, createScopedRepositoryProvider(IntersolveVoucherEntity), createScopedRepositoryProvider(TwilioMessageEntity), - createScopedRepositoryProvider(RegistrationDataEntity), + createScopedRepositoryProvider(RegistrationAttributeDataEntity), createScopedRepositoryProvider(NoteEntity), createScopedRepositoryProvider(TransactionEntity), createScopedRepositoryProvider(EventEntity), diff --git a/services/121-service/src/registration/registrations.service.ts b/services/121-service/src/registration/registrations.service.ts index 5cd974ff7b..46b54f33b6 100644 --- a/services/121-service/src/registration/registrations.service.ts +++ b/services/121-service/src/registration/registrations.service.ts @@ -1,15 +1,14 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { plainToClass } from 'class-transformer'; -import { validate } from 'class-validator'; import { Equal, FindOneOptions, Repository } from 'typeorm'; import { EventsService } from '@121-service/src/events/events.service'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderConfigurationEnum } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { FinancialServiceProviderQuestionRepository } from '@121-service/src/financial-service-providers/repositories/financial-service-provider-question.repository'; +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { getFinancialServiceProviderSettingByNameOrThrow } from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; import { MessageContentType } from '@121-service/src/notifications/enum/message-type.enum'; import { ProgramNotificationEnum } from '@121-service/src/notifications/enum/program-notification.enum'; import { LookupService } from '@121-service/src/notifications/lookup/lookup.service'; @@ -23,30 +22,29 @@ import { IntersolveVisaService } from '@121-service/src/payments/fsp-integration import { IntersolveVisaApiError } from '@121-service/src/payments/fsp-integration/intersolve-visa/intersolve-visa-api.error'; import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { - ImportRegistrationsDto, - ImportResult, -} from '@121-service/src/registration/dto/bulk-import.dto'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; +import { ImportResult } from '@121-service/src/registration/dto/bulk-import.dto'; import { CreateRegistrationDto } from '@121-service/src/registration/dto/create-registration.dto'; -import { CustomDataDto } from '@121-service/src/registration/dto/custom-data.dto'; +import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; import { MessageHistoryDto } from '@121-service/src/registration/dto/message-history.dto'; import { ReferenceProgramIdScopeDto } from '@121-service/src/registration/dto/registrationProgramIdScope.dto'; import { AdditionalAttributes, Attributes, - UpdateAttributeDto, UpdateRegistrationDto, } from '@121-service/src/registration/dto/update-registration.dto'; import { - AnswerTypes, - CustomDataAttributes, -} from '@121-service/src/registration/enum/custom-data-attributes'; + DefaultRegistrationDataAttributeNames, + RegistrationAttributeTypes, +} from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum, RegistrationStatusTimestampField, } from '@121-service/src/registration/enum/registration-status.enum'; +import { RegistrationValidationInputType } from '@121-service/src/registration/enum/registration-validation-input-type.enum'; import { ErrorEnum } from '@121-service/src/registration/errors/registration-data.error'; +import { ValidationRegistrationConfig } from '@121-service/src/registration/interfaces/validate-registration-config.interface'; +import { ValidatedRegistrationInput } from '@121-service/src/registration/interfaces/validated-registration-input.interface'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; import { RegistrationDataScopedRepository } from '@121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository'; import { RegistrationUtilsService } from '@121-service/src/registration/modules/registration-utilts/registration-utils.service'; @@ -56,6 +54,7 @@ import { RegistrationViewScopedRepository } from '@121-service/src/registration/ import { InclusionScoreService } from '@121-service/src/registration/services/inclusion-score.service'; import { RegistrationsImportService } from '@121-service/src/registration/services/registrations-import.service'; import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; +import { RegistrationsInputValidator } from '@121-service/src/registration/validators/registrations-input-validator'; import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; import { UserEntity } from '@121-service/src/user/user.entity'; import { UserService } from '@121-service/src/user/user.service'; @@ -67,12 +66,8 @@ export class RegistrationsService { private readonly userRepository: Repository; @InjectRepository(ProgramEntity) private readonly programRepository: Repository; - @InjectRepository(ProgramQuestionEntity) - private readonly programQuestionRepository: Repository; - @InjectRepository(FinancialServiceProviderEntity) - private readonly fspRepository: Repository; - @InjectRepository(FspQuestionEntity) - private readonly fspAttributeRepository: Repository; + @InjectRepository(ProgramRegistrationAttributeEntity) + private readonly programRegistrationAttributeRepository: Repository; public constructor( private readonly lookupService: LookupService, @@ -87,9 +82,9 @@ export class RegistrationsService { private readonly registrationScopedRepository: RegistrationScopedRepository, private readonly eventsService: EventsService, private readonly registrationViewScopedRepository: RegistrationViewScopedRepository, - private readonly financialServiceProviderQuestionRepository: FinancialServiceProviderQuestionRepository, private readonly programFinancialServiceProviderConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, private readonly registrationDataScopedRepository: RegistrationDataScopedRepository, + private readonly registrationsInputValidator: RegistrationsInputValidator, ) {} // This methods can be used to get the same formattted data as the pagination query using referenceId @@ -253,7 +248,7 @@ export class RegistrationsService { programId, }: { referenceId: string; - relations?: string[]; + relations?: (keyof RegistrationEntity)[]; programId?: number; }): Promise { if (!referenceId) { @@ -275,61 +270,9 @@ export class RegistrationsService { return registration; } - public async addFsp( - referenceId: string, - fspId: number, - ): Promise { - const registration = await this.getRegistrationOrThrow({ - referenceId, - }); - const fsp = await this.fspRepository.findOneOrFail({ - where: { id: Equal(fspId) }, - relations: ['questions'], - }); - registration.fsp = fsp; - return await this.registrationUtilsService.save(registration); - } - - public async addRegistrationDataBulk( - dataArray: CustomDataDto[], - ): Promise { - const registrations: RegistrationEntity[] = []; - for (const data of dataArray) { - const registration = await this.addRegistrationData( - data.referenceId, - data.key, - data.value, - ); - registrations.push(registration); - } - return registrations; - } - - public async addRegistrationData( - referenceId: string, - customDataKey: string, - customDataValueRaw: string | string[], - ): Promise { - const registration = await this.getRegistrationOrThrow({ - referenceId, - }); - const customDataValue = await this.cleanCustomDataIfPhoneNr( - customDataKey, - customDataValueRaw, - registration.programId, - ); - return await this.registrationDataService.saveData( - registration, - customDataValue, - { - name: customDataKey, - }, - ); - } - public async cleanCustomDataIfPhoneNr( customDataKey: string, - customDataValue: string | number | string[], + customDataValue: string | number | string[] | boolean | null, programId: number, ) { const allowEmptyPhoneNumber = ( @@ -339,16 +282,11 @@ export class RegistrationsService { )?.allowEmptyPhoneNumber; const answersTypeTel: string[] = []; - const fspAttributesTypeTel = await this.fspAttributeRepository.find({ - where: { answerType: Equal(AnswerTypes.tel) }, - }); - for (const fspAttr of fspAttributesTypeTel) { - answersTypeTel.push(fspAttr.name); - } - const programQuestionsTypeTel = await this.programQuestionRepository.find({ - where: { answerType: Equal(AnswerTypes.tel) }, - }); - for (const question of programQuestionsTypeTel) { + const programRegistrationAttributes = + await this.programRegistrationAttributeRepository.find({ + where: { type: Equal(RegistrationAttributeTypes.tel) }, + }); + for (const question of programRegistrationAttributes) { answersTypeTel.push(question.name); } @@ -358,7 +296,7 @@ export class RegistrationsService { if ( !allowEmptyPhoneNumber && - customDataKey === CustomDataAttributes.phoneNumber + customDataKey === DefaultRegistrationDataAttributeNames.phoneNumber ) { // phoneNumber cannot be empty if (!customDataValue) { @@ -387,13 +325,14 @@ export class RegistrationsService { ); } - public async importRegistrations( - csvFile, + public async importRegistrationsFromCsv( + csvFile: Express.Multer.File, programId: number, userId: number, ): Promise { const program = await this.findProgramOrThrow(programId); - return await this.registrationsImportService.importRegistrations( + this.throwIfProgramIsNotPublished(program.published); + return await this.registrationsImportService.importRegistrationsFromCsv( csvFile, program, userId, @@ -401,7 +340,7 @@ export class RegistrationsService { } public async patchBulk( - csvFile: any, + csvFile: Express.Multer.File, programId: number, userId: number, reason: string, @@ -414,34 +353,32 @@ export class RegistrationsService { ); } - public async importRegistrationFromJson( - validatedJsonData: ImportRegistrationsDto[], + public async importRegistrationsFromJson( + jsonData: Record[], programId: number, userId: number, ): Promise { const program = await this.findProgramOrThrow(programId); - if (!program?.published) { + this.throwIfProgramIsNotPublished(program.published); + return await this.registrationsImportService.importRegistrations( + jsonData, + program, + userId, + ); + } + + private throwIfProgramIsNotPublished(published: boolean): void { + if (!published) { const errors = 'Registrations are not allowed for this program yet, try again later.'; throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); } - const validateRegistrationsInput = - await this.registrationsImportService.validateImportAsRegisteredInput( - validatedJsonData, - programId, - userId, - ); - return await this.registrationsImportService.importValidatedRegistrations( - validateRegistrationsInput, - program, - userId, - ); } private async findProgramOrThrow(programId: number): Promise { const program = await this.programRepository.findOne({ where: { id: Equal(programId) }, - relations: ['programCustomAttributes'], + relations: ['programRegistrationAttributes'], }); if (!program) { const errors = 'Program not found.'; @@ -452,8 +389,8 @@ export class RegistrationsService { public transformRegistrationByNamingConvention( nameColumns: string[], - registrationObject: Record, // Allow dynamic key access - ): Record { + registrationObject: MappedPaginatedRegistrationDto, // Allow dynamic key access + ): MappedPaginatedRegistrationDto { const fullnameConcat: string[] = []; // Loop through nameColumns and access properties dynamically @@ -465,7 +402,7 @@ export class RegistrationsService { } // Concatenate the full name and assign to the 'name' property - registrationObject['name'] = fullnameConcat.join(' '); + registrationObject.name = fullnameConcat.join(' '); return registrationObject; // Return the modified object } @@ -491,56 +428,153 @@ export class RegistrationsService { } } - public async updateRegistration( - programId: number, - referenceId: string, - updateRegistrationDto: UpdateRegistrationDto, - ) { + public async validateInputAndUpdateRegistration({ + programId, + referenceId, + updateRegistrationDto, + userId, + }: { + programId: number; + referenceId: string; + updateRegistrationDto: UpdateRegistrationDto; + userId: number; + }): Promise { + const validationConfig: ValidationRegistrationConfig = { + validatePhoneNumberLookup: true, + validateUniqueReferenceId: false, + validateExistingReferenceId: false, + }; + const updateDataWithReferenceId = { + referenceId, + ...updateRegistrationDto.data, + }; + + let validateRegistrationPatchData; + try { + validateRegistrationPatchData = + await this.registrationsInputValidator.validateAndCleanInput({ + registrationInputArray: [updateDataWithReferenceId], + programId, + userId, + typeOfInput: RegistrationValidationInputType.update, + validationConfig, + }); + } catch (error) { + if (error instanceof HttpException) { + this.processHttpExceptionOnRegistrationUpdate(error); + } else { + throw error; + } + } + + // if all valid, process update + return await this.updateRegistration({ + programId, + referenceId, + validatedRegistrationInput: validateRegistrationPatchData[0], + reason: updateRegistrationDto.reason, + }); + } + + // TODO: Refactor this, it works around the fact that registrationsInputValidator throws Http exception with line numbers and columns names + // May be better to solve this in the registrationsInputValidator depending on the type of validation + private processHttpExceptionOnRegistrationUpdate( + error: HttpException, + ): never { + if (error.getStatus() === 400) { + const errorResponse = error.getResponse(); + let errorMessage: object | string = ''; + if (Array.isArray(errorResponse)) { + errorMessage = errorResponse + .map((err) => `${err.column}: ${err.error}`) + .join(', '); + } else { + errorMessage = errorResponse; + } + throw new HttpException(errorMessage, HttpStatus.BAD_REQUEST); + } else { + throw error; // Re-throw the error if it's not an HttpException with status 400 + } + } + + public async updateRegistration({ + programId, + referenceId, + validatedRegistrationInput, + reason, + }: { + programId: number; + referenceId: string; + validatedRegistrationInput: ValidatedRegistrationInput; + reason: string | undefined; + }) { let nrAttributesUpdated = 0; - const { data: partialRegistration } = updateRegistrationDto; + const { data: registrationDataInput, ...partialRegistrationInput } = + validatedRegistrationInput; let registrationToUpdate = await this.getRegistrationOrThrow({ referenceId, - relations: ['program', 'fsp'], + relations: ['program'], programId, }); + const program = registrationToUpdate.program; + const oldViewRegistration = await this.getPaginateRegistrationForReferenceId(referenceId, programId); // Track whether maxPayments has been updated to match paymentCount let maxPaymentsMatchesPaymentCount = false; - for (const attributeKey of Object.keys(partialRegistration)) { - const attributeValue: string | number | string[] = - typeof partialRegistration[attributeKey] === 'boolean' - ? String(partialRegistration[attributeKey]) - : partialRegistration[attributeKey]; + for (const attributeKey of Object.keys(partialRegistrationInput)) { + const attributeValue: string | number | string[] | boolean = + partialRegistrationInput[attributeKey]; const oldValue = oldViewRegistration[attributeKey]; if (String(oldValue) !== String(attributeValue)) { if ( - attributeKey === 'maxPayments' && + attributeKey === AdditionalAttributes.maxPayments && Number(attributeValue) === registrationToUpdate.paymentCount ) { maxPaymentsMatchesPaymentCount = true; } - registrationToUpdate = await this.updateAttribute( - attributeKey, - attributeValue, - registrationToUpdate, - ); + registrationToUpdate = await this.updateAttribute({ + attribute: attributeKey, + value: attributeValue, + registration: registrationToUpdate, + program, + }); + nrAttributesUpdated++; + } + } + + for (const attributeKey of Object.keys(registrationDataInput)) { + const attributeValue: string | number | string[] | boolean | null = + typeof registrationDataInput[attributeKey] === 'boolean' + ? String(registrationDataInput[attributeKey]) + : registrationDataInput[attributeKey]; + + const oldValue = oldViewRegistration[attributeKey]; + + if (String(oldValue) !== String(attributeValue)) { + registrationToUpdate = await this.updateAttribute({ + attribute: attributeKey, + value: attributeValue, + registration: registrationToUpdate, + program, + }); nrAttributesUpdated++; } } if (maxPaymentsMatchesPaymentCount) { - registrationToUpdate = await this.updateAttribute( - 'registrationStatus', - RegistrationStatusEnum.completed, - registrationToUpdate, - ); - nrAttributesUpdated++; // Increment for registrationStatus update + registrationToUpdate = await this.updateAttribute({ + attribute: 'registrationStatus', + value: RegistrationStatusEnum.completed, + registration: registrationToUpdate, + program, + }); + nrAttributesUpdated++; } const newRegistration = await this.getPaginateRegistrationForReferenceId( @@ -554,18 +588,24 @@ export class RegistrationsService { { ...oldViewRegistration }, { ...newRegistration }, { - additionalLogAttributes: { reason: updateRegistrationDto.reason }, + additionalLogAttributes: { reason }, }, ); return newRegistration; } } - private async updateAttribute( - attribute: Attributes | string, - value: string | number | string[], - registration: RegistrationEntity, - ): Promise { + private async updateAttribute({ + attribute, + value, + registration, + program, + }: { + attribute: Attributes | string; + value: string | number | string[] | boolean | null; + registration: RegistrationEntity; + program: ProgramEntity; + }): Promise { value = await this.cleanCustomDataIfPhoneNr( attribute, value, @@ -576,38 +616,52 @@ export class RegistrationsService { registration[attribute] = value; } - // This checks registration attributes (like paymentAmountMultiplier) - const errors = await validate(registration); - if (errors.length > 0) { - throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); - } - if ( !Object.values(AdditionalAttributes).includes( attribute as AdditionalAttributes, ) ) { - try { - await this.registrationDataService.saveData(registration, value, { - name: attribute, - }); - } catch (error) { - // This is an exception because the phoneNumber is in the registration entity, not in the registrationData. - if (attribute === Attributes.phoneNumber) { - registration.phoneNumber = value.toString(); - await this.registrationUtilsService.save(registration); - } else { - if (error.name !== ErrorEnum.RegistrationDataError) { - throw error; + if (value === null) { + await this.registrationDataService.deleteProgramRegistrationAttributeData( + registration, + { + name: attribute, + }, + ); + } else { + try { + await this.registrationDataService.saveData(registration, value, { + name: attribute, + }); + } catch (error) { + // This is an exception because the phoneNumber is in the registration entity, not in the registrationData. + if (attribute === Attributes.phoneNumber) { + registration.phoneNumber = value.toString(); + await this.registrationUtilsService.save(registration); + } else { + if (error.name !== ErrorEnum.RegistrationDataError) { + throw error; + } } } } } + + if ( + attribute === + AdditionalAttributes.programFinancialServiceProviderConfigurationName + ) { + registration.programFinancialServiceProviderConfigurationId = + await this.getChosenFspConfigurationId({ + registration, + newFspConfigurationName: String(value), + }); + } const savedRegistration = await this.registrationUtilsService.save(registration); const calculatedRegistration = await this.inclusionScoreService.calculatePaymentAmountMultiplier( - registration.program, + program, registration.referenceId, ); if (calculatedRegistration) { @@ -632,15 +686,14 @@ export class RegistrationsService { const registrationHasVisaCustomer = await this.intersolveVisaService.hasIntersolveCustomer(registration.id); if (registrationHasVisaCustomer) { - // TODO: REFACTOR: Find a way to not have the data fields hardcoded in this function. -> can be implemented in registration data refeactor type ContactInformationKeys = keyof ContactInformation; const fieldNames: ContactInformationKeys[] = [ - 'addressStreet', - 'addressHouseNumber', - 'addressHouseNumberAddition', - 'addressPostalCode', - 'addressCity', - 'phoneNumber', + FinancialServiceProviderAttributes.addressStreet, + FinancialServiceProviderAttributes.addressHouseNumber, + FinancialServiceProviderAttributes.addressHouseNumberAddition, + FinancialServiceProviderAttributes.addressPostalCode, + FinancialServiceProviderAttributes.addressCity, + FinancialServiceProviderAttributes.phoneNumber, ]; const registrationData = await this.registrationDataScopedRepository.getRegistrationDataArrayByName( @@ -687,9 +740,9 @@ export class RegistrationsService { return []; } - const customAttributesPhoneNumberNames = [ - CustomDataAttributes.phoneNumber as string, - CustomDataAttributes.whatsappPhoneNumber as string, + const registrationAttributesPhoneNumberNames = [ + DefaultRegistrationDataAttributeNames.phoneNumber as string, + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber as string, ]; const matchingRegistrations = ( @@ -713,7 +766,10 @@ export class RegistrationsService { for (const d of matchingRegistrationData) { const dataName = await d.getDataName(); - if (dataName && customAttributesPhoneNumberNames.includes(dataName)) { + if ( + dataName && + registrationAttributesPhoneNumberNames.includes(dataName) + ) { matchingRegistrations.push({ programId: d.registration.programId, referenceId: d.registration.referenceId, @@ -781,137 +837,28 @@ export class RegistrationsService { return filteredRegistrations; } - public async updateChosenFsp({ - referenceId, - newFspName, - newFspAttributesRaw = {}, - userId, + public async getChosenFspConfigurationId({ + registration, + newFspConfigurationName, }: { - referenceId: string; - newFspName: FinancialServiceProviders; - newFspAttributesRaw?: Record; - userId: number; - }) { + registration: RegistrationEntity; + newFspConfigurationName: string; + }): Promise { //Identify new FSP - const newFsp = await this.fspRepository.findOne({ - where: { fsp: Equal(newFspName) }, - relations: ['questions'], - }); - if (!newFsp) { - const errors = `FSP with this name not found`; - throw new HttpException({ errors }, HttpStatus.NOT_FOUND); - } - - // Check if required attributes are present - newFsp.questions.forEach((requiredAttribute) => { - if (!Object.keys(newFspAttributesRaw).includes(requiredAttribute.name)) { - const requiredAttributes = newFsp.questions - .map((a) => a.name) - .join(', '); - const errors = `Not all required FSP attributes provided correctly: ${requiredAttributes}`; - throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); - } - }); - - // Get registration by referenceId - const registration = await this.getRegistrationOrThrow({ - referenceId, - relations: ['fsp', 'fsp.questions'], - }); - if (registration.fsp?.id === newFsp.id) { - const errors = `New FSP is the same as existing FSP for this Person Affected.`; - throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); - } - - // Check if potential phonenumbers are correct and clean them - const newFspAttributes = {}; - for (const [key, value] of Object.entries(newFspAttributesRaw)) { - newFspAttributes[key] = await this.cleanCustomDataIfPhoneNr( - key, - value, - registration.programId, - ); - } - - // Get old registration to log - const oldViewRegistration = - await this.getPaginateRegistrationForReferenceId( - referenceId, - registration.programId, - ); - - // Remove old attributes - const oldFsp = registration.fsp; - for (const attribute of oldFsp?.questions) { - const regData = - await this.registrationDataService.getRegistrationDataByName( - registration, - attribute.name, - ); - await this.registrationDataScopedRepository.deleteUnscoped({ - id: regData?.id, - }); - } - - // Update FSP - const updatedRegistration = await this.addFsp(referenceId, newFsp.id); - - // Add new attributes - for (const attribute of updatedRegistration.fsp.questions) { - await this.validateAttribute( - referenceId, - attribute.name, - newFspAttributes[attribute.name], - userId, - ); - await this.addRegistrationData( - referenceId, - attribute.name, - newFspAttributes[attribute.name], - ); - } - await this.registrationUtilsService.save(updatedRegistration); - - const newViewRegistration = - await this.getPaginateRegistrationForReferenceId( - referenceId, - registration.programId, + const newFspConfig = + await this.programFinancialServiceProviderConfigurationRepository.findOne( + { + where: { + name: Equal(newFspConfigurationName), + programId: Equal(registration.programId), + }, + }, ); - - if (process.env.SYNC_WITH_THIRD_PARTIES) { - await this.sendContactInformationToIntersolve(updatedRegistration); - } - - // Log change - await this.eventsService.log(oldViewRegistration, newViewRegistration, { - additionalLogAttributes: { reason: 'Financial service provider change' }, - }); - - return newViewRegistration; - } - - public async validateAttribute( - referenceId: string, - attributeName: string, - value: any, - userId: number, - ): Promise { - if (attributeName === 'referenceId') { - const errors = `Cannot update referenceId`; - throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); - } - const attributeDto: UpdateAttributeDto = { - referenceId, - attribute: attributeName, - value, - userId, - }; - const errors = await validate( - plainToClass(UpdateAttributeDto, attributeDto), - ); - if (errors.length > 0) { - throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); + if (!newFspConfig) { + const error = `FSP with this name not found`; + throw new Error(error); } + return newFspConfig.id; } public async getMessageHistoryRegistration( @@ -1019,32 +966,45 @@ export class RegistrationsService { const registration = await this.getRegistrationOrThrow({ referenceId, programId, + relations: ['programFinancialServiceProviderConfiguration'], }); + if ( + !registration.programFinancialServiceProviderConfigurationId || + registration.programFinancialServiceProviderConfiguration + ?.financialServiceProviderName !== + FinancialServiceProviders.intersolveVisa + ) { + throw new HttpException( + `This registration is not associated with the Intersolve Visa financial service provider.`, + HttpStatus.BAD_REQUEST, + ); + } + const intersolveVisaConfig = - await this.programFinancialServiceProviderConfigurationRepository.getValuesByNamesOrThrow( + await this.programFinancialServiceProviderConfigurationRepository.getPropertiesByNamesOrThrow( { - programId, - financialServiceProviderName: - FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationId: + registration.programFinancialServiceProviderConfigurationId, names: [ - FinancialServiceProviderConfigurationEnum.brandCode, - FinancialServiceProviderConfigurationEnum.coverLetterCode, + FinancialServiceProviderConfigurationProperties.brandCode, + FinancialServiceProviderConfigurationProperties.coverLetterCode, ], }, ); // TODO: REFACTOR: This 'ugly' code is now also in payments.service.createAndAddIntersolveVisaTransactionJobs. This should be refactored when there's a better way of getting registration data. - const intersolveVisaQuestions = - await this.financialServiceProviderQuestionRepository.getQuestionsByFspName( + const intersolveVisaAttributes = + getFinancialServiceProviderSettingByNameOrThrow( FinancialServiceProviders.intersolveVisa, - ); - const intersolveVisaQuestionNames = intersolveVisaQuestions.map( + ).attributes; + + const intersolveVisaAttributeNames = intersolveVisaAttributes.map( (q) => q.name, ); const dataFieldNames = [ - 'fullName', - 'phoneNumber', - ...intersolveVisaQuestionNames, + FinancialServiceProviderAttributes.fullName, + FinancialServiceProviderAttributes.phoneNumber, + ...intersolveVisaAttributeNames, ]; const registrationData = @@ -1069,7 +1029,10 @@ export class RegistrationsService { ); for (const name of dataFieldNames) { - if (name === 'addressHouseNumberAddition') continue; // Skip non-required property + if ( + name === FinancialServiceProviderAttributes.addressHouseNumberAddition + ) + continue; // Skip non-required property if ( mappedRegistrationData[name] === null || mappedRegistrationData[name] === undefined || @@ -1086,24 +1049,46 @@ export class RegistrationsService { try { await this.intersolveVisaService.reissueCard({ registrationId: registration.id, + // Why do we need this? reference: registration.referenceId, - name: mappedRegistrationData['fullName'], + name: mappedRegistrationData[ + FinancialServiceProviderAttributes.fullName + ], contactInformation: { - addressStreet: mappedRegistrationData['addressStreet'], - addressHouseNumber: mappedRegistrationData['addressHouseNumber'], + addressStreet: + mappedRegistrationData[ + FinancialServiceProviderAttributes.addressStreet + ], + addressHouseNumber: + mappedRegistrationData[ + FinancialServiceProviderAttributes.addressHouseNumber + ], addressHouseNumberAddition: - mappedRegistrationData['addressHouseNumberAddition'], - addressPostalCode: mappedRegistrationData['addressPostalCode'], - addressCity: mappedRegistrationData['addressCity'], - phoneNumber: mappedRegistrationData['phoneNumber'], // In the above for loop it is checked that this is not undefined or empty + mappedRegistrationData[ + FinancialServiceProviderAttributes.addressHouseNumberAddition + ], + addressPostalCode: + mappedRegistrationData[ + FinancialServiceProviderAttributes.addressPostalCode + ], + addressCity: + mappedRegistrationData[ + FinancialServiceProviderAttributes.addressCity + ], + phoneNumber: + mappedRegistrationData[ + FinancialServiceProviderAttributes.phoneNumber + ], // In the above for loop it is checked that this is not undefined or empty }, brandCode: intersolveVisaConfig.find( - (c) => c.name === FinancialServiceProviderConfigurationEnum.brandCode, + (c) => + c.name === + FinancialServiceProviderConfigurationProperties.brandCode, )?.value as string, // This must be a string. If it is not, the intersolve API will return an error (maybe). coverLetterCode: intersolveVisaConfig.find( (c) => c.name === - FinancialServiceProviderConfigurationEnum.coverLetterCode, + FinancialServiceProviderConfigurationProperties.coverLetterCode, )?.value as string, // This must be a string. If it is not, the intersolve API will return an error (maybe). }); } catch (error) { diff --git a/services/121-service/src/registration/services/inclusion-score.service.ts b/services/121-service/src/registration/services/inclusion-score.service.ts index 0108c936ea..d39f9b71f1 100644 --- a/services/121-service/src/registration/services/inclusion-score.service.ts +++ b/services/121-service/src/registration/services/inclusion-score.service.ts @@ -3,8 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Equal, Repository } from 'typeorm'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { AnswerTypes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationDataService } from '@121-service/src/registration/modules/registration-data/registration-data.service'; import { RegistrationUtilsService } from '@121-service/src/registration/modules/registration-utilts/registration-utils.service'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; @@ -63,10 +63,10 @@ export class InclusionScoreService { const program = await this.programRepository.findOneOrFail({ where: { id: Equal(registration.program.id) }, - relations: ['programQuestions'], + relations: ['programRegistrationAttributes'], }); - const score = this.calculateScoreAllProgramQuestions( - program.programQuestions, + const score = this.calculateScoreAllProgramAttributes( + program.programRegistrationAttributes, scoreList, ); @@ -80,14 +80,17 @@ export class InclusionScoreService { ): Promise { const registration = await this.registrationScopedRepository.findOneOrFail({ where: { referenceId: Equal(referenceId) }, - relations: ['data', 'data.programQuestion'], + relations: ['data', 'data.programRegistrationAttribute'], }); const scoreList = {}; for (const entry of registration.data) { - if (entry.programQuestion) { + if (entry.programRegistrationAttribute) { const attrValue = entry.value; - const newKeyName = entry.programQuestion.name; - if (entry.programQuestion.answerType === AnswerTypes.multiSelect) { + const newKeyName = entry.programRegistrationAttribute.name; + if ( + entry.programRegistrationAttribute.type === + RegistrationAttributeTypes.multiSelect + ) { if (scoreList[newKeyName] !== undefined) { scoreList[newKeyName].push(attrValue); } else { @@ -101,27 +104,27 @@ export class InclusionScoreService { return scoreList; } - private calculateScoreAllProgramQuestions( - programQuestions: ProgramQuestionEntity[], + private calculateScoreAllProgramAttributes( + programRegistrationAttributes: ProgramRegistrationAttributeEntity[], scoreList: object, ): number { let totalScore = 0; - for (const question of programQuestions) { - const questionName = question.name; - if (scoreList[questionName]) { - const answerPA = scoreList[questionName]; - switch (question.answerType) { - case AnswerTypes.dropdown: + for (const attribute of programRegistrationAttributes) { + const attributeName = attribute.name; + if (scoreList[attributeName]) { + const answerPA = scoreList[attributeName]; + switch (attribute.type) { + case RegistrationAttributeTypes.dropdown: totalScore = - totalScore + this.getScoreForDropDown(question, answerPA); + totalScore + this.getScoreForDropDown(attribute, answerPA); break; - case AnswerTypes.numeric: + case RegistrationAttributeTypes.numeric: totalScore = - totalScore + this.getScoreForNumeric(question, answerPA); + totalScore + this.getScoreForNumeric(attribute, answerPA); break; - case AnswerTypes.multiSelect: + case RegistrationAttributeTypes.multiSelect: totalScore = - totalScore + this.getScoreForMultiSelect(question, answerPA); + totalScore + this.getScoreForMultiSelect(attribute, answerPA); break; } } @@ -130,40 +133,48 @@ export class InclusionScoreService { } private getScoreForDropDown( - programQuestion: ProgramQuestionEntity, + programRegistrationAttribute: ProgramRegistrationAttributeEntity, answerPA: object, ): number { - // If questions has no scoring system return 0; - if (Object.keys(programQuestion.scoring).length === 0) { + // If attribute has no scoring system return 0; + if (Object.keys(programRegistrationAttribute.scoring).length === 0) { return 0; } let score = 0; - const options = JSON.parse(JSON.stringify(programQuestion.options)); + const options = JSON.parse( + JSON.stringify(programRegistrationAttribute.options), + ); for (const value of options) { - if (value.option == answerPA && programQuestion.scoring[value.option]) { - score = Number(programQuestion.scoring[value.option]); + if ( + value.option == answerPA && + programRegistrationAttribute.scoring[value.option] + ) { + score = Number(programRegistrationAttribute.scoring[value.option]); } } return score; } private getScoreForMultiSelect( - programQuestion: ProgramQuestionEntity, + programRegistrationAttribute: ProgramRegistrationAttributeEntity, answerPA: object[], ): number { - // If questions has no scoring system return 0; - if (Object.keys(programQuestion.scoring).length === 0) { + // If attribute has no scoring system return 0; + if (Object.keys(programRegistrationAttribute.scoring).length === 0) { return 0; } let score = 0; - const options = JSON.parse(JSON.stringify(programQuestion.options)); + const options = JSON.parse( + JSON.stringify(programRegistrationAttribute.options), + ); for (const selectedOption of answerPA) { for (const value of options) { if ( value.option == selectedOption && - programQuestion.scoring[value.option] + programRegistrationAttribute.scoring[value.option] ) { - score = score + Number(programQuestion.scoring[value.option]); + score = + score + Number(programRegistrationAttribute.scoring[value.option]); } } } @@ -171,15 +182,16 @@ export class InclusionScoreService { } private getScoreForNumeric( - programQuestion: ProgramQuestionEntity, + programRegistrationAttribute: ProgramRegistrationAttributeEntity, answerPA: number, ): number { let score = 0; - if (programQuestion.scoring['multiplier']) { + if (programRegistrationAttribute.scoring['multiplier']) { if (isNaN(answerPA)) { answerPA = 0; } - score = Number(programQuestion.scoring['multiplier']) * answerPA; + score = + Number(programRegistrationAttribute.scoring['multiplier']) * answerPA; } return score; } diff --git a/services/121-service/src/registration/services/registrations-bulk.service.ts b/services/121-service/src/registration/services/registrations-bulk.service.ts index 9a3b00d85f..be675d469d 100644 --- a/services/121-service/src/registration/services/registrations-bulk.service.ts +++ b/services/121-service/src/registration/services/registrations-bulk.service.ts @@ -17,6 +17,10 @@ import { IntersolveVoucherEntity } from '@121-service/src/payments/fsp-integrati import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity'; import { BulkActionResultDto } from '@121-service/src/registration/dto/bulk-action-result.dto'; import { MessageSizeType as MessageSizeTypeDto } from '@121-service/src/registration/dto/message-size-type.dto'; +import { + DefaultRegistrationDataAttributeNames, + GenericRegistrationAttributes, +} from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { RegistrationDataScopedRepository } from '@121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; @@ -295,23 +299,33 @@ export class RegistrationsBulkService { usedPlaceholders?: string[]; selectColumns?: string[]; }): PaginateQuery { - query.select = ['referenceId', 'programId', ...selectColumns]; + query.select = [ + GenericRegistrationAttributes.referenceId, + 'programId', + ...selectColumns, + ]; if (includePaymentAttributes) { - query.select.push('paymentAmountMultiplier'); - query.select.push('financialServiceProvider'); + query.select.push(GenericRegistrationAttributes.paymentAmountMultiplier); + query.select.push('programFinancialServiceProviderConfigurationId'); + query.select.push( + GenericRegistrationAttributes.programFinancialServiceProviderConfigurationName, + ); + query.select.push('financialServiceProviderName'); } if (includeSendMessageProperties) { query.select.push('id'); - query.select.push('preferredLanguage'); - query.select.push('whatsappPhoneNumber'); - query.select.push('phoneNumber'); + query.select.push(GenericRegistrationAttributes.preferredLanguage); + query.select.push( + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber, + ); + query.select.push(GenericRegistrationAttributes.phoneNumber); } if (usedPlaceholders && usedPlaceholders?.length > 0) { query.select = [...query.select, ...usedPlaceholders]; } if (includeStatusChangeProperties) { query.select.push('id'); - query.select.push('status'); + query.select.push(GenericRegistrationAttributes.status); } // Remove duplicates from select query.select = [...new Set(query.select)]; diff --git a/services/121-service/src/registration/services/registrations-import.service.spec.ts b/services/121-service/src/registration/services/registrations-import.service.spec.ts index c5bb5cdaa0..703edb608c 100644 --- a/services/121-service/src/registration/services/registrations-import.service.spec.ts +++ b/services/121-service/src/registration/services/registrations-import.service.spec.ts @@ -2,10 +2,9 @@ import { TestBed } from '@automock/jest'; import { HttpException, HttpStatus } from '@nestjs/common'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { ProgramService } from '@121-service/src/programs/programs.service'; -import { GenericAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { GenericRegistrationAttributes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationsImportService } from '@121-service/src/registration/services/registrations-import.service'; import { RegistrationsInputValidator } from '@121-service/src/registration/validators/registrations-input-validator'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; @@ -13,21 +12,7 @@ import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; describe('RegistrationsImportService', () => { let registrationsImportService: RegistrationsImportService; - const programId = 2; const language = LanguageEnum.en; - const importRegistrationsCsvInput = [ - { - namePartnerOrganization: 'ABC', - preferredLanguage: language, - paymentAmountMultiplier: '', - maxPayments: '5', - nameFirst: 'Test', - nameLast: 'Test', - phoneNumber: '31600000000', - fspName: FinancialServiceProviders.intersolveVoucherPaper, - whatsappPhoneNumber: '', - }, - ]; beforeEach(async () => { const { unit, unitRef } = TestBed.create( @@ -57,7 +42,7 @@ describe('RegistrationsImportService', () => { [ { lineNumber: 1, - column: GenericAttributes.phoneNumber, + column: GenericRegistrationAttributes.phoneNumber, value: '', error: 'PhoneNumber is not allowed to be empty', }, @@ -86,28 +71,4 @@ describe('RegistrationsImportService', () => { it('should be defined', () => { expect(registrationsImportService).toBeDefined(); }); - - describe('validate registrations to import', () => { - it('should throw an error if phoneNumber is empty while not allowed', async () => { - // Arrange - importRegistrationsCsvInput[0].phoneNumber = ''; - const userId = 1; - - // Assert - await expect( - registrationsImportService.validateImportAsRegisteredInput( - importRegistrationsCsvInput, - programId, - userId, - ), - ).rejects.toHaveProperty('response', [ - { - lineNumber: 1, - column: GenericAttributes.phoneNumber, - value: '', - error: 'PhoneNumber is not allowed to be empty', - }, - ]); - }); - }); }); diff --git a/services/121-service/src/registration/services/registrations-import.service.ts b/services/121-service/src/registration/services/registrations-import.service.ts index d9c235813f..9888306100 100644 --- a/services/121-service/src/registration/services/registrations-import.service.ts +++ b/services/121-service/src/registration/services/registrations-import.service.ts @@ -6,35 +6,27 @@ import { v4 as uuid } from 'uuid'; import { AdditionalActionType } from '@121-service/src/actions/action.entity'; import { ActionsService } from '@121-service/src/actions/actions.service'; import { EventsService } from '@121-service/src/events/events.service'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; -import { CustomAttributeType } from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { ProgramService } from '@121-service/src/programs/programs.service'; -import { - ImportRegistrationsDto, - ImportResult, -} from '@121-service/src/registration/dto/bulk-import.dto'; +import { ImportResult } from '@121-service/src/registration/dto/bulk-import.dto'; import { RegistrationDataInfo } from '@121-service/src/registration/dto/registration-data-relation.model'; import { RegistrationsUpdateJobDto as RegistrationUpdateJobDto } from '@121-service/src/registration/dto/registration-update-job.dto'; -import { ValidationConfigDto } from '@121-service/src/registration/dto/validate-registration-config.dto'; import { - AnswerTypes, - Attribute, AttributeWithOptionalLabel, - GenericAttributes, - QuestionType, -} from '@121-service/src/registration/enum/custom-data-attributes'; -import { RegistrationCsvValidationEnum } from '@121-service/src/registration/enum/registration-csv-validation.enum'; + GenericRegistrationAttributes, + RegistrationAttributeTypes, +} from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; +import { RegistrationValidationInputType } from '@121-service/src/registration/enum/registration-validation-input-type.enum'; +import { ValidationRegistrationConfig } from '@121-service/src/registration/interfaces/validate-registration-config.interface'; +import { ValidatedRegistrationInput } from '@121-service/src/registration/interfaces/validated-registration-input.interface'; import { QueueRegistrationUpdateService } from '@121-service/src/registration/modules/queue-registrations-update/queue-registrations-update.service'; import { RegistrationDataScopedRepository } from '@121-service/src/registration/modules/registration-data/repositories/registration-data.scoped.repository'; import { RegistrationUtilsService } from '@121-service/src/registration/modules/registration-utilts/registration-utils.service'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { InclusionScoreService } from '@121-service/src/registration/services/inclusion-score.service'; import { RegistrationsInputValidatorHelpers } from '@121-service/src/registration/validators/registrations-input.validator.helper'; import { RegistrationsInputValidator } from '@121-service/src/registration/validators/registrations-input-validator'; @@ -45,14 +37,8 @@ const MASS_UPDATE_ROW_LIMIT = 100000; @Injectable() export class RegistrationsImportService { - @InjectRepository(ProgramQuestionEntity) - private readonly programQuestionRepository: Repository; - @InjectRepository(ProgramCustomAttributeEntity) - private readonly programCustomAttributeRepository: Repository; - @InjectRepository(FinancialServiceProviderEntity) - private readonly fspRepository: Repository; - @InjectRepository(FspQuestionEntity) - private readonly fspAttributeRepository: Repository; + @InjectRepository(ProgramRegistrationAttributeEntity) + private readonly programRegistrationAttributeRepository: Repository; @InjectRepository(ProgramEntity) private readonly programRepository: Repository; @@ -66,10 +52,11 @@ export class RegistrationsImportService { private readonly eventsService: EventsService, private readonly queueRegistrationUpdateService: QueueRegistrationUpdateService, private readonly registrationsInputValidator: RegistrationsInputValidator, + private readonly programFinancialServiceProviderConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, ) {} public async patchBulk( - csvFile: any, + csvFile: Express.Multer.File, programId: number, userId: number, reason: string, @@ -78,31 +65,20 @@ export class RegistrationsImportService { csvFile, MASS_UPDATE_ROW_LIMIT, ); - const columnNames = Object.keys(bulkUpdateRecords[0]); - const validatedRegistrations = await this.validateBulkUpdateInput( - bulkUpdateRecords, - programId, - userId, - ); - // Filter out only columns that were in the original csv - const filteredRegistrations = validatedRegistrations.map((registration) => { - return columnNames.reduce((acc, key) => { - if (key in registration) { - acc[key] = registration[key]; - } - return acc; - }, {}); - }); + // Do initial validation of the input without the checks that are slow + // So the user gets some feedback immidiately after upload + // The rest of the checks will be done in the queue (the user will get no feedback of this) + await this.validateBulkUpdateInput(bulkUpdateRecords, programId, userId); // Prepare the job array to push to the queue - const updateJobs: RegistrationUpdateJobDto[] = filteredRegistrations.map( - (registration) => { - const updateData = { ...registration }; - delete updateData['referenceId']; + const updateJobs: RegistrationUpdateJobDto[] = bulkUpdateRecords.map( + (record) => { + const referenceId = record['referenceId']; + delete record['referenceId']; return { - referenceId: registration['referenceId'], - data: updateData, + referenceId, + data: record, programId, reason, } as RegistrationUpdateJobDto; @@ -128,10 +104,10 @@ export class RegistrationsImportService { programId: number, ): Promise { const genericAttributes: string[] = [ - GenericAttributes.referenceId, - GenericAttributes.fspName, - GenericAttributes.phoneNumber, - GenericAttributes.preferredLanguage, + GenericRegistrationAttributes.referenceId, + GenericRegistrationAttributes.programFinancialServiceProviderConfigurationName, + GenericRegistrationAttributes.phoneNumber, + GenericRegistrationAttributes.preferredLanguage, ]; const dynamicAttributes: string[] = ( await this.getDynamicAttributes(programId) @@ -142,13 +118,15 @@ export class RegistrationsImportService { }); // If paymentAmountMultiplier automatic, then drop from template if (!program.paymentAmountMultiplierFormula) { - genericAttributes.push(String(GenericAttributes.paymentAmountMultiplier)); + genericAttributes.push( + String(GenericRegistrationAttributes.paymentAmountMultiplier), + ); } if (program.enableMaxPayments) { - genericAttributes.push(String(GenericAttributes.maxPayments)); + genericAttributes.push(String(GenericRegistrationAttributes.maxPayments)); } if (program.enableScope) { - genericAttributes.push(String(GenericAttributes.scope)); + genericAttributes.push(String(GenericRegistrationAttributes.scope)); } const attributes = genericAttributes.concat(dynamicAttributes); @@ -156,12 +134,12 @@ export class RegistrationsImportService { } public async importRegistrations( - csvFile, + inputRegistrations: Record[], program: ProgramEntity, userId: number, ): Promise { - const validatedImportRecords = await this.csvToValidatedRegistrations( - csvFile, + const validatedImportRecords = await this.validateImportRegistrationsInput( + inputRegistrations, program.id, userId, ); @@ -172,8 +150,26 @@ export class RegistrationsImportService { ); } + public async importRegistrationsFromCsv( + csvFile: Express.Multer.File, + program: ProgramEntity, + userId: number, + ): Promise { + const maxRecords = 1000; + const importRecords = await this.fileImportService.validateCsv( + csvFile, + maxRecords, + ); + // TODO: Improve the typing of what comes out validateCsv function to avoid this cast + return this.importRegistrations( + importRecords as Record[], + program, + userId, + ); + } + public async importValidatedRegistrations( - validatedImportRecords: ImportRegistrationsDto[], + validatedImportRecords: ValidatedRegistrationInput[], program: ProgramEntity, userId: number, ): Promise { @@ -181,6 +177,13 @@ export class RegistrationsImportService { const dynamicAttributes = await this.getDynamicAttributes(program.id); const registrations: RegistrationEntity[] = []; const customDataList: Record[] = []; + + const programFinancialServiceProviderConfigurations = + await this.getProgramFinancialServiceProviderConfigurations( + validatedImportRecords, + program, + ); + for await (const record of validatedImportRecords) { const registration = new RegistrationEntity(); registration.referenceId = record.referenceId || uuid(); @@ -201,20 +204,22 @@ export class RegistrationsImportService { registration.scope = record.scope || ''; } for await (const att of dynamicAttributes) { - if (att.type === CustomAttributeType.boolean) { + if (att.type === RegistrationAttributeTypes.boolean) { customData[att.name] = - RegistrationsInputValidatorHelpers.stringToBoolean( - record[att.name], + RegistrationsInputValidatorHelpers.inputToBoolean( + record.data[att.name], false, ); } else { - customData[att.name] = record[att.name]; + customData[att.name] = record.data[att.name]; } } - const fsp = await this.fspRepository.findOneOrFail({ - where: { fsp: Equal(record.fspName) }, - }); - registration.fsp = fsp; + + registration.programFinancialServiceProviderConfiguration = + programFinancialServiceProviderConfigurations[ + record.programFinancialServiceProviderConfigurationName! + ]; + registrations.push(registration); customDataList.push(customData); } @@ -243,7 +248,7 @@ export class RegistrationsImportService { // Save registration data in bulk for performance const dynamicAttributeRelations = await this.programService.getAllRelationProgram(program.id); - let registrationDataArrayAllPa: RegistrationDataEntity[] = []; + let registrationDataArrayAllPa: RegistrationAttributeDataEntity[] = []; for (const [i, registration] of savedRegistrations.entries()) { const registrationDataArray = this.prepareRegistrationData( registration, @@ -286,14 +291,54 @@ export class RegistrationsImportService { return { aggregateImportResult: { countImported } }; } + private async getProgramFinancialServiceProviderConfigurations( + validatedImportRecords: ValidatedRegistrationInput[], + program: ProgramEntity, + ) { + const programFinancialServiceProviderConfigurations = {}; + const uniqueConfigNames = Array.from( + new Set( + validatedImportRecords + .filter( + (record) => + record.programFinancialServiceProviderConfigurationName !== + undefined, + ) + .map( + (record) => record.programFinancialServiceProviderConfigurationName, + ), + ), + ); + for (const programFinancialServiceProviderConfigurationName of uniqueConfigNames) { + programFinancialServiceProviderConfigurations[ + programFinancialServiceProviderConfigurationName! + ] = + await this.programFinancialServiceProviderConfigurationRepository.findOneOrFail( + { + where: { + name: Equal( + programFinancialServiceProviderConfigurationName ?? '', + ), + programId: Equal(program.id), + }, + }, + ); + } + return programFinancialServiceProviderConfigurations; + } + private async programHasInclusionScore(programId: number): Promise { - const programQuestions = await this.programQuestionRepository.find({ - where: { - programId: Equal(programId), - }, - }); - for (const q of programQuestions) { - if (q.scoring != null && JSON.stringify(q.scoring) !== '{}') { + const programRegistrationAttributes = + await this.programRegistrationAttributeRepository.find({ + where: { + programId: Equal(programId), + }, + }); + for (const attribute of programRegistrationAttributes) { + if ( + attribute.scoring != null && + JSON.stringify(attribute.scoring) !== '{}' + ) { return true; } } @@ -304,215 +349,99 @@ export class RegistrationsImportService { registration: RegistrationEntity, customData: object, dynamicAttributeRelations: RegistrationDataInfo[], - ): RegistrationDataEntity[] { - const registrationDataArray: RegistrationDataEntity[] = []; + ): RegistrationAttributeDataEntity[] { + const registrationDataArray: RegistrationAttributeDataEntity[] = []; for (const att of dynamicAttributeRelations) { - if (att.relation.fspQuestionId && att.fspId !== registration.fspId) { - continue; - } let values: unknown[] = []; - if (att.type === CustomAttributeType.boolean) { + if (att.type === RegistrationAttributeTypes.boolean) { values.push( - RegistrationsInputValidatorHelpers.stringToBoolean( + RegistrationsInputValidatorHelpers.inputToBoolean( customData[att.name], false, ), ); - } else if (att.type === CustomAttributeType.text) { + } else if (att.type === RegistrationAttributeTypes.text) { values.push(customData[att.name] ? customData[att.name] : ''); - } else if (att.type === AnswerTypes.multiSelect) { + } else if (att.type === RegistrationAttributeTypes.multiSelect) { values = customData[att.name].split('|'); } else { values.push(customData[att.name]); } for (const value of values) { - if (value == null) { - throw new Error(`Missing value for attribute ${att.name}`); + if (value != null) { + const registrationData = new RegistrationAttributeDataEntity(); + registrationData.registration = registration; + registrationData.value = value as string; + registrationData.programRegistrationAttributeId = + att.relation.programRegistrationAttributeId; + registrationDataArray.push(registrationData); } - const registrationData = new RegistrationDataEntity(); - registrationData.registration = registration; - registrationData.value = value as string; - registrationData.programCustomAttributeId = - att.relation.programCustomAttributeId ?? null; - registrationData.programQuestionId = - att.relation.programQuestionId ?? null; - registrationData.fspQuestionId = att.relation.fspQuestionId ?? null; - registrationDataArray.push(registrationData); } } return registrationDataArray; } - private async csvToValidatedRegistrations( - csvFile: any[], - programId: number, - userId: number, - ): Promise { - const maxRecords = 1000; - const importRecords = await this.fileImportService.validateCsv( - csvFile, - maxRecords, - ); - return await this.validateImportAsRegisteredInput( - importRecords, - programId, - userId, - ); - } - - private async getProgramCustomAttributes( - programId: number, - ): Promise { - return ( - await this.programCustomAttributeRepository.find({ - where: { program: { id: Equal(programId) } }, - }) - ).map((c) => { - return { - id: c.id, - name: c.name, - type: c.type, - label: c.label, - questionType: QuestionType.programCustomAttribute, - }; - }); - } - private async getDynamicAttributes( programId: number, ): Promise { - let attributes: (AttributeWithOptionalLabel & { - fspName?: FinancialServiceProviders; - })[] = []; - const programCustomAttributes = - await this.getProgramCustomAttributes(programId); - attributes = [...attributes, ...programCustomAttributes]; - - const programQuestions = ( - await this.programQuestionRepository.find({ + const programRegistrationAttributes = ( + await this.programRegistrationAttributeRepository.find({ where: { program: { id: Equal(programId) } }, }) - ).map((c) => { + ).map((attribute) => { return { - id: c.id, - name: c.name, - type: c.answerType, - options: c.options, - questionType: QuestionType.programQuestion, + id: attribute.id, + name: attribute.name, + type: attribute.type, + options: attribute.options, + isRequired: attribute.isRequired, } as AttributeWithOptionalLabel; }); - attributes = [...attributes, ...programQuestions]; - - const fspAttributes = await this.fspAttributeRepository.find({ - relations: ['fsp', 'fsp.program'], - }); - const programFspAttributes = fspAttributes - .filter((a) => a.fsp.program.map((p) => p.id).includes(programId)) - .map((c) => { - return { - id: c.id, - name: c.name, - type: c.answerType, - fspName: c.fsp.fsp as FinancialServiceProviders, - questionType: QuestionType.fspQuestion, - }; - }); - attributes = [...programFspAttributes.reverse(), ...attributes]; - - // deduplicate attributes and concatenate fsp names - const deduplicatedAttributes = attributes.reduce((acc, curr) => { - const existingAttribute = acc.find((a) => a.name === curr.name); - if (existingAttribute) { - if (curr.questionType) { - if (!existingAttribute.questionTypes) { - existingAttribute.questionTypes = []; - } - - if (!existingAttribute.questionTypes.includes(curr.questionType)) { - existingAttribute.questionTypes.push(curr.questionType); - } - } - - if (curr.fspName) { - if (!existingAttribute.fspNames) { - existingAttribute.fspNames = []; - } - existingAttribute.fspNames.push(curr.fspName); - } - } else { - acc.push({ - id: curr.id, - name: curr.name, - type: curr.type, - options: curr.options, - fspNames: curr.fspName ? [curr.fspName] : [], - questionTypes: curr.questionType ? [curr.questionType] : [], - }); - } - return acc; - }, [] as AttributeWithOptionalLabel[]); - return deduplicatedAttributes; + return programRegistrationAttributes; } - public async validateImportAsRegisteredInput( - csvArray: any[], + public async validateImportRegistrationsInput( + registrationInputToValidate: Record< + string, + string | boolean | number | undefined + >[], programId: number, userId: number, - ): Promise { - const { allowEmptyPhoneNumber } = - await this.programService.findProgramOrThrow(programId); - const validationConfig = new ValidationConfigDto({ - validatePhoneNumberEmpty: !allowEmptyPhoneNumber, + ): Promise { + const validationConfig: ValidationRegistrationConfig = { validatePhoneNumberLookup: true, - validateClassValidator: true, validateUniqueReferenceId: true, - validateScope: true, - validatePreferredLanguage: true, - }); - const dynamicAttributes = await this.getDynamicAttributes(programId); - return (await this.registrationsInputValidator.validateAndCleanRegistrationsInput( - csvArray, + validateExistingReferenceId: true, + }; + const data = await this.registrationsInputValidator.validateAndCleanInput({ + registrationInputArray: registrationInputToValidate, programId, userId, - dynamicAttributes, - RegistrationCsvValidationEnum.importAsRegistered, + typeOfInput: RegistrationValidationInputType.create, validationConfig, - )) as ImportRegistrationsDto[]; + }); + return data; } private async validateBulkUpdateInput( csvArray: any[], programId: number, userId: number, - ): Promise { - const { allowEmptyPhoneNumber } = - await this.programService.findProgramOrThrow(programId); - - // Checking if there is any phoneNumber values in the submitted CSV file - const hasPhoneNumber = csvArray.some((row) => row.phoneNumber); - - const validationConfig = new ValidationConfigDto({ + ): Promise { + const validationConfig: ValidationRegistrationConfig = { validateExistingReferenceId: false, - // if there is no phoneNumber column in the submitted CSV file, but program is configured to not allow empty phone number - // then we are considering, in database we already have phone numbers for registrations and we are not expecting to update phone number through mas update. - // So ignoring phone number validation - validatePhoneNumberEmpty: hasPhoneNumber && !allowEmptyPhoneNumber, validatePhoneNumberLookup: false, - validateClassValidator: true, validateUniqueReferenceId: false, - validateScope: true, - validatePreferredLanguage: true, - }); - - const dynamicAttributes = await this.getDynamicAttributes(programId); - - return (await this.registrationsInputValidator.validateAndCleanRegistrationsInput( - csvArray, - programId, - userId, - dynamicAttributes, - RegistrationCsvValidationEnum.bulkUpdate, - validationConfig, - )) as ImportRegistrationsDto[]; + }; + const result = await this.registrationsInputValidator.validateAndCleanInput( + { + registrationInputArray: csvArray, + programId, + userId, + typeOfInput: RegistrationValidationInputType.update, + validationConfig, + }, + ); + return result; } } diff --git a/services/121-service/src/registration/services/registrations-pagination.service.ts b/services/121-service/src/registration/services/registrations-pagination.service.ts index 9b65614c3d..ca77f8ed2a 100644 --- a/services/121-service/src/registration/services/registrations-pagination.service.ts +++ b/services/121-service/src/registration/services/registrations-pagination.service.ts @@ -22,10 +22,6 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-pr import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { ProgramService } from '@121-service/src/programs/programs.service'; -import { - getFspDisplayNameMapping, - overwriteFspDisplayName, -} from '@121-service/src/programs/utils/overwrite-fsp-display-name.helper'; import { AllowedFilterOperatorsString, PaginateConfigRegistrationView, @@ -37,10 +33,10 @@ import { RegistrationDataInfo, RegistrationDataRelation, } from '@121-service/src/registration/dto/registration-data-relation.model'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; import { PaymentFilterEnum } from '@121-service/src/registration/enum/payment-filter.enum'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { RegistrationViewEntity } from '@121-service/src/registration/registration-view.entity'; import { RegistrationViewScopedRepository } from '@121-service/src/registration/repositories/registration-view-scoped.repository'; import { ScopedQueryBuilder } from '@121-service/src/scoped.repository'; @@ -93,8 +89,11 @@ export class RegistrationsPaginationService { } } - // If you want to select fspDisplayName, you also need to get financialServiceProvider because we need this to find the correct fspDisplayName - if (query.select && query.select.includes('fspDisplayName')) { + // If you want to select programFinancialServiceProviderConfigurationLabel, you also need to get financialServiceProvider because we need this to find the correct programFinancialServiceProviderConfigurationLabel + if ( + query.select && + query.select.includes('programFinancialServiceProviderConfigurationLabel') + ) { if (fullnameNamingConvention) { query.select.push('financialServiceProvider'); } @@ -116,7 +115,7 @@ export class RegistrationsPaginationService { await this.programService.getAllRelationProgram(programId); const registrationDataNamesProgram = registrationDataRelations .map((r) => r.name) - .filter((r) => r !== CustomDataAttributes.phoneNumber); // Phonenumber is already in the registration table so we do not need to filter on it twice + .filter((r) => r !== DefaultRegistrationDataAttributeNames.phoneNumber); // Phonenumber is already in the registration table so we do not need to filter on it twice // Check if the filter contains at least one registration data name if (query.filter) { @@ -213,16 +212,16 @@ export class RegistrationsPaginationService { >['data'] = []; for (let i = 0; i < totalPages; i++) { - const registrations = await this.getPaginate( + const paginateResult = await this.getPaginate( paginateQuery, programId, true, false, baseQuery ? baseQuery.clone() : undefined, // We need to create a seperate querybuilder object twice or it will be modified twice ); - totalPages = registrations.meta.totalPages; + totalPages = paginateResult.meta.totalPages; paginateQuery.page = paginateQuery.page + 1; - allRegistrations = allRegistrations.concat(...registrations.data); + allRegistrations = allRegistrations.concat(...paginateResult.data); } return allRegistrations; } @@ -297,7 +296,9 @@ export class RegistrationsPaginationService { const registrationDataNamesProgram = registrationDataRelations.map( (r) => r.name, ); - registrationDataNamesProgram.push(CustomDataAttributes.phoneNumber); + registrationDataNamesProgram.push( + DefaultRegistrationDataAttributeNames.phoneNumber, + ); // Check if the filter contains at least one registration data name for (const registrationDataName of registrationDataNamesProgram) { @@ -492,7 +493,6 @@ export class RegistrationsPaginationService { orignalSelect, fullnameNamingConvention, hasPersonalReadPermission, - programId, }: { paginatedResult: Paginated; registrationDataRelations: RegistrationDataInfo[]; @@ -502,36 +502,20 @@ export class RegistrationsPaginationService { hasPersonalReadPermission: boolean; programId: number; }): Promise { - const program = await this.programRepository.findOneOrFail({ - where: { id: Equal(programId) }, - relations: ['financialServiceProviders', 'programFspConfiguration'], - }); - const fspDisplayNameMapping = getFspDisplayNameMapping(program); - return paginatedResult.data.map((registration) => { const mappedRootRegistration = this.mapRootRegistration( registration, select, hasPersonalReadPermission, ); - // Add personal data permission check here - const mappedRegistration = this.mapRegistrationData( - registration.data, - mappedRootRegistration, - registrationDataRelations, - ); - if (orignalSelect.includes('fspDisplayName')) { - const overriddenFspDisplayName = overwriteFspDisplayName( - mappedRegistration.financialServiceProvider, - fspDisplayNameMapping, - ); - if (overriddenFspDisplayName) { - mappedRegistration.fspDisplayName = overriddenFspDisplayName; - } - if (!orignalSelect.includes('financialServiceProvider')) { - delete mappedRegistration.financialServiceProvider; - } - } + + const mappedRegistration = hasPersonalReadPermission + ? this.mapRegistrationData( + registration.data, + mappedRootRegistration, + registrationDataRelations, + ) + : mappedRootRegistration; if ((!select || select.includes('name')) && hasPersonalReadPermission) { return this.mapRegistrationName({ @@ -570,24 +554,21 @@ export class RegistrationsPaginationService { } private mapRegistrationData( - registrationDataArray: RegistrationDataEntity[], + registrationDataArray: RegistrationAttributeDataEntity[], mappedRegistration: ReturnType< RegistrationsPaginationService['mapRootRegistration'] >, registrationDataInfoArray: RegistrationDataInfo[], ) { - if (!registrationDataArray || registrationDataArray.length < 1) { + if (!registrationDataInfoArray || registrationDataInfoArray.length < 1) { return mappedRegistration; } + const findRelation = ( dataRelation: RegistrationDataRelation, - data: RegistrationDataEntity, + data: RegistrationAttributeDataEntity, ): boolean => { - const propertiesToCheck = [ - 'programQuestionId', - 'fspQuestionId', - 'programCustomAttributeId', - ]; + const propertiesToCheck = ['programRegistrationAttributeId']; for (const property of propertiesToCheck) { if ( dataRelation[property] === data[property] && @@ -598,14 +579,18 @@ export class RegistrationsPaginationService { } return false; }; - for (const registrationData of registrationDataArray) { - const dataRelation = registrationDataInfoArray.find((x) => - findRelation(x.relation, registrationData), + + for (const dataRelation of registrationDataInfoArray) { + const registrationData = registrationDataArray.find((x) => + findRelation(dataRelation.relation, x), ); - if (dataRelation && dataRelation.name) { + if (registrationData) { mappedRegistration[dataRelation.name] = registrationData.value; + } else { + mappedRegistration[dataRelation.name] = null; } } + return mappedRegistration; } @@ -734,26 +719,46 @@ export class RegistrationsPaginationService { return queryBuilder; } - public getQueryBuilderForFsp( - programId: number, - payment: number, - fspName: FinancialServiceProviders, - status?: TransactionStatusEnum, - ): ScopedQueryBuilder { + public getQueryBuilderForFspInstructions({ + programId, + payment, + programFinancialServiceProviderConfigurationId, + financialServiceProviderName, + status, + }: { + programId: number; + payment: number; + programFinancialServiceProviderConfigurationId?: number; + financialServiceProviderName?: FinancialServiceProviders; + status?: TransactionStatusEnum; + }): ScopedQueryBuilder { const query = this.registrationViewScopedRepository .createQueryBuilder('registration') .innerJoin('registration.latestTransactions', 'latestTransaction') .innerJoin('latestTransaction.transaction', 'transaction') - .innerJoin('transaction.financialServiceProvider', 'fsp') .andWhere('registration.programId = :programId', { programId }) .andWhere('transaction.payment = :payment', { payment }) - .andWhere('fsp.fsp = :fsp', { - fsp: fspName, - }) .orderBy('registration.referenceId', 'ASC'); if (status) { query.andWhere('transaction.status = :status', { status }); } + if (programFinancialServiceProviderConfigurationId) { + query.andWhere( + 'transaction.programFinancialServiceProviderConfigurationId = :programFinancialServiceProviderConfigurationId', + { programFinancialServiceProviderConfigurationId }, + ); + } + if (financialServiceProviderName) { + query + .leftJoin( + 'transaction.programFinancialServiceProviderConfiguration', + 'programFinancialServiceProviderConfiguration', + ) + .andWhere( + 'programFinancialServiceProviderConfiguration.financialServiceProviderName = :financialServiceProviderName', + { financialServiceProviderName }, + ); + } return query; } } diff --git a/services/121-service/src/registration/validators/registration-data-type.class.validator.spec.ts b/services/121-service/src/registration/validators/registration-data-type.class.validator.spec.ts deleted file mode 100644 index fbb68f5f36..0000000000 --- a/services/121-service/src/registration/validators/registration-data-type.class.validator.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Repository } from 'typeorm'; - -import { AppDataSource } from '@121-service/src/appdatasource'; -import { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity'; -import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationDataTypeClassValidator } from '@121-service/src/registration/validators/registration-data-type.class.validator'; - -jest.mock('../../appdatasource', () => ({ - AppDataSource: { - getRepository: { - findOne: jest.fn().mockResolvedValue(null), - }, - }, -})); - -describe('RegistrationDataTypeClassValidator', () => { - let validator: RegistrationDataTypeClassValidator; - let mockRegistrationRepository: Partial>; - - beforeEach(() => { - validator = new RegistrationDataTypeClassValidator(); - mockRegistrationRepository = { findOne: jest.fn().mockResolvedValue(null) }; - - AppDataSource.getRepository = jest - .fn() - .mockReturnValue(mockRegistrationRepository); - }); - - it('should return false if referenceId or attribute are undefined', async () => { - const isValid = await validator.validate(undefined, { - object: { userId: 1 }, - constraints: [{ referenceId: 'referenceId', attribute: 'attribute' }], - value: undefined, - targetName: '', - property: '', - }); - - expect(isValid).toBe(false); - }); - - it('should return false if registration does not exist for provided referenceId', async () => { - mockRegistrationRepository.findOne = jest.fn().mockResolvedValue(null); - const isValid = await validator.validate('value', { - object: { referenceId: 'nonexistent', attribute: 'attribute', userId: 1 }, - constraints: [{ referenceId: 'referenceId', attribute: 'attribute' }], - value: undefined, - targetName: '', - property: '', - }); - - expect(isValid).toBe(false); - }); - - it('should return true for valid attribute type', async () => { - const mockProgram = { - getValidationInfoForQuestionName: jest - .fn() - .mockResolvedValue({ type: 'text', options: [] }), - }; - mockRegistrationRepository.findOne = jest - .fn() - .mockResolvedValue({ program: mockProgram }); - const isValid = await validator.validate('some text', { - object: { referenceId: 'existing', attribute: 'attribute', userId: 1 }, - constraints: [{ referenceId: 'referenceId', attribute: 'attribute' }], - value: undefined, - targetName: '', - property: '', - }); - - expect(isValid).toBe(true); - }); - - it('should return false for an invalid scope', async () => { - const mockAssignmentRepo = { - findOne: jest - .fn() - .mockResolvedValue({ scope: 'allowedScope', userId: 1, programId: 1 }), - }; - AppDataSource.getRepository = jest.fn().mockImplementation((entity) => { - if (entity === ProgramAidworkerAssignmentEntity) { - return mockAssignmentRepo; - } - return mockRegistrationRepository; - }); - - const isValid = await validator.validate('invalidScope', { - object: { referenceId: 'existing', attribute: 'scope', userId: 1 }, - constraints: [{ referenceId: 'referenceId', attribute: 'attribute' }], - value: undefined, - targetName: '', - property: '', - }); - - expect(isValid).toBe(false); - }); -}); diff --git a/services/121-service/src/registration/validators/registration-data-type.class.validator.ts b/services/121-service/src/registration/validators/registration-data-type.class.validator.ts deleted file mode 100644 index 3b1d7b40ea..0000000000 --- a/services/121-service/src/registration/validators/registration-data-type.class.validator.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { - registerDecorator, - validate, - ValidationArguments, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, -} from 'class-validator'; -import { Equal, Repository } from 'typeorm'; - -import { AppDataSource } from '@121-service/src/appdatasource'; -import { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity'; -import { CreateRegistrationDto } from '@121-service/src/registration/dto/create-registration.dto'; -import { Attributes } from '@121-service/src/registration/dto/update-registration.dto'; -import { - AnswerTypes, - CustomAttributeType, -} from '@121-service/src/registration/enum/custom-data-attributes'; -import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; - -@ValidatorConstraint({ name: 'validateAttributeType', async: true }) -export class RegistrationDataTypeClassValidator - implements ValidatorConstraintInterface -{ - private message: string; - - public async validate( - value: any, - args: ValidationArguments, - ): Promise { - const referenceId = args.object[args.constraints[0]['referenceId']]; - const attribute = args.object[args.constraints[0]['attribute']]; - const userId = args.object['userId']; - - if (referenceId === undefined || attribute === undefined) { - this.message = 'ReferenceId or attribute are undefined'; - return false; - } - const registrationRepository = - AppDataSource.getRepository(RegistrationEntity); - const registration = await registrationRepository.findOne({ - where: { referenceId: Equal(referenceId) }, - relations: ['program'], - }); - if (!registration) { - this.message = `Registration was not found for referenceId: '${referenceId}'`; - return false; - } - const validationInfo = - await registration.program.getValidationInfoForQuestionName(attribute); - if (!validationInfo.type) { - this.message = `Attribute '${attribute}' was not found for the program related to the reference id:' ${referenceId}'`; - return false; - } - if (attribute == Attributes.referenceId) { - return this.referenceIdIsValid( - value, - registrationRepository, - referenceId, - ); - } - const typeValid = this.typeIsValid( - value, - validationInfo.type, - attribute, - validationInfo.options, - ); - if (!typeValid) { - return false; - } - - if (attribute == Attributes.scope) { - return this.scopeIsValid(value, userId, registration.programId); - } - - return true; - } - - private createErrorMessageType(type, value, attribute): void { - const valueString = Array.isArray(value) ? JSON.stringify(value) : value; - this.message = `The value '${valueString}' given for the attribute '${attribute}' does not have the correct format for type '${type}'`; - } - - public defaultMessage(_args: ValidationArguments): string { - return this.message; - } - - private async referenceIdIsValid( - value: string, - registrationRepository: Repository, - orignalReferenceId: string, - ): Promise { - const registration = await registrationRepository.findOne({ - where: { referenceId: Equal(value) }, - relations: ['program'], - }); - if (registration && registration.referenceId !== orignalReferenceId) { - this.message = `ReferenceId '${value}' already exists`; - return false; - } else { - const dto = new CreateRegistrationDto(); - dto.referenceId = value; - const valResult = await validate(dto); - if (valResult.length > 0) { - try { - if (valResult[0].constraints) { - this.message = Object.values(valResult[0].constraints).join(', '); - } - } catch (e) { - this.message = JSON.stringify(valResult); - } - return false; - } - } - return true; - } - - private async scopeIsValid( - value: string, - userId: number, - programId: number, - ): Promise { - const assignmentRepo = AppDataSource.getRepository( - ProgramAidworkerAssignmentEntity, - ); - const assignment = await assignmentRepo.findOne({ - where: { userId: Equal(userId), programId: Equal(programId) }, - }); - const requestScope = assignment?.scope ? assignment.scope : ''; - const scopeValid = - (requestScope && value.startsWith(requestScope)) || !requestScope; - if (!scopeValid) { - this.message = `'${value}' is not a valid scope. The valid scope for this account is '${requestScope}'`; - } - return scopeValid; - } - - private typeIsValid( - value: any, - type: string, - attribute: string, - options?: any[], - ): boolean | null { - let isValid: boolean | null = false; - if (type === AnswerTypes.date) { - const datePattern = - /^(0?[1-9]|[12][0-9]|3[01])-(0?[1-9]|1[0-2])-(19[2-9][0-9]|20[0-1][0-9])$/; - isValid = datePattern.test(value); - } else if (type === AnswerTypes.dropdown) { - isValid = this.optionIsValid(value, options); - } else if (type === AnswerTypes.multiSelect) { - isValid = this.multiSelectIsValid(value, options); - } else if (type === AnswerTypes.numeric) { - isValid = !isNaN(+value); - } else if (type === AnswerTypes.numericNullable) { - isValid = !isNaN(+value) || null; - } else if (type === AnswerTypes.tel) { - // Potential refactor: put lookup code here - isValid = this.phoneNumberIsValid(value); - } else if (type === AnswerTypes.text || type === CustomAttributeType.text) { - isValid = typeof value === 'string'; - } else if (type === CustomAttributeType.boolean) { - isValid = typeof value == 'boolean' || this.valueIsBool(value); - } else { - this.message = `Type '${type}' is unknown'`; - return false; - } - this.createErrorMessageType(type, value, attribute); - return isValid; - } - - private multiSelectIsValid(values: any, options?: any[]): boolean { - if (!Array.isArray(values) || values.length === 0) { - return false; - } - if (!(values.length === new Set(values).size)) { - return false; - } - for (const value of values) { - if (!this.optionIsValid(value, options)) { - return false; - } - } - return true; - } - - private phoneNumberIsValid(value: any): boolean { - if (value === '') { - return true; - } else if (!!value && value.length >= 8 && value.length <= 17) { - return true; - } else { - return false; - } - } - - private optionIsValid(value: any, options?: any[]): boolean { - if (!options) { - return false; - } else { - for (const option of options) { - if (option.option === value) { - return true; - } - } - return false; - } - } - - private valueIsBool(value): boolean { - const allowedValues = ['true', 'yes', '1', 'false', '0', 'no', null]; - return allowedValues.includes(value); - } -} - -class ValidateRegistrationDataAttributessDto { - public referenceId: string; - public attribute: string; -} - -export function IsRegistrationDataValidType( - validationAttributes: ValidateRegistrationDataAttributessDto, - validationOptions?: ValidationOptions, -): any { - return function (object: Record, propertyName: string) { - registerDecorator({ - target: object.constructor, - propertyName, - options: validationOptions, - constraints: [validationAttributes], - validator: RegistrationDataTypeClassValidator, - }); - }; -} diff --git a/services/121-service/src/registration/validators/registrations-input-validator.spec.ts b/services/121-service/src/registration/validators/registrations-input-validator.spec.ts index 82f57ca19f..bb681f3dcd 100644 --- a/services/121-service/src/registration/validators/registrations-input-validator.spec.ts +++ b/services/121-service/src/registration/validators/registrations-input-validator.spec.ts @@ -3,93 +3,87 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { LookupService } from '@121-service/src/notifications/lookup/lookup.service'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; import { - AttributeWithOptionalLabel, - QuestionType, -} from '@121-service/src/registration/enum/custom-data-attributes'; -import { RegistrationCsvValidationEnum } from '@121-service/src/registration/enum/registration-csv-validation.enum'; + DefaultRegistrationDataAttributeNames, + GenericRegistrationAttributes, + RegistrationAttributeTypes, +} from '@121-service/src/registration/enum/registration-attribute.enum'; +import { RegistrationValidationInputType } from '@121-service/src/registration/enum/registration-validation-input-type.enum'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; +import { RegistrationViewScopedRepository } from '@121-service/src/registration/repositories/registration-view-scoped.repository'; +import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; import { RegistrationsInputValidator } from '@121-service/src/registration/validators/registrations-input-validator'; +import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { UserService } from '@121-service/src/user/user.service'; const programId = 1; const userId = 1; -const dynamicAttributes: AttributeWithOptionalLabel[] = [ +const dynamicAttributes: Partial[] = [ { id: 8, - name: 'addressStreet', - type: 'text', - fspNames: [FinancialServiceProviders.intersolveVisa], - questionTypes: [QuestionType.fspQuestion], + name: FinancialServiceProviderAttributes.addressStreet, + type: RegistrationAttributeTypes.text, + isRequired: false, }, { id: 9, - name: 'addressHouseNumber', - type: 'numeric', - fspNames: [FinancialServiceProviders.intersolveVisa], - questionTypes: [QuestionType.fspQuestion], + name: FinancialServiceProviderAttributes.addressHouseNumber, + type: RegistrationAttributeTypes.numeric, + isRequired: false, }, { id: 10, - name: 'addressHouseNumberAddition', - type: 'text', - fspNames: [FinancialServiceProviders.intersolveVisa], - questionTypes: [QuestionType.fspQuestion], + name: FinancialServiceProviderAttributes.addressHouseNumberAddition, + type: RegistrationAttributeTypes.text, + isRequired: false, }, { id: 11, - name: 'addressPostalCode', - type: 'text', - fspNames: [FinancialServiceProviders.intersolveVisa], - questionTypes: [QuestionType.fspQuestion], + name: FinancialServiceProviderAttributes.addressPostalCode, + type: RegistrationAttributeTypes.text, + isRequired: false, }, { id: 12, - name: 'addressCity', - type: 'text', - fspNames: [FinancialServiceProviders.intersolveVisa], - questionTypes: [QuestionType.fspQuestion], + name: FinancialServiceProviderAttributes.addressCity, + type: RegistrationAttributeTypes.text, + isRequired: false, }, { id: 13, - name: 'whatsappPhoneNumber', - type: 'tel', - fspNames: [ - FinancialServiceProviders.intersolveVisa, - FinancialServiceProviders.intersolveVoucherWhatsapp, - ], - questionTypes: [QuestionType.fspQuestion], + name: FinancialServiceProviderAttributes.whatsappPhoneNumber, + type: RegistrationAttributeTypes.tel, + isRequired: false, }, { id: 3, - name: 'fullName', - type: 'text', + name: FinancialServiceProviderAttributes.fullName, + type: RegistrationAttributeTypes.text, options: null, - fspNames: [], - questionTypes: [QuestionType.programQuestion], + isRequired: false, }, { id: 4, - name: 'phoneNumber', - type: 'tel', + name: DefaultRegistrationDataAttributeNames.phoneNumber, + type: RegistrationAttributeTypes.tel, options: null, - fspNames: [], - questionTypes: [QuestionType.programQuestion], + isRequired: false, }, { id: 5, name: 'house', - type: 'dropdown', + type: RegistrationAttributeTypes.dropdown, options: [ { option: 'lannister', label: { en: 'Lannister' } }, { option: 'stark', label: { en: 'Stark' } }, { option: 'greyjoy', label: { en: 'Greyjoy' } }, ], - fspNames: [], - questionTypes: [QuestionType.programQuestion], + isRequired: false, }, ]; @@ -102,6 +96,12 @@ describe('RegistrationsInputValidator', () => { mockProgramRepository = {}; mockRegistrationRepository = {}; + const mockRegistrationViewScopedRepository = { + createQueryBuilder: jest.fn().mockReturnValue({ + andWhere: jest.fn().mockReturnThis(), + }), + // other methods... + }; const module: TestingModule = await Test.createTestingModule({ providers: [ RegistrationsInputValidator, @@ -113,6 +113,7 @@ describe('RegistrationsInputValidator', () => { provide: getRepositoryToken(RegistrationEntity), useValue: mockRegistrationRepository, }, + { provide: UserService, useValue: { @@ -125,6 +126,16 @@ describe('RegistrationsInputValidator', () => { lookupAndCorrect: jest.fn().mockResolvedValue('1234567890'), }, }, + { + provide: RegistrationsPaginationService, + useValue: { + getRegistrationsChunked: jest.fn().mockResolvedValue([]), + }, + }, + { + provide: RegistrationViewScopedRepository, + useValue: mockRegistrationViewScopedRepository, + }, ], }).compile(); @@ -133,10 +144,18 @@ describe('RegistrationsInputValidator', () => { ); mockRegistrationRepository.findOne = jest.fn().mockResolvedValue(null); - mockProgramRepository.findOneByOrFail = jest.fn().mockResolvedValue({ + mockProgramRepository.findOneOrFail = jest.fn().mockResolvedValue({ id: 1, name: 'Test Program', languages: ['en'], + programFinancialServiceProviderConfigurations: [ + { + financialServiceProviderName: + FinancialServiceProviders.intersolveVoucherWhatsapp, + name: 'Intersolve-voucher-whatsapp', + }, + ], + programRegistrationAttributes: dynamicAttributes, }); }); @@ -152,19 +171,24 @@ describe('RegistrationsInputValidator', () => { addressStreet: 'newStreet1', addressHouseNumber: '2', addressHouseNumberAddition: 'Ground', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, scope: 'country', house: 'stark', }, ]; - const result = await validator.validateAndCleanRegistrationsInput( - csvArray, + const result = await validator.validateAndCleanInput({ + registrationInputArray: csvArray, programId, userId, - dynamicAttributes, - RegistrationCsvValidationEnum.importAsRegistered, - ); + typeOfInput: RegistrationValidationInputType.create, + validationConfig: { + validatePhoneNumberLookup: true, + validateUniqueReferenceId: true, + validateExistingReferenceId: true, + }, + }); expect(result).toBeInstanceOf(Array); expect(result.length).toBe(1); @@ -173,7 +197,7 @@ describe('RegistrationsInputValidator', () => { '00dc9451-1273-484c-b2e8-ae21b51a96ab', ); expect(result[0]).toHaveProperty( - 'fspName', + 'programFinancialServiceProviderConfigurationName', FinancialServiceProviders.intersolveVoucherWhatsapp, ); expect(result[0]).toHaveProperty('paymentAmountMultiplier', 2); @@ -192,60 +216,112 @@ describe('RegistrationsInputValidator', () => { addressStreet: 'newStreet1', addressHouseNumber: '2', addressHouseNumberAddition: 'Ground', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, scope: 'country', }, ]; await expect( - validator.validateAndCleanRegistrationsInput( - csvArray, + validator.validateAndCleanInput({ + registrationInputArray: csvArray, programId, userId, - dynamicAttributes, - RegistrationCsvValidationEnum.importAsRegistered, - ), + typeOfInput: RegistrationValidationInputType.create, + validationConfig: { + validatePhoneNumberLookup: true, + validateUniqueReferenceId: true, + validateExistingReferenceId: true, + }, + }), ).rejects.toThrow(HttpException); }); it('should report errors for rows missing mandatory fields on import', async () => { const csvArray = [ { - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, preferredLanguage: 'en', }, ]; const programId = 1; const userId = 1; - const dynamicAttributes = []; await expect( - validator.validateAndCleanRegistrationsInput( - csvArray, + validator.validateAndCleanInput({ + registrationInputArray: csvArray, programId, userId, - dynamicAttributes, - RegistrationCsvValidationEnum.importAsRegistered, - ), + typeOfInput: RegistrationValidationInputType.create, + validationConfig: { + validatePhoneNumberLookup: true, + validateUniqueReferenceId: true, + validateExistingReferenceId: true, + }, + }), ).rejects.toThrow(HttpException); }); it('should not report errors for rows missing mandatory fields on bulk update', async () => { const csvArray = [ { - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, preferredLanguage: 'en', }, ]; await expect( - validator.validateAndCleanRegistrationsInput( - csvArray, + validator.validateAndCleanInput({ + registrationInputArray: csvArray, programId, userId, - dynamicAttributes, - RegistrationCsvValidationEnum.bulkUpdate, - ), + typeOfInput: RegistrationValidationInputType.update, + validationConfig: { + validateExistingReferenceId: false, + validatePhoneNumberLookup: false, + validateUniqueReferenceId: false, + }, + }), ).rejects.toThrow(HttpException); }); + + it('should report errors for a missing phonenumber when it is not allowed', async () => { + const csvArray = [ + { + namePartnerOrganization: 'ABC', + preferredLanguage: LanguageEnum.en, + maxPayments: '5', + nameFirst: 'Test', + nameLast: 'Test', + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, + whatsappPhoneNumber: '1234567890', + scope: 'country', + }, + ]; + + await expect( + validator.validateAndCleanInput({ + registrationInputArray: csvArray, + programId, + userId, + typeOfInput: RegistrationValidationInputType.create, + validationConfig: { + validateExistingReferenceId: true, + validatePhoneNumberLookup: true, + validateUniqueReferenceId: true, + }, + }), + ).rejects.toHaveProperty('response', [ + { + lineNumber: 1, + column: GenericRegistrationAttributes.phoneNumber, + value: undefined, + error: + 'PhoneNumber is required when creating a new registration for this program. Set allowEmptyPhoneNumber to true in the program settings to allow empty phone numbers', + }, + ]); + }); }); diff --git a/services/121-service/src/registration/validators/registrations-input-validator.ts b/services/121-service/src/registration/validators/registrations-input-validator.ts index 544375440b..a60fd127da 100644 --- a/services/121-service/src/registration/validators/registrations-input-validator.ts +++ b/services/121-service/src/registration/validators/registrations-input-validator.ts @@ -1,33 +1,31 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { validate } from 'class-validator'; -import { Equal, Repository } from 'typeorm'; +import { Equal, Not, Repository } from 'typeorm'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; import { LookupService } from '@121-service/src/notifications/lookup/lookup.service'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; -import { - BulkImportDto, - ImportRegistrationsDto, -} from '@121-service/src/registration/dto/bulk-import.dto'; -import { BulkUpdateDto } from '@121-service/src/registration/dto/bulk-update.dto'; +import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; import { AdditionalAttributes } from '@121-service/src/registration/dto/update-registration.dto'; -import { ValidationConfigDto } from '@121-service/src/registration/dto/validate-registration-config.dto'; -import { ValidateRegistrationErrorObjectDto } from '@121-service/src/registration/dto/validate-registration-error-object.dto'; import { - AnswerTypes, - Attribute, - AttributeWithOptionalLabel, - CustomAttributeType, - GenericAttributes, - QuestionType, -} from '@121-service/src/registration/enum/custom-data-attributes'; -import { RegistrationCsvValidationEnum } from '@121-service/src/registration/enum/registration-csv-validation.enum'; + GenericRegistrationAttributes, + RegistrationAttributeTypes, +} from '@121-service/src/registration/enum/registration-attribute.enum'; +import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; +import { RegistrationValidationInputType } from '@121-service/src/registration/enum/registration-validation-input-type.enum'; +import { ValidationRegistrationConfig } from '@121-service/src/registration/interfaces/validate-registration-config.interface'; +import { ValidateRegistrationErrorObject } from '@121-service/src/registration/interfaces/validate-registration-error-object.interface'; +import { ValidatedRegistrationInput } from '@121-service/src/registration/interfaces/validated-registration-input.interface'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; -import { RegistrationsInputValidatorHelpers } from '@121-service/src/registration/validators/registrations-input.validator.helper'; +import { RegistrationViewScopedRepository } from '@121-service/src/registration/repositories/registration-view-scoped.repository'; +import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { UserService } from '@121-service/src/user/user.service'; +type InputAttributeType = string | boolean | number | undefined; + @Injectable() export class RegistrationsInputValidator { @InjectRepository(ProgramEntity) @@ -38,17 +36,36 @@ export class RegistrationsInputValidator { constructor( private readonly userService: UserService, private readonly lookupService: LookupService, + private readonly registrationPaginationService: RegistrationsPaginationService, + private readonly registrationViewScopedRepository: RegistrationViewScopedRepository, ) {} - public async validateAndCleanRegistrationsInput( - csvArray: any[], - programId: number, - userId: number, - dynamicAttributes: AttributeWithOptionalLabel[], - typeOfInput: RegistrationCsvValidationEnum, - validationConfig: ValidationConfigDto = new ValidationConfigDto(), - ): Promise { - const errors: ValidateRegistrationErrorObjectDto[] = []; + public async validateAndCleanInput({ + registrationInputArray, + programId, + userId, + typeOfInput, + validationConfig, + }: { + registrationInputArray: Record[]; + programId: number; + userId: number; + typeOfInput: RegistrationValidationInputType; + validationConfig: ValidationRegistrationConfig; + }): Promise { + // empty map + let originalRegistrationsMap = new Map< + string, + MappedPaginatedRegistrationDto + >(); + if (typeOfInput === RegistrationValidationInputType.update) { + originalRegistrationsMap = await this.getOriginalRegistrationsOrThrow( + registrationInputArray, + programId, + ); + } + + const errors: ValidateRegistrationErrorObject[] = []; const phoneNumberLookupResults: Record = {}; const userScope = await this.userService.getUserScopeForProgram( @@ -57,11 +74,15 @@ export class RegistrationsInputValidator { ); if (validationConfig.validateUniqueReferenceId) { - this.validateUniqueReferenceIds(csvArray); + this.validateUniqueReferenceIds(registrationInputArray); } - const program = await this.programRepository.findOneByOrFail({ - id: programId, + const program = await this.programRepository.findOneOrFail({ + where: { id: Equal(programId) }, + relations: [ + 'programFinancialServiceProviderConfigurations', + 'programRegistrationAttributes', + ], }); const languageMapping = this.createLanguageMapping( @@ -69,86 +90,153 @@ export class RegistrationsInputValidator { ); const validatedArray: any = []; - const importRecordMap = { - [RegistrationCsvValidationEnum.importAsRegistered]: - ImportRegistrationsDto, - [RegistrationCsvValidationEnum.bulkUpdate]: BulkUpdateDto, - }; - - for (const [i, row] of csvArray.entries()) { - const importRecordClass = importRecordMap[typeOfInput]; - const importRecord = importRecordClass - ? new importRecordClass() - : ({} as any); + + for (const [i, row] of registrationInputArray.entries()) { + const originalRegistration = + typeOfInput === RegistrationValidationInputType.update + ? originalRegistrationsMap.get(row.referenceId as string) + : undefined; + + const validatedRegistrationInput: ValidatedRegistrationInput = { + data: {}, + }; /* * ============================================================= * Add default registration attributes without custom validation * ============================================================= */ - importRecord.fspName = row.fspName; - if (!program.paymentAmountMultiplierFormula) { - importRecord.paymentAmountMultiplier = row.paymentAmountMultiplier - ? +row.paymentAmountMultiplier - : null; + const { + errorOjb: errorObjPaymentAmountMultiplier, + validatedPaymentAmountMultiplier, + } = this.validatePaymentAmountMultiplier({ + value: row.paymentAmountMultiplier, + programPaymentAmountMultiplierFormula: + program.paymentAmountMultiplierFormula, + i, + }); + if (errorObjPaymentAmountMultiplier) { + errors.push(errorObjPaymentAmountMultiplier); + } else { + validatedRegistrationInput.paymentAmountMultiplier = + validatedPaymentAmountMultiplier; } - - if (program.enableMaxPayments) { - importRecord.maxPayments = row.maxPayments ? +row.maxPayments : null; + if (program.enableMaxPayments && row.maxPayments != null) { + const { errorObj: errorObjMaxPayments, validatedMaxPayments } = + this.validateMaxPayments({ + value: row.maxPayments, + originalRegistration, + i, + }); + if (errorObjMaxPayments) { + errors.push(errorObjMaxPayments); + } else { + validatedRegistrationInput.maxPayments = validatedMaxPayments; + } } /* * ======================================== - * Validate default registration attributes + * Validate default registration properties * ======================================== */ - const errorObjScope = this.validateRowScope( + const errorObjScope = this.validateRowScope({ row, userScope, i, - validationConfig, - ); + typeOfInput, + }); if (errorObjScope) { errors.push(errorObjScope); + } else if (program.enableScope) { + // We know that scope is undefined or string, or an error would have occured + validatedRegistrationInput.scope = row[AdditionalAttributes.scope] as + | undefined + | string; } - if (program.enableScope) { - importRecord.scope = row[AdditionalAttributes.scope]; - } - importRecord.referenceId = row.referenceId; - - const { errorObj: errorObjLanguage, preferredLanguage: _ } = - this.validatePreferredLanguage( - row.preferredLanguage, - languageMapping, - i, - validationConfig, - ); + const { + errorObj: errorObjLanguage, + preferredLanguage: preferredLanguage, + } = this.validatePreferredLanguage({ + preferredLanguage: row.preferredLanguage, + languageMapping, + i, + typeOfInput, + }); if (errorObjLanguage) { errors.push(errorObjLanguage); } - importRecord.preferredLanguage = this.updateLanguage( - row.preferredLanguage, - languageMapping, - ); + if (preferredLanguage) { + validatedRegistrationInput.preferredLanguage = preferredLanguage; + } - const errorObjReferenceId = await this.validateReferenceId( + const errorObjReferenceId = await this.validateReferenceId({ row, i, validationConfig, - ); + }); if (errorObjReferenceId) { errors.push(errorObjReferenceId); + } else if (row.referenceId != null) { + validatedRegistrationInput.referenceId = row.referenceId as string; } - importRecord.referenceId = row.referenceId; - const errorObj = this.validatePhoneNumberEmpty(row, i, validationConfig); - if (errorObj) { - errors.push(errorObj); - } else { - importRecord.phoneNumber = row.phoneNumber ? row.phoneNumber : ''; // If the phone number is empty use an empty string + const errorObjValidatePhoneNr = this.validatePhoneNumberEmpty({ + row, + i, + program, + typeOfInput, + }); + if (errorObjValidatePhoneNr) { + errors.push(errorObjValidatePhoneNr); + } else if (row.phoneNumber !== undefined) { + validatedRegistrationInput.phoneNumber = row.phoneNumber + ? String(row.phoneNumber) + : null; + } + + /* + * ============================================= + * Validate fsp config related attributes + * ============================================= + */ + const errorObjFspConfig = this.validateProgramFspConfigurationName({ + programFinancialServiceProviderConfigurationName: + row[ + AdditionalAttributes + .programFinancialServiceProviderConfigurationName + ], + programFinancialServiceProviderConfigurations: + program.programFinancialServiceProviderConfigurations, + i, + typeOfInput, + }); + if (errorObjFspConfig) { + errors.push(errorObjFspConfig); + } else if ( + row[ + AdditionalAttributes.programFinancialServiceProviderConfigurationName + ] as string + ) { + validatedRegistrationInput[ + AdditionalAttributes.programFinancialServiceProviderConfigurationName + ] = row[ + AdditionalAttributes.programFinancialServiceProviderConfigurationName + ] as string; } + const errorObjsFspRequiredAttributes = this.validateFspRequiredAttributes( + { + row, + originalRegistration, + programFinancialServiceProviderConfigurations: + program.programFinancialServiceProviderConfigurations, + i, + }, + ); + errors.push(...errorObjsFspRequiredAttributes); + /* * ============================================= * Validate dynamic registration data attributes @@ -156,21 +244,29 @@ export class RegistrationsInputValidator { */ // Filter dynamic atttributes that are not relevant for this fsp if question is only fsp specific - const dynamicAttributesForFsp = dynamicAttributes.filter((att) => - this.isDynamicAttributeForFsp(att, row.fspName), - ); await Promise.all( - dynamicAttributesForFsp.map(async (att) => { + program.programRegistrationAttributes.map(async (att) => { // Skip validation if the attribute is not present in the row and it is a bulk update because you do not have to update all attributes in a bulk update if ( - typeOfInput === RegistrationCsvValidationEnum.bulkUpdate && - row[att.name] == null + typeOfInput === RegistrationValidationInputType.update && + row[att.name] === undefined ) { return; } - if (att.type === AnswerTypes.tel) { + // If attribute is not required skip in case of undefined and on null add to validatedRegistrationInput so it will be removed later on + if (!att.isRequired) { + if (row[att.name] === undefined) { + return; + } + if (row[att.name] == null) { + validatedRegistrationInput.data[att.name] = null; + return; + } + } + + if (att.type === RegistrationAttributeTypes.tel) { /* * ================================================================== * If an attribute is a phone number, validate it using Twilio lookup @@ -179,31 +275,36 @@ export class RegistrationsInputValidator { if (row[att.name] && validationConfig.validatePhoneNumberLookup) { const { errorObj, sanitized } = - await this.validateLookupPhoneNumber( - row[att.name], + await this.validateLookupPhoneNumber({ + value: row[att.name], i, phoneNumberLookupResults, - ); + }); if (errorObj) { errors.push(errorObj); - } else { - phoneNumberLookupResults[row[att.name]] = sanitized; - importRecord[att.name] = sanitized; + } else if (row[att.name]) { + // we can assume here that the orginal value is a string else it would not have returned an error object + phoneNumberLookupResults[row[att.name] as string] = sanitized; + validatedRegistrationInput.data[att.name] = sanitized as string; } } else { - importRecord[att.name] = row[att.name] ? row[att.name] : ''; // If the phone number is empty use an empty string + validatedRegistrationInput.data[att.name] = row[att.name] + ? (row[att.name] as string) + : null; } return; } - if (att.type === AnswerTypes.dropdown) { + if (att.type === RegistrationAttributeTypes.dropdown) { const optionNames = att.options ? att.options?.map((option) => option.option) : []; - if (optionNames.includes(row[att.name])) { + if (optionNames.includes(String(row[att.name]))) { // Validation passed - importRecord[att.name] = row[att.name]; + validatedRegistrationInput.data[att.name] = row[ + att.name + ] as string; return; } @@ -228,16 +329,23 @@ export class RegistrationsInputValidator { * If an attribute is anything else, validate it as such * ============================================================ */ - const errorObj = this.validateNonTelephoneDynamicAttribute( - row[att.name], - att.type, - att.name, + if (att.isRequired === false && row[att.name] == null) { + validatedRegistrationInput.data[att.name] = null; + return; + } + const errorObj = this.validateTypesNumericBoolTextDate({ + value: row[att.name], + type: att.type, + attribute: att.name, i, - ); + }); if (errorObj && Object.keys(row).includes(att.name)) { errors.push(errorObj); - } else { - importRecord[att.name] = row[att.name]; + } else if (row[att.name] !== undefined) { + validatedRegistrationInput.data[att.name] = row[att.name] as + | string + | number + | boolean; } }), ); @@ -247,24 +355,23 @@ export class RegistrationsInputValidator { throw new HttpException(errors, HttpStatus.BAD_REQUEST); } - if (validationConfig.validateClassValidator) { - const result = await validate(importRecord); - if (result.length > 0) { - let error = result[0].toString(); - if (result[0]?.constraints) { - error = Object.values(result[0].constraints).join(', '); - } - - const errorObj = { - lineNumber: i + 1, - column: result[0].property, - value: result[0].value, - error, - }; - errors.push(errorObj); + const result = await validate(validatedRegistrationInput); + if (result.length > 0) { + let error = result[0].toString(); + if (result[0]?.constraints) { + error = Object.values(result[0].constraints).join(', '); } + + const errorObj = { + lineNumber: i + 1, + column: result[0].property, + value: result[0].value, + error, + }; + errors.push(errorObj); } - validatedArray.push(importRecord); + + validatedArray.push(validatedRegistrationInput); } // Throw the errors at once @@ -275,10 +382,12 @@ export class RegistrationsInputValidator { return validatedArray; } - private validateUniqueReferenceIds(csvArray: any[]): void { + private validateUniqueReferenceIds( + csvArray: Record[], + ): void { const allReferenceIds = csvArray - .filter((row) => row.referenceId) - .map((row) => row.referenceId); + .filter((row) => row[AdditionalAttributes.referenceId]) + .map((row) => row[AdditionalAttributes.referenceId]); const uniqueReferenceIds = [...new Set(allReferenceIds)]; if (uniqueReferenceIds.length < allReferenceIds.length) { throw new HttpException( @@ -288,7 +397,9 @@ export class RegistrationsInputValidator { } } - private createLanguageMapping(programLanguages: string[]): object { + private createLanguageMapping( + programLanguages: string[], + ): Record { const languageNamesApi = new Intl.DisplayNames(['en'], { type: 'language', }); @@ -309,140 +420,242 @@ export class RegistrationsInputValidator { return mapping; } - private validatePreferredLanguage( - preferredLanguage: string, - languageMapping: any, - i: number, - validationConfig: ValidationConfigDto = new ValidationConfigDto(), - ): { - errorObj?: ValidateRegistrationErrorObjectDto; - preferredLanguage?: LanguageEnum; - } { - if (validationConfig.validatePreferredLanguage) { - const cleanedPreferredLanguage = - typeof preferredLanguage === 'string' - ? preferredLanguage.trim().toLowerCase() - : preferredLanguage; - - const errorObj = this.checkLanguage( - cleanedPreferredLanguage, - languageMapping, - i, - ); + private validateProgramFspConfigurationName({ + programFinancialServiceProviderConfigurationName, + programFinancialServiceProviderConfigurations, + i, + typeOfInput, + }: { + programFinancialServiceProviderConfigurationName: InputAttributeType; + programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[]; + i: number; + typeOfInput: RegistrationValidationInputType; + }): ValidateRegistrationErrorObject | undefined { + // The registration is being patched, and the programFinancialServiceProviderConfigurationName is not being updated so the validation can be skipped + if ( + typeOfInput === RegistrationValidationInputType.update && + (programFinancialServiceProviderConfigurationName == null || + programFinancialServiceProviderConfigurationName === '') + ) { + return; + } - if (errorObj) { - return { errorObj, preferredLanguage: undefined }; - } else { - const value = this.updateLanguage( - cleanedPreferredLanguage, - languageMapping, - ); - return { errorObj: undefined, preferredLanguage: value }; - } + if ( + !programFinancialServiceProviderConfigurationName || + !programFinancialServiceProviderConfigurations.some( + (fspConfig) => + fspConfig.name === programFinancialServiceProviderConfigurationName, + ) + ) { + return { + lineNumber: i, + value: programFinancialServiceProviderConfigurationName, + column: + AdditionalAttributes.programFinancialServiceProviderConfigurationName, + error: `FinancialServiceProviderConfigurationName ${programFinancialServiceProviderConfigurationName} not found in program. Allowed values: ${programFinancialServiceProviderConfigurations + .map((fspConfig) => fspConfig.name) + .join(', ')}`, + }; } - return { - errorObj: undefined, - preferredLanguage: preferredLanguage as LanguageEnum, - }; } - private checkLanguage( - inPreferredLanguage: string, - programLanguageMapping: object, - i: number, - ): ValidateRegistrationErrorObjectDto | undefined { + private validatePreferredLanguage({ + preferredLanguage, + languageMapping, + i, + typeOfInput, + }: { + preferredLanguage: InputAttributeType; + languageMapping: any; + i: number; + typeOfInput: RegistrationValidationInputType; + }): { + errorObj: ValidateRegistrationErrorObject | undefined; + preferredLanguage: LanguageEnum | undefined; + } { + const errorObj = this.checkLanguage({ + preferredLanguage, + languageMapping, + i, + typeOfInput, + }); + const cleanedPreferredLanguage = - typeof inPreferredLanguage === 'string' - ? inPreferredLanguage.trim().toLowerCase() - : inPreferredLanguage; - if (!cleanedPreferredLanguage) { + typeof preferredLanguage === 'string' + ? preferredLanguage.trim().toLowerCase() + : String(preferredLanguage); + if (errorObj) { + return { errorObj, preferredLanguage: undefined }; + } + const value = this.updateLanguage( + cleanedPreferredLanguage, + languageMapping, + ); + return { errorObj: undefined, preferredLanguage: value }; + } + + private checkLanguage({ + preferredLanguage, + languageMapping, + i, + typeOfInput, + }: { + preferredLanguage: InputAttributeType; + languageMapping: object; + i: number; + typeOfInput: RegistrationValidationInputType; + }): ValidateRegistrationErrorObject | undefined { + if ( + typeOfInput === RegistrationValidationInputType.update && + !preferredLanguage === undefined + ) { + return; + } + if (!preferredLanguage) { return; } else if ( - !Object.keys(programLanguageMapping).includes(cleanedPreferredLanguage) && - !Object.values(programLanguageMapping).some( - (x) => x.toLowerCase() == cleanedPreferredLanguage.toLowerCase(), + !Object.keys(languageMapping).includes(preferredLanguage.toString()) && + !Object.values(languageMapping).some( + (x) => x.toLowerCase() == preferredLanguage.toString().toLowerCase(), ) ) { return { lineNumber: i + 1, column: AdditionalAttributes.preferredLanguage, - value: inPreferredLanguage, - error: `Language error: Allowed values of this program for preferredLanguage: ${Object.values( - programLanguageMapping, - ).join(', ')}, ${Object.keys(programLanguageMapping).join(', ')}`, + value: preferredLanguage, + error: `Language error: Allowed values of this program for ${AdditionalAttributes.preferredLanguage}: ${Object.values( + languageMapping, + ).join(', ')}, ${Object.keys(languageMapping).join(', ')}`, }; } } private updateLanguage( - inPreferredLanguage: string, + preferredLanguage: string | undefined, programLanguageMapping: object, ): LanguageEnum | undefined { - const cleanedPreferredLanguage = - typeof inPreferredLanguage === 'string' - ? inPreferredLanguage.trim().toLowerCase() - : inPreferredLanguage; - if (!cleanedPreferredLanguage) { + if (!preferredLanguage) { return LanguageEnum.en; - } else if ( - Object.keys(programLanguageMapping).includes(cleanedPreferredLanguage) - ) { - return programLanguageMapping[cleanedPreferredLanguage]; + } + if (Object.keys(programLanguageMapping).includes(preferredLanguage)) { + return programLanguageMapping[preferredLanguage]; } else if ( Object.values(programLanguageMapping).some( - (x) => x.toLowerCase() == cleanedPreferredLanguage.toLowerCase(), + (x) => x.toLowerCase() == preferredLanguage.toLowerCase(), ) ) { for (const value of Object.values(programLanguageMapping)) { - if (value.toLowerCase() === cleanedPreferredLanguage) { + if (value.toLowerCase() === preferredLanguage) { return value; } } } } - private validateRowScope( - row: any, - userScope: string, - i: number, - validationConfig: ValidationConfigDto, - ): ValidateRegistrationErrorObjectDto | undefined { - if (validationConfig.validateScope) { - const correctScope = this.recordHasAllowedScope(row, userScope); - if (!correctScope) { - return { - lineNumber: i + 1, - column: AdditionalAttributes.scope, - value: row[AdditionalAttributes.scope], - error: `User has program scope ${userScope} and does not have access to registration scope ${ - row[AdditionalAttributes.scope] - }`, - }; - } + private validateRowScope({ + row, + userScope, + i, + typeOfInput, + }: { + row: Record; + userScope: string; + i: number; + typeOfInput: RegistrationValidationInputType; + }): ValidateRegistrationErrorObject | undefined { + const correctScope = this.rowHasAllowedScope({ + row, + userScope, + typeOfInput, + }); + if (!correctScope) { + return { + lineNumber: i + 1, + column: AdditionalAttributes.scope, + value: row[AdditionalAttributes.scope], + error: `User has program scope ${userScope} and does not have access to registration scope ${ + row[AdditionalAttributes.scope] + }`, + }; } } - private recordHasAllowedScope(record: any, userScope: string): boolean { - return ( - (userScope && - record[AdditionalAttributes.scope]?.startsWith(userScope)) || - !userScope - ); + private rowHasAllowedScope({ + row, + userScope, + typeOfInput, + }: { + row: Record; + userScope: string; + typeOfInput: RegistrationValidationInputType; + }): boolean { + const scopeOfInput = row[AdditionalAttributes.scope]; + + // Other types than string or null/undefined are not allowed + if (typeof scopeOfInput !== 'string' && scopeOfInput != null) { + return false; + } + + // All scopes allowed for this user + if (userScope === '') { + return true; + } + + // If scopeOfInput is null, return true for bulkUpdate, false otherwise + if (scopeOfInput == null) { + return typeOfInput === RegistrationValidationInputType.update; + } + + // Check if scopeOfInput starts with userScope + return scopeOfInput.startsWith(userScope); } - private async validateReferenceId( - row: any, - i: number, - validationConfig: ValidationConfigDto, - ): Promise { - if (validationConfig.validateExistingReferenceId && row.referenceId) { + private async validateReferenceId({ + row, + i, + validationConfig, + }: { + row: Record; + i: number; + validationConfig: ValidationRegistrationConfig; + }): Promise { + if (!row.referenceId) { + return; + } + if (typeof row.referenceId !== 'string') { + return { + lineNumber: i + 1, + column: GenericRegistrationAttributes.referenceId, + value: row.referenceId, + error: 'referenceId must be a string', + }; + } + + if (row.referenceId.includes('$')) { + return { + lineNumber: i + 1, + column: GenericRegistrationAttributes.referenceId, + value: row.referenceId, + error: `${GenericRegistrationAttributes.referenceId} contains a $ character`, + }; + } + + if (row.referenceId.length < 5 || row.referenceId.length > 200) { + return { + lineNumber: i + 1, + column: GenericRegistrationAttributes.referenceId, + value: row.referenceId, + error: 'referenceId must be between 5 and 200 characters', + }; + } + if (validationConfig.validateExistingReferenceId) { const registration = await this.registrationRepository.findOne({ where: { referenceId: Equal(row.referenceId) }, }); if (registration) { return { lineNumber: i + 1, - column: GenericAttributes.referenceId, + column: GenericRegistrationAttributes.referenceId, value: row.referenceId, error: 'referenceId already exists in database', }; @@ -450,117 +663,374 @@ export class RegistrationsInputValidator { } } - private validatePhoneNumberEmpty( - row: any, - i: number, - validationConfig: ValidationConfigDto, - ): ValidateRegistrationErrorObjectDto | undefined { - if (!row.phoneNumber && validationConfig.validatePhoneNumberEmpty) { - return { - lineNumber: i + 1, - column: GenericAttributes.phoneNumber, - value: row.phoneNumber, - error: 'PhoneNumber is not allowed to be empty', - }; - } - } - - private isDynamicAttributeForFsp( - attribute: Attribute | AttributeWithOptionalLabel, - fspName: FinancialServiceProviders, - ): boolean { - // If the CSV does not have fspName all attributes may be relevant because a bulk PATCH may be for multiple FSPs - if (!fspName) { - return true; + private validatePhoneNumberEmpty({ + row, + i, + program, + typeOfInput, + }: { + row: any; + i: number; + program: ProgramEntity; + typeOfInput: RegistrationValidationInputType; + }): ValidateRegistrationErrorObject | undefined { + // If the program allows empty phone numbers, skip this validation + if (program.allowEmptyPhoneNumber) { + return; } if ( - attribute.questionTypes && - (attribute.questionTypes.length > 1 || - attribute.questionTypes[0] !== QuestionType.fspQuestion) + typeOfInput === RegistrationValidationInputType.create && + !row.phoneNumber ) { - // The attribute has multiple question types or is not FSP-specific - return true; + return { + lineNumber: i + 1, + column: GenericRegistrationAttributes.phoneNumber, + value: undefined, + error: + 'PhoneNumber is required when creating a new registration for this program. Set allowEmptyPhoneNumber to true in the program settings to allow empty phone numbers', + }; } if ( - attribute.questionTypes && - attribute.questionTypes.length === 1 && - attribute.questionTypes[0] === QuestionType.fspQuestion && - attribute.fspNames?.includes(fspName) + typeOfInput === RegistrationValidationInputType.update && + row.phoneNumber === '' ) { - // The attribute has a single question type that is FSP-specific and is relevant for the FSP of this registration - return true; + // on an update phonenumber can be empty if it is not being updated + return { + lineNumber: i + 1, + column: GenericRegistrationAttributes.phoneNumber, + value: row.phoneNumber, + error: + 'PhoneNumber is not allowed to be updated to an empty value. Set allowEmptyPhoneNumber to true in the program settings to allow empty phone numbers', + }; } - - // The attribute is not relevant - return false; } - private async validateLookupPhoneNumber( - value: string, - i: number, - phoneNumberLookupResults: Record, - ): Promise<{ - errorObj?: ValidateRegistrationErrorObjectDto; + private async validateLookupPhoneNumber({ + value, + i, + phoneNumberLookupResults, + }: { + value: InputAttributeType; + i: number; + phoneNumberLookupResults: Record; + }): Promise<{ + errorObj?: ValidateRegistrationErrorObject; sanitized?: string; }> { let sanitized: string | undefined; - if (phoneNumberLookupResults[value]) { - sanitized = phoneNumberLookupResults[value]; + const valueString = value ? value.toString() : ''; + if (phoneNumberLookupResults[valueString]) { + sanitized = phoneNumberLookupResults[valueString]; } else { - sanitized = await this.lookupService.lookupAndCorrect(value, true); + sanitized = await this.lookupService.lookupAndCorrect(valueString, true); } if (!sanitized && !!value) { - const errorObj = { + const errorObj: ValidateRegistrationErrorObject = { lineNumber: i + 1, column: 'phoneNumber', value, - error: 'PhoneNumber is not valid according to Twilio lookup', + error: + 'This value is not a valid phonenumber according to Twilio lookup', }; return { errorObj, sanitized }; } return { errorObj: undefined, sanitized }; } - private validateNonTelephoneDynamicAttribute( - value: string, - type: string, - columnName: string, - i: number, - ): ValidateRegistrationErrorObjectDto | undefined { - const cleanedValue = this.cleanNonTelephoneDynamicAttribute(value, type); - if (cleanedValue === null) { - const errorObj = { + private validateFspRequiredAttributes({ + row, + originalRegistration, + programFinancialServiceProviderConfigurations, + i, + }: { + row: object; + originalRegistration: MappedPaginatedRegistrationDto | undefined; + programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[]; + i: number; + }): ValidateRegistrationErrorObject[] { + // Decide which required attributes to check + // If the updated row has a value a new fsp configuration name, check the required attributes for that fsp + // Otherwise, check the required attributes for the original registration that is in the database + + const relevantFspConfigName = + row[ + GenericRegistrationAttributes + .programFinancialServiceProviderConfigurationName + ] ?? + originalRegistration?.programFinancialServiceProviderConfigurationName; + if (!relevantFspConfigName) { + // If the programFinancialServiceProviderConfigurationName is neither in the row nor in the original registration, we cannot check the required attributes + // Errors will be thrown in a different validation step + return []; + } + + const requiredAttributes = this.getRequiredAttributesForFsp( + relevantFspConfigName, + programFinancialServiceProviderConfigurations, + ); + const errors: ValidateRegistrationErrorObject[] = []; + for (const attribute of requiredAttributes) { + // Check if required attributes are not being deleted or set to nullable in the PATCH / POST request + if (row.hasOwnProperty(attribute)) { + if (row[attribute] == null || row[attribute] === '') { + errors.push({ + lineNumber: i + 1, + column: attribute, + value: row[attribute], + error: `Cannot update/set ${attribute} with a nullable value as it is required for the FSP: ${relevantFspConfigName}`, + }); + continue; + } + } + + // If the programFinancialServiceProviderConfigurationName being updated / set in this request + // check if a combination orignal registration and new row has all required attributes + if ( + row[ + GenericRegistrationAttributes + .programFinancialServiceProviderConfigurationName + ] + ) { + // Check if the required attributes are present in the row + if ( + !this.isRequiredAttributeInObject(attribute, row) && + !this.isRequiredAttributeInObject(attribute, originalRegistration) + ) { + errors.push({ + lineNumber: i + 1, + column: attribute, + value: undefined, + error: `Cannot update '${attribute}' is required for the FSP: '${relevantFspConfigName}'`, + }); + } + } + } + return errors; + } + + private isRequiredAttributeInObject( + attribute: string, + body: object | undefined, + ): boolean { + if (!body) { + return false; + } + return ( + body.hasOwnProperty(attribute) && + body[attribute] != null && + body[attribute] !== '' + ); + } + + private getRequiredAttributesForFsp( + programFinancialServiceProviderConfigurationName: string, + programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[], + ): string[] { + const fspName = programFinancialServiceProviderConfigurations.find( + (programFspConfig) => + programFspConfig.name === + programFinancialServiceProviderConfigurationName, + )?.financialServiceProviderName; + const foundFsp = FINANCIAL_SERVICE_PROVIDER_SETTINGS.find( + (fsp) => fsp.name === fspName, + ); + if (!foundFsp) { + return []; + } + const requiredAttributes = foundFsp.attributes.filter( + (attribute) => attribute.isRequired, + ); + return requiredAttributes.map((attribute) => attribute.name); + } + + private async getOriginalRegistrationsOrThrow( + csvArray: object[], + programId: number, + ): Promise> { + const referenceIds = csvArray + .filter((row) => row[GenericRegistrationAttributes.referenceId]) + .map((row) => row[GenericRegistrationAttributes.referenceId]); + let qb = this.registrationViewScopedRepository + .createQueryBuilder('registration') + .andWhere({ status: Not(RegistrationStatusEnum.deleted) }); + if (referenceIds.length > 0) { + qb = qb.andWhere('registration.referenceId IN (:...referenceIds)', { + referenceIds, + }); + } + const originalRegistrations = + await this.registrationPaginationService.getRegistrationsChunked( + programId, + { limit: 10000, path: '' }, + 10000, + qb, + ); + const originalRegistrationsMap = new Map( + originalRegistrations.map((reg) => [reg.referenceId, reg]), + ); + const notFoundIds = referenceIds.filter( + (id) => !originalRegistrationsMap.has(id), + ); + if (notFoundIds.length > 0) { + throw new HttpException( + `The following referenceIds were not found in the database: ${notFoundIds.join(', ')}`, + HttpStatus.NOT_FOUND, + ); + } + return originalRegistrationsMap; + } + + // Basic here mean anything that is not a telephone number or a dropdown + private validateTypesNumericBoolTextDate({ + value, + type, + attribute, + i, + }: { + value: string[] | string | number | boolean | undefined; + type: string; + attribute: string; + i: number; + }): ValidateRegistrationErrorObject | undefined { + let isValid: boolean | null = false; + let message = ''; + if (type === RegistrationAttributeTypes.date) { + const datePattern = + /^(0?[1-9]|[12][0-9]|3[01])-(0?[1-9]|1[0-2])-(19[2-9][0-9]|20[0-1][0-9])$/; + isValid = typeof value === 'string' && datePattern.test(value); + } else if (type === RegistrationAttributeTypes.numeric) { + isValid = value != null && !isNaN(+value); + } else if (type === RegistrationAttributeTypes.numericNullable) { + isValid = value == null || !isNaN(+value); + } else if (type === RegistrationAttributeTypes.text) { + isValid = typeof value === 'string'; + } else if (type === RegistrationAttributeTypes.boolean) { + isValid = this.valueIsBool(value); + } else { + message = `Type '${type}' is unknown'`; + } + if (!isValid) { + return { lineNumber: i + 1, - column: columnName, - value, - error: `Value is not a valid ${type}`, + column: attribute, + value: Array.isArray(value) ? value.toString() : value, + error: message + ? message + : this.createErrorMessageInvalidAttributeType({ + type, + value, + attribute, + }), }; - return errorObj; } } - private cleanNonTelephoneDynamicAttribute( - value: string, - type: string, - ): number | boolean | string | null { - switch (type) { - case AnswerTypes.numeric: - if (value == null) { - return null; - } - // Convert the value to a number and return it - // If the value is not a number, return null - return isNaN(Number(value)) ? null : Number(value); - case CustomAttributeType.boolean: - // Convert the value to a boolean and return it - // If the value is not a boolean, return null - const convertedValue = - RegistrationsInputValidatorHelpers.stringToBoolean(value); - return convertedValue === undefined ? null : convertedValue; - default: - // If the type is neither numeric nor boolean, return the original value - return value; + private createErrorMessageInvalidAttributeType({ + type, + value, + attribute, + }: { + type: string; + value: string[] | string | number | boolean | undefined; + attribute: string; + }): string { + const valueString = Array.isArray(value) ? JSON.stringify(value) : value; + return `The value '${valueString}' given for the attribute '${attribute}' does not have the correct format for type '${type}'`; + } + + private valueIsBool( + value: string[] | string | number | boolean | undefined, + ): boolean { + if (typeof value === 'boolean') { + return true; + } + if (typeof value !== 'string') { + return false; + } + const allowedValues = ['true', 'yes', '1', 'false', '0', 'no', null]; + return allowedValues.includes(value); + } + + private validatePaymentAmountMultiplier({ + value, + programPaymentAmountMultiplierFormula, + i, + }: { + value: InputAttributeType; + programPaymentAmountMultiplierFormula: string | null; + i: number; + }): { + errorOjb?: ValidateRegistrationErrorObject | undefined; + validatedPaymentAmountMultiplier?: number | undefined; + } { + if (programPaymentAmountMultiplierFormula && value != null) { + return { + errorOjb: { + lineNumber: i + 1, + column: GenericRegistrationAttributes.paymentAmountMultiplier, + value, + error: + 'Program has a paymentAmountMultiplierFormula, so the paymentAmountMultiplier should not be set as it will be calculated', + }, + }; + } + if (value == null) { + // The value is not set, so no further validation is needed and the value will later be stored as 1 + return { + validatedPaymentAmountMultiplier: undefined, + }; + } + if (isNaN(+value) || +value <= 0) { + return { + errorOjb: { + lineNumber: i + 1, + column: GenericRegistrationAttributes.paymentAmountMultiplier, + value, + error: 'this field must be a positive number', + }, + }; + } + return { validatedPaymentAmountMultiplier: +value }; + } + + private validateMaxPayments({ + value, + originalRegistration, + i, + }: { + value: InputAttributeType; + originalRegistration: MappedPaginatedRegistrationDto | undefined; + i: number; + }): { + errorObj?: ValidateRegistrationErrorObject | undefined; + validatedMaxPayments?: number | undefined; + } { + // It's always allowed to remove the maxPayments value + // When you upload a csv file, the value is an empty string + if (value == null || value === '') { + return { validatedMaxPayments: undefined }; + } + if (isNaN(+value) || +value <= 0) { + return { + errorObj: { + lineNumber: i + 1, + column: GenericRegistrationAttributes.maxPayments, + value, + error: 'MaxPayments must be a positive number or left empty', + }, + }; + } + if (originalRegistration && +value < originalRegistration.paymentCount) { + return { + errorObj: { + lineNumber: i + 1, + column: GenericRegistrationAttributes.maxPayments, + value, + error: `MaxPayments cannot be lower than the current paymentCount (${originalRegistration.paymentCount})`, + }, + }; } + return { validatedMaxPayments: +value }; } } diff --git a/services/121-service/src/registration/validators/registrations-input.validator.helper.spec.ts b/services/121-service/src/registration/validators/registrations-input.validator.helper.spec.ts index a24a3d51f1..ca12d722ca 100644 --- a/services/121-service/src/registration/validators/registrations-input.validator.helper.spec.ts +++ b/services/121-service/src/registration/validators/registrations-input.validator.helper.spec.ts @@ -3,50 +3,43 @@ import { RegistrationsInputValidatorHelpers } from '@121-service/src/registratio describe('RegistrationsInputValidatorHelpers', () => { describe('stringToBoolean', () => { it('should convert "true", "yes", and "1" to true', () => { - expect(RegistrationsInputValidatorHelpers.stringToBoolean('true')).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('true')).toBe( true, ); - expect(RegistrationsInputValidatorHelpers.stringToBoolean('yes')).toBe( - true, - ); - expect(RegistrationsInputValidatorHelpers.stringToBoolean('1')).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('yes')).toBe( true, ); + expect(RegistrationsInputValidatorHelpers.inputToBoolean('1')).toBe(true); }); it('should convert "false", "no", "0", "", and null to false', () => { - expect(RegistrationsInputValidatorHelpers.stringToBoolean('false')).toBe( - false, - ); - expect(RegistrationsInputValidatorHelpers.stringToBoolean('no')).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('false')).toBe( false, ); - expect(RegistrationsInputValidatorHelpers.stringToBoolean('0')).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('no')).toBe( false, ); - expect(RegistrationsInputValidatorHelpers.stringToBoolean('')).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('0')).toBe( false, ); - expect(RegistrationsInputValidatorHelpers.stringToBoolean(null)).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('')).toBe(false); + expect(RegistrationsInputValidatorHelpers.inputToBoolean(null)).toBe( false, ); }); it('should return undefined for unrecognized strings if no default value is provided', () => { expect( - RegistrationsInputValidatorHelpers.stringToBoolean('unrecognized'), + RegistrationsInputValidatorHelpers.inputToBoolean('unrecognized'), ).toBeUndefined(); }); it('should return the default value for unrecognized strings if provided', () => { expect( - RegistrationsInputValidatorHelpers.stringToBoolean( - 'unrecognized', - true, - ), + RegistrationsInputValidatorHelpers.inputToBoolean('unrecognized', true), ).toBe(true); expect( - RegistrationsInputValidatorHelpers.stringToBoolean( + RegistrationsInputValidatorHelpers.inputToBoolean( 'unrecognized', false, ), @@ -55,24 +48,24 @@ describe('RegistrationsInputValidatorHelpers', () => { it('should return the default value for undefined input if provided', () => { expect( - RegistrationsInputValidatorHelpers.stringToBoolean(undefined, true), + RegistrationsInputValidatorHelpers.inputToBoolean(undefined, true), ).toBe(true); expect( - RegistrationsInputValidatorHelpers.stringToBoolean(undefined, false), + RegistrationsInputValidatorHelpers.inputToBoolean(undefined, false), ).toBe(false); }); it('should return undefined for undefined input if no default value is provided', () => { expect( - RegistrationsInputValidatorHelpers.stringToBoolean(undefined), + RegistrationsInputValidatorHelpers.inputToBoolean(undefined), ).toBeUndefined(); }); it('should handle boolean input by returning it directly', () => { - expect(RegistrationsInputValidatorHelpers.stringToBoolean('true')).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('true')).toBe( true, ); - expect(RegistrationsInputValidatorHelpers.stringToBoolean('false')).toBe( + expect(RegistrationsInputValidatorHelpers.inputToBoolean('false')).toBe( false, ); }); diff --git a/services/121-service/src/registration/validators/registrations-input.validator.helper.ts b/services/121-service/src/registration/validators/registrations-input.validator.helper.ts index c4a887e343..5e0d612201 100644 --- a/services/121-service/src/registration/validators/registrations-input.validator.helper.ts +++ b/services/121-service/src/registration/validators/registrations-input.validator.helper.ts @@ -1,23 +1,27 @@ export class RegistrationsInputValidatorHelpers { - static stringToBoolean( - string: string | null | undefined, + static inputToBoolean( + input: string | null | undefined | number | boolean, defaultValue?: boolean, ): boolean | undefined { - if (typeof string === 'boolean') { - return string; + if (typeof input === 'boolean') { + return input; } - if (string === null) { + if (typeof input === 'number') { + return input === 1; + } + + if (input === null) { return false; } - if (string === undefined) { + if (input === undefined) { return this.isValueUndefinedOrNull(defaultValue) ? undefined : defaultValue; } - switch (string.toLowerCase().trim()) { + switch (input.toLowerCase().trim()) { case 'true': case 'yes': case '1': diff --git a/services/121-service/src/scoped.repository.spec.ts b/services/121-service/src/scoped.repository.spec.ts index 2239363f86..aa5c4e632a 100644 --- a/services/121-service/src/scoped.repository.spec.ts +++ b/services/121-service/src/scoped.repository.spec.ts @@ -1,15 +1,15 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { TestBed } from '@automock/jest'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { ScopedRepository } from '@121-service/src/scoped.repository'; describe('ScopedRepository', () => { - let scopedRepository: ScopedRepository; + let scopedRepository: ScopedRepository; beforeEach(() => { const { unit: scopedRepositoryUnit } = TestBed.create( - ScopedRepository, + ScopedRepository, ).compile(); scopedRepository = scopedRepositoryUnit; diff --git a/services/121-service/src/scripts/seed-helper.ts b/services/121-service/src/scripts/seed-helper.ts index 0111852c66..33b2d3affa 100644 --- a/services/121-service/src/scripts/seed-helper.ts +++ b/services/121-service/src/scripts/seed-helper.ts @@ -1,19 +1,24 @@ -import { HttpException, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import crypto from 'crypto'; import { DataSource, DeepPartial, Equal, In } from 'typeorm'; import { DEBUG } from '@121-service/src/config'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { FspQuestionEntity } from '@121-service/src/financial-service-providers/fsp-question.entity'; +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviderDto } from '@121-service/src/financial-service-providers/financial-service-provider.dto'; +import { FINANCIAL_SERVICE_PROVIDER_SETTINGS } from '@121-service/src/financial-service-providers/financial-service-providers-settings.const'; import { MessageTemplateEntity } from '@121-service/src/notifications/message-template/message-template.entity'; import { MessageTemplateService } from '@121-service/src/notifications/message-template/message-template.service'; import { OrganizationEntity } from '@121-service/src/organization/organization.entity'; -import { ProgramFinancialServiceProviderConfigurationsService } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.service'; +import { ProgramFinancialServiceProviderConfigurationEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration.entity'; +import { ProgramFinancialServiceProviderConfigurationPropertyEntity } from '@121-service/src/program-financial-service-provider-configurations/entities/program-financial-service-provider-configuration-property.entity'; +import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-service/src/program-financial-service-provider-configurations/program-financial-service-provider-configurations.repository'; import { ProgramEntity } from '@121-service/src/programs/program.entity'; import { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity'; -import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity'; -import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity'; -import { AnswerTypes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; import { UserEntity } from '@121-service/src/user/user.entity'; import { UserRoleEntity } from '@121-service/src/user/user-role.entity'; @@ -25,7 +30,7 @@ export class SeedHelper { public constructor( private dataSource: DataSource, private readonly messageTemplateService: MessageTemplateService, - private readonly programFspConfigurationService: ProgramFinancialServiceProviderConfigurationsService, + private readonly programFspConfigurationRepository: ProgramFinancialServiceProviderConfigurationRepository, ) {} public async addDefaultUsers( program: ProgramEntity, @@ -232,112 +237,122 @@ export class SeedHelper { isApiTests: boolean, ): Promise { const programRepository = this.dataSource.getRepository(ProgramEntity); - const fspRepository = this.dataSource.getRepository( - FinancialServiceProviderEntity, - ); - const programCustomAttributeRepository = this.dataSource.getRepository( - ProgramCustomAttributeEntity, - ); - const programQuestionRepository = this.dataSource.getRepository( - ProgramQuestionEntity, + const programRegistrationAttributeRepo = this.dataSource.getRepository( + ProgramRegistrationAttributeEntity, ); const programExampleDump = JSON.stringify(programExample); - const program = JSON.parse(programExampleDump); + const programFromJSON = JSON.parse(programExampleDump); if (DEBUG && !isApiTests) { - program.published = true; + programFromJSON.published = true; } - const programReturn = await programRepository.save(program); - - // Remove original program custom attributes and add it to a separate variable - const programCustomAttributes = program.programCustomAttributes; - program.programCustomAttributes = []; - if (programCustomAttributes) { - for (const attribute of programCustomAttributes) { - attribute.program = programReturn; - await programCustomAttributeRepository.save(attribute); - } - } + const programReturn = await programRepository.save(programFromJSON); - // Remove original program questions and add it to a separate variable - const programQuestions = program.programQuestions; - program.programQuestions = []; - for (const question of programQuestions) { - if (question.answerType === AnswerTypes.dropdown) { - const scoringKeys = Object.keys(question.scoring); + // Remove original program registration attributes and add it to a separate variable + const programRegistrationAttributes = + programFromJSON.programRegistrationAttributes; + programFromJSON.programRegistrationAttibutes = []; + for (const attribute of programRegistrationAttributes) { + attribute.isRequired = attribute.isRequired || false; + if (attribute.answerType === RegistrationAttributeTypes.dropdown) { + const scoringKeys = Object.keys(attribute.scoring); if (scoringKeys.length > 0) { - const optionKeys = question.options.map(({ option }) => option); + const optionKeys = attribute.options.map(({ option }) => option); const areOptionScoringEqual = JSON.stringify(scoringKeys.sort()) == JSON.stringify(optionKeys.sort()); if (!areOptionScoringEqual) { throw new HttpException( - 'Option and scoring is not equal of question ' + question.name, + 'Option and scoring is not equal of question ' + attribute.name, 404, ); } } - // assert(optionsArray.includes(scoringkey)); } - question.program = program; - await programQuestionRepository.save(question); + attribute.programId = programReturn.id; + await programRegistrationAttributeRepo.save(attribute); } - // Remove original fsp and add it to a separate variable const foundProgram = await programRepository.findOneOrFail({ where: { id: Equal(programReturn.id) }, }); - const fsps = program.financialServiceProviders; - foundProgram.financialServiceProviders = []; - for (const fsp of fsps) { - const fspReturn = await fspRepository.findOneOrFail({ - where: { fsp: Equal(fsp.fsp) }, - }); - foundProgram.financialServiceProviders.push(fspReturn); - if (fsp.configuration && fsp.configuration.length > 0) { - for (const config of fsp.configuration) { - let fspConfigValue = config.value; - if (typeof config.value === 'string') { - fspConfigValue = process.env[config.value] || config.value; - } - - await this.programFspConfigurationService.create(programReturn.id, { - fspId: fspReturn.id, - name: config.name, - value: fspConfigValue, - }); - } + const fspConfigArrayFromJson = + programFromJSON.programFinancialServiceProviderConfigurations; + foundProgram.programFinancialServiceProviderConfigurations = []; + + for (const fspConfigFromJson of fspConfigArrayFromJson) { + const financialServiceProviderObject = + FINANCIAL_SERVICE_PROVIDER_SETTINGS.find( + (fsp) => fsp.name === fspConfigFromJson.financialServiceProvider, + ); + if (!financialServiceProviderObject) { + throw new HttpException( + `FSP with name ${fspConfigFromJson.financialServiceProvider} not found in FINANCIAL_SERVICE_PROVIDER_SETTINGS`, + HttpStatus.NOT_FOUND, + ); } + + const programFspConfig = this.createProgramFspConfiguration( + fspConfigFromJson, + financialServiceProviderObject, + foundProgram.id, + ); + await this.programFspConfigurationRepository.save(programFspConfig); } - return await programRepository.save(foundProgram); + return programRepository.findOneByOrFail({ id: Equal(foundProgram.id) }); } - public async addFsp(fspInput: any): Promise { - const exampleDump = JSON.stringify(fspInput); - const fsp = JSON.parse(exampleDump); - - const fspRepository = this.dataSource.getRepository( - FinancialServiceProviderEntity, + private createProgramFspConfiguration( + fspConfigFromJson: { + financialServiceProvider: FinancialServiceProviders; + properties: { name: string; value: string }[] | undefined; + name?: string; + label: LocalizedString; + }, + financialServiceProviderObject: FinancialServiceProviderDto, + programId: number, + ): ProgramFinancialServiceProviderConfigurationEntity { + const fspConfigEntity = + new ProgramFinancialServiceProviderConfigurationEntity(); + fspConfigEntity.financialServiceProviderName = + fspConfigFromJson.financialServiceProvider; + fspConfigEntity.properties = this.createProgramFspConfigurationProperties( + fspConfigFromJson.properties ?? [], ); + fspConfigEntity.label = fspConfigFromJson.label + ? fspConfigFromJson.label + : financialServiceProviderObject.defaultLabel; + fspConfigEntity.name = fspConfigFromJson.name + ? fspConfigFromJson.name + : financialServiceProviderObject.name; + fspConfigEntity.transactions = []; + fspConfigEntity.programId = programId; + return fspConfigEntity; + } - const fspQuestionRepository = - this.dataSource.getRepository(FspQuestionEntity); - - // Remove original custom criteria and add it to a separate variable - const questions = fsp.questions; - fsp.questions = []; - - const fspReturn = await fspRepository.save(fsp); + private createProgramFspConfigurationProperties( + propertiesFromJSON: { name: string; value: string }[], + ): ProgramFinancialServiceProviderConfigurationPropertyEntity[] { + const fspConfigPropertyEntities: ProgramFinancialServiceProviderConfigurationPropertyEntity[] = + []; - for (const question of questions) { - question.fsp = fspReturn; - const customReturn = await fspQuestionRepository.save(question); - fsp.questions.push(customReturn); + for (const property of propertiesFromJSON) { + let fspConfigPropertyValue = property.value; + if (typeof property.value === 'string') { + fspConfigPropertyValue = process.env[property.value] || property.value; + } + const fspConfigPropertyEntity = + new ProgramFinancialServiceProviderConfigurationPropertyEntity(); + fspConfigPropertyEntity.name = + property.name as FinancialServiceProviderConfigurationProperties; + fspConfigPropertyEntity.value = fspConfigPropertyValue; + fspConfigPropertyEntities.push(fspConfigPropertyEntity); } + return fspConfigPropertyEntities; } public async assignAidworker( diff --git a/services/121-service/src/scripts/seed-init.ts b/services/121-service/src/scripts/seed-init.ts index 695b22482d..d40748857b 100644 --- a/services/121-service/src/scripts/seed-init.ts +++ b/services/121-service/src/scripts/seed-init.ts @@ -4,13 +4,6 @@ import { DataSource, Equal } from 'typeorm'; import { QueueRegistryService } from '@121-service/src/queue-registry/queue-registry.service'; import { InterfaceScript } from '@121-service/src/scripts/scripts.module'; -import { SeedHelper } from '@121-service/src/scripts/seed-helper'; -import fspCommercialBankEthiopia from '@121-service/src/seed-data/fsp/fsp-commercial-bank-ethiopia.json'; -import fspExcel from '@121-service/src/seed-data/fsp/fsp-excel.json'; -import fspIntersolveVisa from '@121-service/src/seed-data/fsp/fsp-intersolve-visa.json'; -import fspIntersolveVoucherPaper from '@121-service/src/seed-data/fsp/fsp-intersolve-voucher-paper.json'; -import fspIntersolveVoucher from '@121-service/src/seed-data/fsp/fsp-intersolve-voucher-whatsapp.json'; -import fspSafaricom from '@121-service/src/seed-data/fsp/fsp-safaricom.json'; import { CustomHttpService } from '@121-service/src/shared/services/custom-http.service'; import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; import { PermissionEntity } from '@121-service/src/user/permissions.entity'; @@ -23,7 +16,6 @@ import { UserType } from '@121-service/src/user/user-type-enum'; export class SeedInit implements InterfaceScript { public constructor( private dataSource: DataSource, - private readonly seedHelper: SeedHelper, private readonly queueRegistryService: QueueRegistryService, private readonly httpService: CustomHttpService, ) {} @@ -44,7 +36,6 @@ export class SeedInit implements InterfaceScript { const permissions = await this.addPermissions(); await this.createDefaultRoles(permissions); await this.createAdminUser(); - await this.seedFsp(); } private async clearCallbacksMockService(): Promise { @@ -323,13 +314,4 @@ export class SeedInit implements InterfaceScript { transaction: 'all', }); } - - private async seedFsp(): Promise { - await this.seedHelper.addFsp(fspIntersolveVoucher); - await this.seedHelper.addFsp(fspIntersolveVoucherPaper); - await this.seedHelper.addFsp(fspIntersolveVisa); - await this.seedHelper.addFsp(fspSafaricom); - await this.seedHelper.addFsp(fspCommercialBankEthiopia); - await this.seedHelper.addFsp(fspExcel); - } } diff --git a/services/121-service/src/scripts/seed-mock-helpers.ts b/services/121-service/src/scripts/seed-mock-helpers.ts index 0360ed46d2..fd2eb86d25 100644 --- a/services/121-service/src/scripts/seed-mock-helpers.ts +++ b/services/121-service/src/scripts/seed-mock-helpers.ts @@ -267,7 +267,15 @@ export class SeedMockHelper { for (const table of tables) { const tableName = table.table_name; if (!['custom_migration', 'typeorm_metadata'].includes(tableName)) { - const sequenceName = `${tableName}_id_seq`; + let sequenceName = `${tableName}_id_seq`; + // this sequences is created with an abbreviated name automatically, so this exception is needed here + if ( + tableName === + 'program_financial_service_provider_configuration_property' + ) { + sequenceName = 'program_financial_service_pro_id_seq'; + } + const maxIdQuery = `SELECT MAX(id) FROM "121-service"."${tableName}"`; const maxIdResult = await this.dataSource.query(maxIdQuery); @@ -279,6 +287,7 @@ export class SeedMockHelper { } } } + console.log('**Done updating sequence numbers.**'); } public async importRegistrations( @@ -286,7 +295,7 @@ export class SeedMockHelper { registrations: object[], accessToken: string, ): Promise { - const url = `${this.axiosCallsService.getBaseUrl()}/programs/${programId}/registrations/import`; + const url = `${this.axiosCallsService.getBaseUrl()}/programs/${programId}/registrations`; const body = registrations; const headers = this.axiosCallsService.accesTokenToHeaders(accessToken); diff --git a/services/121-service/src/scripts/sql/mock-intersolve-voucher-attributes.sql b/services/121-service/src/scripts/sql/mock-intersolve-voucher-attributes.sql index 050a500f2f..86a93c81d4 100644 --- a/services/121-service/src/scripts/sql/mock-intersolve-voucher-attributes.sql +++ b/services/121-service/src/scripts/sql/mock-intersolve-voucher-attributes.sql @@ -1,13 +1,14 @@ WITH fsp_data AS ( - SELECT fa.id - FROM "121-service".financial_service_provider_question fa - LEFT JOIN "121-service".financial_service_provider f ON f.id = fa."fspId" - WHERE "name" = 'whatsappPhoneNumber' AND f.fsp = 'Intersolve-voucher-whatsapp' + SELECT pra.id + FROM "121-service".program_registration_attribute pra + LEFT JOIN "121-service".program p ON p.id = pra."programId" + LEFT JOIN "121-service".program_financial_service_provider_configuration f ON f."programId" = p.id + WHERE pra."name" = 'whatsappPhoneNumber' AND f."financialServiceProviderName" = 'Intersolve-voucher-whatsapp' ) UPDATE "121-service".intersolve_voucher iv SET "whatsappPhoneNumber" = rd."value" FROM "121-service".imagecode_export_vouchers iev LEFT JOIN "121-service".registration r ON r.id = iev."registrationId" -LEFT JOIN "121-service".registration_data rd ON r.id = rd."registrationId", fsp_data fd -WHERE iv.id = iev."voucherId" AND rd."fspQuestionId" = fd.id; +LEFT JOIN "121-service".registration_attribute_data rd ON r.id = rd."registrationId", fsp_data fd +WHERE iv.id = iev."voucherId" AND rd."programRegistrationAttributeId" = fd.id; diff --git a/services/121-service/src/scripts/sql/mock-make-phone-unique.sql b/services/121-service/src/scripts/sql/mock-make-phone-unique.sql index 9fe5bcb24b..2ace1bb3ee 100644 --- a/services/121-service/src/scripts/sql/mock-make-phone-unique.sql +++ b/services/121-service/src/scripts/sql/mock-make-phone-unique.sql @@ -1,9 +1,9 @@ -update "121-service".registration_data +update "121-service".registration_attribute_data set "value" = CAST(10000000000 + floor(random() * 90000000000) AS bigint) - WHERE "programQuestionId" IN (SELECT id FROM "121-service".program_question WHERE "name" = 'phoneNumber') OR "fspQuestionId" IN (SELECT id FROM "121-service".financial_service_provider_question WHERE "name" = 'whatsappPhoneNumber'); + WHERE "programRegistrationAttributeId" IN (SELECT id FROM "121-service".program_registration_attribute WHERE "name" = 'phoneNumber' OR "name" = 'whatsappPhoneNumber'); ; update "121-service".registration r set "phoneNumber" = rd."value" - from "121-service".registration_data rd - where rd."registrationId" = r."id" and rd."programQuestionId" in (SELECT id FROM "121-service".program_question WHERE "name" = 'phoneNumber') + from "121-service".registration_attribute_data rd + where rd."registrationId" = r."id" and rd."programRegistrationAttributeId" in (SELECT id FROM "121-service".program_registration_attribute WHERE "name" = 'phoneNumber') ; diff --git a/services/121-service/src/scripts/sql/mock-payment-transactions.sql b/services/121-service/src/scripts/sql/mock-payment-transactions.sql index 3b0a8461b4..2a83dfbefa 100644 --- a/services/121-service/src/scripts/sql/mock-payment-transactions.sql +++ b/services/121-service/src/scripts/sql/mock-payment-transactions.sql @@ -7,11 +7,11 @@ INSERT INTO "121-service"."transaction" "customData", "transactionStep", "programId", - "financialServiceProviderId", "registrationId", amount, updated, - "userId" + "userId", + "programFinancialServiceProviderConfigurationId" ) SELECT created + INTERVAL '1 millisecond' * ROW_NUMBER() OVER (ORDER BY id), @@ -21,11 +21,11 @@ SELECT "customData", "transactionStep", "programId", - "financialServiceProviderId", "registrationId", amount, updated, - "userId" + "userId", + "programFinancialServiceProviderConfigurationId" FROM "121-service"."transaction" WHERE diff --git a/services/121-service/src/scripts/sql/mock-registration-data.sql b/services/121-service/src/scripts/sql/mock-registration-data.sql index 82884bf976..b16ff26931 100644 --- a/services/121-service/src/scripts/sql/mock-registration-data.sql +++ b/services/121-service/src/scripts/sql/mock-registration-data.sql @@ -1,22 +1,20 @@ INSERT INTO - "121-service"."registration_data" ( + "121-service"."registration_attribute_data" ( SELECT id + ( SELECT max(id) FROM - "121-service"."registration_data") AS id, + "121-service"."registration_attribute_data") AS id, created, + updated, "registrationId" + ( SELECT max("registrationId") FROM - "121-service"."registration_data") AS "registrationId", - "programQuestionId", - "fspQuestionId", - "programCustomAttributeId", - value, - updated + "121-service"."registration_attribute_data") AS "registrationId", + "programRegistrationAttributeId", + value FROM - "121-service"."registration_data"); + "121-service"."registration_attribute_data"); diff --git a/services/121-service/src/scripts/sql/mock-registrations.sql b/services/121-service/src/scripts/sql/mock-registrations.sql index 5a5369eeb2..4b239cf7a9 100644 --- a/services/121-service/src/scripts/sql/mock-registrations.sql +++ b/services/121-service/src/scripts/sql/mock-registrations.sql @@ -16,7 +16,6 @@ INSERT "paymentAmountMultiplier", "programId", "userId", - "fspId", updated, "registrationProgramId" + ( SELECT @@ -32,6 +31,7 @@ INSERT ELSE 'zeeland.goes' END ELSE scope - END + END, + "programFinancialServiceProviderConfigurationId" FROM "121-service".registration); diff --git a/services/121-service/src/scripts/sql/mock-transations-one-per-registrations.sql b/services/121-service/src/scripts/sql/mock-transations-one-per-registrations.sql index bc55d129da..79e2bf3c46 100644 --- a/services/121-service/src/scripts/sql/mock-transations-one-per-registrations.sql +++ b/services/121-service/src/scripts/sql/mock-transations-one-per-registrations.sql @@ -6,11 +6,11 @@ INSERT INTO "121-service"."transaction" ( "customData", "transactionStep", "programId", - "financialServiceProviderId", "registrationId", amount, updated, - "userId" + "userId", + "programFinancialServiceProviderConfigurationId" ) SELECT created + INTERVAL '1 millisecond' * ROW_NUMBER() OVER (ORDER BY id), @@ -20,11 +20,11 @@ SELECT "customData", "transactionStep", "programId", - "financialServiceProviderId", "registrationId" + (SELECT max("registrationId") FROM "121-service"."transaction"), amount, updated, - "userId" + "userId", + "programFinancialServiceProviderConfigurationId" FROM "121-service"."transaction" WHERE diff --git a/services/121-service/src/scripts/sql/mock-visa-customers.sql b/services/121-service/src/scripts/sql/mock-visa-customers.sql index f0d2877b3a..110e60f01c 100644 --- a/services/121-service/src/scripts/sql/mock-visa-customers.sql +++ b/services/121-service/src/scripts/sql/mock-visa-customers.sql @@ -9,5 +9,5 @@ INSERT ,'mock-holderId' ,r.id from "121-service".registration r - left join "121-service".financial_service_provider f on r."fspId" = f.id - where f.fsp = 'Intersolve-visa'); \ No newline at end of file + left join "121-service".program_financial_service_provider_configuration f on r."programFinancialServiceProviderConfigurationId" = f.id + where f."financialServiceProviderName" = 'Intersolve-visa'); \ No newline at end of file diff --git a/services/121-service/src/seed-data/fsp/fsp-commercial-bank-ethiopia.json b/services/121-service/src/seed-data/fsp/fsp-commercial-bank-ethiopia.json deleted file mode 100644 index 2fa3ff21c7..0000000000 --- a/services/121-service/src/seed-data/fsp/fsp-commercial-bank-ethiopia.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "fsp": "Commercial-bank-ethiopia", - "integrationType": "api", - "displayName": { - "en": "Commercial Bank of Ethiopia" - }, - "questions": [ - { - "name": "bankAccountNumber", - "label": { - "en": "Bank Account Number" - }, - "placeholder": { - "en": "Please type your bank account number" - }, - "export": ["all-people-affected", "included"], - "answerType": "text", - "pattern": ".+", - "options": null, - "showInPeopleAffectedTable": true - } - ] -} diff --git a/services/121-service/src/seed-data/fsp/fsp-excel.json b/services/121-service/src/seed-data/fsp/fsp-excel.json deleted file mode 100644 index 60885eacd7..0000000000 --- a/services/121-service/src/seed-data/fsp/fsp-excel.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "fsp": "Excel", - "displayName": { - "en": "Excel Payment Instructions" - }, - "integrationType": "csv", - "hasReconciliation": true, - "questions": [] -} diff --git a/services/121-service/src/seed-data/fsp/fsp-intersolve-visa.json b/services/121-service/src/seed-data/fsp/fsp-intersolve-visa.json deleted file mode 100644 index fe88f7130a..0000000000 --- a/services/121-service/src/seed-data/fsp/fsp-intersolve-visa.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "fsp": "Intersolve-visa", - "integrationType": "api", - "displayName": { - "en": "Visa debit card" - }, - "notifyOnTransaction": true, - "questions": [ - { - "name": "addressStreet", - "label": { - "en": "Address street" - }, - "export": ["all-people-affected", "included"], - "answerType": "text", - "pattern": ".+", - "showInPeopleAffectedTable": false - }, - { - "name": "addressHouseNumber", - "label": { - "en": "Address house number" - }, - "export": ["all-people-affected", "included"], - "answerType": "numeric", - "showInPeopleAffectedTable": false - }, - { - "name": "addressHouseNumberAddition", - "label": { - "en": "Address house number addition" - }, - "export": ["all-people-affected", "included"], - "answerType": "text", - "showInPeopleAffectedTable": false - }, - { - "name": "addressPostalCode", - "label": { - "en": "Address postal code" - }, - "export": ["all-people-affected", "included"], - "answerType": "text", - "pattern": ".+", - "showInPeopleAffectedTable": false - }, - { - "name": "addressCity", - "label": { - "en": "Address city" - }, - "export": ["all-people-affected", "included"], - "answerType": "text", - "pattern": ".+", - "showInPeopleAffectedTable": false - }, - { - "name": "whatsappPhoneNumber", - "label": { - "en": "WhatsApp Nr." - }, - "placeholder": { - "ar": "00 00 00 00 0 00+", - "en": "+00 0 00 00 00 00" - }, - "export": ["all-people-affected", "included"], - "answerType": "tel", - "options": null, - "duplicateCheck": true, - "showInPeopleAffectedTable": false - } - ] -} diff --git a/services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-paper.json b/services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-paper.json deleted file mode 100644 index e518556154..0000000000 --- a/services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-paper.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "fsp": "Intersolve-voucher-paper", - "integrationType": "api", - "displayName": { - "en": "Albert Heijn voucher paper" - }, - "questions": [] -} diff --git a/services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-whatsapp.json b/services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-whatsapp.json deleted file mode 100644 index 7233101ef6..0000000000 --- a/services/121-service/src/seed-data/fsp/fsp-intersolve-voucher-whatsapp.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "fsp": "Intersolve-voucher-whatsapp", - "integrationType": "api", - "displayName": { - "en": "Albert Heijn voucher WhatsApp" - }, - "questions": [ - { - "name": "whatsappPhoneNumber", - "label": { - "en": "WhatsApp Nr." - }, - "placeholder": { - "ar": "00 00 00 00 0 00+", - "en": "+00 0 00 00 00 00" - }, - "export": ["all-people-affected", "included"], - "answerType": "tel", - "options": null, - "duplicateCheck": true, - "showInPeopleAffectedTable": true - } - ] -} diff --git a/services/121-service/src/seed-data/fsp/fsp-safaricom.json b/services/121-service/src/seed-data/fsp/fsp-safaricom.json deleted file mode 100644 index bb8bbd5620..0000000000 --- a/services/121-service/src/seed-data/fsp/fsp-safaricom.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "fsp": "Safaricom", - "integrationType": "api", - "displayName": { - "en": "Safaricom" - }, - "questions": [ - { - "name": "phoneNumber", - "label": { - "en": "Phone Number" - }, - "placeholder": { - "en": "+243000000000" - }, - "export": ["all-people-affected", "included"], - "answerType": "tel", - "options": null, - "showInPeopleAffectedTable": false - } - ] -} diff --git a/services/121-service/src/seed-data/mock/registration-pv.data.ts b/services/121-service/src/seed-data/mock/registration-pv.data.ts index 52812de63d..e3fc2f8a8d 100644 --- a/services/121-service/src/seed-data/mock/registration-pv.data.ts +++ b/services/121-service/src/seed-data/mock/registration-pv.data.ts @@ -1,5 +1,5 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; export const registrationAHWhatsapp = { @@ -8,8 +8,9 @@ export const registrationAHWhatsapp = { paymentAmountMultiplier: 1, fullName: 'Juan Garcia', scope: 'utrecht.houten', - [CustomDataAttributes.phoneNumber]: '14155238888', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, - [CustomDataAttributes.whatsappPhoneNumber]: '14155238888', + [DefaultRegistrationDataAttributeNames.phoneNumber]: '14155238888', + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: '14155238888', namePartnerOrganization: 'Help Elkaar', }; diff --git a/services/121-service/src/seed-data/mock/visa-card.data.ts b/services/121-service/src/seed-data/mock/visa-card.data.ts index 5ed54d9160..1b5d6593aa 100644 --- a/services/121-service/src/seed-data/mock/visa-card.data.ts +++ b/services/121-service/src/seed-data/mock/visa-card.data.ts @@ -1,5 +1,5 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; export const programIdVisa = 3; @@ -11,12 +11,13 @@ export const registrationVisa = { preferredLanguage: LanguageEnum.en, paymentAmountMultiplier: 1, fullName: 'Jane Doe', - [CustomDataAttributes.phoneNumber]: '14155238887', - fspName: FinancialServiceProviders.intersolveVisa, + [DefaultRegistrationDataAttributeNames.phoneNumber]: '14155238887', + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, addressStreet: 'Teststraat', addressHouseNumber: '1', addressHouseNumberAddition: '', addressPostalCode: '1234AB', addressCity: 'Stad', - [CustomDataAttributes.whatsappPhoneNumber]: '14155238887', + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: '14155238887', }; diff --git a/services/121-service/src/seed-data/program/program-demo.json b/services/121-service/src/seed-data/program/program-demo.json index b9495d9e27..6e6f5799f2 100644 --- a/services/121-service/src/seed-data/program/program-demo.json +++ b/services/121-service/src/seed-data/program/program-demo.json @@ -23,38 +23,9 @@ "distributionFrequency": "week", "distributionDuration": 10, "fixedTransferValue": 35, - "financialServiceProviders": [ - { - "fsp": "Intersolve-voucher-whatsapp", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - }, - { - "fsp": "Excel", - "configuration": [ - { - "name": "columnsToExport", - "value": ["name", "birthdate", "documentid", "phoneNumber"] - }, - { - "name": "columnToMatch", - "value": "phoneNumber" - } - ] - } - ], "targetNrRegistrations": 870, "tryWhatsAppFirst": true, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "name", "label": { @@ -64,10 +35,8 @@ "fr": "Nom", "nl": "Naam" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -82,10 +51,8 @@ "fr": "Deuxième prénom", "nl": "Tweede naam" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -100,10 +67,8 @@ "fr": "Prénom", "nl": "Voornaam" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -118,8 +83,7 @@ "fr": "Genre", "nl": "Geslacht" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "M", @@ -152,7 +116,6 @@ } } ], - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -167,10 +130,8 @@ "fr": "Date de naissance", "nl": "Geboortedatum" }, - "answerType": "date", - "questionType": "standard", + "type": "date", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -185,10 +146,8 @@ "fr": "Numéro d'identification", "nl": "Document ID" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "duplicateCheck": true, @@ -204,8 +163,7 @@ "fr": "Chef de ménage", "nl": "Hoofd huishouden" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Oui", @@ -228,7 +186,6 @@ } } ], - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -243,10 +200,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -261,10 +216,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 1 @@ -281,10 +234,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -299,10 +250,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 1 @@ -319,8 +268,7 @@ "fr": "", "nl": "" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "returnee", @@ -363,7 +311,6 @@ } } ], - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "returnee": 2, @@ -383,8 +330,7 @@ "fr": "", "nl": "" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "widowed", @@ -447,7 +393,6 @@ } } ], - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "widowed": 1, @@ -469,10 +414,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 1 @@ -489,10 +432,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 1 @@ -509,10 +450,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 2 @@ -529,10 +468,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 2 @@ -549,10 +486,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 2 @@ -569,10 +504,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 2 @@ -589,10 +522,8 @@ "fr": "", "nl": "" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": { "multiplier": 2 @@ -609,8 +540,7 @@ "fr": "", "nl": "" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Oui", @@ -633,11 +563,25 @@ } } ], - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, "showInPeopleAffectedTable": false + }, + { + "name": "whatsappPhoneNumber", + "label": { + "en": "WhatsApp Nr." + }, + "placeholder": { + "ar": "00 00 00 00 0 00+", + "en": "+00 0 00 00 00 00" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "duplicateCheck": true, + "showInPeopleAffectedTable": true } ], "aboutProgram": { @@ -649,5 +593,33 @@ }, "fullnameNamingConvention": ["name", "middlename", "firstname"], "languages": ["ar", "en", "es", "fr", "nl"], - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Intersolve-voucher-whatsapp", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + }, + { + "financialServiceProvider": "Excel", + "properties": [ + { + "name": "columnsToExport", + "value": ["name", "birthdate", "documentid", "phoneNumber"] + }, + { + "name": "columnToMatch", + "value": "phoneNumber" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-eth-joint-response.json b/services/121-service/src/seed-data/program/program-eth-joint-response.json index cbe1710090..e9f3414643 100644 --- a/services/121-service/src/seed-data/program/program-eth-joint-response.json +++ b/services/121-service/src/seed-data/program/program-eth-joint-response.json @@ -15,25 +15,17 @@ "distributionFrequency": "month", "distributionDuration": 6, "fixedTransferValue": 1, - "financialServiceProviders": [ - { - "fsp": "Commercial-bank-ethiopia" - } - ], "targetNrRegistrations": 870, "tryWhatsAppFirst": false, "bankAccountNumberPlaceholder": "1000263499216", - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullname", "label": { "en": "First name" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -44,8 +36,7 @@ "label": { "en": "Gender" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Masculin ", @@ -62,11 +53,24 @@ } } ], - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, "showInPeopleAffectedTable": false + }, + { + "name": "bankAccountNumber", + "label": { + "en": "Bank Account Number" + }, + "placeholder": { + "en": "Please type your bank account number" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "pattern": ".+", + "options": null, + "showInPeopleAffectedTable": true } ], "aboutProgram": { @@ -76,5 +80,10 @@ "languages": ["en"], "enableMaxPayments": true, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Commercial-bank-ethiopia" + } + ] } diff --git a/services/121-service/src/seed-data/program/program-joint-response-ANE.json b/services/121-service/src/seed-data/program/program-joint-response-ANE.json index d7fc740a96..8456c661de 100644 --- a/services/121-service/src/seed-data/program/program-joint-response-ANE.json +++ b/services/121-service/src/seed-data/program/program-joint-response-ANE.json @@ -18,24 +18,16 @@ "fixedTransferValue": 7000, "tryWhatsAppFirst": false, "paymentAmountMultiplierFormula": null, - "financialServiceProviders": [ - { - "fsp": "Commercial-bank-ethiopia" - } - ], "targetNrRegistrations": 250, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullName", "label": { "en": "Name", "et_AM": "ስም" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -49,11 +41,9 @@ "en": "Phone number", "et_AM": "ስልክ ቁጥር" }, - "answerType": "tel", - "questionType": "standard", + "type": "tel", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -67,11 +57,9 @@ "en": "ID Card number", "et_AM": "የመታውቂያ ቁጥር" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -85,11 +73,9 @@ "en": "Age", "et_AM": "እድሜ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -103,8 +89,7 @@ "en": "Gender", "et_AM": "ጾታ" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male", @@ -122,7 +107,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -136,11 +120,9 @@ "en": "How many female?", "et_AM": "ሴት አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -154,11 +136,9 @@ "en": "How many male?", "et_AM": "ወንድ አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -172,11 +152,9 @@ "en": "Total family members", "et_AM": "በቤተሰብዎ ውስጥ ያሉት አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -190,11 +168,9 @@ "en": "Female under 18", "et_AM": "ሴት ከ18 ዓመት በታች" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -208,11 +184,9 @@ "en": "Male under 18", "et_AM": "ወንድ ከ18 ዓመት በታች" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -226,11 +200,9 @@ "en": "Female over 18", "et_AM": "ሴት ከ18 ዓመት በላይ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -244,11 +216,9 @@ "en": "Male over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -262,11 +232,9 @@ "en": "Female with disability under 18", "et_AM": "ሴት ከ18 ዓመት በታች አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -280,11 +248,9 @@ "en": "Male with disability under 18", "et_AM": "ወንድ ከ18 ዓመት በታች አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -298,11 +264,9 @@ "en": "Female with disability over 18", "et_AM": "ሴት ከ18 ዓመት በላይ አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -316,17 +280,29 @@ "en": "Male with disability over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], "duplicateCheck": false, "placeholder": null, "showInPeopleAffectedTable": false + }, + { + "name": "bankAccountNumber", + "label": { + "en": "Bank Account Number" + }, + "placeholder": { + "en": "Please type your bank account number" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "pattern": ".+", + "options": null, + "showInPeopleAffectedTable": true } ], "aboutProgram": { @@ -336,5 +312,20 @@ "languages": ["en", "et_AM"], "enableMaxPayments": true, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Commercial-bank-ethiopia", + "properties": [ + { + "name": "username", + "value": "COMMERCIAL_BANK_ETHIOPIA_PASSWORD" + }, + { + "name": "password", + "value": "COMMERCIAL_BANK_ETHIOPIA_USERNAME" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-joint-response-EKHCDC.json b/services/121-service/src/seed-data/program/program-joint-response-EKHCDC.json index 64e2a00828..c609ab7084 100644 --- a/services/121-service/src/seed-data/program/program-joint-response-EKHCDC.json +++ b/services/121-service/src/seed-data/program/program-joint-response-EKHCDC.json @@ -18,24 +18,16 @@ "fixedTransferValue": 7000, "tryWhatsAppFirst": false, "paymentAmountMultiplierFormula": null, - "financialServiceProviders": [ - { - "fsp": "Commercial-bank-ethiopia" - } - ], "targetNrRegistrations": 250, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullName", "label": { "en": "Name", "et_AM": "ስም" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -49,11 +41,9 @@ "en": "Phone number", "et_AM": "ስልክ ቁጥር" }, - "answerType": "tel", - "questionType": "standard", + "type": "tel", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -67,11 +57,9 @@ "en": "ID Card number", "et_AM": "የመታውቂያ ቁጥር" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -85,11 +73,9 @@ "en": "Age", "et_AM": "እድሜ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -103,8 +89,7 @@ "en": "Gender", "et_AM": "ጾታ" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male", @@ -122,7 +107,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -136,11 +120,9 @@ "en": "How many female?", "et_AM": "ሴት አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -154,11 +136,9 @@ "en": "How many male?", "et_AM": "ወንድ አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -172,11 +152,9 @@ "en": "Total family members", "et_AM": "በቤተሰብዎ ውስጥ ያሉት አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -190,11 +168,9 @@ "en": "Female under 18", "et_AM": "ሴት ከ18 ዓመት በታች" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -208,11 +184,9 @@ "en": "Male under 18", "et_AM": "ወንድ ከ18 ዓመት በታች" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -226,11 +200,9 @@ "en": "Female over 18", "et_AM": "ሴት ከ18 ዓመት በላይ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -244,11 +216,9 @@ "en": "Male over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -262,11 +232,9 @@ "en": "Female with disability under 18", "et_AM": "ሴት ከ18 ዓመት በታች አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -280,11 +248,9 @@ "en": "Male with disability under 18", "et_AM": "ወንድ ከ18 ዓመት በታች አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -298,11 +264,9 @@ "en": "Female with disability over 18", "et_AM": "ሴት ከ18 ዓመት በላይ አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -316,17 +280,29 @@ "en": "Male with disability over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], "duplicateCheck": false, "placeholder": null, "showInPeopleAffectedTable": false + }, + { + "name": "bankAccountNumber", + "label": { + "en": "Bank Account Number" + }, + "placeholder": { + "en": "Please type your bank account number" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "pattern": ".+", + "options": null, + "showInPeopleAffectedTable": true } ], "aboutProgram": { @@ -336,5 +312,20 @@ "languages": ["en", "et_AM"], "enableMaxPayments": true, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Commercial-bank-ethiopia", + "properties": [ + { + "name": "username", + "value": "COMMERCIAL_BANK_ETHIOPIA_PASSWORD" + }, + { + "name": "password", + "value": "COMMERCIAL_BANK_ETHIOPIA_USERNAME" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-joint-response-dorcas.json b/services/121-service/src/seed-data/program/program-joint-response-dorcas.json index 8ef65806a9..e89c41c333 100644 --- a/services/121-service/src/seed-data/program/program-joint-response-dorcas.json +++ b/services/121-service/src/seed-data/program/program-joint-response-dorcas.json @@ -18,24 +18,16 @@ "fixedTransferValue": 7000, "tryWhatsAppFirst": false, "paymentAmountMultiplierFormula": null, - "financialServiceProviders": [ - { - "fsp": "Commercial-bank-ethiopia" - } - ], "targetNrRegistrations": 250, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullName", "label": { "en": "Name", "et_AM": "ስም" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -49,11 +41,9 @@ "en": "Phone number", "et_AM": "ስልክ ቁጥር" }, - "answerType": "tel", - "questionType": "standard", + "type": "tel", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -67,11 +57,9 @@ "en": "ID Card number", "et_AM": "የመታውቂያ ቁጥር" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -85,11 +73,9 @@ "en": "Age", "et_AM": "እድሜ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -103,8 +89,7 @@ "en": "Gender", "et_AM": "ጾታ" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male", @@ -122,7 +107,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -136,11 +120,9 @@ "en": "How many female?", "et_AM": "ሴት አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -154,11 +136,9 @@ "en": "How many male?", "et_AM": "ወንድ አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -172,11 +152,9 @@ "en": "Total family members", "et_AM": "በቤተሰብዎ ውስጥ ያሉት አባላት" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -190,11 +168,9 @@ "en": "Female under 18", "et_AM": "ሴት ከ18 ዓመት በታች" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -208,11 +184,9 @@ "en": "Male under 18", "et_AM": "ወንድ ከ18 ዓመት በታች" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -226,11 +200,9 @@ "en": "Female over 18", "et_AM": "ሴት ከ18 ዓመት በላይ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -244,11 +216,9 @@ "en": "Male over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -262,11 +232,9 @@ "en": "Female with disability under 18", "et_AM": "ሴት ከ18 ዓመት በታች አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -280,11 +248,9 @@ "en": "Male with disability under 18", "et_AM": "ወንድ ከ18 ዓመት በታች አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -298,11 +264,9 @@ "en": "Female with disability over 18", "et_AM": "ሴት ከ18 ዓመት በላይ አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -316,17 +280,29 @@ "en": "Male with disability over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ አካል ጉዳተኛ" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], "duplicateCheck": false, "placeholder": null, "showInPeopleAffectedTable": false + }, + { + "name": "bankAccountNumber", + "label": { + "en": "Bank Account Number" + }, + "placeholder": { + "en": "Please type your bank account number" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "pattern": ".+", + "options": null, + "showInPeopleAffectedTable": true } ], "aboutProgram": { @@ -336,5 +312,20 @@ "languages": ["en", "et_AM"], "enableMaxPayments": true, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Commercial-bank-ethiopia", + "properties": [ + { + "name": "username", + "value": "COMMERCIAL_BANK_ETHIOPIA_PASSWORD" + }, + { + "name": "password", + "value": "COMMERCIAL_BANK_ETHIOPIA_USERNAME" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-krcs-baringo.json b/services/121-service/src/seed-data/program/program-krcs-baringo.json index 2d168a84bb..4b74d6506b 100644 --- a/services/121-service/src/seed-data/program/program-krcs-baringo.json +++ b/services/121-service/src/seed-data/program/program-krcs-baringo.json @@ -16,24 +16,16 @@ "distributionDuration": 6, "fixedTransferValue": 6539, "paymentAmountMultiplierFormula": null, - "financialServiceProviders": [ - { - "fsp": "Safaricom" - } - ], "targetNrRegistrations": 2850, "tryWhatsAppFirst": false, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullName", "label": { "en": "First Name" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -44,8 +36,7 @@ "label": { "en": "Gender" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male", @@ -61,7 +52,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -74,11 +64,9 @@ "label": { "en": "Age" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -91,8 +79,7 @@ "label": { "en": "Marital status of beneficiary" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "married", @@ -120,7 +107,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -133,8 +119,7 @@ "label": { "en": "Registration Type" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "self", @@ -150,7 +135,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -163,11 +147,9 @@ "label": { "en": "ID number (MPESA)" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -180,11 +162,9 @@ "label": { "en": "Name of Alternate" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -197,11 +177,9 @@ "label": { "en": "Total household size" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -214,11 +192,9 @@ "label": { "en": "Total household <5" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -231,11 +207,9 @@ "label": { "en": "Total household >60" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -248,8 +222,7 @@ "label": { "en": "Receiving other Social Assistance?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -265,7 +238,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -278,11 +250,9 @@ "label": { "en": "County" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -295,11 +265,9 @@ "label": { "en": "Sub County" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -312,11 +280,9 @@ "label": { "en": "Ward" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -329,11 +295,9 @@ "label": { "en": "Location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -346,11 +310,9 @@ "label": { "en": "Sub location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -363,11 +325,9 @@ "label": { "en": "Village" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -380,11 +340,9 @@ "label": { "en": "Nearest school" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -397,8 +355,7 @@ "label": { "en": "Area type" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "rural", @@ -414,7 +371,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -427,8 +383,7 @@ "label": { "en": "What are the main livelihood sources for the household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "salary_from_formal_employment", @@ -468,7 +423,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -481,11 +435,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -498,11 +450,9 @@ "label": { "en": "How many males 0 - 5 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -515,11 +465,9 @@ "label": { "en": "How many females 0 - 5 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -532,11 +480,9 @@ "label": { "en": "How many males 6 - 12 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -549,11 +495,9 @@ "label": { "en": "How many females 6 - 12 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -566,11 +510,9 @@ "label": { "en": "How many males 13 - 24 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -583,11 +525,9 @@ "label": { "en": "How many females 13 - 24 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -600,11 +540,9 @@ "label": { "en": "How many males 25 - 59 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -617,11 +555,9 @@ "label": { "en": "How many females 25 - 59 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -634,11 +570,9 @@ "label": { "en": "How many males over 60 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -651,11 +585,9 @@ "label": { "en": "How many females over 60 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -668,11 +600,9 @@ "label": { "en": "Total males in household" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -685,11 +615,9 @@ "label": { "en": "Total females in household" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -702,8 +630,7 @@ "label": { "en": "Do you have household members living with disability?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -719,7 +646,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -732,11 +658,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -749,8 +673,7 @@ "label": { "en": "Do you have household members living with chronic illness?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -766,7 +689,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -779,11 +701,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -796,8 +716,7 @@ "label": { "en": "Do you have any pregnant or lactating woman in your household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -813,7 +732,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -826,11 +744,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -843,11 +759,9 @@ "label": { "en": "How many habitable ROOMS does this dwelling unit contain?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -860,8 +774,7 @@ "label": { "en": "What is the TENURE status of the dwelling unit and/or surrounding terrain/land?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Owner occupied", @@ -877,7 +790,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -890,8 +802,7 @@ "label": { "en": "If owner occupied, state whether:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "purchased", @@ -919,7 +830,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -932,11 +842,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -949,8 +857,7 @@ "label": { "en": "If rented or provided, stated whether:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "government", @@ -996,7 +903,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1009,11 +915,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1026,8 +930,7 @@ "label": { "en": "What is the main construction material used for the ROOF?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Corrugated iron sheets", @@ -1085,7 +988,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1098,11 +1000,9 @@ "label": { "en": "If other ROOFING material state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1115,8 +1015,7 @@ "label": { "en": "What is the main construction material used for the WALL?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "corrugated_iron_sheets", @@ -1180,7 +1079,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1193,11 +1091,9 @@ "label": { "en": "If other type of WALL state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1210,8 +1106,7 @@ "label": { "en": "What is the main construction material used for the FLOOR?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "cement", @@ -1245,7 +1140,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1258,11 +1152,9 @@ "label": { "en": "If other type of FLOOR state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1275,8 +1167,7 @@ "label": { "en": "The dwelling unit is at RISK of:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "landslide", @@ -1310,7 +1201,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1323,11 +1213,9 @@ "label": { "en": "If other type of RISK, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1340,8 +1228,7 @@ "label": { "en": "What is the main source of WATER?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "pond", @@ -1435,7 +1322,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1448,11 +1334,9 @@ "label": { "en": "Any other source of WATER, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1465,8 +1349,7 @@ "label": { "en": "Does your household own pigs?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1482,7 +1365,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1495,11 +1377,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1512,8 +1392,7 @@ "label": { "en": "Does your household own chicken?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1529,7 +1408,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1542,8 +1420,7 @@ "label": { "en": "What is the MAIN mode of HUMAN WASTE Disposal?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "main_sewer", @@ -1601,7 +1478,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1614,11 +1490,9 @@ "label": { "en": "If any other HUMAN WASTER Disposal mode, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1631,8 +1505,7 @@ "label": { "en": "What is the MAIN type of COOKING Fuel?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "electricity", @@ -1684,7 +1557,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1697,11 +1569,9 @@ "label": { "en": "Specify other type of COOKING Fuel" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1714,8 +1584,7 @@ "label": { "en": "What is the MAIN type of LIGHTING?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "electricity", @@ -1767,7 +1636,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1780,11 +1648,9 @@ "label": { "en": "Specify other type of LIGHTING" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1797,8 +1663,7 @@ "label": { "en": "Does your household OWN any of the following ITEMS?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "television__tv", @@ -1850,7 +1715,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1863,8 +1727,7 @@ "label": { "en": "Does your household own exotic cattle?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1880,7 +1743,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1893,11 +1755,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1910,8 +1770,7 @@ "label": { "en": "Does your household own indigenous cattle?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1927,7 +1786,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1940,11 +1798,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1957,8 +1813,7 @@ "label": { "en": "Does your household own sheep?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1974,7 +1829,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1987,11 +1841,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2004,8 +1856,7 @@ "label": { "en": "Does your household own goats?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2021,7 +1872,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2034,11 +1884,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2051,8 +1899,7 @@ "label": { "en": "Does your household own camels?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2068,7 +1915,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2081,11 +1927,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2098,8 +1942,7 @@ "label": { "en": "Does your household own donkeys?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2115,7 +1958,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2128,11 +1970,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2145,11 +1985,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2162,11 +2000,9 @@ "label": { "en": "How many LIVE BIRTHS have occured in this household in the last 12 months (1 year)?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2179,11 +2015,9 @@ "label": { "en": "How many DEATHS have occured in this household in the last 12 months (1 year)?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2196,8 +2030,7 @@ "label": { "en": "Currently, the CONDITIONS of your household are?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "poor", @@ -2225,7 +2058,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2238,8 +2070,7 @@ "label": { "en": "Did anyone reduce or skip meals?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2255,7 +2086,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2268,11 +2098,9 @@ "label": { "en": "Receiving benefits from any National Safety Net Programmes?" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2285,11 +2113,9 @@ "label": { "en": "If Yes, Name the PROGRAMME and AGENCY" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2302,8 +2128,7 @@ "label": { "en": "What type of BENEFIT do you receive?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "cash", @@ -2325,7 +2150,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2338,11 +2162,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2355,11 +2177,9 @@ "label": { "en": "If Cash, how much (in Ksh) was the benefit in the last receipt?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2372,11 +2192,9 @@ "label": { "en": "If in-kind specify the kind of benefit" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2389,8 +2207,7 @@ "label": { "en": "Do you have any feedback on response we are currently carrying out?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2406,7 +2223,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2419,11 +2235,9 @@ "label": { "en": "If yes, share your feedback on response process" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2436,8 +2250,7 @@ "label": { "en": "Who in your family decides on how to spend the money?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male_household_head", @@ -2465,7 +2278,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2478,8 +2290,7 @@ "label": { "en": "Are there posibility of conflicts in the households over who receives Cash?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2495,7 +2306,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2508,8 +2318,7 @@ "label": { "en": "Do you have a gendered division of labour/roles, work, activities, responsibilities of women and men in this household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2525,7 +2334,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2538,11 +2346,9 @@ "label": { "en": "Kindly elaborate" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2555,17 +2361,28 @@ "label": { "en": "GPS location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], "duplicateCheck": false, "placeholder": null, "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "label": { + "en": "Phone Number" + }, + "placeholder": { + "en": "+243000000000" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false } ], "aboutProgram": { @@ -2575,5 +2392,10 @@ "languages": ["en"], "enableMaxPayments": true, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Safaricom" + } + ] } diff --git a/services/121-service/src/seed-data/program/program-krcs-turkana.json b/services/121-service/src/seed-data/program/program-krcs-turkana.json index ad8878bd33..6f427d4039 100644 --- a/services/121-service/src/seed-data/program/program-krcs-turkana.json +++ b/services/121-service/src/seed-data/program/program-krcs-turkana.json @@ -16,24 +16,16 @@ "distributionDuration": 6, "fixedTransferValue": 12327, "paymentAmountMultiplierFormula": null, - "financialServiceProviders": [ - { - "fsp": "Safaricom" - } - ], "targetNrRegistrations": 4000, "tryWhatsAppFirst": false, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullName", "label": { "en": "First Name" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -44,8 +36,7 @@ "label": { "en": "Gender" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male", @@ -61,7 +52,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -74,11 +64,9 @@ "label": { "en": "Age" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -91,8 +79,7 @@ "label": { "en": "Marital status of beneficiary" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "married", @@ -120,7 +107,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -133,8 +119,7 @@ "label": { "en": "Registration Type" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "self", @@ -150,7 +135,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -163,11 +147,9 @@ "label": { "en": "ID number (MPESA)" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -180,11 +162,9 @@ "label": { "en": "Name of Alternate" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -197,11 +177,9 @@ "label": { "en": "Total household size" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -214,11 +192,9 @@ "label": { "en": "Total household <5" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -231,11 +207,9 @@ "label": { "en": "Total household >60" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -248,8 +222,7 @@ "label": { "en": "Receiving other Social Assistance?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -265,7 +238,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -278,11 +250,9 @@ "label": { "en": "County" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -295,11 +265,9 @@ "label": { "en": "Sub County" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -312,11 +280,9 @@ "label": { "en": "Ward" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -329,11 +295,9 @@ "label": { "en": "Location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -346,11 +310,9 @@ "label": { "en": "Sub location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -363,11 +325,9 @@ "label": { "en": "Village" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -380,11 +340,9 @@ "label": { "en": "Nearest school" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -397,8 +355,7 @@ "label": { "en": "Area type" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "rural", @@ -414,7 +371,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -427,8 +383,7 @@ "label": { "en": "What are the main livelihood sources for the household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "salary_from_formal_employment", @@ -468,7 +423,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -481,11 +435,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -498,11 +450,9 @@ "label": { "en": "How many males 0 - 5 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -515,11 +465,9 @@ "label": { "en": "How many females 0 - 5 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -532,11 +480,9 @@ "label": { "en": "How many males 6 - 12 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -549,11 +495,9 @@ "label": { "en": "How many females 6 - 12 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -566,11 +510,9 @@ "label": { "en": "How many males 13 - 24 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -583,11 +525,9 @@ "label": { "en": "How many females 13 - 24 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -600,11 +540,9 @@ "label": { "en": "How many males 25 - 59 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -617,11 +555,9 @@ "label": { "en": "How many females 25 - 59 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -634,11 +570,9 @@ "label": { "en": "How many males over 60 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -651,11 +585,9 @@ "label": { "en": "How many females over 60 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -668,11 +600,9 @@ "label": { "en": "Total males in household" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -685,11 +615,9 @@ "label": { "en": "Total females in household" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -702,8 +630,7 @@ "label": { "en": "Do you have household members living with disability?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -719,7 +646,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -732,11 +658,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -749,8 +673,7 @@ "label": { "en": "Do you have household members living with chronic illness?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -766,7 +689,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -779,11 +701,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -796,8 +716,7 @@ "label": { "en": "Do you have any pregnant or lactating woman in your household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -813,7 +732,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -826,11 +744,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -843,11 +759,9 @@ "label": { "en": "How many habitable ROOMS does this dwelling unit contain?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -860,8 +774,7 @@ "label": { "en": "What is the TENURE status of the dwelling unit and/or surrounding terrain/land?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Owner occupied", @@ -877,7 +790,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -890,8 +802,7 @@ "label": { "en": "If owner occupied, state whether:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "purchased", @@ -919,7 +830,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -932,11 +842,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -949,8 +857,7 @@ "label": { "en": "If rented or provided, stated whether:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "government", @@ -996,7 +903,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1009,11 +915,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1026,8 +930,7 @@ "label": { "en": "What is the main construction material used for the ROOF?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Corrugated iron sheets", @@ -1085,7 +988,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1098,11 +1000,9 @@ "label": { "en": "If other ROOFING material state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1115,8 +1015,7 @@ "label": { "en": "What is the main construction material used for the WALL?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "corrugated_iron_sheets", @@ -1180,7 +1079,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1193,11 +1091,9 @@ "label": { "en": "If other type of WALL state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1210,8 +1106,7 @@ "label": { "en": "What is the main construction material used for the FLOOR?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "cement", @@ -1245,7 +1140,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1258,11 +1152,9 @@ "label": { "en": "If other type of FLOOR state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1275,8 +1167,7 @@ "label": { "en": "The dwelling unit is at RISK of:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "landslide", @@ -1310,7 +1201,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1323,11 +1213,9 @@ "label": { "en": "If other type of RISK, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1340,8 +1228,7 @@ "label": { "en": "What is the main source of WATER?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "pond", @@ -1435,7 +1322,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1448,11 +1334,9 @@ "label": { "en": "Any other source of WATER, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1465,8 +1349,7 @@ "label": { "en": "Does your household own pigs?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1482,7 +1365,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1495,11 +1377,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1512,8 +1392,7 @@ "label": { "en": "Does your household own chicken?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1529,7 +1408,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1542,8 +1420,7 @@ "label": { "en": "What is the MAIN mode of HUMAN WASTE Disposal?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "main_sewer", @@ -1601,7 +1478,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1614,11 +1490,9 @@ "label": { "en": "If any other HUMAN WASTER Disposal mode, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1631,8 +1505,7 @@ "label": { "en": "What is the MAIN type of COOKING Fuel?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "electricity", @@ -1684,7 +1557,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1697,11 +1569,9 @@ "label": { "en": "Specify other type of COOKING Fuel" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1714,8 +1584,7 @@ "label": { "en": "What is the MAIN type of LIGHTING?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "electricity", @@ -1767,7 +1636,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1780,11 +1648,9 @@ "label": { "en": "Specify other type of LIGHTING" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1797,8 +1663,7 @@ "label": { "en": "Does your household OWN any of the following ITEMS?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "television__tv", @@ -1850,7 +1715,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1863,8 +1727,7 @@ "label": { "en": "Does your household own exotic cattle?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1880,7 +1743,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1893,11 +1755,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1910,8 +1770,7 @@ "label": { "en": "Does your household own indigenous cattle?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1927,7 +1786,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1940,11 +1798,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1957,8 +1813,7 @@ "label": { "en": "Does your household own sheep?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1974,7 +1829,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1987,11 +1841,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2004,8 +1856,7 @@ "label": { "en": "Does your household own goats?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2021,7 +1872,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2034,11 +1884,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2051,8 +1899,7 @@ "label": { "en": "Does your household own camels?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2068,7 +1915,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2081,11 +1927,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2098,8 +1942,7 @@ "label": { "en": "Does your household own donkeys?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2115,7 +1958,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2128,11 +1970,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2145,11 +1985,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2162,11 +2000,9 @@ "label": { "en": "How many LIVE BIRTHS have occured in this household in the last 12 months (1 year)?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2179,11 +2015,9 @@ "label": { "en": "How many DEATHS have occured in this household in the last 12 months (1 year)?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2196,8 +2030,7 @@ "label": { "en": "Currently, the CONDITIONS of your household are?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "poor", @@ -2225,7 +2058,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2238,8 +2070,7 @@ "label": { "en": "Did anyone reduce or skip meals?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2255,7 +2086,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2268,11 +2098,9 @@ "label": { "en": "Receiving benefits from any National Safety Net Programmes?" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2285,11 +2113,9 @@ "label": { "en": "If Yes, Name the PROGRAMME and AGENCY" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2302,8 +2128,7 @@ "label": { "en": "What type of BENEFIT do you receive?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "cash", @@ -2325,7 +2150,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2338,11 +2162,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2355,11 +2177,9 @@ "label": { "en": "If Cash, how much (in Ksh) was the benefit in the last receipt?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2372,11 +2192,9 @@ "label": { "en": "If in-kind specify the kind of benefit" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2389,8 +2207,7 @@ "label": { "en": "Do you have any feedback on response we are currently carrying out?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2406,7 +2223,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2419,11 +2235,9 @@ "label": { "en": "If yes, share your feedback on response process" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2436,8 +2250,7 @@ "label": { "en": "Who in your family decides on how to spend the money?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male_household_head", @@ -2465,7 +2278,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2478,8 +2290,7 @@ "label": { "en": "Are there posibility of conflicts in the households over who receives Cash?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2495,7 +2306,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2508,8 +2318,7 @@ "label": { "en": "Do you have a gendered division of labour/roles, work, activities, responsibilities of women and men in this household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2525,7 +2334,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2538,11 +2346,9 @@ "label": { "en": "Kindly elaborate" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2555,17 +2361,28 @@ "label": { "en": "GPS location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], "duplicateCheck": false, "placeholder": null, "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "label": { + "en": "Phone Number" + }, + "placeholder": { + "en": "+243000000000" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false } ], "aboutProgram": { @@ -2575,5 +2392,10 @@ "languages": ["en"], "enableMaxPayments": true, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Safaricom" + } + ] } diff --git a/services/121-service/src/seed-data/program/program-krcs-westpokot.json b/services/121-service/src/seed-data/program/program-krcs-westpokot.json index f3afe075aa..d0a5238716 100644 --- a/services/121-service/src/seed-data/program/program-krcs-westpokot.json +++ b/services/121-service/src/seed-data/program/program-krcs-westpokot.json @@ -16,24 +16,16 @@ "distributionDuration": 3, "fixedTransferValue": 6391, "paymentAmountMultiplierFormula": null, - "financialServiceProviders": [ - { - "fsp": "Safaricom" - } - ], "targetNrRegistrations": 1800, "tryWhatsAppFirst": false, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullName", "label": { "en": "First Name" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -44,8 +36,7 @@ "label": { "en": "Gender" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male", @@ -61,7 +52,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -74,11 +64,9 @@ "label": { "en": "Age" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -91,8 +79,7 @@ "label": { "en": "Marital status of beneficiary" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "married", @@ -120,7 +107,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -133,8 +119,7 @@ "label": { "en": "Registration Type" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "self", @@ -150,7 +135,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -163,11 +147,9 @@ "label": { "en": "ID number (MPESA)" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -180,11 +162,9 @@ "label": { "en": "Name of Alternate" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -197,11 +177,9 @@ "label": { "en": "Total household size" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -214,11 +192,9 @@ "label": { "en": "Total household <5" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -231,11 +207,9 @@ "label": { "en": "Total household >60" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -248,8 +222,7 @@ "label": { "en": "Receiving other Social Assistance?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -265,7 +238,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -278,11 +250,9 @@ "label": { "en": "County" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -295,11 +265,9 @@ "label": { "en": "Sub County" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -312,11 +280,9 @@ "label": { "en": "Ward" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -329,11 +295,9 @@ "label": { "en": "Location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -346,11 +310,9 @@ "label": { "en": "Sub location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -363,11 +325,9 @@ "label": { "en": "Village" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -380,11 +340,9 @@ "label": { "en": "Nearest school" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -397,8 +355,7 @@ "label": { "en": "Area type" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "rural", @@ -414,7 +371,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -427,8 +383,7 @@ "label": { "en": "What are the main livelihood sources for the household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "salary_from_formal_employment", @@ -468,7 +423,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -481,11 +435,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -498,11 +450,9 @@ "label": { "en": "How many males 0 - 5 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -515,11 +465,9 @@ "label": { "en": "How many females 0 - 5 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -532,11 +480,9 @@ "label": { "en": "How many males 6 - 12 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -549,11 +495,9 @@ "label": { "en": "How many females 6 - 12 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -566,11 +510,9 @@ "label": { "en": "How many males 13 - 24 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -583,11 +525,9 @@ "label": { "en": "How many females 13 - 24 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -600,11 +540,9 @@ "label": { "en": "How many males 25 - 59 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -617,11 +555,9 @@ "label": { "en": "How many females 25 - 59 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -634,11 +570,9 @@ "label": { "en": "How many males over 60 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -651,11 +585,9 @@ "label": { "en": "How many females over 60 years?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -668,11 +600,9 @@ "label": { "en": "Total males in household" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -685,11 +615,9 @@ "label": { "en": "Total females in household" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -702,8 +630,7 @@ "label": { "en": "Do you have household members living with disability?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -719,7 +646,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -732,11 +658,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -749,8 +673,7 @@ "label": { "en": "Do you have household members living with chronic illness?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -766,7 +689,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -779,11 +701,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -796,8 +716,7 @@ "label": { "en": "Do you have any pregnant or lactating woman in your household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -813,7 +732,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -826,11 +744,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -843,11 +759,9 @@ "label": { "en": "How many habitable ROOMS does this dwelling unit contain?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -860,8 +774,7 @@ "label": { "en": "What is the TENURE status of the dwelling unit and/or surrounding terrain/land?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Owner occupied", @@ -877,7 +790,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -890,8 +802,7 @@ "label": { "en": "If owner occupied, state whether:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "purchased", @@ -919,7 +830,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -932,11 +842,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -949,8 +857,7 @@ "label": { "en": "If rented or provided, stated whether:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "government", @@ -996,7 +903,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1009,11 +915,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1026,8 +930,7 @@ "label": { "en": "What is the main construction material used for the ROOF?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "Corrugated iron sheets", @@ -1085,7 +988,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1098,11 +1000,9 @@ "label": { "en": "If other ROOFING material state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1115,8 +1015,7 @@ "label": { "en": "What is the main construction material used for the WALL?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "corrugated_iron_sheets", @@ -1180,7 +1079,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1193,11 +1091,9 @@ "label": { "en": "If other type of WALL state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1210,8 +1106,7 @@ "label": { "en": "What is the main construction material used for the FLOOR?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "cement", @@ -1245,7 +1140,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1258,11 +1152,9 @@ "label": { "en": "If other type of FLOOR state" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1275,8 +1167,7 @@ "label": { "en": "The dwelling unit is at RISK of:" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "landslide", @@ -1310,7 +1201,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1323,11 +1213,9 @@ "label": { "en": "If other type of RISK, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1340,8 +1228,7 @@ "label": { "en": "What is the main source of WATER?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "pond", @@ -1435,7 +1322,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1448,11 +1334,9 @@ "label": { "en": "Any other source of WATER, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1465,8 +1349,7 @@ "label": { "en": "Does your household own pigs?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1482,7 +1365,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1495,11 +1377,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1512,8 +1392,7 @@ "label": { "en": "Does your household own chicken?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1529,7 +1408,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1542,8 +1420,7 @@ "label": { "en": "What is the MAIN mode of HUMAN WASTE Disposal?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "main_sewer", @@ -1601,7 +1478,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1614,11 +1490,9 @@ "label": { "en": "If any other HUMAN WASTER Disposal mode, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1631,8 +1505,7 @@ "label": { "en": "What is the MAIN type of COOKING Fuel?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "electricity", @@ -1684,7 +1557,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1697,11 +1569,9 @@ "label": { "en": "Specify other type of COOKING Fuel" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1714,8 +1584,7 @@ "label": { "en": "What is the MAIN type of LIGHTING?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "electricity", @@ -1767,7 +1636,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1780,11 +1648,9 @@ "label": { "en": "Specify other type of LIGHTING" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1797,8 +1663,7 @@ "label": { "en": "Does your household OWN any of the following ITEMS?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "television__tv", @@ -1850,7 +1715,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1863,8 +1727,7 @@ "label": { "en": "Does your household own exotic cattle?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1880,7 +1743,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1893,11 +1755,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1910,8 +1770,7 @@ "label": { "en": "Does your household own indigenous cattle?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1927,7 +1786,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1940,11 +1798,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1957,8 +1813,7 @@ "label": { "en": "Does your household own sheep?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -1974,7 +1829,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -1987,11 +1841,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2004,8 +1856,7 @@ "label": { "en": "Does your household own goats?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2021,7 +1872,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2034,11 +1884,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2051,8 +1899,7 @@ "label": { "en": "Does your household own camels?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2068,7 +1915,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2081,11 +1927,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2098,8 +1942,7 @@ "label": { "en": "Does your household own donkeys?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2115,7 +1958,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2128,11 +1970,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2145,11 +1985,9 @@ "label": { "en": "If yes, how many?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2162,11 +2000,9 @@ "label": { "en": "How many LIVE BIRTHS have occured in this household in the last 12 months (1 year)?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2179,11 +2015,9 @@ "label": { "en": "How many DEATHS have occured in this household in the last 12 months (1 year)?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2196,8 +2030,7 @@ "label": { "en": "Currently, the CONDITIONS of your household are?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "poor", @@ -2225,7 +2058,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2238,8 +2070,7 @@ "label": { "en": "Did anyone reduce or skip meals?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2255,7 +2086,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2268,11 +2098,9 @@ "label": { "en": "Receiving benefits from any National Safety Net Programmes?" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2285,11 +2113,9 @@ "label": { "en": "If Yes, Name the PROGRAMME and AGENCY" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2302,8 +2128,7 @@ "label": { "en": "What type of BENEFIT do you receive?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "cash", @@ -2325,7 +2150,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2338,11 +2162,9 @@ "label": { "en": "If other, specify" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2355,11 +2177,9 @@ "label": { "en": "If Cash, how much (in Ksh) was the benefit in the last receipt?" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2372,11 +2192,9 @@ "label": { "en": "If in-kind specify the kind of benefit" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2389,8 +2207,7 @@ "label": { "en": "Do you have any feedback on response we are currently carrying out?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2406,7 +2223,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2419,11 +2235,9 @@ "label": { "en": "If yes, share your feedback on response process" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2436,8 +2250,7 @@ "label": { "en": "Who in your family decides on how to spend the money?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "male_household_head", @@ -2465,7 +2278,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2478,8 +2290,7 @@ "label": { "en": "Are there posibility of conflicts in the households over who receives Cash?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2495,7 +2306,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2508,8 +2318,7 @@ "label": { "en": "Do you have a gendered division of labour/roles, work, activities, responsibilities of women and men in this household?" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "yes", @@ -2525,7 +2334,6 @@ } ], "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2538,11 +2346,9 @@ "label": { "en": "Kindly elaborate" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], @@ -2555,17 +2361,28 @@ "label": { "en": "GPS location" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, "scoring": {}, - "persistence": true, "pattern": null, "editableInPortal": true, "export": ["all-people-affected", "included"], "duplicateCheck": false, "placeholder": null, "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "label": { + "en": "Phone Number" + }, + "placeholder": { + "en": "+243000000000" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false } ], "aboutProgram": { @@ -2575,5 +2392,10 @@ "languages": ["en"], "enableMaxPayments": true, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Safaricom" + } + ] } diff --git a/services/121-service/src/seed-data/program/program-nlrc-ocw.json b/services/121-service/src/seed-data/program/program-nlrc-ocw.json index e7d429b27d..e147b13768 100644 --- a/services/121-service/src/seed-data/program/program-nlrc-ocw.json +++ b/services/121-service/src/seed-data/program/program-nlrc-ocw.json @@ -15,52 +15,17 @@ "distributionFrequency": "2-weeks", "distributionDuration": 46, "fixedTransferValue": 25, - "financialServiceProviders": [ - { - "fsp": "Intersolve-voucher-whatsapp", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - }, - { - "fsp": "Intersolve-visa", - "configuration": [ - { - "name": "brandCode", - "value": "INTERSOLVE_VISA_BRAND_CODE" - }, - { - "name": "coverLetterCode", - "value": "INTERSOLVE_VISA_COVERLETTER_CODE" - }, - { - "name": "fundingTokenCode", - "value": "INTERSOLVE_VISA_FUNDINGTOKEN_CODE" - } - ] - } - ], "targetNrRegistrations": 100000, - "programCustomAttributes": [], "tryWhatsAppFirst": true, - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "fullName", "label": { "en": "Full Name" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "pattern": null, "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -74,15 +39,74 @@ "placeholder": { "en": "+31 6 00 00 00 00" }, - "answerType": "tel", - "questionType": "standard", + "type": "tel", "options": null, - "persistence": true, "export": [], "scoring": {}, "duplicateCheck": true, "editableInPortal": false, "showInPeopleAffectedTable": false + }, + { + "name": "whatsappPhoneNumber", + "label": { + "en": "WhatsApp Nr." + }, + "placeholder": { + "ar": "00 00 00 00 0 00+", + "en": "+00 0 00 00 00 00" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "duplicateCheck": true, + "showInPeopleAffectedTable": true + }, + { + "name": "addressStreet", + "label": { + "en": "Address street" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "showInPeopleAffectedTable": false + }, + { + "name": "addressHouseNumber", + "label": { + "en": "Address house number" + }, + "export": ["all-people-affected", "included"], + "type": "numeric", + "showInPeopleAffectedTable": false + }, + { + "name": "addressHouseNumberAddition", + "label": { + "en": "Address house number addition" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "isRequired": false, + "showInPeopleAffectedTable": false + }, + { + "name": "addressPostalCode", + "label": { + "en": "Address postal code" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "showInPeopleAffectedTable": false + }, + { + "name": "addressCity", + "label": { + "en": "Address city" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "showInPeopleAffectedTable": false } ], "aboutProgram": { @@ -93,5 +117,37 @@ "enableMaxPayments": false, "enableScope": false, "monitoringDashboardUrl": "https://app.powerbi.com/view?r=eyJrIjoiNDkxN2Q1NmEtOWIxOC00ZTYyLTkxMTMtYmI3MmE4YTVkMTE2IiwidCI6ImEyYjUzYmU1LTczNGUtNGU2Yy1hYjBkLWQxODRmNjBmZDkxNyIsImMiOjh9", - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Intersolve-voucher-whatsapp", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + }, + { + "financialServiceProvider": "Intersolve-visa", + "properties": [ + { + "name": "brandCode", + "value": "INTERSOLVE_VISA_BRAND_CODE" + }, + { + "name": "coverLetterCode", + "value": "INTERSOLVE_VISA_COVERLETTER_CODE" + }, + { + "name": "fundingTokenCode", + "value": "INTERSOLVE_VISA_FUNDINGTOKEN_CODE" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-nlrc-pv.json b/services/121-service/src/seed-data/program/program-nlrc-pv.json index a4d87ef357..4d05e607c4 100644 --- a/services/121-service/src/seed-data/program/program-nlrc-pv.json +++ b/services/121-service/src/seed-data/program/program-nlrc-pv.json @@ -15,66 +15,9 @@ "distributionFrequency": "week", "distributionDuration": 90, "fixedTransferValue": 17.5, - "financialServiceProviders": [ - { - "fsp": "Intersolve-voucher-whatsapp", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - }, - { - "fsp": "Intersolve-voucher-paper", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - }, - { - "fsp": "Intersolve-visa", - "configuration": [ - { - "name": "brandCode", - "value": "INTERSOLVE_VISA_BRAND_CODE" - }, - { - "name": "coverLetterCode", - "value": "INTERSOLVE_VISA_COVERLETTER_CODE" - }, - { - "name": "fundingTokenCode", - "value": "INTERSOLVE_VISA_FUNDINGTOKEN_CODE" - } - ] - }, - { - "fsp": "Excel", - "configuration": [ - { - "name": "displayName", - "value": { - "en": "Occasional (physical)", - "nl": "Af-en-toe (Fysiek)" - } - } - ] - } - ], "targetNrRegistrations": 250, "tryWhatsAppFirst": true, - "programCustomAttributes": [ + "programRegistrationAttributes": [ { "label": { "en": "Partner Organization" @@ -83,19 +26,15 @@ "type": "text", "export": ["all-people-affected", "included", "payment"], "showInPeopleAffectedTable": true - } - ], - "programQuestions": [ + }, { "name": "fullName", "label": { "en": "Full Name" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "pattern": null, "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -109,16 +48,77 @@ "placeholder": { "en": "+31 6 00 00 00 00" }, - "answerType": "tel", - "questionType": "standard", + "type": "tel", "pattern": null, "options": null, - "persistence": true, "export": [], "scoring": {}, "duplicateCheck": true, "editableInPortal": false, "showInPeopleAffectedTable": false + }, + { + "name": "whatsappPhoneNumber", + "label": { + "en": "WhatsApp Nr." + }, + "placeholder": { + "ar": "00 00 00 00 0 00+", + "en": "+00 0 00 00 00 00" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "duplicateCheck": true, + "showInPeopleAffectedTable": true + }, + { + "name": "addressStreet", + "label": { + "en": "Address street" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "pattern": ".+", + "showInPeopleAffectedTable": false + }, + { + "name": "addressHouseNumber", + "label": { + "en": "Address house number" + }, + "export": ["all-people-affected", "included"], + "type": "numeric", + "showInPeopleAffectedTable": false + }, + { + "name": "addressHouseNumberAddition", + "label": { + "en": "Address house number addition" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "showInPeopleAffectedTable": false + }, + { + "name": "addressPostalCode", + "label": { + "en": "Address postal code" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "pattern": ".+", + "showInPeopleAffectedTable": false + }, + { + "name": "addressCity", + "label": { + "en": "Address city" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "pattern": ".+", + "showInPeopleAffectedTable": false } ], "aboutProgram": { @@ -135,5 +135,66 @@ "languages": ["ar", "en", "es", "fr", "in", "nl", "pt_BR", "tl"], "enableMaxPayments": true, "enableScope": true, - "allowEmptyPhoneNumber": true + "allowEmptyPhoneNumber": true, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Intersolve-voucher-whatsapp", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + }, + { + "financialServiceProvider": "Intersolve-voucher-paper", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + }, + { + "financialServiceProvider": "Intersolve-visa", + "properties": [ + { + "name": "brandCode", + "value": "INTERSOLVE_VISA_BRAND_CODE" + }, + { + "name": "coverLetterCode", + "value": "INTERSOLVE_VISA_COVERLETTER_CODE" + }, + { + "name": "fundingTokenCode", + "value": "INTERSOLVE_VISA_FUNDINGTOKEN_CODE" + } + ] + }, + { + "financialServiceProvider": "Excel", + "properties": [ + { + "name": "displayName", + "value": { + "en": "Occasional (physical)", + "nl": "Af-en-toe (Fysiek)" + } + }, + { + "name": "columnToMatch", + "value": "phoneNumber" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-pilot-dorcas-eth.json b/services/121-service/src/seed-data/program/program-pilot-dorcas-eth.json new file mode 100644 index 0000000000..091acbb2df --- /dev/null +++ b/services/121-service/src/seed-data/program/program-pilot-dorcas-eth.json @@ -0,0 +1,730 @@ +{ + "published": true, + "validation": true, + "location": "Ethiopia", + "ngo": "Dorcas", + "titlePortal": { + "en": "121 Digital Cash Aid Program", + "am_ET": "121 ዲጂታል የገንዘብ እርዳታ ፕሮግራም" + }, + "description": { + "en": "" + }, + "startDate": "2021-12-01T12:00:00Z", + "endDate": "2022-03-31T12:00:00Z", + "currency": "ETB", + "distributionFrequency": "month", + "distributionDuration": 3, + "fixedTransferValue": 6250, + "targetNrRegistrations": 55, + "programRegistrationAttributes": [ + { + "label": { + "en": "Business plan delivered" + }, + "name": "businessPlanDelivered", + "type": "boolean" + }, + { + "label": { + "en": "Completed training" + }, + "name": "completedTraining", + "type": "boolean" + }, + { + "label": { + "en": "Complete all milestones" + }, + "name": "milestones", + "type": "boolean" + }, + { + "name": "name", + "label": { + "en": "Full name" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "age", + "label": { + "en": "Age" + }, + "type": "dropdown", + "options": [ + { + "option": "0-17", + "label": { + "en": "0 - 17 years", + "am_ET": "0 - 17 ዓመት" + } + }, + { + "option": "18-30", + "label": { + "en": "18 - 30 years", + "am_ET": "18 - 30 ዓመት" + } + }, + { + "option": "31-", + "label": { + "en": "31 years or older", + "am_ET": "31 ዓመት ወይም ከዚያ በላይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "0-17": 0, + "18-30": 5, + "31-": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "education", + "label": { + "en": "Education level" + }, + "type": "dropdown", + "options": [ + { + "option": "primary", + "label": { + "en": "Primary school (grade 1-8)", + "am_ET": "የመጀመሪያ ደረጃ ትምህርት ያጠናቀቀ (1-8 ክፍል)" + } + }, + { + "option": "secondary", + "label": { + "en": "Secondary school (grade 9-12)", + "am_ET": "የሁለተኛ ደረጃ ትምህርት ያጠናቀቀ (9-12 ክፍል)" + } + }, + { + "option": "university", + "label": { + "en": "University", + "am_ET": "ዩኒቨርሲቲ" + } + }, + { + "option": "other", + "label": { + "en": "Other", + "am_ET": "ሌላ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nationalIdNumber", + "label": { + "en": "National ID" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfFamilyMembers", + "label": { + "en": "Family members" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfFemale", + "label": { + "en": "Nr. of females" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfMale", + "label": { + "en": "Nr. of males" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "hasHusband", + "label": { + "en": "Has husband" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "whenReturnee", + "label": { + "en": "Returnee" + }, + "type": "dropdown", + "options": [ + { + "option": "noReturnee", + "label": { + "en": "I did not return from a country in the Middle East", + "am_ET": "ከመካከለኛው ምስራቅ ሀገር አልተመለስኩም" + } + }, + { + "option": "less2years", + "label": { + "en": "Less than 2 years", + "am_ET": "ከ 2 ዓመት በታች" + } + }, + { + "option": "2-4years", + "label": { + "en": "2 - 4 years", + "am_ET": "2-4 ዓመት" + } + }, + { + "option": "more4years", + "label": { + "en": "More than 4 years", + "am_ET": "ከ 4 ዓመት በላይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "noReturnee": 0, + "less2years": 5, + "2-4years": 3, + "more4years": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "isHouseholdLeader", + "label": { + "en": "Household leader" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 3, + "no": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "business", + "label": { + "en": "Owns business" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "businessLicenseAddis", + "label": { + "en": "Business license" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 2 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "typeBusiness", + "label": { + "en": "Business type" + }, + "type": "dropdown", + "options": [ + { + "option": "restaurant", + "label": { + "en": "Small cafe or restaurant", + "am_ET": "አነስተኛ ካፌ ወይም ምግብ ቤት" + } + }, + { + "option": "coffee", + "label": { + "en": "Coffee house", + "am_ET": "ቡና ቤት" + } + }, + { + "option": "bakery", + "label": { + "en": "Bakery", + "am_ET": "ዳቦ ቤት" + } + }, + { + "option": "hair", + "label": { + "en": "Hair salon / beauty salon", + "am_ET": "የፀጉር ሳሎን / የውበት ሳሎን" + } + }, + { + "option": "clothes", + "label": { + "en": "Clothes shop / boutique", + "am_ET": "የልብስ ሱቅ / ቡቲክ" + } + }, + { + "option": "retail", + "label": { + "en": "Retail shop", + "am_ET": "የችርቻሮ ሱቅ" + } + }, + { + "option": "other", + "label": { + "en": "Other", + "am_ET": "ሌላ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "typeBusinessOther", + "label": { + "en": "Other business type" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "businessGoal", + "label": { + "en": "Business goal" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + }, + { + "option": "limited", + "label": { + "en": "I have a limited goal and interest to expand my small business", + "am_ET": "የእኔን አነስተኛ ንግድ ለማስፋፋት የተወሰነ ግብ እና ፍላጎት አለኝ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 0, + "limited": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "businessPlan", + "label": { + "en": "Business plan" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "ageBusiness", + "label": { + "en": "Age business" + }, + "type": "dropdown", + "options": [ + { + "option": "1year-", + "label": { + "en": "Less than 1 year", + "am_ET": "ከ1 አመት በታች" + } + }, + { + "option": "1-2years", + "label": { + "en": "1 - 2 years", + "am_ET": "1-2 ዓመት" + } + }, + { + "option": "3-4years", + "label": { + "en": "3 - 4 years", + "am_ET": "3-4 ዓመት" + } + }, + { + "option": "4years+", + "label": { + "en": "More than 4 years", + "am_ET": "ከ 4 ዓመት በላይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "1year-": 0, + "1-2years": 1, + "3-4years": 2, + "4years+": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "incomeBusiness", + "label": { + "en": "Income business" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "costBusiness", + "label": { + "en": "Cost business" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "committedToTraining", + "label": { + "en": "Training" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + }, + { + "option": "notSure", + "label": { + "en": "Not sure", + "am_ET": "እርግጠኛ አይደለሁም" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 0, + "notSure": 2 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "vocationalCourses", + "label": { + "en": "Courses" + }, + "type": "dropdown", + "options": [ + { + "option": "specificAlign", + "label": { + "en": "I am trained in a specific vocational or specialised courses that aligns with my business areas", + "am_ET": "ከንግድ አካባቢዎቼ ጋር የሚጣጣሙ ልዩ የሙያ ወይም ልዩ ኮርሶች ሰልጥኛለሁ።" + } + }, + { + "option": "specificNoAlign", + "label": { + "en": "I am trained in a specific vocational or specialised courses but that doesn't align with my business areas", + "am_ET": "በልዩ ሙያ ወይም በልዩ ኮርሶች ሰልጥኛለሁ ግን ያ ከንግድ ስራዬ ጋር አይጣጣምም።" + } + }, + { + "option": "usefulSkills", + "label": { + "en": "I only have practical skills", + "am_ET": "የእጅ ሙያ ብቻ ነው ያለኝ" + } + }, + { + "option": "none", + "label": { + "en": "I do not have any", + "am_ET": "ምንም የለኝም" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "specificAlign": 4, + "specificNoAlign": 3, + "usefulSkills": 2, + "none": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "savingMethod", + "label": { + "en": "Saving method" + }, + "type": "dropdown", + "options": [ + { + "option": "homeSaving", + "label": { + "en": "Home saving", + "am_ET": "የቤት ቁጠባ" + } + }, + { + "option": "bank", + "label": { + "en": "Bank", + "am_ET": "ባንክ" + } + }, + { + "option": "mobileMoney", + "label": { + "en": "Mobile money", + "am_ET": "የሞባይል ገንዘብ" + } + }, + { + "option": "ekrub", + "label": { + "en": "Ekrub", + "am_ET": "እቁብ" + } + }, + { + "option": "other", + "label": { + "en": "Other", + "am_ET": "ሌላ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "label": { + "en": "Phone Number" + }, + "placeholder": { + "en": "+251 000 000 000", + "am_ET": "+251 000 000 000" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "typePhoneNumber", + "label": { + "en": "Phone Type" + }, + "type": "dropdown", + "options": [ + { + "option": "ownSmartPhone", + "label": { + "en": "Own smartphone number", + "am_ET": "የግል ሞባይል መስመር" + } + }, + { + "option": "ownFeaturePhone", + "label": { + "en": "Own basic / feature phone number", + "am_ET": "የግል የቤት ሰልክ መስመር" + } + }, + { + "option": "notOwnPhone", + "label": { + "en": "Someone else's phone number", + "am_ET": "የጎረቤት ስልክ መስመር" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "showInPeopleAffectedTable": false + } + ], + "aboutProgram": { + "en": "Returnees face economic and social difficulties upon return to Ethiopia. Therefore, this programme is designed to support a successful economic reintegration of returnees from the Middle East through a cash for livelihoods intervention. The project targets vulnerable women with existing small-scale business, who are victims of trafficking.", + "am_ET": "ከስደት ተመላሾች ወደ ኢትዮጵያ ሲመለሱ ኢኮኖሚያዊ እና ማህበራዊ ችግሮች ይገጥማቸዋል። ስለዚህ ይህ ፕሮግራም ከመካከለኛው ምስራቅ የተመለሱ ስደተኞችን መልሶ ለማቋቋም የገንዘብ ድጋፍ ለማድረግ የተነደፈ ነው።" + }, + "fullnameNamingConvention": ["name"], + "languages": ["en", "am_ET"], + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "BelCash" + } + ] +} diff --git a/services/121-service/src/seed-data/program/program-pilot-lbn.json b/services/121-service/src/seed-data/program/program-pilot-lbn.json new file mode 100644 index 0000000000..a4d2ffbae2 --- /dev/null +++ b/services/121-service/src/seed-data/program/program-pilot-lbn.json @@ -0,0 +1,1788 @@ +{ + "published": true, + "validation": true, + "location": "Lebanon", + "ngo": "Dorcas", + "titlePortal": { + "en": "Dorcas-Tabitha cash aid program", + "ar": "برنامج دوركاس طابيثا للمساعدات النقدية " + }, + "description": { + "en": "" + }, + "startDate": "2021-12-01T12:00:00Z", + "endDate": "2022-06-30T12:00:00Z", + "currency": "USD", + "distributionFrequency": "month", + "distributionDuration": 6, + "fixedTransferValue": 50, + "targetNrRegistrations": 340, + "programRegistrationAttributes": [ + { + "name": "invited-by-dorcas", + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "name", + "label": { + "en": "Name" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "age", + "label": { + "en": "Age" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "gender", + "label": { + "en": "Gender" + }, + "type": "dropdown", + "options": [ + { + "option": "male", + "label": { + "en": "Male", + "ar": "ذكر" + } + }, + { + "option": "female", + "label": { + "en": "Female", + "ar": "أنثى" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nationality", + "label": { + "en": "Nationality" + }, + "type": "dropdown", + "options": [ + { + "option": "lebanese", + "label": { + "en": "Lebanese", + "ar": "لبنانية" + } + }, + { + "option": "syrian", + "label": { + "en": "Syrian", + "ar": "سورية" + } + }, + { + "option": "other", + "label": { + "en": "Other", + "ar": "اُخرى" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "address-city", + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "address-street", + "label": { + "en": "City" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "address-building", + "label": { + "en": "Building" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "address-floor", + "label": { + "en": "Floor" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "social-registry", + "label": { + "en": "Social registry number" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "family-status", + "label": { + "en": "Family status" + }, + "type": "dropdown", + "options": [ + { + "option": "married", + "label": { + "en": "Married", + "ar": "متزوج" + } + }, + { + "option": "single-no-children", + "label": { + "en": "Divorced/Single/Widow (no Children)", + "ar": "مطلق/أعزب/أرمل ( لا أولاد)" + } + }, + { + "option": "single-parent", + "label": { + "en": "Single parent", + "ar": "مقدم(ة) الرعاية الوحيد(ة)" + } + }, + { + "option": "child-headed", + "label": { + "en": "Child headed house hold", + "ar": "أسرة يعيلها طفل " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "married": 0, + "single-no-children": 1, + "single-parent": 3, + "child-headed": 6 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "men-60+", + "label": { + "en": "Men 60+" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": { + "multiplier": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "women-60+", + "label": { + "en": "Women 60+" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": { + "multiplier": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "men-18-15", + "label": { + "en": "Men 18-59" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": { + "multiplier": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "women-18-59", + "label": { + "en": "Women 18-59" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": { + "multiplier": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "men-18-", + "label": { + "en": "Men 0-18" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": { + "multiplier": 2 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "woman-18-", + "label": { + "en": "Women 0-18" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": { + "multiplier": 2 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "woman-pregnant", + "label": { + "en": "Women pregnant" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 2, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "children-3-", + "label": { + "en": "Children" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 2, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "beirut-blast-affected", + "label": { + "en": "Beirut blast" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 2, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "beirut-blast-move", + "label": { + "en": "Forced moved" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 2, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "foster-family", + "label": { + "en": "Foster family" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 2, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "hospital-child", + "label": { + "en": "Children hospital" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 4, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "hospital-adult", + "label": { + "en": "Adult hospital" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 2, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "disability-adult-seeing", + "label": { + "en": "Disability seeing" + }, + "type": "dropdown", + "options": [ + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + }, + { + "option": "yes-some", + "label": { + "en": "Yes/Some difficulty", + "ar": "نعم / بعض الصعوبة " + } + }, + { + "option": "yes-lot", + "label": { + "en": "Yes/A lot of difficulty", + "ar": "نعم / الكثير من الصعوبة" + } + }, + { + "option": "yes-completely", + "label": { + "en": "Yes/Cannot do at all", + "ar": "نعم / لا يمكن القيام به على الإطلاق " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "no": 0, + "yes-some": 2, + "yes-lot": 4, + "yes-completely": 6 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "disability-adult-hearing", + "label": { + "en": "Disability hearing" + }, + "type": "dropdown", + "options": [ + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + }, + { + "option": "yes-some", + "label": { + "en": "Yes/Some difficulty", + "ar": "نعم / بعض الصعوبة " + } + }, + { + "option": "yes-lot", + "label": { + "en": "Yes/A lot of difficulty", + "ar": "نعم / الكثير من الصعوبة" + } + }, + { + "option": "yes-completely", + "label": { + "en": "Yes/Cannot do at all", + "ar": "نعم / لا يمكن القيام به على الإطلاق " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "no": 0, + "yes-some": 2, + "yes-lot": 4, + "yes-completely": 6 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "disability-adult-walking", + "label": { + "en": "Disability walking" + }, + "type": "dropdown", + "options": [ + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + }, + { + "option": "yes-some", + "label": { + "en": "Yes/Some difficulty", + "ar": "نعم / بعض الصعوبة " + } + }, + { + "option": "yes-lot", + "label": { + "en": "Yes/A lot of difficulty", + "ar": "نعم / الكثير من الصعوبة" + } + }, + { + "option": "yes-completely", + "label": { + "en": "Yes/Cannot do at all", + "ar": "نعم / لا يمكن القيام به على الإطلاق " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "no": 0, + "yes-some": 2, + "yes-lot": 4, + "yes-completely": 6 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "disability-adult-remembering", + "label": { + "en": "Disability remembering" + }, + "type": "dropdown", + "options": [ + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + }, + { + "option": "yes-some", + "label": { + "en": "Yes/Some difficulty", + "ar": "نعم / بعض الصعوبة " + } + }, + { + "option": "yes-lot", + "label": { + "en": "Yes/A lot of difficulty", + "ar": "نعم / الكثير من الصعوبة" + } + }, + { + "option": "yes-completely", + "label": { + "en": "Yes/Cannot do at all", + "ar": "نعم / لا يمكن القيام به على الإطلاق " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "no": 0, + "yes-some": 2, + "yes-lot": 4, + "yes-completely": 6 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "disability-adult-self-care", + "label": { + "en": "Disability self-care" + }, + "type": "dropdown", + "options": [ + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + }, + { + "option": "yes-some", + "label": { + "en": "Yes/Some difficulty", + "ar": "نعم / بعض الصعوبة " + } + }, + { + "option": "yes-lot", + "label": { + "en": "Yes/A lot of difficulty", + "ar": "نعم / الكثير من الصعوبة" + } + }, + { + "option": "yes-completely", + "label": { + "en": "Yes/Cannot do at all", + "ar": "نعم / لا يمكن القيام به على الإطلاق " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "no": 0, + "yes-some": 2, + "yes-lot": 4, + "yes-completely": 6 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "disability-adult-self-communicating", + "label": { + "en": "Disability communicating" + }, + "type": "dropdown", + "options": [ + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + }, + { + "option": "yes-some", + "label": { + "en": "Yes/Some difficulty", + "ar": "نعم / بعض الصعوبة " + } + }, + { + "option": "yes-lot", + "label": { + "en": "Yes/A lot of difficulty", + "ar": "نعم / الكثير من الصعوبة" + } + }, + { + "option": "yes-completely", + "label": { + "en": "Yes/Cannot do at all", + "ar": "نعم / لا يمكن القيام به على الإطلاق " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "no": 0, + "yes-some": 2, + "yes-lot": 4, + "yes-completely": 6 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "chronic-disease-child", + "label": { + "en": "Children chronic disease" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 4, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "chronic-disease-adult", + "label": { + "en": "Adult chronic disease" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 2, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "expenses-health", + "label": { + "en": "Health expenses" + }, + "type": "dropdown", + "options": [ + { + "option": "below-500000", + "label": { + "en": "Below 500.000 LBP", + "ar": "أقل من 500.000 ليرة لبنانية " + } + }, + { + "option": "500000-1000000", + "label": { + "en": "Between 500.000 and 1.000.000 LBP", + "ar": "بين 500.000 و 1.000.000 ليرة لبنانية " + } + }, + { + "option": "above-1000000", + "label": { + "en": "Above 1.000.000 LBP", + "ar": "فوق 1.000.000 ليرة لبنانية " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "below-500000": 0, + "500000-1000000": 1, + "above-1000000": 2 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "expenses-shelter", + "label": { + "en": "Shelter expenses" + }, + "type": "dropdown", + "options": [ + { + "option": "below-1000000", + "label": { + "en": "Below 1.000.000 LBP", + "ar": "أقل من 1.000.000 ليرة لبنانية " + } + }, + { + "option": "1000000-3000000", + "label": { + "en": "Between 1.000.000 and 3.000.000 LBP", + "ar": "بين 1.000.000 و 3.000.000 ليرة لبنانية " + } + }, + { + "option": "3000000-5000000", + "label": { + "en": "Between 3.000.000 and 5.000.000 LBP", + "ar": "بين 3.000.000 و 5.000.000 ليرة لبنانية " + } + }, + { + "option": "above-5000000", + "label": { + "en": "Above 5.000.000 LBP", + "ar": "فوق 5.000.000 ليرة لبنانية " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "below-1000000": 2, + "1000000-3000000": 4, + "3000000-5000000": 6, + "above-5000000": 8 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "expenses-food", + "label": { + "en": "Food expenses" + }, + "type": "dropdown", + "options": [ + { + "option": "below-1000000", + "label": { + "en": "Below 1.000.000 LBP", + "ar": "أقل من 1.000.000 ليرة لبنانية " + } + }, + { + "option": "1000000-3000000", + "label": { + "en": "Between 1.000.000 and 3.000.000 LBP", + "ar": "بين 1.000.000 و 3.000.000 ليرة لبنانية " + } + }, + { + "option": "3000000-5000000", + "label": { + "en": "Between 3.000.000 and 5.000.000 LBP", + "ar": "بين 3.000.000 و 5.000.000 ليرة لبنانية " + } + }, + { + "option": "above-5000000", + "label": { + "en": "Above 5.000.000 LBP", + "ar": "فوق 5.000.000 ليرة لبنانية " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "below-1000000": 2, + "1000000-3000000": 4, + "3000000-5000000": 6, + "above-5000000": 8 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-sold-assests", + "label": { + "en": "Sold household goods" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-spent-saving", + "label": { + "en": "Spend savings" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-borrowed-money", + "label": { + "en": "Borrowed money" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-recuded-expenses", + "label": { + "en": "Reduced expenses" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-withdraw-school", + "label": { + "en": "Withdraw food" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-child-labor", + "label": { + "en": "Child labor" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-household-migrated", + "label": { + "en": "Household migrated" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "food-sold-real-estate", + "label": { + "en": "Sold real estate" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 1, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "7-days-eat-less", + "label": { + "en": "Eating less" + }, + "type": "dropdown", + "options": [ + { + "option": "never", + "label": { + "en": "Never", + "ar": "أبداً" + } + }, + { + "option": "yes-1-2", + "label": { + "en": "1 or 2 days", + "ar": "يوم أو يومين " + } + }, + { + "option": "yes-3-4", + "label": { + "en": "3 or 4 days", + "ar": "3 أو 4 أيام " + } + }, + { + "option": "yes-5-6", + "label": { + "en": "5 or 6 days", + "ar": "5 أو 6 أيام " + } + }, + { + "option": "yes-7", + "label": { + "en": "7 days", + "ar": "7 أيام" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "never": 0, + "yes-1-2": 1, + "yes-3-4": 2, + "yes-5-6": 3, + "yes-7": 4 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "7-days-borrowed-food", + "label": { + "en": "Borrowed food" + }, + "type": "dropdown", + "options": [ + { + "option": "never", + "label": { + "en": "Never", + "ar": "أبداً" + } + }, + { + "option": "yes-1-2", + "label": { + "en": "1 or 2 days", + "ar": "يوم أو يومين " + } + }, + { + "option": "yes-3-4", + "label": { + "en": "3 or 4 days", + "ar": "3 أو 4 أيام " + } + }, + { + "option": "yes-5-6", + "label": { + "en": "5 or 6 days", + "ar": "5 أو 6 أيام " + } + }, + { + "option": "yes-7", + "label": { + "en": "7 days", + "ar": "7 أيام" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "never": 0, + "yes-1-2": 1, + "yes-3-4": 2, + "yes-5-6": 3, + "yes-7": 4 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "7-days-reduce-portions", + "label": { + "en": "Reduced portions" + }, + "type": "dropdown", + "options": [ + { + "option": "never", + "label": { + "en": "Never", + "ar": "أبداً" + } + }, + { + "option": "yes-1-2", + "label": { + "en": "1 or 2 days", + "ar": "يوم أو يومين " + } + }, + { + "option": "yes-3-4", + "label": { + "en": "3 or 4 days", + "ar": "3 أو 4 أيام " + } + }, + { + "option": "yes-5-6", + "label": { + "en": "5 or 6 days", + "ar": "5 أو 6 أيام " + } + }, + { + "option": "yes-7", + "label": { + "en": "7 days", + "ar": "7 أيام" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "never": 0, + "yes-1-2": 1, + "yes-3-4": 2, + "yes-5-6": 3, + "yes-7": 4 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "7-days-reduce-portions-adults", + "label": { + "en": "Reduced intake adults" + }, + "type": "dropdown", + "options": [ + { + "option": "never", + "label": { + "en": "Never", + "ar": "أبداً" + } + }, + { + "option": "yes-1-2", + "label": { + "en": "1 or 2 days", + "ar": "يوم أو يومين " + } + }, + { + "option": "yes-3-4", + "label": { + "en": "3 or 4 days", + "ar": "3 أو 4 أيام " + } + }, + { + "option": "yes-5-6", + "label": { + "en": "5 or 6 days", + "ar": "5 أو 6 أيام " + } + }, + { + "option": "yes-7", + "label": { + "en": "7 days", + "ar": "7 أيام" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "never": 0, + "yes-1-2": 1, + "yes-3-4": 2, + "yes-5-6": 3, + "yes-7": 4 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "7-days-reduce-nr-meals", + "label": { + "en": "Reduced meals" + }, + "type": "dropdown", + "options": [ + { + "option": "never", + "label": { + "en": "Never", + "ar": "أبداً" + } + }, + { + "option": "yes-1-2", + "label": { + "en": "1 or 2 days", + "ar": "يوم أو يومين " + } + }, + { + "option": "yes-3-4", + "label": { + "en": "3 or 4 days", + "ar": "3 أو 4 أيام " + } + }, + { + "option": "yes-5-6", + "label": { + "en": "5 or 6 days", + "ar": "5 أو 6 أيام " + } + }, + { + "option": "yes-7", + "label": { + "en": "7 days", + "ar": "7 أيام" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "never": 0, + "yes-1-2": 1, + "yes-3-4": 2, + "yes-5-6": 3, + "yes-7": 4 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "income", + "label": { + "en": "Household income" + }, + "type": "dropdown", + "options": [ + { + "option": "below-1000000", + "label": { + "en": "Below 1.000.000 LBP", + "ar": "أقل من 1.000.000 ليرة لبنانية " + } + }, + { + "option": "1000000-3000000", + "label": { + "en": "Between 1.000.000 and 3.000.000 LBP", + "ar": "بين 1.000.000 و 3.000.000 ليرة لبنانية " + } + }, + { + "option": "3000000-5000000", + "label": { + "en": "Between 3.000.000 and 5.000.000 LBP", + "ar": "بين 3.000.000 و 5.000.000 ليرة لبنانية " + } + }, + { + "option": "above-5000000", + "label": { + "en": "Above 5.000.000 LBP", + "ar": "فوق 5.000.000 ليرة لبنانية " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "below-1000000": 4, + "1000000-3000000": 3, + "3000000-5000000": 2, + "above-5000000": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "income-source", + "label": { + "en": "Income source" + }, + "type": "dropdown", + "options": [ + { + "option": "salary-regular", + "label": { + "en": "Salary for regular work", + "ar": "الراتب عن العمل المنتظم " + } + }, + { + "option": "salary-occasional", + "label": { + "en": "Salary for occasional work", + "ar": "الراتب للعمل يومي أو متقطع" + } + }, + { + "option": "savings", + "label": { + "en": "Savings", + "ar": "مدخرات" + } + }, + { + "option": "debts", + "label": { + "en": "Debts", + "ar": "ديون" + } + }, + { + "option": "gifts", + "label": { + "en": "Gifts", + "ar": "هدايا " + } + }, + { + "option": "remittances", + "label": { + "en": "Remittances", + "ar": "التحويلات " + } + }, + { + "option": "pension", + "label": { + "en": "Pension", + "ar": "راتب تقاعد" + } + }, + { + "option": "humanitarian-assistance", + "label": { + "en": "Humanitarian assistance", + "ar": "مساعدات إنسانية" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "salary-regular": 1, + "salary-occasional": 4, + "savings": 2, + "debts": 5, + "gifts": 3, + "remittances": 1, + "pension": 2, + "humanitarian-assistance": 4 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "debt", + "label": { + "en": "Debt" + }, + "type": "dropdown", + "options": [ + { + "option": "no", + "label": { + "en": "I don’t have any debt", + "ar": "ليس لدينا أي ديون " + } + }, + { + "option": "below-1000000", + "label": { + "en": "Below 1.000.000 LBP", + "ar": "أقل من 1.000.000 ليرة لبنانية " + } + }, + { + "option": "1000000-3000000", + "label": { + "en": "Between 1.000.000 and 3.000.000 LBP", + "ar": "بين 1.000.000 و 3.000.000 ليرة لبنانية " + } + }, + { + "option": "3000000-5000000", + "label": { + "en": "Between 3.000.000 and 5.000.000 LBP", + "ar": "بين 3.000.000 و 5.000.000 ليرة لبنانية " + } + }, + { + "option": "above-5000000", + "label": { + "en": "Above 5.000.000 LBP", + "ar": "فوق 5.000.000 ليرة لبنانية " + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "below-1000000": 1, + "1000000-3000000": 2, + "3000000-5000000": 3, + "above-5000000": 4, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "other-help", + "label": { + "en": "Other help" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "ar": "نعم" + } + }, + { + "option": "no", + "label": { + "en": "No", + "ar": "لا" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "no": 4, + "yes": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "where-register", + "label": { + "en": "Where registered" + }, + "type": "dropdown", + "options": [ + { + "option": "at-centre", + "label": { + "en": "At the centre", + "ar": "في مركز" + } + }, + { + "option": "self", + "label": { + "en": "Somewhere else", + "ar": "مكان آخر" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "name-social-worker", + "label": { + "en": "Social worker" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "label": { + "en": "Phone Number" + }, + "placeholder": { + "en": "+961 000 000 000" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "nameFirst", + "label": { + "en": "First Name" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "nameLast", + "label": { + "en": "Last Name" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "options": null, + "showInPeopleAffectedTable": false + } + ], + "aboutProgram": { + "en": "This Dorcas-Tabitha cash aid program was created to support people living in Achrafieh and Batroun that experience difficulties in meeting their basic needs.

    After registering for this project and answering questions, we may call you to make an appoint for a home visit to check your situation. After the home visit, we will send you an SMS to let you know if you are selected or not.

    IF YOU ARE SELECTED the Dorcas-Tabitha cash aid program aims to help you:
    • Until the Dorcas-Tabitha cash aid program ends, May 31, 2021.

    IF YOU ARE SELECTED the Dorcas-Tabitha cash aid program will support you with the following:
    • A monthly cash grant of $50. You will receive an SMS from BoB finance. You can use this SMS and your ID card to pick up the cash grant at a BoB finance branch.

    If you have any questions, feedback or a complaint please contact us via telephone or WhatsApp on one of the following phone numbers:
    • Achrafieh: +961 81 232 710
    • Batroun: +961 70 169 265

    You can also check our website:
    • www.tabithalb.org", + "ar": "تم إنشاء برنامج دوركاس - طابيثا للمساعدات النقدية لدعم الأشخاص الذين يعيشون في الأشرفية والبترون الذين يواجهون صعوبات في تلبية احتياجاتهم الأساسية. بعد التسجيل في هذا المشروع والإجابة على الأسئلة ، قد نتصل بكم لتحديد موعد لزيارة منزلية للتحقق من وضعكم المعيشي. بعد الزيارة المنزلية ، سوف نرسل لكم رسالة نصية لإعلامكم إذا تم اختياركم أم لا ، حتى انتهاء برنامج المساعدة النقدية لدوركاس - طابيثا ، في 31 أيار 2021. إذا تم اختيارك ، فسوف يدعمكم برنامج المساعدة النقدية لدوركاس - طابيثا بما يلي: & bull؛ منحة نقدية شهرية بقيمة 50 دولارًا. سوف تتلقى رسالة نصية قصيرة من BoB Finance. يمكنك استخدام هذه الرسالة النصية القصيرة وبطاقة الهوية الخاصة بك للحصول على المنحة النقدية من أحد فروع BoB Finance. إذا كانت لديك أي أسئلة أو ملاحظات أو شكوى ، فيرجى الاتصال بنا عبر الهاتف أو WhatsApp على أحد أرقام الهواتف التالية: & bull؛ الأشرفية: 96181232710+ & bull؛ البترون: 96170169265+ يمكنك أيضًا مراجعة موقعنا على الإنترنت: & bull؛ www.tabithalb.org " + }, + "fullnameNamingConvention": ["nameFirst", "nameLast"], + "languages": ["en", "ar"], + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "BoB-finance" + } + ] +} diff --git a/services/121-service/src/seed-data/program/program-pilot-ukr.json b/services/121-service/src/seed-data/program/program-pilot-ukr.json new file mode 100644 index 0000000000..c3511804ea --- /dev/null +++ b/services/121-service/src/seed-data/program/program-pilot-ukr.json @@ -0,0 +1,417 @@ +{ + "published": true, + "validation": true, + "location": "Ukraine", + "ngo": "Dorcas Ukraine", + "titlePortal": { + "en": "Dorcas Ukraine cash aid program", + "uk": "Програма грошової допомоги Доркас Україна", + "ru": "Программа денежной помощи Доркас Украина" + }, + "description": { + "en": "", + "uk": "", + "ru": "" + }, + "startDate": "2022-04-15T12:00:00Z", + "endDate": "2022-06-30T12:00:00Z", + "currency": "UAH", + "distributionFrequency": "once", + "distributionDuration": 10, + "fixedTransferValue": 6600, + "paymentAmountMultiplierFormula": "0 + 1 * nrOfHouseHoldMembers", + "targetNrRegistrations": 340, + "programRegistrationAttributes": [ + { + "name": "enumeratorName", + "type": "text", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfHouseHoldMembers", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfMaleAdults", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOFemaleAdults", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfMaleElderly", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOFemaleElderly", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfBoys", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfGirls", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfIllnessInHousehold", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfDisabilitiesInHousehold", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfPregnantInHousehold", + "type": "numeric", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "householdSituation", + "type": "dropdown", + "options": [ + { + "option": "residingConflictedArea", + "label": { + "en": "We are currently residing in a conflict affected area", + "uk": "Зараз ми проживаємо в області, де ведуться бойові дії", + "ru": "В данный момент мы проживаем в области, где ведутся боевые действия" + } + }, + { + "option": "displacedAndSettled", + "label": { + "en": "We have been displaced by the current conflict and we settled somewhere else in the same country", + "uk": "Ми стали внутрішньо переміщеними особами в результаті конфлікту/війни у межах України", + "ru": "Из-за войны мы стали переселенцами в пределах Украины." + } + }, + { + "option": "displacedAndMoving", + "label": { + "en": "We have been displaced by the current conflict and we are on the move", + "uk": "Ми стали внутрішньо переміщеними особами в результаті війни і зараз у процесі переїзду", + "ru": "Мы стали переселенцами в результате войны и сейчас в процессе переезда." + } + }, + { + "option": "hostingDisplaced", + "label": { + "en": "We are hosting someone who was displaced by the current conflict", + "uk": "Ми місцева родина, у якої проживають внутрішньо переміщені особи", + "ru": "Мы - местная семья, у которой проживают переселенцы" + } + } + ], + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "placeholder": { + "en": "+380 000 000 000", + "uk": "+380 000 000 000", + "ru": "+380 000 000 000" + }, + "type": "tel", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "gender", + "type": "dropdown", + "options": [ + { + "option": "male", + "label": { + "en": "Male", + "uk": "Чол.", + "ru": "Мужчина" + } + }, + { + "option": "female", + "label": { + "en": "Female", + "uk": "Жін.", + "ru": "Женщина" + } + }, + { + "option": "otherGender", + "label": { + "en": "-", + "uk": "-", + "ru": "-" + } + } + ], + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "dateOfBirth", + "type": "date", + "options": null, + "export": ["all-people-affected"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "taxNumberAdult1", + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "duplicateCheck": true, + "showInPeopleAffectedTable": false + }, + { + "name": "taxNumberAdult2", + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "duplicateCheck": true, + "showInPeopleAffectedTable": false + }, + { + "name": "taxNumberAdult3", + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "duplicateCheck": true, + "showInPeopleAffectedTable": false + }, + { + "name": "taxNumberAdult4", + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "duplicateCheck": true, + "showInPeopleAffectedTable": false + }, + { + "name": "commentsFromEnumerator", + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "showInPeopleAffectedTable": false + }, + { + "name": "oblast", + "label": { + "en": "Oblast" + }, + "type": "dropdown", + "options": [ + { + "option": "zakarpatska", + "label": { + "en": "Zakarpatska", + "uk": "Закарпатська", + "ru": "Закарпатская" + } + } + ], + "export": ["all-people-affected", "included"], + "showInPeopleAffectedTable": false + }, + { + "name": "raion", + "label": { + "en": "Raion" + }, + "type": "dropdown", + "options": [ + { + "option": "berekivskyi", + "label": { + "en": "Berekivskyi", + "uk": "Берегівський", + "ru": "Береговской" + } + }, + { + "option": "mukachivskyi ", + "label": { + "en": "Mukachivskyi", + "uk": "Мукачівський", + "ru": "Мукачевский" + } + }, + { + "option": "uzhorodskyi", + "label": { + "en": "Uzhorodskyi", + "uk": "Ужгородський", + "ru": "Ужгородский" + } + } + ], + "export": ["all-people-affected", "included"], + "showInPeopleAffectedTable": false + }, + { + "name": "street", + "label": { + "en": "Street" + }, + "type": "text", + "export": ["all-people-affected", "included"], + "showInPeopleAffectedTable": false + }, + { + "name": "house", + "label": { + "en": "House" + }, + "type": "text", + "export": ["all-people-affected", "included"], + "showInPeopleAffectedTable": false + }, + { + "name": "apartmentOrOffice", + "label": { + "en": "Apt / Office" + }, + "type": "text", + "export": ["all-people-affected", "included"], + "showInPeopleAffectedTable": false + }, + { + "name": "postalIndex", + "label": { + "en": "Postal Index" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "city", + "label": { + "en": "City" + }, + "type": "text", + "export": ["all-people-affected", "included"], + "showInPeopleAffectedTable": false + }, + { + "name": "lastName", + "label": { + "en": "Last Name" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "firstName", + "label": { + "en": "First Name" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "fathersName", + "label": { + "en": "Father's Name" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "taxId", + "label": { + "en": "Tax ID" + }, + "export": ["all-people-affected", "included"], + "type": "text", + "options": null, + "duplicateCheck": true, + "showInPeopleAffectedTable": false + }, + { + "name": "telephone", + "label": { + "en": "Telephone" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false + } + ], + "aboutProgram": { + "en": "Hello, my name is ... and I am carrying out a registration activity for DORCAS to distribute cash to support people who had to leave their homes in East Ukraine due to the war. DORCAS is an international NGO from the Netherlands supporting people affected by conflict and will ask you questions about you, your family and personal information needed to distribute cash. This project aims to provide cash to allow you to cover your most urgent basic needs. Registration does not mean you are automatically selected for the project.", + "uk": "Доброго дня, мене звати .... я співробітник благодійного фонду Доркас і займаюся реєстрацією переселенців для отримання грошової допомоги. Доркас - голанський благодійний фонд, який займається наданням допомоги людям, постраждалим від війни. Мені потрібно задати вам питання про вас, вашу родину а також паспортні дані для можливості отримання грошової допомоги. Відповідь на питання анкети не означає автоматичного залучення до програми і отримання грошової допомоги", + "ru": "Добрый день, меня зовут …. я сотрудник благотворительного фонда Доркас и занимаюсь регистрацией переселенцев для получения денежной помощи. Доркас – голландский благотворительный фонд, который предоставляет помощь людям, пострадавшим от войны. Я задам вам вопросы про вас, вашу семью, а также паспортные данные для возможности получения денежной помощи. Ответы на вопросы не означают автоматическое включение в программу и получение денежной помощи. " + }, + "fullnameNamingConvention": ["lastName", "firstName", "fathersName"], + "languages": ["en", "uk", "ru"], + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "UkrPoshta" + } + ] +} diff --git a/services/121-service/src/seed-data/program/program-pilot-zoa-eth.json b/services/121-service/src/seed-data/program/program-pilot-zoa-eth.json new file mode 100644 index 0000000000..5409847632 --- /dev/null +++ b/services/121-service/src/seed-data/program/program-pilot-zoa-eth.json @@ -0,0 +1,730 @@ +{ + "published": true, + "validation": true, + "location": "Ethiopia", + "ngo": "ZOA", + "titlePortal": { + "en": "121 Digital Cash Aid Program", + "am_ET": "121 ዲጂታል የገንዘብ እርዳታ ፕሮግራም" + }, + "description": { + "en": "" + }, + "startDate": "2021-12-01T12:00:00Z", + "endDate": "2022-03-31T12:00:00Z", + "currency": "ETB", + "distributionFrequency": "month", + "distributionDuration": 3, + "fixedTransferValue": 6250, + "targetNrRegistrations": 55, + "programRegistrationAttributes": [ + { + "label": { + "en": "Business plan delivered" + }, + "name": "businessPlanDelivered", + "type": "boolean" + }, + { + "label": { + "en": "Completed training" + }, + "name": "completedTraining", + "type": "boolean" + }, + { + "label": { + "en": "Complete all milestones" + }, + "name": "milestones", + "type": "boolean" + }, + { + "name": "name", + "label": { + "en": "Full name" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "age", + "label": { + "en": "Age" + }, + "type": "dropdown", + "options": [ + { + "option": "0-17", + "label": { + "en": "0 - 17 years", + "am_ET": "0 - 17 ዓመት" + } + }, + { + "option": "18-30", + "label": { + "en": "18 - 30 years", + "am_ET": "18 - 30 ዓመት" + } + }, + { + "option": "31-", + "label": { + "en": "31 years or older", + "am_ET": "31 ዓመት ወይም ከዚያ በላይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "0-17": 0, + "18-30": 5, + "31-": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "education", + "label": { + "en": "Education level" + }, + "type": "dropdown", + "options": [ + { + "option": "primary", + "label": { + "en": "Primary school (grade 1-8)", + "am_ET": "የመጀመሪያ ደረጃ ትምህርት ያጠናቀቀ (1-8 ክፍል)" + } + }, + { + "option": "secondary", + "label": { + "en": "Secondary school (grade 9-12)", + "am_ET": "የሁለተኛ ደረጃ ትምህርት ያጠናቀቀ (9-12 ክፍል)" + } + }, + { + "option": "university", + "label": { + "en": "University", + "am_ET": "ዩኒቨርሲቲ" + } + }, + { + "option": "other", + "label": { + "en": "Other", + "am_ET": "ሌላ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nationalIdNumber", + "label": { + "en": "National ID" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfFamilyMembers", + "label": { + "en": "Family members" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfFemale", + "label": { + "en": "Nr. of females" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "nrOfMale", + "label": { + "en": "Nr. of males" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "hasHusband", + "label": { + "en": "Has husband" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "whenReturnee", + "label": { + "en": "Returnee" + }, + "type": "dropdown", + "options": [ + { + "option": "noReturnee", + "label": { + "en": "I did not return from a country in the Middle East", + "am_ET": "ከመካከለኛው ምስራቅ ሀገር አልተመለስኩም" + } + }, + { + "option": "less2years", + "label": { + "en": "Less than 2 years", + "am_ET": "ከ 2 ዓመት በታች" + } + }, + { + "option": "2-4years", + "label": { + "en": "2 - 4 years", + "am_ET": "2-4 ዓመት" + } + }, + { + "option": "more4years", + "label": { + "en": "More than 4 years", + "am_ET": "ከ 4 ዓመት በላይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "noReturnee": 0, + "less2years": 5, + "2-4years": 3, + "more4years": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "isHouseholdLeader", + "label": { + "en": "Household leader" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 3, + "no": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "business", + "label": { + "en": "Owns business" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 0 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "businessLicenseAddis", + "label": { + "en": "Business license" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 2 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "typeBusiness", + "label": { + "en": "Business type" + }, + "type": "dropdown", + "options": [ + { + "option": "restaurant", + "label": { + "en": "Small cafe or restaurant", + "am_ET": "አነስተኛ ካፌ ወይም ምግብ ቤት" + } + }, + { + "option": "coffee", + "label": { + "en": "Coffee house", + "am_ET": "ቡና ቤት" + } + }, + { + "option": "bakery", + "label": { + "en": "Bakery", + "am_ET": "ዳቦ ቤት" + } + }, + { + "option": "hair", + "label": { + "en": "Hair salon / beauty salon", + "am_ET": "የፀጉር ሳሎን / የውበት ሳሎን" + } + }, + { + "option": "clothes", + "label": { + "en": "Clothes shop / boutique", + "am_ET": "የልብስ ሱቅ / ቡቲክ" + } + }, + { + "option": "retail", + "label": { + "en": "Retail shop", + "am_ET": "የችርቻሮ ሱቅ" + } + }, + { + "option": "other", + "label": { + "en": "Other", + "am_ET": "ሌላ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "typeBusinessOther", + "label": { + "en": "Other business type" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "businessGoal", + "label": { + "en": "Business goal" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + }, + { + "option": "limited", + "label": { + "en": "I have a limited goal and interest to expand my small business", + "am_ET": "የእኔን አነስተኛ ንግድ ለማስፋፋት የተወሰነ ግብ እና ፍላጎት አለኝ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 0, + "limited": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "businessPlan", + "label": { + "en": "Business plan" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "ageBusiness", + "label": { + "en": "Age business" + }, + "type": "dropdown", + "options": [ + { + "option": "1year-", + "label": { + "en": "Less than 1 year", + "am_ET": "ከ1 አመት በታች" + } + }, + { + "option": "1-2years", + "label": { + "en": "1 - 2 years", + "am_ET": "1-2 ዓመት" + } + }, + { + "option": "3-4years", + "label": { + "en": "3 - 4 years", + "am_ET": "3-4 ዓመት" + } + }, + { + "option": "4years+", + "label": { + "en": "More than 4 years", + "am_ET": "ከ 4 ዓመት በላይ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "1year-": 0, + "1-2years": 1, + "3-4years": 2, + "4years+": 3 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "incomeBusiness", + "label": { + "en": "Income business" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "costBusiness", + "label": { + "en": "Cost business" + }, + "type": "numeric", + "options": null, + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "committedToTraining", + "label": { + "en": "Training" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "en": "Yes", + "am_ET": "አዎ" + } + }, + { + "option": "no", + "label": { + "en": "No", + "am_ET": "አይ" + } + }, + { + "option": "notSure", + "label": { + "en": "Not sure", + "am_ET": "እርግጠኛ አይደለሁም" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "yes": 5, + "no": 0, + "notSure": 2 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "vocationalCourses", + "label": { + "en": "Courses" + }, + "type": "dropdown", + "options": [ + { + "option": "specificAlign", + "label": { + "en": "I am trained in a specific vocational or specialised courses that aligns with my business areas", + "am_ET": "ከንግድ አካባቢዎቼ ጋር የሚጣጣሙ ልዩ የሙያ ወይም ልዩ ኮርሶች ሰልጥኛለሁ።" + } + }, + { + "option": "specificNoAlign", + "label": { + "en": "I am trained in a specific vocational or specialised courses but that doesn't align with my business areas", + "am_ET": "በልዩ ሙያ ወይም በልዩ ኮርሶች ሰልጥኛለሁ ግን ያ ከንግድ ስራዬ ጋር አይጣጣምም።" + } + }, + { + "option": "usefulSkills", + "label": { + "en": "I only have practical skills", + "am_ET": "የእጅ ሙያ ብቻ ነው ያለኝ" + } + }, + { + "option": "none", + "label": { + "en": "I do not have any", + "am_ET": "ምንም የለኝም" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": { + "specificAlign": 4, + "specificNoAlign": 3, + "usefulSkills": 2, + "none": 1 + }, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "savingMethod", + "label": { + "en": "Saving method" + }, + "type": "dropdown", + "options": [ + { + "option": "homeSaving", + "label": { + "en": "Home saving", + "am_ET": "የቤት ቁጠባ" + } + }, + { + "option": "bank", + "label": { + "en": "Bank", + "am_ET": "ባንክ" + } + }, + { + "option": "mobileMoney", + "label": { + "en": "Mobile money", + "am_ET": "የሞባይል ገንዘብ" + } + }, + { + "option": "ekrub", + "label": { + "en": "Ekrub", + "am_ET": "እቁብ" + } + }, + { + "option": "other", + "label": { + "en": "Other", + "am_ET": "ሌላ" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "editableInPortal": false, + "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "label": { + "en": "Phone Number" + }, + "placeholder": { + "en": "+251 000 000 000", + "am_ET": "+251 000 000 000" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "typePhoneNumber", + "label": { + "en": "Phone Type" + }, + "type": "dropdown", + "options": [ + { + "option": "ownSmartPhone", + "label": { + "en": "Own smartphone number", + "am_ET": "የግል ሞባይል መስመር" + } + }, + { + "option": "ownFeaturePhone", + "label": { + "en": "Own basic / feature phone number", + "am_ET": "የግል የቤት ሰልክ መስመር" + } + }, + { + "option": "notOwnPhone", + "label": { + "en": "Someone else's phone number", + "am_ET": "የጎረቤት ስልክ መስመር" + } + } + ], + "export": ["all-people-affected", "included"], + "scoring": {}, + "showInPeopleAffectedTable": false + } + ], + "aboutProgram": { + "en": "Returnees face economic and social difficulties upon return to Ethiopia. Therefore, this programme is designed to support a successful economic reintegration of returnees from the Middle East through a cash for livelihoods intervention. The project targets vulnerable women with existing small-scale business, who are victims of trafficking.", + "am_ET": "ከስደት ተመላሾች ወደ ኢትዮጵያ ሲመለሱ ኢኮኖሚያዊ እና ማህበራዊ ችግሮች ይገጥማቸዋል። ስለዚህ ይህ ፕሮግራም ከመካከለኛው ምስራቅ የተመለሱ ስደተኞችን መልሶ ለማቋቋም የገንዘብ ድጋፍ ለማድረግ የተነደፈ ነው።" + }, + "fullnameNamingConvention": ["name"], + "languages": ["en", "am_ET"], + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "BelCash" + } + ] +} diff --git a/services/121-service/src/seed-data/program/program-test-one-admin.json b/services/121-service/src/seed-data/program/program-test-one-admin.json index 122ba4596e..9d3f262c33 100644 --- a/services/121-service/src/seed-data/program/program-test-one-admin.json +++ b/services/121-service/src/seed-data/program/program-test-one-admin.json @@ -24,36 +24,8 @@ "distributionDuration": 8, "fixedTransferValue": 10, "paymentAmountMultiplierFormula": "1 + 1 * dragon", - "financialServiceProviders": [ - { - "fsp": "Intersolve-voucher-whatsapp", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - }, - { - "fsp": "Excel", - "configuration": [ - { - "name": "columnsToExport", - "value": ["name", "dob", "house", "phoneNumber"] - }, - { - "name": "columnToMatch", - "value": "phoneNumber" - } - ] - } - ], "targetNrRegistrations": 250, - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "name", "label": { @@ -63,10 +35,8 @@ "fr": "Nom", "nl": "Naam" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "showInPeopleAffectedTable": false @@ -80,10 +50,8 @@ "fr": "Date de naissance", "nl": "Geboortedatum" }, - "answerType": "date", - "questionType": "standard", + "type": "date", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -98,8 +66,7 @@ "fr": "Maison", "nl": "Huis" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", "options": [ { "option": "lannister", @@ -136,8 +103,7 @@ "fr": "Nombre de dragons", "nl": "Aantal draken" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "editableInPortal": true, "scoring": { @@ -146,34 +112,6 @@ "showInPeopleAffectedTable": false } ], - "programCustomAttributes": [ - { - "label": { - "ar": "لا يعرف شيئًا", - "en": "Knows nothing", - "es": "No sabe nada", - "fr": "Ne sait rien", - "nl": "Weet niets" - }, - "name": "knowsNothing", - "type": "boolean", - "editableInPortal": true, - "showInPeopleAffectedTable": true - }, - { - "label": { - "ar": "الشعار", - "en": "Motto", - "es": "Lema", - "fr": "Devise", - "nl": "Motto" - }, - "name": "motto", - "type": "text", - "editableInPortal": true, - "showInPeopleAffectedTable": true - } - ], "aboutProgram": { "ar": "[حول برنامج المساعدات]", "en": "[about aid program]", @@ -186,5 +124,33 @@ "tryWhatsAppFirst": true, "enableMaxPayments": false, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Intersolve-voucher-whatsapp", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + }, + { + "financialServiceProvider": "Excel", + "properties": [ + { + "name": "columnsToExport", + "value": ["name", "dob", "house", "phoneNumber"] + }, + { + "name": "columnToMatch", + "value": "phoneNumber" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-test.json b/services/121-service/src/seed-data/program/program-test.json index 744e1a1a34..18095077ee 100644 --- a/services/121-service/src/seed-data/program/program-test.json +++ b/services/121-service/src/seed-data/program/program-test.json @@ -24,36 +24,8 @@ "distributionDuration": 8, "fixedTransferValue": 10, "paymentAmountMultiplierFormula": "1 + 1 * dragon", - "financialServiceProviders": [ - { - "fsp": "Intersolve-voucher-whatsapp", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - }, - { - "fsp": "Excel", - "configuration": [ - { - "name": "columnsToExport", - "value": ["name", "dob", "house", "phoneNumber"] - }, - { - "name": "columnToMatch", - "value": "phoneNumber" - } - ] - } - ], "targetNrRegistrations": 250, - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "name", "label": { @@ -63,10 +35,8 @@ "fr": "Nom", "nl": "Naam" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "showInPeopleAffectedTable": false @@ -80,10 +50,8 @@ "fr": "Date de naissance", "nl": "Geboortedatum" }, - "answerType": "date", - "questionType": "standard", + "type": "date", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": true, @@ -98,8 +66,8 @@ "fr": "Maison", "nl": "Huis" }, - "answerType": "dropdown", - "questionType": "standard", + "type": "dropdown", + "isRequired": true, "options": [ { "option": "lannister", @@ -136,17 +104,14 @@ "fr": "Nombre de dragons", "nl": "Aantal draken" }, - "answerType": "numeric", - "questionType": "standard", + "type": "numeric", "options": null, "editableInPortal": true, "scoring": { "multiplier": 2 }, "showInPeopleAffectedTable": false - } - ], - "programCustomAttributes": [ + }, { "label": { "ar": "لا يعرف شيئًا", @@ -172,6 +137,136 @@ "type": "text", "editableInPortal": true, "showInPeopleAffectedTable": true + }, + { + "name": "whatsappPhoneNumber", + "label": { + "en": "WhatsApp Nr." + }, + "placeholder": { + "ar": "00 00 00 00 0 00+", + "en": "+00 0 00 00 00 00" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "duplicateCheck": true, + "showInPeopleAffectedTable": true + }, + { + "name": "personalId", + "label": { + "ar": "الهوية الشخصية", + "en": "Personal ID", + "es": "ID Personal", + "fr": "ID Personnel", + "nl": "Nationaal ID" + }, + "type": "text", + "pattern": ".+", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "phoneNumber", + "label": { + "ar": "الهاتف.", + "en": "Phone Nr.", + "es": "Nr. teléfono", + "fr": "Nr. de téléphone", + "nl": "Telefoonnr." + }, + "placeholder": { + "en": "+000 000 00 00" + }, + "type": "tel", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "accountId", + "label": { + "ar": "حسابك", + "en": "Account Nr.", + "es": "Num. de cuenta", + "fr": "Num. de compte", + "nl": "Rekeningnr." + }, + "type": "numeric", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "date", + "label": { + "ar": "تاريخ", + "en": "Date", + "es": "Fecha", + "fr": "Date", + "nl": "Datum" + }, + "type": "date", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "openAnswer", + "label": { + "ar": "إجابة مفتوحة", + "en": "Open Answer", + "es": "Abrir respuesta", + "fr": "Réponse ouverte", + "nl": "Open antwoord" + }, + "type": "text", + "pattern": ".+", + "options": null, + "showInPeopleAffectedTable": false + }, + { + "name": "fixedChoice", + "label": { + "ar": "خيار", + "en": "Choice", + "es": "Elección", + "fr": "Choix", + "nl": "Keuze" + }, + "type": "dropdown", + "options": [ + { + "option": "yes", + "label": { + "ar": "نعم", + "en": "Yes", + "es": "Sí", + "fr": "Oui", + "nl": "Ja" + } + }, + { + "option": "no", + "label": { + "ar": "لا", + "en": "No", + "es": "No", + "fr": "Non", + "nl": "Nee" + } + } + ], + "showInPeopleAffectedTable": false + }, + { + "name": "healthArea", + "label": { + "en": "Health area", + "fr": "Aire de santé" + }, + "type": "text", + "options": null, + "export": ["all-people-affected", "included"], + "showInPeopleAffectedTable": false } ], "aboutProgram": { @@ -186,5 +281,58 @@ "tryWhatsAppFirst": true, "enableMaxPayments": false, "enableScope": false, - "allowEmptyPhoneNumber": false + "allowEmptyPhoneNumber": false, + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Intersolve-voucher-whatsapp", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + }, + { + "financialServiceProvider": "Excel", + "name": "ironBank", + "label": { + "en": "Iron Bank", + "fr": "Banque de fer", + "nl": "IJzeren Bank" + }, + "properties": [ + { + "name": "columnsToExport", + "value": ["name", "dob", "dragon", "phoneNumber"] + }, + { + "name": "columnToMatch", + "value": "phoneNumber" + } + ] + }, + { + "financialServiceProvider": "Excel", + "name": "gringotts", + "label": { + "en": "Gringotts", + "fr": "Gringotts", + "nl": "Goudgrijp" + }, + "properties": [ + { + "name": "columnsToExport", + "value": ["name", "dob", "dragon", "phoneNumber"] + }, + { + "name": "columnToMatch", + "value": "phoneNumber" + } + ] + } + ] } diff --git a/services/121-service/src/seed-data/program/program-validation.json b/services/121-service/src/seed-data/program/program-validation.json index f9b1546465..d8eb001920 100644 --- a/services/121-service/src/seed-data/program/program-validation.json +++ b/services/121-service/src/seed-data/program/program-validation.json @@ -23,36 +23,8 @@ "distributionFrequency": "month", "distributionDuration": 3, "fixedTransferValue": 10, - "financialServiceProviders": [ - { - "fsp": "Intersolve-voucher-whatsapp", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - }, - { - "fsp": "Intersolve-voucher-paper", - "configuration": [ - { - "name": "username", - "value": "INTERSOLVE_USERNAME" - }, - { - "name": "password", - "value": "INTERSOLVE_PASSWORD" - } - ] - } - ], "targetNrRegistrations": 250, - "programQuestions": [ + "programRegistrationAttributes": [ { "name": "nameFirst", "label": { @@ -62,10 +34,8 @@ "fr": "Prénom", "nl": "Voornaam" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": false, @@ -80,10 +50,8 @@ "fr": "Nom de famille", "nl": "Achternaam" }, - "answerType": "text", - "questionType": "standard", + "type": "text", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "editableInPortal": false, @@ -101,13 +69,26 @@ "placeholder": { "en": "+31 6 00 00 00 00" }, - "answerType": "tel", - "questionType": "standard", + "type": "tel", "options": null, - "persistence": true, "export": ["all-people-affected", "included"], "scoring": {}, "showInPeopleAffectedTable": false + }, + { + "name": "whatsappPhoneNumber", + "label": { + "en": "WhatsApp Nr." + }, + "placeholder": { + "ar": "00 00 00 00 0 00+", + "en": "+00 0 00 00 00 00" + }, + "export": ["all-people-affected", "included"], + "type": "tel", + "options": null, + "duplicateCheck": true, + "showInPeopleAffectedTable": true } ], "aboutProgram": { @@ -118,5 +99,33 @@ "nl": "[beschrijving van het programma]" }, "fullnameNamingConvention": ["nameFirst", "nameLast"], - "languages": ["ar", "en", "es", "fr", "nl"] + "languages": ["ar", "en", "es", "fr", "nl"], + "programFinancialServiceProviderConfigurations": [ + { + "financialServiceProvider": "Intersolve-voucher-whatsapp", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + }, + { + "financialServiceProvider": "Intersolve-voucher-paper", + "properties": [ + { + "name": "username", + "value": "INTERSOLVE_USERNAME" + }, + { + "name": "password", + "value": "INTERSOLVE_PASSWORD" + } + ] + } + ] } diff --git a/services/121-service/src/shared/interceptors/program-existence.interceptor.ts b/services/121-service/src/shared/interceptors/program-existence.interceptor.ts new file mode 100644 index 0000000000..e74ae1576e --- /dev/null +++ b/services/121-service/src/shared/interceptors/program-existence.interceptor.ts @@ -0,0 +1,66 @@ +import { + CallHandler, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Observable } from 'rxjs'; +import { Equal } from 'typeorm'; + +import { AuthenticatedUserParameters } from '@121-service/src/guards/authenticated-user.decorator'; +import { ProgramRepository } from '@121-service/src/programs/repositories/program.repository'; + +// +@Injectable() +export class ProgramExistenceInterceptor implements NestInterceptor { + constructor( + private readonly programRepository: ProgramRepository, + private readonly reflector: Reflector, + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const request = context.switchToHttp().getRequest(); + if (!request.params.programId) { + return next.handle(); + } + const programId = request.params.programId; + const handler = context.getHandler(); + const classRef = context.getClass(); + const authParams = + this.reflector.get( + 'authenticationParameters', + handler, + ) || + this.reflector.get( + 'authenticationParameters', + classRef, + ); + + // This check makes used of the AuthenticatedUserParameters decorator to determine if an endpoint regquires a user to be an admin or organization admin + // If the user is an admin or organization admin check if the programId exists + // This is not needed for regular users as they can only access their own programs + if (authParams?.isAdmin || authParams?.isOrganizationAdmin) { + await this.validateProgramExists(programId); + } + + return next.handle(); + } + + public async validateProgramExists(programId: number): Promise { + const programCount = await this.programRepository.count({ + where: { id: Equal(programId) }, + }); + if (programCount === 0) { + throw new HttpException( + `Program with id ${programId} not found`, + HttpStatus.NOT_FOUND, + ); + } + } +} diff --git a/services/121-service/src/transaction-job-processors/transaction-job-processors.service.spec.ts b/services/121-service/src/transaction-job-processors/transaction-job-processors.service.spec.ts index 2c7cba95aa..7f9d034f22 100644 --- a/services/121-service/src/transaction-job-processors/transaction-job-processors.service.spec.ts +++ b/services/121-service/src/transaction-job-processors/transaction-job-processors.service.spec.ts @@ -1,9 +1,6 @@ import { TestBed } from '@automock/jest'; import { UpdateResult } from 'typeorm'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { FinancialServiceProviderRepository } from '@121-service/src/financial-service-providers/repositories/financial-service-provider.repository'; import { SafaricomApiError } from '@121-service/src/payments/fsp-integration/safaricom/errors/safaricom-api.error'; import { SafaricomService } from '@121-service/src/payments/fsp-integration/safaricom/safaricom.service'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; @@ -26,11 +23,6 @@ const mockedRegistration: RegistrationEntity = { preferredLanguage: 'en', } as unknown as RegistrationEntity; -const mockedFinancialServiceProvider: FinancialServiceProviderEntity = { - id: 1, - name: FinancialServiceProviders.safaricom, -} as unknown as FinancialServiceProviderEntity; - const mockedTransaction: TransactionEntity = { id: 1, amount: 25, @@ -48,6 +40,7 @@ const mockedTransactionJob: SafaricomTransactionJobDto = { bulkSize: 10, phoneNumber: '254708374149', idNumber: 'nat-123', + programFinancialServiceProviderConfigurationId: 1, originatorConversationId: 'originator-conversation-id', }; @@ -68,7 +61,6 @@ describe('TransactionJobProcessorsService', () => { let registrationScopedRepository: RegistrationScopedRepository; let latestTransactionRepository: LatestTransactionRepository; let transactionScopedRepository: ScopedRepository; - let financialServiceProviderRepository: FinancialServiceProviderRepository; beforeEach(async () => { const { unit, unitRef } = TestBed.create( @@ -84,11 +76,6 @@ describe('TransactionJobProcessorsService', () => { RegistrationScopedRepository, ); - financialServiceProviderRepository = - unitRef.get( - FinancialServiceProviderRepository, - ); - transactionScopedRepository = unitRef.get< ScopedRepository >(getScopedRepositoryProviderName(TransactionEntity)); @@ -111,10 +98,6 @@ describe('TransactionJobProcessorsService', () => { .spyOn(registrationScopedRepository, 'updateUnscoped') .mockResolvedValueOnce({} as UpdateResult); - jest - .spyOn(financialServiceProviderRepository, 'getByName') - .mockResolvedValueOnce(mockedFinancialServiceProvider); - jest .spyOn(programRepository, 'findByIdOrFail') .mockResolvedValueOnce(mockedProgram as ProgramEntity); @@ -147,9 +130,6 @@ describe('TransactionJobProcessorsService', () => { expect(registrationScopedRepository.getByReferenceId).toHaveBeenCalledWith({ referenceId: mockedTransactionJob.referenceId, }); - expect(financialServiceProviderRepository.getByName).toHaveBeenCalledWith( - FinancialServiceProviders.safaricom, - ); expect(safaricomService.doTransfer).toHaveBeenCalledWith( expect.objectContaining({ transferAmount: mockedTransactionJob.transactionAmount, diff --git a/services/121-service/src/transaction-job-processors/transaction-job-processors.service.ts b/services/121-service/src/transaction-job-processors/transaction-job-processors.service.ts index 16769596a6..91ce36e8d5 100644 --- a/services/121-service/src/transaction-job-processors/transaction-job-processors.service.ts +++ b/services/121-service/src/transaction-job-processors/transaction-job-processors.service.ts @@ -2,12 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Equal } from 'typeorm'; import { EventsService } from '@121-service/src/events/events.service'; -import { - FinancialServiceProviderConfigurationEnum, - FinancialServiceProviders, -} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { FinancialServiceProviderEntity } from '@121-service/src/financial-service-providers/financial-service-provider.entity'; -import { FinancialServiceProviderRepository } from '@121-service/src/financial-service-providers/repositories/financial-service-provider.repository'; +import { FinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { MessageContentType } from '@121-service/src/notifications/enum/message-type.enum'; import { ProgramNotificationEnum } from '@121-service/src/notifications/enum/program-notification.enum'; import { MessageProcessTypeExtension } from '@121-service/src/notifications/message-job.dto'; @@ -41,7 +36,7 @@ interface ProcessTransactionResultInput { paymentNumber: number; userId: number; calculatedTransferAmountInMajorUnit: number; - financialServiceProviderId: number; + programFinancialServiceProviderConfigurationId: number; registration: RegistrationEntity; oldRegistration: RegistrationEntity; isRetry: boolean; @@ -61,7 +56,6 @@ export class TransactionJobProcessorsService { private readonly queueMessageService: MessageQueuesService, @Inject(getScopedRepositoryProviderName(TransactionEntity)) private readonly transactionScopedRepository: ScopedRepository, - private readonly financialServiceProviderRepository: FinancialServiceProviderRepository, private readonly latestTransactionRepository: LatestTransactionRepository, private readonly programRepository: ProgramRepository, private readonly eventsService: EventsService, @@ -72,10 +66,6 @@ export class TransactionJobProcessorsService { ): Promise { const registration = await this.getRegistrationOrThrow(input.referenceId); const oldRegistration = structuredClone(registration); - const financialServiceProvider = - await this.getFinancialServiceProviderOrThrow( - FinancialServiceProviders.intersolveVisa, - ); let transferAmountInMajorUnit: number; try { @@ -94,7 +84,8 @@ export class TransactionJobProcessorsService { userId: input.userId, calculatedTransferAmountInMajorUnit: input.transactionAmountInMajorUnit, // Use the original amount here since we were unable to calculate the transfer amount. The error message is also clear enough so users should not be confused about the potentially high amount. - financialServiceProviderId: financialServiceProvider.id, + programFinancialServiceProviderConfigurationId: + input.programFinancialServiceProviderConfigurationId, registration, oldRegistration, isRetry: input.isRetry, @@ -107,41 +98,17 @@ export class TransactionJobProcessorsService { throw error; } - // Check if all required properties are present. If not, create a failed transaction and throw an error. - for (const [name, value] of Object.entries(input)) { - if (name === 'addressHouseNumberAddition') continue; // Skip non-required property - - // Define "empty" based on your needs. Here, we check for null, undefined, or an empty string. - if (value === null || value === undefined || value === '') { - const errorText = `Property ${name} is undefined`; - await this.createTransactionAndUpdateRegistration({ - programId: input.programId, - paymentNumber: input.paymentNumber, - userId: input.userId, - calculatedTransferAmountInMajorUnit: transferAmountInMajorUnit, - financialServiceProviderId: financialServiceProvider.id, - registration, - oldRegistration, - isRetry: input.isRetry, - status: TransactionStatusEnum.error, - errorText, - }); - return; - } - } - let intersolveVisaDoTransferOrIssueCardReturnDto: DoTransferOrIssueCardReturnType; try { const intersolveVisaConfig = - await this.programFinancialServiceProviderConfigurationRepository.getValuesByNamesOrThrow( + await this.programFinancialServiceProviderConfigurationRepository.getPropertiesByNamesOrThrow( { - programId: input.programId, - financialServiceProviderName: - FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationId: + input.programFinancialServiceProviderConfigurationId, names: [ - FinancialServiceProviderConfigurationEnum.brandCode, - FinancialServiceProviderConfigurationEnum.coverLetterCode, - FinancialServiceProviderConfigurationEnum.fundingTokenCode, + FinancialServiceProviderConfigurationProperties.brandCode, + FinancialServiceProviderConfigurationProperties.coverLetterCode, + FinancialServiceProviderConfigurationProperties.fundingTokenCode, ], }, ); @@ -162,17 +129,18 @@ export class TransactionJobProcessorsService { transferAmountInMajorUnit, brandCode: intersolveVisaConfig.find( (c) => - c.name === FinancialServiceProviderConfigurationEnum.brandCode, + c.name === + FinancialServiceProviderConfigurationProperties.brandCode, )?.value as string, // This must be a string. If it is not, the intersolve API will return an error (maybe). coverLetterCode: intersolveVisaConfig.find( (c) => c.name === - FinancialServiceProviderConfigurationEnum.coverLetterCode, + FinancialServiceProviderConfigurationProperties.coverLetterCode, )?.value as string, // This must be a string. If it is not, the intersolve API will return an error (maybe). fundingTokenCode: intersolveVisaConfig.find( (c) => c.name === - FinancialServiceProviderConfigurationEnum.fundingTokenCode, + FinancialServiceProviderConfigurationProperties.fundingTokenCode, )?.value as string, // This must be a string. If it is not, the intersolve API will return an error (maybe). }); } catch (error) { @@ -182,7 +150,8 @@ export class TransactionJobProcessorsService { paymentNumber: input.paymentNumber, userId: input.userId, calculatedTransferAmountInMajorUnit: transferAmountInMajorUnit, - financialServiceProviderId: financialServiceProvider.id, + programFinancialServiceProviderConfigurationId: + input.programFinancialServiceProviderConfigurationId, registration, oldRegistration, isRetry: input.isRetry, @@ -218,7 +187,8 @@ export class TransactionJobProcessorsService { userId: input.userId, calculatedTransferAmountInMajorUnit: intersolveVisaDoTransferOrIssueCardReturnDto.amountTransferredInMajorUnit, - financialServiceProviderId: financialServiceProvider.id, + programFinancialServiceProviderConfigurationId: + input.programFinancialServiceProviderConfigurationId, registration, oldRegistration, isRetry: input.isRetry, @@ -234,33 +204,8 @@ export class TransactionJobProcessorsService { transactionJob.referenceId, ); const oldRegistration = structuredClone(registration); - const financialServiceProvider = - await this.getFinancialServiceProviderOrThrow( - FinancialServiceProviders.safaricom, - ); - // 2. Check if all required properties are present. If not, create a failed transaction and throw an error. - for (const [name, value] of Object.entries(transactionJob)) { - // Define "empty" based on your needs. Here, we check for null, undefined, or an empty string. - if (value === null || value === undefined || value === '') { - const errorText = `Property ${name} is undefined`; - await this.createTransactionAndUpdateRegistration({ - programId: transactionJob.programId, - paymentNumber: transactionJob.paymentNumber, - userId: transactionJob.userId, - calculatedTransferAmountInMajorUnit: transactionJob.transactionAmount, - financialServiceProviderId: financialServiceProvider.id, - registration, - oldRegistration, - isRetry: transactionJob.isRetry, - status: TransactionStatusEnum.error, - errorText, - }); - return; - } - } - - // 3. Check for existing Safaricom Transfer with the same originatorConversationId, because that means this job has already been (partly) processed. In case of a server crash, jobs that were in process are processed again. + // 2. Check for existing Safaricom Transfer with the same originatorConversationId, because that means this job has already been (partly) processed. In case of a server crash, jobs that were in process are processed again. let safaricomTransfer = await this.safaricomTransferScopedRepository.findOne({ where: { @@ -277,7 +222,8 @@ export class TransactionJobProcessorsService { paymentNumber: transactionJob.paymentNumber, userId: transactionJob.userId, calculatedTransferAmountInMajorUnit: transactionJob.transactionAmount, - financialServiceProviderId: financialServiceProvider.id, + programFinancialServiceProviderConfigurationId: + transactionJob.programFinancialServiceProviderConfigurationId, registration, oldRegistration, isRetry: transactionJob.isRetry, @@ -296,7 +242,7 @@ export class TransactionJobProcessorsService { transactionId = safaricomTransfer.transactionId; } - // 4. Start the transfer, if failure update to error transaction and return early + // 3. Start the transfer, if failure update to error transaction and return early try { await this.safaricomService.doTransfer({ transferAmount: transactionJob.transactionAmount, @@ -320,9 +266,9 @@ export class TransactionJobProcessorsService { } } - // 5. No messages sent for safaricom + // 4. No messages sent for safaricom - // 6. No transaction stored or updated after API-call, because waiting transaction is already stored earlier and will remain 'waiting' at this stage (to be updated via callback) + // 5. No transaction stored or updated after API-call, because waiting transaction is already stored earlier and will remain 'waiting' at this stage (to be updated via callback) } private async getRegistrationOrThrow( @@ -340,23 +286,12 @@ export class TransactionJobProcessorsService { return registration; } - private async getFinancialServiceProviderOrThrow( - fspName: FinancialServiceProviders, - ): Promise { - const financialServiceProvider = - await this.financialServiceProviderRepository.getByName(fspName); - if (!financialServiceProvider) { - throw new Error('Financial Service Provider not found'); - } - return financialServiceProvider; - } - private async createTransactionAndUpdateRegistration({ programId, paymentNumber, userId, calculatedTransferAmountInMajorUnit, - financialServiceProviderId, + programFinancialServiceProviderConfigurationId, registration, oldRegistration, isRetry, @@ -366,7 +301,7 @@ export class TransactionJobProcessorsService { const resultTransaction = await this.createTransaction({ amount: calculatedTransferAmountInMajorUnit, registration, - financialServiceProviderId, + programFinancialServiceProviderConfigurationId, programId, paymentNumber, userId, @@ -431,7 +366,7 @@ export class TransactionJobProcessorsService { private async createTransaction({ amount, // transaction entity are always in major unit registration, - financialServiceProviderId, + programFinancialServiceProviderConfigurationId, programId, paymentNumber, userId, @@ -440,7 +375,7 @@ export class TransactionJobProcessorsService { }: { amount: number; registration: RegistrationEntity; - financialServiceProviderId: number; + programFinancialServiceProviderConfigurationId: number; programId: number; paymentNumber: number; userId: number; @@ -451,7 +386,8 @@ export class TransactionJobProcessorsService { transaction.amount = amount; transaction.created = new Date(); transaction.registration = registration; - transaction.financialServiceProviderId = financialServiceProviderId; + transaction.programFinancialServiceProviderConfigurationId = + programFinancialServiceProviderConfigurationId; transaction.programId = programId; transaction.payment = paymentNumber; transaction.userId = userId; diff --git a/services/121-service/src/transaction-queues/dto/intersolve-visa-transaction-job.dto.ts b/services/121-service/src/transaction-queues/dto/intersolve-visa-transaction-job.dto.ts index 4ed9357b6b..5562e7527a 100644 --- a/services/121-service/src/transaction-queues/dto/intersolve-visa-transaction-job.dto.ts +++ b/services/121-service/src/transaction-queues/dto/intersolve-visa-transaction-job.dto.ts @@ -3,14 +3,15 @@ export interface IntersolveVisaTransactionJobDto { readonly paymentNumber: number; readonly referenceId: string; readonly transactionAmountInMajorUnit: number; // This is in the major unit of the currency, for example whole euros + readonly programFinancialServiceProviderConfigurationId: number; readonly isRetry: boolean; readonly userId: number; readonly bulkSize: number; - readonly name?: string; - readonly addressStreet?: string; - readonly addressHouseNumber?: string; - readonly addressHouseNumberAddition?: string; + readonly name: string; + readonly addressStreet: string; + readonly addressHouseNumber: string; + readonly addressHouseNumberAddition: string; readonly addressPostalCode?: string; - readonly addressCity?: string; - readonly phoneNumber?: string; + readonly addressCity: string; + readonly phoneNumber: string; } diff --git a/services/121-service/src/transaction-queues/dto/safaricom-transaction-job.dto.ts b/services/121-service/src/transaction-queues/dto/safaricom-transaction-job.dto.ts index 9b2150f224..de1d2f526e 100644 --- a/services/121-service/src/transaction-queues/dto/safaricom-transaction-job.dto.ts +++ b/services/121-service/src/transaction-queues/dto/safaricom-transaction-job.dto.ts @@ -1,5 +1,6 @@ export interface SafaricomTransactionJobDto { readonly programId: number; + readonly programFinancialServiceProviderConfigurationId: number; readonly paymentNumber: number; readonly referenceId: string; readonly transactionAmount: number; @@ -7,6 +8,6 @@ export interface SafaricomTransactionJobDto { readonly userId: number; readonly bulkSize: number; readonly originatorConversationId: string; - readonly phoneNumber?: string; - readonly idNumber?: string; + readonly phoneNumber: string; + readonly idNumber: string; } diff --git a/services/121-service/src/transaction-queues/transaction-queues.service.spec.ts b/services/121-service/src/transaction-queues/transaction-queues.service.spec.ts index 71df253bd7..b02e14fb1c 100644 --- a/services/121-service/src/transaction-queues/transaction-queues.service.spec.ts +++ b/services/121-service/src/transaction-queues/transaction-queues.service.spec.ts @@ -22,6 +22,7 @@ const mockIntersolveVisaTransactionJobDto: IntersolveVisaTransactionJobDto[] = [ addressPostalCode: '1234AB', addressCity: 'Den Haag', phoneNumber: '14155238886', + programFinancialServiceProviderConfigurationId: 1, }, ]; @@ -37,6 +38,7 @@ const mockSafaricomTransactionJobDto: SafaricomTransactionJobDto[] = [ phoneNumber: '254708374149', idNumber: 'nat-123', originatorConversationId: 'originator-id', + programFinancialServiceProviderConfigurationId: 1, }, ]; diff --git a/services/121-service/src/user/enum/permission.enum.ts b/services/121-service/src/user/enum/permission.enum.ts index f3047b93e8..2ad50db52b 100644 --- a/services/121-service/src/user/enum/permission.enum.ts +++ b/services/121-service/src/user/enum/permission.enum.ts @@ -21,9 +21,6 @@ export enum PermissionEnum { // Program(s) ProgramUPDATE = 'program.update', - ProgramQuestionUPDATE = 'program:question.update', - ProgramQuestionDELETE = 'program:question.delete', - ProgramCustomAttributeUPDATE = 'program:custom-attribute.update', ProgramMetricsREAD = 'program:metrics.read', // Payment(s) @@ -51,7 +48,7 @@ export enum PermissionEnum { RegistrationAttributeUPDATE = 'registration:attribute.update', RegistrationAttributeFinancialUPDATE = 'registration:attribute:financial.update', - RegistrationFspUPDATE = 'registration:fsp.update', + RegistrationFspConfigUPDATE = 'registration:fsp-config.update', RegistrationNotificationREAD = 'registration:notification.read', RegistrationNotificationCREATE = 'registration:notification.create', diff --git a/services/121-service/src/utils/file-import/file-import.service.ts b/services/121-service/src/utils/file-import/file-import.service.ts index a21ebe7061..37d934bf81 100644 --- a/services/121-service/src/utils/file-import/file-import.service.ts +++ b/services/121-service/src/utils/file-import/file-import.service.ts @@ -4,7 +4,10 @@ import { Readable } from 'typeorm/platform/PlatformTools'; @Injectable() export class FileImportService { - public async validateCsv(csvFile, maxRecords?: number): Promise { + public async validateCsv( + csvFile: Express.Multer.File, + maxRecords?: number, + ): Promise { const indexLastPoint = csvFile.originalname.lastIndexOf('.'); const extension = csvFile.originalname.substr( indexLastPoint, diff --git a/services/121-service/src/utils/registration-data-query/registration-data-query.service.ts b/services/121-service/src/utils/registration-data-query/registration-data-query.service.ts index 1f98fc326d..c49d2a6a60 100644 --- a/services/121-service/src/utils/registration-data-query/registration-data-query.service.ts +++ b/services/121-service/src/utils/registration-data-query/registration-data-query.service.ts @@ -6,8 +6,8 @@ import { RegistrationDataOptions, RegistrationDataRelation, } from '@121-service/src/registration/dto/registration-data-relation.model'; -import { GenericAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { GenericRegistrationAttributes } from '@121-service/src/registration/enum/registration-attribute.enum'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { RegistrationScopedRepository } from '@121-service/src/registration/repositories/registration-scoped.repository'; @Injectable() @@ -24,9 +24,9 @@ export class RegistrationDataScopedQueryService { .createQueryBuilder('registration') .select([ `registration.referenceId as "referenceId"`, - `registration."${GenericAttributes.phoneNumber}"`, - `registration."${GenericAttributes.preferredLanguage}"`, - `coalesce(registration."${GenericAttributes.paymentAmountMultiplier}",1) as "paymentAmountMultiplier"`, + `registration."${GenericRegistrationAttributes.phoneNumber}"`, + `registration."${GenericRegistrationAttributes.preferredLanguage}"`, + `coalesce(registration."${GenericRegistrationAttributes.paymentAmountMultiplier}",1) as "paymentAmountMultiplier"`, ]) .andWhere(`registration.referenceId IN (:...referenceIds)`, { referenceIds, @@ -47,20 +47,13 @@ export class RegistrationDataScopedQueryService { const uniqueSubQueryId = uuid().replace(/-/g, '').toLowerCase(); subQuery = subQuery .andWhere(`"${uniqueSubQueryId}"."registrationId" = registration.id`) - .from(RegistrationDataEntity, uniqueSubQueryId); - if (relation?.programQuestionId) { + .from(RegistrationAttributeDataEntity, uniqueSubQueryId); + if (relation?.programRegistrationAttributeId) { subQuery = subQuery.andWhere( - `"${uniqueSubQueryId}"."programQuestionId" = ${relation.programQuestionId}`, - ); - } else if (relation?.programCustomAttributeId) { - subQuery = subQuery.andWhere( - `"${uniqueSubQueryId}"."programCustomAttributeId" = ${relation.programCustomAttributeId}`, - ); - } else if (relation?.fspQuestionId) { - subQuery = subQuery.andWhere( - `"${uniqueSubQueryId}"."fspQuestionId" = ${relation.fspQuestionId}`, + `"${uniqueSubQueryId}"."programRegistrationAttributeId" = ${relation.programRegistrationAttributeId}`, ); } + // Because of string_agg no distinction between multi-select and other is needed subQuery.addSelect( `string_agg("${uniqueSubQueryId}".value,'|' order by value)`, diff --git a/services/121-service/src/utils/scope/createFindWhereOptions.helper.spec.ts b/services/121-service/src/utils/scope/createFindWhereOptions.helper.spec.ts index 5b9b997be8..8e187ec18b 100644 --- a/services/121-service/src/utils/scope/createFindWhereOptions.helper.spec.ts +++ b/services/121-service/src/utils/scope/createFindWhereOptions.helper.spec.ts @@ -1,6 +1,6 @@ import { Equal, FindManyOptions, FindOperator } from 'typeorm'; -import { RegistrationDataEntity } from '@121-service/src/registration/registration-data.entity'; +import { RegistrationAttributeDataEntity } from '@121-service/src/registration/registration-attribute-data.entity'; import { convertToScopedOptions, FindOptionsCombined, @@ -9,12 +9,12 @@ import { describe('createFindWhereOptions helper', () => { it('should return correct scoped whereFilters', () => { // Arrange - const options: FindOptionsCombined = { + const options: FindOptionsCombined = { where: { program: { id: Equal(3) }, registrationStatus: Equal('included'), }, - } as unknown as FindOptionsCombined; + } as unknown as FindOptionsCombined; const relationArrayToRegistration = []; const requestScope = 'utrecht'; @@ -27,16 +27,17 @@ describe('createFindWhereOptions helper', () => { program: { id: 3, enableScope: false }, registrationStatus: 'included', }; - const expectedOptions: FindOptionsCombined = { - ...options, - // This ensures the toEqual checks for the 'adding the where' part. - where: [expectedWhereQueryScope, expectedWhereQueryScopeEnabled], - } as unknown as FindOptionsCombined; + const expectedOptions: FindOptionsCombined = + { + ...options, + // This ensures the toEqual checks for the 'adding the where' part. + where: [expectedWhereQueryScope, expectedWhereQueryScopeEnabled], + } as unknown as FindOptionsCombined; // Act const convertedScopedOptions = convertToScopedOptions< - RegistrationDataEntity, - FindManyOptions + RegistrationAttributeDataEntity, + FindManyOptions >(options, relationArrayToRegistration, requestScope); // Transform to comparable form diff --git a/services/121-service/src/wrapper.type.ts b/services/121-service/src/wrapper.type.ts index 2acd7340a5..e1a683c35f 100644 --- a/services/121-service/src/wrapper.type.ts +++ b/services/121-service/src/wrapper.type.ts @@ -3,7 +3,3 @@ * caused by reflection metadata saving the type of the property. */ export type WrapperType = T; // WrapperType === Relation - -export function getEnumValue(enumValue: T): T { - return enumValue; -} diff --git a/services/121-service/swagger.json b/services/121-service/swagger.json index c0f5c811ce..870ad3a84c 100644 --- a/services/121-service/swagger.json +++ b/services/121-service/swagger.json @@ -227,52 +227,34 @@ }, { "method": "post", - "path": "/api/programs/{programId}/program-questions", + "path": "/api/programs/{programId}/registration-attributes", "params": [ "programId" ] }, { "method": "patch", - "path": "/api/programs/{programId}/program-questions/{programQuestionId}", + "path": "/api/programs/{programId}/registration-attributes/{programRegistrationAttributeName}", "params": [ "programId", - "programQuestionId" + "programRegistrationAttributeName" ], - "returnType": "ProgramQuestionEntity" + "returnType": "ProgramRegistrationAttributeEntity" }, { "method": "delete", - "path": "/api/programs/{programId}/program-questions/{programQuestionId}", + "path": "/api/programs/{programId}/registration-attributes/{programRegistrationAttributeId}", "params": [ "programId", - "programQuestionId" + "programRegistrationAttributeId" ] }, - { - "method": "post", - "path": "/api/programs/{programId}/custom-attributes", - "params": [ - "programId" - ] - }, - { - "method": "patch", - "path": "/api/programs/{programId}/custom-attributes/{customAttributeId}", - "params": [ - "programId", - "customAttributeId" - ], - "returnType": "ProgramCustomAttributeEntity" - }, { "method": "get", "path": "/api/programs/{programId}/attributes", "params": [ "programId", - "includeCustomAttributes", - "includeProgramQuestions", - "includeFspQuestions", + "includeProgramRegistrationAttributes", "includeTemplateDefaultAttributes", "filterShowInPeopleAffectedTable" ] @@ -304,78 +286,99 @@ { "method": "get", "path": "/api/financial-service-providers", - "params": [] + "params": [], + "returnType": "FinancialServiceProviderDto" }, { "method": "get", - "path": "/api/financial-service-providers/{fspId}", + "path": "/api/financial-service-providers/{financialServiceProviderName}", "params": [ - "fspId" + "financialServiceProviderName" ], - "returnType": "FinancialServiceProviderEntity" + "returnType": "FinancialServiceProviderDto" }, { - "method": "patch", - "path": "/api/financial-service-providers/{fspId}", + "method": "get", + "path": "/api/programs/{programId}/financial-service-provider-configurations", "params": [ - "fspId" - ], - "returnType": "FinancialServiceProviderEntity" + "programId" + ] + }, + { + "method": "post", + "path": "/api/programs/{programId}/financial-service-provider-configurations", + "params": [ + "programId" + ] }, { "method": "patch", - "path": "/api/financial-service-providers/{fspId}/attribute/{attributeName}", + "path": "/api/programs/{programId}/financial-service-provider-configurations/{name}", "params": [ - "fspId", - "attributeName" - ], - "returnType": "FspQuestionEntity" + "programId", + "name" + ] }, { "method": "delete", - "path": "/api/financial-service-providers/{fspId}/attribute/{attributeName}", + "path": "/api/programs/{programId}/financial-service-provider-configurations/{name}", "params": [ - "fspId", - "attributeName" - ], - "returnType": "FspQuestionEntity" + "programId", + "name" + ] }, { "method": "post", - "path": "/api/financial-service-providers/{fspId}/attribute", + "path": "/api/programs/{programId}/financial-service-provider-configurations/{name}/properties", "params": [ - "fspId" - ], - "returnType": "FspQuestionEntity" + "programId", + "name" + ] }, { - "method": "get", - "path": "/api/programs/{programId}/fsp-configuration", + "method": "patch", + "path": "/api/programs/{programId}/financial-service-provider-configurations/{name}/properties/{propertyName}", "params": [ - "programId" + "programId", + "name", + "propertyName" ] }, { - "method": "post", - "path": "/api/programs/{programId}/fsp-configuration", + "method": "delete", + "path": "/api/programs/{programId}/financial-service-provider-configurations/{name}/properties/{propertyName}", "params": [ - "programId" + "programId", + "name", + "propertyName" ] }, { - "method": "put", - "path": "/api/programs/{programId}/fsp-configuration/{programFspConfigurationId}", + "method": "get", + "path": "/api/programs/{programId}/transactions", "params": [ "programId", - "programFspConfigurationId" + "referenceId", + "payment" ] }, { - "method": "delete", - "path": "/api/programs/{programId}/fsp-configuration/{programFspConfigurationId}", + "method": "get", + "path": "/api/programs/{programId}/events", "params": [ "programId", - "programFspConfigurationId" + "format", + "toDate", + "fromDate", + "referenceId" + ] + }, + { + "method": "get", + "path": "/api/programs/{programId}/registrations/{registrationId}/events", + "params": [ + "registrationId", + "programId" ] }, { @@ -472,34 +475,6 @@ "path": "/api/financial-service-providers/intersolve-voucher/send-reminders", "params": [] }, - { - "method": "get", - "path": "/api/programs/{programId}/transactions", - "params": [ - "programId", - "referenceId", - "payment" - ] - }, - { - "method": "get", - "path": "/api/programs/{programId}/events", - "params": [ - "programId", - "format", - "toDate", - "fromDate", - "referenceId" - ] - }, - { - "method": "get", - "path": "/api/programs/{programId}/registrations/{registrationId}/events", - "params": [ - "registrationId", - "programId" - ] - }, { "method": "get", "path": "/api/programs/{programId}/metrics/export-list/{exportType}", @@ -515,8 +490,9 @@ "filter.preferredLanguage", "filter.inclusionScore", "filter.paymentAmountMultiplier", - "filter.financialServiceProvider", - "filter.fspDisplayName", + "filter.financialServiceProviderName", + "filter.programFinancialServiceProviderConfigurationName", + "filter.programFinancialServiceProviderConfigurationLabel", "filter.registrationProgramId", "filter.maxPayments", "filter.paymentCount", @@ -566,14 +542,14 @@ }, { "method": "post", - "path": "/api/programs/{programId}/registrations/import-registrations", + "path": "/api/programs/{programId}/registrations/import", "params": [ "programId" ] }, { "method": "post", - "path": "/api/programs/{programId}/registrations/import", + "path": "/api/programs/{programId}/registrations", "params": [ "programId" ] @@ -593,8 +569,9 @@ "filter.preferredLanguage", "filter.inclusionScore", "filter.paymentAmountMultiplier", - "filter.financialServiceProvider", - "filter.fspDisplayName", + "filter.financialServiceProviderName", + "filter.programFinancialServiceProviderConfigurationName", + "filter.programFinancialServiceProviderConfigurationLabel", "filter.registrationProgramId", "filter.maxPayments", "filter.paymentCount", @@ -633,8 +610,9 @@ "filter.preferredLanguage", "filter.inclusionScore", "filter.paymentAmountMultiplier", - "filter.financialServiceProvider", - "filter.fspDisplayName", + "filter.financialServiceProviderName", + "filter.programFinancialServiceProviderConfigurationName", + "filter.programFinancialServiceProviderConfigurationLabel", "filter.registrationProgramId", "filter.maxPayments", "filter.paymentCount", @@ -653,7 +631,7 @@ }, { "method": "get", - "path": "/api/programs/{programId}/registrations/import-template", + "path": "/api/programs/{programId}/registrations/import/template", "params": [ "programId" ] @@ -674,8 +652,9 @@ "filter.preferredLanguage", "filter.inclusionScore", "filter.paymentAmountMultiplier", - "filter.financialServiceProvider", - "filter.fspDisplayName", + "filter.financialServiceProviderName", + "filter.programFinancialServiceProviderConfigurationName", + "filter.programFinancialServiceProviderConfigurationLabel", "filter.registrationProgramId", "filter.maxPayments", "filter.paymentCount", @@ -707,14 +686,6 @@ "phonenumber" ] }, - { - "method": "put", - "path": "/api/programs/{programId}/registrations/{referenceId}/fsp", - "params": [ - "programId", - "referenceId" - ] - }, { "method": "post", "path": "/api/programs/{programId}/registrations/message", @@ -731,8 +702,9 @@ "filter.preferredLanguage", "filter.inclusionScore", "filter.paymentAmountMultiplier", - "filter.financialServiceProvider", - "filter.fspDisplayName", + "filter.financialServiceProviderName", + "filter.programFinancialServiceProviderConfigurationName", + "filter.programFinancialServiceProviderConfigurationLabel", "filter.registrationProgramId", "filter.maxPayments", "filter.paymentCount", @@ -840,8 +812,9 @@ "filter.preferredLanguage", "filter.inclusionScore", "filter.paymentAmountMultiplier", - "filter.financialServiceProvider", - "filter.fspDisplayName", + "filter.financialServiceProviderName", + "filter.programFinancialServiceProviderConfigurationName", + "filter.programFinancialServiceProviderConfigurationLabel", "filter.registrationProgramId", "filter.maxPayments", "filter.paymentCount", @@ -942,13 +915,6 @@ "path": "/api/financial-service-providers/commercial-bank-ethiopia/account-enquiries/validation", "params": [] }, - { - "method": "post", - "path": "/api/migrate-visa/visa", - "params": [ - "limit" - ] - }, { "method": "post", "path": "/api/notifications/whatsapp/status", diff --git a/services/121-service/test-registration-data/test-registrations-OCW.csv b/services/121-service/test-registration-data/test-registrations-OCW.csv index 1b366da5ef..2f42bae0b7 100644 --- a/services/121-service/test-registration-data/test-registrations-OCW.csv +++ b/services/121-service/test-registration-data/test-registrations-OCW.csv @@ -1,4 +1,4 @@ -referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,fspName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity +referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phoneNumber,programFinancialServiceProviderConfigurationName,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition,addressPostalCode,addressCity 00dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 01dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-create-customer,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 02dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-create-wallet,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag @@ -12,10 +12,8 @@ referenceId,preferredLanguage,paymentAmountMultiplier,maxPayments,fullName,phone 10dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-get-card-CARD_CLOSED,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 11dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-create-debit-card,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag 12dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-load-balance,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag -13dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-spent-4000,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB, -14dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-current-balance-14000,14155238876,Intersolve-visa,14155238886,Straat,1,A,1234AB, -15dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-current-balance-8000-mock-spent-7000,14155238856,Intersolve-visa,14155238886,Straat,1,A,1234AB, -16dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,succeed,14155238886,Intersolve-visa,14155238886,,,,, -17dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,succeed,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag +13dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-spent-4000,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag +14dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-current-balance-14000,14155238876,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag +15dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-current-balance-8000-mock-spent-7000,14155238856,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag +17dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,succeed,14155238886,Intersolve-voucher-whatsapp,14155238886,Straat,1,A,1234AB,Den Haag 18dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-fail-create-order,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB,Den Haag -19dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,,mock-address-city-fail,14155238886,Intersolve-visa,14155238886,Straat,1,A,1234AB, diff --git a/services/121-service/test-registration-data/test-registrations-patch-OCW-chosen-FSP.csv b/services/121-service/test-registration-data/test-registrations-patch-OCW-chosen-FSP.csv new file mode 100644 index 0000000000..8767fef5ea --- /dev/null +++ b/services/121-service/test-registration-data/test-registrations-patch-OCW-chosen-FSP.csv @@ -0,0 +1,3 @@ +referenceId,programFinancialServiceProviderConfigurationName +00dc9451-1273-484c-b2e8-ae21b51a96ab,Intersolve-voucher-whatsapp +01dc9451-1273-484c-b2e8-ae21b51a96ab,Intersolve-visa diff --git a/services/121-service/test-registration-data/test-registrations-patch-OCW-without-phoneNumber-column.csv b/services/121-service/test-registration-data/test-registrations-patch-OCW-without-phoneNumber-column.csv index 0df01242af..671f9867d5 100644 --- a/services/121-service/test-registration-data/test-registrations-patch-OCW-without-phoneNumber-column.csv +++ b/services/121-service/test-registration-data/test-registrations-patch-OCW-without-phoneNumber-column.csv @@ -1,4 +1,3 @@ -referenceId,preferredLanguage,paymentAmountMultiplier,fullName,addressStreet,addressHouseNumber,addressHouseNumberAddition -00dc9451-1273-484c-b2e8-ae21b51a96ab,ar,2,updated name1,newStreet1,2, -01dc9451-1273-484c-b2e8-ae21b51a96ab,nl,3,updated name 2,newStreet2,3,updated -16dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,New NAme mate,,,, +referenceId,preferredLanguage,paymentAmountMultiplier,fullName,addressStreet,addressHouseNumber,addressHouseNumberAddition,phoneNumber +00dc9451-1273-484c-b2e8-ae21b51a96ab,ar,2,updated name1,newStreet1,2,,14155238880, +17dc9451-1273-484c-b2e8-ae21b51a96ab,nl,3,updated name 2,newStreet2,3,updated,, diff --git a/services/121-service/test-registration-data/test-registrations-patch-OCW.csv b/services/121-service/test-registration-data/test-registrations-patch-OCW.csv index 933b8b2105..ac71ef8d51 100644 --- a/services/121-service/test-registration-data/test-registrations-patch-OCW.csv +++ b/services/121-service/test-registration-data/test-registrations-patch-OCW.csv @@ -1,4 +1,3 @@ referenceId,preferredLanguage,paymentAmountMultiplier,fullName,phoneNumber,whatsappPhoneNumber,addressStreet,addressHouseNumber,addressHouseNumberAddition 00dc9451-1273-484c-b2e8-ae21b51a96ab,ar,2,updated name1,14155238880,14155238880,newStreet1,2, 01dc9451-1273-484c-b2e8-ae21b51a96ab,nl,3,updated name 2,14155238881,14155238881,newStreet2,3,updated -16dc9451-1273-484c-b2e8-ae21b51a96ab,en,1,New NAme mate,14155238887,14155238886,,,, diff --git a/services/121-service/test/commercial-bank-ethiopia/export-validation-report.test.ts b/services/121-service/test/commercial-bank-ethiopia/export-validation-report.test.ts index 620cc82083..acfa41c4fa 100644 --- a/services/121-service/test/commercial-bank-ethiopia/export-validation-report.test.ts +++ b/services/121-service/test/commercial-bank-ethiopia/export-validation-report.test.ts @@ -1,6 +1,4 @@ -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { waitFor } from '@121-service/src/utils/waitFor.helper'; import { getCbeValidationReport, @@ -11,35 +9,10 @@ import { getAccessToken, resetDB, } from '@121-service/test/helpers/utility.helper'; +import { registrationCbe } from '@121-service/test/registrations/pagination/pagination-data'; describe('Export CBE validation report', () => { const programId = 1; - - const registrationCbe = { - referenceId: 'registration-cbe-1', - phoneNumber: '14155238886', - preferredLanguage: LanguageEnum.en, - paymentAmountMultiplier: 1, - fspName: FinancialServiceProviders.commercialBankEthiopia, - maxPayments: 3, - fullName: 'ANDUALEM MOHAMMED YIMER', - idNumber: '39231855170', - age: '48', - gender: 'male', - howManyFemale: '1', - howManyMale: '2', - totalFamilyMembers: '3', - howManyFemaleUnder18: '1', - howManyMaleUnder18: '2', - howManyFemaleOver18: '1', - howManyMaleOver18: '1', - howManyFemaleDisabilityUnder18: '2', - howManyMaleDisabilityUnder18: '1', - howManyFemaleDisabilityOver18: '1', - howManyMaleDisabilityOver18: '2', - bankAccountNumber: '407951684723597', - }; - let accessToken: string; beforeEach(async () => { diff --git a/services/121-service/test/event/get-event.test.ts b/services/121-service/test/event/get-event.test.ts index de7723e07c..a240467906 100644 --- a/services/121-service/test/event/get-event.test.ts +++ b/services/121-service/test/event/get-event.test.ts @@ -1,7 +1,7 @@ import { HttpStatus } from '@nestjs/common'; import { EventEnum } from '@121-service/src/events/enum/event.enum'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import { registrationVisa } from '@121-service/src/seed-data/mock/visa-card.data'; import { @@ -48,9 +48,10 @@ describe('Get events', () => { addressStreet: 'updated street', }; const expectedAttributesObject = { - oldValue: registrationVisa[CustomDataAttributes.phoneNumber], + oldValue: + registrationVisa[DefaultRegistrationDataAttributeNames.phoneNumber], newValue: updatePhoneNumber, - fieldName: CustomDataAttributes.phoneNumber, + fieldName: DefaultRegistrationDataAttributeNames.phoneNumber, reason, }; @@ -102,7 +103,8 @@ describe('Get events', () => { const event = eventsResult.body.find( (event) => event.type === EventEnum.registrationDataChange && - event.attributes.fieldName === CustomDataAttributes.phoneNumber, + event.attributes.fieldName === + DefaultRegistrationDataAttributeNames.phoneNumber, ); expect(event.attributes).toEqual(expectedAttributesObject); }); @@ -114,9 +116,10 @@ describe('Get events', () => { phoneNumber: updatePhoneNumber, }; const expectedAttributesObject = { - oldValue: registrationVisa[CustomDataAttributes.phoneNumber], + oldValue: + registrationVisa[DefaultRegistrationDataAttributeNames.phoneNumber], newValue: updatePhoneNumber, - fieldName: CustomDataAttributes.phoneNumber, + fieldName: DefaultRegistrationDataAttributeNames.phoneNumber, reason, }; const date = new Date(); diff --git a/services/121-service/test/fixtures/scoped-registrations.ts b/services/121-service/test/fixtures/scoped-registrations.ts index 38316a7ba0..4dd8fa5a33 100644 --- a/services/121-service/test/fixtures/scoped-registrations.ts +++ b/services/121-service/test/fixtures/scoped-registrations.ts @@ -1,5 +1,5 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { DebugScope } from '@121-service/src/scripts/enum/debug-scope.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; @@ -10,36 +10,40 @@ export const registrationScopedGoesPv = { referenceId: 'reference-id-scoped-goes-pv', scope: DebugScope.ZeelandGoes, preferredLanguage: LanguageEnum.en, - [CustomDataAttributes.phoneNumber]: '15005550111', + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550111', fullName: 'Jane Doe', - fspName: FinancialServiceProviders.intersolveVoucherPaper, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherPaper, }; export const registrationScopedMiddelburgPv = { referenceId: 'reference-id-scoped-middelburg-pv', scope: DebugScope.ZeelandMiddelburg, preferredLanguage: LanguageEnum.en, - [CustomDataAttributes.phoneNumber]: '15005550112', + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550112', fullName: 'Juliet Marsh', - fspName: FinancialServiceProviders.intersolveVoucherPaper, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherPaper, }; export const registrationScopedUtrechtPv = { referenceId: 'reference-id-scoped-utrecht-pv', preferredLanguage: LanguageEnum.nl, scope: DebugScope.UtrechtHouten, - [CustomDataAttributes.phoneNumber]: '15005550121', + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550121', fullName: 'Sam Winters', - fspName: FinancialServiceProviders.intersolveVoucherPaper, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherPaper, }; export const registrationNotScopedPv = { referenceId: 'reference-id-not-scoped-pv', scope: '', preferredLanguage: LanguageEnum.en, - [CustomDataAttributes.phoneNumber]: '15005550200', + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550200', fullName: 'Nick Brouwers', - fspName: FinancialServiceProviders.intersolveVoucherPaper, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherPaper, }; export const registrationsPV = [ diff --git a/services/121-service/test/helpers/assert.helper.ts b/services/121-service/test/helpers/assert.helper.ts index 09a991f80c..2a7c64b0e0 100644 --- a/services/121-service/test/helpers/assert.helper.ts +++ b/services/121-service/test/helpers/assert.helper.ts @@ -1,6 +1,7 @@ import { MessageTemplateEntity } from '@121-service/src/notifications/message-template/message-template.entity'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; +import { LocalizedString } from '@121-service/src/shared/types/localized-string.type'; export function processMessagePlaceholders( messageTemplates: MessageTemplateEntity[], @@ -21,12 +22,28 @@ export function processMessagePlaceholders( return processedTemplate; } -export function assertRegistrationImport(response: any, expected: any): void { - expect(response.phoneNumber).toBe(expected.phoneNumber); - expect(response.fullName).toBe(expected.fullName); - expect(response.addressStreet).toBe(expected.addressStreet); - expect(response.addressHouseNumber).toBe(expected.addressHouseNumber); - expect(response.addressHouseNumberAddition).toBe( - expected.addressHouseNumberAddition, - ); +export function assertRegistrationBulkUpdate( + patchData: Record< + string, + string | undefined | boolean | number | null | LocalizedString + >, + updatedRegistration: Record< + string, + string | undefined | boolean | number | null + >, + originalRegistration: Record< + string, + string | undefined | boolean | number | null + >, +): void { + for (const key in patchData) { + expect(JSON.stringify(updatedRegistration[key])).toBe( + JSON.stringify(patchData[key]), + ); + } + for (const key in originalRegistration) { + if (patchData[key] === undefined && key !== 'name') { + expect(updatedRegistration[key]).toStrictEqual(originalRegistration[key]); + } + } } diff --git a/services/121-service/test/helpers/program-financial-service-provider-configuration.helper.ts b/services/121-service/test/helpers/program-financial-service-provider-configuration.helper.ts new file mode 100644 index 0000000000..8c328aa5de --- /dev/null +++ b/services/121-service/test/helpers/program-financial-service-provider-configuration.helper.ts @@ -0,0 +1,146 @@ +import * as request from 'supertest'; + +import { FinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { CreateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration-property.dto'; +import { ProgramFinancialServiceProviderConfigurationPropertyResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-property-response.dto'; +import { ProgramFinancialServiceProviderConfigurationResponseDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/program-financial-service-provider-configuration-response.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto'; +import { getServer } from '@121-service/test/helpers/utility.helper'; + +export async function postProgramFinancialServiceProviderConfiguration({ + programId, + body, + accessToken, +}: { + programId: number; + body: CreateProgramFinancialServiceProviderConfigurationDto; + accessToken: string; +}): Promise { + return await getServer() + .post(`/programs/${programId}/financial-service-provider-configurations`) + .set('Cookie', [accessToken]) + .send(body); +} + +export async function patchProgramFinancialServiceProviderConfiguration({ + programId, + name, + body, + accessToken, +}: { + programId: number; + name: string; + body: UpdateProgramFinancialServiceProviderConfigurationDto; + accessToken: string; +}): Promise< + Omit & { + body: ProgramFinancialServiceProviderConfigurationResponseDto; + } +> { + return await getServer() + .patch( + `/programs/${programId}/financial-service-provider-configurations/${name}`, + ) + .set('Cookie', [accessToken]) + .send(body); +} + +export async function getProgramFinancialServiceProviderConfigurations({ + programId, + accessToken, +}: { + programId: number; + accessToken: string; +}): Promise< + Omit & { + body: ProgramFinancialServiceProviderConfigurationResponseDto[]; + } +> { + return await getServer() + .get(`/programs/${programId}/financial-service-provider-configurations`) + .set('Cookie', [accessToken]); +} + +export async function deleteProgramFinancialServiceProviderConfiguration({ + programId, + name, + accessToken, +}: { + programId: number; + name: string; + accessToken: string; +}): Promise { + return await getServer() + .delete( + `/programs/${programId}/financial-service-provider-configurations/${name}`, + ) + .set('Cookie', [accessToken]); +} + +export async function postProgramFinancialServiceProviderConfigurationProperties({ + programId, + properties, + accessToken, + name, +}: { + programId: number; + properties: CreateProgramFinancialServiceProviderConfigurationPropertyDto[]; + name: string; + accessToken: string; +}): Promise< + Omit & { + body: ProgramFinancialServiceProviderConfigurationPropertyResponseDto[]; + } +> { + return await getServer() + .post( + `/programs/${programId}/financial-service-provider-configurations/${name}/properties`, + ) + .set('Cookie', [accessToken]) + .send(properties); +} + +export async function patchProgramFinancialServiceProviderConfigurationProperty({ + programId, + configName, + propertyName, + body, + accessToken, +}: { + programId: number; + configName: string; + propertyName: FinancialServiceProviderConfigurationProperties; + body: UpdateProgramFinancialServiceProviderConfigurationPropertyDto; + accessToken: string; +}): Promise< + Omit & { + body: ProgramFinancialServiceProviderConfigurationPropertyResponseDto; + } +> { + return await getServer() + .patch( + `/programs/${programId}/financial-service-provider-configurations/${configName}/properties/${propertyName}`, + ) + .set('Cookie', [accessToken]) + .send(body); +} + +export async function deleteProgramFinancialServiceProviderConfigurationProperty({ + programId, + configName, + propertyName, + accessToken, +}: { + programId: number; + configName: string; + propertyName: FinancialServiceProviderConfigurationProperties; + accessToken: string; +}): Promise { + return await getServer() + .delete( + `/programs/${programId}/financial-service-provider-configurations/${configName}/properties/${propertyName}`, + ) + .set('Cookie', [accessToken]); +} diff --git a/services/121-service/test/helpers/program.helper.ts b/services/121-service/test/helpers/program.helper.ts index 7617515887..017e360cdf 100644 --- a/services/121-service/test/helpers/program.helper.ts +++ b/services/121-service/test/helpers/program.helper.ts @@ -7,8 +7,10 @@ import { } from '@121-service/src/notifications/message-template/dto/message-template.dto'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { CreateProgramDto } from '@121-service/src/programs/dto/create-program.dto'; -import { CreateProgramCustomAttributeDto } from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; -import { CreateProgramQuestionDto } from '@121-service/src/programs/dto/program-question.dto'; +import { + ProgramRegistrationAttributeDto, + UpdateProgramRegistrationAttributeDto, +} from '@121-service/src/programs/dto/program-registration-attribute.dto'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { waitFor } from '@121-service/src/utils/waitFor.helper'; @@ -55,26 +57,34 @@ export async function getProgram( .set('Cookie', [accessToken]); } -export async function postProgramQuestion( - programQuestion: CreateProgramQuestionDto, +export async function postProgramRegistrationAttribute( + programRegistrationAttribute: ProgramRegistrationAttributeDto, programId: number, accessToken: string, ): Promise { return await getServer() - .post(`/programs/${programId}/program-questions`) + .post(`/programs/${programId}/registration-attributes`) .set('Cookie', [accessToken]) - .send(programQuestion); + .send(programRegistrationAttribute); } -export async function postCustomAttribute( - customAttribute: CreateProgramCustomAttributeDto, - programId: number, - accessToken: string, -): Promise { +export async function patchProgramRegistrationAttribute({ + programRegistrationAttributeName, + programRegistrationAttribute, + programId, + accessToken, +}: { + programRegistrationAttributeName: string; + programRegistrationAttribute: UpdateProgramRegistrationAttributeDto; + programId: number; + accessToken: string; +}): Promise { return await getServer() - .post(`/programs/${programId}/custom-attributes`) + .patch( + `/programs/${programId}/registration-attributes/${programRegistrationAttributeName}`, + ) .set('Cookie', [accessToken]) - .send(customAttribute); + .send(programRegistrationAttribute); } export async function unpublishProgram( @@ -183,34 +193,6 @@ export async function getFspInstructions( .query({ format: 'json' }); } -export async function updateFinancialServiceProvider( - programId: number, - accessToken: string, - paymentReferenceIds: string[], - newFspName: string, - whatsappPhoneNumber: string, - addressStreet: string, - addressHouseNumber: string, - addressHouseNumberAddition: string, - addressPostalCode: string, - addressCity: string, -): Promise { - return await getServer() - .put(`/programs/${programId}/registrations/${paymentReferenceIds}/fsp`) - .set('Cookie', [accessToken]) - .send({ - newFspName, - newFspAttributes: { - whatsappPhoneNumber, - addressStreet, - addressHouseNumber, - addressHouseNumberAddition, - addressPostalCode, - addressCity, - }, - }); -} - export async function importFspReconciliationData( programId: number, paymentNr: number, @@ -242,27 +224,6 @@ function jsonArrayToCsv(json: object[]): string { return csv.join('\r\n'); } -export async function getFspConfiguration( - programId: number, - accessToken: string, -): Promise { - return await getServer() - .get(`/programs/${programId}/fsp-configuration`) - .set('Cookie', [accessToken]); -} - -export async function deleteFspConfiguration( - programId: number, - programFspConfigurationId: number, - accessToken: string, -): Promise { - return await getServer() - .delete( - `/programs/${programId}/fsp-configuration/${programFspConfigurationId}`, - ) - .set('Cookie', [accessToken]); -} - export async function exportList( programId: number, exportType: string, @@ -412,6 +373,8 @@ export async function waitForMessagesToComplete({ referenceIdsWaitingForMessages = messageHistoriesWithoutMinimumMessages.map( ({ referenceId }) => referenceId, ); + // To not overload the server and get 429 + await waitFor(100); } if (referenceIdsWaitingForMessages.length > 0) { @@ -514,3 +477,18 @@ export async function getMessageTemplates( .get(`/notifications/${programId}/message-templates`) .set('Cookie', [accessToken]); } + +export async function setAllProgramsRegistrationAttributesNonRequired( + programId: number, + accessToken: string, +) { + const program = (await getProgram(programId, accessToken)).body; + for (const attribute of program.programRegistrationAttributes) { + await patchProgramRegistrationAttribute({ + programRegistrationAttributeName: attribute.name, + programRegistrationAttribute: { isRequired: false }, + programId, + accessToken, + }); + } +} diff --git a/services/121-service/test/helpers/registration.helper.ts b/services/121-service/test/helpers/registration.helper.ts index 1e781ff77f..eae8771913 100644 --- a/services/121-service/test/helpers/registration.helper.ts +++ b/services/121-service/test/helpers/registration.helper.ts @@ -17,7 +17,7 @@ export function importRegistrations( accessToken: string, ): Promise { return getServer() - .post(`/programs/${programId}/registrations/import`) + .post(`/programs/${programId}/registrations`) .set('Cookie', [accessToken]) .send(registrations); } @@ -28,7 +28,7 @@ export function importRegistrationsCSV( accessToken: string, ): Promise { return getServer() - .post(`/programs/${programId}/registrations/import-registrations`) + .post(`/programs/${programId}/registrations/import`) .set('Cookie', [accessToken]) .attach('file', filePath); } @@ -470,7 +470,7 @@ export async function getImportRegistrationsTemplate( const accessToken = await getAccessToken(); return getServer() - .get(`/programs/${programId}/registrations/import-template`) + .get(`/programs/${programId}/registrations/import/template`) .set('Cookie', [accessToken]) .send(); } diff --git a/services/121-service/test/helpers/utility.helper.ts b/services/121-service/test/helpers/utility.helper.ts index 1f8d02abdb..4a4e1bcaa3 100644 --- a/services/121-service/test/helpers/utility.helper.ts +++ b/services/121-service/test/helpers/utility.helper.ts @@ -174,13 +174,15 @@ export function cleanProgramForAssertions(originalProgram: any): any { 'endDate', 'updated', 'created', + 'programFinancialServiceProviderConfigurations', + 'financialServiceProviderConfigurations', ]); const attributesToSort = [ { attribute: 'editableAttributes', key: 'name' }, { attribute: 'paTableAttributes', key: 'name' }, { attribute: 'financialServiceProviders', key: 'fsp' }, - { attribute: 'programQuestions', key: 'name' }, + { attribute: 'programRegistrationAttributes', key: 'name' }, { attribute: 'filterableAttributes', key: 'group' }, ]; @@ -201,22 +203,5 @@ export function cleanProgramForAssertions(originalProgram: any): any { ); } - if (program.financialServiceProviders) { - program.financialServiceProviders = program.financialServiceProviders.map( - (financialServiceProvider: any) => { - if (!financialServiceProvider.questions) { - return financialServiceProvider; - } - - return { - ...financialServiceProvider, - questions: financialServiceProvider.questions.sort( - sortByAttribute('name'), - ), - }; - }, - ); - } - return program; } diff --git a/services/121-service/test/interceptors/program-exists.interceptor.test.ts b/services/121-service/test/interceptors/program-exists.interceptor.test.ts new file mode 100644 index 0000000000..4cae5f87f7 --- /dev/null +++ b/services/121-service/test/interceptors/program-exists.interceptor.test.ts @@ -0,0 +1,33 @@ +import { HttpStatus } from '@nestjs/common'; + +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; +import { postProgramFinancialServiceProviderConfiguration } from '@121-service/test/helpers/program-financial-service-provider-configuration.helper'; +import { + getAccessToken, + resetDB, +} from '@121-service/test/helpers/utility.helper'; + +describe('Program exist interceptor', () => { + let accessToken: string; + + beforeEach(async () => { + await resetDB(SeedScript.nlrcMultiple); + accessToken = await getAccessToken(); + }); + + it('should throw an error if the program does not exist', async () => { + // Act + const nonExistingProgramId = 999999; + const result = await postProgramFinancialServiceProviderConfiguration({ + programId: nonExistingProgramId, + body: new CreateProgramFinancialServiceProviderConfigurationDto(), + accessToken, + }); + + // Assert + expect(result.statusCode).toBe(HttpStatus.NOT_FOUND); + expect(result.body?.message).toContain('Program'); + expect(result.body?.message).toContain(String(nonExistingProgramId)); + }); +}); diff --git a/services/121-service/test/metrics/export-list.test.ts b/services/121-service/test/metrics/export-list.test.ts index f7c76b9556..d3783c15ec 100644 --- a/services/121-service/test/metrics/export-list.test.ts +++ b/services/121-service/test/metrics/export-list.test.ts @@ -33,7 +33,7 @@ function createExportObject( const exportObject = { ...registration, }; - delete exportObject.fspName; + delete exportObject.programFinancialServiceProviderConfigurationName; // remove empty values Object.keys(exportObject).forEach( (key) => !exportObject[key] && delete exportObject[key], diff --git a/services/121-service/test/payment/__snapshots__/do-payment-fsp-excel.test.ts.snap b/services/121-service/test/payment/__snapshots__/do-payment-fsp-excel.test.ts.snap index e12cb4adbe..4565adc6ee 100644 --- a/services/121-service/test/payment/__snapshots__/do-payment-fsp-excel.test.ts.snap +++ b/services/121-service/test/payment/__snapshots__/do-payment-fsp-excel.test.ts.snap @@ -1,8 +1,134 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Do payment with Excel FSP Export FSP instructions Should return all program-registration-attributes on Get FSP instruction with Excel-FSP when "columnsToExport" is not set 1`] = ` +[ + { + "data": [ + { + "accountId": null, + "amount": 10, + "date": null, + "dob": "31-08-1990", + "dragon": "0", + "fixedChoice": null, + "healthArea": "", + "house": "lannister", + "knowsNothing": "false", + "motto": "A lanister always pays his debts", + "name": "Jaime Lannister", + "openAnswer": "", + "personalId": "", + "phoneNumber": "14155235556", + "referenceId": "westeros987654322", + "whatsappPhoneNumber": "14155235555", + }, + ], + "fileNamePrefix": "gringotts", + }, + { + "data": [ + { + "accountId": null, + "amount": 20, + "date": null, + "dob": "31-08-1990", + "dragon": "1", + "fixedChoice": null, + "healthArea": "", + "house": "stark", + "knowsNothing": "true", + "motto": "Winter is coming", + "name": "John Snow", + "openAnswer": "", + "personalId": "", + "phoneNumber": "14155235554", + "referenceId": "westeros123456789", + "whatsappPhoneNumber": "14155235554", + }, + { + "accountId": null, + "amount": 10, + "date": null, + "dob": "31-08-1990", + "dragon": "0", + "fixedChoice": null, + "healthArea": "", + "house": "stark", + "knowsNothing": "false", + "motto": "A girl has no name", + "name": "Arya Stark", + "openAnswer": "", + "personalId": "", + "phoneNumber": "14155235555", + "referenceId": "westeros987654321", + "whatsappPhoneNumber": "14155235555", + }, + ], + "fileNamePrefix": "ironBank", + }, +] +`; + +exports[`Do payment with Excel FSP Export FSP instructions Should return specified columns on Get FSP instruction with Excel-FSP when "columnsToExport" is set 1`] = ` +[ + { + "data": [ + { + "amount": 10, + "dob": "31-08-1990", + "dragon": "0", + "name": "Jaime Lannister", + "phoneNumber": "14155235556", + "referenceId": "westeros987654322", + }, + ], + "fileNamePrefix": "gringotts", + }, + { + "data": [ + { + "amount": 20, + "dob": "31-08-1990", + "dragon": "1", + "name": "John Snow", + "phoneNumber": "14155235554", + "referenceId": "westeros123456789", + }, + { + "amount": 10, + "dob": "31-08-1990", + "dragon": "0", + "name": "Arya Stark", + "phoneNumber": "14155235555", + "referenceId": "westeros987654321", + }, + ], + "fileNamePrefix": "ironBank", + }, +] +`; + +exports[`Do payment with Excel FSP Import FSP reconciliation data Should give an error when status column is missing 1`] = ` +{ + "errors": "The 'status' column is either missing or contains unexpected values. It should only contain 'success' or 'error'.", +} +`; + exports[`Do payment with Excel FSP Import FSP reconciliation data should give me a CSV template when I request it 1`] = ` [ - "phoneNumber", - "status", + { + "name": "gringotts", + "template": [ + "phoneNumber", + "status", + ], + }, + { + "name": "ironBank", + "template": [ + "phoneNumber", + "status", + ], + }, ] `; diff --git a/services/121-service/test/payment/do-payment-commercial-bank-ethiopia.test.ts b/services/121-service/test/payment/do-payment-commercial-bank-ethiopia.test.ts new file mode 100644 index 0000000000..855a02cbeb --- /dev/null +++ b/services/121-service/test/payment/do-payment-commercial-bank-ethiopia.test.ts @@ -0,0 +1,81 @@ +import { HttpStatus } from '@nestjs/common'; + +import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; +import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; +import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; +import { + doPayment, + getTransactions, + waitForPaymentTransactionsToComplete, +} from '@121-service/test/helpers/program.helper'; +import { + awaitChangePaStatus, + importRegistrations, +} from '@121-service/test/helpers/registration.helper'; +import { + getAccessToken, + resetDB, +} from '@121-service/test/helpers/utility.helper'; +import { registrationCbe } from '@121-service/test/registrations/pagination/pagination-data'; + +describe('Do payment to 1 PA', () => { + const programId = 1; + const payment = 1; + const amount = 12; + + describe('with FSP: Commercial Bank Ethiopia', () => { + let accessToken: string; + + beforeEach(async () => { + await resetDB(SeedScript.ethJointResponse); + accessToken = await getAccessToken(); + }); + + it('should successfully pay-out', async () => { + // Arrange + await importRegistrations(programId, [registrationCbe], accessToken); + + await awaitChangePaStatus( + programId, + [registrationCbe.referenceId], + RegistrationStatusEnum.included, + accessToken, + ); + const paymentReferenceIds = [registrationCbe.referenceId]; + + // Act + const doPaymentResponse = await doPayment( + programId, + payment, + amount, + paymentReferenceIds, + accessToken, + ); + + await waitForPaymentTransactionsToComplete( + programId, + paymentReferenceIds, + accessToken, + 3001, + [TransactionStatusEnum.success, TransactionStatusEnum.error], + ); + + // Assert + const getTransactionsBody = await getTransactions( + programId, + payment, + registrationCbe.referenceId, + accessToken, + ); + + expect(doPaymentResponse.status).toBe(HttpStatus.ACCEPTED); + expect(doPaymentResponse.body.applicableCount).toBe( + paymentReferenceIds.length, + ); + expect(getTransactionsBody.body[0].status).toBe( + TransactionStatusEnum.success, + ); + expect(getTransactionsBody.body[0].errorMessage).toBe(null); + }); + }); +}); diff --git a/services/121-service/test/payment/do-payment-fsp-excel.test.ts b/services/121-service/test/payment/do-payment-fsp-excel.test.ts index 444ea896f9..c8ff2d665c 100644 --- a/services/121-service/test/payment/do-payment-fsp-excel.test.ts +++ b/services/121-service/test/payment/do-payment-fsp-excel.test.ts @@ -1,24 +1,24 @@ /* eslint-disable jest/no-conditional-expect */ import { HttpStatus } from '@nestjs/common'; -import { - FinancialServiceProviderConfigurationEnum, - FinancialServiceProviders, -} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; +import { FinancialServiceProviderConfigurationProperties } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { ImportStatus } from '@121-service/src/registration/dto/bulk-import.dto'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import programTest from '@121-service/src/seed-data/program/program-test.json'; import { - deleteFspConfiguration, doPayment, - getFspConfiguration, getFspInstructions, getTransactions, importFspReconciliationData, waitForPaymentTransactionsToComplete, } from '@121-service/test/helpers/program.helper'; +import { + deleteProgramFinancialServiceProviderConfigurationProperty, + getProgramFinancialServiceProviderConfigurations, +} from '@121-service/test/helpers/program-financial-service-provider-configuration.helper'; import { awaitChangePaStatus, getImportFspReconciliationTemplate, @@ -34,6 +34,7 @@ import { registrationPV5, registrationWesteros1, registrationWesteros2, + registrationWesteros3, } from '@121-service/test/registrations/pagination/pagination-data'; describe('Do payment with Excel FSP', () => { @@ -43,7 +44,11 @@ describe('Do payment with Excel FSP', () => { const paymentNr = 1; // Registrations - const registrationsWesteros = [registrationWesteros1, registrationWesteros2]; + const registrationsWesteros = [ + registrationWesteros1, + registrationWesteros2, + registrationWesteros3, + ]; const referenceIdsWesteros = registrationsWesteros.map( (registration) => registration.referenceId, ); @@ -120,17 +125,6 @@ describe('Do payment with Excel FSP', () => { describe('Export FSP instructions', () => { it('Should return specified columns on Get FSP instruction with Excel-FSP when "columnsToExport" is set', async () => { // Arrange - const configValue = programTest.financialServiceProviders - .find((fsp) => fsp.fsp === FinancialServiceProviders.excel) - ?.configuration?.find( - (c) => - c.name === - FinancialServiceProviderConfigurationEnum.columnsToExport, - ); - - const columns = Array.isArray(configValue?.value) - ? [...configValue.value, 'amount'] - : []; // Act const transactionsResponse = await getTransactions( @@ -145,68 +139,37 @@ describe('Do payment with Excel FSP', () => { paymentNr, accessToken, ); - const fspInstructions = fspInstructionsResponse.body.data; + const fspInstructions = fspInstructionsResponse.body; // Assert - // Check if transactions are on 'waiting' status for (const transaction of transactionsResponse.body) { expect(transaction.status).toBe(TransactionStatusEnum.waiting); } - - // Also check if the right amount of transactions are created - expect(fspInstructions.length).toBe(referenceIdsWesteros.length); - - // Also check if the right phonenumber are in the transactions - expect(fspInstructions.map((r) => r.phoneNumber).sort()).toEqual( - phoneNumbersWesteros.sort(), - ); - - // Check if the rows are created with the right values - for (const row of fspInstructions) { - const registration = registrationsWesteros.find( - (r) => r.name === row.name, - )!; - expect(registration).toBeDefined(); - for (const [key, value] of Object.entries(row)) { - if (key === 'amount') { - const multipliedAmount = amount * (registration.dragon + 1); - expect(value).toBe(multipliedAmount); - } else { - expect(value).toEqual(String(registration[key])); - } - } - } - - // Check if the right columns are exported - const columnsInFspInstructions = Object.keys(fspInstructions[0]); - expect(columnsInFspInstructions.sort()).toEqual(columns.sort()); + // Sort fspInstructions by phoneNumber + expect(fspInstructions).toMatchSnapshot(); }); - it('Should return all program-question/program-custom attributes on Get FSP instruction with Excel-FSP when "columnsToExport" is not set', async () => { + it('Should return all program-registration-attributes on Get FSP instruction with Excel-FSP when "columnsToExport" is not set', async () => { // Arrange - const programAttributeColumns = programTest.programCustomAttributes.map( - (pa) => pa.name, - ); - const programQuestionColumns = programTest.programQuestions.map( - (pq) => pq.name, - ); - const columns = programAttributeColumns - .concat(programQuestionColumns) - .concat(['amount']); - - const fspConfig = await getFspConfiguration( - programIdWesteros, - accessToken, - ); - const columnsToExportFspConfigRecord = fspConfig.body.find( - (c) => - c.name === FinancialServiceProviderConfigurationEnum.columnsToExport, - ); - await deleteFspConfiguration( - programIdWesteros, - columnsToExportFspConfigRecord.id, - accessToken, - ); + const programAttributeColumns = + programTest.programRegistrationAttributes.map((pa) => pa.name); + programAttributeColumns.concat(['amount']); + + const fspConfigurations = + await getProgramFinancialServiceProviderConfigurations({ + programId: programIdWesteros, + accessToken, + }); + + for (const fspConfiguration of fspConfigurations.body) { + await deleteProgramFinancialServiceProviderConfigurationProperty({ + programId: programIdWesteros, + configName: fspConfiguration.name, + propertyName: + FinancialServiceProviderConfigurationProperties.columnsToExport, + accessToken, + }); + } // Act const fspInstructionsResponse = await getFspInstructions( @@ -214,40 +177,19 @@ describe('Do payment with Excel FSP', () => { paymentNr, accessToken, ); - const fspInstructions = fspInstructionsResponse.body.data; - - const namesWesteros = registrationsWesteros.map((r) => r.name); - // Also check if the right names are in the transactions - expect(fspInstructions.map((r) => r.name).sort()).toEqual( - namesWesteros.sort(), - ); + // Assert + expect(fspInstructionsResponse.statusCode).toBe(HttpStatus.OK); - // Check if the right columns are exported - const columnsInFspInstructions = Object.keys(fspInstructions[0]); - expect(columnsInFspInstructions.sort()).toEqual(columns.sort()); + const fspInstructions = fspInstructionsResponse.body; - // Assert if the values are correct - for (const row of fspInstructions) { - const registration = registrationsWesteros.find( - (r) => r.name === row.name, - )!; - for (const [key, value] of Object.entries(row)) { - expect(registration).toBeDefined(); - if (key === 'amount') { - const multipliedAmount = amount * (registration.dragon + 1); - expect(value).toBe(multipliedAmount); - } else { - expect(value).toEqual(String(registration[key])); - } - } - } + expect(fspInstructions).toMatchSnapshot(); }); }); describe('Import FSP reconciliation data', () => { it('Should update transaction status based on imported reconciliation data', async () => { // Arrange - const matchColumn = 'phoneNumber'; + const matchColumn = FinancialServiceProviderAttributes.phoneNumber; // construct reconciliation-file here const reconciliationData = [ { @@ -258,6 +200,10 @@ describe('Do payment with Excel FSP', () => { [matchColumn]: registrationWesteros2.phoneNumber, status: TransactionStatusEnum.error, }, + { + [matchColumn]: registrationWesteros3.phoneNumber, + status: TransactionStatusEnum.success, + }, { [matchColumn]: '123456789', status: TransactionStatusEnum.error }, ]; @@ -288,7 +234,9 @@ describe('Do payment with Excel FSP', () => { // Check per import record if it is imported or not found for (const importResultRecord of importResultRecords) { if (phoneNumbersWesteros.includes(importResultRecord[matchColumn])) { - expect(importResultRecord.importStatus).toBe(ImportStatus.imported); + expect(importResultRecord.importStatus).not.toBe( + ImportStatus.notFound, + ); } else { expect(importResultRecord.importStatus).toBe(ImportStatus.notFound); } @@ -313,5 +261,26 @@ describe('Do payment with Excel FSP', () => { expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.sort()).toMatchSnapshot(); }); + + it('Should give an error when status column is missing', async () => { + // Arrange + const matchColumn = FinancialServiceProviderAttributes.phoneNumber; + // construct reconciliation-file here + const reconciliationData = [ + { + [matchColumn]: registrationWesteros1.phoneNumber, + }, + ]; + + // Act + const importResult = await importFspReconciliationData( + programIdWesteros, + paymentNr, + accessToken, + reconciliationData, + ); + expect(importResult.statusCode).toBe(HttpStatus.NOT_FOUND); + expect(importResult.body).toMatchSnapshot(); + }); }); }); diff --git a/services/121-service/test/payment/do-payment-safaricom.test.ts b/services/121-service/test/payment/do-payment-fsp-safaricom.test.ts similarity index 87% rename from services/121-service/test/payment/do-payment-safaricom.test.ts rename to services/121-service/test/payment/do-payment-fsp-safaricom.test.ts index c527653a53..299f49986d 100644 --- a/services/121-service/test/payment/do-payment-safaricom.test.ts +++ b/services/121-service/test/payment/do-payment-fsp-safaricom.test.ts @@ -30,7 +30,8 @@ describe('Do payment to 1 PA', () => { const amount = 12327; const registrationSafaricom = { referenceId: '01dc9451-1273-484c-b2e8-ae21b51a96ab', - fspName: FinancialServiceProviders.safaricom, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.safaricom, phoneNumber: '254708374149', preferredLanguage: LanguageEnum.en, paymentAmountMultiplier: 1, @@ -40,22 +41,22 @@ describe('Do payment to 1 PA', () => { age: 25, maritalStatus: 'married', registrationType: 'self', - nationalId: 32121321, + nationalId: '32121321', nameAlternate: 'test', - totalHH: 56, + totalHH: '56', totalSub5: 1, totalAbove60: 1, otherSocialAssistance: 'no', county: 'ethiopia', subCounty: 'ethiopia', ward: 'dsa', - location: 21, - subLocation: 2113, + location: '21', + subLocation: '2113', village: 'adis abea', - nearestSchool: 213321, + nearestSchool: '213321', areaType: 'urban', mainSourceLivelihood: 'salary_from_formal_employment', - mainSourceLivelihoodOther: 213, + mainSourceLivelihoodOther: '213', Male05: 1, Female05: 0, Male612: 0, @@ -77,59 +78,59 @@ describe('Do payment to 1 PA', () => { habitableRooms: 0, tenureStatusOfDwelling: 'Owner occupied', ownerOccupiedState: 'purchased', - ownerOccupiedStateOther: 0, + ownerOccupiedStateOther: '0', rentedFrom: 'individual', - rentedFromOther: 0, + rentedFromOther: '0', constructionMaterialRoof: 'tin', - ifRoofOtherSpecify: 31213, + ifRoofOtherSpecify: '31213', constructionMaterialWall: 'tiles', - ifWallOtherSpecify: 231312, + ifWallOtherSpecify: '231312', constructionMaterialFloor: 'cement', ifFloorOtherSpecify: 'asdsd', dwellingRisk: 'fire', - ifRiskOtherSpecify: 123213, + ifRiskOtherSpecify: '123213', mainSourceOfWater: 'lake', ifWaterOtherSpecify: 'dasdas', pigs: 'no', - ifYesPigs: 123123, + ifYesPigs: '123123', chicken: 'no', mainModeHumanWasteDisposal: 'septic_tank', - ifHumanWasteOtherSpecify: 31213, + ifHumanWasteOtherSpecify: '31213', cookingFuel: 'electricity', ifFuelOtherSpecify: 'asdsda', Lighting: 'electricity', ifLightingOtherSpecify: 'dasasd', householdItems: 'none', excoticCattle: 'no', - ifYesExoticCattle: 12231123, + ifYesExoticCattle: '12231123', IndigenousCattle: 'no', - ifYesIndigenousCattle: 123132123, + ifYesIndigenousCattle: '123132123', sheep: 'no', - ifYesSheep: 12312312, + ifYesSheep: '12312312', goats: 'no', - ifYesGoats: 312123, + ifYesGoats: '312123', camels: 'no', - ifYesCamels: 312123, + ifYesCamels: '312123', donkeys: 'no', - ifYesDonkeys: 213312, - ifYesChicken: 2, - howManyBirths: 0, - howManyDeaths: 0, + ifYesDonkeys: '213312', + ifYesChicken: '2', + howManyBirths: '0', + howManyDeaths: '0', householdConditions: 'poor', skipMeals: 'no', - receivingBenefits: 0, - ifYesNameProgramme: 0, + receivingBenefits: '0', + ifYesNameProgramme: '0', typeOfBenefit: 'in_kind', - ifOtherBenefit: 2123312, - ifCash: 12312, - ifInKind: 132132, + ifOtherBenefit: '2123312', + ifCash: '12312', + ifInKind: '132132', feedbackOnRespons: 'no', - ifYesFeedback: 312123, + ifYesFeedback: '312123', whoDecidesHowToSpend: 'male_household_head', possibilityForConflicts: 'no', genderedDivision: 'no', ifYesElaborate: 'asddas', - geopoint: 123231, + geopoint: '123231', }; describe('with FSP: Safaricom', () => { @@ -201,17 +202,12 @@ describe('Do payment to 1 PA', () => { expect(getTransactionsBody.body[0].errorMessage).toBe(null); }); - it('should give error about phoneNumber', async () => { - const program = { - allowEmptyPhoneNumber: true, - }; - + it('should give error the initial safaricom api call', async () => { // Act // Call the update function - await patchProgram(2, program as UpdateProgramDto, accessToken); // Arrange - registrationSafaricom.phoneNumber = ''; + registrationSafaricom.phoneNumber = '254000000000'; await importRegistrations( programId, [registrationSafaricom], @@ -258,21 +254,16 @@ describe('Do payment to 1 PA', () => { TransactionStatusEnum.error, ); expect(getTransactionsBody.body[0].errorMessage).toBe( - 'Property phoneNumber is undefined', + '401.002.01 - Error Occurred - Invalid Access Token - mocked_access_token', ); }); - it('should successfully retry pay-out after empty phoneNumber error', async () => { - const program = { - allowEmptyPhoneNumber: true, - }; - + it('should successfully retry pay-out after an initial failure', async () => { // Act // Call the update function - await patchProgram(2, program as UpdateProgramDto, accessToken); // Arrange - registrationSafaricom.phoneNumber = ''; + registrationSafaricom.phoneNumber = '254000000000'; await importRegistrations( programId, [registrationSafaricom], diff --git a/services/121-service/test/payment/do-payment-fsp-visa-debit/do-payment-fsp-visa-debit-error.test.ts b/services/121-service/test/payment/do-payment-fsp-visa-debit/do-payment-fsp-visa-debit-error.test.ts index 5347166543..5ba9e77613 100644 --- a/services/121-service/test/payment/do-payment-fsp-visa-debit/do-payment-fsp-visa-debit-error.test.ts +++ b/services/121-service/test/payment/do-payment-fsp-visa-debit/do-payment-fsp-visa-debit-error.test.ts @@ -1,6 +1,9 @@ import { HttpStatus } from '@nestjs/common'; -import { FinancialServiceProviderConfigurationEnum } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { IntersolveVisa121ErrorText } from '@121-service/src/payments/fsp-integration/intersolve-visa/enums/intersolve-visa-121-error-text.enum'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; @@ -13,12 +16,11 @@ import { } from '@121-service/src/seed-data/mock/visa-card.data'; import { waitFor } from '@121-service/src/utils/waitFor.helper'; import { - deleteFspConfiguration, doPayment, - getFspConfiguration, getTransactions, waitForPaymentTransactionsToComplete, } from '@121-service/test/helpers/program.helper'; +import { deleteProgramFinancialServiceProviderConfigurationProperty } from '@121-service/test/helpers/program-financial-service-provider-configuration.helper'; import { awaitChangePaStatus, importRegistrations, @@ -291,7 +293,7 @@ describe('Do failing payment with FSP Visa Debit', () => { ); }); - it('should fail pay-out by visa debit if coverletterCode is not configured for the program', async () => { + it('should fail pay-out by visa debit if coverletterCode or fundingToken is not configured for the program', async () => { // Arrange await importRegistrations(programIdVisa, [registrationVisa], accessToken); await awaitChangePaStatus( @@ -302,17 +304,20 @@ describe('Do failing payment with FSP Visa Debit', () => { ); const paymentReferenceIds = [registrationVisa.referenceId]; - const fspConfig = await getFspConfiguration(programIdVisa, accessToken); - const coverLetterCodeForFspConfigRecord = fspConfig.body.find( - (fspConfig) => - fspConfig.name === - FinancialServiceProviderConfigurationEnum.coverLetterCode, - ); - await deleteFspConfiguration( - programIdVisa, - coverLetterCodeForFspConfigRecord.id, + await deleteProgramFinancialServiceProviderConfigurationProperty({ + programId: programIdVisa, + configName: FinancialServiceProviders.intersolveVisa, + propertyName: + FinancialServiceProviderConfigurationProperties.coverLetterCode, accessToken, - ); + }); + await deleteProgramFinancialServiceProviderConfigurationProperty({ + programId: programIdVisa, + configName: FinancialServiceProviders.intersolveVisa, + propertyName: + FinancialServiceProviderConfigurationProperties.fundingTokenCode, + accessToken, + }); // Act const doPaymentResponse = await doPayment( @@ -324,6 +329,13 @@ describe('Do failing payment with FSP Visa Debit', () => { ); expect(doPaymentResponse.status).toBe(HttpStatus.BAD_REQUEST); + // Check if both properties are mentioned in the error message + expect(doPaymentResponse.body.message).toContain( + FinancialServiceProviderConfigurationProperties.coverLetterCode, + ); + expect(doPaymentResponse.body.message).toContain( + FinancialServiceProviderConfigurationProperties.fundingTokenCode, + ); }); it('should show a failed transaction if an idempotency key is duplicate', async () => { diff --git a/services/121-service/test/payment/do-payment-fsp-voucher.test.ts b/services/121-service/test/payment/do-payment-fsp-voucher.test.ts index e5aa0cf00d..5517d316df 100644 --- a/services/121-service/test/payment/do-payment-fsp-voucher.test.ts +++ b/services/121-service/test/payment/do-payment-fsp-voucher.test.ts @@ -34,7 +34,8 @@ describe('Do payment to 1 PA', () => { nameFirst: 'John', nameLast: 'Smith', phoneNumber: '14155238886', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, whatsappPhoneNumber: '14155238886', }; diff --git a/services/121-service/test/payment/payment-count-completed.test.ts b/services/121-service/test/payment/payment-count-completed.test.ts index 1dcde816d8..ec24513e6e 100644 --- a/services/121-service/test/payment/payment-count-completed.test.ts +++ b/services/121-service/test/payment/payment-count-completed.test.ts @@ -34,7 +34,8 @@ describe('Do a payment to a PA with maxPayments=1', () => { nameFirst: 'John', nameLast: 'Smith', phoneNumber: '14155238886', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, whatsappPhoneNumber: '14155238886', maxPayments: 1, }; diff --git a/services/121-service/test/payment/payment-in-progress.test.ts b/services/121-service/test/payment/payment-in-progress.test.ts index 74fabeb072..534dc98a85 100644 --- a/services/121-service/test/payment/payment-in-progress.test.ts +++ b/services/121-service/test/payment/payment-in-progress.test.ts @@ -29,7 +29,9 @@ describe('Payment in progress', () => { ); const registrationsVisaOcw = registrationsOCW.filter( - (r) => r.fspName === FinancialServiceProviders.intersolveVisa, + (r) => + r.programFinancialServiceProviderConfigurationName === + FinancialServiceProviders.intersolveVisa, ); // Create a registration with a different referenceId from OCW registrations as the default ones from PV have no VISA const registrationsVisaPV = [ diff --git a/services/121-service/test/program/__snapshots__/create-fsp-configuration.test.ts.snap b/services/121-service/test/program/__snapshots__/create-fsp-configuration.test.ts.snap deleted file mode 100644 index 86e92539a0..0000000000 --- a/services/121-service/test/program/__snapshots__/create-fsp-configuration.test.ts.snap +++ /dev/null @@ -1,543 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Create program questions should create a program with FSP configuration: Create program response for program: NLRC OCW program 1`] = ` -{ - "aboutProgram": { - "en": "This is the OCW program.", - }, - "allowEmptyPhoneNumber": false, - "budget": null, - "currency": "EUR", - "description": { - "en": "", - }, - "distributionDuration": 46, - "distributionFrequency": "2-weeks", - "editableAttributes": [ - { - "label": { - "en": "Full Name", - }, - "name": "fullName", - "type": "text", - }, - ], - "enableMaxPayments": false, - "enableScope": false, - "filterableAttributes": [ - { - "filters": [ - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "lastMessageStatus", - }, - ], - "group": "messages", - }, - { - "filters": [ - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "addressCity", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "addressHouseNumber", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "addressHouseNumberAddition", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "addressPostalCode", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "addressStreet", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "financialServiceProvider", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "fullName", - }, - { - "allowedOperators": [ - "$eq", - "$null", - ], - "isInteger": true, - "name": "inclusionScore", - }, - { - "allowedOperators": [ - "$eq", - "$null", - ], - "isInteger": true, - "name": "paymentAmountMultiplier", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "personAffectedSequence", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "phoneNumber", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "preferredLanguage", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "referenceId", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "registrationCreatedDate", - }, - { - "allowedOperators": [ - "$eq", - "$in", - "$ilike", - "$null", - ], - "isInteger": false, - "name": "whatsappPhoneNumber", - }, - ], - "group": "paAttributes", - }, - { - "filters": [ - { - "allowedOperators": [ - "$eq", - "$null", - ], - "isInteger": true, - "name": "failedPayment", - }, - { - "allowedOperators": [ - "$eq", - "$null", - ], - "isInteger": true, - "name": "notYetSentPayment", - }, - { - "allowedOperators": [ - "$eq", - "$null", - ], - "isInteger": true, - "name": "paymentCount", - }, - { - "allowedOperators": [ - "$eq", - "$null", - ], - "isInteger": true, - "name": "successPayment", - }, - { - "allowedOperators": [ - "$eq", - "$null", - ], - "isInteger": true, - "name": "waitingPayment", - }, - ], - "group": "payments", - }, - ], - "financialServiceProviders": [ - { - "displayName": { - "en": "Visa debit card", - }, - "fsp": "Intersolve-visa", - "hasReconciliation": false, - "id": 3, - "integrationType": "api", - "notifyOnTransaction": true, - "questions": [ - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 6, - "label": { - "en": "Address city", - }, - "name": "addressCity", - "options": null, - "pattern": ".+", - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "numeric", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 3, - "label": { - "en": "Address house number", - }, - "name": "addressHouseNumber", - "options": null, - "pattern": null, - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 4, - "label": { - "en": "Address house number addition", - }, - "name": "addressHouseNumberAddition", - "options": null, - "pattern": null, - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 5, - "label": { - "en": "Address postal code", - }, - "name": "addressPostalCode", - "options": null, - "pattern": ".+", - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 2, - "label": { - "en": "Address street", - }, - "name": "addressStreet", - "options": null, - "pattern": ".+", - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "tel", - "duplicateCheck": true, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 7, - "label": { - "en": "WhatsApp Nr.", - }, - "name": "whatsappPhoneNumber", - "options": null, - "pattern": null, - "placeholder": { - "ar": "00 00 00 00 0 00+", - "en": "+00 0 00 00 00 00", - }, - "showInPeopleAffectedTable": false, - }, - ], - }, - { - "displayName": { - "en": "Intersolve Voucher WhatsApp", - "es": "Intersolve Voucher WhatsApp Spanish translation", - "nl": "Intersolve Voucher WhatsApp Dutch translation", - }, - "fsp": "Intersolve-voucher-whatsapp", - "hasReconciliation": false, - "id": 1, - "integrationType": "api", - "notifyOnTransaction": false, - "questions": [ - { - "answerType": "tel", - "duplicateCheck": true, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 1, - "id": 1, - "label": { - "en": "WhatsApp Nr.", - }, - "name": "whatsappPhoneNumber", - "options": null, - "pattern": null, - "placeholder": { - "ar": "00 00 00 00 0 00+", - "en": "+00 0 00 00 00 00", - }, - "showInPeopleAffectedTable": true, - }, - ], - }, - ], - "fixedTransferValue": 25, - "fullnameNamingConvention": [ - "fullName", - ], - "id": 4, - "languages": [ - "en", - "nl", - "ar", - "tr", - ], - "location": "Nederland", - "monitoringDashboardUrl": "https://app.powerbi.com/view?r=eyJrIjoiNDkxN2Q1NmEtOWIxOC00ZTYyLTkxMTMtYmI3MmE4YTVkMTE2IiwidCI6ImEyYjUzYmU1LTczNGUtNGU2Yy1hYjBkLWQxODRmNjBmZDkxNyIsImMiOjh9", - "ngo": "NLRC", - "paTableAttributes": [ - { - "label": { - "en": "Address city", - }, - "name": "addressCity", - "questionType": "fspQuestion", - "type": "text", - }, - { - "label": { - "en": "Address house number", - }, - "name": "addressHouseNumber", - "questionType": "fspQuestion", - "type": "numeric", - }, - { - "label": { - "en": "Address house number addition", - }, - "name": "addressHouseNumberAddition", - "questionType": "fspQuestion", - "type": "text", - }, - { - "label": { - "en": "Address postal code", - }, - "name": "addressPostalCode", - "questionType": "fspQuestion", - "type": "text", - }, - { - "label": { - "en": "Address street", - }, - "name": "addressStreet", - "questionType": "fspQuestion", - "type": "text", - }, - { - "label": { - "en": "Full Name", - }, - "name": "fullName", - "questionType": "programQuestion", - "type": "text", - }, - { - "label": { - "en": "Phone Number", - }, - "name": "phoneNumber", - "questionType": "programQuestion", - "type": "tel", - }, - { - "label": { - "en": "WhatsApp Nr.", - }, - "name": "whatsappPhoneNumber", - "questionType": "fspQuestion", - "type": "tel", - }, - { - "label": { - "en": "WhatsApp Nr.", - }, - "name": "whatsappPhoneNumber", - "questionType": "fspQuestion", - "type": "tel", - }, - ], - "paymentAmountMultiplierFormula": null, - "programCustomAttributes": [], - "programQuestions": [ - { - "answerType": "text", - "duplicateCheck": false, - "editableInPortal": true, - "export": [ - "all-people-affected", - "included", - ], - "id": 5, - "label": { - "en": "Full Name", - }, - "name": "fullName", - "options": null, - "pattern": null, - "persistence": true, - "placeholder": null, - "programId": 4, - "questionType": "standard", - "scoring": {}, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "tel", - "duplicateCheck": true, - "editableInPortal": false, - "export": [], - "id": 6, - "label": { - "en": "Phone Number", - }, - "name": "phoneNumber", - "options": null, - "pattern": null, - "persistence": true, - "placeholder": { - "en": "+31 6 00 00 00 00", - }, - "programId": 4, - "questionType": "standard", - "scoring": {}, - "showInPeopleAffectedTable": false, - }, - ], - "published": true, - "targetNrRegistrations": 100000, - "titlePortal": { - "en": "NLRC OCW program", - }, - "tryWhatsAppFirst": true, - "validation": false, -} -`; diff --git a/services/121-service/test/program/__snapshots__/create-program.test.ts.snap b/services/121-service/test/program/__snapshots__/create-program.test.ts.snap index a9505def26..0df29bed3e 100644 --- a/services/121-service/test/program/__snapshots__/create-program.test.ts.snap +++ b/services/121-service/test/program/__snapshots__/create-program.test.ts.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Create program should not be able to post a program with 2 of the same names 1`] = ` +{ + "errors": "The following names: 'fullName' are used more than once program registration attributes", +} +`; + +exports[`Create program should not be able to post a program with missing names of full name naming convention 1`] = ` +{ + "errors": "Element 'middle_name' of fullnameNamingConvention is not found in program registration attributes", +} +`; + exports[`Create program should post a program: Create program response for program: DRA Joint Response 2023 Ethiopia - Dorcas 1`] = ` { "aboutProgram": { @@ -15,6 +27,7 @@ exports[`Create program should post a program: Create program response for progr "distributionFrequency": "month", "editableAttributes": [ { + "isRequired": false, "label": { "en": "Age", "et_AM": "እድሜ", @@ -23,6 +36,15 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, + "label": { + "en": "Bank Account Number", + }, + "name": "bankAccountNumber", + "type": "text", + }, + { + "isRequired": false, "label": { "en": "Name", "et_AM": "ስም", @@ -31,6 +53,7 @@ exports[`Create program should post a program: Create program response for progr "type": "text", }, { + "isRequired": false, "label": { "en": "Gender", "et_AM": "ጾታ", @@ -39,6 +62,7 @@ exports[`Create program should post a program: Create program response for progr "type": "dropdown", }, { + "isRequired": false, "label": { "en": "How many female?", "et_AM": "ሴት አባላት", @@ -47,6 +71,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female with disability over 18", "et_AM": "ሴት ከ18 ዓመት በላይ አካል ጉዳተኛ", @@ -55,6 +80,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female with disability under 18", "et_AM": "ሴት ከ18 ዓመት በታች አካል ጉዳተኛ", @@ -63,6 +89,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female over 18", "et_AM": "ሴት ከ18 ዓመት በላይ", @@ -71,6 +98,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female under 18", "et_AM": "ሴት ከ18 ዓመት በታች", @@ -79,6 +107,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "How many male?", "et_AM": "ወንድ አባላት", @@ -87,6 +116,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male with disability over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ አካል ጉዳተኛ", @@ -95,6 +125,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male with disability under 18", "et_AM": "ወንድ ከ18 ዓመት በታች አካል ጉዳተኛ", @@ -103,6 +134,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ", @@ -111,6 +143,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male under 18", "et_AM": "ወንድ ከ18 ዓመት በታች", @@ -119,6 +152,7 @@ exports[`Create program should post a program: Create program response for progr "type": "numeric", }, { + "isRequired": false, "label": { "en": "ID Card number", "et_AM": "የመታውቂያ ቁጥር", @@ -127,6 +161,7 @@ exports[`Create program should post a program: Create program response for progr "type": "text", }, { + "isRequired": false, "label": { "en": "Phone number", "et_AM": "ስልክ ቁጥር", @@ -135,6 +170,7 @@ exports[`Create program should post a program: Create program response for progr "type": "tel", }, { + "isRequired": false, "label": { "en": "Total family members", "et_AM": "በቤተሰብዎ ውስጥ ያሉት አባላት", @@ -464,40 +500,6 @@ exports[`Create program should post a program: Create program response for progr "group": "payments", }, ], - "financialServiceProviders": [ - { - "displayName": { - "en": "Commercial Bank of Ethiopia", - }, - "fsp": "Commercial-bank-ethiopia", - "hasReconciliation": false, - "id": 5, - "integrationType": "api", - "notifyOnTransaction": false, - "questions": [ - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 5, - "id": 9, - "label": { - "en": "Bank Account Number", - }, - "name": "bankAccountNumber", - "options": null, - "pattern": ".+", - "placeholder": { - "en": "Please type your bank account number", - }, - "showInPeopleAffectedTable": true, - }, - ], - }, - ], "fixedTransferValue": 7000, "fullnameNamingConvention": [ "fullName", @@ -512,170 +514,169 @@ exports[`Create program should post a program: Create program response for progr "ngo": "Dorcas", "paTableAttributes": [ { + "isRequired": false, "label": { "en": "Age", "et_AM": "እድሜ", }, "name": "age", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Bank Account Number", }, "name": "bankAccountNumber", - "questionType": "fspQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Name", "et_AM": "ስም", }, "name": "fullName", - "questionType": "programQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Gender", "et_AM": "ጾታ", }, "name": "gender", - "questionType": "programQuestion", "type": "dropdown", }, { + "isRequired": false, "label": { "en": "How many female?", "et_AM": "ሴት አባላት", }, "name": "howManyFemale", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female with disability over 18", "et_AM": "ሴት ከ18 ዓመት በላይ አካል ጉዳተኛ", }, "name": "howManyFemaleDisabilityOver18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female with disability under 18", "et_AM": "ሴት ከ18 ዓመት በታች አካል ጉዳተኛ", }, "name": "howManyFemaleDisabilityUnder18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female over 18", "et_AM": "ሴት ከ18 ዓመት በላይ", }, "name": "howManyFemaleOver18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Female under 18", "et_AM": "ሴት ከ18 ዓመት በታች", }, "name": "howManyFemaleUnder18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "How many male?", "et_AM": "ወንድ አባላት", }, "name": "howManyMale", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male with disability over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ አካል ጉዳተኛ", }, "name": "howManyMaleDisabilityOver18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male with disability under 18", "et_AM": "ወንድ ከ18 ዓመት በታች አካል ጉዳተኛ", }, "name": "howManyMaleDisabilityUnder18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ", }, "name": "howManyMaleOver18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Male under 18", "et_AM": "ወንድ ከ18 ዓመት በታች", }, "name": "howManyMaleUnder18", - "questionType": "programQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "ID Card number", "et_AM": "የመታውቂያ ቁጥር", }, "name": "idNumber", - "questionType": "programQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Phone number", "et_AM": "ስልክ ቁጥር", }, "name": "phoneNumber", - "questionType": "programQuestion", "type": "tel", }, { + "isRequired": false, "label": { "en": "Total family members", "et_AM": "በቤተሰብዎ ውስጥ ያሉት አባላት", }, "name": "totalFamilyMembers", - "questionType": "programQuestion", "type": "numeric", }, ], "paymentAmountMultiplierFormula": null, - "programCustomAttributes": [], - "programQuestions": [ + "programRegistrationAttributes": [ { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 10, + "id": 29, + "isRequired": false, "label": { "en": "Age", "et_AM": "እድሜ", @@ -683,22 +684,44 @@ exports[`Create program should post a program: Create program response for progr "name": "age", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", + }, + { + "duplicateCheck": false, + "editableInPortal": false, + "export": [ + "all-people-affected", + "included", + ], + "id": 42, + "isRequired": false, + "label": { + "en": "Bank Account Number", + }, + "name": "bankAccountNumber", + "options": null, + "pattern": ".+", + "placeholder": { + "en": "Please type your bank account number", + }, + "programId": 5, + "scoring": {}, + "showInPeopleAffectedTable": true, + "type": "text", }, { - "answerType": "text", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 7, + "id": 26, + "isRequired": false, "label": { "en": "Name", "et_AM": "ስም", @@ -706,22 +729,21 @@ exports[`Create program should post a program: Create program response for progr "name": "fullName", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "text", }, { - "answerType": "dropdown", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 11, + "id": 30, + "isRequired": false, "label": { "en": "Gender", "et_AM": "ጾታ", @@ -744,22 +766,21 @@ exports[`Create program should post a program: Create program response for progr }, ], "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "dropdown", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 12, + "id": 31, + "isRequired": false, "label": { "en": "How many female?", "et_AM": "ሴት አባላት", @@ -767,22 +788,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyFemale", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 21, + "id": 40, + "isRequired": false, "label": { "en": "Female with disability over 18", "et_AM": "ሴት ከ18 ዓመት በላይ አካል ጉዳተኛ", @@ -790,22 +810,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyFemaleDisabilityOver18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 19, + "id": 38, + "isRequired": false, "label": { "en": "Female with disability under 18", "et_AM": "ሴት ከ18 ዓመት በታች አካል ጉዳተኛ", @@ -813,22 +832,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyFemaleDisabilityUnder18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 17, + "id": 36, + "isRequired": false, "label": { "en": "Female over 18", "et_AM": "ሴት ከ18 ዓመት በላይ", @@ -836,22 +854,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyFemaleOver18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 15, + "id": 34, + "isRequired": false, "label": { "en": "Female under 18", "et_AM": "ሴት ከ18 ዓመት በታች", @@ -859,22 +876,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyFemaleUnder18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 13, + "id": 32, + "isRequired": false, "label": { "en": "How many male?", "et_AM": "ወንድ አባላት", @@ -882,22 +898,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyMale", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 22, + "id": 41, + "isRequired": false, "label": { "en": "Male with disability over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ አካል ጉዳተኛ", @@ -905,22 +920,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyMaleDisabilityOver18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 20, + "id": 39, + "isRequired": false, "label": { "en": "Male with disability under 18", "et_AM": "ወንድ ከ18 ዓመት በታች አካል ጉዳተኛ", @@ -928,22 +942,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyMaleDisabilityUnder18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 18, + "id": 37, + "isRequired": false, "label": { "en": "Male over 18", "et_AM": "ወንድ ከ18 ዓመት በላይ", @@ -951,22 +964,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyMaleOver18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 16, + "id": 35, + "isRequired": false, "label": { "en": "Male under 18", "et_AM": "ወንድ ከ18 ዓመት በታች", @@ -974,22 +986,21 @@ exports[`Create program should post a program: Create program response for progr "name": "howManyMaleUnder18", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, { - "answerType": "text", "duplicateCheck": true, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 9, + "id": 28, + "isRequired": false, "label": { "en": "ID Card number", "et_AM": "የመታውቂያ ቁጥር", @@ -997,22 +1008,21 @@ exports[`Create program should post a program: Create program response for progr "name": "idNumber", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "text", }, { - "answerType": "tel", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 8, + "id": 27, + "isRequired": false, "label": { "en": "Phone number", "et_AM": "ስልክ ቁጥር", @@ -1020,22 +1030,21 @@ exports[`Create program should post a program: Create program response for progr "name": "phoneNumber", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "tel", }, { - "answerType": "numeric", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 14, + "id": 33, + "isRequired": false, "label": { "en": "Total family members", "et_AM": "በቤተሰብዎ ውስጥ ያሉት አባላት", @@ -1043,12 +1052,11 @@ exports[`Create program should post a program: Create program response for progr "name": "totalFamilyMembers", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 5, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "numeric", }, ], "published": true, @@ -1077,12 +1085,69 @@ exports[`Create program should post a program: Create program response for progr "distributionFrequency": "2-weeks", "editableAttributes": [ { + "isRequired": false, + "label": { + "en": "Address city", + }, + "name": "addressCity", + "type": "text", + }, + { + "isRequired": false, + "label": { + "en": "Address house number", + }, + "name": "addressHouseNumber", + "type": "numeric", + }, + { + "isRequired": false, + "label": { + "en": "Address house number addition", + }, + "name": "addressHouseNumberAddition", + "type": "text", + }, + { + "isRequired": false, + "label": { + "en": "Address postal code", + }, + "name": "addressPostalCode", + "type": "text", + }, + { + "isRequired": false, + "label": { + "en": "Address street", + }, + "name": "addressStreet", + "type": "text", + }, + { + "isRequired": false, "label": { "en": "Full Name", }, "name": "fullName", "type": "text", }, + { + "isRequired": false, + "label": { + "en": "Phone Number", + }, + "name": "phoneNumber", + "type": "tel", + }, + { + "isRequired": false, + "label": { + "en": "WhatsApp Nr.", + }, + "name": "whatsappPhoneNumber", + "type": "tel", + }, ], "enableMaxPayments": false, "enableScope": false, @@ -1299,164 +1364,6 @@ exports[`Create program should post a program: Create program response for progr "group": "payments", }, ], - "financialServiceProviders": [ - { - "displayName": { - "en": "Visa debit card", - }, - "fsp": "Intersolve-visa", - "hasReconciliation": false, - "id": 3, - "integrationType": "api", - "notifyOnTransaction": true, - "questions": [ - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 6, - "label": { - "en": "Address city", - }, - "name": "addressCity", - "options": null, - "pattern": ".+", - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "numeric", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 3, - "label": { - "en": "Address house number", - }, - "name": "addressHouseNumber", - "options": null, - "pattern": null, - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 4, - "label": { - "en": "Address house number addition", - }, - "name": "addressHouseNumberAddition", - "options": null, - "pattern": null, - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 5, - "label": { - "en": "Address postal code", - }, - "name": "addressPostalCode", - "options": null, - "pattern": ".+", - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "text", - "duplicateCheck": false, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 2, - "label": { - "en": "Address street", - }, - "name": "addressStreet", - "options": null, - "pattern": ".+", - "placeholder": null, - "showInPeopleAffectedTable": false, - }, - { - "answerType": "tel", - "duplicateCheck": true, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 3, - "id": 7, - "label": { - "en": "WhatsApp Nr.", - }, - "name": "whatsappPhoneNumber", - "options": null, - "pattern": null, - "placeholder": { - "ar": "00 00 00 00 0 00+", - "en": "+00 0 00 00 00 00", - }, - "showInPeopleAffectedTable": false, - }, - ], - }, - { - "displayName": { - "en": "Albert Heijn voucher WhatsApp", - }, - "fsp": "Intersolve-voucher-whatsapp", - "hasReconciliation": false, - "id": 1, - "integrationType": "api", - "notifyOnTransaction": false, - "questions": [ - { - "answerType": "tel", - "duplicateCheck": true, - "export": [ - "all-people-affected", - "included", - ], - "fspId": 1, - "id": 1, - "label": { - "en": "WhatsApp Nr.", - }, - "name": "whatsappPhoneNumber", - "options": null, - "pattern": null, - "placeholder": { - "ar": "00 00 00 00 0 00+", - "en": "+00 0 00 00 00 00", - }, - "showInPeopleAffectedTable": true, - }, - ], - }, - ], "fixedTransferValue": 25, "fullnameNamingConvention": [ "fullName", @@ -1473,123 +1380,241 @@ exports[`Create program should post a program: Create program response for progr "ngo": "NLRC", "paTableAttributes": [ { + "isRequired": false, "label": { "en": "Address city", }, "name": "addressCity", - "questionType": "fspQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Address house number", }, "name": "addressHouseNumber", - "questionType": "fspQuestion", "type": "numeric", }, { + "isRequired": false, "label": { "en": "Address house number addition", }, "name": "addressHouseNumberAddition", - "questionType": "fspQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Address postal code", }, "name": "addressPostalCode", - "questionType": "fspQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Address street", }, "name": "addressStreet", - "questionType": "fspQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Full Name", }, "name": "fullName", - "questionType": "programQuestion", "type": "text", }, { + "isRequired": false, "label": { "en": "Phone Number", }, "name": "phoneNumber", - "questionType": "programQuestion", "type": "tel", }, { + "isRequired": false, "label": { "en": "WhatsApp Nr.", }, "name": "whatsappPhoneNumber", - "questionType": "fspQuestion", "type": "tel", }, + ], + "paymentAmountMultiplierFormula": null, + "programRegistrationAttributes": [ { + "duplicateCheck": false, + "editableInPortal": false, + "export": [ + "all-people-affected", + "included", + ], + "id": 25, + "isRequired": false, "label": { - "en": "WhatsApp Nr.", + "en": "Address city", }, - "name": "whatsappPhoneNumber", - "questionType": "fspQuestion", - "type": "tel", + "name": "addressCity", + "options": null, + "pattern": null, + "placeholder": null, + "programId": 4, + "scoring": {}, + "showInPeopleAffectedTable": false, + "type": "text", + }, + { + "duplicateCheck": false, + "editableInPortal": false, + "export": [ + "all-people-affected", + "included", + ], + "id": 22, + "isRequired": false, + "label": { + "en": "Address house number", + }, + "name": "addressHouseNumber", + "options": null, + "pattern": null, + "placeholder": null, + "programId": 4, + "scoring": {}, + "showInPeopleAffectedTable": false, + "type": "numeric", + }, + { + "duplicateCheck": false, + "editableInPortal": false, + "export": [ + "all-people-affected", + "included", + ], + "id": 23, + "isRequired": false, + "label": { + "en": "Address house number addition", + }, + "name": "addressHouseNumberAddition", + "options": null, + "pattern": null, + "placeholder": null, + "programId": 4, + "scoring": {}, + "showInPeopleAffectedTable": false, + "type": "text", + }, + { + "duplicateCheck": false, + "editableInPortal": false, + "export": [ + "all-people-affected", + "included", + ], + "id": 24, + "isRequired": false, + "label": { + "en": "Address postal code", + }, + "name": "addressPostalCode", + "options": null, + "pattern": null, + "placeholder": null, + "programId": 4, + "scoring": {}, + "showInPeopleAffectedTable": false, + "type": "text", + }, + { + "duplicateCheck": false, + "editableInPortal": false, + "export": [ + "all-people-affected", + "included", + ], + "id": 21, + "isRequired": false, + "label": { + "en": "Address street", + }, + "name": "addressStreet", + "options": null, + "pattern": null, + "placeholder": null, + "programId": 4, + "scoring": {}, + "showInPeopleAffectedTable": false, + "type": "text", }, - ], - "paymentAmountMultiplierFormula": null, - "programCustomAttributes": [], - "programQuestions": [ { - "answerType": "text", "duplicateCheck": false, "editableInPortal": true, "export": [ "all-people-affected", "included", ], - "id": 5, + "id": 18, + "isRequired": false, "label": { "en": "Full Name", }, "name": "fullName", "options": null, "pattern": null, - "persistence": true, "placeholder": null, "programId": 4, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "text", }, { - "answerType": "tel", "duplicateCheck": true, "editableInPortal": false, "export": [], - "id": 6, + "id": 19, + "isRequired": false, "label": { "en": "Phone Number", }, "name": "phoneNumber", "options": null, "pattern": null, - "persistence": true, "placeholder": { "en": "+31 6 00 00 00 00", }, "programId": 4, - "questionType": "standard", "scoring": {}, "showInPeopleAffectedTable": false, + "type": "tel", + }, + { + "duplicateCheck": true, + "editableInPortal": false, + "export": [ + "all-people-affected", + "included", + ], + "id": 20, + "isRequired": false, + "label": { + "en": "WhatsApp Nr.", + }, + "name": "whatsappPhoneNumber", + "options": null, + "pattern": null, + "placeholder": { + "ar": "00 00 00 00 0 00+", + "en": "+00 0 00 00 00 00", + }, + "programId": 4, + "scoring": {}, + "showInPeopleAffectedTable": true, + "type": "tel", }, ], "published": true, diff --git a/services/121-service/test/program/create-custom-attribute.test.ts b/services/121-service/test/program/create-custom-attribute.test.ts deleted file mode 100644 index 095c7d2e2d..0000000000 --- a/services/121-service/test/program/create-custom-attribute.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable jest/no-conditional-expect */ -import { HttpStatus } from '@nestjs/common'; - -import { CreateProgramCustomAttributeDto } from '@121-service/src/programs/dto/create-program-custom-attribute.dto'; -import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import { postCustomAttribute } from '@121-service/test/helpers/program.helper'; -import { - getAccessToken, - resetDB, -} from '@121-service/test/helpers/utility.helper'; -import { programIdPV } from '@121-service/test/registrations/pagination/pagination-data'; - -describe('Create program custom attributes', () => { - let accessToken: string; - - const customAttribute = { - name: 'district', - type: 'text', - label: { - en: 'District', - fr: 'Département', - }, - showInPeopleAffectedTable: true, - duplicateCheck: true, - }; - - beforeEach(async () => { - await resetDB(SeedScript.nlrcMultiple); - accessToken = await getAccessToken(); - }); - - it('should post a program attributes', async () => { - // Act - const createReponse = await postCustomAttribute( - customAttribute as CreateProgramCustomAttributeDto, - programIdPV, - accessToken, - ); - - // Assert - expect(createReponse.statusCode).toBe(HttpStatus.CREATED); - }); - - it('should no be able to post a attribute with a name that already exists', async () => { - // Arrange - await postCustomAttribute( - customAttribute as CreateProgramCustomAttributeDto, - programIdPV, - accessToken, - ); - // Act - const createReponse2 = await postCustomAttribute( - customAttribute as CreateProgramCustomAttributeDto, - programIdPV, - accessToken, - ); - // Assert - expect(createReponse2.statusCode).toBe(HttpStatus.BAD_REQUEST); - }); - - it('should no be able to post a attribute without obligatory attributes', async () => { - // Arrange - const requiredAttributes = ['name', 'type', 'label']; - for (const attribute of requiredAttributes) { - const programAttributeCopy: Partial = { - ...customAttribute, - }; - delete programAttributeCopy[attribute as keyof typeof customAttribute]; - - const createReponse = await postCustomAttribute( - programAttributeCopy as CreateProgramCustomAttributeDto, - programIdPV, - accessToken, - ); - // Assert - expect(createReponse.statusCode).toBe(HttpStatus.BAD_REQUEST); - } - }); - - it('should no be able to post a attribute with a fsp/program question that exists', async () => { - // Arrange - const names = ['fullName', 'whatsappPhoneNumber']; - for (const name of names) { - const programAttributeCopy = { ...customAttribute }; - programAttributeCopy.name = name; - - // Act - const createReponse = await postCustomAttribute( - programAttributeCopy as CreateProgramCustomAttributeDto, - programIdPV, - accessToken, - ); - // Assert - expect(createReponse.statusCode).toBe(HttpStatus.BAD_REQUEST); - } - }); -}); diff --git a/services/121-service/test/program/create-fsp-configuration.test.ts b/services/121-service/test/program/create-fsp-configuration.test.ts deleted file mode 100644 index f510a63e74..0000000000 --- a/services/121-service/test/program/create-fsp-configuration.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable jest/no-conditional-expect */ -import { HttpStatus } from '@nestjs/common'; - -import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import programOCW from '@121-service/src/seed-data/program/program-nlrc-ocw.json'; -import { - getProgram, - postProgram, -} from '@121-service/test/helpers/program.helper'; -import { - cleanProgramForAssertions, - getAccessToken, - resetDB, -} from '@121-service/test/helpers/utility.helper'; - -describe('Create program questions', () => { - let accessToken: string; - - beforeEach(async () => { - await resetDB(SeedScript.nlrcMultiple); - accessToken = await getAccessToken(); - }); - - it('should create a program with FSP configuration', async () => { - // Arrange - const program = JSON.parse(JSON.stringify(programOCW)); - - // Add test display name to program - const intersolveVoucherWhatsappTranslations = { - name: 'displayName', - value: { - en: 'Intersolve Voucher WhatsApp', - nl: 'Intersolve Voucher WhatsApp Dutch translation', - es: 'Intersolve Voucher WhatsApp Spanish translation', - }, - }; - - program.financialServiceProviders[0].configuration.push( - intersolveVoucherWhatsappTranslations, - ); - - // Act - const createProgramResponse = await postProgram(program, accessToken); - - // Assert - const programId = createProgramResponse.body.id; - const getProgramResponse = await getProgram(programId, accessToken); - expect(createProgramResponse.statusCode).toBe(HttpStatus.CREATED); - - const cleanedProgram = cleanProgramForAssertions(program); - const cleanedProgramResponse = cleanProgramForAssertions( - getProgramResponse.body, - ); - - expect(cleanedProgramResponse).toMatchSnapshot( - `Create program response for program: ${program.titlePortal.en}`, - ); - - expect(cleanedProgramResponse).toMatchObject(cleanedProgram); - }); -}); diff --git a/services/121-service/test/program/create-program-question.test.ts b/services/121-service/test/program/create-program-question.test.ts deleted file mode 100644 index 36eda64764..0000000000 --- a/services/121-service/test/program/create-program-question.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint-disable jest/no-conditional-expect */ -import { HttpStatus } from '@nestjs/common'; - -import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; -import { CreateProgramQuestionDto } from '@121-service/src/programs/dto/program-question.dto'; -import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import { postProgramQuestion } from '@121-service/test/helpers/program.helper'; -import { - getAccessToken, - resetDB, -} from '@121-service/test/helpers/utility.helper'; -import { programIdPV } from '@121-service/test/registrations/pagination/pagination-data'; - -describe('Create program', () => { - let accessToken: string; - - const programQuestion = { - name: 'string', - options: {}, - scoring: {}, - persistence: true, - pattern: 'string', - showInPeopleAffectedTable: true, - editableInPortal: true, - export: [ExportType.allPeopleAffected, ExportType.included], - label: { - en: 'Last Name', - }, - placeholder: { - en: '+31 6 00 00 00 00', - }, - duplicateCheck: false, - answerType: 'text', - questionType: 'standard', - }; - - beforeEach(async () => { - await resetDB(SeedScript.nlrcMultiple); - accessToken = await getAccessToken(); - }); - - it('should post a program questions', async () => { - // Act - const createReponse = await postProgramQuestion( - programQuestion as CreateProgramQuestionDto, - programIdPV, - accessToken, - ); - - // Assert - expect(createReponse.statusCode).toBe(HttpStatus.CREATED); - }); - - it('should no be able to post a question with a name that already exists', async () => { - // Arrange - await postProgramQuestion( - programQuestion as CreateProgramQuestionDto, - programIdPV, - accessToken, - ); - // Act - const createReponse2 = await postProgramQuestion( - programQuestion as CreateProgramQuestionDto, - programIdPV, - accessToken, - ); - // Assert - expect(createReponse2.statusCode).toBe(HttpStatus.BAD_REQUEST); - }); - - it('should no be able to post a question without obligatory attributes', async () => { - // Arrange - const requiredAttributes = ['name', 'questionType', 'label', 'answerType']; - for (const attribute of requiredAttributes) { - const programQuestionCopy: Partial = { - ...programQuestion, - }; - delete programQuestionCopy[attribute as keyof typeof programQuestion]; - - const createResponse = await postProgramQuestion( - programQuestionCopy as CreateProgramQuestionDto, - programIdPV, - accessToken, - ); - // Assert - expect(createResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); - } - }); - - it('should no be able to post a question with a fsp/custom attribute name that exists', async () => { - // Arrange - const names = ['namePartnerOrganization', 'whatsappPhoneNumber']; - for (const name of names) { - const programQuestionCopy = { ...programQuestion }; - programQuestionCopy.name = name; - - // Act - const createReponse = await postProgramQuestion( - programQuestionCopy as CreateProgramQuestionDto, - programIdPV, - accessToken, - ); - // Assert - expect(createReponse.statusCode).toBe(HttpStatus.BAD_REQUEST); - } - }); -}); diff --git a/services/121-service/test/program/create-program-registration-attribute.test.ts b/services/121-service/test/program/create-program-registration-attribute.test.ts new file mode 100644 index 0000000000..5d6d2d0d21 --- /dev/null +++ b/services/121-service/test/program/create-program-registration-attribute.test.ts @@ -0,0 +1,112 @@ +/* eslint-disable jest/no-conditional-expect */ +import { HttpStatus } from '@nestjs/common'; + +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; +import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; +import { ProgramRegistrationAttributeDto } from '@121-service/src/programs/dto/program-registration-attribute.dto'; +import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; +import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; +import { postProgramRegistrationAttribute } from '@121-service/test/helpers/program.helper'; +import { + getAccessToken, + resetDB, +} from '@121-service/test/helpers/utility.helper'; +import { programIdPV } from '@121-service/test/registrations/pagination/pagination-data'; + +describe('Create program', () => { + let accessToken: string; + + const programRegistrationAttribute: ProgramRegistrationAttributeDto = { + name: 'string', + options: [], + scoring: {}, + pattern: 'string', + showInPeopleAffectedTable: true, + editableInPortal: true, + export: [ExportType.allPeopleAffected, ExportType.included], + label: { + en: 'Last Name', + }, + placeholder: { + en: '+31 6 00 00 00 00', + }, + duplicateCheck: false, + type: RegistrationAttributeTypes.text, + isRequired: false, + }; + + beforeEach(async () => { + await resetDB(SeedScript.nlrcMultiple); + accessToken = await getAccessToken(); + }); + + it('should post a program registration attribute', async () => { + // Act + const createReponse = await postProgramRegistrationAttribute( + programRegistrationAttribute, + programIdPV, + accessToken, + ); + // Assert + expect(createReponse.statusCode).toBe(HttpStatus.CREATED); + }); + + it('should not be able to post a registration attributes with a name that already exists', async () => { + // Arrange + await postProgramRegistrationAttribute( + programRegistrationAttribute, + programIdPV, + accessToken, + ); + // Act + const createReponse2 = await postProgramRegistrationAttribute( + programRegistrationAttribute as any, + programIdPV, + accessToken, + ); + // Assert + expect(createReponse2.statusCode).toBe(HttpStatus.BAD_REQUEST); + }); + + it('should not be able to post a registration attributes without obligatory attributes', async () => { + // Arrange + const requiredAttributes = ['name', 'type', 'label']; + for (const attribute of requiredAttributes) { + const programRegistrationAttributeCopy = { + ...programRegistrationAttribute, + }; + delete programRegistrationAttributeCopy[attribute]; + + const createResponse = await postProgramRegistrationAttribute( + programRegistrationAttributeCopy as any, + programIdPV, + accessToken, + ); + // Assert + expect(createResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); + } + }); + + it('should not be able to post a registration attributes with an attribute name that exists', async () => { + // Arrange + const names = [ + 'namePartnerOrganization', + FinancialServiceProviderAttributes.whatsappPhoneNumber, + ]; + for (const name of names) { + const programRegistrationAttributeCopy = { + ...programRegistrationAttribute, + }; + programRegistrationAttributeCopy.name = name; + + // Act + const createReponse = await postProgramRegistrationAttribute( + programRegistrationAttributeCopy, + programIdPV, + accessToken, + ); + // Assert + expect(createReponse.statusCode).toBe(HttpStatus.BAD_REQUEST); + } + }); +}); diff --git a/services/121-service/test/program/create-program.test.ts b/services/121-service/test/program/create-program.test.ts index 1aeb0b51b0..991a0ca503 100644 --- a/services/121-service/test/program/create-program.test.ts +++ b/services/121-service/test/program/create-program.test.ts @@ -26,34 +26,36 @@ describe('Create program', () => { // Arrange const programOcwJson = JSON.parse(JSON.stringify(programOCW)); const programEthJson = JSON.parse(JSON.stringify(programEth)); - const programs = [programOcwJson, programEthJson]; + const seedPrograms = [programOcwJson, programEthJson]; - for (const program of programs) { + for (const seedProgram of seedPrograms) { // Act - const createProgramResponse = await postProgram(program, accessToken); + const createProgramResponse = await postProgram(seedProgram, accessToken); // Assert const programId = createProgramResponse.body.id; const getProgramResponse = await getProgram(programId, accessToken); expect(createProgramResponse.statusCode).toBe(HttpStatus.CREATED); - const cleanedProgram = cleanProgramForAssertions(program); + const cleanedSeedProgram = cleanProgramForAssertions(seedProgram); const cleanedProgramResponse = cleanProgramForAssertions( getProgramResponse.body, ); expect(cleanedProgramResponse).toMatchSnapshot( - `Create program response for program: ${program.titlePortal.en}`, + `Create program response for program: ${seedProgram.titlePortal.en}`, ); - expect(cleanedProgramResponse).toMatchObject(cleanedProgram); + expect(cleanedProgramResponse).toMatchObject(cleanedSeedProgram); } }); it('should not be able to post a program with 2 of the same names', async () => { // Arrange const programEthJson = JSON.parse(JSON.stringify(programEth)); - programEthJson.programQuestions[0].name = 'age'; + programEthJson.programRegistrationAttributes.push( + programEthJson.programRegistrationAttributes[0], + ); // Act const createProgramResponse = await postProgram( programEthJson, @@ -64,6 +66,7 @@ describe('Create program', () => { // Assert // const programId = createProgramResponse.body.id; expect(createProgramResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(createProgramResponse.body).toMatchSnapshot(); // A new program should not have been created expect(getProgramResponse.statusCode).toBe(HttpStatus.NOT_FOUND); @@ -83,35 +86,7 @@ describe('Create program', () => { // Assert // const programId = createProgramResponse.body.id; expect(createProgramResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); - - // A new program should not have been created - expect(getProgramResponse.statusCode).toBe(HttpStatus.NOT_FOUND); - }); - - it('should not be able to post a program with double names', async () => { - // Arrange - const programEthJson = JSON.parse(JSON.stringify(programEth)); - // Act - const attribute = { - name: 'gender', - type: 'text', - label: { - en: 'string', - fr: 'string', - }, - showInPeopleAffectedTable: true, - duplicateCheck: true, - }; - programEthJson.programCustomAttributes.push(attribute); - const createProgramResponse = await postProgram( - programEthJson, - accessToken, - ); - const getProgramResponse = await getProgram(4, accessToken); - - // Assert - // const programId = createProgramResponse.body.id; - expect(createProgramResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(createProgramResponse.body).toMatchSnapshot(); // A new program should not have been created expect(getProgramResponse.statusCode).toBe(HttpStatus.NOT_FOUND); diff --git a/services/121-service/test/program/manage-program-financial-service-provider-configuration.test.ts b/services/121-service/test/program/manage-program-financial-service-provider-configuration.test.ts new file mode 100644 index 0000000000..c0a009ee41 --- /dev/null +++ b/services/121-service/test/program/manage-program-financial-service-provider-configuration.test.ts @@ -0,0 +1,357 @@ +/* eslint-disable jest/no-conditional-expect */ +import { HttpStatus } from '@nestjs/common'; + +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration.dto'; +import { UpdateProgramFinancialServiceProviderConfigurationPropertyDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/update-program-financial-service-provider-configuration-property.dto'; +import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; +import { programIdVisa } from '@121-service/src/seed-data/mock/visa-card.data'; +import programOCW from '@121-service/src/seed-data/program/program-nlrc-ocw.json'; +import { + deleteProgramFinancialServiceProviderConfiguration, + deleteProgramFinancialServiceProviderConfigurationProperty, + getProgramFinancialServiceProviderConfigurations, + patchProgramFinancialServiceProviderConfiguration, + patchProgramFinancialServiceProviderConfigurationProperty, + postProgramFinancialServiceProviderConfiguration, + postProgramFinancialServiceProviderConfigurationProperties, +} from '@121-service/test/helpers/program-financial-service-provider-configuration.helper'; +import { seedPaidRegistrations } from '@121-service/test/helpers/registration.helper'; +import { + getAccessToken, + resetDB, +} from '@121-service/test/helpers/utility.helper'; +import { registrationOCW5 } from '@121-service/test/registrations/pagination/pagination-data'; + +// Only tests most of the happy paths, edge cases are mostly covered in the unit tests + +const seededFspConfigVoucher = + programOCW.programFinancialServiceProviderConfigurations.find( + (fspConfig) => + fspConfig.financialServiceProvider === + FinancialServiceProviders.intersolveVoucherWhatsapp, + )!; + +const createProgramFspConfigurationDto: CreateProgramFinancialServiceProviderConfigurationDto = + { + name: 'Intersolve Voucher WhatsApp name', + label: { + en: 'Intersolve Voucher WhatsApp label', + nl: 'Intersolve Voucher WhatsApp label Dutch translation', + es: 'Intersolve Voucher WhatsApp label Spanish translation', + }, + financialServiceProviderName: + FinancialServiceProviders.intersolveVoucherWhatsapp, + properties: [ + { + name: FinancialServiceProviderConfigurationProperties.username, + value: 'user123', + }, + { + name: FinancialServiceProviderConfigurationProperties.password, + value: 'password123', + }, + ], + }; + +describe('Manage financial service provider configurations', () => { + let accessToken: string; + + beforeEach(async () => { + await resetDB(SeedScript.nlrcMultiple); + accessToken = await getAccessToken(); + }); + + it('should add program financial service provider configuration to an existing program', async () => { + // Act + const result = await postProgramFinancialServiceProviderConfiguration({ + programId: programIdVisa, + body: createProgramFspConfigurationDto, + accessToken, + }); + const getResult = await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const getResultConfig = getResult.body.find( + (config) => config.name === createProgramFspConfigurationDto.name, + ); + // Assert + expect(result.statusCode).toBe(HttpStatus.CREATED); + expect(result.body).toEqual( + expect.objectContaining({ + name: createProgramFspConfigurationDto.name, + label: createProgramFspConfigurationDto.label, + financialServiceProviderName: + createProgramFspConfigurationDto.financialServiceProviderName, + }), + ); + const propertyNamesResult = result.body.properties.map( + (property) => property.name, + ); + const propertyNamesExpected = + createProgramFspConfigurationDto.properties!.map( + (property) => property.name, + ); + expect(propertyNamesResult).toEqual( + expect.arrayContaining(propertyNamesExpected), + ); + // All properties should have updated field as timestamp + result.body.properties.forEach((property) => { + const date = new Date(property.updated); + expect(!isNaN(date.getTime())).toBeTruthy(); + }); + // Ensure that the update data is reflected in the get response so actually updated in the db + expect(getResultConfig).toEqual(result.body); + }); + + it('should patch existing program financial service provider configuration', async () => { + // Act + const updateProgramFinancialServiceProviderConfigurationDto: UpdateProgramFinancialServiceProviderConfigurationDto = + { + label: { + en: 'Intersolve Voucher WhatsApp label updated', + nl: 'Intersolve Voucher WhatsApp label Dutch translation updated', + es: 'Intersolve Voucher WhatsApp label Spanish translation updated', + }, + properties: [ + { + name: FinancialServiceProviderConfigurationProperties.username, + value: 'user1234', + }, + ], + }; + const name = seededFspConfigVoucher.financialServiceProvider; + const result = await patchProgramFinancialServiceProviderConfiguration({ + programId: programIdVisa, + name, + body: updateProgramFinancialServiceProviderConfigurationDto, + accessToken, + }); + const getResult = await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const getResultConfig = getResult.body.find( + (config) => config.name === name, + ); + + // Assert + expect(result.statusCode).toBe(HttpStatus.OK); + expect(result.body).toEqual( + expect.objectContaining({ + name, + label: updateProgramFinancialServiceProviderConfigurationDto.label, + financialServiceProviderName: + seededFspConfigVoucher.financialServiceProvider, + }), + ); + const propertyNamesResult = result.body.properties.map( + (property) => property.name, + ); + const propertyNamesExpected = + updateProgramFinancialServiceProviderConfigurationDto.properties!.map( + (property) => property.name, + ); + expect(propertyNamesResult).toEqual( + expect.arrayContaining(propertyNamesExpected), + ); + // All properties should have updated field as timestamp + result.body.properties.forEach((property) => { + const date = new Date(property.updated); + expect(!isNaN(date.getTime())).toBeTruthy(); + }); + // Ensure that the update data is reflected in the get response so actually updated in the db + expect(getResultConfig).toEqual(result.body); + }); + + it('should delete existing program financial service provider configuration', async () => { + // Act + const name = seededFspConfigVoucher.financialServiceProvider; + const result = await deleteProgramFinancialServiceProviderConfiguration({ + programId: programIdVisa, + name, + accessToken, + }); + const getResult = await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const getResultConfig = getResult.body.find( + (config) => config.name === name, + ); + // Assert + expect(result.statusCode).toBe(HttpStatus.NO_CONTENT); + expect(getResultConfig).toBeUndefined(); + }); + + // Checking this exception in api test because it's hard to unit test the more complex transaction querybuilder part + it('should not delete existing program financial service provider configuration because of transactions', async () => { + // Prepare + await seedPaidRegistrations([registrationOCW5], programIdVisa); + + // Act + const name = seededFspConfigVoucher.financialServiceProvider; + const result = await deleteProgramFinancialServiceProviderConfiguration({ + programId: programIdVisa, + name, + accessToken, + }); + const getResult = await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const getResultConfig = getResult.body.find( + (config) => config.name === name, + ); + // Assert + expect(result.statusCode).toBe(HttpStatus.CONFLICT); + expect(getResultConfig).toBeDefined(); + }); + + it('should add program financial service provider configuration properties to an existing program financial service provider configuration', async () => { + // Prepare + const createProgramFspConfigurationDtoNoProperties = { + ...createProgramFspConfigurationDto, + properties: undefined, + }; + await postProgramFinancialServiceProviderConfiguration({ + programId: programIdVisa, + body: createProgramFspConfigurationDtoNoProperties, + accessToken, + }); + + // Act + const result = + await postProgramFinancialServiceProviderConfigurationProperties({ + programId: programIdVisa, + properties: createProgramFspConfigurationDto.properties!, + accessToken, + name: createProgramFspConfigurationDto.name, + }); + + const getResult = await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const getResultConfig = getResult.body.find( + (config) => config.name === createProgramFspConfigurationDto.name, + ); + // Assert + expect(result.statusCode).toBe(HttpStatus.CREATED); + const propertyNamesResult = result.body.map((property) => property.name); + const propertyNamesExpected = + createProgramFspConfigurationDto.properties!.map( + (property) => property.name, + ); + expect(propertyNamesResult).toEqual( + expect.arrayContaining(propertyNamesExpected), + ); + // All properties should have updated field as timestamp + result.body.forEach((property) => { + const date = new Date(property.updated); + expect(!isNaN(date.getTime())).toBeTruthy(); + }); + // Ensure that the update data is reflected in the get response so actually updated in the db + expect(getResultConfig?.properties).toEqual(result.body); + }); + + it('should patch a property of an existing program financial service provider configuration', async () => { + // Prepare + const updatedPropertyDto: UpdateProgramFinancialServiceProviderConfigurationPropertyDto = + { + value: 'user1234', + }; + + // Act + const getResultBefore = + await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const usernamePropertyBefore = getResultBefore.body + .find( + (config) => + config.name === seededFspConfigVoucher.financialServiceProvider, + )! + .properties.find( + (property) => + property.name === + FinancialServiceProviderConfigurationProperties.username, + ); + + const patchResult = + await patchProgramFinancialServiceProviderConfigurationProperty({ + programId: programIdVisa, + configName: seededFspConfigVoucher.financialServiceProvider, + propertyName: FinancialServiceProviderConfigurationProperties.username, + body: updatedPropertyDto, + accessToken, + }); + + const getResultAfter = + await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const usernamePropertyAfter = getResultAfter.body + .find( + (config) => + config.name === seededFspConfigVoucher.financialServiceProvider, + )! + .properties.find( + (property) => + property.name === + FinancialServiceProviderConfigurationProperties.username, + ); + + // Assert + expect(patchResult.statusCode).toBe(HttpStatus.OK); + + // Ensure that the username property value has been updated checking if the updated timestamp is later, since the value is not returned in the response + expect(usernamePropertyAfter?.updated).not.toEqual( + usernamePropertyBefore?.updated, + ); + expect(new Date(usernamePropertyAfter!.updated).getTime()).toBeGreaterThan( + new Date(usernamePropertyBefore!.updated).getTime(), + ); + }); + + it('should delete a property of an existing program financial service provider configuration', async () => { + // Act + const deleteResult = + await deleteProgramFinancialServiceProviderConfigurationProperty({ + programId: programIdVisa, + configName: seededFspConfigVoucher.financialServiceProvider, + propertyName: FinancialServiceProviderConfigurationProperties.username, + accessToken, + }); + + const getResultAfter = + await getProgramFinancialServiceProviderConfigurations({ + programId: programIdVisa, + accessToken, + }); + const config = getResultAfter.body.find( + (config) => + config.name === seededFspConfigVoucher.financialServiceProvider, + ); + + const usernamePropertyAfter = config?.properties.find( + (property) => + property.name === + FinancialServiceProviderConfigurationProperties.username, + ); + + // Assert + expect(deleteResult.statusCode).toBe(HttpStatus.NO_CONTENT); + expect(usernamePropertyAfter).toBeUndefined(); + expect(config?.properties.length).toBe( + seededFspConfigVoucher.properties.length - 1, + ); + }); +}); diff --git a/services/121-service/test/program/message-template.test.ts b/services/121-service/test/program/message-template.test.ts index 5fd46b8b79..bd3324fdd8 100644 --- a/services/121-service/test/program/message-template.test.ts +++ b/services/121-service/test/program/message-template.test.ts @@ -44,10 +44,6 @@ describe('Message template', () => { messageTemplate as CreateMessageTemplateDto, accessToken, ); - console.log( - 'postMessageTemplateResult: ', - postMessageTemplateResult.statusCode, - ); // Assert expect(postMessageTemplateResult.statusCode).toBe(HttpStatus.CREATED); diff --git a/services/121-service/test/program/notes.test.ts b/services/121-service/test/program/notes.test.ts index 660d6d9fbb..744444da14 100644 --- a/services/121-service/test/program/notes.test.ts +++ b/services/121-service/test/program/notes.test.ts @@ -1,47 +1,31 @@ /* eslint-disable jest/no-conditional-expect */ import { HttpStatus } from '@nestjs/common'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { getNotes, postNote } from '@121-service/test/helpers/program.helper'; import { importRegistrations } from '@121-service/test/helpers/registration.helper'; import { getAccessToken, resetDB, } from '@121-service/test/helpers/utility.helper'; +import { registrationOCW1 } from '@121-service/test/registrations/pagination/pagination-data'; describe('Notes', () => { let accessToken: string; const programId = 3; - const registration = { - referenceId: '456984cc-1066-48f3-b70b-0e16c3fe5bce', - preferredLanguage: LanguageEnum.en, - paymentAmountMultiplier: 1, - firstName: 'John', - lastName: 'Smith', - phoneNumber: '14155238886', - fspName: FinancialServiceProviders.safaricom, - whatsappPhoneNumber: '14155238886', - addressStreet: 'Teststraat', - addressHouseNumber: '1', - addressHouseNumberAddition: '', - addressPostalCode: '1234AB', - addressCity: 'Stad', - }; const noteText = 'test note'; beforeEach(async () => { await resetDB(SeedScript.nlrcMultiple); accessToken = await getAccessToken(); - await importRegistrations(programId, [registration], accessToken); + await importRegistrations(programId, [registrationOCW1], accessToken); }); it('should post a note', async () => { // Act const postNoteResponse = await postNote( - registration.referenceId, + registrationOCW1.referenceId, noteText, programId, accessToken, @@ -52,11 +36,16 @@ describe('Notes', () => { }); it('should get a note', async () => { - await postNote(registration.referenceId, noteText, programId, accessToken); + await postNote( + registrationOCW1.referenceId, + noteText, + programId, + accessToken, + ); // Act const getNoteResponse = await getNotes( - registration.referenceId, + registrationOCW1.referenceId, programId, accessToken, ); diff --git a/services/121-service/test/program/update-program.test.ts b/services/121-service/test/program/update-program.test.ts index 01e156cfcb..6d9d42c062 100644 --- a/services/121-service/test/program/update-program.test.ts +++ b/services/121-service/test/program/update-program.test.ts @@ -1,6 +1,5 @@ import { HttpStatus } from '@nestjs/common'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { UpdateProgramDto } from '@121-service/src/programs/dto/update-program.dto'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import { patchProgram } from '@121-service/test/helpers/program.helper'; @@ -51,47 +50,4 @@ describe('Update program', () => { ); expect(updateProgramResponse.body.budget).toBe(program.budget); }); - - it('should add an fsp to a program', async () => { - // Arrange - const program = { - financialServiceProviders: JSON.parse( - JSON.stringify([{ fsp: FinancialServiceProviders.excel }]), - ), - }; - - // Act - const updateProgramResponse = await patchProgram( - 2, - program as UpdateProgramDto, - accessToken, - ); - - // Assert - expect(updateProgramResponse.statusCode).toBe(HttpStatus.OK); - const hasSpecificKeyValue = - updateProgramResponse.body.financialServiceProviders.some( - (fsp) => fsp.fsp === FinancialServiceProviders.excel, - ); - expect(hasSpecificKeyValue).toBeTruthy(); - }); - - it('should not be able to add an fsp that does not exists to a program', async () => { - // Arrange - const program = { - financialServiceProviders: JSON.parse( - JSON.stringify([{ fsp: 'non-existing-fsp' }]), - ), - }; - - // Act - const updateProgramResponse = await patchProgram( - 2, - program as UpdateProgramDto, - accessToken, - ); - - // Assert - expect(updateProgramResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); - }); }); diff --git a/services/121-service/test/registrations/__snapshots__/calculate-payment-amount-multiplier.test.ts.snap b/services/121-service/test/registrations/__snapshots__/calculate-payment-amount-multiplier.test.ts.snap new file mode 100644 index 0000000000..42562f5191 --- /dev/null +++ b/services/121-service/test/registrations/__snapshots__/calculate-payment-amount-multiplier.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Set/calculate payment amount multiplier should error if paymentAmountMultiplier is set while program has a formula 1`] = ` +{ + "data": [], + "links": { + "current": "http://localhost:3000/api/programs/2/registrations?page=1&limit=20&sortBy=id:ASC&filter.referenceId=westeros123456789", + }, + "meta": { + "currentPage": 1, + "filter": { + "referenceId": "westeros123456789", + }, + "itemsPerPage": 20, + "sortBy": [ + [ + "id", + "ASC", + ], + ], + "totalItems": 0, + "totalPages": 0, + }, +} +`; diff --git a/services/121-service/test/registrations/__snapshots__/import-registration.test.ts.snap b/services/121-service/test/registrations/__snapshots__/import-registration.test.ts.snap index ee018f1532..f9b1aa0a1d 100644 --- a/services/121-service/test/registrations/__snapshots__/import-registration.test.ts.snap +++ b/services/121-service/test/registrations/__snapshots__/import-registration.test.ts.snap @@ -7,17 +7,27 @@ exports[`Import a registration should give me a CSV template when I request it 1 "addressHouseNumberAddition", "addressPostalCode", "addressStreet", - "fspName", "fullName", "paymentAmountMultiplier", "phoneNumber", "preferredLanguage", + "programFinancialServiceProviderConfigurationName", "referenceId", "whatsappPhoneNumber", ] `; -exports[`Import a registration should throw an error with a dropdown custom atribute set to null 1`] = ` +exports[`Import a registration should throw an error when a required fsp attribute is missing 1`] = ` +[ + { + "column": "whatsappPhoneNumber", + "error": "Cannot update 'whatsappPhoneNumber' is required for the FSP: 'Intersolve-voucher-whatsapp'", + "lineNumber": 1, + }, +] +`; + +exports[`Import a registration should throw an error with a dropdown registration atribute set to null 1`] = ` [ { "column": "house", @@ -28,11 +38,11 @@ exports[`Import a registration should throw an error with a dropdown custom atri ] `; -exports[`Import a registration should throw an error with a numeric custom atribute set to null 1`] = ` +exports[`Import a registration should throw an error with a numeric registration atribute set to null 1`] = ` [ { "column": "addressHouseNumber", - "error": "Value is not a valid numeric", + "error": "Cannot update/set addressHouseNumber with a nullable value as it is required for the FSP: Intersolve-visa", "lineNumber": 1, "value": null, }, diff --git a/services/121-service/test/registrations/__snapshots__/update-registration-program-financial-service-provider-configuration.test.ts.snap b/services/121-service/test/registrations/__snapshots__/update-registration-program-financial-service-provider-configuration.test.ts.snap new file mode 100644 index 0000000000..d63af4b531 --- /dev/null +++ b/services/121-service/test/registrations/__snapshots__/update-registration-program-financial-service-provider-configuration.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Update program financial servce provider configuration of PA should fail when updating program financial servce provider configuration when a required property of new FSP is not yet present 1`] = ` +{ + "message": "addressCity: Cannot update 'addressCity' is required for the FSP: 'Intersolve-visa', addressHouseNumber: Cannot update 'addressHouseNumber' is required for the FSP: 'Intersolve-visa', addressPostalCode: Cannot update 'addressPostalCode' is required for the FSP: 'Intersolve-visa', addressStreet: Cannot update 'addressStreet' is required for the FSP: 'Intersolve-visa'", + "statusCode": 400, +} +`; diff --git a/services/121-service/test/registrations/__snapshots__/update-registration.test.ts.snap b/services/121-service/test/registrations/__snapshots__/update-registration.test.ts.snap new file mode 100644 index 0000000000..c9023e1461 --- /dev/null +++ b/services/121-service/test/registrations/__snapshots__/update-registration.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Update attribute of PA should fail when removing a required program registration attribute 1`] = ` +{ + "message": "motto: The value 'null' given for the attribute 'motto' does not have the correct format for type 'text'", + "statusCode": 400, +} +`; diff --git a/services/121-service/test/registrations/bulk-update-registration.test.ts b/services/121-service/test/registrations/bulk-update-registration.test.ts new file mode 100644 index 0000000000..a1f280ad71 --- /dev/null +++ b/services/121-service/test/registrations/bulk-update-registration.test.ts @@ -0,0 +1,269 @@ +import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; +import { waitFor } from '@121-service/src/utils/waitFor.helper'; +import { assertRegistrationBulkUpdate } from '@121-service/test/helpers/assert.helper'; +import { patchProgram } from '@121-service/test/helpers/program.helper'; +import { + bulkUpdateRegistrationsCSV, + importRegistrationsCSV, + searchRegistrationByReferenceId, +} from '@121-service/test/helpers/registration.helper'; +import { + getAccessToken, + resetDB, +} from '@121-service/test/helpers/utility.helper'; + +describe('Update attribute of multiple PAs via Bulk update', () => { + const programIdOcw = 3; + + let accessToken: string; + + beforeEach(async () => { + await resetDB(SeedScript.nlrcMultiple); + accessToken = await getAccessToken(); + + await importRegistrationsCSV( + programIdOcw, + './test-registration-data/test-registrations-OCW.csv', + accessToken, + ); + }); + + it('Should bulk update and validate changed records', async () => { + const registrationDataThatWillChangePa1 = { + phoneNumber: '14155238880', + fullName: 'updated name1', + addressStreet: 'newStreet1', + addressHouseNumber: '2', + addressHouseNumberAddition: '', + preferredLanguage: 'ar', + paymentAmountMultiplier: 2, + whatsappPhoneNumber: '14155238880', + }; + const registrationDataThatWillChangePa2 = { + phoneNumber: '14155238881', + fullName: 'updated name 2', + addressStreet: 'newStreet2', + addressHouseNumber: '3', + addressHouseNumberAddition: 'updated', + preferredLanguage: 'nl', + paymentAmountMultiplier: 3, + whatsappPhoneNumber: '14155238881', + }; + + // Registration before patch + const searchByReferenceIdBeforePatchPa1 = + await searchRegistrationByReferenceId( + '00dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa1BeforePatch = searchByReferenceIdBeforePatchPa1.body.data[0]; + + const searchByReferenceIdBeforePatchPa2 = + await searchRegistrationByReferenceId( + '01dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa2BeforePatch = searchByReferenceIdBeforePatchPa2.body.data[0]; + + // Act + const bulkUpdateResult = await bulkUpdateRegistrationsCSV( + programIdOcw, + './test-registration-data/test-registrations-patch-OCW.csv', + accessToken, + 'test-reason', + ); + expect(bulkUpdateResult.statusCode).toBe(200); + await waitFor(2000); + + const searchByReferenceIdAfterPatchPa1 = + await searchRegistrationByReferenceId( + '00dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + + const pa1AfterPatch = searchByReferenceIdAfterPatchPa1.body.data[0]; + + const searchByReferenceIdAfterPatchPa2 = + await searchRegistrationByReferenceId( + '01dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + + const pa2AfterPatch = searchByReferenceIdAfterPatchPa2.body.data[0]; + + // Assert + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa1, + pa1AfterPatch, + pa1BeforePatch, + ); + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa2, + pa2AfterPatch, + pa2BeforePatch, + ); + }); + + it('Should bulk update chosen FSP and validate changed records', async () => { + const registrationDataThatWillChangePa1 = { + financialServiceProviderName: 'Intersolve-voucher-whatsapp', + programFinancialServiceProviderConfigurationId: 5, + programFinancialServiceProviderConfigurationName: + 'Intersolve-voucher-whatsapp', + programFinancialServiceProviderConfigurationLabel: { + en: 'Albert Heijn voucher WhatsApp', + }, + }; + const registrationDataThatWillChangePa2 = { + financialServiceProviderName: 'Intersolve-visa', + programFinancialServiceProviderConfigurationId: 6, + programFinancialServiceProviderConfigurationName: 'Intersolve-visa', + programFinancialServiceProviderConfigurationLabel: { + en: 'Visa debit card', + }, + }; + + // Registration before patch + const searchByReferenceIdBeforePatchPa1 = + await searchRegistrationByReferenceId( + '00dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa1BeforePatch = searchByReferenceIdBeforePatchPa1.body.data[0]; + + const searchByReferenceIdBeforePatchPa2 = + await searchRegistrationByReferenceId( + '01dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa2BeforePatch = searchByReferenceIdBeforePatchPa2.body.data[0]; + + // Act + const bulkUpdateResult = await bulkUpdateRegistrationsCSV( + programIdOcw, + './test-registration-data/test-registrations-patch-OCW-chosen-FSP.csv', + accessToken, + 'test-reason', + ); + expect(bulkUpdateResult.statusCode).toBe(200); + await waitFor(2000); + + const searchByReferenceIdAfterPatchPa1 = + await searchRegistrationByReferenceId( + '00dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + + const pa1AfterPatch = searchByReferenceIdAfterPatchPa1.body.data[0]; + + const searchByReferenceIdAfterPatchPa2 = + await searchRegistrationByReferenceId( + '01dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + + const pa2AfterPatch = searchByReferenceIdAfterPatchPa2.body.data[0]; + + // Assert + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa1, + pa1AfterPatch, + pa1BeforePatch, + ); + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa2, + pa2AfterPatch, + pa2BeforePatch, + ); + }); + + it('Should bulk update if phoneNumber column is empty and program is configured as allowing empty phone number', async () => { + const registrationDataThatWillChangePa1 = { + fullName: 'updated name1', + addressStreet: 'newStreet1', + addressHouseNumber: '2', + addressHouseNumberAddition: '', + preferredLanguage: 'ar', + paymentAmountMultiplier: 2, + phoneNumber: '14155238880', + }; + const registrationDataThatWillChangePa2 = { + fullName: 'updated name 2', + addressStreet: 'newStreet2', + addressHouseNumber: '3', + addressHouseNumberAddition: 'updated', + preferredLanguage: 'nl', + paymentAmountMultiplier: 3, + phoneNumber: null, + }; + await patchProgram( + programIdOcw, + { allowEmptyPhoneNumber: true }, + accessToken, + ); + + // Registration before patch + const searchByReferenceIdBeforePatchPa1 = + await searchRegistrationByReferenceId( + '00dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa1BeforePatch = searchByReferenceIdBeforePatchPa1.body.data[0]; + + const searchByReferenceIdBeforePatchPa2 = + await searchRegistrationByReferenceId( + '17dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa2BeforePatch = searchByReferenceIdBeforePatchPa2.body.data[0]; + + // Act + const bulkUpdateResult = await bulkUpdateRegistrationsCSV( + programIdOcw, + './test-registration-data/test-registrations-patch-OCW-without-phoneNumber-column.csv', + accessToken, + 'test-reason', + ); + expect(bulkUpdateResult.statusCode).toBe(200); + + await waitFor(2000); + + const searchByReferenceIdAfterPatchPa1 = + await searchRegistrationByReferenceId( + '00dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa1AfterPatch = searchByReferenceIdAfterPatchPa1.body.data[0]; + + const searchByReferenceIdAfterPatchPa2 = + await searchRegistrationByReferenceId( + '17dc9451-1273-484c-b2e8-ae21b51a96ab', + programIdOcw, + accessToken, + ); + const pa2AfterPatch = searchByReferenceIdAfterPatchPa2.body.data[0]; + + // Assert + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa1, + pa1AfterPatch, + pa1BeforePatch, + ); + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa2, + pa2AfterPatch, + pa2BeforePatch, + ); + }); +}); diff --git a/services/121-service/test/registrations/calculate-payment-amount-multiplier.test.ts b/services/121-service/test/registrations/calculate-payment-amount-multiplier.test.ts new file mode 100644 index 0000000000..294cae32fe --- /dev/null +++ b/services/121-service/test/registrations/calculate-payment-amount-multiplier.test.ts @@ -0,0 +1,130 @@ +import { HttpStatus } from '@nestjs/common'; + +import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; +import { + importRegistrations, + searchRegistrationByReferenceId, +} from '@121-service/test/helpers/registration.helper'; +import { + getAccessToken, + resetDB, +} from '@121-service/test/helpers/utility.helper'; +import { + programIdPV, + programIdWesteros, + registrationPV5, + registrationWesteros1, +} from '@121-service/test/registrations/pagination/pagination-data'; + +describe('Set/calculate payment amount multiplier', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + }); + + it('should automatically calculate payment amount based on formula', async () => { + await resetDB(SeedScript.testMultiple); + const nrOfDragons = 2; + + // Arrange + const registrationWesterosCopy = { ...registrationWesteros1 }; + registrationWesterosCopy.dragon = nrOfDragons; + + // Act + const responseImport = await importRegistrations( + programIdWesteros, + [registrationWesterosCopy], + accessToken, + ); + const searchRegistrationResponse = await searchRegistrationByReferenceId( + registrationWesterosCopy.referenceId, + programIdWesteros, + accessToken, + ); + const importedRegistration = searchRegistrationResponse.body.data[0]; + + // Assert + expect(responseImport.statusCode).toBe(HttpStatus.CREATED); + expect(importedRegistration.paymentAmountMultiplier).toBe(nrOfDragons + 1); + }); + + it('should error if paymentAmountMultiplier is set while program has a formula', async () => { + await resetDB(SeedScript.testMultiple); + // Arrange + const registrationWesterosCopy = { + ...registrationWesteros1, + ...{ paymentAmountMultiplier: 3 }, + }; + + // Act + const responseImport = await importRegistrations( + programIdWesteros, + [registrationWesterosCopy], + accessToken, + ); + + const searchRegistrationResponse = await searchRegistrationByReferenceId( + registrationWesterosCopy.referenceId, + programIdWesteros, + accessToken, + ); + // Assert + expect(responseImport.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(searchRegistrationResponse.body).toMatchSnapshot(); + }); + + it('should set paymentAmountMultiplier to 1 if program has no formula and paymentAmountMultiplier in import is not set', async () => { + await resetDB(SeedScript.nlrcMultiple); + // Arrange + const registrationPvCopy = { + ...registrationPV5, + }; + + // Act + const responseImport = await importRegistrations( + programIdPV, + [registrationPvCopy], + accessToken, + ); + + const searchRegistrationResponse = await searchRegistrationByReferenceId( + registrationPvCopy.referenceId, + programIdPV, + accessToken, + ); + const importedRegistration = searchRegistrationResponse.body.data[0]; + // Assert + expect(responseImport.statusCode).toBe(HttpStatus.CREATED); + expect(searchRegistrationResponse.body.data.length).toBe(1); + expect(importedRegistration.paymentAmountMultiplier).toBe(1); + }); + + it('should set paymentAmountMultiplier based paymentAmountMultiplier if program has no formula', async () => { + await resetDB(SeedScript.nlrcMultiple); + // Arrange + const paymentAmountMultiplier = 3; + const registrationPvCopy = { + ...registrationPV5, + ...{ paymentAmountMultiplier }, + }; + // Act + const responseImport = await importRegistrations( + programIdPV, + [registrationPvCopy], + accessToken, + ); + + const searchRegistrationResponse = await searchRegistrationByReferenceId( + registrationPvCopy.referenceId, + programIdPV, + accessToken, + ); + const importedRegistration = searchRegistrationResponse.body.data[0]; + // Assert + expect(responseImport.statusCode).toBe(HttpStatus.CREATED); + expect(importedRegistration.paymentAmountMultiplier).toBe( + paymentAmountMultiplier, + ); + }); +}); diff --git a/services/121-service/test/registrations/import-registration.test.ts b/services/121-service/test/registrations/import-registration.test.ts index ff21097e99..4808040431 100644 --- a/services/121-service/test/registrations/import-registration.test.ts +++ b/services/121-service/test/registrations/import-registration.test.ts @@ -1,5 +1,6 @@ import { HttpStatus } from '@nestjs/common'; +import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { DebugScope } from '@121-service/src/scripts/enum/debug-scope.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import { registrationVisa } from '@121-service/src/seed-data/mock/visa-card.data'; @@ -9,6 +10,7 @@ import { } from '@121-service/test/fixtures/scoped-registrations'; import { patchProgram, + setAllProgramsRegistrationAttributesNonRequired, unpublishProgram, } from '@121-service/test/helpers/program.helper'; import { @@ -24,6 +26,8 @@ import { import { programIdOCW, programIdPV, + programIdWesteros, + registrationPV5, registrationWesteros1, } from '@121-service/test/registrations/pagination/pagination-data'; @@ -51,15 +55,44 @@ describe('Import a registration', () => { ); const registration = result.body.data[0]; for (const key in registrationVisa) { - if (key === 'fspName') { - // eslint-disable-next-line jest/no-conditional-expect - expect(registration['financialServiceProvider']).toBe( - registrationVisa[key], - ); - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(registration[key]).toBe(registrationVisa[key]); + expect(registration[key]).toBe(registrationVisa[key]); + } + }); + + it('should import registration with mixed attributes (dropdown, boolean, string, numeric)', async () => { + // Arrange + await resetDB(SeedScript.testMultiple); + accessToken = await getAccessToken(); + + // Act + const response = await importRegistrations( + programIdWesteros, + [registrationWesteros1], + accessToken, + ); + + expect(response.statusCode).toBe(HttpStatus.CREATED); + + const result = await searchRegistrationByReferenceId( + registrationWesteros1.referenceId, + programIdWesteros, + accessToken, + ); + const registration = result.body.data[0]; + for (const key in registrationWesteros1) { + // TODO: Number & Boolean is converted to string maybe we should fix this in the future + const expectedValue = registrationWesteros1[key]; + const actualValue = registration[key]; + + let normalizedExpectedValue = expectedValue; + if ( + typeof expectedValue === 'number' || + typeof expectedValue === 'boolean' + ) { + normalizedExpectedValue = expectedValue.toString(); } + + expect(actualValue).toBe(normalizedExpectedValue); } }); @@ -106,15 +139,7 @@ describe('Import a registration', () => { const registrationResult = result.body.data[0]; for (const key in registrationScopedGoesPv) { - if (key === 'fspName') { - // eslint-disable-next-line jest/no-conditional-expect - expect(registrationResult['financialServiceProvider']).toBe( - registrationScopedGoesPv[key], - ); - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(registrationResult[key]).toBe(registrationScopedGoesPv[key]); - } + expect(registrationResult[key]).toBe(registrationScopedGoesPv[key]); } }); @@ -173,43 +198,34 @@ describe('Import a registration', () => { // Arrange await resetDB(SeedScript.nlrcMultiple); accessToken = await getAccessToken(); - const registrationVisaCopy = { ...registrationVisa }; + const registrationPVCopy = { ...registrationPV5 }; // @ts-expect-error "The operand of a 'delete' operator must be optional.ts(2790)" - delete registrationVisaCopy.phoneNumber; + delete registrationPVCopy.phoneNumber; const programUpdate = { allowEmptyPhoneNumber: true, }; - await patchProgram(programIdOCW, programUpdate, accessToken); + await patchProgram(programIdPV, programUpdate, accessToken); // Act const response = await importRegistrations( - programIdOCW, - [registrationVisaCopy], + programIdPV, + [registrationPVCopy], accessToken, ); - expect(response.statusCode).toBe(HttpStatus.CREATED); const result = await searchRegistrationByReferenceId( - registrationVisaCopy.referenceId, - programIdOCW, + registrationPVCopy.referenceId, + programIdPV, accessToken, ); const registration = result.body.data[0]; - for (const key in registrationVisaCopy) { - if (key === 'fspName') { - // eslint-disable-next-line jest/no-conditional-expect - expect(registration['financialServiceProvider']).toBe( - registrationVisaCopy[key], - ); - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(registration[key]).toBe(registrationVisaCopy[key]); - } + for (const key in registrationPVCopy) { + expect(registration[key]).toBe(registrationPVCopy[key]); } }); - it('should throw an error with a numeric custom atribute set to null', async () => { + it('should throw an error with a numeric registration atribute set to null', async () => { // Arrange await resetDB(SeedScript.nlrcMultiple); accessToken = await getAccessToken(); @@ -237,7 +253,7 @@ describe('Import a registration', () => { expect(registration).toHaveLength(0); }); - it('should throw an error with a dropdown custom atribute set to null', async () => { + it('should throw an error with a dropdown registration atribute set to null', async () => { // Arrange await resetDB(SeedScript.test); accessToken = await getAccessToken(); @@ -266,6 +282,76 @@ describe('Import a registration', () => { expect(registration).toHaveLength(0); }); + it('should throw an error when a required fsp attribute is missing', async () => { + // Arrange + await resetDB(SeedScript.test); + accessToken = await getAccessToken(); + + // Removes whatsapp from original registration + const { + whatsappPhoneNumber: _whatsappPhoneNumber, + ...registrationWesteros1Copy + } = registrationWesteros1; + registrationWesteros1Copy.programFinancialServiceProviderConfigurationName = + FinancialServiceProviders.intersolveVoucherWhatsapp; + + const programIdWestoros = 1; + + // Act + const response = await importRegistrations( + programIdWestoros, + [registrationWesteros1Copy], + accessToken, + ); + + expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(response.body).toMatchSnapshot(); + + const result = await searchRegistrationByReferenceId( + registrationWesteros1Copy.referenceId, + programIdWestoros, + accessToken, + ); + + const registration = result.body.data; + expect(registration).toHaveLength(0); + }); + + it('should throw an error when uploading a non existing fsp', async () => { + // Arrange + await resetDB(SeedScript.testMultiple); + accessToken = await getAccessToken(); + + // Removes whatsapp from original registration + const { + whatsappPhoneNumber: _whatsappPhoneNumber, + ...registrationWesteros1Copy + } = registrationWesteros1; + registrationWesteros1Copy.programFinancialServiceProviderConfigurationName = + 'non-existing-fsp'; + + // Act + const response = await importRegistrations( + programIdWesteros, + [registrationWesteros1Copy], + accessToken, + ); + + expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(response.body[0].error).toContain( + registrationWesteros1Copy.programFinancialServiceProviderConfigurationName, + ); + + const result = await searchRegistrationByReferenceId( + registrationWesteros1Copy.referenceId, + programIdWesteros, + accessToken, + ); + + const registration = result.body.data; + expect(registration).toHaveLength(0); + }); + it('should give me a CSV template when I request it', async () => { // Arrange await resetDB(SeedScript.nlrcMultiple); @@ -275,4 +361,43 @@ describe('Import a registration', () => { expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.sort()).toMatchSnapshot(); }); + + it('should import registration with null values when all attributes are non-required attributes', async () => { + // Arrange + await resetDB(SeedScript.testMultiple); + accessToken = await getAccessToken(); + const registrationWesterosEmpty = { + referenceId: 'registrationWesterosEmpty', + programFinancialServiceProviderConfigurationName: 'ironBank', + }; + + const programUpdate = { + allowEmptyPhoneNumber: true, + }; + await patchProgram(programIdWesteros, programUpdate, accessToken); + + // Patch all programRegistationAttributes to be non-required + await setAllProgramsRegistrationAttributesNonRequired( + programIdWesteros, + accessToken, + ); + + // Act + const response = await importRegistrations( + programIdPV, + [registrationWesterosEmpty], + accessToken, + ); + expect(response.statusCode).toBe(HttpStatus.CREATED); + + const result = await searchRegistrationByReferenceId( + registrationWesterosEmpty.referenceId, + programIdPV, + accessToken, + ); + const registration = result.body.data[0]; + for (const key in registrationWesterosEmpty) { + expect(registration[key]).toBe(registrationWesterosEmpty[key]); + } + }); }); diff --git a/services/121-service/test/registrations/mass-update-registration.test.ts b/services/121-service/test/registrations/mass-update-registration.test.ts deleted file mode 100644 index 6a4b0745ab..0000000000 --- a/services/121-service/test/registrations/mass-update-registration.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import { waitFor } from '@121-service/src/utils/waitFor.helper'; -import { assertRegistrationImport } from '@121-service/test/helpers/assert.helper'; -import { - bulkUpdateRegistrationsCSV, - importRegistrationsCSV, - searchRegistrationByReferenceId, -} from '@121-service/test/helpers/registration.helper'; -import { - getAccessToken, - resetDB, -} from '@121-service/test/helpers/utility.helper'; - -describe('Update attribute of multiple PAs via Bulk update', () => { - const programIdOcw = 3; - - let accessToken: string; - - beforeEach(async () => { - await resetDB(SeedScript.nlrcMultiple); - accessToken = await getAccessToken(); - - await importRegistrationsCSV( - programIdOcw, - './test-registration-data/test-registrations-OCW.csv', - accessToken, - ); - }); - - it('Should bulk update and validate changed records', async () => { - const registrationDataThatWillChangePa1 = { - phoneNumber: '14155238880', - fullName: 'updated name1', - addressStreet: 'newStreet1', - addressHouseNumber: '2', - addressHouseNumberAddition: '', - }; - const registrationDataThatWillChangePa2 = { - phoneNumber: '14155238881', - fullName: 'updated name 2', - addressStreet: 'newStreet2', - addressHouseNumber: '3', - addressHouseNumberAddition: 'updated', - }; - - // Act - const bulkUpdateResult = await bulkUpdateRegistrationsCSV( - programIdOcw, - './test-registration-data/test-registrations-patch-OCW.csv', - accessToken, - 'Bulk update for test registrations due to data validation improvements', - ); - expect(bulkUpdateResult.statusCode).toBe(200); - await waitFor(2000); - - const searchByReferenceIdAfterPatchPa1 = - await searchRegistrationByReferenceId( - '00dc9451-1273-484c-b2e8-ae21b51a96ab', - programIdOcw, - accessToken, - ); - - const pa1AfterPatch = searchByReferenceIdAfterPatchPa1.body.data[0]; - - const searchByReferenceIdAfterPatchPa2 = - await searchRegistrationByReferenceId( - '01dc9451-1273-484c-b2e8-ae21b51a96ab', - programIdOcw, - accessToken, - ); - - const pa2AfterPatch = searchByReferenceIdAfterPatchPa2.body.data[0]; - - // Assert - assertRegistrationImport(pa1AfterPatch, registrationDataThatWillChangePa1); - assertRegistrationImport(pa2AfterPatch, registrationDataThatWillChangePa2); - }); - - it('Should bulk update if phoneNumber column is empty and program is configured as not allowing empty phone number', async () => { - const registrationDataThatWillChangePa1 = { - phoneNumber: '14155238886', - fullName: 'updated name1', - addressStreet: 'newStreet1', - addressHouseNumber: '2', - addressHouseNumberAddition: '', - }; - const registrationDataThatWillChangePa2 = { - phoneNumber: '14155238886', - fullName: 'updated name 2', - addressStreet: 'newStreet2', - addressHouseNumber: '3', - addressHouseNumberAddition: 'updated', - }; - - // Act - const bulkUpdateResult = await bulkUpdateRegistrationsCSV( - programIdOcw, - './test-registration-data/test-registrations-patch-OCW-without-phoneNumber-column.csv', - accessToken, - 'Bulk update for test registrations due to data validation improvements', - ); - expect(bulkUpdateResult.statusCode).toBe(200); - - await waitFor(2000); - - const searchByReferenceIdAfterPatchPa1 = - await searchRegistrationByReferenceId( - '00dc9451-1273-484c-b2e8-ae21b51a96ab', - programIdOcw, - accessToken, - ); - const pa1AfterPatch = searchByReferenceIdAfterPatchPa1.body.data[0]; - - const searchByReferenceIdAfterPatchPa2 = - await searchRegistrationByReferenceId( - '01dc9451-1273-484c-b2e8-ae21b51a96ab', - programIdOcw, - accessToken, - ); - const pa2AfterPatch = searchByReferenceIdAfterPatchPa2.body.data[0]; - - // Assert - assertRegistrationImport(pa1AfterPatch, registrationDataThatWillChangePa1); - assertRegistrationImport(pa2AfterPatch, registrationDataThatWillChangePa2); - }); -}); diff --git a/services/121-service/test/registrations/pagination/get-registration-permission.test.ts b/services/121-service/test/registrations/pagination/get-registration-permission.test.ts index b422fa4d6c..875957c7f0 100644 --- a/services/121-service/test/registrations/pagination/get-registration-permission.test.ts +++ b/services/121-service/test/registrations/pagination/get-registration-permission.test.ts @@ -53,7 +53,8 @@ describe('Load PA table', () => { referenceId: registrationOCW1.referenceId, paymentAmountMultiplier: 1, preferredLanguage: registrationOCW1.preferredLanguage, - financialServiceProvider: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, }; const notExpectedValueObject = { fullName: registrationOCW1.fullName, diff --git a/services/121-service/test/registrations/pagination/get-registration.test.ts b/services/121-service/test/registrations/pagination/get-registration.test.ts index 38ce79fc05..7c437dfc82 100644 --- a/services/121-service/test/registrations/pagination/get-registration.test.ts +++ b/services/121-service/test/registrations/pagination/get-registration.test.ts @@ -1,3 +1,4 @@ +import { FinancialServiceProviderAttributes } from '@121-service/src/financial-service-providers/enum/financial-service-provider-attributes.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import { getRegistrations, @@ -18,13 +19,15 @@ import { describe('Load PA table', () => { describe('getting registration using paginate', () => { let accessToken: string; - const attribute1 = 'whatsappPhoneNumber'; - const attribute2 = 'addressCity'; + const attribute1 = FinancialServiceProviderAttributes.whatsappPhoneNumber; + const attribute2 = FinancialServiceProviderAttributes.addressCity; const attribute3 = 'referenceId'; const attributeName = 'name'; const attributeFullName = 'fullName'; - const attributeFspDisplayName = 'fspDisplayName'; - const attributeFinancelServiceProvider = 'financialServiceProvider'; + const attributeprogramFinancialServiceProviderConfigurationLabel = + 'programFinancialServiceProviderConfigurationLabel'; + const attributeProgramFinancialServiceProviderConfigurationName = + 'programFinancialServiceProviderConfigurationName'; beforeEach(async () => { await resetDB(SeedScript.nlrcMultiple); @@ -89,6 +92,7 @@ describe('Load PA table', () => { // Assert expect(data[0]).toHaveProperty(attributeName); expect(data[0]).toHaveProperty(attributeFullName); + expect(data[0]).not.toHaveProperty(attribute1); }); it('should only return full name', async () => { @@ -108,9 +112,11 @@ describe('Load PA table', () => { expect(data[0]).not.toHaveProperty(attributeFullName); }); - it('should only return fspDisplayName', async () => { + it('should only return programFinancialServiceProviderConfigurationLabel', async () => { // Arrange - const requestedDynamicAttributes = [attributeFspDisplayName]; + const requestedDynamicAttributes = [ + attributeprogramFinancialServiceProviderConfigurationLabel, + ]; // Act const getRegistrationsResponse = await getRegistrations({ @@ -121,8 +127,12 @@ describe('Load PA table', () => { const data = getRegistrationsResponse.body.data; // Assert - expect(data[0]).toHaveProperty(attributeFspDisplayName); - expect(data[0]).not.toHaveProperty(attributeFinancelServiceProvider); + expect(data[0]).toHaveProperty( + attributeprogramFinancialServiceProviderConfigurationLabel, + ); + expect(data[0]).not.toHaveProperty( + attributeProgramFinancialServiceProviderConfigurationName, + ); }); it('Should return specified amount of PA per page', async () => { diff --git a/services/121-service/test/registrations/pagination/pagination-data.ts b/services/121-service/test/registrations/pagination/pagination-data.ts index cbde4e8856..d16a2975a3 100644 --- a/services/121-service/test/registrations/pagination/pagination-data.ts +++ b/services/121-service/test/registrations/pagination/pagination-data.ts @@ -2,7 +2,7 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-pr import { RegistrationEntity } from '@121-service/src/registration/registration.entity'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; interface RegistrationWithFspName extends RegistrationEntity { - fspName?: string; + programFinancialServiceProviderConfigurationName?: string; } export function createExpectedValueObject( @@ -11,11 +11,9 @@ export function createExpectedValueObject( ): Partial { const expectedValueObject = { ...registration, - financialServiceProvider: registration.fspName, registrationProgramId: sequenceNumber, personAffectedSequence: `PA #${sequenceNumber}`, }; - delete expectedValueObject.fspName; return expectedValueObject; } @@ -31,7 +29,8 @@ export const registrationOCW1 = { paymentAmountMultiplier: 1, fullName: 'John Smith', phoneNumber: '14155236666', - fspName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, whatsappPhoneNumber: '14155238886', addressStreet: 'Teststraat', addressHouseNumber: '1', @@ -46,7 +45,8 @@ export const registrationOCW2 = { paymentAmountMultiplier: 1, fullName: 'Anna Hello', phoneNumber: '14155237775', - fspName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, whatsappPhoneNumber: '14155237775', addressStreet: 'Teststeeg', addressHouseNumber: '2', @@ -61,7 +61,8 @@ export const registrationOCW3 = { paymentAmountMultiplier: 2, fullName: 'Sophia Johnson', phoneNumber: '14155236666', - fspName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, whatsappPhoneNumber: '14155236666', addressStreet: 'DifferentStreet', addressHouseNumber: '3', @@ -76,7 +77,8 @@ export const registrationOCW4 = { paymentAmountMultiplier: 3, fullName: 'Luiz Garcia', phoneNumber: '14155235555', - fspName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, whatsappPhoneNumber: '14155235555', addressStreet: 'AnotherStreet', addressHouseNumber: '4', @@ -91,7 +93,8 @@ export const registrationOCW5 = { paymentAmountMultiplier: 3, fullName: 'Lars Larsson', phoneNumber: '14155235556', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, whatsappPhoneNumber: '14155235556', }; @@ -109,7 +112,8 @@ export const registrationPV5 = { paymentAmountMultiplier: 1, fullName: 'Gemma Houtenbos', phoneNumber: '14155235556', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, whatsappPhoneNumber: '14155235555', }; @@ -119,7 +123,8 @@ export const registrationPV6 = { paymentAmountMultiplier: 1, fullName: 'Jan Janssen', phoneNumber: '14155235551', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, whatsappPhoneNumber: '14155235551', }; @@ -129,7 +134,8 @@ export const registrationPV7 = { paymentAmountMultiplier: 1, fullName: 'Joost Herlembach', phoneNumber: '14155235551', - fspName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, whatsappPhoneNumber: '14155235551', addressStreet: 'Teststraat', addressHouseNumber: '1', @@ -144,7 +150,8 @@ export const registrationPV8 = { paymentAmountMultiplier: 1, fullName: 'Jack Strong', phoneNumber: '14155235557', - fspName: FinancialServiceProviders.intersolveVisa, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVisa, whatsappPhoneNumber: '14155235557', addressStreet: 'Teststraat', addressHouseNumber: '1', @@ -166,7 +173,8 @@ export const registrationPvScoped = { paymentAmountMultiplier: 1, fullName: 'Freya Midgard', phoneNumber: '14155235554', - fspName: FinancialServiceProviders.intersolveVoucherWhatsapp, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherWhatsapp, whatsappPhoneNumber: '14155235554', scope: 'utrecht', }; @@ -179,7 +187,7 @@ export const expectedAttributes = [ 'preferredLanguage', 'inclusionScore', 'paymentAmountMultiplier', - 'financialServiceProvider', + 'financialServiceProviderName', 'registrationProgramId', 'personAffectedSequence', 'name', @@ -190,12 +198,12 @@ export const registrationWesteros1 = { referenceId: 'westeros123456789', preferredLanguage: 'en', name: 'John Snow', - dob: '283-12-31', + dob: '31-08-1990', house: 'stark', dragon: 1, knowsNothing: true, phoneNumber: '14155235554', - fspName: 'Excel', + programFinancialServiceProviderConfigurationName: 'ironBank', whatsappPhoneNumber: '14155235554', motto: 'Winter is coming', }; @@ -204,19 +212,60 @@ export const registrationWesteros2 = { referenceId: 'westeros987654321', preferredLanguage: 'en', name: 'Arya Stark', - dob: '288-12-31', + dob: '31-08-1990', house: 'stark', dragon: 0, knowsNothing: false, phoneNumber: '14155235555', - fspName: 'Excel', + programFinancialServiceProviderConfigurationName: 'ironBank', whatsappPhoneNumber: '14155235555', motto: 'A girl has no name', }; +export const registrationWesteros3 = { + referenceId: 'westeros987654322', + preferredLanguage: 'en', + name: 'Jaime Lannister', + dob: '31-08-1990', + house: 'lannister', + dragon: 0, + knowsNothing: false, + phoneNumber: '14155235556', + programFinancialServiceProviderConfigurationName: 'gringotts', + whatsappPhoneNumber: '14155235555', + motto: 'A lanister always pays his debts', +}; + +export const registrationCbe = { + referenceId: 'registration-cbe-1', + phoneNumber: '14155238886', + preferredLanguage: LanguageEnum.en, + paymentAmountMultiplier: 1, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.commercialBankEthiopia, + maxPayments: 3, + fullName: 'ANDUALEM MOHAMMED YIMER', + idNumber: '39231855170', + age: '48', + gender: 'male', + howManyFemale: '1', + howManyMale: '2', + totalFamilyMembers: '3', + howManyFemaleUnder18: '1', + howManyMaleUnder18: '2', + howManyFemaleOver18: '1', + howManyMaleOver18: '1', + howManyFemaleDisabilityUnder18: '2', + howManyMaleDisabilityUnder18: '1', + howManyFemaleDisabilityOver18: '1', + howManyMaleDisabilityOver18: '2', + bankAccountNumber: '407951684723597', +}; + export const registrationSafaricom = { referenceId: '01dc9451-1273-484c-b2e8-ae21b51a96ab', - fspName: FinancialServiceProviders.safaricom, + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.safaricom, phoneNumber: '254708374149', preferredLanguage: LanguageEnum.en, paymentAmountMultiplier: 1, @@ -235,13 +284,13 @@ export const registrationSafaricom = { county: 'ethiopia', subCounty: 'ethiopia', ward: 'dsa', - location: 21, - subLocation: 2113, + location: '21', + subLocation: '2113', village: 'adis abea', - nearestSchool: 213321, + nearestSchool: '213321', areaType: 'urban', mainSourceLivelihood: 'salary_from_formal_employment', - mainSourceLivelihoodOther: 213, + mainSourceLivelihoodOther: '213', Male05: 1, Female05: 0, Male612: 0, @@ -263,24 +312,24 @@ export const registrationSafaricom = { habitableRooms: 0, tenureStatusOfDwelling: 'Owner occupied', ownerOccupiedState: 'purchased', - ownerOccupiedStateOther: 0, + ownerOccupiedStateOther: '0', rentedFrom: 'individual', - rentedFromOther: 0, + rentedFromOther: '0', constructionMaterialRoof: 'tin', - ifRoofOtherSpecify: 31213, + ifRoofOtherSpecify: '31213', constructionMaterialWall: 'tiles', - ifWallOtherSpecify: 231312, + ifWallOtherSpecify: '231312', constructionMaterialFloor: 'cement', ifFloorOtherSpecify: 'asdsd', dwellingRisk: 'fire', - ifRiskOtherSpecify: 123213, + ifRiskOtherSpecify: '123213', mainSourceOfWater: 'lake', ifWaterOtherSpecify: 'dasdas', pigs: 'no', ifYesPigs: 123123, chicken: 'no', mainModeHumanWasteDisposal: 'septic_tank', - ifHumanWasteOtherSpecify: 31213, + ifHumanWasteOtherSpecify: '31213', cookingFuel: 'electricity', ifFuelOtherSpecify: 'asdsda', Lighting: 'electricity', @@ -303,19 +352,19 @@ export const registrationSafaricom = { howManyDeaths: 0, householdConditions: 'poor', skipMeals: 'no', - receivingBenefits: 0, - ifYesNameProgramme: 0, + receivingBenefits: '0', + ifYesNameProgramme: '0', typeOfBenefit: 'in_kind', - ifOtherBenefit: 2123312, + ifOtherBenefit: '2123312', ifCash: 12312, - ifInKind: 132132, + ifInKind: '132132', feedbackOnRespons: 'no', - ifYesFeedback: 312123, + ifYesFeedback: '312123', whoDecidesHowToSpend: 'male_household_head', possibilityForConflicts: 'no', genderedDivision: 'no', ifYesElaborate: 'asddas', - geopoint: 123231, + geopoint: '123231', }; export const registrationsSafaricom = [registrationSafaricom]; diff --git a/services/121-service/test/registrations/registrations-by-phone-number.test.ts b/services/121-service/test/registrations/registrations-by-phone-number.test.ts index 47dd1d9699..dc892c11f8 100644 --- a/services/121-service/test/registrations/registrations-by-phone-number.test.ts +++ b/services/121-service/test/registrations/registrations-by-phone-number.test.ts @@ -1,6 +1,6 @@ import { HttpStatus } from '@nestjs/common'; -import { CustomDataAttributes } from '@121-service/src/registration/enum/custom-data-attributes'; +import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; import { DebugScope } from '@121-service/src/scripts/enum/debug-scope.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import { registrationVisa } from '@121-service/src/seed-data/mock/visa-card.data'; @@ -25,20 +25,21 @@ describe('/ Registrations - by phone-number', () => { const registrationOnlyPhoneNumber = { ...registrationNotScopedPv, referenceId: 'test-pa-1', - [CustomDataAttributes.phoneNumber]: '15005550010', - [CustomDataAttributes.whatsappPhoneNumber]: undefined, + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550010', + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: undefined, }; const registrationOnlyPhoneNumberUnique = { ...registrationNotScopedPv, referenceId: 'test-pa-with-different-phone-number', - [CustomDataAttributes.phoneNumber]: '15005550020', - [CustomDataAttributes.whatsappPhoneNumber]: undefined, + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550020', + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: undefined, }; const registrationOnlyPhoneNumberSame = { ...registrationNotScopedPv, referenceId: 'test-pa-with-same-phone-number', - [CustomDataAttributes.phoneNumber]: registrationOnlyPhoneNumber.phoneNumber, - [CustomDataAttributes.whatsappPhoneNumber]: undefined, + [DefaultRegistrationDataAttributeNames.phoneNumber]: + registrationOnlyPhoneNumber.phoneNumber, + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: undefined, }; beforeEach(async () => { @@ -74,7 +75,9 @@ describe('/ Registrations - by phone-number', () => { it('should return the correct registration', async () => { // Arrange const testPhoneNumber = - registrationOnlyPhoneNumberUnique[CustomDataAttributes.phoneNumber]; + registrationOnlyPhoneNumberUnique[ + DefaultRegistrationDataAttributeNames.phoneNumber + ]; // Act const response = await searchRegistrationByPhoneNumber( @@ -85,15 +88,17 @@ describe('/ Registrations - by phone-number', () => { // Assert expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.length).toBe(1); - expect(response.body[0][CustomDataAttributes.phoneNumber]).toBe( - testPhoneNumber, - ); + expect( + response.body[0][DefaultRegistrationDataAttributeNames.phoneNumber], + ).toBe(testPhoneNumber); }); it('should return all matching registrations', async () => { // Arrange const testPhoneNumber = - registrationOnlyPhoneNumber[CustomDataAttributes.phoneNumber]; + registrationOnlyPhoneNumber[ + DefaultRegistrationDataAttributeNames.phoneNumber + ]; // Act const response = await searchRegistrationByPhoneNumber( @@ -104,18 +109,20 @@ describe('/ Registrations - by phone-number', () => { // Assert expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.length).toBe(2); - expect(response.body[0][CustomDataAttributes.phoneNumber]).toBe( - testPhoneNumber, - ); - expect(response.body[1][CustomDataAttributes.phoneNumber]).toBe( - testPhoneNumber, - ); + expect( + response.body[0][DefaultRegistrationDataAttributeNames.phoneNumber], + ).toBe(testPhoneNumber); + expect( + response.body[1][DefaultRegistrationDataAttributeNames.phoneNumber], + ).toBe(testPhoneNumber); }); it('should find registration(s) with phonenumber "+"-notation', async () => { // Arrange const testPhoneNumber = - registrationOnlyPhoneNumberUnique[CustomDataAttributes.phoneNumber]; + registrationOnlyPhoneNumberUnique[ + DefaultRegistrationDataAttributeNames.phoneNumber + ]; // Act const response = await searchRegistrationByPhoneNumber( @@ -126,9 +133,9 @@ describe('/ Registrations - by phone-number', () => { // Assert expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.length).toBe(1); - expect(response.body[0][CustomDataAttributes.phoneNumber]).toBe( - testPhoneNumber, - ); + expect( + response.body[0][DefaultRegistrationDataAttributeNames.phoneNumber], + ).toBe(testPhoneNumber); }); it('should find registration with matching WhatsApp-phonenumber', async () => { @@ -137,8 +144,9 @@ describe('/ Registrations - by phone-number', () => { const registrationWithWhatsApp = { ...registrationVisa, referenceId: 'test-pa-with-same-whatsapp-number', - [CustomDataAttributes.phoneNumber]: '15005550050', - [CustomDataAttributes.whatsappPhoneNumber]: testPhoneNumber, + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550050', + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: + testPhoneNumber, }; await importRegistrations( programIdOCW, @@ -155,9 +163,11 @@ describe('/ Registrations - by phone-number', () => { // Assert expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.length).toBe(1); - expect(response.body[0][CustomDataAttributes.whatsappPhoneNumber]).toBe( - testPhoneNumber, - ); + expect( + response.body[0][ + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber + ], + ).toBe(testPhoneNumber); }); it('should find all registrations with matching WhatsApp/regular-phonenumber(s)', async () => { @@ -166,8 +176,9 @@ describe('/ Registrations - by phone-number', () => { const registrationWithSameWhatsApp = { ...registrationVisa, referenceId: 'test-pa-with-same-whatsapp-number', - [CustomDataAttributes.phoneNumber]: '15005550050', - [CustomDataAttributes.whatsappPhoneNumber]: testPhoneNumber, + [DefaultRegistrationDataAttributeNames.phoneNumber]: '15005550050', + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: + testPhoneNumber, }; await importRegistrations( programIdOCW, @@ -186,14 +197,18 @@ describe('/ Registrations - by phone-number', () => { expect(response.body.length).toBe(2); expect( [ - response.body[0][CustomDataAttributes.whatsappPhoneNumber], - response.body[0][CustomDataAttributes.phoneNumber], + response.body[0][ + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber + ], + response.body[0][DefaultRegistrationDataAttributeNames.phoneNumber], ].includes(testPhoneNumber), ).toBe(true); expect( [ - response.body[1][CustomDataAttributes.whatsappPhoneNumber], - response.body[1][CustomDataAttributes.phoneNumber], + response.body[1][ + DefaultRegistrationDataAttributeNames.whatsappPhoneNumber + ], + response.body[1][DefaultRegistrationDataAttributeNames.phoneNumber], ].includes(testPhoneNumber), ).toBe(true); }); @@ -204,8 +219,9 @@ describe('/ Registrations - by phone-number', () => { const registrationInOtherProgram = { ...registrationVisa, referenceId: 'test-pa-with-same-phone-number-in-other-program', - [CustomDataAttributes.phoneNumber]: testPhoneNumber, - [CustomDataAttributes.whatsappPhoneNumber]: '15005550300', + [DefaultRegistrationDataAttributeNames.phoneNumber]: testPhoneNumber, + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: + '15005550300', }; await importRegistrations( programIdOCW, @@ -222,24 +238,27 @@ describe('/ Registrations - by phone-number', () => { // Assert expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.length).toBe(2); - expect(response.body[0][CustomDataAttributes.phoneNumber]).toBe( - testPhoneNumber, - ); - expect(response.body[1][CustomDataAttributes.phoneNumber]).toBe( - testPhoneNumber, - ); + expect( + response.body[0][DefaultRegistrationDataAttributeNames.phoneNumber], + ).toBe(testPhoneNumber); + expect( + response.body[1][DefaultRegistrationDataAttributeNames.phoneNumber], + ).toBe(testPhoneNumber); }); it('should only find registrations with matching scope', async () => { //Arrange const testPhoneNumber = - registrationOnlyPhoneNumberUnique[CustomDataAttributes.phoneNumber]; + registrationOnlyPhoneNumberUnique[ + DefaultRegistrationDataAttributeNames.phoneNumber + ]; const registrationInOtherProgramZeelandMiddelburg = { ...registrationVisa, referenceId: 'test-pa-with-same-phone-number-in-other-program-zeeland-middelburg', - [CustomDataAttributes.phoneNumber]: testPhoneNumber, - [CustomDataAttributes.whatsappPhoneNumber]: '15005550201', + [DefaultRegistrationDataAttributeNames.phoneNumber]: testPhoneNumber, + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: + '15005550201', scope: DebugScope.ZeelandMiddelburg, }; @@ -247,8 +266,9 @@ describe('/ Registrations - by phone-number', () => { ...registrationVisa, referenceId: 'test-pa-with-same-phone-number-in-other-program-utrecht-houten', - [CustomDataAttributes.phoneNumber]: testPhoneNumber, - [CustomDataAttributes.whatsappPhoneNumber]: '15005550202', + [DefaultRegistrationDataAttributeNames.phoneNumber]: testPhoneNumber, + [DefaultRegistrationDataAttributeNames.whatsappPhoneNumber]: + '15005550202', scope: DebugScope.UtrechtHouten, }; @@ -273,8 +293,8 @@ describe('/ Registrations - by phone-number', () => { // Assert expect(response.statusCode).toBe(HttpStatus.OK); expect(response.body.length).toBe(1); - expect(response.body[0][CustomDataAttributes.phoneNumber]).toBe( - testPhoneNumber, - ); + expect( + response.body[0][DefaultRegistrationDataAttributeNames.phoneNumber], + ).toBe(testPhoneNumber); }); }); diff --git a/services/121-service/test/registrations/send-message-with-placeholder.test.ts b/services/121-service/test/registrations/send-message-with-placeholder.test.ts index 891b2508c0..7b25351b90 100644 --- a/services/121-service/test/registrations/send-message-with-placeholder.test.ts +++ b/services/121-service/test/registrations/send-message-with-placeholder.test.ts @@ -1,6 +1,6 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { getFinancialServiceProviderSettingByNameOrThrow } from '@121-service/src/financial-service-providers/financial-service-provider-settings.helpers'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; -import fspIntersolveJson from '@121-service/src/seed-data/fsp/fsp-intersolve-voucher-paper.json'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { waitForMessagesToComplete } from '@121-service/test/helpers/program.helper'; import { @@ -22,7 +22,8 @@ describe('Send custom message with placeholders', () => { paymentAmountMultiplier: 2, fullName: 'John Smith', phoneNumber: '14155238886', - fspName: FinancialServiceProviders.intersolveVoucherPaper, // use SMS PA, so that template directly arrives + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherPaper, // use SMS PA, so that template directly arrives namePartnerOrganization: 'Test organization', maxPayments: 2, paymentCountRemaining: 2, @@ -40,7 +41,7 @@ describe('Send custom message with placeholders', () => { it('should send message with placeholder values processed', async () => { // Arrange const message = - 'This is a test message with {{namePartnerOrganization}} and {{paymentAmountMultiplier}} and {{fspDisplayName}} and {{fullName}} and {{paymentCountRemaining}}'; + 'This is a test message with {{namePartnerOrganization}} and {{paymentAmountMultiplier}} and {{programFinancialServiceProviderConfigurationLabel}} and {{fullName}} and {{paymentCountRemaining}}'; // Act await sendMessage( @@ -73,9 +74,13 @@ describe('Send custom message with placeholders', () => { new RegExp('{{paymentAmountMultiplier}}', 'g'), String(registrationAh.paymentAmountMultiplier), ); + const labelInPreferredLanguage = + getFinancialServiceProviderSettingByNameOrThrow( + FinancialServiceProviders.intersolveVoucherPaper, + ).defaultLabel[registrationAh.preferredLanguage]; processedMessage = processedMessage.replace( - new RegExp('{{fspDisplayName}}', 'g'), - fspIntersolveJson.displayName[registrationAh.preferredLanguage], + new RegExp('{{programFinancialServiceProviderConfigurationLabel}}', 'g'), + labelInPreferredLanguage!, ); processedMessage = processedMessage.replace( new RegExp('{{fullName}}', 'g'), diff --git a/services/121-service/test/registrations/send-templated-message.test.ts b/services/121-service/test/registrations/send-templated-message.test.ts index 117105b8db..dd0a15164a 100644 --- a/services/121-service/test/registrations/send-templated-message.test.ts +++ b/services/121-service/test/registrations/send-templated-message.test.ts @@ -33,7 +33,8 @@ describe('Sending templated message', () => { nameFirst: 'John', nameLast: 'Smith', phoneNumber: '14155238886', - fspName: FinancialServiceProviders.intersolveVoucherPaper, // use SMS PA, so that template directly arrives + programFinancialServiceProviderConfigurationName: + FinancialServiceProviders.intersolveVoucherPaper, // use SMS PA, so that template directly arrives namePartnerOrganization: 'Test organization', }; diff --git a/services/121-service/test/registrations/update-registration-program-financial-service-provider-configuration.test.ts b/services/121-service/test/registrations/update-registration-program-financial-service-provider-configuration.test.ts new file mode 100644 index 0000000000..3c1a579604 --- /dev/null +++ b/services/121-service/test/registrations/update-registration-program-financial-service-provider-configuration.test.ts @@ -0,0 +1,255 @@ +import { HttpStatus } from '@nestjs/common'; + +import { + FinancialServiceProviderConfigurationProperties, + FinancialServiceProviders, +} from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; +import { CreateProgramFinancialServiceProviderConfigurationDto } from '@121-service/src/program-financial-service-provider-configurations/dtos/create-program-financial-service-provider-configuration.dto'; +import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; +import { registrationVisa } from '@121-service/src/seed-data/mock/visa-card.data'; +import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; +import { DefaultUserRole } from '@121-service/src/user/user-role.enum'; +import { + doPayment, + getTransactions, + waitForPaymentTransactionsToComplete, +} from '@121-service/test/helpers/program.helper'; +import { postProgramFinancialServiceProviderConfiguration } from '@121-service/test/helpers/program-financial-service-provider-configuration.helper'; +import { + importRegistrations, + seedIncludedRegistrations, + updateRegistration, +} from '@121-service/test/helpers/registration.helper'; +import { + getAccessToken, + removePermissionsFromRole, + resetDB, +} from '@121-service/test/helpers/utility.helper'; +import { registrationPvScoped } from '@121-service/test/registrations/pagination/pagination-data'; + +const programIdPv = 2; +const programIdOcw = 3; + +async function setupNlrcEnvironment() { + await resetDB(SeedScript.nlrcMultiple); + const accessToken = await getAccessToken(); + + await importRegistrations(programIdOcw, [registrationVisa], accessToken); + await importRegistrations(programIdPv, [registrationPvScoped], accessToken); + + return accessToken; +} + +describe('Update program financial servce provider configuration of PA', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await setupNlrcEnvironment(); + }); + + it('should succeed when updating program financial servce provider configuration when all required properties of new FSP are already present', async () => { + // Arrange + await setupNlrcEnvironment(); + + // Intersolve-visa and Intersolve-voucher-whatsapp both have whatsappPhoneNumber as required + const newProgramFinancialServiceProviderConfigurationName = + 'Intersolve-voucher-whatsapp'; + const dataUpdate = { + programFinancialServiceProviderConfigurationName: + newProgramFinancialServiceProviderConfigurationName, + }; + const reason = 'automated test'; + + // Act + const response = await updateRegistration( + programIdOcw, + registrationVisa.referenceId, + dataUpdate, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.OK); + expect(response.body.financialServiceProviderName).toBe( + newProgramFinancialServiceProviderConfigurationName, + ); + }); + + it('should fail when updating program financial servce provider configuration when a required property of new FSP is not yet present', async () => { + // Arrange + await setupNlrcEnvironment(); + + // Intersolve-voucher-whtatsapp does not have e.g. addressStreet which is required for Intersolve-visa + const newProgramFinancialServiceProviderConfigurationName = + 'Intersolve-visa'; + const dataUpdate = { + programFinancialServiceProviderConfigurationName: + newProgramFinancialServiceProviderConfigurationName, + }; + const reason = 'automated test'; + + // Act + const response = await updateRegistration( + programIdPv, + registrationPvScoped.referenceId, + dataUpdate, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(response.body).toMatchSnapshot(); + }); + + it('should succeed when updating program financial servce provider configuration when missing required properties of new FSP are passed along with the request', async () => { + // Arrange + await setupNlrcEnvironment(); + + // Intersolve-voucher-whtatsapp does not have e.g. addressStreet which is required for Intersolve-visa + // The missing attributes can be passed along (or can be updated first) + const newProgramFinancialServiceProviderConfigurationName = + 'Intersolve-visa'; + const dataUpdate = { + programFinancialServiceProviderConfigurationName: + newProgramFinancialServiceProviderConfigurationName, + addressStreet: 'Teststraat 1', + addressHouseNumber: '1', + addressCity: 'Teststad', + addressPostalCode: '1234AB', + }; + const reason = 'automated test'; + + // Act + const response = await updateRegistration( + programIdPv, + registrationPvScoped.referenceId, + dataUpdate, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.OK); + expect(response.body.programFinancialServiceProviderConfigurationName).toBe( + newProgramFinancialServiceProviderConfigurationName, + ); + }); + + it('should fail when updating program financial servce provider configuration without right permission', async () => { + // Arrange + await setupNlrcEnvironment(); + + await removePermissionsFromRole(DefaultUserRole.Admin, [ + PermissionEnum.RegistrationFspConfigUPDATE, + ]); + accessToken = await getAccessToken(); + + // Intersolve-visa and Intersolve-voucher-whatsapp both have 'whatsappPhoneNumber' as required, so this would succeed apart from the permission + const newProgramFinancialServiceProviderConfigurationName = + 'Intersolve-voucher-whatsapp'; + const dataUpdate = { + programFinancialServiceProviderConfigurationName: + newProgramFinancialServiceProviderConfigurationName, + }; + const reason = 'automated test'; + + // Act + const response = await updateRegistration( + programIdOcw, + registrationVisa.referenceId, + dataUpdate, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.FORBIDDEN); + }); + + it('should succeed updating registration to a newly added FSP config of which the name is not the same as the FSP and doing a payment', async () => { + // Arrange + const payment = 1; + await resetDB(SeedScript.nlrcMultiple); + await seedIncludedRegistrations( + [registrationPvScoped], + programIdPv, + accessToken, + ); + + const newProgramFinancialServiceProviderConfigurationName = + 'VoucherNumberTwo'; + const fspConfigBody: CreateProgramFinancialServiceProviderConfigurationDto = + { + name: newProgramFinancialServiceProviderConfigurationName, + financialServiceProviderName: + FinancialServiceProviders.intersolveVoucherWhatsapp, + label: { en: 'Voucher number 2' }, + properties: [ + { + name: FinancialServiceProviderConfigurationProperties.password, + value: 'password', + }, + { + name: FinancialServiceProviderConfigurationProperties.username, + value: 'username', + }, + ], + }; + await postProgramFinancialServiceProviderConfiguration({ + programId: programIdPv, + body: fspConfigBody, + accessToken, + }); + + const dataUpdate = { + programFinancialServiceProviderConfigurationName: + newProgramFinancialServiceProviderConfigurationName, + }; + const reason = 'automated test'; + + // Act + const response = await updateRegistration( + programIdPv, + registrationPvScoped.referenceId, + dataUpdate, + reason, + accessToken, + ); + await doPayment( + programIdPv, + payment, + 15, + [registrationPvScoped.referenceId], + accessToken, + ); + await waitForPaymentTransactionsToComplete( + programIdPv, + [registrationPvScoped.referenceId], + accessToken, + 30_000, + ); + + const transactionsResponse = await getTransactions( + programIdPv, + payment, + registrationPvScoped.referenceId, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.OK); + expect(response.body.programFinancialServiceProviderConfigurationName).toBe( + newProgramFinancialServiceProviderConfigurationName, + ); + expect(transactionsResponse.text).toContain('success'); + expect( + transactionsResponse.body[0] + .programFinancialServiceProviderConfigurationName, + ).toBe(newProgramFinancialServiceProviderConfigurationName); + expect(transactionsResponse.body[0].financialServiceProviderName).toBe( + FinancialServiceProviders.intersolveVoucherWhatsapp, + ); + }); +}); diff --git a/services/121-service/test/registrations/update-registration.test.ts b/services/121-service/test/registrations/update-registration.test.ts index 41e0721c4f..72f7ad6cf4 100644 --- a/services/121-service/test/registrations/update-registration.test.ts +++ b/services/121-service/test/registrations/update-registration.test.ts @@ -3,6 +3,11 @@ import { HttpStatus } from '@nestjs/common'; import { DebugScope } from '@121-service/src/scripts/enum/debug-scope.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import { registrationVisa } from '@121-service/src/seed-data/mock/visa-card.data'; +import { + patchProgram, + patchProgramRegistrationAttribute, + setAllProgramsRegistrationAttributesNonRequired, +} from '@121-service/test/helpers/program.helper'; import { importRegistrations, searchRegistrationByReferenceId, @@ -13,14 +18,27 @@ import { getAccessTokenScoped, resetDB, } from '@121-service/test/helpers/utility.helper'; -import { registrationPvScoped } from '@121-service/test/registrations/pagination/pagination-data'; +import { + programIdWesteros, + registrationPvScoped, + registrationWesteros1, +} from '@121-service/test/registrations/pagination/pagination-data'; const updatePhoneNumber = '15005550099'; +const programIdPv = 2; +const programIdOcw = 3; -describe('Update attribute of PA', () => { - const programIdPv = 2; - const programIdOcw = 3; +async function setupNlrcEnvironment() { + await resetDB(SeedScript.nlrcMultiple); + const accessToken = await getAccessToken(); + await importRegistrations(programIdOcw, [registrationVisa], accessToken); + await importRegistrations(programIdPv, [registrationPvScoped], accessToken); + + return accessToken; +} + +describe('Update attribute of PA', () => { let accessToken: string; beforeEach(async () => { @@ -33,6 +51,7 @@ describe('Update attribute of PA', () => { it('should not update unknown registration', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const wrongReferenceId = registrationVisa.referenceId + '-fail-test'; const updatePhoneData = { phoneNumber: updatePhoneNumber, @@ -49,11 +68,12 @@ describe('Update attribute of PA', () => { ); // Assert - expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(response.statusCode).toBe(HttpStatus.NOT_FOUND); }); - it('should succesfully update', async () => { + it('should succesfully update multiple fields', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const reason = 'automated test'; const dataUpdateSucces = { phoneNumber: updatePhoneNumber, @@ -64,8 +84,8 @@ describe('Update attribute of PA', () => { // Act const response = await updateRegistration( - programIdOcw, - registrationVisa.referenceId, + programIdPv, + registrationPvScoped.referenceId, dataUpdateSucces, reason, accessToken, @@ -75,8 +95,8 @@ describe('Update attribute of PA', () => { expect(response.statusCode).toBe(HttpStatus.OK); const result = await searchRegistrationByReferenceId( - registrationVisa.referenceId, - programIdOcw, + registrationPvScoped.referenceId, + programIdPv, accessToken, ); const registration = result.body.data[0]; @@ -87,12 +107,16 @@ describe('Update attribute of PA', () => { dataUpdateSucces.paymentAmountMultiplier, ); // Is old data still the same? - expect(registration.fullName).toBe(registrationVisa.fullName); + expect(registration.preferredLanguage).toBe( + registrationPvScoped.preferredLanguage, + ); }); it('should fail on wrong phonenumber', async () => { // Arrange - const updatePhoneNumber = '150'; + accessToken = await setupNlrcEnvironment(); + // Uses MockPhoneNumbers.LookupFail phonenumber + const updatePhoneNumber = '16005550005'; const dataUpdatePhoneFail = { fullName: 'Jane', phoneNumber: updatePhoneNumber, @@ -124,85 +148,9 @@ describe('Update attribute of PA', () => { expect(registration.fullName).toBe(registrationVisa.fullName); }); - it('should fail on duplicate referenceId', async () => { - // Arrange - const registrationVisa2 = { - ...registrationVisa, - referenceId: 'duplicate-reference-id', - }; - await importRegistrations(programIdOcw, [registrationVisa2], accessToken); - const dataUpdateReferenceIdFail = { - fullName: 'Jane', - referenceId: registrationVisa2.referenceId, - }; - const reason = 'automated test'; - - // Act - const response = await updateRegistration( - programIdOcw, - registrationVisa.referenceId, - dataUpdateReferenceIdFail, - reason, - accessToken, - ); - - // Assert - expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); - - const result = await searchRegistrationByReferenceId( - registrationVisa.referenceId, - programIdOcw, - accessToken, - ); - const registration = result.body.data[0]; - - expect(registration.phoneNumber).toBe(registrationVisa.phoneNumber); - expect(registration.fullName).toBe(registrationVisa.fullName); - expect(registration.paymentAmountMultiplier).toBe( - registrationVisa.paymentAmountMultiplier, - ); - - // Is old data still the same? - expect(registration.fullName).toBe(registrationVisa.fullName); - }); - - it('should fail on short referenceId', async () => { - // Arrange - const registrationVisa2 = { - ...registrationVisa, - referenceId: 'shor', //t - }; - await importRegistrations(programIdOcw, [registrationVisa2], accessToken); - const dataUpdateReferenceIdFail = { - fullName: 'Jane', - referenceId: registrationVisa2.referenceId, - }; - const reason = 'automated test'; - - // Act - const response = await updateRegistration( - programIdOcw, - registrationVisa.referenceId, - dataUpdateReferenceIdFail, - reason, - accessToken, - ); - - // Assert - expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); - const result = await searchRegistrationByReferenceId( - registrationVisa.referenceId, - programIdOcw, - accessToken, - ); - const registration = result.body.data[0]; - - // Is old data still the same? - expect(registration.fullName).toBe(registrationVisa.fullName); - }); - it('should fail on updating financial data without the right permission', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const dataUpdateFinanancialFail = { paymentAmountMultiplier: 5, referenceId: registrationVisa.referenceId, @@ -239,6 +187,7 @@ describe('Update attribute of PA', () => { it('should fail on updating non financial data without the right permission', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const dataUpdateNonFinanancialFail = { phoneNumber: 5, referenceId: registrationVisa.referenceId, @@ -273,6 +222,7 @@ describe('Update attribute of PA', () => { it('should update scope within current users scope', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const newScope = 'utrecht.houten'; const reason = 'automated test'; const updateDto = { @@ -302,6 +252,7 @@ describe('Update attribute of PA', () => { it('should not update scope outside current users scope', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const oldScope = registrationPvScoped.scope; const newScope = 'zeeland'; const reason = 'automated test'; @@ -329,4 +280,155 @@ describe('Update attribute of PA', () => { expect(updateResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); expect(registration.scope).toBe(oldScope); }); + + it('should fail on removing a program registration attribute which is part of the fsp config and required', async () => { + // Arrange + accessToken = await setupNlrcEnvironment(); + const dataUpdateStreetFail = { + addressStreet: null, + addressCity: 'Zaandam', + }; + + const reason = 'automated test'; + // Act + const response = await updateRegistration( + programIdOcw, + registrationVisa.referenceId, + dataUpdateStreetFail, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); + const result = await searchRegistrationByReferenceId( + registrationVisa.referenceId, + programIdOcw, + accessToken, + ); + const registration = result.body.data[0]; + + // Is old data still the same? + expect(registration.addressStreet).toBe(registrationVisa.addressStreet); + expect(registration.addressCity).toBe(registrationVisa.addressCity); + }); + + it('should succeed on removing a program registration attribute which is part of the fsp config but not-required', async () => { + // Arrange + accessToken = await setupNlrcEnvironment(); + const dataUpdateAdditionSuccess = { + addressHouseNumberAddition: null, + }; + + const reason = 'automated test'; + // Act + const response = await updateRegistration( + programIdOcw, + registrationVisa.referenceId, + dataUpdateAdditionSuccess, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.OK); + const result = await searchRegistrationByReferenceId( + registrationVisa.referenceId, + programIdOcw, + accessToken, + ); + const registration = result.body.data[0]; + + expect(registration.addressHouseNumberAddition == null).toBe(true); + }); + + it('should succeed on removing all program registration attributes of test program', async () => { + // Arrange + await resetDB(SeedScript.testMultiple); + accessToken = await getAccessToken(); + await importRegistrations( + programIdWesteros, + [registrationWesteros1], + accessToken, + ); + const programUpdate = { + allowEmptyPhoneNumber: true, + }; + await patchProgram(programIdWesteros, programUpdate, accessToken); + + await setAllProgramsRegistrationAttributesNonRequired( + programIdWesteros, + accessToken, + ); + + const dataUpdateToEmpty = { + healthArea: null, + dob: null, + house: null, + dragon: null, + knowsNothing: null, + phoneNumber: null, + whatsappPhoneNumber: null, + motto: null, + }; + + const reason = 'automated test'; + // Act + const response = await updateRegistration( + programIdWesteros, + registrationWesteros1.referenceId, + dataUpdateToEmpty, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.OK); + const result = await searchRegistrationByReferenceId( + registrationWesteros1.referenceId, + programIdWesteros, + accessToken, + ); + const registration = result.body.data[0]; + for (const key in dataUpdateToEmpty) { + // Check if value is null + expect(registration[key]).toBe(null); + } + }); + + it('should fail when removing a required program registration attribute', async () => { + // Arrange + await resetDB(SeedScript.testMultiple); + accessToken = await getAccessToken(); + await importRegistrations( + programIdWesteros, + [registrationWesteros1], + accessToken, + ); + + await patchProgramRegistrationAttribute({ + programRegistrationAttributeName: 'motto', + programRegistrationAttribute: { isRequired: true }, + programId: programIdWesteros, + accessToken, + }); + + const dataUpdateToEmpty = { + motto: null, + }; + + const reason = 'automated test'; + // Act + const response = await updateRegistration( + programIdWesteros, + registrationWesteros1.referenceId, + dataUpdateToEmpty, + reason, + accessToken, + ); + + // Assert + expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST); + expect(response.body).toMatchSnapshot(); + }); }); diff --git a/services/121-service/test/tsconfig.json b/services/121-service/test/tsconfig.json index 21ba23bc8c..a751088a52 100644 --- a/services/121-service/test/tsconfig.json +++ b/services/121-service/test/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "types": ["node", "jest", "supertest"] + "types": ["node", "jest", "supertest", "Multer"] }, "exclude": ["../node_modules", "../dist", "../src/**/*"], "include": ["**/*.ts"], diff --git a/services/121-service/test/users/user-roles.test.ts b/services/121-service/test/users/user-roles.test.ts index 65182dd502..c3cc11f8e9 100644 --- a/services/121-service/test/users/user-roles.test.ts +++ b/services/121-service/test/users/user-roles.test.ts @@ -27,11 +27,7 @@ describe('/ Users', () => { role: 'test-manager', label: 'Do stuff with certain permissions', description: 'This is a test role', - permissions: [ - 'program.update', - 'program:custom-attribute.update', - 'program:metrics.read', - ], + permissions: ['program.update', 'program:metrics.read'], }); // Assert @@ -50,11 +46,7 @@ describe('/ Users', () => { role: roleId, label: 'Do stuff with certain permissions', description: 'This is a test role', - permissions: [ - 'program.update', - 'program:custom-attribute.update', - 'program:metrics.read', - ], + permissions: ['program.update', 'program:metrics.read'], }); // Assert @@ -68,11 +60,7 @@ describe('/ Users', () => { role: roleId, label: 'Do stuff with certain permissions', description: 'This is a test role', - permissions: [ - 'program.update', - 'program:custom-attribute.update', - 'program:metrics.read', - ], + permissions: ['program.update', 'program:metrics.read'], }); // Assert diff --git a/services/121-service/test/visa-card/block-visa-card.test.ts b/services/121-service/test/visa-card/block-visa-card.test.ts index ce90d4cc91..96b63336ce 100644 --- a/services/121-service/test/visa-card/block-visa-card.test.ts +++ b/services/121-service/test/visa-card/block-visa-card.test.ts @@ -23,7 +23,7 @@ import { resetDB, } from '@121-service/test/helpers/utility.helper'; -describe('Block visa debit card', () => { +describe('(Un)Block visa debit card', () => { let accessToken: string; beforeEach(async () => { @@ -79,8 +79,8 @@ describe('Block visa debit card', () => { registrationVisa.referenceId, accessToken, ); - // Assert + // Assert expect(blockVisaResponse.status).toBe(200); expect(visaWalletResponseAfterBlock.body.cards[0].status).toBe( VisaCard121Status.Paused, diff --git a/services/121-service/tsconfig.json b/services/121-service/tsconfig.json index 4c0bcb7108..0e331ef452 100644 --- a/services/121-service/tsconfig.json +++ b/services/121-service/tsconfig.json @@ -25,7 +25,7 @@ "checkJs": true, "incremental": true, "outDir": "./dist", - "types": ["express", "node"], + "types": ["express", "node", "jest"], "typeRoots": ["./node_modules/@types"], "resolveJsonModule": true, "baseUrl": "./", diff --git a/services/mock-service/src/twilio/twilio.service.ts b/services/mock-service/src/twilio/twilio.service.ts index 289bb25958..95563c5b7b 100644 --- a/services/mock-service/src/twilio/twilio.service.ts +++ b/services/mock-service/src/twilio/twilio.service.ts @@ -23,6 +23,7 @@ enum MockPhoneNumbers { NoIncomingYesMessage = '16005550002', FailFaultyTemplateError = '16005550003', FailNoWhatsAppNumber = '16005550004', + LookupFail = '16005550005', } // See: services/121-service/src/notifications/enum/message-type.enum.ts @@ -37,6 +38,12 @@ export class TwilioService { if (!phoneNumber) { phoneNumber = '+31600000000'; } + if (phoneNumber.includes(MockPhoneNumbers.LookupFail)) { + return { + phone_number: null, + national_format: null, + }; + } await setTimeoutQueue(400); // This is the average time of an actual twilio lookup return { phone_number: phoneNumber, @@ -78,6 +85,7 @@ export class TwilioService { // 1. First loop through different error rseponses and return early if ( !twilioMessagesCreateDto.To || + twilioMessagesCreateDto.To === 'not available' || // this relates to 'to' being set to 'not available' in the sms.service > send Sms() twilioMessagesCreateDto.To.includes(MockPhoneNumbers.FailGeneric) ) { response.status = TwilioStatus.failed;