Skip to content

Commit 62e9ee1

Browse files
committed
feat: fixed balance check + calculation, added order history API
1 parent 0f062f9 commit 62e9ee1

8 files changed

Lines changed: 157 additions & 11 deletions

File tree

src/subdomains/core/custody/controllers/custody.controller.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { RoleGuard } from 'src/shared/auth/role.guard';
88
import { UserActiveGuard } from 'src/shared/auth/user-active.guard';
99
import { UserRole } from 'src/shared/auth/user-role.enum';
1010
import { AssetService } from 'src/shared/models/asset/asset.service';
11-
import { UserService } from 'src/subdomains/generic/user/models/user/user.service';
1211
import { PdfDto } from 'src/subdomains/core/buy-crypto/routes/buy/dto/pdf.dto';
12+
import { UserService } from 'src/subdomains/generic/user/models/user/user.service';
1313
import { CustodySignupDto } from '../dto/input/custody-signup.dto';
1414
import { GetCustodyInfoDto } from '../dto/input/get-custody-info.dto';
1515
import { GetCustodyPdfDto } from '../dto/input/get-custody-pdf.dto';
1616
import { CustodyAuthDto } from '../dto/output/custody-auth.dto';
1717
import { CustodyBalanceDto, CustodyHistoryDto } from '../dto/output/custody-balance.dto';
18+
import { CustodyOrderHistoryDto } from '../dto/output/custody-order-history.dto';
1819
import { CustodyOrderDto } from '../dto/output/custody-order.dto';
1920
import { CustodyOrderService } from '../services/custody-order.service';
2021
import { CustodyPdfService } from '../services/custody-pdf.service';
@@ -66,6 +67,14 @@ export class CustodyController {
6667
return this.service.createCustodyAccount(jwt.account, dto, ip);
6768
}
6869

70+
@Get('order')
71+
@ApiBearerAuth()
72+
@UseGuards(AuthGuard(), RoleGuard(UserRole.CUSTODY), UserActiveGuard())
73+
@ApiOkResponse({ type: CustodyOrderHistoryDto, isArray: true })
74+
async getOrders(@GetJwt() jwt: JwtPayload): Promise<CustodyOrderHistoryDto[]> {
75+
return this.custodyOrderService.getOrdersByUserData(jwt.account);
76+
}
77+
6978
@Post('order')
7079
@ApiBearerAuth()
7180
@UseGuards(AuthGuard(), RoleGuard(UserRole.CUSTODY), UserActiveGuard())
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { CustodyOrderType } from '../../enums/custody';
3+
4+
export enum CustodyOrderHistoryStatus {
5+
WAITING_FOR_PAYMENT = 'WaitingForPayment',
6+
CHECK_PENDING = 'CheckPending',
7+
PROCESSING = 'Processing',
8+
COMPLETED = 'Completed',
9+
FAILED = 'Failed',
10+
}
11+
12+
export class CustodyOrderHistoryDto {
13+
@ApiProperty({ enum: CustodyOrderType })
14+
type: CustodyOrderType;
15+
16+
@ApiProperty({ enum: CustodyOrderHistoryStatus })
17+
status: CustodyOrderHistoryStatus;
18+
19+
@ApiPropertyOptional()
20+
inputAmount?: number;
21+
22+
@ApiPropertyOptional()
23+
inputAsset?: string;
24+
25+
@ApiPropertyOptional()
26+
outputAmount?: number;
27+
28+
@ApiPropertyOptional()
29+
outputAsset?: string;
30+
}

src/subdomains/core/custody/entities/custody-order.entity.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,16 @@ export class CustodyOrder extends IEntity {
9191
status: CustodyOrderStatus.COMPLETED,
9292
});
9393
}
94+
95+
fail(): UpdateResult<CustodyOrder> {
96+
return Util.updateEntity<CustodyOrder>(this, {
97+
status: CustodyOrderStatus.FAILED,
98+
});
99+
}
100+
101+
reset(): UpdateResult<CustodyOrder> {
102+
return Util.updateEntity<CustodyOrder>(this, {
103+
status: CustodyOrderStatus.CREATED,
104+
});
105+
}
94106
}

src/subdomains/core/custody/enums/custody.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ export enum CustodyOrderType {
1616
SAVING_WITHDRAWAL = 'SavingWithdrawal',
1717
}
1818

19+
export const CustodyIncomingTypes = [CustodyOrderType.DEPOSIT, CustodyOrderType.RECEIVE];
20+
export const CustodySwapTypes = [CustodyOrderType.SWAP, CustodyOrderType.SAVING_DEPOSIT, CustodyOrderType.SAVING_WITHDRAWAL];
21+
1922
export enum CustodyOrderStatus {
2023
CREATED = 'Created',
2124
CONFIRMED = 'Confirmed',
2225
APPROVED = 'Approved',
2326
IN_PROGRESS = 'InProgress',
2427
COMPLETED = 'Completed',
28+
FAILED = 'Failed',
2529
}
2630

2731
export enum CustodyOrderStepStatus {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { CustodyOrderHistoryDto, CustodyOrderHistoryStatus } from '../dto/output/custody-order-history.dto';
2+
import { CustodyOrder } from '../entities/custody-order.entity';
3+
import { CustodyIncomingTypes, CustodyOrderStatus, CustodySwapTypes } from '../enums/custody';
4+
5+
export class CustodyOrderHistoryDtoMapper {
6+
static mapList(orders: CustodyOrder[]): CustodyOrderHistoryDto[] {
7+
return orders.map((order) => this.map(order));
8+
}
9+
10+
static map(order: CustodyOrder): CustodyOrderHistoryDto {
11+
const isIncoming = CustodyIncomingTypes.includes(order.type);
12+
const isSwap = CustodySwapTypes.includes(order.type);
13+
14+
return {
15+
type: order.type,
16+
status: this.mapStatus(order),
17+
inputAmount: isIncoming || isSwap ? order.inputAmount ?? order.transactionRequest?.estimatedAmount : order.inputAmount,
18+
inputAsset: order.inputAsset?.name,
19+
outputAmount: isIncoming ? order.outputAmount : order.outputAmount ?? order.transactionRequest?.amount,
20+
outputAsset: order.outputAsset?.name,
21+
};
22+
}
23+
24+
private static mapStatus(order: CustodyOrder): CustodyOrderHistoryStatus {
25+
const isIncoming = CustodyIncomingTypes.includes(order.type);
26+
27+
switch (order.status) {
28+
case CustodyOrderStatus.CONFIRMED:
29+
return isIncoming ? CustodyOrderHistoryStatus.WAITING_FOR_PAYMENT : CustodyOrderHistoryStatus.CHECK_PENDING;
30+
31+
case CustodyOrderStatus.APPROVED:
32+
case CustodyOrderStatus.IN_PROGRESS:
33+
return CustodyOrderHistoryStatus.PROCESSING;
34+
35+
case CustodyOrderStatus.COMPLETED:
36+
return CustodyOrderHistoryStatus.COMPLETED;
37+
38+
case CustodyOrderStatus.FAILED:
39+
return CustodyOrderHistoryStatus.FAILED;
40+
}
41+
}
42+
}

src/subdomains/core/custody/services/custody-job.service.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { Injectable } from '@nestjs/common';
22
import { CronExpression } from '@nestjs/schedule';
3+
import { LessThan } from 'typeorm';
34
import { DfxOrderStepAdapter } from '../adapter/dfx-order-step.adapter';
45
import { OrderConfig } from '../config/order-config';
56

7+
import { Config } from 'src/config/config';
68
import { Process } from 'src/shared/services/process.service';
79
import { DfxCron } from 'src/shared/utils/cron';
10+
import { Util } from 'src/shared/utils/util';
811
import { CustodyOrderStatus, CustodyOrderStepContext, CustodyOrderStepStatus } from '../enums/custody';
912
import { CustodyOrderStepRepository } from '../repositories/custody-order-step.repository';
1013
import { CustodyOrderRepository } from '../repositories/custody-order.repository';
@@ -26,6 +29,22 @@ export class CustodyJobService {
2629
await this.checkStep();
2730
}
2831

32+
@DfxCron(CronExpression.EVERY_DAY_AT_4AM, { process: Process.CUSTODY })
33+
async resetExpiredConfirmedOrders() {
34+
const expiryDate = Util.daysBefore(Config.txRequestWaitingExpiryDays);
35+
36+
const expiredOrders = await this.custodyOrderRepo.find({
37+
where: {
38+
status: CustodyOrderStatus.CONFIRMED,
39+
updated: LessThan(expiryDate),
40+
},
41+
});
42+
43+
for (const order of expiredOrders) {
44+
await this.custodyOrderRepo.update(...order.reset());
45+
}
46+
}
47+
2948
private async executeOrder() {
3049
const approvedOrders = await this.custodyOrderRepo.find({
3150
where: { status: CustodyOrderStatus.APPROVED },

src/subdomains/core/custody/services/custody-order.service.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import { JwtPayload } from 'src/shared/auth/jwt-payload.interface';
1111
import { Asset } from 'src/shared/models/asset/asset.entity';
1212
import { AssetService } from 'src/shared/models/asset/asset.service';
1313
import { FiatService } from 'src/shared/models/fiat/fiat.service';
14+
import { User } from 'src/subdomains/generic/user/models/user/user.entity';
1415
import { UserService } from 'src/subdomains/generic/user/models/user/user.service';
1516
import { TransactionRequest } from 'src/subdomains/supporting/payment/entities/transaction-request.entity';
16-
import { Equal } from 'typeorm';
17+
import { Equal, In, Not } from 'typeorm';
1718
import { BuyCrypto } from '../../buy-crypto/process/entities/buy-crypto.entity';
1819
import { BuyService } from '../../buy-crypto/routes/buy/buy.service';
1920
import { SwapService } from '../../buy-crypto/routes/swap/swap.service';
@@ -23,12 +24,18 @@ import { OrderConfig } from '../config/order-config';
2324
import { CreateCustodyOrderInternalDto } from '../dto/input/create-custody-order.dto';
2425
import { GetCustodyInfoDto } from '../dto/input/get-custody-info.dto';
2526
import { UpdateCustodyOrderInternalDto } from '../dto/input/update-custody-order.dto';
27+
import { CustodyOrderHistoryDto } from '../dto/output/custody-order-history.dto';
2628
import { CustodyOrderResponseDto } from '../dto/output/custody-order-response.dto';
2729
import { CustodyOrderDto } from '../dto/output/custody-order.dto';
28-
import { CustodyBalance } from '../entities/custody-balance.entity';
2930
import { CustodyOrderStep } from '../entities/custody-order-step.entity';
3031
import { CustodyOrder } from '../entities/custody-order.entity';
31-
import { CustodyOrderStepCommand, CustodyOrderStepContext, CustodyOrderType } from '../enums/custody';
32+
import {
33+
CustodyOrderStatus,
34+
CustodyOrderStepCommand,
35+
CustodyOrderStepContext,
36+
CustodyOrderType,
37+
} from '../enums/custody';
38+
import { CustodyOrderHistoryDtoMapper } from '../mappers/custody-order-history-dto.mapper';
3239
import { CustodyOrderResponseDtoMapper } from '../mappers/custody-order-response-dto.mapper';
3340
import { GetCustodyOrderDtoMapper } from '../mappers/get-custody-order-dto.mapper';
3441
import { CustodyOrderStepRepository } from '../repositories/custody-order-step.repository';
@@ -81,14 +88,15 @@ export class CustodyOrderService {
8188
paymentInfo = CustodyOrderResponseDtoMapper.mapBuyPaymentInfo(buyPaymentInfo);
8289
break;
8390
}
91+
8492
case CustodyOrderType.WITHDRAWAL: {
8593
const sourceAsset = await this.getCustodyAsset(dto.sourceAsset);
8694
if (!sourceAsset) throw new NotFoundException('Source asset not found');
8795

8896
const targetCurrency = await this.fiatService.getFiatByName(dto.targetAsset);
8997
if (!targetCurrency) throw new NotFoundException('Target currency not found');
9098

91-
this.checkBalance(sourceAsset, dto.sourceAmount, user.custodyBalances);
99+
await this.checkBalance(sourceAsset, dto.sourceAmount, user);
92100

93101
const sellPaymentInfo = await this.sellService.createSellPaymentInfo(
94102
jwt.user,
@@ -102,14 +110,15 @@ export class CustodyOrderService {
102110
paymentInfo = CustodyOrderResponseDtoMapper.mapSellPaymentInfo(sellPaymentInfo);
103111
break;
104112
}
113+
105114
case CustodyOrderType.SWAP: {
106115
const sourceAsset = await this.getCustodyAsset(dto.sourceAsset);
107116
if (!sourceAsset) throw new NotFoundException('Source asset not found');
108117

109118
const targetAsset = await this.getCustodyAsset(dto.targetAsset);
110119
if (!targetAsset) throw new NotFoundException('Target asset not found');
111120

112-
this.checkBalance(sourceAsset, dto.sourceAmount, user.custodyBalances);
121+
await this.checkBalance(sourceAsset, dto.sourceAmount, user);
113122

114123
const swapPaymentInfo = await this.swapService.createSwapPaymentInfo(
115124
jwt.user,
@@ -123,6 +132,7 @@ export class CustodyOrderService {
123132
paymentInfo = CustodyOrderResponseDtoMapper.mapSwapPaymentInfo(swapPaymentInfo);
124133
break;
125134
}
135+
126136
case CustodyOrderType.SEND: {
127137
const sourceAsset = await this.getCustodyAsset(dto.sourceAsset);
128138
if (!sourceAsset) throw new NotFoundException('Source asset not found');
@@ -134,7 +144,7 @@ export class CustodyOrderService {
134144
});
135145
if (!targetAsset) throw new NotFoundException('Target asset not found');
136146

137-
this.checkBalance(sourceAsset, dto.sourceAmount, user.custodyBalances);
147+
await this.checkBalance(sourceAsset, dto.sourceAmount, user);
138148

139149
const targetUser = await this.userService.getUserByAddress(dto.targetAddress, { userData: true });
140150
if (!targetUser || targetUser.userData.id !== user.userData.id)
@@ -152,6 +162,7 @@ export class CustodyOrderService {
152162
paymentInfo = CustodyOrderResponseDtoMapper.mapSwapPaymentInfo(swapPaymentInfo);
153163
break;
154164
}
165+
155166
case CustodyOrderType.RECEIVE: {
156167
const sourceAsset = await this.getCustodyAsset(dto.sourceAsset);
157168
if (!sourceAsset) throw new NotFoundException('Asset not found');
@@ -181,6 +192,17 @@ export class CustodyOrderService {
181192
};
182193
}
183194

195+
async getOrdersByUserData(userDataId: number): Promise<CustodyOrderHistoryDto[]> {
196+
const orders = await this.custodyOrderRepo.find({
197+
where: { user: { userData: { id: userDataId } }, status: Not(CustodyOrderStatus.CREATED) },
198+
relations: { inputAsset: true, outputAsset: true, transactionRequest: true },
199+
order: { created: 'DESC' },
200+
take: 100,
201+
});
202+
203+
return CustodyOrderHistoryDtoMapper.mapList(orders);
204+
}
205+
184206
async createOrderInternal(dto: CreateCustodyOrderInternalDto): Promise<CustodyOrder> {
185207
const order = this.custodyOrderRepo.create(dto);
186208

@@ -265,9 +287,17 @@ export class CustodyOrderService {
265287
.sort((a, b) => this.CustodyChains.indexOf(a.blockchain) - this.CustodyChains.indexOf(b.blockchain))[0];
266288
}
267289

268-
private checkBalance(asset: Asset, amount: number, custodyBalances: CustodyBalance[]): void {
269-
const assetBalance = custodyBalances.find((a) => a.asset.id === asset.id);
270-
if (!assetBalance || assetBalance.balance < amount)
290+
private async checkBalance(asset: Asset, amount: number, user: User): Promise<void> {
291+
const assetBalance = user.custodyBalances.find((a) => a.asset.id === asset.id);
292+
const balance = assetBalance?.balance ?? 0;
293+
294+
const pendingAmount = await this.custodyOrderRepo.sum('outputAmount', {
295+
user: { id: user.id },
296+
outputAsset: { id: asset.id },
297+
status: In([CustodyOrderStatus.CONFIRMED, CustodyOrderStatus.APPROVED, CustodyOrderStatus.IN_PROGRESS]),
298+
});
299+
300+
if (balance < pendingAmount + amount)
271301
throw new BadRequestException('This transaction can only be created manually by support');
272302
}
273303
}

src/subdomains/core/custody/services/custody.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class CustodyService {
125125
.select('SUM(custodyOrder.outputAmount)', 'withdrawal')
126126
.where('custodyOrder.userId = :id', { id: user.id })
127127
.andWhere('custodyOrder.outputAssetId = :asset', { asset: asset.id })
128-
.andWhere('custodyOrder.status != :status', { status: CustodyOrderStatus.CREATED })
128+
.andWhere('custodyOrder.status = :status', { status: CustodyOrderStatus.COMPLETED })
129129
.getRawOne<{ withdrawal: number }>();
130130

131131
const balance = deposit - withdrawal;

0 commit comments

Comments
 (0)