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
1 change: 1 addition & 0 deletions src/shared/services/process.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum Process {
MONITORING = 'Monitoring',
MONITOR_CONNECTION_POOL = 'MonitorConnectionPool',
UPDATE_STATISTIC = 'UpdateStatistic',
UPDATE_REALUNIT_STATS = 'UpdateRealunitStats',
KYC = 'Kyc',
KYC_IDENT_REVIEW = 'KycIdentReview',
KYC_NATIONALITY_REVIEW = 'KycNationalityReview',
Expand Down
93 changes: 93 additions & 0 deletions src/subdomains/generic/kyc/__tests__/kyc-admin.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service';
import { WebhookService } from 'src/subdomains/generic/user/services/webhook/webhook.service';
import { KycStepName } from '../enums/kyc-step-name.enum';
import { ReviewStatus } from '../enums/review-status.enum';
import { KycStepRepository } from '../repositories/kyc-step.repository';
import { KycAdminService } from '../services/kyc-admin.service';
import { KycNotificationService } from '../services/kyc-notification.service';
import { KycService } from '../services/kyc.service';
import { NameCheckService } from '../services/name-check.service';

describe('KycAdminService', () => {
let service: KycAdminService;
let kycStepRepo: jest.Mocked<KycStepRepository>;

beforeEach(async () => {
kycStepRepo = createMock<KycStepRepository>();

const module: TestingModule = await Test.createTestingModule({
providers: [
KycAdminService,
{ provide: KycStepRepository, useValue: kycStepRepo },
{ provide: WebhookService, useValue: createMock<WebhookService>() },
{ provide: KycService, useValue: createMock<KycService>() },
{ provide: KycNotificationService, useValue: createMock<KycNotificationService>() },
{ provide: UserDataService, useValue: createMock<UserDataService>() },
{ provide: NameCheckService, useValue: createMock<NameCheckService>() },
],
}).compile();

service = module.get(KycAdminService);
});

describe('getKycStepCounts', () => {
function mockQueryBuilder(rows: { name: KycStepName; status: ReviewStatus; count: string }[]): {
select: jest.Mock;
addSelect: jest.Mock;
where: jest.Mock;
groupBy: jest.Mock;
addGroupBy: jest.Mock;
andWhere: jest.Mock;
getRawMany: jest.Mock;
} {
const query = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
addGroupBy: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue(rows),
};
kycStepRepo.createQueryBuilder.mockReturnValue(query as never);
return query;
}

it('returns an empty array and skips querying when names is empty', async () => {
const result = await service.getKycStepCounts([]);

expect(result).toEqual([]);
expect(kycStepRepo.createQueryBuilder).not.toHaveBeenCalled();
});

it('maps raw rows and coerces count to number, without date filters', async () => {
const query = mockQueryBuilder([
{ name: KycStepName.IDENT, status: ReviewStatus.COMPLETED, count: '5' },
{ name: KycStepName.IDENT, status: ReviewStatus.IN_PROGRESS, count: '2' },
]);

const result = await service.getKycStepCounts([KycStepName.IDENT]);

expect(result).toEqual([
{ name: KycStepName.IDENT, status: ReviewStatus.COMPLETED, count: 5 },
{ name: KycStepName.IDENT, status: ReviewStatus.IN_PROGRESS, count: 2 },
]);
expect(query.where).toHaveBeenCalledWith('kycStep.name IN (:...names)', { names: [KycStepName.IDENT] });
expect(query.andWhere).not.toHaveBeenCalled();
});

it('applies from and to date filters when provided', async () => {
const query = mockQueryBuilder([]);
const from = new Date('2024-01-01');
const to = new Date('2024-02-01');

const result = await service.getKycStepCounts([KycStepName.CONTACT_DATA], from, to);

expect(result).toEqual([]);
expect(query.andWhere).toHaveBeenCalledWith('kycStep.created >= :from', { from });
expect(query.andWhere).toHaveBeenCalledWith('kycStep.created <= :to', { to });
});
});
});
24 changes: 24 additions & 0 deletions src/subdomains/generic/kyc/services/kyc-admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ export class KycAdminService {
return this.kycStepRepo.find({ where: { userData: { id: userDataId } }, relations });
}

