Skip to content

Commit

Permalink
AB#30474 Validate fsp config and required attributes WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruben committed Oct 24, 2024
1 parent a2941c7 commit e24f253
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class RegistrationDataRelation {
public programRegistrationAttributeId?: number;
public programRegistrationAttributeId: number;
}

interface RegistrationDataOpionsWithRequiredRelation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ export class RegistrationsImportService {
registrationData.registration = registration;
registrationData.value = value as string;
registrationData.programRegistrationAttributeId =
att.relation.programRegistrationAttributeId ?? null;
att.relation.programRegistrationAttributeId;
registrationDataArray.push(registrationData);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { RegistrationCsvValidationEnum } from '@121-service/src/registration/enu
import { RegistrationEntity } from '@121-service/src/registration/registration.entity';
import { RegistrationsInputValidator } from '@121-service/src/registration/validators/registrations-input-validator';
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;
Expand Down Expand Up @@ -90,6 +92,12 @@ describe('RegistrationsInputValidator', () => {
mockProgramRepository = {};
mockRegistrationRepository = {};

const mockRegistrationViewScopedRepository = {
createQueryBuilder: jest.fn().mockReturnValue({
andWhere: jest.fn().mockReturnThis(),
}),
// other methods...
};
const module: TestingModule = await Test.createTestingModule({
providers: [
RegistrationsInputValidator,
Expand All @@ -101,6 +109,7 @@ describe('RegistrationsInputValidator', () => {
provide: getRepositoryToken(RegistrationEntity),
useValue: mockRegistrationRepository,
},

{
provide: UserService,
useValue: {
Expand All @@ -113,6 +122,16 @@ describe('RegistrationsInputValidator', () => {
lookupAndCorrect: jest.fn().mockResolvedValue('1234567890'),
},
},
{
provide: RegistrationsPaginationService,
useValue: {
getRegistrationsChunked: jest.fn().mockResolvedValue([]),
},
},
{
provide: RegistrationViewScopedRepository,
useValue: mockRegistrationViewScopedRepository,
},
],
}).compile();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { validate } from 'class-validator';
import { Equal, Repository } from 'typeorm';
import { Equal, Not, Repository } from 'typeorm';

import { FINANCIAL_SERVICE_PROVIDERS } from '@121-service/src/financial-service-providers/financial-service-providers.const';
import { LookupService } from '@121-service/src/notifications/lookup/lookup.service';
Expand All @@ -25,6 +25,10 @@ import { RegistrationEntity } from '@121-service/src/registration/registration.e
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';

@Injectable()
export class RegistrationsInputValidator {
Expand All @@ -36,6 +40,8 @@ export class RegistrationsInputValidator {
constructor(
private readonly userService: UserService,
private readonly lookupService: LookupService,
private readonly registrationPaginationService: RegistrationsPaginationService,
private readonly registrationViewScopedRepository: RegistrationViewScopedRepository,
) {}

public async validateAndCleanRegistrationsInput(
Expand All @@ -46,6 +52,11 @@ export class RegistrationsInputValidator {
typeOfInput: RegistrationCsvValidationEnum,
validationConfig: ValidationConfigDto = new ValidationConfigDto(),
): Promise<ImportRegistrationsDto[] | BulkImportDto[]> {
const originalRegistrations = await this.getOriginalRegistrations(
csvArray,
programId,
);

const errors: ValidateRegistrationErrorObjectDto[] = [];
const phoneNumberLookupResults: Record<string, string | undefined> = {};

Expand Down Expand Up @@ -85,34 +96,6 @@ export class RegistrationsInputValidator {
* Add default registration attributes without custom validation
* =============================================================
*/
const errorObjFspConfig = this.validateProgramFspConfigurationName({
programFinancialServiceProviderConfigurationName:
row[
AdditionalAttributes
.programFinancialServiceProviderConfigurationName
],
programFinancialServiceProviderConfigurations:
program.programFinancialServiceProviderConfigurations,
i,
});
if (errorObjFspConfig) {
errors.push(errorObjFspConfig);
} else {
importRecord[
AdditionalAttributes.programFinancialServiceProviderConfigurationName
] =
row[
AdditionalAttributes.programFinancialServiceProviderConfigurationName
];
}

importRecord.programFinancialServiceProviderConfigurationName =
row.programFinancialServiceProviderConfigurationName;
// ##TODO add validation to check if the financialServiceProvider is valid and the required attributes are present (only for import not bulk update)
const requiredAttributesForFsp = this.getRequiredAttributesForFsp(
importRecord.programFinancialServiceProviderConfigurationName,
program.programFinancialServiceProviderConfigurations,
);

if (!program.paymentAmountMultiplierFormula) {
importRecord.paymentAmountMultiplier = row.paymentAmountMultiplier
Expand Down Expand Up @@ -169,13 +152,54 @@ export class RegistrationsInputValidator {
}
importRecord.referenceId = row.referenceId;

const errorObj = this.validatePhoneNumberEmpty(row, i, validationConfig);
if (errorObj) {
errors.push(errorObj);
const errorObjValidatePhoneNr = this.validatePhoneNumberEmpty(
row,
i,
validationConfig,
);
if (errorObjValidatePhoneNr) {
errors.push(errorObjValidatePhoneNr);
} else {
importRecord.phoneNumber = row.phoneNumber ? row.phoneNumber : ''; // If the phone number is empty use an empty string
}

/*
* =============================================
* Validate fsp config related attributes
* =============================================
*/
const errorObjFspConfig = this.validateProgramFspConfigurationName({
programFinancialServiceProviderConfigurationName:
row[
AdditionalAttributes
.programFinancialServiceProviderConfigurationName
],
programFinancialServiceProviderConfigurations:
program.programFinancialServiceProviderConfigurations,
i,
typeOfInput,
});
if (errorObjFspConfig) {
errors.push(errorObjFspConfig);
} else {
importRecord[
AdditionalAttributes.programFinancialServiceProviderConfigurationName
] =
row[
AdditionalAttributes.programFinancialServiceProviderConfigurationName
];
}

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,
);
errors.push(...errorObjsFspRequiredAttributes);

/*
* =============================================
* Validate dynamic registration data attributes
Expand Down Expand Up @@ -340,11 +364,21 @@ export class RegistrationsInputValidator {
programFinancialServiceProviderConfigurationName,
programFinancialServiceProviderConfigurations,
i,
typeOfInput,
}: {
programFinancialServiceProviderConfigurationName: string;
programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[];
i: number;
typeOfInput: RegistrationCsvValidationEnum;
}): ValidateRegistrationErrorObjectDto | undefined {
// The registration is being patched, and the programFinancialServiceProviderConfigurationName is not being updated so the validation can be skipped
if (
typeOfInput === RegistrationCsvValidationEnum.bulkUpdate &&
!programFinancialServiceProviderConfigurationName
) {
return;
}

if (
!programFinancialServiceProviderConfigurationName ||
!programFinancialServiceProviderConfigurations.some(
Expand Down Expand Up @@ -526,27 +560,6 @@ export class RegistrationsInputValidator {
}
}

private getRequiredAttributesForFsp(
programFinancialServiceProviderConfigurationName: string,
programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[],
): string[] {
const fspName = programFinancialServiceProviderConfigurations.find(
(programFspConfig) =>
programFspConfig.name ===
programFinancialServiceProviderConfigurationName,
)?.financialServiceProviderName;
const foundFsp = FINANCIAL_SERVICE_PROVIDERS.find(
(fsp) => fsp.name === fspName,
);
if (!foundFsp) {
return [];
}
const requiredAttributes = foundFsp.attributes.filter(
(attribute) => attribute.isRequired,
);
return requiredAttributes.map((attribute) => attribute.name);
}

private async validateLookupPhoneNumber(
value: string,
i: number,
Expand Down Expand Up @@ -614,4 +627,126 @@ export class RegistrationsInputValidator {
return value;
}
}

private validateFspRequiredAttributes(
row: object,
originalRegistration: MappedPaginatedRegistrationDto | undefined,
programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[],
): ValidateRegistrationErrorObjectDto[] {
// Decide which required attributes to check
// If the updated row has a value a new fsp configuration name, check the required attributes for that fsp
// Otherwise, check the required attributes for the original registration that is in the database

const relevantFspConfigName =
row[
GenericRegistrationAttributes
.programFinancialServiceProviderConfigurationName
] ??
originalRegistration?.programFinancialServiceProviderConfigurationName;
if (!relevantFspConfigName) {
// If the programFinancialServiceProviderConfigurationName is neither in the row nor in the original registration, we cannot check the required attributes
// Errors will be thrown in a different validation step
return [];
}

const requiredAttributes = this.getRequiredAttributesForFsp(
relevantFspConfigName,
programFinancialServiceProviderConfigurations,
);

const errors: ValidateRegistrationErrorObjectDto[] = [];
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,
column: attribute,
value: row[attribute],
error: `Cannot update/set ${attribute} with a nullable value as it is required for the FSP: ${relevantFspConfigName}`,
});
continue;
}
}

// If the programFinancialServiceProviderConfigurationName being updated / set in this request
// check if a combination orignal registration and new row has all required attributes
if (
row[
GenericRegistrationAttributes
.programFinancialServiceProviderConfigurationName
]
) {
// Check if the required attributes are present in the row
if (
!this.isRequiredAttributeInObject(attribute, row) &&
!this.isRequiredAttributeInObject(attribute, originalRegistration)
) {
errors.push({
lineNumber: 0,
column: attribute,
value: row[attribute],
error: `Cannot update ${attribute} with a nullable value as it is required for the FSP: ${relevantFspConfigName}`,
});
}
}
}
return errors;
}

private isRequiredAttributeInObject(
attribute: string,
body: object | undefined,
): boolean {
if (!body) {
return false;
}
return (
body.hasOwnProperty(attribute) &&
body[attribute] != null &&
body[attribute] !== ''
);
}

private getRequiredAttributesForFsp(
programFinancialServiceProviderConfigurationName: string,
programFinancialServiceProviderConfigurations: ProgramFinancialServiceProviderConfigurationEntity[],
): string[] {
const fspName = programFinancialServiceProviderConfigurations.find(
(programFspConfig) =>
programFspConfig.name ===
programFinancialServiceProviderConfigurationName,
)?.financialServiceProviderName;
const foundFsp = FINANCIAL_SERVICE_PROVIDERS.find(
(fsp) => fsp.name === fspName,
);
if (!foundFsp) {
return [];
}
const requiredAttributes = foundFsp.attributes.filter(
(attribute) => attribute.isRequired,
);
return requiredAttributes.map((attribute) => attribute.name);
}

private async getOriginalRegistrations(
csvArray: object[],
programId: number,
) {
const referenceIds = csvArray
.filter((row) => row[GenericRegistrationAttributes.referenceId])
.map((row) => row[GenericRegistrationAttributes.referenceId]);
const qb = this.registrationViewScopedRepository
.createQueryBuilder('registration')
.andWhere({ status: Not(RegistrationStatusEnum.deleted) })
.andWhere('registration.referenceId IN (:...referenceIds)', {
referenceIds,
});
return await this.registrationPaginationService.getRegistrationsChunked(
programId,
{ limit: 10000, path: '' },
10000,
qb,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ exports[`Import a registration should give me a CSV template when I request it 1
]
`;

exports[`Import a registration should throw an error when a required a fsp attribute is missing 1`] = `
[
{
"column": "whatsappPhoneNumber",
"error": "Cannot update whatsappPhoneNumber with a nullable value as it is required for the FSP: Intersolve-voucher-whatsapp",
"lineNumber": 0,
},
]
`;

exports[`Import a registration should throw an error with a dropdown registration atribute set to null 1`] = `
[
{
Expand Down
Loading

0 comments on commit e24f253

Please sign in to comment.