Skip to content
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

feat(FN-3163): Create new DTFS exporters in Salesforce #4021

Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
6f10894
feat: mvp create Salesforce party call when party not present
Sep 23, 2024
6c06a57
feat: post by companies house number
Sep 25, 2024
03db4e8
feat: update routes documentation
Sep 25, 2024
da26b21
feat: use direct customer GET
Oct 8, 2024
7bbf0f0
feat: send company name with registration number
Oct 16, 2024
d2db190
feat: add TODO pending SF URN gen process
Oct 22, 2024
538603d
feat: getpartyurn from creation response
Oct 25, 2024
4b4cadf
feat: update creation response type
Oct 28, 2024
80e4368
feat: feature flag new behaviour
Oct 30, 2024
3d0c070
feat: feature flag tests
Oct 30, 2024
a0bc706
feat: update route docstring
Oct 30, 2024
14ea107
feat: additional feature flagging
Oct 31, 2024
0a4fff2
refactor: align with existing process.env initialisation
Oct 31, 2024
549455f
refactor: codacy appeasal
Nov 4, 2024
6a2e28a
feat: update test content
Nov 4, 2024
5e4e575
refactor: codacy appeasal
Nov 4, 2024
5eee535
refactor: sonarcloud appeasal
Nov 4, 2024
a7a9aa8
refactor: revert appeasal
Nov 4, 2024
97202c7
refactor: lint
Nov 5, 2024
2a54c9e
refactor: lint
Nov 5, 2024
c36a450
refactor: lint
Nov 5, 2024
fb6f563
feat: check feature flagging correctly and update error messages
Nov 6, 2024
3aaf88c
feat: increase timeout
Nov 6, 2024
f61bd47
feat: reduce timeout
Nov 7, 2024
ff8a3a3
feat: rename endpoint direct -> sf
Nov 7, 2024
5f657de
feat: optional chaining
Nov 7, 2024
68b8954
feat: test current getPartyUrn behaviour
Nov 7, 2024
2e6d4ef
feat: test feature flagged getPartyUrn
Nov 7, 2024
5a98020
feat: test feature flagged controller behaviour
Nov 7, 2024
d9ca0e5
docs: update docstring
Nov 7, 2024
efe4ffd
feat: update test behaviour for non-flagged case
Nov 8, 2024
26da8c7
feat: confirm createparty is not called without feature flag
Nov 8, 2024
0a872e3
refactor: rename feature flag
Nov 11, 2024
6348e29
feat: align feature flagging with the current implementation
Nov 11, 2024
c01d1f5
docs: update
Nov 11, 2024
0bfe9ef
test: split up tests
Nov 11, 2024
9e67cd3
refactor: refactor nested if and ternary
Nov 11, 2024
13e86e9
feat: respond to pr comments
Nov 25, 2024
aab01f4
feat: mvp create Salesforce party call when party not present
Sep 23, 2024
6645fb5
feat: post by companies house number
Sep 25, 2024
e4a3827
feat: send company name with registration number
Oct 16, 2024
9959cef
feat: getpartyurn from creation response
Oct 25, 2024
4da58d0
feat: feature flag new behaviour
Oct 30, 2024
d3a5c89
refactor: align with existing process.env initialisation
Oct 31, 2024
3b71ffb
refactor: codacy appeasal
Nov 4, 2024
80a9ab0
refactor: codacy appeasal
Nov 4, 2024
8c5b0ef
refactor: sonarcloud appeasal
Nov 4, 2024
145474a
refactor: revert appeasal
Nov 4, 2024
0232d3b
refactor: lint
Nov 5, 2024
e09e759
feat: check feature flagging correctly and update error messages
Nov 6, 2024
a878a3e
feat: test current getPartyUrn behaviour
Nov 7, 2024
895c042
feat: test feature flagged getPartyUrn
Nov 7, 2024
639d4fa
feat: test feature flagged controller behaviour
Nov 7, 2024
6385446
feat: update test behaviour for non-flagged case
Nov 8, 2024
86fa72f
feat: confirm createparty is not called without feature flag
Nov 8, 2024
653c651
refactor: rename feature flag
Nov 11, 2024
4e5e2cf
feat: align feature flagging with the current implementation
Nov 11, 2024
149fbc0
test: split up tests
Nov 11, 2024
3271b3b
feat: respond to pr comments
Nov 25, 2024
3a442e1
feat: rework to make get or create a single request handled by apim
Nov 27, 2024
38ca5c4
docs: getPartyUrn docstring
Dec 2, 2024
baa6e78
feat: tweak post-rebase
Dec 3, 2024
1eef3f7
feat: tweak post-rebase
Dec 3, 2024
78847f9
test: update deal.party-db.test.js
Dec 3, 2024
1016c84
refactor: cleanup requires
Dec 3, 2024
61daa9b
fix: naming bug
Dec 3, 2024
e0462ae
refactor: cleanup
Dec 3, 2024
04a662d
refactor: remove blank line
Dec 3, 2024
ec492ae
refactor: linter appeasal
Dec 3, 2024
0d9161c
refactor: rename feature flag
Dec 19, 2024
6ca58b4
feat: remove redundant tests
Dec 19, 2024
fe19ae5
feat: change controller error handling behaviour
Dec 19, 2024
e2505da
feat: tweak companyregno naming
Dec 19, 2024
ba57645
docs: add 400 responses to swagger docs
Dec 19, 2024
65411ea
refactor: use HttpStatusCode
Dec 19, 2024
0881db4
feat: add no companies house number log and refactor
Dec 19, 2024
2953d28
feat: update deal party-db and revert null check
Dec 19, 2024
3a9c762
feat: optional chaining
Dec 20, 2024
ad76b4a
feat: update error logging and refactor deal party db tests
Dec 20, 2024
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
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,5 @@ FF_TFM_DEAL_CANCELLATION_ENABLED=false
FF_PORTAL_FACILITY_AMENDMENTS_ENABLED=false
FF_TFM_SSO_ENABLED=false
FF_FEE_RECORD_CORRECTION_ENABLED=false
AUTOMATIC_SALESFORCE_CUSTOMER_CREATION_ENABLED=false
GEF_DEAL_VERSION=0
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ services:
- DELETION_AUDIT_LOGS_TTL_SECONDS
- FF_TFM_FACILITY_END_DATE_ENABLED
- FF_TFM_DEAL_CANCELLATION_ENABLED
- AUTOMATIC_SALESFORCE_CUSTOMER_CREATION_ENABLED
- ENTRA_ID_CLIENT_ID
- ENTRA_ID_CLOUD_INSTANCE
- ENTRA_ID_TENANT_ID
Expand Down Expand Up @@ -320,6 +321,7 @@ services:
- NODE_ENV
- RATE_LIMIT_THRESHOLD
- ESTORE_CRON_MANAGER_SCHEDULE
- AUTOMATIC_SALESFORCE_CUSTOMER_CREATION_ENABLED
entrypoint: npx ts-node external-api/src/index.ts

