update: refactor create-order endpoint

This commit is contained in:
Andrew Besedin 2026-02-13 19:53:32 +03:00
parent 5160b593ef
commit 0f3aa4a73f
6 changed files with 207 additions and 52 deletions

1
shared/constants.ts Normal file
View file

@ -0,0 +1 @@
export const NON_NEGATIVE_REAL_NUMBER_REGEX = /^\d+(\.\d+)?$/;

View file

@ -1,5 +1,7 @@
import { Request, Response } from 'express';
import Decimal from 'decimal.js';
import CreateOrderRes, { CreateOrderErrorCode } from '@/interfaces/responses/orders/CreateOrderRes';
import candlesModel from '../models/Candles';
import ordersModel from '../models/Orders';
import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody';
@ -16,70 +18,97 @@ import Currency from '../schemes/Currency';
import { validateTokensInput } from '../../shared/utils';
class OrdersController {
async createOrder(req: Request, res: Response) {
static CURRENCY_DECIMAL_POINT_NOT_FOUND_ERROR_MSG = 'CURRENCY_DECIMAL_POINT_MISSING';
async createOrder(req: Request, res: Response<CreateOrderRes>) {
try {
const { orderData } = req.body as CreateOrderBody;
const body = req.body as CreateOrderBody;
const { orderData } = body;
const { price, amount, pairId } = orderData;
const isFull =
orderData &&
orderData?.type &&
orderData?.side &&
orderData?.price &&
orderData?.amount &&
orderData?.pairId;
const priceDecimal = new Decimal(price);
const amountDecimal = new Decimal(amount);
const priceDecimal = new Decimal(orderData?.price || 0);
const amountDecimal = new Decimal(orderData?.amount || 0);
const pair = await Pair.findByPk(orderData?.pairId);
const pair = await Pair.findByPk(pairId);
const firstCurrency = await Currency.findByPk(pair?.first_currency_id);
const secondCurrency = await Currency.findByPk(pair?.second_currency_id);
if (!pair || !firstCurrency || !secondCurrency) {
return res.status(400).send({ success: false, data: 'Invalid pair data' });
return res.status(400).send({
success: false,
data: CreateOrderErrorCode.INVALID_ORDER_DATA,
});
}
const firstCurrencyDecimalPoint = firstCurrency?.asset_info?.decimal_point || 12;
const secondCurrencyDecimalPoint = secondCurrency?.asset_info?.decimal_point || 12;
const firstCurrencyDP = firstCurrency.asset_info?.decimal_point;
const secondCurrencyDP = secondCurrency.asset_info?.decimal_point;
const rangeCorrect = (() => {
const priceCorrect = validateTokensInput(
orderData?.price,
secondCurrencyDecimalPoint,
).valid;
const amountCorrect = validateTokensInput(
orderData?.amount,
firstCurrencyDecimalPoint,
).valid;
return priceCorrect && amountCorrect;
})();
const priceDecimalPointCorrect = priceDecimal.toString().replace('.', '').length <= 20;
const amountDecimalPointCorrect =
amountDecimal.toString().replace('.', '').length <= 18;
if (!priceDecimalPointCorrect || !amountDecimalPointCorrect) {
return res.status(400).send({ success: false, data: 'Invalid pair data' });
if (firstCurrencyDP === undefined || secondCurrencyDP === undefined) {
throw new Error(OrdersController.CURRENCY_DECIMAL_POINT_NOT_FOUND_ERROR_MSG);
}
if (!isFull || !rangeCorrect)
return res.status(400).send({ success: false, data: 'Invalid order data' });
const totalDecimal = priceDecimal.mul(amountDecimal);
const total = totalDecimal.toFixed();
const isPriceValid = validateTokensInput(price, secondCurrencyDP).valid;
const isAmountValid = validateTokensInput(amount, firstCurrencyDP).valid;
const isTotalValid = validateTokensInput(total, secondCurrencyDP).valid;
const areAmountsValid = isPriceValid && isAmountValid && isTotalValid;
if (!areAmountsValid) {
return res.status(400).send({
success: false,
data: CreateOrderErrorCode.INVALID_ORDER_DATA,
});
}
const result = await ordersModel.createOrder(req.body);
if (result.data === 'Invalid order data') return res.status(400).send(result);
if (result.data === 'Invalid order data')
return res.status(400).send({
success: false,
data: CreateOrderErrorCode.INVALID_ORDER_DATA,
});
if (result.data === 'Same order') return res.status(400).send(result);
if (result.data === 'Same order')
return res.status(400).send({
success: false,
data: CreateOrderErrorCode.SAME_ORDER,
});
if (result.data === 'Internal error') return res.status(500).send(result);
if (result.data === 'Internal error') {
throw new Error('orderModel.createOrder returned Internal error');
}
res.status(200).send(result);
if (typeof result.data === 'string') {
throw new Error('Invalid orderModel.createOrder result');
}
const createdOrder = result.data;
res.status(200).send({
success: true,
data: {
id: createdOrder.id,
type: createdOrder.type,
timestamp: createdOrder.timestamp,
side: createdOrder.side,
price: createdOrder.price,
amount: createdOrder.amount,
total: createdOrder.total,
pair_id: createdOrder.pairId,
user_id: createdOrder.userId,
status: createdOrder.status,
left: createdOrder.left,
hasNotification: createdOrder.hasNotification,
immediateMatch: createdOrder.immediateMatch,
},
});
} catch (err) {
console.log(err);
res.status(500).send({ success: false, data: 'Unhandled error' });
res.status(500).send({ success: false, data: CreateOrderErrorCode.UNHANDLED_ERROR });
}
}

View file

@ -1,10 +1,24 @@
import OfferType from '../../common/OfferType';
import Side from '../../common/Side';
import { body } from 'express-validator';
import { NON_NEGATIVE_REAL_NUMBER_REGEX } from 'shared/constants';
import UserData from '../../common/UserData';
interface OrderData {
type: OfferType;
side: Side;
export enum CreateOrderType {
// eslint-disable-next-line no-unused-vars
BUY = 'buy',
// eslint-disable-next-line no-unused-vars
SELL = 'sell',
}
export enum CreateOrderSide {
// eslint-disable-next-line no-unused-vars
LIMIT = 'limit',
// eslint-disable-next-line no-unused-vars
MARKET = 'market',
}
interface CreateOrderData {
type: CreateOrderType;
side: CreateOrderSide;
price: string;
amount: string;
pairId: string;
@ -12,7 +26,26 @@ interface OrderData {
interface CreateOrderBody {
userData: UserData;
orderData: OrderData;
orderData: CreateOrderData;
}
export const createOrderValidator = [
body('orderData').isObject().withMessage('orderData must be an object'),
body('orderData.type')
.isIn(Object.values(CreateOrderType))
.withMessage(`Invalid orderData.type value`),
body('orderData.side')
.isIn(Object.values(CreateOrderSide))
.withMessage(`Invalid orderData.side value`),
body('orderData.price')
.isString()
.matches(NON_NEGATIVE_REAL_NUMBER_REGEX)
.withMessage('orderData.price must be a positive decimal string'),
body('orderData.amount')
.isString()
.matches(NON_NEGATIVE_REAL_NUMBER_REGEX)
.withMessage('orderData.amount must be a positive decimal string'),
body('orderData.pairId').isString().withMessage('orderData.pairId must be a string'),
];
export default CreateOrderBody;

View file

@ -0,0 +1,36 @@
export type CreateOrderSuccessRes = {
success: true;
data: {
hasNotification: boolean;
id: number;
type: string;
timestamp: number;
side: string;
price: string;
amount: string;
total: string;
pair_id: number;
user_id: number;
status: string;
left: string;
immediateMatch?: true;
};
};
export enum CreateOrderErrorCode {
// eslint-disable-next-line no-unused-vars
INVALID_ORDER_DATA = 'Invalid order data',
// eslint-disable-next-line no-unused-vars
SAME_ORDER = 'Same order',
// eslint-disable-next-line no-unused-vars
UNHANDLED_ERROR = 'Unhandled error',
}
export type CreateOrderErrorRes = {
success: false;
data: CreateOrderErrorCode;
};
type CreateOrderRes = CreateOrderSuccessRes | CreateOrderErrorRes;
export default CreateOrderRes;

View file

@ -68,7 +68,30 @@ class OrdersModel {
return matchedOrders;
}
async createOrder(body: CreateOrderBody) {
async createOrder(body: CreateOrderBody): Promise<
| {
success: false;
data: string;
}
| {
success: true;
data: {
id: number;
type: string;
timestamp: number;
side: string;
price: string;
amount: string;
total: string;
pairId: number;
userId: number;
status: string;
left: string;
hasNotification: boolean;
immediateMatch?: true;
};
}
> {
try {
const { orderData } = body;
const { userData } = body;
@ -173,13 +196,41 @@ class OrdersModel {
return {
success: true,
data: {
...newOrder.toJSON(),
id: newOrder.id,
type: newOrder.type,
timestamp: newOrder.timestamp,
side: newOrder.side,
price: newOrder.price,
amount: newOrder.amount,
total: newOrder.total,
pairId: newOrder.pair_id,
userId: newOrder.user_id,
status: newOrder.status,
left: newOrder.left,
hasNotification: newOrder.hasNotification,
immediateMatch: true,
},
};
}
return { success: true, data: newOrder.toJSON() };
return {
success: true,
data: {
id: newOrder.id,
type: newOrder.type,
timestamp: newOrder.timestamp,
side: newOrder.side,
price: newOrder.price,
amount: newOrder.amount,
total: newOrder.total,
pairId: newOrder.pair_id,
userId: newOrder.user_id,
status: newOrder.status,
left: newOrder.left,
hasNotification: newOrder.hasNotification,
},
};
} catch (err) {
console.log(err);
return { success: false, data: 'Internal error' };

View file

@ -1,4 +1,5 @@
import express from 'express';
import { createOrderValidator } from '@/interfaces/bodies/orders/CreateOrderBody.js';
import middleware from '../middleware/middleware.js';
import ordersController from '../controllers/orders.controller.js';
@ -15,7 +16,11 @@ ordersRouter.use(
middleware.verifyToken,
);
ordersRouter.post('/orders/create', ordersController.createOrder);
ordersRouter.post(
'/orders/create',
middleware.expressValidator(createOrderValidator),
ordersController.createOrder,
);
ordersRouter.post('/orders/get-page', ordersController.getOrdersPage);
ordersRouter.post('/orders/get-user-page', ordersController.getUserOrdersPage);
ordersRouter.post('/orders/get', ordersController.getUserOrders);