Skip to content

feat(DTFS2-7793): add update amendment status endpoint to dtfs-central #4193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d9c9ec2
feat(DTFS2-7793): add submit amendment to checker endpoint
Jan 30, 2025
d0e77a4
feat(DTFS2-7793): add service unit tests
Jan 30, 2025
17304f9
feat(DTFS2-7793): add controller unit tests
Jan 30, 2025
2efda19
feat(DTFS2-7793): add api test (wip)
Jan 30, 2025
061733b
feat(DTFS2-7793): refactor to make service more DRY
Jan 31, 2025
8bc059c
feat(DTFS2-7793): validate that the amendment is of an expected status
Jan 31, 2025
5ba2e55
feat(DTFS2-7793): generate valid amendment structure
Jan 31, 2025
b0ffdaf
feat(DTFS2-7793): add validateAmendmentIsComplete function to amendme…
Jan 31, 2025
372d93a
feat(DTFS2-7793): add jsdoc
Jan 31, 2025
be71521
feat(DTFS2-7793): update updatePortalFacilityAmendmentByAmendmentId u…
Jan 31, 2025
fed066a
feat(DTFS2-7793): update service unit tests
Jan 31, 2025
2539c9d
feat(DTFS2-7793): add unit tests for validateAmendmentIsComplete serv…
Jan 31, 2025
52d682b
feat(DTFS2-7793): add unit tests for generatePortalFacilityAmendment
Jan 31, 2025
2597927
feat(DTFS2-7793): update typo in controller unit test
Jan 31, 2025
dc8051d
feat(DTFS2-7793): add error unit tests
Jan 31, 2025
48f786f
feat(DTFS2-7793): refactor to post status endpoint
Jan 31, 2025
622e302
feat(DTFS2-7793): rename files & update swagger
Feb 3, 2025
74eea25
feat(DTFS2-7793): update api test
Feb 3, 2025
0851d4b
feat(DTFS2-7793): update view model types
Feb 3, 2025
fb4eddc
feat(DTFS2-7793): update method to PATCH
Feb 4, 2025
39c3abf
feat(DTFS2-7793): use payload rather than path params
Feb 4, 2025
1a191cc
feat(DTFS2-7793): update api test
Feb 4, 2025
86a85e7
feat(DTFS2-7793): update test describe
Feb 4, 2025
86507f9
feat(DTFS2-7793): update timestamp discrepancy in unit test
Feb 4, 2025
d123c3c
feat(DTFS2-7793): update amendment status controller and unit test
Feb 4, 2025
e5b70b8
feat(DTFS2-7793): update unit test describe
Feb 4, 2025
eb77c17
feat(DTFS2-7793): remove duplicate effectiveDate in mock
Feb 4, 2025
4ea8a37
feat(DTFS2-7793): add middleware unit tests
Feb 4, 2025
bdf7d6b
feat(DTFS2-7793): remove obsolete tests from portal api middleware va…
Feb 4, 2025
e0cba89
Merge branch 'main' into feat/DTFS2-7793/submit-amendment-to-checker-…
MarRobSoftwire Feb 5, 2025
7016b1c
feat(DTFS2-7793): check no other amendments submitted on deal
Feb 5, 2025
88112aa
feat(DTFS2-7793): fix duplicate import after merge
Feb 6, 2025
27e7d84
feat(DTFS2-7793): update service unit test
Feb 6, 2025
72a7a91
feat(DTFS2-7793): update api test
Feb 6, 2025
8209148
feat(DTFS2-7793): code review
Feb 6, 2025
fa22f35
feat(DTFS2-7793): add validateNoOtherAmendmentsUnderWayOnDeal unit te…
Feb 7, 2025
8c102f0
feat(DTFS2-7793): update test description
Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Response } from 'supertest';
import * as z from 'zod';
import { PORTAL_FACILITY_AMENDMENT_USER_VALUES } from '@ukef/dtfs2-common/schemas';
import { PortalFacilityAmendment } from '@ukef/dtfs2-common';
import { PortalFacilityAmendmentWithUkefId } from '@ukef/dtfs2-common';
import { generatePortalAuditDetails } from '@ukef/dtfs2-common/change-stream';
import { testApi } from '../test-api';