async getKycStepCounts(
names: KycStepName[],
from?: Date,
to?: Date,
): Promise<{ name: KycStepName; status: ReviewStatus; count: number }[]> {
if (!names.length) return [];

const query = this.kycStepRepo
.createQueryBuilder('kycStep')
.select('kycStep.name', 'name')
.addSelect('kycStep.status', 'status')
.addSelect('COUNT(kycStep.id)', 'count')
.where('kycStep.name IN (:...names)', { names })
.groupBy('kycStep.name')
.addGroupBy('kycStep.status');

if (from) query.andWhere('kycStep.created >= :from', { from });
if (to) query.andWhere('kycStep.created <= :to', { to });

return query
.getRawMany<{ name: KycStepName; status: ReviewStatus; count: string }>()
.then((rows) => rows.map((r) => ({ name: r.name, status: r.status, count: Number(r.count) })));
}

async updateKycStep(stepId: number, dto: UpdateKycStepDto): Promise<void> {
const kycStep = await this.kycStepRepo.findOne({
where: { id: stepId },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,37 @@ describe('UserDataService', () => {
expect(savedArg.id).toBe(1);
});
});

describe('getNewUserDataCount', () => {
function mockQueryBuilder(count: number): { andWhere: jest.Mock; getCount: jest.Mock } {
const query = {
andWhere: jest.fn().mockReturnThis(),
getCount: jest.fn().mockResolvedValue(count),
};
userDataRepo.createQueryBuilder.mockReturnValue(query as never);
return query;
}

it('returns the count without any date filter', async () => {
const query = mockQueryBuilder(42);

const result = await service.getNewUserDataCount();

expect(result).toBe(42);
expect(query.andWhere).not.toHaveBeenCalled();
expect(query.getCount).toHaveBeenCalledTimes(1);
});

it('applies from and to date filters when provided', async () => {
const query = mockQueryBuilder(7);
const from = new Date('2024-01-01');
const to = new Date('2024-02-01');

const result = await service.getNewUserDataCount(from, to);

expect(result).toBe(7);
expect(query.andWhere).toHaveBeenCalledWith('userData.created >= :from', { from });
expect(query.andWhere).toHaveBeenCalledWith('userData.created <= :to', { to });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ export class UserDataService {
return this.phoneCallCompletedSubject.asObservable();
}

async getNewUserDataCount(from?: Date, to?: Date): Promise<number> {
const query = this.userDataRepo.createQueryBuilder('userData');
if (from) query.andWhere('userData.created >= :from', { from });
if (to) query.andWhere('userData.created <= :to', { to });
return query.getCount();
}

async getUserDataByUser(userId: number): Promise<UserData> {
return this.userDataRepo
.createQueryBuilder('userData')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,37 @@ describe('UserService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});

describe('getNewUserCount', () => {
function mockQueryBuilder(count: number): { andWhere: jest.Mock; getCount: jest.Mock } {
const query = {
andWhere: jest.fn().mockReturnThis(),
getCount: jest.fn().mockResolvedValue(count),
};
(userRepo.createQueryBuilder as jest.Mock).mockReturnValue(query);
return query;
}

it('returns the count without any date filter', async () => {
const query = mockQueryBuilder(100);

const result = await service.getNewUserCount();

expect(result).toBe(100);
expect(query.andWhere).not.toHaveBeenCalled();
expect(query.getCount).toHaveBeenCalledTimes(1);
});

it('applies from and to date filters when provided', async () => {
const query = mockQueryBuilder(12);
const from = new Date('2024-01-01');
const to = new Date('2024-02-01');

const result = await service.getNewUserCount(from, to);

expect(result).toBe(12);
expect(query.andWhere).toHaveBeenCalledWith('user.created >= :from', { from });
expect(query.andWhere).toHaveBeenCalledWith('user.created <= :to', { to });
});
});
});
7 changes: 7 additions & 0 deletions src/subdomains/generic/user/models/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ export class UserService {
return this.userRepo.findOne({ where: { address }, relations });
}

async getNewUserCount(from?: Date, to?: Date): Promise<number> {
const query = this.userRepo.createQueryBuilder('user');
if (from) query.andWhere('user.created >= :from', { from });
if (to) query.andWhere('user.created <= :to', { to });
return query.getCount();
}

async getUserByKey(key: string, value: any, onlyDefaultRelation = false): Promise<User> {
const query = this.userRepo
.createQueryBuilder('user')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { BuyCryptoRepository } from 'src/subdomains/core/buy-crypto/process/repositories/buy-crypto.repository';
import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service';
import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service';
import { TransactionRequestType } from '../entities/transaction-request.entity';
import { TransactionRepository } from '../repositories/transaction.repository';
import { SpecialExternalAccountService } from '../services/special-external-account.service';
import { TransactionService } from '../services/transaction.service';

describe('TransactionService', () => {
let service: TransactionService;
let repo: jest.Mocked<TransactionRepository>;

beforeEach(async () => {
repo = createMock<TransactionRepository>();

const module: TestingModule = await Test.createTestingModule({
providers: [
TransactionService,
{ provide: TransactionRepository, useValue: repo },
{ provide: UserDataService, useValue: createMock<UserDataService>() },
{ provide: BankDataService, useValue: createMock<BankDataService>() },
{ provide: SpecialExternalAccountService, useValue: createMock<SpecialExternalAccountService>() },
{ provide: BuyCryptoRepository, useValue: createMock<BuyCryptoRepository>() },
],
}).compile();

service = module.get(TransactionService);
});

describe('getAssetTradingStats', () => {
function mockQueryBuilder(rows: { type: TransactionRequestType; volume: string; count: string }[]): {
select: jest.Mock;
addSelect: jest.Mock;
innerJoin: jest.Mock;
where: jest.Mock;
andWhere: jest.Mock;
groupBy: jest.Mock;
getRawMany: jest.Mock;
} {
const subQuery = { where: jest.fn().mockReturnThis(), orWhere: jest.fn().mockReturnThis() };
const query = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockImplementation(function (this: unknown, arg: unknown) {
// Execute the Brackets where-factory so its callback body is exercised.
if (arg && typeof (arg as { whereFactory?: unknown }).whereFactory === 'function') {
(arg as { whereFactory: (qb: unknown) => void }).whereFactory(subQuery);
}
return this;
}),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue(rows),
};
repo.createQueryBuilder.mockReturnValue(query as never);
return query;
}

it('maps raw rows to numbers without date filters', async () => {
const query = mockQueryBuilder([
{ type: TransactionRequestType.BUY, volume: '1234.5', count: '10' },
{ type: TransactionRequestType.SELL, volume: '500', count: '3' },
]);

const result = await service.getAssetTradingStats(408);

expect(result).toEqual([
{ type: TransactionRequestType.BUY, volume: 1234.5, count: 10 },
{ type: TransactionRequestType.SELL, volume: 500, count: 3 },
]);
expect(query.groupBy).toHaveBeenCalledWith('request.type');
expect(query.andWhere).not.toHaveBeenCalledWith('transaction.created >= :from', expect.anything());
expect(query.andWhere).not.toHaveBeenCalledWith('transaction.created <= :to', expect.anything());
});

it('coerces null/NaN volume and count to 0', async () => {
mockQueryBuilder([
{ type: TransactionRequestType.BUY, volume: null as unknown as string, count: undefined as unknown as string },
]);

const result = await service.getAssetTradingStats(408);

expect(result).toEqual([{ type: TransactionRequestType.BUY, volume: 0, count: 0 }]);
});

it('applies from and to date filters when provided', async () => {
const query = mockQueryBuilder([]);
const from = new Date('2024-01-01');
const to = new Date('2024-02-01');

const result = await service.getAssetTradingStats(408, from, to);

expect(result).toEqual([]);
expect(query.andWhere).toHaveBeenCalledWith('transaction.created >= :from', { from });
expect(query.andWhere).toHaveBeenCalledWith('transaction.created <= :to', { to });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,42 @@ export class TransactionService {
});
}

async getAssetTradingStats(
assetId: number,
from?: Date,
to?: Date,
): Promise<{ type: TransactionRequestType; volume: number; count: number }[]> {
const query = this.repo
.createQueryBuilder('transaction')
.select('request.type', 'type')
.addSelect('SUM(transaction.amountInChf)', 'volume')
.addSelect('COUNT(transaction.id)', 'count')
.innerJoin('transaction.request', 'request')
.where('transaction.type IS NOT NULL')
.andWhere('transaction.amountInChf IS NOT NULL')
.andWhere(
new Brackets((qb) =>
qb
.where('(request.type = :buy AND request.targetId = :assetId)', {
buy: TransactionRequestType.BUY,
assetId,
})
.orWhere('(request.type = :sell AND request.sourceId = :assetId)', {
sell: TransactionRequestType.SELL,
assetId,
}),
),
)
.groupBy('request.type');

if (from) query.andWhere('transaction.created >= :from', { from });
if (to) query.andWhere('transaction.created <= :to', { to });

return query
.getRawMany<{ type: TransactionRequestType; volume: string; count: string }>()
.then((rows) => rows.map((r) => ({ type: r.type, volume: Number(r.volume) || 0, count: Number(r.count) || 0 })));
}

async getTransactionByKey(key: string, value: any): Promise<Transaction> {
return this.repo
.createQueryBuilder('transaction')
Expand Down
Loading