diff --git a/packages/core/src/__tests__/kyc-api.test.ts b/packages/core/src/__tests__/kyc-api.test.ts new file mode 100644 index 00000000..07e46176 --- /dev/null +++ b/packages/core/src/__tests__/kyc-api.test.ts @@ -0,0 +1,186 @@ +import { KycApi } from '../client/KycApi'; +import { DfxHttpClient } from '../client/DfxHttpClient'; +import { + AccountType, + KycFinancialQuestions, + KycFinancialResponses, + KycPersonalData, + KycStepName, + KycStepStatus, + KycStepSubmit, + QuestionType, +} from '../definitions/kyc'; + +function createMockHttpClient(response?: unknown) { + const requestAbsoluteMock = jest.fn().mockResolvedValue(response); + + return { + request: jest.fn(), + requestAbsolute: requestAbsoluteMock, + getBaseUrl: jest.fn().mockReturnValue('https://api.dfx.swiss'), + getApiUrl: jest.fn().mockReturnValue('https://api.dfx.swiss/v1'), + setToken: jest.fn(), + getToken: jest.fn(), + } as unknown as DfxHttpClient & { requestAbsolute: jest.Mock }; +} + +const personalData: KycPersonalData = { + accountType: AccountType.PERSONAL, + firstName: 'Ada', + lastName: 'Lovelace', + phone: '+491701234567', + address: { + street: 'Main', + city: 'Berlin', + zip: '10115', + country: { + id: 1, + symbol: 'DE', + name: 'Germany', + locationAllowed: true, + kycAllowed: true, + nationalityAllowed: true, + bankAllowed: true, + cardAllowed: true, + cryptoAllowed: true, + kycOrganizationAllowed: true, + }, + }, +}; + +const financialData: KycFinancialResponses = { + responses: [{ key: 'income', value: '50000' }], +}; + +const submitUrl = 'https://api.dfx.swiss/v2/kyc/data/personal/42'; +const kycCode = 'kyc-code-1'; + +describe('KycApi', () => { + describe('setPersonalData', () => { + it('returns complete=false and missingFields from the HTTP response', async () => { + const response: KycStepSubmit = { + name: KycStepName.PERSONAL_DATA, + status: KycStepStatus.IN_PROGRESS, + sequenceNumber: 1, + complete: false, + missingFields: ['address.city', 'phone'], + }; + const mockHttp = createMockHttpClient(response); + const api = new KycApi(mockHttp); + + const result = await api.setPersonalData(kycCode, submitUrl, personalData); + + expect(result).toEqual(response); + expect(result.complete).toBe(false); + expect(mockHttp.requestAbsolute).toHaveBeenCalledTimes(1); + expect(mockHttp.requestAbsolute).toHaveBeenCalledWith({ + url: submitUrl, + method: 'PUT', + data: personalData, + token: false, + headers: { 'x-kyc-code': kycCode }, + }); + }); + + it('returns complete=true with empty missingFields from the HTTP response', async () => { + const response: KycStepSubmit = { + name: KycStepName.PERSONAL_DATA, + status: KycStepStatus.IN_REVIEW, + sequenceNumber: 1, + complete: true, + missingFields: [], + }; + const mockHttp = createMockHttpClient(response); + const api = new KycApi(mockHttp); + + const result = await api.setPersonalData(kycCode, submitUrl, personalData); + + expect(result).toEqual(response); + expect(result.complete).toBe(true); + }); + + it('returns undefined fields when the API does not report completeness', async () => { + const response: KycStepSubmit = { + name: KycStepName.PERSONAL_DATA, + status: KycStepStatus.IN_PROGRESS, + sequenceNumber: 1, + }; + const mockHttp = createMockHttpClient(response); + const api = new KycApi(mockHttp); + + const result = await api.setPersonalData(kycCode, submitUrl, personalData); + + expect(result).toEqual(response); + expect(result.complete).toBeUndefined(); + expect(result.missingFields).toBeUndefined(); + }); + }); + + describe('setFinancialData', () => { + const financialUrl = 'https://api.dfx.swiss/v2/kyc/data/financial/7'; + + it('returns complete and missingFields from the HTTP response', async () => { + const response: KycStepSubmit = { + name: KycStepName.FINANCIAL_DATA, + status: KycStepStatus.IN_PROGRESS, + sequenceNumber: 2, + complete: false, + missingFields: ['income', 'assets'], + }; + const mockHttp = createMockHttpClient(response); + const api = new KycApi(mockHttp); + + const result = await api.setFinancialData(kycCode, financialUrl, financialData); + + expect(result).toEqual(response); + expect(result.missingFields).toEqual(['income', 'assets']); + expect(mockHttp.requestAbsolute).toHaveBeenCalledTimes(1); + expect(mockHttp.requestAbsolute).toHaveBeenCalledWith({ + url: financialUrl, + method: 'PUT', + data: financialData, + token: false, + headers: { 'x-kyc-code': kycCode }, + }); + }); + }); + + describe('getFinancialData', () => { + const financialUrl = 'https://api.dfx.swiss/v2/kyc/data/financial/7'; + + it('returns question conditions from the HTTP response', async () => { + const response: KycFinancialQuestions = { + responses: [], + questions: [ + { + key: 'occupation_description', + type: QuestionType.TEXT, + title: 'Describe your occupation', + description: 'Only if employed', + conditions: [{ question: 'occupation', response: 'employed' }], + }, + { + key: 'income', + type: QuestionType.SINGLE_CHOICE, + title: 'Income', + description: 'Annual income', + }, + ], + }; + const mockHttp = createMockHttpClient(response); + const api = new KycApi(mockHttp); + + const result = await api.getFinancialData(kycCode, financialUrl, 'en'); + + expect(result).toEqual(response); + expect(result.questions[0].conditions).toEqual([{ question: 'occupation', response: 'employed' }]); + expect(mockHttp.requestAbsolute).toHaveBeenCalledTimes(1); + expect(mockHttp.requestAbsolute).toHaveBeenCalledWith({ + url: `${financialUrl}?lang=en`, + method: 'GET', + token: false, + headers: { 'x-kyc-code': kycCode }, + }); + }); + }); +}); diff --git a/packages/core/src/client/KycApi.ts b/packages/core/src/client/KycApi.ts index f20f18e5..ea0979a9 100644 --- a/packages/core/src/client/KycApi.ts +++ b/packages/core/src/client/KycApi.ts @@ -3,6 +3,7 @@ import { KycInfo, KycStepSession, KycStepBase, + KycStepSubmit, KycStepName, KycStepType, KycContactData, @@ -75,8 +76,8 @@ export class KycApi { return this.kycRequest(code, { url, method: 'PUT', data }); } - async setPersonalData(code: string, url: string, data: KycPersonalData): Promise { - return this.kycRequest(code, { url, method: 'PUT', data }); + async setPersonalData(code: string, url: string, data: KycPersonalData): Promise { + return this.kycRequest(code, { url, method: 'PUT', data }); } async setManualIdentData(code: string, url: string, data: KycManualIdentData): Promise { @@ -120,8 +121,8 @@ export class KycApi { return this.kycRequest(code, { url: `${url}${query}`, method: 'GET' }); } - async setFinancialData(code: string, url: string, data: KycFinancialResponses): Promise { - return this.kycRequest(code, { url, method: 'PUT', data }); + async setFinancialData(code: string, url: string, data: KycFinancialResponses): Promise { + return this.kycRequest(code, { url, method: 'PUT', data }); } async setPaymentData(code: string, url: string, data: PaymentData): Promise { diff --git a/packages/core/src/definitions/index.ts b/packages/core/src/definitions/index.ts index 48e7e7f6..882a84ec 100644 --- a/packages/core/src/definitions/index.ts +++ b/packages/core/src/definitions/index.ts @@ -73,6 +73,7 @@ export type { KycStepBase, KycStep, KycStepSession, + KycStepSubmit, KycContactData, KycAddress, KycPersonalData, @@ -94,6 +95,7 @@ export type { KycFinancialResponse, KycFinancialResponses, KycFinancialOption, + KycFinancialCondition, KycFinancialQuestion, KycFinancialQuestions, TfaSetup, diff --git a/packages/core/src/definitions/kyc.ts b/packages/core/src/definitions/kyc.ts index 6fba2d66..c5ac3ef4 100644 --- a/packages/core/src/definitions/kyc.ts +++ b/packages/core/src/definitions/kyc.ts @@ -212,6 +212,24 @@ export interface KycStepSession extends KycStepBase { session?: KycSessionInfo; } +/** Response of KYC data submit endpoints (personal / financial). */ +export interface KycStepSubmit extends KycStepBase { + /** + * Whether the submission fulfilled all required fields and the step advanced. + * When false, a draft was saved but the step did not progress. + * Absent on API versions that do not report submission completeness. + */ + complete?: boolean; + /** + * Missing required field paths (personal: e.g. `firstName`, `address.city`; + * financial: unanswered applicable question keys). Empty when `complete` is + * true. May also be empty while `complete` is false if the blocking field + * belongs to another step. Absent on API versions that do not report + * submission completeness. + */ + missingFields?: string[]; +} + // personal data export interface KycContactData { mail: string; @@ -441,16 +459,19 @@ export interface KycFinancialOption { text: string; } +/** Condition under which a financial question is applicable (previous answer match). */ +export interface KycFinancialCondition { + question: string; + response: string; +} + export interface KycFinancialQuestion { key: string; type: QuestionType; title: string; description: string; options?: KycFinancialOption[]; - conditions?: { - question: string; - response: string; - }[]; + conditions?: KycFinancialCondition[]; } export interface KycFinancialQuestions extends KycFinancialResponses { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5bcfe80d..aef4d893 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -165,6 +165,7 @@ export type { KycStepBase, KycStep, KycStepSession, + KycStepSubmit, KycContactData, KycAddress, KycPersonalData, @@ -186,6 +187,7 @@ export type { KycFinancialResponse, KycFinancialResponses, KycFinancialOption, + KycFinancialCondition, KycFinancialQuestion, KycFinancialQuestions, TfaSetup, diff --git a/packages/react/src/definitions/kyc.ts b/packages/react/src/definitions/kyc.ts index 0e7e23d5..a274bca1 100644 --- a/packages/react/src/definitions/kyc.ts +++ b/packages/react/src/definitions/kyc.ts @@ -38,6 +38,7 @@ export type { KycStepBase, KycStep, KycStepSession, + KycStepSubmit, KycContactData, KycAddress, KycPersonalData, @@ -59,6 +60,7 @@ export type { KycFinancialResponse, KycFinancialResponses, KycFinancialOption, + KycFinancialCondition, KycFinancialQuestion, KycFinancialQuestions, TfaSetup, diff --git a/packages/react/src/hooks/kyc.hook.ts b/packages/react/src/hooks/kyc.hook.ts index 0cf829eb..7173985f 100644 --- a/packages/react/src/hooks/kyc.hook.ts +++ b/packages/react/src/hooks/kyc.hook.ts @@ -21,6 +21,7 @@ import { TfaLevel, KycFile, KycStepBase, + KycStepSubmit, KycBeneficialData, KycOperationalData, PaymentData, @@ -52,7 +53,7 @@ export interface KycInterface { // updates setContactData: (code: string, url: string, data: KycContactData) => Promise; - setPersonalData: (code: string, url: string, data: KycPersonalData) => Promise; + setPersonalData: (code: string, url: string, data: KycPersonalData) => Promise; setManualIdentData: (code: string, url: string, data: KycManualIdentData) => Promise; setLegalEntityData: (code: string, url: string, data: KycLegalEntityData) => Promise; setSoleProprietorshipData: (code: string, url: string, data: KycFileData) => Promise; @@ -63,7 +64,7 @@ export interface KycInterface { setBeneficialData: (code: string, url: string, data: KycBeneficialData) => Promise; setOperationalData: (code: string, url: string, data: KycOperationalData) => Promise; getFinancialData: (code: string, url: string, lang?: string) => Promise; - setFinancialData: (code: string, url: string, data: KycFinancialResponses) => Promise; + setFinancialData: (code: string, url: string, data: KycFinancialResponses) => Promise; setPaymentData: (code: string, url: string, data: PaymentData) => Promise; setRecallData: (code: string, url: string, data: RecallData) => Promise; setAddressChangeData: (code: string, url: string, data: KycChangeAddressData) => Promise; @@ -175,7 +176,7 @@ export function useKyc(): KycInterface { ); const setPersonalData = useCallback( - async (code: string, url: string, data: KycPersonalData): Promise => { + async (code: string, url: string, data: KycPersonalData): Promise => { return call({ url, code, method: 'PUT', data }); }, [call], @@ -299,7 +300,7 @@ export function useKyc(): KycInterface { ); const setFinancialData = useCallback( - async (code: string, url: string, data: KycFinancialResponses): Promise => { + async (code: string, url: string, data: KycFinancialResponses): Promise => { return call({ url, code, method: 'PUT', data }); }, [call], diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index e749ddb3..f4f605b1 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -115,6 +115,7 @@ export { KycStepReason, KycStep, KycStepSession, + KycStepSubmit, KycContactData, KycAddress, KycPersonalData, @@ -122,6 +123,7 @@ export { KycFinancialResponse, KycFinancialResponses, KycFinancialOption, + KycFinancialCondition, KycFinancialQuestion, KycFinancialQuestions, KycManualIdentData,