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 index 6b2bd03d99..31032d6db5 100644 --- a/e2e/test-registration-data/test-registrations-test-westeros-1000.csv +++ b/e2e/test-registration-data/test-registrations-test-westeros-1000.csv @@ -1,4 +1,4 @@ -phoneNumber,preferredLanguage,fspName,knowsNothing,textCustomAttribute,name,dob,house,dragon,skills,whatsappPhoneNumber,personalId,fixedChoice,openAnswer,date,accountId +phoneNumber,preferredLanguage,programFinancialServiceProviderConfigurationName,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 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 7598182fe1..69ec5f0661 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 @@ -209,10 +209,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 = diff --git a/services/121-service/jest.unit.config.js b/services/121-service/jest.unit.config.js index b4cc547d9c..cb90152973 100644 --- a/services/121-service/jest.unit.config.js +++ b/services/121-service/jest.unit.config.js @@ -3,6 +3,9 @@ module.exports = { preset: 'ts-jest', rootDir: '.', testMatch: ['/**/*.spec.ts'], + transform: { + '^.+\\.ts?$': ['ts-jest', { tsconfig: '/test/tsconfig.json' }], + }, coverageReporters: ['json', 'lcov'], modulePathIgnorePatterns: ['/dist/'], moduleNameMapper: { diff --git a/services/121-service/package-lock.json b/services/121-service/package-lock.json index d3b6bfc866..b51b069148 100644 --- a/services/121-service/package-lock.json +++ b/services/121-service/package-lock.json @@ -21,6 +21,7 @@ "@nestjs/typeorm": "^10.0.2", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.7", + "@types/multer": "^1.4.12", "applicationinsights": "^2.9.5", "bull": "^4.16.2", "bwip-js": "^2.0.11", @@ -5704,7 +5705,6 @@ "version": "1.4.12", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", - "dev": true, "dependencies": { "@types/express": "*" } diff --git a/services/121-service/package.json b/services/121-service/package.json index ebaa7194c1..9c1bb22df5 100644 --- a/services/121-service/package.json +++ b/services/121-service/package.json @@ -54,6 +54,7 @@ "@nestjs/typeorm": "^10.0.2", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.7", + "@types/multer": "^1.4.12", "applicationinsights": "^2.9.5", "bull": "^4.16.2", "bwip-js": "^2.0.11", 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 b65c08d658..18174982d1 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 @@ -17,7 +17,6 @@ import { getQueueName } from '@121-service/src/utils/unit-test.helpers'; const programId = 3; const paymentNr = 5; const userId = 1; -const mockCredentials = { username: '1234', password: '1234' }; const sendPaymentData: PaPaymentDataDto[] = [ { transactionAmount: 22, 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 e72d65fdbd..1906ae85d4 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 @@ -313,7 +313,7 @@ export class ExcelService ); } - private joinRegistrationsAndImportRecords( + public joinRegistrationsAndImportRecords( registrations: Awaited< ReturnType >, 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 f54760ea5d..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 @@ -620,7 +620,7 @@ export class IntersolveVisaService public async hasIntersolveCustomer(registrationId: number): Promise { const count = await this.intersolveVisaCustomerScopedRepository.count({ where: { - registrationId: registrationId, + registrationId: Equal(registrationId), }, }); return count > 0; 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 2d2a38f344..8dcbbd873d 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 @@ -13,7 +13,6 @@ const programId = 3; const paymentNr = 5; const usernameValue = '1234'; const passwordValue = '4567'; -const mockCredentials = { username: usernameValue, password: passwordValue }; const sendPaymentData: PaPaymentDataDto[] = [ { transactionAmount: 22, 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 8e5751a611..cfea563c34 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,11 +1,11 @@ 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'; -import { Relation } from 'typeorm'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; export class TransactionReturnDto { @ApiProperty({ 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 index 86dc878b65..6aa4fea131 100644 --- 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 @@ -9,11 +9,10 @@ import { } from 'typeorm'; import { CascadeDeleteEntity } from '@121-service/src/base.entity'; -import { ProgramEntity } from '@121-service/src/programs/program.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/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', [ diff --git a/services/121-service/src/programs/dto/program-registration-attribute.dto.ts b/services/121-service/src/programs/dto/program-registration-attribute.dto.ts index 3801fa07f7..5e2b33f9c6 100644 --- a/services/121-service/src/programs/dto/program-registration-attribute.dto.ts +++ b/services/121-service/src/programs/dto/program-registration-attribute.dto.ts @@ -19,35 +19,36 @@ import { LocalizedString } from '@121-service/src/shared/types/localized-string. import { WrapperType } from '@121-service/src/wrapper.type'; class BaseProgramRegistrationAttributeDto { - @ApiProperty({}) - @IsNotEmpty() - @IsString() - public readonly name: string; - @ApiProperty({ required: false }) @ValidateIf((o) => o.answerType === 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 +56,7 @@ class BaseProgramRegistrationAttributeDto { @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 +65,7 @@ class BaseProgramRegistrationAttributeDto { }) @IsOptional() public placeholder?: LocalizedString; + @ApiProperty({ example: false, required: false, @@ -72,6 +75,11 @@ class BaseProgramRegistrationAttributeDto { } export class ProgramRegistrationAttributeDto extends BaseProgramRegistrationAttributeDto { + @ApiProperty({}) + @IsNotEmpty() + @IsString() + public readonly name: string; + @ApiProperty({ example: { en: 'Please enter your last name:', diff --git a/services/121-service/src/programs/program.entity.ts b/services/121-service/src/programs/program.entity.ts index 448a72a40c..f83fd09277 100644 --- a/services/121-service/src/programs/program.entity.ts +++ b/services/121-service/src/programs/program.entity.ts @@ -1,19 +1,13 @@ 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 { 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 { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity'; import { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; -import { Attributes } from '@121-service/src/registration/dto/update-registration.dto'; -import { - Attribute, - RegistrationAttributeTypes, -} from '@121-service/src/registration/enum/registration-attribute.enum'; +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'; @@ -155,64 +149,4 @@ export class ProgramEntity extends CascadeDeleteEntity { }, ]); } - - public async getValidationInfoForAttributeName( - name: string, - ): Promise { - if (name === Attributes.paymentAmountMultiplier) { - return { type: RegistrationAttributeTypes.numeric }; - } else if (name === Attributes.maxPayments) { - return { type: RegistrationAttributeTypes.numericNullable }; - } else if (name === Attributes.referenceId) { - return { type: RegistrationAttributeTypes.text }; - } else if (name === Attributes.phoneNumber) { - return { type: RegistrationAttributeTypes.tel }; - } else if (name === Attributes.preferredLanguage) { - return { - type: RegistrationAttributeTypes.dropdown, - options: await this.getPreferredLanguageOptions(), - }; - } else if (name === Attributes.scope) { - return { type: RegistrationAttributeTypes.text }; - } else if ( - name === Attributes.programFinancialServiceProviderConfigurationName - ) { - return { type: RegistrationAttributeTypes.text }; - } - - const repo = AppDataSource.getRepository(ProgramEntity); - const resultProgramRegistrationAttribute = await repo - .createQueryBuilder('program') - .leftJoin( - 'program.programRegistrationAttributes', - 'programRegistrationAttribute', - ) - .where('program.id = :programId', { programId: this.id }) - .andWhere('programRegistrationAttribute.name = :name', { name }) - .select('"programRegistrationAttribute"."type"', 'type') - .addSelect('"programRegistrationAttribute"."options"', 'options') - .getRawOne(); - - if ( - resultProgramRegistrationAttribute && - resultProgramRegistrationAttribute.type - ) { - return { - type: resultProgramRegistrationAttribute.type as RegistrationAttributeTypes, - options: resultProgramRegistrationAttribute.options, - }; - } - 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 0ac3dd9c6b..407544b7f4 100644 --- a/services/121-service/src/programs/programs.controller.ts +++ b/services/121-service/src/programs/programs.controller.ts @@ -263,18 +263,18 @@ You can also leave the body empty.`, required: true, type: 'integer', }) - @Patch(':programId/registration-attributes/:programRegistrationAttributeId') + @Patch(':programId/registration-attributes/:programRegistrationAttributeName') public async updateProgramRegistrationAttribute( @Body() updateProgramRegistrationAttributeDto: UpdateProgramRegistrationAttributeDto, @Param('programId', ParseIntPipe) programId: number, - @Param('programRegistrationAttributeId', ParseIntPipe) - programRegistrationAttributeId: number, + @Param('programRegistrationAttributeName') + programRegistrationAttributeName: string, ): Promise { return await this.programService.updateProgramRegistrationAttribute( programId, - programRegistrationAttributeId, + programRegistrationAttributeName, updateProgramRegistrationAttributeDto, ); } diff --git a/services/121-service/src/programs/programs.service.ts b/services/121-service/src/programs/programs.service.ts index 50541c7aa3..68359f7ae4 100644 --- a/services/121-service/src/programs/programs.service.ts +++ b/services/121-service/src/programs/programs.service.ts @@ -415,18 +415,18 @@ export class ProgramService { public async updateProgramRegistrationAttribute( programId: number, - programRegistrationAttributeId: number, + programRegistrationAttributeName: string, updateProgramRegistrationAttribute: UpdateProgramRegistrationAttributeDto, ): Promise { const programRegistrationAttribute = await this.programRegistrationAttributeRepository.findOne({ where: { - id: Equal(programRegistrationAttributeId), + name: Equal(programRegistrationAttributeName), programId: Equal(programId), }, }); if (!programRegistrationAttribute) { - const errors = `No programRegistrationAttribute found with id ${programRegistrationAttributeId} for program ${programId}`; + const errors = `No programRegistrationAttribute found with name ${programRegistrationAttributeName} for program ${programId}`; throw new HttpException({ errors }, HttpStatus.NOT_FOUND); } 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-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/update-registration.dto.ts b/services/121-service/src/registration/dto/update-registration.dto.ts index 19f4f2741d..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 { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; -import { IsRegistrationDataValidType } from '@121-service/src/registration/validators/registration-data-type.class.validator'; export enum AdditionalAttributes { paymentAmountMultiplier = 'paymentAmountMultiplier', @@ -20,33 +19,12 @@ export type Attributes = | AdditionalAttributes | DefaultRegistrationDataAttributeNames; -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 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 index 9b3c9ce352..a39bdb0e38 100644 --- 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 @@ -1,6 +1,6 @@ -export class ValidateRegistrationErrorObjectDto { - public lineNumber: number; - public column: string; - public value: string; - public error: string; +export interface ValidateRegistrationErrorObject { + lineNumber: number; + column: string; + error: string; + value: string | number | undefined | 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..6f357d2e26 --- /dev/null +++ b/services/121-service/src/registration/interfaces/validate-registration-config.interface.ts @@ -0,0 +1,5 @@ +export class ValidationRegistrationConfig { + validateUniqueReferenceId: boolean; + validateExistingReferenceId: boolean; + validatePhoneNumberLookup: 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..44bc19c9f2 --- /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'; + +// Had to use a type here for using Pick +type RegistrationEntityProperties = Partial< + Pick< + InstanceType, + | 'programId' + | 'registrationStatus' + | 'referenceId' + | 'phoneNumber' + | 'preferredLanguage' + | 'inclusionScore' + | 'paymentAmountMultiplier' + | 'maxPayments' + | 'scope' + > +>; + +export interface ValidatedRegistrationInput + extends RegistrationEntityProperties { + programFinancialServiceProviderConfigurationName?: string; + data: Record; +} 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 4da4479193..4eba8ab8f5 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 @@ -21,9 +21,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.queueRegistrationUpdate.add(ProcessNameRegistration.update, job); } 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 b388959b03..ed7be5ea5e 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 @@ -223,4 +223,26 @@ export class RegistrationDataService { 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); + } + 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/processsors/registrations-update.processor.ts b/services/121-service/src/registration/processsors/registrations-update.processor.ts index 08db3ea9b9..7fdc4ef6bf 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.validateBodyAndUpdateRegistration({ + programId: jobData.programId, + referenceId: jobData.referenceId, + updateRegistrationDto: dto, + userId: jobData?.request?.userId, + }); } } diff --git a/services/121-service/src/registration/registrations.controller.ts b/services/121-service/src/registration/registrations.controller.ts index d6455e1e2d..24f873b263 100644 --- a/services/121-service/src/registration/registrations.controller.ts +++ b/services/121-service/src/registration/registrations.controller.ts @@ -93,8 +93,7 @@ export class RegistrationsController { @Req() req: ScopedUserRequest, ): Promise { const userId = RequestHelper.getUserId(req); - - return await this.registrationsService.importRegistrations( + return await this.registrationsService.importRegistrationsFromCsv( csvFile, programId, userId, @@ -119,8 +118,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, ); @@ -181,7 +180,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, @@ -360,7 +359,7 @@ 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); @@ -383,7 +382,7 @@ export class RegistrationsController { throw new HttpException({ errors }, HttpStatus.FORBIDDEN); } - const partialRegistration = updateRegistrationDataDto.data; + const partialRegistration = updateRegistrationDto.data; if (!hasUpdateFinancialPermission && hasRegistrationUpdatePermission) { for (const attributeKey of Object.keys(partialRegistration)) { @@ -409,22 +408,12 @@ export class RegistrationsController { } } - // 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.validateBodyAndUpdateRegistration({ programId, referenceId, - updateRegistrationDataDto, - ); + updateRegistrationDto, + userId, + }); } @AuthenticatedUser() diff --git a/services/121-service/src/registration/registrations.service.ts b/services/121-service/src/registration/registrations.service.ts index db68b0812e..3d681b4d9b 100644 --- a/services/121-service/src/registration/registrations.service.ts +++ b/services/121-service/src/registration/registrations.service.ts @@ -1,7 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { plainToClass } from 'class-transformer'; -import { validate } from 'class-validator'; import { Equal, Repository } from 'typeorm'; import { EventsService } from '@121-service/src/events/events.service'; @@ -22,19 +20,14 @@ import { IntersolveVisaService } from '@121-service/src/payments/fsp-integration 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 { ProgramRegistrationAttributeEntity } from '@121-service/src/programs/program-registration-attribute.entity'; -import { - ImportRegistrationsDto, - ImportResult, -} from '@121-service/src/registration/dto/bulk-import.dto'; +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 { @@ -45,7 +38,10 @@ 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 +52,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'; @@ -85,6 +82,7 @@ export class RegistrationsService { private readonly registrationViewScopedRepository: RegistrationViewScopedRepository, 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 @@ -270,46 +268,9 @@ export class RegistrationsService { return 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 = ( @@ -362,13 +323,14 @@ export class RegistrationsService { ); } - public async importRegistrations( + 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, @@ -376,7 +338,7 @@ export class RegistrationsService { } public async patchBulk( - csvFile: any, + csvFile: Express.Multer.File, programId: number, userId: number, reason: string, @@ -389,28 +351,26 @@ 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 { @@ -466,19 +426,96 @@ export class RegistrationsService { } } - public async updateRegistration( - programId: number, - referenceId: string, - updateRegistrationDto: UpdateRegistrationDto, - ) { + public async validateBodyAndUpdateRegistration({ + 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'], programId, }); + const program = registrationToUpdate.program; const oldViewRegistration = await this.getPaginateRegistrationForReferenceId(referenceId, programId); @@ -486,11 +523,9 @@ export class RegistrationsService { // 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]; @@ -501,22 +536,43 @@ export class RegistrationsService { ) { 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( @@ -530,18 +586,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, @@ -552,29 +614,32 @@ 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; + } } } } @@ -584,16 +649,17 @@ export class RegistrationsService { attribute === AdditionalAttributes.programFinancialServiceProviderConfigurationName ) { - registration = await this.updateChosenFspConfiguration({ - registration, - newFspConfigurationName: String(value), - }); + 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) { @@ -768,13 +834,13 @@ export class RegistrationsService { return filteredRegistrations; } - public async updateChosenFspConfiguration({ + public async getChosenFspConfigurationId({ registration, newFspConfigurationName, }: { registration: RegistrationEntity; newFspConfigurationName: string; - }): Promise { + }): Promise { //Identify new FSP const newFspConfig = await this.programFinancialServiceProviderConfigurationRepository.findOne( @@ -789,59 +855,7 @@ export class RegistrationsService { const error = `FSP with this name not found`; throw new Error(error); } - registration.programFinancialServiceProviderConfigurationId = - newFspConfig.id; - return registration; - - // ###TODO: Use the new FSP configuration to check if required attributes are already stored with the registration - - // 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: ['programFinancialServiceProviderConfiguration'], - // }); - // if ( - // registration.programFinancialServiceProviderConfigurationId === - // newFspConfig.id - // ) { - // const errors = `New FSP config id is the same as existing FSP config id for this Person Affected.`; - // throw new HttpException({ errors }, HttpStatus.BAD_REQUEST); - // } - } - - 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); - } + return newFspConfig.id; } public async getMessageHistoryRegistration( 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 5ad7663ffc..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,7 +2,6 @@ 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 { GenericRegistrationAttributes } from '@121-service/src/registration/enum/registration-attribute.enum'; @@ -13,22 +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', - programFinancialServiceProviderConfigurationName: - FinancialServiceProviders.intersolveVoucherPaper, - whatsappPhoneNumber: '', - }, - ]; beforeEach(async () => { const { unit, unitRef } = TestBed.create( @@ -87,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: GenericRegistrationAttributes.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 4d6c756035..e6208bb92c 100644 --- a/services/121-service/src/registration/services/registrations-import.service.ts +++ b/services/121-service/src/registration/services/registrations-import.service.ts @@ -10,20 +10,18 @@ import { ProgramFinancialServiceProviderConfigurationRepository } from '@121-ser import { ProgramEntity } from '@121-service/src/programs/program.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 { AttributeWithOptionalLabel, GenericRegistrationAttributes, RegistrationAttributeTypes, } from '@121-service/src/registration/enum/registration-attribute.enum'; -import { RegistrationCsvValidationEnum } from '@121-service/src/registration/enum/registration-csv-validation.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'; @@ -58,7 +56,7 @@ export class RegistrationsImportService { ) {} public async patchBulk( - csvFile: any, + csvFile: Express.Multer.File, programId: number, userId: number, reason: string, @@ -67,31 +65,21 @@ 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 updateData = { ...registration, ...registration.data }; + const referenceId = record['referenceId']; + delete record['referenceId']; return { - referenceId: registration['referenceId'], - data: updateData, + referenceId, + data: record, programId, reason, } as RegistrationUpdateJobDto; @@ -147,12 +135,12 @@ export class RegistrationsImportService { } public async importRegistrations( - csvFile: Express.Multer.File, + inputRegistrations: Record[], program: ProgramEntity, userId: number, ): Promise { - const validatedImportRecords = await this.csvToValidatedRegistrations( - csvFile, + const validatedImportRecords = await this.validateImportRegistrationsInput( + inputRegistrations, program.id, userId, ); @@ -163,8 +151,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 { @@ -194,21 +200,22 @@ export class RegistrationsImportService { for await (const att of dynamicAttributes) { 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]; } } + // ##TODO: Should this be moved out of the loop for performance? const programFinancialServiceProviderConfiguration = await this.programFinancialServiceProviderConfigurationRepository.findOneOrFail( { where: { name: Equal( - record.programFinancialServiceProviderConfigurationName, + record.programFinancialServiceProviderConfigurationName ?? '', ), programId: Equal(program.id), }, @@ -315,7 +322,7 @@ export class RegistrationsImportService { let values: unknown[] = []; if (att.type === RegistrationAttributeTypes.boolean) { values.push( - RegistrationsInputValidatorHelpers.stringToBoolean( + RegistrationsInputValidatorHelpers.inputToBoolean( customData[att.name], false, ), @@ -341,23 +348,6 @@ export class RegistrationsImportService { return registrationDataArray; } - private async csvToValidatedRegistrations( - csvFile: Express.Multer.File, - 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 getDynamicAttributes( programId: number, ): Promise { @@ -377,65 +367,48 @@ export class RegistrationsImportService { 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 a0b176945a..cf752fcb7a 100644 --- a/services/121-service/src/registration/services/registrations-pagination.service.ts +++ b/services/121-service/src/registration/services/registrations-pagination.service.ts @@ -18,8 +18,8 @@ import { WhereExpressionBuilder, } from 'typeorm'; -import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.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 { ProgramEntity } from '@121-service/src/programs/program.entity'; import { ProgramService } from '@121-service/src/programs/programs.service'; import { @@ -582,9 +582,10 @@ export class RegistrationsPaginationService { >, registrationDataInfoArray: RegistrationDataInfo[], ) { - if (!registrationDataArray || registrationDataArray.length < 1) { + if (!registrationDataInfoArray || registrationDataInfoArray.length < 1) { return mappedRegistration; } + const findRelation = ( dataRelation: RegistrationDataRelation, data: RegistrationAttributeDataEntity, @@ -600,14 +601,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; } 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 5fc68f9e13..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 = { - getValidationInfoForAttributeName: 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 38ee4bb1f3..0000000000 --- a/services/121-service/src/registration/validators/registration-data-type.class.validator.ts +++ /dev/null @@ -1,231 +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 { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; -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.getValidationInfoForAttributeName(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 === 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 = datePattern.test(value); - } else if (type === RegistrationAttributeTypes.dropdown) { - isValid = this.optionIsValid(value, options); - } else if (type === RegistrationAttributeTypes.multiSelect) { - isValid = this.multiSelectIsValid(value, options); - } else if (type === RegistrationAttributeTypes.numeric) { - isValid = !isNaN(+value); - } else if (type === RegistrationAttributeTypes.numericNullable) { - isValid = !isNaN(+value) || null; - } else if (type === RegistrationAttributeTypes.tel) { - // Potential refactor: put lookup code here - isValid = this.phoneNumberIsValid(value); - } else if (type === RegistrationAttributeTypes.text) { - isValid = typeof value === 'string'; - } else if (type === RegistrationAttributeTypes.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 4a470776ca..dc5ad2bf7e 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 @@ -6,20 +6,22 @@ import { Repository } from 'typeorm'; 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, + GenericRegistrationAttributes, RegistrationAttributeTypes, } from '@121-service/src/registration/enum/registration-attribute.enum'; -import { RegistrationCsvValidationEnum } from '@121-service/src/registration/enum/registration-csv-validation.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'; -import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; -import { RegistrationViewScopedRepository } from '@121-service/src/registration/repositories/registration-view-scoped.repository'; const programId = 1; const userId = 1; -const dynamicAttributes: AttributeWithOptionalLabel[] = [ +const dynamicAttributes: Partial[] = [ { id: 8, name: 'addressStreet', @@ -151,6 +153,7 @@ describe('RegistrationsInputValidator', () => { name: 'Intersolve-voucher-whatsapp', }, ], + programRegistrationAttributes: dynamicAttributes, }); }); @@ -173,13 +176,17 @@ describe('RegistrationsInputValidator', () => { }, ]; - 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); @@ -214,13 +221,17 @@ describe('RegistrationsInputValidator', () => { ]; 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); }); @@ -234,16 +245,19 @@ describe('RegistrationsInputValidator', () => { ]; 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); }); @@ -257,13 +271,55 @@ describe('RegistrationsInputValidator', () => { ]; 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 c8d1909f1d..86111e146d 100644 --- a/services/121-service/src/registration/validators/registrations-input-validator.ts +++ b/services/121-service/src/registration/validators/registrations-input-validator.ts @@ -7,28 +7,25 @@ import { FINANCIAL_SERVICE_PROVIDERS } from '@121-service/src/financial-service- import { LookupService } from '@121-service/src/notifications/lookup/lookup.service'; 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 { - 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 { ValidateRegistrationErrorObject } from '@121-service/src/registration/dto/validate-registration-error-object.dto'; import { - AttributeWithOptionalLabel, GenericRegistrationAttributes, RegistrationAttributeTypes, } from '@121-service/src/registration/enum/registration-attribute.enum'; -import { RegistrationCsvValidationEnum } from '@121-service/src/registration/enum/registration-csv-validation.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 { 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 { RegistrationsInputValidatorHelpers } from '@121-service/src/registration/validators/registrations-input.validator.helper'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; import { UserService } from '@121-service/src/user/user.service'; -import { RegistrationsPaginationService } from '@121-service/src/registration/services/registrations-pagination.service'; -import { RegistrationViewScopedRepository } from '@121-service/src/registration/repositories/registration-view-scoped.repository'; -import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; -import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; + +type InputAttributeType = string | boolean | number | undefined; @Injectable() export class RegistrationsInputValidator { @@ -44,20 +41,32 @@ export class RegistrationsInputValidator { private readonly registrationViewScopedRepository: RegistrationViewScopedRepository, ) {} - public async validateAndCleanRegistrationsInput( - csvArray: any[], - programId: number, - userId: number, - dynamicAttributes: AttributeWithOptionalLabel[], - typeOfInput: RegistrationCsvValidationEnum, - validationConfig: ValidationConfigDto = new ValidationConfigDto(), - ): Promise { - const originalRegistrations = await this.getOriginalRegistrations( - csvArray, - programId, - ); + 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: ValidateRegistrationErrorObjectDto[] = []; + const errors: ValidateRegistrationErrorObject[] = []; const phoneNumberLookupResults: Record = {}; const userScope = await this.userService.getUserScopeForProgram( @@ -66,12 +75,15 @@ export class RegistrationsInputValidator { ); if (validationConfig.validateUniqueReferenceId) { - this.validateUniqueReferenceIds(csvArray); + this.validateUniqueReferenceIds(registrationInputArray); } const program = await this.programRepository.findOneOrFail({ where: { id: Equal(programId) }, - relations: ['programFinancialServiceProviderConfigurations'], + relations: [ + 'programFinancialServiceProviderConfigurations', + 'programRegistrationAttributes', + ], }); const languageMapping = this.createLanguageMapping( @@ -79,88 +91,110 @@ 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 * ============================================================= */ - - 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 errorObjValidatePhoneNr = this.validatePhoneNumberEmpty( + const errorObjValidatePhoneNr = this.validatePhoneNumberEmpty({ row, i, - validationConfig, - ); + program, + typeOfInput, + }); if (errorObjValidatePhoneNr) { errors.push(errorObjValidatePhoneNr); - } else { - importRecord.phoneNumber = row.phoneNumber ? row.phoneNumber : ''; // If the phone number is empty use an empty string + } else if (row.phoneNumber !== undefined) { + validatedRegistrationInput.phoneNumber = row.phoneNumber + ? String(row.phoneNumber) + : null; } /* @@ -181,22 +215,26 @@ export class RegistrationsInputValidator { }); if (errorObjFspConfig) { errors.push(errorObjFspConfig); - } else { - importRecord[ + } else if ( + row[ AdditionalAttributes.programFinancialServiceProviderConfigurationName - ] = - row[ - AdditionalAttributes.programFinancialServiceProviderConfigurationName - ]; + ] as string + ) { + validatedRegistrationInput[ + AdditionalAttributes.programFinancialServiceProviderConfigurationName + ] = row[ + AdditionalAttributes.programFinancialServiceProviderConfigurationName + ] as string; } const errorObjsFspRequiredAttributes = this.validateFspRequiredAttributes( - row, - // ##TODO: Look into optimizing or at least test the performance of this on bulk updates - originalRegistrations.find( - (reg) => reg.referenceId === row.referenceId, - ), - program.programFinancialServiceProviderConfigurations, + { + row, + originalRegistration, + programFinancialServiceProviderConfigurations: + program.programFinancialServiceProviderConfigurations, + i, + }, ); errors.push(...errorObjsFspRequiredAttributes); @@ -209,15 +247,15 @@ export class RegistrationsInputValidator { // Filter dynamic atttributes that are not relevant for this fsp if question is only fsp specific await Promise.all( - dynamicAttributes.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.isRequired && row[att.name] == null) { + if (!att.isRequired && row[att.name] === undefined) { return; } @@ -230,31 +268,45 @@ 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 === RegistrationAttributeTypes.dropdown) { + if (!att.isRequired) { + if (row[att.name] == null) { + return (validatedRegistrationInput.data[att.name] = null); + } else { + // Skip validation if the attribute is not present in the row and it is not required + return; + } + } + 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; } @@ -279,16 +331,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; } }), ); @@ -298,24 +357,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 @@ -326,10 +384,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( @@ -339,7 +399,9 @@ export class RegistrationsInputValidator { } } - private createLanguageMapping(programLanguages: string[]): object { + private createLanguageMapping( + programLanguages: string[], + ): Record { const languageNamesApi = new Intl.DisplayNames(['en'], { type: 'language', }); @@ -366,15 +428,16 @@ export class RegistrationsInputValidator { i, typeOfInput, }: { - programFinancialServiceProviderConfigurationName: string; + programFinancialServiceProviderConfigurationName: InputAttributeType; programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[]; i: number; - typeOfInput: RegistrationCsvValidationEnum; - }): ValidateRegistrationErrorObjectDto | undefined { + typeOfInput: RegistrationValidationInputType; + }): ValidateRegistrationErrorObject | undefined { // The registration is being patched, and the programFinancialServiceProviderConfigurationName is not being updated so the validation can be skipped if ( - typeOfInput === RegistrationCsvValidationEnum.bulkUpdate && - !programFinancialServiceProviderConfigurationName + typeOfInput === RegistrationValidationInputType.update && + (programFinancialServiceProviderConfigurationName == null || + programFinancialServiceProviderConfigurationName === '') ) { return; } @@ -388,149 +451,206 @@ export class RegistrationsInputValidator { ) { return { lineNumber: i, + value: programFinancialServiceProviderConfigurationName, column: AdditionalAttributes.programFinancialServiceProviderConfigurationName, - value: programFinancialServiceProviderConfigurationName, - error: `FinancialServiceProviderConfigurationName ${programFinancialServiceProviderConfigurationName} not found in program. Allowed values: ${programFinancialServiceProviderConfigurations.join(', ')}`, + error: `FinancialServiceProviderConfigurationName ${programFinancialServiceProviderConfigurationName} not found in program. Allowed values: ${programFinancialServiceProviderConfigurations + .map((fspConfig) => fspConfig.name) + .join(', ')}`, }; } } - private validatePreferredLanguage( - preferredLanguage: string, - languageMapping: any, - i: number, - validationConfig: ValidationConfigDto = new ValidationConfigDto(), - ): { - errorObj?: ValidateRegistrationErrorObjectDto; - preferredLanguage?: LanguageEnum; + private validatePreferredLanguage({ + preferredLanguage, + languageMapping, + i, + typeOfInput, + }: { + preferredLanguage: InputAttributeType; + languageMapping: any; + i: number; + typeOfInput: RegistrationValidationInputType; + }): { + errorObj: ValidateRegistrationErrorObject | undefined; + preferredLanguage: LanguageEnum | undefined; } { - if (validationConfig.validatePreferredLanguage) { - const cleanedPreferredLanguage = - typeof preferredLanguage === 'string' - ? preferredLanguage.trim().toLowerCase() - : preferredLanguage; - - const errorObj = this.checkLanguage( - cleanedPreferredLanguage, - languageMapping, - i, - ); + const errorObj = this.checkLanguage({ + preferredLanguage, + languageMapping, + i, + typeOfInput, + }); - if (errorObj) { - return { errorObj, preferredLanguage: undefined }; - } else { - const value = this.updateLanguage( - cleanedPreferredLanguage, - languageMapping, - ); - return { errorObj: undefined, preferredLanguage: value }; - } + const cleanedPreferredLanguage = + typeof preferredLanguage === 'string' + ? preferredLanguage.trim().toLowerCase() + : String(preferredLanguage); + if (errorObj) { + return { errorObj, preferredLanguage: undefined }; } - return { - errorObj: undefined, - preferredLanguage: preferredLanguage as LanguageEnum, - }; + const value = this.updateLanguage( + cleanedPreferredLanguage, + languageMapping, + ); + return { errorObj: undefined, preferredLanguage: value }; } - private checkLanguage( - inPreferredLanguage: string, - programLanguageMapping: object, - i: number, - ): ValidateRegistrationErrorObjectDto | undefined { - const cleanedPreferredLanguage = - typeof inPreferredLanguage === 'string' - ? inPreferredLanguage.trim().toLowerCase() - : inPreferredLanguage; - if (!cleanedPreferredLanguage) { + 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 (row.referenceId && row.referenceId.includes('$')) { + 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: AdditionalAttributes.referenceId, + column: GenericRegistrationAttributes.referenceId, value: row.referenceId, - error: `${AdditionalAttributes.referenceId} contains a $ character`, + error: `${GenericRegistrationAttributes.referenceId} contains a $ character`, }; } - if (validationConfig.validateExistingReferenceId && row.referenceId) { + + 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) }, }); @@ -545,37 +665,70 @@ export class RegistrationsInputValidator { } } - private validatePhoneNumberEmpty( - row: any, - i: number, - validationConfig: ValidationConfigDto, - ): ValidateRegistrationErrorObjectDto | undefined { - if (!row.phoneNumber && validationConfig.validatePhoneNumberEmpty) { + 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 ( + typeOfInput === RegistrationValidationInputType.create && + !row.phoneNumber + ) { + 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 ( + typeOfInput === RegistrationValidationInputType.update && + row.phoneNumber === '' + ) { + // 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 empty', + 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', }; } } - 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, @@ -586,12 +739,17 @@ export class RegistrationsInputValidator { return { errorObj: undefined, sanitized }; } - private validateNonTelephoneDynamicAttribute( - value: string, - type: string, - columnName: string, - i: number, - ): ValidateRegistrationErrorObjectDto | undefined { + private validateNonTelephoneDynamicAttribute({ + value, + type, + columnName, + i, + }: { + value: InputAttributeType; + type: string; + columnName: string; + i: number; + }): ValidateRegistrationErrorObject | undefined { const cleanedValue = this.cleanNonTelephoneDynamicAttribute(value, type); if (cleanedValue === null) { const errorObj = { @@ -605,34 +763,40 @@ export class RegistrationsInputValidator { } private cleanNonTelephoneDynamicAttribute( - value: string, + value: InputAttributeType, type: string, - ): number | boolean | string | null { + ): InputAttributeType { switch (type) { case RegistrationAttributeTypes.numeric: if (value == null) { - return null; + return undefined; } // 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); + return isNaN(Number(value)) ? undefined : Number(value); case RegistrationAttributeTypes.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; + RegistrationsInputValidatorHelpers.inputToBoolean(value); + return convertedValue === undefined ? undefined : convertedValue; default: // If the type is neither numeric nor boolean, return the original value - return value; + return value as string; } } - private validateFspRequiredAttributes( - row: object, - originalRegistration: MappedPaginatedRegistrationDto | undefined, - programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[], - ): ValidateRegistrationErrorObjectDto[] { + 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 @@ -653,14 +817,13 @@ export class RegistrationsInputValidator { relevantFspConfigName, programFinancialServiceProviderConfigurations, ); - - const errors: ValidateRegistrationErrorObjectDto[] = []; + 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: 0, + 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}`, @@ -683,10 +846,10 @@ export class RegistrationsInputValidator { !this.isRequiredAttributeInObject(attribute, originalRegistration) ) { errors.push({ - lineNumber: 0, + lineNumber: i + 1, column: attribute, - value: row[attribute], - error: `Cannot update ${attribute} with a nullable value as it is required for the FSP: ${relevantFspConfigName}`, + value: undefined, + error: `Cannot update '${attribute}' is required for the FSP: '${relevantFspConfigName}'`, }); } } @@ -729,24 +892,190 @@ export class RegistrationsInputValidator { return requiredAttributes.map((attribute) => attribute.name); } - private async getOriginalRegistrations( + private async getOriginalRegistrationsOrThrow( csvArray: object[], programId: number, - ) { + ): Promise> { const referenceIds = csvArray .filter((row) => row[GenericRegistrationAttributes.referenceId]) .map((row) => row[GenericRegistrationAttributes.referenceId]); - const qb = this.registrationViewScopedRepository + let qb = this.registrationViewScopedRepository .createQueryBuilder('registration') - .andWhere({ status: Not(RegistrationStatusEnum.deleted) }) - .andWhere('registration.referenceId IN (:...referenceIds)', { + .andWhere({ status: Not(RegistrationStatusEnum.deleted) }); + if (referenceIds.length > 0) { + qb = qb.andWhere('registration.referenceId IN (:...referenceIds)', { referenceIds, }); - return await this.registrationPaginationService.getRegistrationsChunked( - programId, - { limit: 10000, path: '' }, - 10000, - qb, + } + 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 && + typeof value === 'boolean' + ) { + isValid = this.valueIsBool(value); + } else { + message = `Type '${type}' is unknown'`; + } + if (!isValid) { + return { + lineNumber: i + 1, + column: attribute, + value: Array.isArray(value) ? value.toString() : value, + error: message + ? message + : this.createErrorMessageInvalidAttributeType({ + type, + value, + attribute, + }), + }; + } + } + + 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 | boolean): boolean { + if (typeof value === 'boolean') { + return true; + } + 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: 'PaymentAmountMultiplier 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 + if (value == null) { + return { validatedMaxPayments: value }; + } + if (isNaN(+value) || +value <= 0) { + return { + errorObj: { + lineNumber: i + 1, + column: GenericRegistrationAttributes.maxPayments, + value, + error: 'MaxPayments must be a positive number', + }, + }; + } + 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/transaction-job-processors/transaction-job-processors.service.ts b/services/121-service/src/transaction-job-processors/transaction-job-processors.service.ts index 8952261127..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,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Equal } from 'typeorm'; import { EventsService } from '@121-service/src/events/events.service'; - 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'; @@ -99,30 +98,6 @@ 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, - programFinancialServiceProviderConfigurationId: - input.programFinancialServiceProviderConfigurationId, - registration, - oldRegistration, - isRetry: input.isRetry, - status: TransactionStatusEnum.error, - errorText, - }); - return; - } - } - let intersolveVisaDoTransferOrIssueCardReturnDto: DoTransferOrIssueCardReturnType; try { const intersolveVisaConfig = @@ -230,29 +205,7 @@ export class TransactionJobProcessorsService { ); const oldRegistration = structuredClone(registration); - // 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, - programFinancialServiceProviderConfigurationId: - transactionJob.programFinancialServiceProviderConfigurationId, - 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: { @@ -289,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, @@ -313,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( 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 61060e803b..b923d6d568 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 @@ -40,6 +40,7 @@ const mockSafaricomTransactionJobDto: SafaricomTransactionJobDto[] = [ phoneNumber: '254708374149', idNumber: 'nat-123', originatorConversationId: 'originator-id', + programFinancialServiceProviderConfigurationId: 1, }, ]; 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 a56fcf782e..2f42bae0b7 100644 --- a/services/121-service/test-registration-data/test-registrations-OCW.csv +++ b/services/121-service/test-registration-data/test-registrations-OCW.csv @@ -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-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/event/get-event.test.ts b/services/121-service/test/event/get-event.test.ts index abc90071b8..a240467906 100644 --- a/services/121-service/test/event/get-event.test.ts +++ b/services/121-service/test/event/get-event.test.ts @@ -1,6 +1,7 @@ import { HttpStatus } from '@nestjs/common'; import { EventEnum } from '@121-service/src/events/enum/event.enum'; +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 { @@ -14,7 +15,6 @@ import { getAccessToken, resetDB, } from '@121-service/test/helpers/utility.helper'; -import { DefaultRegistrationDataAttributeNames } from '@121-service/src/registration/enum/registration-attribute.enum'; const updatePhoneNumber = '15005550099'; diff --git a/services/121-service/test/helpers/assert.helper.ts b/services/121-service/test/helpers/assert.helper.ts index 09a991f80c..1722a7ad3d 100644 --- a/services/121-service/test/helpers/assert.helper.ts +++ b/services/121-service/test/helpers/assert.helper.ts @@ -21,12 +21,23 @@ 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, + updatedRegistration: Record< + string, + string | undefined | boolean | number | null + >, + originalRegistration: Record< + string, + string | undefined | boolean | number | null + >, +): void { + for (const key in patchData) { + expect(updatedRegistration[key]).toBe(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.helper.ts b/services/121-service/test/helpers/program.helper.ts index f548181113..0f530c84e9 100644 --- a/services/121-service/test/helpers/program.helper.ts +++ b/services/121-service/test/helpers/program.helper.ts @@ -7,7 +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 { ProgramRegistrationAttributeDto } from '@121-service/src/programs/dto/program-registration-attribute.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'; @@ -65,6 +68,25 @@ export async function postProgramRegistrationAttribute( .send(programRegistrationAttribute); } +export async function patchProgramRegistrationAttribute({ + programRegistrationAttributeName, + programRegistrationAttribute, + programId, + accessToken, +}: { + programRegistrationAttributeName: string; + programRegistrationAttribute: UpdateProgramRegistrationAttributeDto; + programId: number; + accessToken: string; +}): Promise { + return await getServer() + .patch( + `/programs/${programId}/registration-attributes/${programRegistrationAttributeName}`, + ) + .set('Cookie', [accessToken]) + .send(programRegistrationAttribute); +} + export async function unpublishProgram( programId: number, accessToken: string, @@ -504,3 +526,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/payment/do-payment-fsp-voucher.test.ts b/services/121-service/test/payment/do-payment-fsp-voucher.test.ts index 5dc795b7c7..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 @@ -1,7 +1,7 @@ import { HttpStatus } from '@nestjs/common'; -import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.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 { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; diff --git a/services/121-service/test/payment/do-payment-safaricom.test.ts b/services/121-service/test/payment/do-payment-safaricom.test.ts index 475ea4c516..2e58160021 100644 --- a/services/121-service/test/payment/do-payment-safaricom.test.ts +++ b/services/121-service/test/payment/do-payment-safaricom.test.ts @@ -97,17 +97,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], @@ -154,21 +149,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/payment-count-completed.test.ts b/services/121-service/test/payment/payment-count-completed.test.ts index 72b40c0a18..ec24513e6e 100644 --- a/services/121-service/test/payment/payment-count-completed.test.ts +++ b/services/121-service/test/payment/payment-count-completed.test.ts @@ -1,7 +1,7 @@ import { HttpStatus } from '@nestjs/common'; -import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.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 { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { SeedScript } from '@121-service/src/scripts/seed-script.enum'; diff --git a/services/121-service/test/program/notes.test.ts b/services/121-service/test/program/notes.test.ts index 7b73e5a3f6..744444da14 100644 --- a/services/121-service/test/program/notes.test.ts +++ b/services/121-service/test/program/notes.test.ts @@ -1,48 +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', - programFinancialServiceProviderConfigurationName: - FinancialServiceProviders.intersolveVisa, - 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, @@ -53,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/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..aaa80ab631 --- /dev/null +++ b/services/121-service/test/registrations/__snapshots__/calculate-payment-amount-multiplier.test.ts.snap @@ -0,0 +1,23 @@ +// 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&", + }, + "meta": { + "currentPage": 1, + "filter": {}, + "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 11af0614f7..03831dc0e5 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 @@ -21,8 +21,19 @@ exports[`Import a registration should throw an error when a required a fsp attri [ { "column": "whatsappPhoneNumber", - "error": "Cannot update whatsappPhoneNumber with a nullable value as it is required for the FSP: Intersolve-voucher-whatsapp", + "error": "Cannot update 'whatsappPhoneNumber' is required for the FSP: 'Intersolve-voucher-whatsapp'", + "lineNumber": 1, + }, +] +`; + +exports[`Import a registration should throw an error when uploading a non existing fsp 1`] = ` +[ + { + "column": "programFinancialServiceProviderConfigurationName", + "error": "FinancialServiceProviderConfigurationName non-existing-fsp not found in program. Allowed values: Intersolve-voucher-whatsapp, bank_a, fsp_all_attributes, fsp_no_attributes, ironBank, gringotts", "lineNumber": 0, + "value": "non-existing-fsp", }, ] `; @@ -42,7 +53,7 @@ exports[`Import a registration should throw an error with a numeric registration [ { "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.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/calculate-payment-amount-multiplier.test.ts b/services/121-service/test/registrations/calculate-payment-amount-multiplier.test.ts new file mode 100644 index 0000000000..09cf282e5f --- /dev/null +++ b/services/121-service/test/registrations/calculate-payment-amount-multiplier.test.ts @@ -0,0 +1,131 @@ +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 7b86c11d1f..22500718f8 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,9 +26,10 @@ import { import { programIdOCW, programIdPV, + programIdWesteros, + registrationPV5, registrationWesteros1, } from '@121-service/test/registrations/pagination/pagination-data'; -import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; describe('Import a registration', () => { let accessToken: string; @@ -56,6 +59,43 @@ describe('Import a registration', () => { } }); + it('should import registration with mixed attributed (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); + } + }); + it('should fail import registrations due to program is not published yet', async () => { // Arrange await resetDB(SeedScript.nlrcMultiple); @@ -158,36 +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) { - expect(registration[key]).toBe(registrationVisaCopy[key]); + for (const key in registrationPVCopy) { + expect(registration[key]).toBe(registrationPVCopy[key]); } }); - // ##TODO this test should be refactored. It should throw an error when an attribute is required and it is not provided - it.skip('should throw an error with a numeric registration 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(); @@ -250,8 +288,10 @@ describe('Import a registration', () => { accessToken = await getAccessToken(); // Removes whatsapp from original registration - const { whatsappPhoneNumber, ...registrationWesteros1Copy } = - registrationWesteros1; + const { + whatsappPhoneNumber: _whatsappPhoneNumber, + ...registrationWesteros1Copy + } = registrationWesteros1; registrationWesteros1Copy.programFinancialServiceProviderConfigurationName = FinancialServiceProviders.intersolveVoucherWhatsapp; @@ -279,6 +319,39 @@ describe('Import a registration', () => { 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).toMatchSnapshot(); + + 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); @@ -288,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 index 6a4b0745ab..53a3a0c1a7 100644 --- a/services/121-service/test/registrations/mass-update-registration.test.ts +++ b/services/121-service/test/registrations/mass-update-registration.test.ts @@ -1,6 +1,7 @@ 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 { assertRegistrationBulkUpdate } from '@121-service/test/helpers/assert.helper'; +import { patchProgram } from '@121-service/test/helpers/program.helper'; import { bulkUpdateRegistrationsCSV, importRegistrationsCSV, @@ -34,6 +35,9 @@ describe('Update attribute of multiple PAs via Bulk update', () => { addressStreet: 'newStreet1', addressHouseNumber: '2', addressHouseNumberAddition: '', + preferredLanguage: 'ar', + paymentAmountMultiplier: 2, + whatsappPhoneNumber: '14155238880', }; const registrationDataThatWillChangePa2 = { phoneNumber: '14155238881', @@ -41,14 +45,34 @@ describe('Update attribute of multiple PAs via Bulk update', () => { 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, - 'Bulk update for test registrations due to data validation improvements', + 'test-reason', ); expect(bulkUpdateResult.statusCode).toBe(200); await waitFor(2000); @@ -72,32 +96,66 @@ describe('Update attribute of multiple PAs via Bulk update', () => { const pa2AfterPatch = searchByReferenceIdAfterPatchPa2.body.data[0]; // Assert - assertRegistrationImport(pa1AfterPatch, registrationDataThatWillChangePa1); - assertRegistrationImport(pa2AfterPatch, registrationDataThatWillChangePa2); + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa1, + pa1AfterPatch, + pa1BeforePatch, + ); + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa2, + pa2AfterPatch, + pa2BeforePatch, + ); }); 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: '', + preferredLanguage: 'ar', + paymentAmountMultiplier: 2, + phoneNumber: '14155238880', }; const registrationDataThatWillChangePa2 = { - phoneNumber: '14155238886', 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, - 'Bulk update for test registrations due to data validation improvements', + 'test-reason', ); expect(bulkUpdateResult.statusCode).toBe(200); @@ -113,14 +171,22 @@ describe('Update attribute of multiple PAs via Bulk update', () => { const searchByReferenceIdAfterPatchPa2 = await searchRegistrationByReferenceId( - '01dc9451-1273-484c-b2e8-ae21b51a96ab', + '17dc9451-1273-484c-b2e8-ae21b51a96ab', programIdOcw, accessToken, ); const pa2AfterPatch = searchByReferenceIdAfterPatchPa2.body.data[0]; // Assert - assertRegistrationImport(pa1AfterPatch, registrationDataThatWillChangePa1); - assertRegistrationImport(pa2AfterPatch, registrationDataThatWillChangePa2); + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa1, + pa1AfterPatch, + pa1BeforePatch, + ); + assertRegistrationBulkUpdate( + registrationDataThatWillChangePa2, + pa2AfterPatch, + pa2BeforePatch, + ); }); }); diff --git a/services/121-service/test/registrations/pagination/pagination-data.ts b/services/121-service/test/registrations/pagination/pagination-data.ts index 3da65fc38f..508bad3f7f 100644 --- a/services/121-service/test/registrations/pagination/pagination-data.ts +++ b/services/121-service/test/registrations/pagination/pagination-data.ts @@ -181,7 +181,7 @@ export const registrationWesteros1 = { referenceId: 'westeros123456789', preferredLanguage: 'en', name: 'John Snow', - dob: '283-12-31', + dob: '31-08-1990', house: 'stark', dragon: 1, knowsNothing: true, @@ -195,7 +195,7 @@ export const registrationWesteros2 = { referenceId: 'westeros987654321', preferredLanguage: 'en', name: 'Arya Stark', - dob: '288-12-31', + dob: '31-08-1990', house: 'stark', dragon: 0, knowsNothing: false, @@ -209,7 +209,7 @@ export const registrationWesteros3 = { referenceId: 'westeros987654322', preferredLanguage: 'en', name: 'Jaime Lannister', - dob: '287-12-31', + dob: '31-08-1990', house: 'lannister', dragon: 0, knowsNothing: false, @@ -241,13 +241,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, @@ -269,24 +269,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', @@ -309,19 +309,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/update-registration.test.ts b/services/121-service/test/registrations/update-registration.test.ts index 41e0721c4f..80c658f19d 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 () => { @@ -31,8 +49,9 @@ describe('Update attribute of PA', () => { await importRegistrations(programIdPv, [registrationPvScoped], accessToken); }); - it('should not update unknown registration', async () => { + it.skip('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.skip('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 () => { + it.skip('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 () => { + it.skip('should fail on updating financial data without the right permission', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const dataUpdateFinanancialFail = { paymentAmountMultiplier: 5, referenceId: registrationVisa.referenceId, @@ -237,8 +185,9 @@ describe('Update attribute of PA', () => { ); }); - it('should fail on updating non financial data without the right permission', async () => { + it.skip('should fail on updating non financial data without the right permission', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const dataUpdateNonFinanancialFail = { phoneNumber: 5, referenceId: registrationVisa.referenceId, @@ -271,8 +220,9 @@ describe('Update attribute of PA', () => { expect(registration.phoneNumber).toBe(registrationVisa.phoneNumber); }); - it('should update scope within current users scope', async () => { + it.skip('should update scope within current users scope', async () => { // Arrange + accessToken = await setupNlrcEnvironment(); const newScope = 'utrecht.houten'; const reason = 'automated test'; const updateDto = { @@ -300,8 +250,9 @@ describe('Update attribute of PA', () => { expect(registration.scope).toBe(newScope); }); - it('should not update scope outside current users scope', async () => { + it.skip('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,156 @@ describe('Update attribute of PA', () => { expect(updateResponse.statusCode).toBe(HttpStatus.BAD_REQUEST); expect(registration.scope).toBe(oldScope); }); + + it.skip('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.skip('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.skip('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, + ); + + const patchResult = await patchProgramRegistrationAttribute({ + programRegistrationAttributeName: 'motto', + programRegistrationAttribute: { isRequired: true }, + programId: programIdWesteros, + accessToken, + }); + console.log(patchResult.body); + + 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/tsconfig.json b/services/121-service/tsconfig.json index 1447001b6e..bf1060e199 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", "jest", "Multer"], + "types": ["express", "node", "Multer", "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..2f282e20f8 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,