gef-ui:
Expand Down
63 changes: 58 additions & 5 deletions external-api/api-tests/v1/party-db.api-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,72 @@

import MockAdapter from 'axios-mock-adapter';
import axios, { HttpStatusCode } from 'axios';
import { MOCK_COMPANY_REGISTRATION_NUMBERS } from '@ukef/dtfs2-common';
import { MOCK_COMPANY_REGISTRATION_NUMBERS, isSalesforceCustomerCreationEnabled } from '@ukef/dtfs2-common';
import { app } from '../../src/createApp';
import { api } from '../api';

const { APIM_MDM_URL } = process.env;
const { VALID, VALID_WITH_LETTERS } = MOCK_COMPANY_REGISTRATION_NUMBERS;
const { get } = api(app);
let axiosMock: MockAdapter;

const axiosMock = new MockAdapter(axios);
axiosMock.onGet(`${APIM_MDM_URL}customers?companyReg=${VALID}`).reply(HttpStatusCode.Ok, {});
axiosMock.onGet(`${APIM_MDM_URL}customers?companyReg=${VALID_WITH_LETTERS}`).reply(HttpStatusCode.Ok, {});
jest.mock('@ukef/dtfs2-common', () => ({
...jest.requireActual('@ukef/dtfs2-common'),
isSalesforceCustomerCreationEnabled: jest.fn(),
}));

beforeEach(() => {
axiosMock = new MockAdapter(axios);

axiosMock.onGet(`${APIM_MDM_URL}customers?companyReg=${VALID}`).reply(HttpStatusCode.Ok, {});
axiosMock.onGet(`${APIM_MDM_URL}customers?companyReg=${VALID_WITH_LETTERS}`).reply(HttpStatusCode.Ok, {});
});

afterEach(() => {
axiosMock.resetHistory();
});

describe('when automatic Salesforce customer creation feature flag is disabled', () => {
beforeEach(() => {
jest.mocked(isSalesforceCustomerCreationEnabled).mockReturnValue(false);
});

describe('/party-db', () => {
describe('GET /party-db', () => {
it(`returns a ${HttpStatusCode.Ok} response with a valid companies house number`, async () => {
const { status } = await get(`/party-db/${VALID}`);

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

it(`returns a ${HttpStatusCode.Ok} response with a valid companies house number`, async () => {
const { status } = await get(`/party-db/${VALID_WITH_LETTERS}`);

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

const invalidCompaniesHouseNumberTestCases = [['ABC22'], ['127.0.0.1'], ['{}'], ['[]']];

describe('when company house number is invalid', () => {
test.each(invalidCompaniesHouseNumberTestCases)(
`returns a ${HttpStatusCode.BadRequest} if you provide an invalid company house number %s`,
async (companyHouseNumber) => {
const { status, body } = await get(`/party-db/${companyHouseNumber}`);

expect(status).toEqual(HttpStatusCode.BadRequest);
expect(body).toMatchObject({ data: 'Invalid company registration number', status: HttpStatusCode.BadRequest });
},
);
});
});
});

describe('when automatic Salesforce customer creation feature flag is enabled', () => {
beforeEach(() => {
jest.mocked(isSalesforceCustomerCreationEnabled).mockReturnValue(true);
});

describe('/party-db', () => {
describe('GET /party-db', () => {
it(`returns a ${HttpStatusCode.Ok} response with a valid companies house number`, async () => {
const { status } = await get(`/party-db/${VALID}`);
Expand Down
88 changes: 73 additions & 15 deletions external-api/src/v1/controllers/party-db.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HEADERS, isValidCompanyRegistrationNumber } from '@ukef/dtfs2-common';
import { CustomExpressRequest, HEADERS, isValidCompanyRegistrationNumber } from '@ukef/dtfs2-common';
import axios, { AxiosError, HttpStatusCode } from 'axios';
import * as dotenv from 'dotenv';
import { Request, Response } from 'express';
Expand All @@ -12,23 +12,81 @@ const headers = {
};

export const lookup = async (req: Request, res: Response) => {
const { partyDbCompanyRegistrationNumber: companyReg } = req.params;
try {
const { partyDbCompanyRegistrationNumber: companyReg } = req.params;

if (!isValidCompanyRegistrationNumber(companyReg)) {
console.error('Invalid company registration number provided %s', companyReg);
return res.status(HttpStatusCode.BadRequest).send({ status: HttpStatusCode.BadRequest, data: 'Invalid company registration number' });
}
if (!isValidCompanyRegistrationNumber(companyReg)) {
console.error('Invalid company registration number provided %s', companyReg);
return res.status(HttpStatusCode.BadRequest).send({ status: HttpStatusCode.BadRequest, data: 'Invalid company registration number' });
}

const response: { status: number; data: unknown } = await axios({
method: 'get',
url: `${APIM_MDM_URL}customers?companyReg=${companyReg}`,
headers,
});

const response: { status: number; data: unknown } = await axios({
method: 'get',
url: `${APIM_MDM_URL}customers?companyReg=${companyReg}`,
headers,
}).catch((error: AxiosError) => {
const { status, data } = response;
return res.status(status).send(data);
} catch (error) {
console.error('Error calling Party DB API %o', error);
return { data: 'Failed to call Party DB API', status: error?.response?.status || HttpStatusCode.InternalServerError };
});
if (error instanceof AxiosError) {
return res.status(error?.response?.status || HttpStatusCode.InternalServerError).send('Error calling Party DB API');
}
return res.status(HttpStatusCode.InternalServerError).send({ status: HttpStatusCode.InternalServerError, message: 'An unknown error occurred' });
}
};

const { status, data } = response;
/**
* Gets a customer in Salesforce, and creates it if it does not exist by sending a request to APIM,
* based on the provided company registration number and company name.
* This function validates the company registration number and sends an HTTP POST request to the MDM API
* to get or create the party. If validation fails or an error occurs, it returns the appropriate HTTP status and error message.
*
* @param {CustomExpressRequest<{ reqBody: { companyName: string } }>} req - The Express request object, which contains:
* - `req.body` - The company name (`companyName`) and companies house number (`companyReg`).
* @param {Response} res - The Express response object used to send the HTTP response.
*
* @returns {Promise<Response>} A promise that resolves to an HTTP response. The response contains:
* - On success: The status and data returned from the MDM API.
* - On failure: A relevant error message with the corresponding status code if there's an issue with the MDM request.
*
* @example
* // Example usage:
* const req = { body: { companyReg: '12345678', companyName: 'Test Corp' } };
* const res = { status: () => res, send: () => {} };
* await getOrCreateParty(req, res);
*/
export const getOrCreateParty = async (req: CustomExpressRequest<{ reqBody: { companyRegNo: string; companyName: string } }>, res: Response) => {
abhi-markan marked this conversation as resolved.
Show resolved Hide resolved
try {
const { companyRegNo: companyRegistrationNumber, companyName } = req.body;

return res.status(status).send(data);
if (!isValidCompanyRegistrationNumber(companyRegistrationNumber)) {
console.error('Invalid company registration number provided %s', companyRegistrationNumber);
return res.status(HttpStatusCode.BadRequest).send({ status: HttpStatusCode.BadRequest, data: 'Invalid company registration number' });
}

if (!companyName) {
console.error('No company name provided');
return res.status(HttpStatusCode.BadRequest).send({ status: HttpStatusCode.BadRequest, data: 'Invalid company name' });
}

const response: { status: number; data: unknown } = await axios({
method: 'post',
url: `${APIM_MDM_URL}customers`,
headers,
data: {
companyRegistrationNumber,
companyName,
},
});
const { status, data } = response;
return res.status(status).send(data);
} catch (error) {
console.error('Error calling Party DB API %o', error);
if (error instanceof AxiosError) {
return res.status(error?.response?.status || HttpStatusCode.InternalServerError).send('Error calling Party DB API');
}
return res.status(HttpStatusCode.InternalServerError).send({ status: HttpStatusCode.InternalServerError, message: 'An unknown error occurred' });
}
};
41 changes: 41 additions & 0 deletions external-api/src/v1/routes/external-apis.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,45 @@ apiRoutes.post('/acbs/facility/:id/amendments', acbs.amendAcbsFacilityPost);
*/
apiRoutes.get('/party-db/:partyDbCompanyRegistrationNumber', partyDb.lookup);

/**
* @openapi
* /party-db/:partyDbCompanyRegistrationNumber:
* post:
* summary: Get a UKEF party from Salesforce, or create it if it doesn't exist
* tags: [APIM, Salesforce]
* description: We only consume the Companies House number and company name. Not all fields are in the response example.
* parameters:
* - in: path
* name: partyDbCompanyRegistrationNumber
* schema:
* type: string
* example: '12341234'
* required: true
* description: Companies House Registration Number for UKEF Party creation
* requestBody:
* required: true
* description: Company fields
* content:
* application/json:
* schema:
* name: companyName
* schema:
* type: string
* example: 'Some Name'
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/definitions/PartyDB'
abhi-markan marked this conversation as resolved.
Show resolved Hide resolved
* 400:
* description: Bad request. Invalid Companies House registration number
* 500:
* description: Error getting or creating the party
*/
apiRoutes.post('/party-db', partyDb.getOrCreateParty);

/**
* @openapi
* /party-db/urn/:urn:
Expand All @@ -380,6 +419,8 @@ apiRoutes.get('/party-db/:partyDbCompanyRegistrationNumber', partyDb.lookup);
* application/json:
* schema:
* $ref: '#/definitions/PartyDB'
* 400:
* description: Bad request. Invalid Companies House registration number
* 404:
* description: Not found
*/
Expand Down
14 changes: 12 additions & 2 deletions gef-ui/server/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ const updateSupportingInformation = async ({ dealId, application, field, user, u
}
};

/**
* Updates the application status for a specific deal.
*
* Notably, the timeout is so long because in the event that the deal status is being updated to 'Submitted',
* multiple API calls are triggered, and the user has to wait for the completion of these in the UI in the
* current implementation.
*
* In the future, these operations should instead be run as part of a fail-safe background process, as discussed
* in the root README. At that point, the timeout can be reduced.
*/
const setApplicationStatus = async ({ dealId, status, userToken }) => {
if (!isValidMongoId(dealId)) {
console.error('setApplicationStatus: API call failed for dealId %s', dealId);
Expand All @@ -113,8 +123,8 @@ const setApplicationStatus = async ({ dealId, status, userToken }) => {
{
status,
},
{ ...config(userToken), timeout: 10000 },
); // Application status has multiple api calls in portal api
{ ...config(userToken), timeout: 30000 },
);
return data;
} catch (error) {
return apiErrorHandler(error);
Expand Down
1 change: 1 addition & 0 deletions libs/common/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
isTfmDealCancellationFeatureFlagEnabled,
isTfmSsoFeatureFlagEnabled,
isPortalFacilityAmendmentsFeatureFlagEnabled,
isSalesforceCustomerCreationEnabled,
} from './is-feature-flag-enabled';
export * from './gef-deal-versioning';
export * from './monetary-value';
Expand Down
5 changes: 5 additions & 0 deletions libs/common/src/helpers/is-feature-flag-enabled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isTfmFacilityEndDateFeatureFlagEnabled,
isTfmPaymentReconciliationFeatureFlagEnabled,
isTfmSsoFeatureFlagEnabled,
isSalesforceCustomerCreationEnabled,
} from './is-feature-flag-enabled';
import { withBooleanFeatureFlagTests } from './with-boolean-feature-flag.tests';

Expand All @@ -15,4 +16,8 @@ describe('is-feature-flag-enabled helpers', () => {
withBooleanFeatureFlagTests({ featureFlagName: 'FF_TFM_DEAL_CANCELLATION_ENABLED', getFeatureFlagValue: isTfmDealCancellationFeatureFlagEnabled });
withBooleanFeatureFlagTests({ featureFlagName: 'FF_PORTAL_FACILITY_AMENDMENTS_ENABLED', getFeatureFlagValue: isPortalFacilityAmendmentsFeatureFlagEnabled });
withBooleanFeatureFlagTests({ featureFlagName: 'FF_TFM_SSO_ENABLED', getFeatureFlagValue: isTfmSsoFeatureFlagEnabled });
withBooleanFeatureFlagTests({
featureFlagName: 'AUTOMATIC_SALESFORCE_CUSTOMER_CREATION_ENABLED',
getFeatureFlagValue: isSalesforceCustomerCreationEnabled,
});
});
3 changes: 3 additions & 0 deletions libs/common/src/helpers/is-feature-flag-enabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const featureFlagsSchema = z.object({
FF_TFM_DEAL_CANCELLATION_ENABLED: featureFlagOptions,
FF_PORTAL_FACILITY_AMENDMENTS_ENABLED: featureFlagOptions,
FF_TFM_SSO_ENABLED: featureFlagOptions,
AUTOMATIC_SALESFORCE_CUSTOMER_CREATION_ENABLED: featureFlagOptions,
});

export type FeatureFlag = keyof z.infer<typeof featureFlagsSchema>;
Expand All @@ -36,3 +37,5 @@ export const isTfmDealCancellationFeatureFlagEnabled = isFeatureFlagEnabled('FF_
export const isPortalFacilityAmendmentsFeatureFlagEnabled = isFeatureFlagEnabled('FF_PORTAL_FACILITY_AMENDMENTS_ENABLED');

export const isTfmSsoFeatureFlagEnabled = isFeatureFlagEnabled('FF_TFM_SSO_ENABLED');

export const isSalesforceCustomerCreationEnabled = isFeatureFlagEnabled('AUTOMATIC_SALESFORCE_CUSTOMER_CREATION_ENABLED');
27 changes: 23 additions & 4 deletions trade-finance-manager-api/src/v1/__mocks__/api.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { isSalesforceCustomerCreationEnabled } = require('@ukef/dtfs2-common');
const { HttpStatusCode } = require('axios');
const { MOCK_FACILITIES } = require('./mock-facilities');
const MOCK_BSS_FACILITIES_USD_CURRENCY = require('./mock-facilities-USD-currency');
const MOCK_CURRENCY_EXCHANGE_RATE = require('./mock-currency-exchange-rate');
Expand Down Expand Up @@ -178,14 +180,31 @@ module.exports = {
getFacilityExposurePeriod: jest.fn(() => ({
exposurePeriodInMonths: 12,
})),
getPartyDbInfo: ({ companyRegNo }) =>
companyRegNo === 'NO_MATCH'
? false
: [
getPartyDbInfo: ({ companyRegNo }) => {
const noCompanyMatch = companyRegNo === 'NO_MATCH';

if (isSalesforceCustomerCreationEnabled()) {
if (noCompanyMatch) {
return { status: HttpStatusCode.NotFound, data: 'Party not found' };
}

return {
status: HttpStatusCode.Ok,
data: [
{
partyUrn: 'testPartyUrn',
},
],
};
}
if (noCompanyMatch) {
return false;
}

return {
partyUrn: 'testPartyUrn',
};
},
findUser: (username) => {
if (username === 'invalidUser') {
return false;
Expand Down
Loading