diff --git a/src/api/forms/service/report-timeline.js b/src/api/forms/service/report-timeline.js deleted file mode 100644 index 9282f119..00000000 --- a/src/api/forms/service/report-timeline.js +++ /dev/null @@ -1,87 +0,0 @@ -import { FormMetricType, FormStatus, getErrorMessage } from '@defra/forms-model' -import { isSameDay } from 'date-fns' - -import { getMetadataCursorOfAllForms } from '~/src/api/forms/repositories/form-metadata-repository.js' -import { mapMetadata } from '~/src/api/forms/service/helpers/mapper.js' -import { logger } from '~/src/helpers/logging/logger.js' -import { client } from '~/src/mongo.js' - -/** - * Generates a set of timeline metrics for each form - * @param {Date} date - date on which to gather the metrics for - */ -export async function generateReportTimeline(date) { - logger.info(`Generating timeline report for date ${date.toString()}`) - - const session = client.startSession() - - const timelineMetrics = /** @type {FormTimelineMetric[]} */ ([]) - - try { - await session.withTransaction(async () => { - const metadataCursor = getMetadataCursorOfAllForms(session) - - for await (const metadata of metadataCursor) { - const strictMetadata = mapMetadata(metadata) - - // Gather timeline metrics for all time, or just a specific day - collectTimelineMetrics(timelineMetrics, strictMetadata, date) - } - }) - } catch (err) { - logger.error( - err, - `[report] Failed to generate timeline report for date ${date.toString()} - ${getErrorMessage(err)}` - ) - - throw err - } finally { - await session.endSession() - } - - logger.info(`Generated timeline report for date ${date.toString()}`) - - return { - timeline: timelineMetrics - } -} - -/** - * Collect timeline metrics - * @param {FormTimelineMetric[]} timelineMetrics - * @param {FormMetadata} metadata - * @param {Date} date - */ -export function collectTimelineMetrics(timelineMetrics, metadata, date) { - // NewFormsCreated - draft only - if (metadata.draft?.createdAt && isSameDay(date, metadata.draft.createdAt)) { - timelineMetrics.push( - /** @type {FormTimelineMetric} */ ({ - type: FormMetricType.TimelineMetric, - formId: metadata.id, - formStatus: FormStatus.Draft, - metricName: 'NewFormsCreated', - metricValue: 1, - createdAt: metadata.draft.createdAt - }) - ) - } - - // FormsPublished - live only - if (metadata.live?.createdAt && isSameDay(date, metadata.live.createdAt)) { - timelineMetrics.push( - /** @type {FormTimelineMetric} */ ({ - type: FormMetricType.TimelineMetric, - formId: metadata.id, - formStatus: FormStatus.Live, - metricName: 'FormsPublished', - metricValue: 1, - createdAt: metadata.live.createdAt - }) - ) - } -} - -/** - * @import { FormMetadata, FormTimelineMetric } from '@defra/forms-model' - */ diff --git a/src/api/forms/service/report-timeline.test.js b/src/api/forms/service/report-timeline.test.js deleted file mode 100644 index bf223714..00000000 --- a/src/api/forms/service/report-timeline.test.js +++ /dev/null @@ -1,322 +0,0 @@ -import { ControllerType } from '@defra/forms-model' -import { - buildCheckboxComponent, - buildDeclarationFieldComponent, - buildFileUploadPage, - buildPaymentComponent, - buildRadioComponent -} from '@defra/forms-model/stubs' -import { ObjectId } from 'mongodb' - -import { - buildDefinition, - buildQuestionPage, - buildSummaryPage, - buildTextFieldComponent -} from '~/src/api/forms/__stubs__/definition.js' -import { buildMetadataDocument } from '~/src/api/forms/__stubs__/metadata.js' -import * as formDefinition from '~/src/api/forms/repositories/form-definition-repository.js' -import { getMetadataCursorOfAllForms } from '~/src/api/forms/repositories/form-metadata-repository.js' -import { - getExpectedTimelineMetrics1, - getExpectedTimelineMetrics2 -} from '~/src/api/forms/service/__stubs__/metrics.js' -import { - getFeatureList, - getUniqueComponentTypes -} from '~/src/api/forms/service/report-overview.js' -import { generateReportTimeline } from '~/src/api/forms/service/report-timeline.js' -import { client } from '~/src/mongo.js' - -jest.mock('~/src/api/forms/repositories/form-definition-repository.js') -jest.mock('~/src/api/forms/repositories/form-metadata-repository.js') - -jest.mock('~/src/mongo.js', () => ({ - client: { - startSession: jest.fn() - }, - db: {}, - METADATA_COLLECTION_NAME: 'form-metadata', - DEFINITION_COLLECTION_NAME: 'form-definition' -})) - -describe('report-timeline', () => { - describe('generateReportTimeline', () => { - /** @type {any} */ - const mockSession = { - withTransaction: jest.fn(), - endSession: jest.fn() - } - const now = new Date() - - beforeEach(() => { - jest.clearAllMocks() - jest.useFakeTimers().setSystemTime(now) - - mockSession.withTransaction = jest - .fn() - .mockImplementation( - async (/** @type {() => Promise} */ callback) => { - return await callback() - } - ) - mockSession.endSession = jest.fn().mockResolvedValue(undefined) - }) - - afterEach(() => { - jest.useRealTimers() - }) - - const form1Id = '449a699bcc9946a6a6d925de' - const form2Id = '0dae1c832b8e4a89963a7825' - const form3Id = '9fb48bd350a64e908c9ea92e' - - it('should gather metrics for all forms, for 7th May 2025', async () => { - const allMetadata = [ - buildMetadataDocument({ - title: 'Form 1 title', - slug: 'form-1-title', - _id: new ObjectId(form1Id), - updatedAt: new Date('2025-05-20T09:10:21.035Z') - }), - buildMetadataDocument({ - title: 'Form 2 title', - slug: 'form-2-title', - _id: new ObjectId(form2Id), - live: { - createdAt: new Date('2025-05-07T09:10:21.035Z'), - createdBy: { - id: '84305e4e-1f52-43d0-a123-9c873b0abb35', - displayName: 'Internal User' - }, - updatedAt: new Date('2025-05-07T09:10:21.035Z'), - updatedBy: { - id: '84305e4e-1f52-43d0-a123-9c873b0abb35', - displayName: 'Internal User' - } - }, - updatedAt: new Date('2025-05-07T09:10:21.035Z') - }), - buildMetadataDocument({ - title: 'Form 3 title', - slug: 'form-3-title', - _id: new ObjectId(form3Id), - updatedAt: new Date('2025-05-20T09:10:21.035Z') - }) - ] - - const mockAsyncIterator = { - [Symbol.asyncIterator]: function* () { - for (const metadata of allMetadata) { - yield metadata - } - } - } - - jest - .mocked(getMetadataCursorOfAllForms) - // @ts-expect-error - resolves to an async iterator like FindCursor - .mockReturnValueOnce(mockAsyncIterator) - - // Form 1 - draft and no live - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - - // Form 2 - draft and live - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - - // Form 3 - draft and no live - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - - const mockNewSession = /** @type {any} */ ({ - withTransaction: jest.fn().mockImplementation(async (callback) => { - return await callback() - }), - endSession: jest.fn().mockResolvedValue(undefined) - }) - jest.mocked(client.startSession).mockReturnValue(mockNewSession) - - const metrics = await generateReportTimeline(new Date(2025, 4, 7)) - - expect(metrics).toEqual(getExpectedTimelineMetrics1()) - }) - - it('should gather metrics for all forms, for 8th August 2025', async () => { - const allMetadata = [ - buildMetadataDocument({ - title: 'Form 1 title', - slug: 'form-1-title', - _id: new ObjectId(form1Id) - }), - buildMetadataDocument({ - title: 'Form 2 title', - slug: 'form-2-title', - _id: new ObjectId(form2Id), - live: { - createdAt: new Date('2025-08-08T09:10:21.035Z'), - createdBy: { - id: '84305e4e-1f52-43d0-a123-9c873b0abb35', - displayName: 'Internal User' - }, - updatedAt: new Date('2025-08-08T09:10:21.035Z'), - updatedBy: { - id: '84305e4e-1f52-43d0-a123-9c873b0abb35', - displayName: 'Internal User' - } - } - }), - buildMetadataDocument({ - title: 'Form 3 title', - slug: 'form-3-title', - _id: new ObjectId(form3Id) - }) - ] - - const mockAsyncIterator = { - [Symbol.asyncIterator]: function* () { - for (const metadata of allMetadata) { - yield metadata - } - } - } - jest - .mocked(getMetadataCursorOfAllForms) - // @ts-expect-error - resolves to an async iterator like FindCursor - .mockReturnValueOnce(mockAsyncIterator) - - // Form 1 - draft and no live - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - // @ts-expect-error - force not def to be returned - jest.mocked(formDefinition.get).mockResolvedValueOnce(undefined) - - // Form 2 - draft and live - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - - // Form 3 - draft and no live - jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({})) - // @ts-expect-error - force not def to be returned - jest.mocked(formDefinition.get).mockResolvedValueOnce(undefined) - - const mockNewSession = /** @type {any} */ ({ - withTransaction: jest.fn().mockImplementation(async (callback) => { - return await callback() - }), - endSession: jest.fn().mockResolvedValue(undefined) - }) - jest.mocked(client.startSession).mockReturnValue(mockNewSession) - - const metrics = await generateReportTimeline(new Date(2025, 7, 8)) - - expect(metrics).toEqual(getExpectedTimelineMetrics2()) - }) - - it('should handle error and still close session', async () => { - jest.mocked(getMetadataCursorOfAllForms).mockImplementationOnce(() => { - throw new Error('report error') - }) - - const mockEndSession = jest.fn().mockResolvedValue(undefined) - const mockNewSession = /** @type {any} */ ({ - withTransaction: jest.fn().mockImplementation(async (callback) => { - return await callback() - }), - endSession: mockEndSession - }) - jest.mocked(client.startSession).mockReturnValue(mockNewSession) - - await expect(() => - generateReportTimeline(new Date(2025, 1, 1)) - ).rejects.toThrow('report error') - - expect(mockEndSession).toHaveBeenCalled() - }) - }) - - describe('getUniqueComponentTypes', () => { - it('should return empty list', () => { - const summaryPage = buildSummaryPage() - const questionPageId = 'd9c99072-d25d-4688-ab7d-3822cffe802b' - const questionPage = buildQuestionPage({ - id: questionPageId - }) - - const definition = buildDefinition({ - pages: [questionPage, summaryPage] - }) - expect(getUniqueComponentTypes(definition)).toEqual(new Set()) - }) - - it('should return unique list', () => { - const summaryPage = buildSummaryPage() - const questionPageId = 'd9c99072-d25d-4688-ab7d-3822cffe802b' - const questionPage = buildQuestionPage({ - id: questionPageId, - components: [ - buildTextFieldComponent(), - buildTextFieldComponent(), - buildCheckboxComponent(), - buildTextFieldComponent(), - buildCheckboxComponent(), - buildRadioComponent() - ] - }) - - const definition = buildDefinition({ - pages: [questionPage, summaryPage] - }) - expect(getUniqueComponentTypes(definition)).toEqual( - new Set(['CheckboxesField', 'RadiosField', 'TextField']) - ) - }) - }) - - describe('getFeatureList', () => { - it('should return empty list', () => { - const summaryPage = buildSummaryPage() - const questionPageId = 'd9c99072-d25d-4688-ab7d-3822cffe802b' - const questionPage = buildQuestionPage({ - id: questionPageId - }) - - const definition = buildDefinition({ - pages: [questionPage, summaryPage] - }) - expect(getFeatureList(definition)).toEqual([]) - }) - - it('should return unique list', () => { - const summaryPage = buildSummaryPage({ - // @ts-expect-error - forcing the controller type - controller: ControllerType.SummaryWithConfirmationEmail - }) - const questionPageId = 'd9c99072-d25d-4688-ab7d-3822cffe802b' - const questionPage = buildQuestionPage({ - id: questionPageId, - components: [ - buildTextFieldComponent(), - buildTextFieldComponent(), - buildCheckboxComponent(), - buildTextFieldComponent(), - buildCheckboxComponent(), - buildRadioComponent(), - buildDeclarationFieldComponent() - ] - }) - const fileUploadPage = buildFileUploadPage() - const paymentPage = buildQuestionPage({ - components: [buildPaymentComponent()] - }) - - const definition = buildDefinition({ - pages: [questionPage, fileUploadPage, paymentPage, summaryPage] - }) - expect(getFeatureList(definition)).toEqual([ - 'File upload', - 'Email confirmation', - 'GOV.UK Pay', - 'Declarations' - ]) - }) - }) -}) diff --git a/src/models/forms.js b/src/models/forms.js index 6a786ab8..847c5cf8 100644 --- a/src/models/forms.js +++ b/src/models/forms.js @@ -151,7 +151,3 @@ export const sectionAssignmentPayloadSchema = Joi.object() .required() }) .required() - -export const reportQuerySchema = Joi.object().keys({ - date: Joi.date().required() -}) diff --git a/src/routes/forms.js b/src/routes/forms.js index 08e0b551..6913dff6 100644 --- a/src/routes/forms.js +++ b/src/routes/forms.js @@ -25,7 +25,6 @@ import { } from '~/src/api/forms/service/index.js' import { migrateDefinitionToV2 } from '~/src/api/forms/service/migration.js' import { generateReportOverview } from '~/src/api/forms/service/report-overview.js' -import { generateReportTimeline } from '~/src/api/forms/service/report-timeline.js' import { getFormVersion, getFormVersions @@ -37,7 +36,6 @@ import { formByIdSchema, formBySlugSchema, migrateDefinitionParamSchema, - reportQuerySchema, updateFormDefinitionSchema } from '~/src/models/forms.js' @@ -468,25 +466,6 @@ export default [ options: { auth: false } - }, - { - method: 'GET', - path: '/report/timeline', - /** - * @param {RequestReport} request - */ - handler(request) { - const { query } = request - const { date } = query - - return generateReportTimeline(date) - }, - options: { - auth: false, - validate: { - query: reportQuerySchema - } - } } ] diff --git a/src/routes/forms.test.js b/src/routes/forms.test.js index 9f5f76bb..bb7eb6d3 100644 --- a/src/routes/forms.test.js +++ b/src/routes/forms.test.js @@ -30,7 +30,6 @@ import { } from '~/src/api/forms/service/index.js' import { migrateDefinitionToV2 } from '~/src/api/forms/service/migration.js' import { generateReportOverview } from '~/src/api/forms/service/report-overview.js' -import { generateReportTimeline } from '~/src/api/forms/service/report-timeline.js' import { getFormVersion, getFormVersions @@ -56,7 +55,6 @@ jest.mock('~/src/api/forms/service/conditions.js') jest.mock('~/src/api/forms/service/versioning.js') jest.mock('~/src/messaging/publish.js') jest.mock('~/src/api/forms/service/report-overview.js') -jest.mock('~/src/api/forms/service/report-timeline.js') describe('Forms route', () => { /** @type {Server} */ @@ -1969,22 +1967,6 @@ describe('Forms route', () => { live: [] }) }) - - test('GET /report-timeline returns data', async () => { - jest.mocked(generateReportTimeline).mockResolvedValue({ - timeline: [] - }) - - const response = await server.inject({ - method: 'GET', - url: '/report/timeline?date=2025-10-10' - }) - - expect(response.statusCode).toEqual(okStatusCode) - expect(response.result).toEqual({ - timeline: [] - }) - }) }) })