Skip to content

Commit

Permalink
#3260 - CAS Integration 3A - Create new Supplier and Site - Name Fix …
Browse files Browse the repository at this point in the history
…And Error Log Improvement (#3843)

- Fixed the order that first and last names were provided to the
formatter.
- Added the HTTP bad request logs for better troubleshooting while the
final error handling ticket is pending.

### Sample logged error (Invalid SIN).

![image](https://github.com/user-attachments/assets/04626581-6a3b-4a9e-a312-2bb194e670e7)
  • Loading branch information
andrewsignori-aot authored Oct 28, 2024
1 parent 8040205 commit 4a2ac40
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { HttpService } from "@nestjs/axios";
import { CASService } from "@sims/integrations/cas";
import { ConfigService } from "@sims/utilities/config";
import { Mocked } from "@suites/doubles.jest";
import { TestBed } from "@suites/unit";

/**
* Default access token used for authentication.
*/
const ACCESS_TOKEN = "access_token";

/**
* Default axios headers for authentication.
*/
export const DEFAULT_CAS_AXIOS_AUTH_HEADER = {
headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
};

/**
* Mock the first post call for authentication.
* @param httpServiceMock mocked for HTTP service.
* @returns mocked axios response.
*/
export function mockAuthenticationResponseOnce(
httpServiceMock: Mocked<HttpService>,
): jest.Mock {
const httpMethodMock = httpServiceMock.axiosRef.post as jest.Mock;
return httpMethodMock.mockResolvedValueOnce({
data: {
access_token: ACCESS_TOKEN,
token_type: "bearer",
expires_in: 3600,
},
});
}

/**
* Initializes the service under test.
* @returns array of mocked services.
*/
export async function initializeService(): Promise<
[
casService: CASService,
httpService: Mocked<HttpService>,
configService: Mocked<ConfigService>,
]
> {
const { unit, unitRef } = await TestBed.solitary(CASService).compile();
const configService = unitRef.get(ConfigService);
const httpService = unitRef.get(HttpService);
configService.casIntegration.baseUrl = "cas-url";
return [unit, httpService, configService];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { CASService, CreateSupplierAndSiteData } from "@sims/integrations/cas";
import { Mocked } from "@suites/unit";
import { HttpService } from "@nestjs/axios";
import {
DEFAULT_CAS_AXIOS_AUTH_HEADER,
initializeService,
mockAuthenticationResponseOnce,
} from "./cas-test.utils";

describe("CASService-createSupplierAndSite", () => {
let casService: CASService;
let httpService: Mocked<HttpService>;

beforeAll(async () => {
[casService, httpService] = await initializeService();
});

beforeEach(() => {
jest.resetAllMocks();
});

it("Should invoke CAS API with formatted payload when all data was provided as expected.", async () => {
// Arrange
mockAuthenticationResponseOnce(httpService).mockResolvedValue({
data: {
SUPPLIER_NUMBER: "123456",
SUPPLIER_SITE_CODE: "001",
},
});

const supplierData: CreateSupplierAndSiteData = {
firstName:
"First With Special Characters áÉíÓú and Very Extensive Number of Characters",
lastName: "Some Last-Name With Length Over the Maximum",
sin: "999999999",
emailAddress: "[email protected]",
supplierSite: {
addressLine1: "Street-Special Characters-ãñè-Maximum",
city: "City Name Over Maximum Length",
provinceCode: "BC",
postalCode: "h1h h2h",
},
};

// Act
await casService.createSupplierAndSite(supplierData);

// Assert
expect(httpService.axiosRef.post).toHaveBeenCalledWith(
"cas-url/cfs/supplier/",
{
SupplierName:
"SOME LAST-NAME WITH LENGTH OVER THE MAXIMUM, FIRST WITH SPECIAL CHARACTERS AEIOU",
SubCategory: "Individual",
Sin: "999999999",
SupplierAddress: [
{
AddressLine1: "STREET-SPECIAL CHARACTERS-ANE-MAXIM",
City: "City Name Over Maximum Le",
Province: "BC",
Country: "CA",
PostalCode: "H1HH2H",
EmailAddress: supplierData.emailAddress,
},
],
},
DEFAULT_CAS_AXIOS_AUTH_HEADER,
);
});
});
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { CASService } from "@sims/integrations/cas/cas.service";
import { TestBed, Mocked } from "@suites/unit";
import { CASService } from "@sims/integrations/cas";
import { Mocked } from "@suites/unit";
import { HttpService } from "@nestjs/axios";
import { ConfigService } from "@sims/utilities/config";

const ACCESS_TOKEN = "access_token";
import {
DEFAULT_CAS_AXIOS_AUTH_HEADER,
initializeService,
mockAuthenticationResponseOnce,
} from "./cas-test.utils";

describe("CASService-getSupplierInfoFromCAS", () => {
let casService: CASService;
let httpService: Mocked<HttpService>;
let configService: Mocked<ConfigService>;

beforeAll(async () => {
const { unit, unitRef } = await TestBed.solitary(CASService).compile();
casService = unit;
configService = unitRef.get(ConfigService);
httpService = unitRef.get(HttpService);
configService.casIntegration.baseUrl = "cas-url";
[casService, httpService] = await initializeService();
});

beforeEach(() => {
Expand All @@ -24,7 +21,7 @@ describe("CASService-getSupplierInfoFromCAS", () => {

it("Should invoke CAS API with last name upper case and without special characters when last name has special characters and is not entirely upper case.", async () => {
// Arrange
mockAuthResponse();
mockAuthenticationResponseOnce(httpService);

// Act
await casService.getSupplierInfoFromCAS(
Expand All @@ -35,20 +32,7 @@ describe("CASService-getSupplierInfoFromCAS", () => {
// Assert
expect(httpService.axiosRef.get).toHaveBeenCalledWith(
"cas-url/cfs/supplier/LAST NAME WITH SPECIAL CHARACTERS: AEIOU-AEIOU/lastname/dummy_sin_value/sin",
{ headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } },
DEFAULT_CAS_AXIOS_AUTH_HEADER,
);
});

/**
* Mock the first post call for authentication.
*/
function mockAuthResponse(): void {
httpService.axiosRef.post = jest.fn().mockResolvedValueOnce({
data: {
access_token: ACCESS_TOKEN,
token_type: "bearer",
expires_in: 3600,
},
});
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ const CAS_CITY_MAX_LENGTH = 25;
* @returns formatted full name.
*/
export function formatUserName(firstName: string, lastName: string): string {
const formattedName = `${lastName}, ${firstName}`.substring(
0,
CAS_SUPPLIER_NAME_MAX_LENGTH,
);
const formattedName = `${lastName}, ${firstName}`
.substring(0, CAS_SUPPLIER_NAME_MAX_LENGTH)
.trim();
return convertToASCIIString(formattedName).toUpperCase();
}

Expand All @@ -28,7 +27,7 @@ export function formatUserName(firstName: string, lastName: string): string {
*/
export function formatAddress(address: string): string {
return convertToASCIIString(
address.substring(0, CAS_ADDRESS_MAX_LENGTH),
address.substring(0, CAS_ADDRESS_MAX_LENGTH).trim(),
).toUpperCase();
}

Expand All @@ -38,7 +37,7 @@ export function formatAddress(address: string): string {
* @returns formatted city.
*/
export function formatCity(city: string): string {
return city.substring(0, CAS_CITY_MAX_LENGTH);
return city.substring(0, CAS_CITY_MAX_LENGTH).trim();
}

/**
Expand Down
25 changes: 21 additions & 4 deletions sources/packages/backend/libs/integrations/src/cas/cas.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Injectable, LoggerService } from "@nestjs/common";
import { HttpStatus, Injectable, LoggerService } from "@nestjs/common";
import {
CASAuthDetails,
CASSupplierResponse,
CreateSupplierAndSiteData,
CreateSupplierAndSiteResponse,
CreateSupplierAndSiteSubmittedData,
} from "./models/cas-service.model";
import { AxiosRequestConfig } from "axios";
import { AxiosError, AxiosRequestConfig } from "axios";
import { HttpService } from "@nestjs/axios";
import { CASIntegrationConfig, ConfigService } from "@sims/utilities/config";
import { stringify } from "querystring";
Expand All @@ -15,7 +15,7 @@ import {
convertToASCIIString,
parseJSONError,
} from "@sims/utilities";
import { CAS_AUTH_ERROR } from "@sims/integrations/constants";
import { CAS_AUTH_ERROR, CAS_BAD_REQUEST } from "@sims/integrations/constants";
import { InjectLogger } from "@sims/utilities/logger";
import {
CASCachedAuthDetails,
Expand All @@ -25,6 +25,11 @@ import {
formatUserName,
} from ".";

/**
* CAS response property that contains further information about errors.
*/
const CAS_RETURNED_MESSAGES = "CAS-Returned-Messages";

@Injectable()
export class CASService {
private cachedCASToken: CASCachedAuthDetails;
Expand Down Expand Up @@ -128,8 +133,8 @@ export class CASService {
const config = await this.getAuthConfig();
const submittedData: CreateSupplierAndSiteSubmittedData = {
SupplierName: formatUserName(
supplierData.lastName,
supplierData.firstName,
supplierData.lastName,
),
SubCategory: "Individual",
Sin: supplierData.sin,
Expand Down Expand Up @@ -159,6 +164,18 @@ export class CASService {
},
};
} catch (error: unknown) {
if (
error instanceof AxiosError &&
error.response?.status === HttpStatus.BAD_REQUEST &&
!!error.response?.data[CAS_RETURNED_MESSAGES]
) {
// Checking for bad request errors for better logging while the
// specific ticket to handle exception is pending.
throw new CustomNamedError(
error.response.data[CAS_RETURNED_MESSAGES],
CAS_BAD_REQUEST,
);
}
throw new Error("Error while creating supplier and site on CAS.", {
cause: error,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export const DOCUMENT_NUMBER_NOT_FOUND = "DOCUMENT_NUMBER_NOT_FOUND";

// Error code used when there is an CAS authentication error.
/**
* Error code used when there is an CAS authentication error.
*/
export const CAS_AUTH_ERROR = "CAS_AUTH_ERROR";
/**
* CAS bad request error.
*/
export const CAS_BAD_REQUEST = "CAS_BAD_REQUEST";

0 comments on commit 4a2ac40

Please sign in to comment.