Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { KycStep } from '../kyc-step.entity';
import { KycStepName } from '../../enums/kyc-step-name.enum';
import { ReviewStatus } from '../../enums/review-status.enum';

describe('KycStep.update', () => {
const buildStep = (overrides: Partial<KycStep> = {}): KycStep =>
Object.assign(new KycStep(), {
id: 1,
name: KycStepName.FINANCIAL_DATA,
status: ReviewStatus.IN_PROGRESS,
sequenceNumber: 5,
...overrides,
});

it('leaves status and sequenceNumber unchanged when they are omitted, and omits both from the returned partial', () => {
const step = buildStep();

const [, update] = step.update(undefined, [{ key: 'income', value: 'salary' }]);

expect(step.status).toBe(ReviewStatus.IN_PROGRESS);
expect(step.sequenceNumber).toBe(5);
expect(update).not.toHaveProperty('status');
expect(update).not.toHaveProperty('sequenceNumber');
});

it('applies an explicit status change and includes it in the returned partial', () => {
const step = buildStep();

const [, update] = step.update(ReviewStatus.INTERNAL_REVIEW, [{ key: 'income', value: 'salary' }]);

expect(step.status).toBe(ReviewStatus.INTERNAL_REVIEW);
expect(update.status).toBe(ReviewStatus.INTERNAL_REVIEW);
});
});
19 changes: 11 additions & 8 deletions src/subdomains/generic/kyc/entities/kyc-step.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,20 @@ export class KycStep extends IEntity {
}