interface FacilityAmendmentResponse extends Response {
body: PortalFacilityAmendment;
body: PortalFacilityAmendmentWithUkefId;
}

/**
Expand All @@ -32,5 +32,13 @@ export const createPortalFacilityAmendment = async ({
const { body } = (await testApi
.put({ dealId, amendment, auditDetails: generatePortalAuditDetails(userId) })
.to(`/v1/portal/facilities/${facilityId}/amendments/`)) as FacilityAmendmentResponse;

// Eligibility Criteria is overwritten in the PUT request
if (amendment.eligibilityCriteria) {
await testApi
.patch({ update: { eligibilityCriteria: amendment.eligibilityCriteria }, auditDetails: generatePortalAuditDetails(userId) })
.to(`/v1/portal/facilities/${facilityId}/amendments/${body.amendmentId}`);
}

return body;
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const newDeal = aDeal({
submissionType: DEAL_SUBMISSION_TYPE.AIN,
}) as AnyObject;

describe('PATCH /v1/portal/facilities/:facilityId/amendments/', () => {
describe('PATCH /v1/portal/facilities/:facilityId/amendments/:amendmentId', () => {
let dealId: string;
let facilityId: string;
let portalUserId: string;
Expand Down Expand Up @@ -164,9 +164,9 @@ describe('PATCH /v1/portal/facilities/:facilityId/amendments/', () => {
});

it(`should return ${HttpStatusCode.Ok} when the payload is valid & the amendment exists`, async () => {
const { status } = (await testApi
const { status } = await testApi
.patch({ update: aPortalFacilityAmendmentUserValues(), auditDetails: generatePortalAuditDetails(portalUserId) })
.to(generateUrl(facilityId, amendmentId))) as ErrorResponse;
.to(generateUrl(facilityId, amendmentId));

expect(status).toEqual(HttpStatusCode.Ok);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { Response } from 'supertest';
import { ObjectId } from 'mongodb';
import { HttpStatusCode } from 'axios';
import { AnyObject, API_ERROR_CODE, DEAL_SUBMISSION_TYPE, DEAL_TYPE, FACILITY_TYPE, MONGO_DB_COLLECTIONS, PORTAL_AMENDMENT_STATUS } from '@ukef/dtfs2-common';
import { generatePortalAuditDetails } from '@ukef/dtfs2-common/change-stream';
import { aPortalFacilityAmendmentUserValues } from '@ukef/dtfs2-common/mock-data-backend';
import wipeDB from '../../wipeDB';
import { testApi } from '../../test-api';
import { createDeal, submitDealToTfm } from '../../helpers/create-deal';
import aDeal from '../deal-builder';
import { aPortalUser } from '../../mocks/test-users/portal-user';
import { createPortalUser } from '../../helpers/create-portal-user';
import { createPortalFacilityAmendment } from '../../helpers/create-portal-facility-amendment';
import { mongoDbClient as db } from '../../../src/drivers/db-client';
import { amendmentsEligibilityCriteria } from '../../../test-helpers/test-data/eligibility-criteria-amendments';

const originalEnv = { ...process.env };

interface ErrorResponse extends Response {
body: { status?: number; message: string; code?: string };
}

const generateUrl = (facilityId: string, amendmentId: string): string => {
return `/v1/portal/facilities/${facilityId}/amendments/${amendmentId}/status`;
};

const newDeal = aDeal({
dealType: DEAL_TYPE.GEF,
submissionType: DEAL_SUBMISSION_TYPE.AIN,
}) as AnyObject;

describe('PATCH /v1/portal/facilities/:facilityId/amendments/:amendmentId/status', () => {
let dealId: string;
let facilityId: string;
let portalUserId: string;

beforeAll(async () => {
await wipeDB.wipe([MONGO_DB_COLLECTIONS.FACILITIES, MONGO_DB_COLLECTIONS.TFM_FACILITIES, MONGO_DB_COLLECTIONS.ELIGIBILITY_CRITERIA_AMENDMENTS]);
await db
.getCollection(MONGO_DB_COLLECTIONS.ELIGIBILITY_CRITERIA_AMENDMENTS)
.then((collection) => collection.insertOne(amendmentsEligibilityCriteria(1, [FACILITY_TYPE.CASH, FACILITY_TYPE.CONTINGENT])));

portalUserId = (await createPortalUser())._id;
});

beforeEach(async () => {
const createDealResponse: { body: { _id: string } } = await createDeal({ deal: newDeal, user: aPortalUser() });
dealId = createDealResponse.body._id;

const createFacilityResponse: { body: { _id: string } } = await testApi
.post({ dealId, type: FACILITY_TYPE.CASH, hasBeenIssued: false })
.to('/v1/portal/gef/facilities');

facilityId = createFacilityResponse.body._id;

await submitDealToTfm({ dealId, dealSubmissionType: DEAL_SUBMISSION_TYPE.AIN, dealType: DEAL_TYPE.GEF });
});

afterAll(() => {
process.env = originalEnv;
});

describe('when FF_PORTAL_FACILITY_AMENDMENTS_ENABLED is set to `false`', () => {
beforeAll(() => {
process.env.FF_PORTAL_FACILITY_AMENDMENTS_ENABLED = 'false';
});

it(`should return ${HttpStatusCode.NotFound}`, async () => {
const amendmentId = new ObjectId().toString();

const { status } = await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus: 'a new status' })
.to(generateUrl(facilityId, amendmentId));

expect(status).toEqual(HttpStatusCode.NotFound);
});
});

describe('when FF_PORTAL_FACILITY_AMENDMENTS_ENABLED is set to `true`', () => {
let amendmentId: string;

beforeAll(() => {
process.env.FF_PORTAL_FACILITY_AMENDMENTS_ENABLED = 'true';
});

beforeEach(async () => {
const existingAmendment = await createPortalFacilityAmendment({
facilityId,
dealId,
userId: portalUserId,
amendment: {
...aPortalFacilityAmendmentUserValues(),
eligibilityCriteria: {
criteria: [{ id: 1, text: 'item 1', answer: true }],
version: 1,
},
},
});

amendmentId = existingAmendment.amendmentId.toString();
});

it(`should return ${HttpStatusCode.BadRequest} when the facility id is invalid`, async () => {
const anInvalidFacilityId = 'InvalidId';

const { body, status } = (await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus: 'a new status' })
.to(generateUrl(anInvalidFacilityId, amendmentId))) as ErrorResponse;

expect(status).toEqual(HttpStatusCode.BadRequest);

expect(body).toEqual({
message: "Expected path parameter 'facilityId' to be a valid mongo id",
code: API_ERROR_CODE.INVALID_MONGO_ID_PATH_PARAMETER,
});
});

it(`should return ${HttpStatusCode.BadRequest} when the amendment id is invalid`, async () => {
const anInvalidAmendmentId = 'InvalidId';

const { body, status } = (await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus: 'a new status' })
.to(generateUrl(facilityId, anInvalidAmendmentId))) as ErrorResponse;

expect(status).toEqual(HttpStatusCode.BadRequest);

expect(body).toEqual({
message: "Expected path parameter 'amendmentId' to be a valid mongo id",
code: API_ERROR_CODE.INVALID_MONGO_ID_PATH_PARAMETER,
});
});

it(`should return ${HttpStatusCode.BadRequest} when the deal id is invalid`, async () => {
const anInvalidDealId = 'dealId';

const { body, status } = (await testApi
.patch({
dealId: anInvalidDealId,
auditDetails: generatePortalAuditDetails(portalUserId),
newStatus: PORTAL_AMENDMENT_STATUS.READY_FOR_CHECKERS_APPROVAL,
})
.to(generateUrl(facilityId, amendmentId))) as ErrorResponse;

expect(status).toEqual(HttpStatusCode.BadRequest);

expect(body).toEqual({
status: HttpStatusCode.BadRequest,
message: ['dealId: _id must be a valid mongo object id (custom)'],
code: API_ERROR_CODE.INVALID_PAYLOAD,
});
});

it(`should return ${HttpStatusCode.BadRequest} when the new status is invalid`, async () => {
const anInvalidStatus = 'a new status';

const { body, status } = (await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus: anInvalidStatus })
.to(generateUrl(facilityId, amendmentId))) as ErrorResponse;

expect(status).toEqual(HttpStatusCode.BadRequest);

expect(body).toEqual({
status: HttpStatusCode.BadRequest,
code: 'INVALID_PAYLOAD',
message: [
`newStatus: Invalid enum value. Expected '${PORTAL_AMENDMENT_STATUS.READY_FOR_CHECKERS_APPROVAL}', received '${anInvalidStatus}' (invalid_enum_value)`,
],
});
});

describe(`when newStatus is ${PORTAL_AMENDMENT_STATUS.READY_FOR_CHECKERS_APPROVAL}`, () => {
const newStatus = PORTAL_AMENDMENT_STATUS.READY_FOR_CHECKERS_APPROVAL;

it(`should return ${HttpStatusCode.NotFound} when the facility does not exist`, async () => {
const aValidButNonExistentFacilityId = new ObjectId().toString();

const { body, status } = (await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus })
.to(generateUrl(aValidButNonExistentFacilityId, amendmentId))) as ErrorResponse;

expect(status).toEqual(HttpStatusCode.NotFound);
expect(body).toEqual({
status: HttpStatusCode.NotFound,
message: `Facility not found: ${aValidButNonExistentFacilityId}`,
});
});

it(`should return ${HttpStatusCode.NotFound} when the amendment does not exist`, async () => {
const aValidButNonExistentAmendmentId = new ObjectId().toString();

const { body, status } = (await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus })
.to(generateUrl(facilityId, aValidButNonExistentAmendmentId))) as ErrorResponse;

expect(status).toEqual(HttpStatusCode.NotFound);
expect(body).toEqual({
status: HttpStatusCode.NotFound,
message: `Amendment not found: ${aValidButNonExistentAmendmentId} on facility: ${facilityId}`,
});
});

it(`should return ${HttpStatusCode.Conflict} when the amendment is incomplete`, async () => {
// Arrange
const anIncompleteAmendment = await createPortalFacilityAmendment({
facilityId,
dealId,
userId: portalUserId,
});
const incompleteAmendmentId = anIncompleteAmendment.amendmentId.toString();

// Act
const { body, status } = (await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus })
.to(generateUrl(facilityId, incompleteAmendmentId))) as ErrorResponse;

// Assert
expect(status).toEqual(HttpStatusCode.Conflict);
expect(body).toEqual({
status: HttpStatusCode.Conflict,
message: `Amendment ${incompleteAmendmentId} on facility ${facilityId} is incomplete: neither changeCoverEndDate nor changeFacilityValue is true`,
});
});

it(`should return ${HttpStatusCode.Ok} when the payload is valid & the amendment exists`, async () => {
const { status } = await testApi
.patch({ dealId, auditDetails: generatePortalAuditDetails(portalUserId), newStatus })
.to(generateUrl(facilityId, amendmentId));

expect(status).toEqual(HttpStatusCode.Ok);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export class TfmFacilitiesRepo {
* @param updatePortalFacilityAmendmentByAmendmentIdParams.facilityId - the facility id
* @param updatePortalFacilityAmendmentByAmendmentIdParams.update - the update to apply
* @param updatePortalFacilityAmendmentByAmendmentIdParams.auditDetails - the users audit details
* @param updatePortalFacilityAmendmentByAmendmentIdParams.allowedStatuses - the statuses of the amendment
*
* @returns The update result.
*/
Expand All @@ -407,18 +408,26 @@ export class TfmFacilitiesRepo {
facilityId,
update,
auditDetails,
allowedStatuses,
}: {
update: Partial<PortalFacilityAmendment>;
amendmentId: ObjectId;
facilityId: ObjectId;
auditDetails: AuditDetails;
allowedStatuses: PortalAmendmentStatus[];
}): Promise<UpdateResult> {
try {
const collection = await this.getCollection();

const findFilter: Filter<TfmFacility> = {
_id: { $eq: new ObjectId(facilityId) },
amendments: { $elemMatch: { amendmentId: { $eq: new ObjectId(amendmentId) }, type: AMENDMENT_TYPES.PORTAL } },
amendments: {
$elemMatch: {
amendmentId: { $eq: new ObjectId(amendmentId) },
type: { $eq: AMENDMENT_TYPES.PORTAL },
status: { $in: allowedStatuses },
},
},
};

const updateFilter: UpdateFilter<TfmFacility> = flatten({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ObjectId, UpdateResult } from 'mongodb';
import { flatten } from 'mongo-dot-notation';
import { MONGO_DB_COLLECTIONS, AMENDMENT_TYPES, AmendmentNotFoundError } from '@ukef/dtfs2-common';
import { MONGO_DB_COLLECTIONS, AMENDMENT_TYPES, AmendmentNotFoundError, PORTAL_AMENDMENT_STATUS } from '@ukef/dtfs2-common';
import { generateAuditDatabaseRecordFromAuditDetails, generatePortalAuditDetails } from '@ukef/dtfs2-common/change-stream';
import { TfmFacilitiesRepo } from './tfm-facilities.repo';
import { mongoDbClient } from '../../drivers/db-client';
Expand All @@ -16,6 +16,7 @@ const update = {
};
const amendmentId = new ObjectId();
const auditDetails = generatePortalAuditDetails(aPortalUser()._id);
const allowedStatuses = [PORTAL_AMENDMENT_STATUS.DRAFT];

const mockUpdateResult = {
modifiedCount: 1,
Expand Down Expand Up @@ -48,7 +49,7 @@ describe('TfmFacilitiesRepo', () => {

it(`should call getCollection with ${MONGO_DB_COLLECTIONS.TFM_FACILITIES}`, async () => {
// Act
await TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails });
await TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails, allowedStatuses });

// Assert
expect(mockGetCollection).toHaveBeenCalledTimes(1);
Expand All @@ -57,13 +58,19 @@ describe('TfmFacilitiesRepo', () => {

it(`should call updateOne with the correct parameters`, async () => {
// Act
await TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails });
await TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails, allowedStatuses });

// Assert

const expectedFindFilter = {
_id: { $eq: new ObjectId(facilityId) },
amendments: { $elemMatch: { amendmentId: { $eq: new ObjectId(amendmentId) }, type: AMENDMENT_TYPES.PORTAL } },
amendments: {
$elemMatch: {
amendmentId: { $eq: new ObjectId(amendmentId) },
type: { $eq: AMENDMENT_TYPES.PORTAL },
status: { $in: allowedStatuses },
},
},
};

const expectedUpdateFilter = flatten({
Expand All @@ -77,7 +84,7 @@ describe('TfmFacilitiesRepo', () => {

it(`should return the updateResult`, async () => {
// Act
const response = await TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails });
const response = await TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails, allowedStatuses });

// Assert
expect(response).toEqual(mockUpdateResult);
Expand All @@ -88,9 +95,9 @@ describe('TfmFacilitiesRepo', () => {
mockUpdateOne.mockResolvedValue({ ...mockUpdateResult, modifiedCount: 0 });

// Act + Assert
await expect(() => TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails })).rejects.toThrow(
AmendmentNotFoundError,
);
await expect(() =>
TfmFacilitiesRepo.updatePortalFacilityAmendmentByAmendmentId({ amendmentId, facilityId, update, auditDetails, allowedStatuses }),
).rejects.toThrow(AmendmentNotFoundError);
});
});
});
Loading
Loading