Skip to content

Commit

Permalink
Implement standalone KYC Case summarization (#103)
Browse files Browse the repository at this point in the history
Signed-off-by: Sean Sundberg <[email protected]>
  • Loading branch information
seansund authored Sep 28, 2023
1 parent 29199e2 commit 025da82
Show file tree
Hide file tree
Showing 22 changed files with 324 additions and 12 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
RTCQC,
Utilities
} from "./views";
import {KycSummarizeNative} from "./views/KycSummarizeNative";

function App() {
const setCurrentUser = useSetAtom(currentUserAtom);
Expand All @@ -42,6 +43,7 @@ function App() {
{title: 'Customer Risk', href: '/customer-risk', element: <CustomerRisk />},
{title: 'RTC - QC', href: '/rtc-qc', element: <RTCQC />},
{title: 'KYC Summarization', href: '/kyc-summarization', element: <KycSummarize />},
{title: 'KYC Summarize', href: '/kyc-summarize', excludeFromMenu: true, element: <KycSummarizeNative returnUrl="/kyc-summarization" />},
{
title: 'Utilities',
href: '/utilities',
Expand Down
4 changes: 4 additions & 0 deletions src/atoms/document-status.atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {atom} from "jotai";
import {DocumentStatusModel} from "../models";

export const documentStatusAtom = atom<DocumentStatusModel[]>([]);
1 change: 1 addition & 0 deletions src/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './data-extraction.atom';
export * from './dashboard.atom';
export * from './current-user.atom';
export * from './navigation.atom';
export * from './kyc-case-summary.atom';
16 changes: 16 additions & 0 deletions src/atoms/kyc-case-summary.atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {atom} from "jotai";
import {loadable} from "jotai/utils";

import {kycCaseSummaryApi} from "../services/kyc-case-summary";
import {KycCaseSummaryModel} from "../models";

const baseAtom = atom<Promise<KycCaseSummaryModel | undefined>>(Promise.resolve(undefined));

export const kycCaseSummaryAtom = atom(
get => get(baseAtom),
async (_get, set, name: string) => {
set(baseAtom, kycCaseSummaryApi().summarize(name).then(summary => ({summary})))
}
)

export const kycCaseSummaryAtomLoadable = loadable(kycCaseSummaryAtom)
65 changes: 65 additions & 0 deletions src/components/DocumentStatus/DocumentStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import React, {useEffect} from 'react';
import {useAtom} from "jotai";
import {timer} from "rxjs";

import {documentStatusAtom} from "../../atoms/document-status.atom";
import {DocumentModel, DocumentStatusModel} from "../../models";
import {fileUploadApi, FileUploadContext, ListFileRequest} from "../../services";
import {first} from "../../utils";

export interface DocumentStatusProps {
context: FileUploadContext;
statuses?: string[];
documents: DocumentModel[];
}

const buildRequest = ({context, statuses}: {context: FileUploadContext, statuses?: string[]}): ListFileRequest => {
const request: ListFileRequest = {context}

if (statuses) {
request.statuses = statuses;
}

return request;
}

const getFilename = (id: string, docs: DocumentModel[]): string => {
return first(docs.filter(doc => doc.id === id).map(doc => doc.name)).orElse(undefined)
}

export const DocumentStatus: React.FunctionComponent<DocumentStatusProps> = (props: DocumentStatusProps) => {
const [documentStatus, setDocumentStatus] = useAtom(documentStatusAtom)
useEffect(() => {
const handle = timer(500, 30000).subscribe({
next: async () => {
if (props.documents.length === 0 && documentStatus.length === 0) {
return
}

const result: DocumentStatusModel[] = await fileUploadApi().listFiles(buildRequest(props))

setDocumentStatus(result
.map(doc => Object.assign(doc, {name: getFilename(doc.id, props.documents)}))
.filter(doc => !!doc.name)
)
}
})

return () => handle.unsubscribe();
})

if (documentStatus.length === 0) {
return (<></>)
}

return (
<div style={{justifyContent: 'flex-start'}}>
<div className="cds--file--label" style={{textAlign: 'left'}}>Document status</div>
<ul>
{documentStatus.map(val => (<li key={val.id} style={{textAlign: 'left'}}>{val.name || val.id} - {val.status}</li>))}
</ul>
</div>
)
}
1 change: 1 addition & 0 deletions src/components/DocumentStatus/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DocumentStatus'
2 changes: 1 addition & 1 deletion src/components/Stack/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {Stack as CarbonStack} from "@carbon/react/lib/components/Stack"

export interface StackProps {
gap: number;
children: unknown[];
children: unknown | unknown[];
}

export const Stack: React.FunctionComponent<StackProps> = (props: StackProps) => {
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './KycSummary'
export * from './KycCaseOverview'
export * from './KycCaseResult'
export * from './ListItems'
export * from './DocumentStatus'
5 changes: 5 additions & 0 deletions src/models/kyc-case.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface DocumentModel extends DocumentInputModel {
content: Buffer;
}

export interface DocumentStatusModel extends Partial<DocumentInputModel> {
id: string;
status?: string;
}


export interface NegativeScreeningModel {
subject: string;
Expand Down
14 changes: 12 additions & 2 deletions src/services/file-upload/file-upload.api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import {DocumentModel} from "../../models";
import {DocumentModel, DocumentStatusModel} from "../../models";

export type FileUploadContext = 'data-extraction' | 'kyc-case';

export const isFileUploadContext = (value: unknown): value is FileUploadContext => {
return value === 'data-extraction' || value === 'kyc-case';
}

export interface ListFileRequest {
context?: FileUploadContext,
statuses?: string[],
}

export abstract class FileUploadApi {
abstract uploadFile(caseId: string, name: string, file: File, context?: FileUploadContext): Promise<DocumentModel>;
abstract uploadFile(caseId: string, name: string, file: File, context?: FileUploadContext, standalone?: boolean): Promise<DocumentModel>;
abstract listFiles(request?: ListFileRequest): Promise<DocumentStatusModel[]>;
}
6 changes: 5 additions & 1 deletion src/services/file-upload/file-upload.mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {FileUploadApi} from "./file-upload.api";
import {kycCaseManagementApi, KycCaseManagementApi} from "../kyc-case-management";
import {DocumentModel} from "../../models";
import {DocumentModel, DocumentStatusModel} from "../../models";

export class FileUploadMock implements FileUploadApi {
service: KycCaseManagementApi;
Expand All @@ -15,4 +15,8 @@ export class FileUploadMock implements FileUploadApi {
return this.service.addDocumentToCase(caseId, name, {content})
}

async listFiles(): Promise<DocumentStatusModel[]> {
return []
}

}
25 changes: 22 additions & 3 deletions src/services/file-upload/file-upload.rest.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import axios from "axios";

import {FileUploadApi, FileUploadContext} from "./file-upload.api";
import {DocumentModel} from "../../models";
import {FileUploadApi, FileUploadContext, ListFileRequest} from "./file-upload.api";
import {DocumentModel, DocumentStatusModel} from "../../models";

interface ListFilesParams {
context?: FileUploadContext;
status?: string;
}

export class FileUploadRest implements FileUploadApi {
uploadFile(caseId: string, name: string, file: File, context: FileUploadContext = 'kyc-case'): Promise<DocumentModel> {
uploadFile(caseId: string, name: string, file: File, context: FileUploadContext = 'kyc-case', standalone: boolean = false): Promise<DocumentModel> {
const url = '/api/document/upload'

const form = new FormData();
form.append('name', name);
form.append('parentId', caseId);
form.append('context', context);
form.append('file', file);
form.append('standalone', '' + standalone);

return axios
.post<DocumentModel>(url, form)
Expand All @@ -20,4 +26,17 @@ export class FileUploadRest implements FileUploadApi {
});
}

async listFiles({context, statuses}: ListFileRequest = {context: 'kyc-case'}): Promise<DocumentStatusModel[]> {
const url: string = '/api/document'

const params: ListFilesParams = {context}
if (statuses) {
params.status = statuses.join(',')
}

return axios
.get<DocumentStatusModel[]>(url, {params})
.then(response => response.data)
}

}
13 changes: 13 additions & 0 deletions src/services/kyc-case-summary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {KycCaseSummaryApi} from "./kyc-case-summary.api";
import {KycCaseSummaryGraphql} from "./kyc-case-summary.graphql";

export * from './kyc-case-summary.api';

let _instance: KycCaseSummaryApi;
export const kycCaseSummaryApi = (): KycCaseSummaryApi => {
if (_instance) {
return _instance;
}

return _instance = new KycCaseSummaryGraphql();
}
4 changes: 4 additions & 0 deletions src/services/kyc-case-summary/kyc-case-summary.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export abstract class KycCaseSummaryApi {
abstract summarize(name: string): Promise<string>;
}
33 changes: 33 additions & 0 deletions src/services/kyc-case-summary/kyc-case-summary.graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {ApolloClient, gql} from "@apollo/client";

import {KycCaseSummaryApi} from "./kyc-case-summary.api.ts";
import {getApolloClient} from "../../backends";

const SUMMARIZE = gql`
query Summarize($name:String!) {
summarize(name:$name) {
result
}
}
`

export class KycCaseSummaryGraphql implements KycCaseSummaryApi {
client: ApolloClient<unknown>;

constructor() {
this.client = getApolloClient();
}

summarize(name: string): Promise<string> {
return this.client
.query<{summarize: {result: string}}>({
query: SUMMARIZE,
variables: {name},
})
.then(result => result.data.summarize.result)
.catch(() => {
return '[Error]'
}) as Promise<string>
}

}
7 changes: 7 additions & 0 deletions src/services/kyc-case-summary/kyc-case-summary.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {KycCaseSummaryApi} from "./kyc-case-summary.api.ts";

export class KycCaseSummaryMock implements KycCaseSummaryApi {
async summarize(name: string): Promise<string> {
return name;
}
}
23 changes: 23 additions & 0 deletions src/services/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ input DocumentInput {
path: String!
}

type DocumentOutput {
id: ID!
name: String
path: String
status: String
}

"""Object representing a key/value pair"""
type FormOption {
text: String!
Expand All @@ -92,16 +99,28 @@ type KycCase {
status: String!
}

type KycCaseChangeEvent {
caseId: ID!
event: String!
}

"""KYC case summary"""
type KycCaseSummary {
error: String
summary: String!
}

input ListDocumentInput {
context: String
statuses: [String!]
}

type Mutation {
addDocumentToCase(caseId: ID!, documentName: String!, documentUrl: String!): KycCase!
approveCase(case: ApproveCaseInput!): KycCase!
createCase(customer: CustomerInput!): KycCase!
deleteCase(id: ID!): KycCase!
deleteDocument(id: ID!): Document!
processCase(id: ID!): KycCase!
removeDocumentFromCase(caseId: ID!, documentId: ID!): KycCase!
reviewCase(case: ReviewCaseInput!): KycCase!
Expand Down Expand Up @@ -151,7 +170,9 @@ type Query {
helloWorld: Greeting!
listCases: [KycCase!]!
listCountries: [FormOption!]!
listDocuments: [Document!]!
listEntityTypes: [FormOption!]!
listFiles(input: ListDocumentInput): [DocumentOutput!]!
listIndustryTypes: [FormOption!]!
listQuestions: [DataExtractionQuestion!]!
screenNews(country: String, dateOfBirth: String, name: String!): NegativeScreening!
Expand All @@ -168,7 +189,9 @@ input ReviewCaseInput {

type Subscription {
extractDataObservable(customer: String!, questions: [DataExtractionQuestionIdInput!]!): [DataExtractionResult!]!
subscribeToCaseChanges: KycCaseChangeEvent!
subscribeToCases: [KycCase!]!
watchCase(id: ID!): KycCase!
}

type SummaryResult {
Expand Down
2 changes: 1 addition & 1 deletion src/views/KYC/KYCCaseDetail/KYCCaseDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const KYCCaseDetail: React.FunctionComponent<KYCCaseDetailProps> = (props
}

if (selectedCase.status === 'Closed') {
return (<KYCCaseCompleted currentCase={selectedCase} returnUrl={props.basePath} />)
return (<KYCCasePending title="Completed" currentCase={selectedCase} returnUrl={props.basePath} />)
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {kycCaseManagementApi} from "../../../../services";
export interface KYCCasePendingProps {
currentCase: KycCaseModel;
returnUrl: string;
title?: string;
}

export const KYCCasePending: React.FunctionComponent<KYCCasePendingProps> = (props: KYCCasePendingProps) => {
Expand All @@ -33,7 +34,7 @@ export const KYCCasePending: React.FunctionComponent<KYCCasePendingProps> = (pro

return (
<Stack gap={5}>
<h2>Pending information</h2>
<h2>{props.title ? props.title : 'Pending information'}</h2>
<KycCaseOverview currentCase={props.currentCase} />
<DocumentList documents={props.currentCase.documents} />
<KycCaseResult currentCase={props.currentCase} />
Expand Down
9 changes: 6 additions & 3 deletions src/views/KYC/KYCCaseDetail/util/event-handler.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {DocumentModel} from "../../../../models";
import {fileUploadApi, FileUploadApi, FileUploadContext} from "../../../../services";
import {fileListUtil} from "../../../../utils";

export const handleFileUploaderChange = (id: string, handleNewDocuments: (newDocuments: DocumentModel[]) => void, setFileStatus: (status: unknown) => void, context: FileUploadContext = 'kyc-case') => {
export const handleFileUploaderChange = (id: string, handleNewDocuments: (newDocuments: DocumentModel[]) => void, setFileStatus: (status: unknown) => void, context: FileUploadContext = 'kyc-case', standalone?: boolean) => {
const fileUploadService: FileUploadApi = fileUploadApi();

return (event: {target: {files: FileList, filenameStatus: string}}) => {
Expand All @@ -14,13 +14,16 @@ export const handleFileUploaderChange = (id: string, handleNewDocuments: (newDoc
setFileStatus('uploading')

// TODO handle document remove
Promise.all(files.map(f => fileUploadService.uploadFile(id, f.name, f, context)))
Promise.all(files.map(f => fileUploadService.uploadFile(id, f.name, f, context, standalone)))
.then((result: DocumentModel[]) => {
setFileStatus('complete');

return result.filter(doc => !!doc);
})
.then(handleNewDocuments)
.catch(() => setFileStatus('error'))
.catch(err => {
console.log('Error uploading file: ', {err})
setFileStatus('error')
})
}
}
Loading

0 comments on commit 025da82

Please sign in to comment.