update(
status: ReviewStatus,
status?: ReviewStatus,
result?: KycStepResult,
comment?: string,
sequenceNumber?: number,
): UpdateResult<KycStep> {
const update: Partial<KycStep> = {
status,
result: this.setResult(result),
comment: this.addComment(comment),
sequenceNumber,
};
// Callers pass undefined to leave a field unchanged; Object.assign would overwrite those properties.
const update = Object.fromEntries(
Object.entries({
status,
result: this.setResult(result),
comment: this.addComment(comment),
sequenceNumber,
}).filter(([, v]) => v !== undefined),
) as Partial<KycStep>;

Object.assign(this, update);

Expand Down Expand Up @@ -365,7 +368,7 @@ export class KycStep extends IEntity {
return this.result;
}

addComment(comment: string): string | undefined {
addComment(comment: string): string {
return [this.comment, comment].filter((c) => c).join(';');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { CountryService } from 'src/shared/models/country/country.service';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import * as processServiceModule from 'src/shared/services/process.service';
import { createCustomUserData } from '../../../user/models/user-data/__mocks__/user-data.entity.mock';
import { AccountType } from '../../../user/models/user-data/account-type.enum';
import { UserData } from '../../../user/models/user-data/user-data.entity';
import { RiskStatus, UserDataStatus } from '../../../user/models/user-data/user-data.enum';
import { UserDataService } from '../../../user/models/user-data/user-data.service';
import { UserStatus } from '../../../user/models/user/user.enum';
import { IdentDocument } from '../../dto/ident.dto';
import { KycError } from '../../dto/kyc-error.enum';
import { FileSubType, FileType, KycFileBlob } from '../../dto/kyc-file.dto';
import { KycStepStatus } from '../../dto/output/kyc-info.dto';
import { SumSubLevelName } from '../../dto/sum-sub.dto';
import { KycFile } from '../../entities/kyc-file.entity';
import { KycStep } from '../../entities/kyc-step.entity';
Expand Down Expand Up @@ -795,3 +797,79 @@ describe('KycService checkDfxApproval step promotion', () => {
expect(kycStepRepo.update).not.toHaveBeenCalled();
});
});

// updateFinancialData / updateFileData call KycStep.update with omitted status or sequenceNumber;
// the in-memory step (and thus the mapped response) must keep those fields.
describe('KycService updateFinancialData incomplete draft', () => {
let service: KycService;
let kycStepRepo: jest.Mocked<KycStepRepository>;

beforeEach(() => {
kycStepRepo = createMock<KycStepRepository>();
service = Object.create(KycService.prototype);
(service as any).kycStepRepo = kycStepRepo;
});

it('returns status InProgress and the existing sequenceNumber, and keeps the step status', async () => {
const kycStep = Object.assign(new KycStep(), {
id: 11,
name: KycStepName.FINANCIAL_DATA,
status: ReviewStatus.IN_PROGRESS,
sequenceNumber: 3,
});
const user = createMock<UserData>({ accountType: AccountType.PERSONAL });
user.getPendingStepOrThrow.mockReturnValue(kycStep);
jest.spyOn(service as any, 'getUser').mockResolvedValue(user);
jest.spyOn(service as any, 'verify2fa').mockResolvedValue(undefined);
jest.spyOn(service as any, 'updateProgress').mockResolvedValue(user);

const response = await service.updateFinancialData('hash', '1.2.3.4', 11, { responses: [] });

expect(response.status).toBe(KycStepStatus.IN_PROGRESS);
expect(response.sequenceNumber).toBe(3);
expect(kycStep.status).toBe(ReviewStatus.IN_PROGRESS);
expect(kycStep.sequenceNumber).toBe(3);
});
});

describe('KycService updateFileData sequenceNumber', () => {
let service: KycService;
let kycStepRepo: jest.Mocked<KycStepRepository>;
let documentService: jest.Mocked<KycDocumentService>;

beforeEach(() => {
kycStepRepo = createMock<KycStepRepository>();
documentService = createMock<KycDocumentService>();
service = Object.create(KycService.prototype);
(service as any).kycStepRepo = kycStepRepo;
(service as any).documentService = documentService;
});

it('returns the step sequenceNumber unchanged after a file upload', async () => {
const kycStep = Object.assign(new KycStep(), {
id: 22,
name: KycStepName.ADDITIONAL_DOCUMENTS,
status: ReviewStatus.IN_PROGRESS,
sequenceNumber: 7,
userData: { kycLevel: 50 },
});
const user = createMock<UserData>();
user.getPendingStepOrThrow.mockReturnValue(kycStep);
jest.spyOn(service as any, 'getUser').mockResolvedValue(user);
jest.spyOn(service as any, 'createStepLog').mockResolvedValue(undefined);
jest.spyOn(service as any, 'updateProgress').mockResolvedValue(user);
documentService.uploadUserFile.mockResolvedValue({ url: 'https://example.com/file.pdf' } as never);

const filePayload = ['data:application/pdf;base64', Buffer.from('x').toString('base64')].join(',');
const response = await service.updateFileData(
'hash',
22,
KycStepName.ADDITIONAL_DOCUMENTS,
{ fileName: 'doc.pdf', file: filePayload },
FileType.ADDITIONAL_DOCUMENTS,
);

expect(response.sequenceNumber).toBe(7);
expect(kycStep.sequenceNumber).toBe(7);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ import {
import { VirtualIban, VirtualIbanStatus } from 'src/subdomains/supporting/bank/virtual-iban/virtual-iban.entity';
import { VirtualIbanIssuanceIntentStatus } from 'src/subdomains/supporting/bank/virtual-iban/virtual-iban-issuance-intent-status.enum';
import { VirtualIbanIssuanceIntent } from 'src/subdomains/supporting/bank/virtual-iban/virtual-iban-issuance-intent.entity';
import { CheckStatus } from 'src/subdomains/core/aml/enums/check-status.enum';
import { KycStep } from 'src/subdomains/generic/kyc/entities/kyc-step.entity';
import { KycStepName } from 'src/subdomains/generic/kyc/enums/kyc-step-name.enum';
import { KycStepType } from 'src/subdomains/generic/kyc/enums/kyc.enum';
import { ReviewStatus } from 'src/subdomains/generic/kyc/enums/review-status.enum';
import { UserData } from '../user-data.entity';
import { KycIdentificationType } from '../kyc-identification-type.enum';
import { KycType, UserDataStatus } from '../user-data.enum';
import { UserDataRepository } from '../user-data.repository';
import {
Expand Down Expand Up @@ -717,6 +720,40 @@ describe('UserDataService', () => {
}
expect(new Set(assigned.map(([, s]) => s)).size).toBe(assigned.length);
});

// COMPLETED video ident steps are not in the cancel list, so update() receives status undefined;
// that must not wipe status, or the VIDEO_ID / bankTransactionVerification branch never fires.
describe.each([KycStepType.VIDEO, KycStepType.SUMSUB_VIDEO])('completed %s slave ident step', (identType) => {
it('sets master identificationType to VIDEO_ID and bankTransactionVerification to UNNECESSARY', async () => {
const master = buildAccount(1000, 50);
const slave = buildAccount(2000, 20);
const videoStep = Object.assign(new KycStep(), {
id: 1,
name: KycStepName.IDENT,
type: identType,
status: ReviewStatus.COMPLETED,
sequenceNumber: 0,
});

userDataRepo.findOne.mockResolvedValueOnce(master).mockResolvedValueOnce(slave);
transactionService.getAllTransactionsForUserData.mockResolvedValue([]);
userRepo.find.mockResolvedValue([]);
bankDataService.getAllBankDatasForUser.mockResolvedValue([]);
virtualIbanService.getFrickVirtualIbansForAccount.mockResolvedValue([]);
kycAdminService.getKycSteps.mockResolvedValueOnce([]).mockResolvedValueOnce([videoStep]);
documentService.copyFiles.mockResolvedValue(undefined);
jest.spyOn(service, 'updateVolumes').mockResolvedValue(undefined);
jest
.spyOn(service as unknown as { updateBankTxTime: () => Promise<void> }, 'updateBankTxTime')
.mockResolvedValue(undefined);

await service.mergeUserData(master.id, slave.id);

expect(master.identificationType).toBe(KycIdentificationType.VIDEO_ID);
expect(master.bankTransactionVerification).toBe(CheckStatus.UNNECESSARY);
expect(videoStep.status).toBe(ReviewStatus.COMPLETED);
});
});
});

describe('mergeUserData virtual IBAN reassignment', () => {
Expand Down