Skip to content

Commit

Permalink
Add async requests to IDApi for KYB and basic KYC (#367)
Browse files Browse the repository at this point in the history
* Add async requests to IDApi for KYB and basic KYC

* remove unused variable

* Fix job status polling retry logic

* Fix: lint error

* bump revision, add changelog and examples

* use right changelog version

* bump version to 3.1.0
  • Loading branch information
solnsubuga authored Jan 31, 2024
1 parent 55e799d commit c278c45
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 90 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.1.0] - 2024-01-31

### Added

- Added support for asynchronous jobs for ID and Business verifications.

## [3.0.2] - 2023-09-29

### Added
Expand All @@ -16,7 +22,8 @@ and this project adheres to

### Changed

- Updated `WebApi` to remove the `id_type` as a required field for DocV Job Type.
- Updated `WebApi` to remove the `id_type` as a required field for DocV Job
Type.

## [3.0.0]

Expand Down
9 changes: 9 additions & 0 deletions examples/business_verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ const id_info = {
try {
const result = await connection.submit_job(partner_params, id_info);
console.info(result);

// submit Asyncjob is an alternative to submit_job that returns a promise
const async_result = await connection.submitAsyncjob(
partner_params,
id_info,
'<your callback url>',
);

console.info(async_result);
} catch (error) {
console.error(error);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "smile-identity-core",
"version": "3.0.2",
"version": "3.1.0",
"description": "The official Smile Identity Node SDK",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
268 changes: 192 additions & 76 deletions src/id-api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import axios from 'axios';
import axios, { AxiosResponse, AxiosInstance, AxiosError } from 'axios';
import Signature from './signature';
import { mapServerUri, sdkVersionInfo, validatePartnerParams } from './helpers';
import { IdInfo, PartnerParams } from './shared';
import { IdInfo, PartnerParams, SignatureInfo } from './shared';
import { JOB_TYPE } from './constants';

interface VerificationRequest extends IdInfo, SignatureInfo {
partner_id: string;
partner_params: PartnerParams;
callback_url?: string;
}

/**
* Validates the provided id info
* @param {IdInfo} idInfo Contains info needed for the verification job
Expand All @@ -24,111 +30,221 @@ const validateIdInfo = (idInfo: IdInfo): void => {
}
};

const configurePayload = ({
api_key,
id_info,
partner_id,
partner_params,
}: {
api_key: string;
id_info: IdInfo;
partner_id: string;
partner_params: PartnerParams;
}) => ({
language: 'javascript',
partner_id,
partner_params: {
...partner_params,
job_type: partner_params.job_type,
},
...id_info,
...new Signature(partner_id, api_key).generate_signature(),
...sdkVersionInfo,
});

export class IDApi {
partner_id: string;

sid_server: string | number;
private partner_id: string;

api_key: string;
private api_key: string;

url: string;
private axiosInstance: AxiosInstance;

constructor(
partner_id: string,
api_key: string,
sid_server: string | number,
) {
this.partner_id = partner_id;
this.sid_server = sid_server;
this.api_key = api_key;
this.url = mapServerUri(sid_server);
const url = mapServerUri(sid_server);

this.axiosInstance = axios.create({
baseURL: `https://${url}`,
});
}

private static createVerificationRequest(
api_key: string,
id_info: IdInfo,
partner_id: string,
partner_params: PartnerParams,
callbackUrl: string | undefined = undefined,
): VerificationRequest {
const request = {
partner_id,
partner_params: {
...partner_params,
job_type: partner_params.job_type,
},
...id_info,
...new Signature(partner_id, api_key).generate_signature(),
...sdkVersionInfo,
};
if (callbackUrl) {
return {
...request,
callback_url: callbackUrl,
};
}
return request;
}

private static validateParams(
id_info: IdInfo,
partner_params: PartnerParams,
): number {
validatePartnerParams(partner_params);
const jobType = parseInt(partner_params.job_type.toString(), 10);

if (
jobType !== JOB_TYPE.BASIC_KYC &&
jobType !== JOB_TYPE.BUSINESS_VERIFICATION
) {
throw new Error(
`Please ensure that you are setting your job_type to ${JOB_TYPE.BASIC_KYC} or ${JOB_TYPE.BUSINESS_VERIFICATION} to query ID Api`,
);
}

validateIdInfo(id_info);
return jobType;
}

/**
* Submit a job to Smile.
* Submit a synchronous job to Smile.
* @template T
* @param {PartnerParams} partner_params - the user_id, job_id, and job_type of the job to submit.
* Can additionally include optional parameters that Smile will return in the
* job status.
* @param {IdInfo} id_info - ID information required to create a job.
* @returns {Promise<object>} A promise that resolves to the job status.
* @returns {Promise<T>} A promise that resolves to type T.
* @throws {Error} If any of the required parameters are missing or if the request fails.
*/
async submit_job(
async submit_job<T>(
partner_params: PartnerParams,
id_info: IdInfo,
): Promise<any> {
): Promise<T> {
try {
validatePartnerParams(partner_params);

const jobType = parseInt(partner_params.job_type.toString(), 10);
const jobType = IDApi.validateParams(id_info, partner_params);
const request = IDApi.createVerificationRequest(
this.api_key,
id_info,
this.partner_id,
partner_params,
);

if (
jobType !== JOB_TYPE.BASIC_KYC &&
jobType !== JOB_TYPE.BUSINESS_VERIFICATION
) {
throw new Error(
`Please ensure that you are setting your job_type to ${JOB_TYPE.BASIC_KYC} or ${JOB_TYPE.BUSINESS_VERIFICATION} to query ID Api`,
);
}
const endpoint =
jobType === JOB_TYPE.BUSINESS_VERIFICATION
? '/business_verification'
: '/id_verification';

validateIdInfo(id_info);
const response: AxiosResponse<T> = await this.axiosInstance.request<T>({
method: 'post',
url: endpoint,
data: request,
});
return response.data;
} catch (err) {
return Promise.reject<any>(err);
}
}

const data = {
api_key: this.api_key,
/**
* Submit an asynchronous job to Smile.
* @template T
* @param {PartnerParams} partner_params - the user_id, job_id, and job_type of the job to submit.
* Can additionally include optional parameters that Smile will return in the
* job status.
* @param {IdInfo} id_info - ID information required to create a job.
* @param {string} callbackUrl callback url to send job status response to.
* @returns {Promise<T>} A promise that resolves to the job status.
* @throws {Error} If any of the required parameters are missing or if the request fails.
*/
async submitAsyncjob<T>(
partner_params: PartnerParams,
id_info: IdInfo,
callbackUrl: string,
): Promise<T> {
try {
const jobType = IDApi.validateParams(id_info, partner_params);
const request = IDApi.createVerificationRequest(
this.api_key,
id_info,
partner_id: this.partner_id,
this.partner_id,
partner_params,
sid_server: this.sid_server,
};

if (jobType === JOB_TYPE.BUSINESS_VERIFICATION) {
const signature = new Signature(
this.partner_id,
this.api_key,
).generate_signature();
const body = {
api_key: this.api_key,
partner_id: this.partner_id,
partner_params,
...id_info,
...signature,
};
const response = await axios.post(
`https://${this.url}/business_verification`,
body,
);
return response.data;
}

const response = await axios.post(
`https://${this.url}/id_verification`,
configurePayload(data),
callbackUrl,
);

const endpoint =
jobType === JOB_TYPE.BUSINESS_VERIFICATION
? '/async_business_verification'
: '/async_id_verification';

const response: AxiosResponse<T> = await this.axiosInstance.request<T>({
method: 'post',
url: endpoint,
data: request,
});
return response.data;
} catch (err) {
return Promise.reject(err);
return Promise.reject<any>(err);
}
}

/**
* Poll job status from Smile.
* @template T
* @param {PartnerParams} partner_params - the user_id, job_id, and job_type of the job to submit.
* Can additionally include optional parameters that Smile will return in the
* job status.
* @param {number} maxRetries - Number of times to retry the request.
* @param {number} timeout Polling timeout in milliseconds.
* @param {number} return_history whether to return history.
* @param {number} return_images Whether to return images.
* @returns {Promise<any>} A promise that resolves to the job status.
* @throws {Error} If any of the required parameters are missing or if the request fails.
*/
async pollJobStatus(
partner_params: PartnerParams,
maxRetries: number = 4,
timeout: number = 5000,
return_history: boolean = false,
return_images: boolean = false,
): Promise<any> {
try {
validatePartnerParams(partner_params);
const data = {
user_id: partner_params.user_id,
job_id: partner_params.job_id,
partner_id: this.partner_id,
history: return_history,
image_links: return_images,
...new Signature(this.partner_id, this.api_key).generate_signature(),
};

const pollEndpoint = (retries: number): Promise<any> => {
return new Promise((resolve, reject) => {
this.axiosInstance
.post('/job_status', data)
.then((response: AxiosResponse) => {
if (
response.data.job_complete ||
retries <= 0 ||
response.status !== 200
) {
return resolve(response.data);
}
// retry again
return setTimeout(() => {
pollEndpoint(retries - 1)
.then(resolve)
.catch(reject);
}, timeout);
})
.catch((error: AxiosError) => {
if (retries > 0) {
setTimeout(() => {
pollEndpoint(retries - 1)
.then(resolve)
.catch(reject);
}, timeout);
} else {
// Max retries reached, reject with the error
reject(error);
}
});
});
};
return await pollEndpoint(maxRetries);
} catch (error) {
return Promise.reject(error);
}
}
}
5 changes: 5 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ export type TokenRequestParams = {
product: string;
callback_url: string;
};

export interface SignatureInfo {
signature: string;
timestamp: number | string;
}
Loading

0 comments on commit c278c45

Please sign in to comment.