From 90734f4163efd62638710be7bebfc28c113427b5 Mon Sep 17 00:00:00 2001 From: Ruby Lavender <67907735+RLavender98@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:00:24 +0000 Subject: [PATCH 1/5] feat(FN-3675): implement custom typeorm naming strategy (#4150) --- .../custom-naming-strategy.test.ts | 51 +++++++++++++++++++ .../custom-naming-strategy.ts | 25 +++++++++ .../src/sql-db-connection/data-source.ts | 2 + ...-RenameEmbeddedColumnsWithCorrectCasing.ts | 25 +++++++++ 4 files changed, 103 insertions(+) create mode 100644 libs/common/src/sql-db-connection/custom-naming-strategy.test.ts create mode 100644 libs/common/src/sql-db-connection/custom-naming-strategy.ts create mode 100644 libs/common/src/sql-db-connection/migrations/1737038776008-RenameEmbeddedColumnsWithCorrectCasing.ts diff --git a/libs/common/src/sql-db-connection/custom-naming-strategy.test.ts b/libs/common/src/sql-db-connection/custom-naming-strategy.test.ts new file mode 100644 index 0000000000..15be3a6a76 --- /dev/null +++ b/libs/common/src/sql-db-connection/custom-naming-strategy.test.ts @@ -0,0 +1,51 @@ +import { CustomNamingStrategy } from './custom-naming-strategy'; + +describe('custom-naming-strategy', () => { + describe('CustomNamingStrategy', () => { + const strategy = new CustomNamingStrategy(); + + describe('columnName', () => { + describe('when customName is provided', () => { + it.each` + customName | embeddedPrefixes | expected | description + ${'custom'} | ${[]} | ${'custom'} | ${'there are no embedded prefixes'} + ${'custom'} | ${['prefix']} | ${'prefixCustom'} | ${'there is one embedded prefixes'} + ${'custom'} | ${['prefix1', 'prefix2']} | ${'prefix1Prefix2Custom'} | ${'there are multiple embedded prefixes'} + ${'customName'} | ${['prefixWord']} | ${'prefixWordCustomName'} | ${'the inputs are in camel case'} + ${'custom_name'} | ${['prefix_word']} | ${'prefixWordCustomName'} | ${'the inputs are in snake case'} + ${'custom name'} | ${['prefix word']} | ${'prefixWordCustomName'} | ${'the inputs are in space separated'} + `( + 'should return the customName prefixed by all the prefixes in camel case when $description', + ({ customName, embeddedPrefixes, expected }: { customName: string; embeddedPrefixes: string[]; expected: string }) => { + // Act + const result = strategy.columnName('propertyName', customName, embeddedPrefixes); + + // Assert + expect(result).toEqual(expected); + }, + ); + }); + + describe('when customName is not provided', () => { + it.each` + propertyName | embeddedPrefixes | expected | description + ${'property'} | ${[]} | ${'property'} | ${'there are no embedded prefixes'} + ${'property'} | ${['prefix']} | ${'prefixProperty'} | ${'there is one embedded prefixes'} + ${'property'} | ${['prefix1', 'prefix2']} | ${'prefix1Prefix2Property'} | ${'there are multiple embedded prefixes'} + ${'propertyName'} | ${['prefixWord']} | ${'prefixWordPropertyName'} | ${'the inputs are in camel case'} + ${'property_name'} | ${['prefix_word']} | ${'prefixWordPropertyName'} | ${'the inputs are in snake case'} + ${'property name'} | ${['prefix word']} | ${'prefixWordPropertyName'} | ${'the inputs are in space separated'} + `( + 'should return the propertyName prefixed by all the prefixes in camel case when $description', + ({ propertyName, embeddedPrefixes, expected }: { propertyName: string; embeddedPrefixes: string[]; expected: string }) => { + // Act + const result = strategy.columnName(propertyName, '', embeddedPrefixes); + + // Assert + expect(result).toEqual(expected); + }, + ); + }); + }); + }); +}); diff --git a/libs/common/src/sql-db-connection/custom-naming-strategy.ts b/libs/common/src/sql-db-connection/custom-naming-strategy.ts new file mode 100644 index 0000000000..815d7ec406 --- /dev/null +++ b/libs/common/src/sql-db-connection/custom-naming-strategy.ts @@ -0,0 +1,25 @@ +import { DefaultNamingStrategy } from 'typeorm'; +import { camelCase } from 'typeorm/util/StringUtils'; + +/** + * We define our own naming strategy because we are using embedded columns. + * + * The DefaultNamingStrategy does not handle embedded columns correctly, + * removing the casing of the embedded column names. + * + * e.g. if we have an embedded property 'fooBar', and the prefix 'bazQux' + * the default strategy would return 'bazQuxFoobar' instead of 'bazQuxFooBar'. + * + * Based on discussion here: @link{https://github.com/typeorm/typeorm/issues/7307}. + */ +export class CustomNamingStrategy extends DefaultNamingStrategy { + columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string { + const name = customName || propertyName; + + if (embeddedPrefixes.length) { + return camelCase([...embeddedPrefixes, name].join(' ')); + } + + return name; + } +} diff --git a/libs/common/src/sql-db-connection/data-source.ts b/libs/common/src/sql-db-connection/data-source.ts index 2ca058bc0d..5180f63067 100644 --- a/libs/common/src/sql-db-connection/data-source.ts +++ b/libs/common/src/sql-db-connection/data-source.ts @@ -1,6 +1,7 @@ import { DataSource, DataSourceOptions } from 'typeorm'; import path from 'path'; import { sqlDbConfig } from './config'; +import { CustomNamingStrategy } from './custom-naming-strategy'; const { SQL_DB_HOST, SQL_DB_PORT, SQL_DB_USERNAME, SQL_DB_PASSWORD, SQL_DB_NAME, SQL_DB_LOGGING_ENABLED } = sqlDbConfig; @@ -19,6 +20,7 @@ const dataSourceOptions: DataSourceOptions = { encrypt: true, trustServerCertificate: true, }, + namingStrategy: new CustomNamingStrategy(), }; export const SqlDbDataSource = new DataSource(dataSourceOptions); diff --git a/libs/common/src/sql-db-connection/migrations/1737038776008-RenameEmbeddedColumnsWithCorrectCasing.ts b/libs/common/src/sql-db-connection/migrations/1737038776008-RenameEmbeddedColumnsWithCorrectCasing.ts new file mode 100644 index 0000000000..b526ceff7c --- /dev/null +++ b/libs/common/src/sql-db-connection/migrations/1737038776008-RenameEmbeddedColumnsWithCorrectCasing.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameEmbeddedColumnsWithCorrectCasing1737038776008 implements MigrationInterface { + name = 'RenameEmbeddedColumnsWithCorrectCasing1737038776008'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + EXEC sp_rename "[FeeRecordCorrection].[requestedByUserFirstname]", "requestedByUserFirstName", "COLUMN" + `); + + await queryRunner.query(` + EXEC sp_rename "[FeeRecordCorrection].[requestedByUserLastname]", "requestedByUserLastName", "COLUMN" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + EXEC sp_rename "[FeeRecordCorrection].[requestedByUserFirstName]", "requestedByUserFirstname", "COLUMN" + `); + + await queryRunner.query(` + EXEC sp_rename "[FeeRecordCorrection].[requestedByUserLastName]", "requestedByUserLastname", "COLUMN" + `); + } +} From 9d3f2235b3924008c5840f97e26bb28bf8de4819 Mon Sep 17 00:00:00 2001 From: Ruby Lavender <67907735+RLavender98@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:05:47 +0000 Subject: [PATCH 2/5] feat(FN-3670): correction sent page e2e-tests (#4146) --- .../correction-sent.spec.js | 91 +++++++++++++++++++ e2e-tests/portal/cypress/e2e/pages/index.js | 1 + .../record-corrections/correctionSent.js | 7 ++ .../confirmation.component-test.js | 4 +- .../_macros/email-confirmation-list.njk | 2 +- .../record-correction/correction-sent.njk | 2 +- 6 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 e2e-tests/portal/cypress/e2e/journeys/payment-report-officer/record-corrections/feature-flag-enabled/correction-sent.spec.js create mode 100644 e2e-tests/portal/cypress/e2e/pages/utilisation-report-service/record-corrections/correctionSent.js diff --git a/e2e-tests/portal/cypress/e2e/journeys/payment-report-officer/record-corrections/feature-flag-enabled/correction-sent.spec.js b/e2e-tests/portal/cypress/e2e/journeys/payment-report-officer/record-corrections/feature-flag-enabled/correction-sent.spec.js new file mode 100644 index 0000000000..cc4be76d6b --- /dev/null +++ b/e2e-tests/portal/cypress/e2e/journeys/payment-report-officer/record-corrections/feature-flag-enabled/correction-sent.spec.js @@ -0,0 +1,91 @@ +import { + UtilisationReportEntityMockBuilder, + FeeRecordCorrectionEntityMockBuilder, + PENDING_RECONCILIATION, + FeeRecordEntityMockBuilder, + FEE_RECORD_STATUS, + RECORD_CORRECTION_REASON, + getFormattedReportPeriodWithLongMonth, +} from '@ukef/dtfs2-common'; +import { NODE_TASKS, BANK1_PAYMENT_REPORT_OFFICER1 } from '../../../../../../../e2e-fixtures'; +import relative from '../../../../relativeURL'; +import { provideCorrection, pendingCorrections, correctionSent } from '../../../../pages'; +import { mainHeading } from '../../../../partials'; + +context('Correction sent page - Fee record correction feature flag enabled', () => { + context('When a correction has been provided', () => { + const reportPeriod = { start: { month: 1, year: 2021 }, end: { month: 1, year: 2021 } }; + + beforeEach(() => { + cy.task(NODE_TASKS.DELETE_ALL_FROM_SQL_DB); + + cy.task('getUserFromDbByEmail', BANK1_PAYMENT_REPORT_OFFICER1.email).then((user) => { + const { _id, bank } = user; + const bankId = bank.id; + + const report = UtilisationReportEntityMockBuilder.forStatus(PENDING_RECONCILIATION) + .withUploadedByUserId(_id.toString()) + .withBankId(bankId) + .withReportPeriod(reportPeriod) + .build(); + + const feeRecord = FeeRecordEntityMockBuilder.forReport(report).withStatus(FEE_RECORD_STATUS.PENDING_CORRECTION).build(); + + const pendingCorrection = FeeRecordCorrectionEntityMockBuilder.forFeeRecord(feeRecord) + .withId(3) + .withIsCompleted(false) + .withReasons([RECORD_CORRECTION_REASON.UTILISATION_INCORRECT]) + .build(); + + cy.task(NODE_TASKS.INSERT_UTILISATION_REPORTS_INTO_DB, [report]); + cy.task(NODE_TASKS.INSERT_FEE_RECORDS_INTO_DB, [feeRecord]); + cy.task(NODE_TASKS.INSERT_FEE_RECORD_CORRECTIONS_INTO_DB, [pendingCorrection]); + }); + + cy.login(BANK1_PAYMENT_REPORT_OFFICER1); + cy.visit(relative(`/utilisation-report-upload`)); + + pendingCorrections.row(1).correctionLink().click(); + + cy.keyboardInput(provideCorrection.utilisationInput(), '12345.67'); + + // Click continue the first time to save and review. + cy.clickContinueButton(); + + // Click continue again on the review page to submit the correction. + cy.clickContinueButton(); + }); + + after(() => { + cy.task(NODE_TASKS.DELETE_ALL_FROM_SQL_DB); + }); + + it('should display confirmation of the record correction submission', () => { + const expectedHeading = `${getFormattedReportPeriodWithLongMonth(reportPeriod)} record correction sent to UKEF`; + + cy.assertText(mainHeading(), expectedHeading); + + cy.assertText(correctionSent.emailText(), 'A confirmation email has been sent to:'); + + cy.task('getUserFromDbByEmail', BANK1_PAYMENT_REPORT_OFFICER1.email).then((user) => { + const { id } = user.bank; + + cy.task(NODE_TASKS.GET_ALL_BANKS).then((banks) => { + const bank = banks.find((b) => b.id === id); + + bank.paymentOfficerTeam.emails.forEach((email) => { + correctionSent.emailList().should('contain', email); + }); + }); + }); + + cy.assertText(correctionSent.ukefNotifiedText(), 'UKEF will be notified via email that a record has been updated.'); + }); + + it('should redirect to the Report GEF utilisation and fees paid when continue is clicked', () => { + cy.clickContinueButton(); + + cy.assertText(mainHeading(), 'Report GEF utilisation and fees'); + }); + }); +}); diff --git a/e2e-tests/portal/cypress/e2e/pages/index.js b/e2e-tests/portal/cypress/e2e/pages/index.js index a4d8c2752d..e70d800190 100644 --- a/e2e-tests/portal/cypress/e2e/pages/index.js +++ b/e2e-tests/portal/cypress/e2e/pages/index.js @@ -57,6 +57,7 @@ module.exports = { confirmation: require('./utilisation-report-service/confirmation'), provideCorrection: require('./utilisation-report-service/record-corrections/provideCorrection'), reviewCorrection: require('./utilisation-report-service/record-corrections/reviewCorrection'), + correctionSent: require('./utilisation-report-service/record-corrections/correctionSent'), problemWithService: require('./problem-with-service'), pendingCorrections: require('./utilisation-report-service/record-corrections/pendingCorrections'), }; diff --git a/e2e-tests/portal/cypress/e2e/pages/utilisation-report-service/record-corrections/correctionSent.js b/e2e-tests/portal/cypress/e2e/pages/utilisation-report-service/record-corrections/correctionSent.js new file mode 100644 index 0000000000..2ab47f0a43 --- /dev/null +++ b/e2e-tests/portal/cypress/e2e/pages/utilisation-report-service/record-corrections/correctionSent.js @@ -0,0 +1,7 @@ +const page = { + emailText: () => cy.get('[data-cy="email-text"]'), + emailList: () => cy.get('[data-cy="email-list"]'), + ukefNotifiedText: () => cy.get('[data-cy="ukef-notified-text"]'), +}; + +module.exports = page; diff --git a/portal/component-tests/utilisation-report-service/confirmation.component-test.js b/portal/component-tests/utilisation-report-service/confirmation.component-test.js index 1a761b1bd7..eae02374aa 100644 --- a/portal/component-tests/utilisation-report-service/confirmation.component-test.js +++ b/portal/component-tests/utilisation-report-service/confirmation.component-test.js @@ -16,8 +16,8 @@ describe(page, () => { wrapper.expectText('[data-cy="main-heading"]').toRead(`${reportPeriod} GEF report sent to UKEF`); }); - it('should render paragraph', () => { - wrapper.expectText('[data-cy="paragraph"]').toRead('A confirmation email has been sent to:'); + it('should render email confirmation list', () => { + wrapper.expectText('[data-cy="email-text"]').toRead('A confirmation email has been sent to:'); for (const paymentOfficerEmail of paymentOfficerEmails) { wrapper.expectElement(`ul.govuk-list > li:contains("${paymentOfficerEmail}")`).toExist(); } diff --git a/portal/templates/utilisation-report-service/_macros/email-confirmation-list.njk b/portal/templates/utilisation-report-service/_macros/email-confirmation-list.njk index 6c99400cd4..467a334ff3 100644 --- a/portal/templates/utilisation-report-service/_macros/email-confirmation-list.njk +++ b/portal/templates/utilisation-report-service/_macros/email-confirmation-list.njk @@ -1,7 +1,7 @@ {% macro render(params) %} {% set emails = params.emails %} -

+

A confirmation email has been sent to: