Skip to content

Commit

Permalink
feat(FN-3664): add risk data to created customers (#1163)
Browse files Browse the repository at this point in the history
## Introduction ✏️
In discussion with Risk teams, they wanted Risk data to be added to
newly created customers in Salesforce.
The APIM side will default Risk rating at B+ and Loss given default at
50%, and the DTFS side will pass Probability of Default through to APIM
to be set on the new record.

## Resolution ✔️
Defaults the first two risk values, and sets the Probability of Default
value to the value DTFS passes through.

## Miscellaneous ➕
A small doc typo fix I found.

---------

Co-authored-by: Nat Dean-Lewis <[email protected]>
  • Loading branch information
natdeanlewissoftwire and Nat Dean-Lewis authored Jan 23, 2025
1 parent 30184c1 commit 5ff21fa
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 17 deletions.
2 changes: 2 additions & 0 deletions src/constants/customers.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ export const CUSTOMERS = {
COMPANYREG: '06012345',
PARTYURN: '00302069',
NAME: 'Testing Systems Ltd',
CREDIT_RISK_RATING: 'B+',
LOSS_GIVEN_DEFAULT: 50,
},
};
22 changes: 22 additions & 0 deletions src/helpers/date-formatter.helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { salesforceFormattedCurrentDate } from './date-formatter.helper';

describe('salesforceFormattedCurrentDate function', () => {
afterEach(() => {
jest.restoreAllMocks();
});

const testCases = [
{ mockDate: new Date('2007-04-27T00:00:00Z'), expected: '2007-04-27' },
{ mockDate: new Date('2007-04-27'), expected: '2007-04-27' },
{ mockDate: new Date(2007, 3, 27), expected: '2007-04-27' },
{ mockDate: new Date('1970-01-01T12:34:56Z'), expected: '1970-01-01' },
{ mockDate: new Date('9999-12-31'), expected: '9999-12-31' },
{ mockDate: new Date('2020-02-29T00:00:00Z'), expected: '2020-02-29' }, // Leap year
];

test.each(testCases)('should format the date $input as $expected', ({ mockDate, expected }) => {
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);

expect(salesforceFormattedCurrentDate()).toBe(expected);
});
});
14 changes: 14 additions & 0 deletions src/helpers/date-formatter.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Returns the current date in the correct format for ingestion by the Salesforce sObject API.
*
* @returns {string} the current date in yyyy-mm-dd format
*/

export const salesforceFormattedCurrentDate = () => {
const today = new Date();
const dd = String(today.getDate()).padStart(2, '0');
const mm = String(today.getMonth() + 1).padStart(2, '0');
const yyyy = String(today.getFullYear());

return `${yyyy}-${mm}-${dd}`;
};
2 changes: 1 addition & 1 deletion src/modules/customers/customers.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('CustomersController', () => {
});

describe('getOrCreateCustomer', () => {
const DTFSCustomerDto: DTFSCustomerDto = { companyRegistrationNumber: CUSTOMERS.EXAMPLES.COMPANYREG, companyName: 'TEST NAME' };
const DTFSCustomerDto: DTFSCustomerDto = { companyRegistrationNumber: CUSTOMERS.EXAMPLES.COMPANYREG, companyName: 'TEST NAME', probabilityOfDefault: 3 };

const getOrCreateCustomerResponse: GetCustomersResponse = [
{
Expand Down
2 changes: 1 addition & 1 deletion src/modules/customers/customers.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('CustomerService', () => {
});

describe('getOrCreateCustomer', () => {
const DTFSCustomerDto: DTFSCustomerDto = { companyRegistrationNumber: '12345678', companyName: 'TEST NAME' };
const DTFSCustomerDto: DTFSCustomerDto = { companyRegistrationNumber: '12345678', companyName: 'TEST NAME', probabilityOfDefault: 3 };
const salesforceCreateCustomerResponse: CreateCustomerSalesforceResponseDto = {
id: 'customer-id',
errors: null,
Expand Down
8 changes: 7 additions & 1 deletion src/modules/customers/customers.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { CUSTOMERS } from '@ukef/constants';
import { DunAndBradstreetService } from '@ukef/helper-modules/dun-and-bradstreet/dun-and-bradstreet.service';
import { salesforceFormattedCurrentDate } from '@ukef/helpers/date-formatter.helper';
import { GetCustomersInformaticaQueryDto } from '@ukef/modules/informatica/dto/get-customers-informatica-query.dto';
import { InformaticaService } from '@ukef/modules/informatica/informatica.service';
import { SalesforceService } from '@ukef/modules/salesforce/salesforce.service';
Expand Down Expand Up @@ -70,7 +72,7 @@ export class CustomersService {

try {
const existingCustomersInInformatica = await this.informaticaService.getCustomers(backendQuery);
// If the customer exist in Informatica
// If the customer does exist in Informatica
if (existingCustomersInInformatica?.[0]) {
return await this.handleInformaticaResponse(res, DTFSCustomerDto, existingCustomersInInformatica);
} else {
Expand Down Expand Up @@ -205,6 +207,10 @@ export class CustomersService {
Party_URN__c: partyUrn,
D_B_Number__c: dunsNumber,
Company_Registration_Number__c: DTFSCustomerDto.companyRegistrationNumber,
CCM_Credit_Risk_Rating__c: CUSTOMERS.EXAMPLES.CREDIT_RISK_RATING,
CCM_Credit_Risk_Rating_Date__c: salesforceFormattedCurrentDate(),
CCM_Loss_Given_Default__c: CUSTOMERS.EXAMPLES.LOSS_GIVEN_DEFAULT,
CCM_Probability_of_Default__c: DTFSCustomerDto.probabilityOfDefault,
};

const salesforceCreateCustomerResponse: CreateCustomerSalesforceResponseDto = await this.salesforceService.createCustomer(createCustomerDto);
Expand Down
27 changes: 26 additions & 1 deletion src/modules/customers/dto/create-customer.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, Length, MaxLength, MinLength } from 'class-validator';
import { IsNotEmpty, IsNumber, IsString, Length, Max, MaxLength, Min, MinLength } from 'class-validator';

export class CreateCustomerDto {
@ApiProperty({ description: 'Account Name' })
Expand All @@ -25,4 +25,29 @@ export class CreateCustomerDto {
@MinLength(8)
@MaxLength(10)
Company_Registration_Number__c: string;

@ApiProperty({ description: 'Credit Risk Rating' })
@IsString()
@IsNotEmpty()
CCM_Credit_Risk_Rating__c: string;

@ApiProperty({ description: 'Credit Risk Rating Date (YYYY-MM-DD)' })
@IsString()
@Length(10)
@IsNotEmpty()
CCM_Credit_Risk_Rating_Date__c: string;

@ApiProperty({ description: 'Loss Given Default' })
@IsNumber()
@IsNotEmpty()
@Min(0)
@Max(100)
CCM_Loss_Given_Default__c: number;

@ApiProperty({ description: 'Probability of Default' })
@IsNumber()
@IsNotEmpty()
@Min(0)
@Max(100)
CCM_Probability_of_Default__c: number;
}
9 changes: 8 additions & 1 deletion src/modules/customers/dto/dtfs-customer.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { IsNotEmpty, IsNumber, IsString, Max, MaxLength, Min, MinLength } from 'class-validator';

export class DTFSCustomerDto {
@ApiProperty({ description: 'Company Registration Number', minLength: 8, maxLength: 10 })
Expand All @@ -13,4 +13,11 @@ export class DTFSCustomerDto {
@IsString()
@IsNotEmpty()
companyName: string;

@ApiProperty({ description: 'Probability of Default' })
@IsNumber()
@IsNotEmpty()
@Min(0)
@Max(100)
probabilityOfDefault: number;
}
89 changes: 77 additions & 12 deletions src/modules/salesforce/salesforce.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { CUSTOMERS } from '@ukef/constants';
import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator';
import { AxiosError, HttpStatusCode } from 'axios';
import { when } from 'jest-when';
Expand Down Expand Up @@ -54,21 +55,61 @@ describe('SalesforceService', () => {
errors: null,
success: true,
};

const query: CreateCustomerDto = {
const baseQuery = {
Name: companyRegNo,
Party_URN__c: '00312345',
D_B_Number__c: '12341234',
Company_Registration_Number__c: companyRegNo,
CCM_Credit_Risk_Rating_Date__c: '2007-03-27',
CCM_Credit_Risk_Rating__c: CUSTOMERS.EXAMPLES.CREDIT_RISK_RATING,
CCM_Loss_Given_Default__c: CUSTOMERS.EXAMPLES.LOSS_GIVEN_DEFAULT,
CCM_Probability_of_Default__c: 3,
};

const expectedHttpServicePostArgs: [string, body: CreateCustomerDto, object] = [
const baseExpectedHttpServicePostArgs: [string, body: CreateCustomerDto, object] = [
customerBasePath,
query,
baseQuery,
{ headers: { Authorization: 'Bearer ' + expectedAccessToken } },
];

it('sends a POST to the Salesforce /sobjects/Account endpoint with the specified request', async () => {
it.each([
baseQuery,
{
Name: companyRegNo,
Party_URN__c: '00312345',
D_B_Number__c: '12341234',
Company_Registration_Number__c: companyRegNo,
CCM_Credit_Risk_Rating_Date__c: '2007-03-27',
CCM_Credit_Risk_Rating__c: CUSTOMERS.EXAMPLES.CREDIT_RISK_RATING,
CCM_Loss_Given_Default__c: CUSTOMERS.EXAMPLES.LOSS_GIVEN_DEFAULT,
CCM_Probability_of_Default__c: 0.0,
},
{
Name: companyRegNo,
Party_URN__c: '00312345',
D_B_Number__c: '12341234',
Company_Registration_Number__c: companyRegNo,
CCM_Credit_Risk_Rating_Date__c: '2007-03-27',
CCM_Credit_Risk_Rating__c: CUSTOMERS.EXAMPLES.CREDIT_RISK_RATING,
CCM_Loss_Given_Default__c: CUSTOMERS.EXAMPLES.LOSS_GIVEN_DEFAULT,
CCM_Probability_of_Default__c: 100.0,
},
{
Name: companyRegNo,
Party_URN__c: '00312345',
D_B_Number__c: '12341234',
Company_Registration_Number__c: companyRegNo,
CCM_Credit_Risk_Rating_Date__c: '2007-03-27',
CCM_Credit_Risk_Rating__c: 'A+++',
CCM_Loss_Given_Default__c: 100,
CCM_Probability_of_Default__c: 14.1,
},
])('sends a POST to the Salesforce /sobjects/Account endpoint with the specified request', async (query) => {
const expectedHttpServicePostArgs: [string, body: CreateCustomerDto, object] = [
customerBasePath,
query,
{ headers: { Authorization: 'Bearer ' + expectedAccessToken } },
];

when(httpServicePost)
.calledWith(...expectedHttpServicePostArgs)
.mockReturnValueOnce(
Expand All @@ -90,23 +131,47 @@ describe('SalesforceService', () => {
it('throws a SalesforceException if the request to Salesforce fails', async () => {
const axiosRequestError = new AxiosError();
when(httpServicePost)
.calledWith(...expectedHttpServicePostArgs)
.calledWith(...baseExpectedHttpServicePostArgs)
.mockReturnValueOnce(throwError(() => axiosRequestError));
const getCustomersPromise = service.createCustomer(query);
const getCustomersPromise = service.createCustomer(baseQuery);

await expect(getCustomersPromise).rejects.toBeInstanceOf(SalesforceException);
await expect(getCustomersPromise).rejects.toThrow('Failed to create customer in Salesforce');
await expect(getCustomersPromise).rejects.toHaveProperty('innerError', axiosRequestError);
});

it('throws a TypeError if the request is malformed', async () => {
const malformedQuery: CreateCustomerDto = {
it.each([
{
Name: companyRegNo,
Party_URN__c: null,
D_B_Number__c: null,
Company_Registration_Number__c: '12341234',
};

CCM_Credit_Risk_Rating_Date__c: '2007-03-27',
CCM_Credit_Risk_Rating__c: null,
CCM_Loss_Given_Default__c: CUSTOMERS.EXAMPLES.LOSS_GIVEN_DEFAULT,
CCM_Probability_of_Default__c: null,
},
{
Name: companyRegNo,
Party_URN__c: null,
D_B_Number__c: null,
Company_Registration_Number__c: '12341234',
CCM_Credit_Risk_Rating_Date__c: '2007-03-27',
CCM_Credit_Risk_Rating__c: CUSTOMERS.EXAMPLES.CREDIT_RISK_RATING,
CCM_Loss_Given_Default__c: CUSTOMERS.EXAMPLES.LOSS_GIVEN_DEFAULT,
CCM_Probability_of_Default__c: 101,
},
{
Name: companyRegNo,
Party_URN__c: null,
D_B_Number__c: null,
Company_Registration_Number__c: '12341234',
CCM_Credit_Risk_Rating_Date__c: '2007-03-27',
CCM_Credit_Risk_Rating__c: CUSTOMERS.EXAMPLES.CREDIT_RISK_RATING,
CCM_Loss_Given_Default__c: CUSTOMERS.EXAMPLES.LOSS_GIVEN_DEFAULT,
CCM_Probability_of_Default__c: -1,
},
])('throws a TypeError if the request is malformed', async (malformedQuery) => {
const typeError = new TypeError("Cannot read properties of undefined (reading 'pipe')");
const getCustomersPromise = service.createCustomer(malformedQuery);

Expand Down
4 changes: 4 additions & 0 deletions test/docs/__snapshots__/get-docs-yaml.api-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -808,9 +808,13 @@ components:
companyName:
type: string
description: Company Name
probabilityOfDefault:
type: number
description: Probability of Default
required:
- companyRegistrationNumber
- companyName
- probabilityOfDefault
CreateUkefIdDto:
type: object
properties:
Expand Down

0 comments on commit 5ff21fa

Please sign in to comment.