2026-03-01 03:04:24 +08:00
|
|
|
import { prisma } from '@/lib/db';
|
|
|
|
|
import { getEnv } from '@/lib/config';
|
|
|
|
|
import { generateRechargeCode } from './code-gen';
|
|
|
|
|
import { createPayment } from '@/lib/easy-pay/client';
|
|
|
|
|
import { verifySign } from '@/lib/easy-pay/sign';
|
|
|
|
|
import { refund as easyPayRefund } from '@/lib/easy-pay/client';
|
|
|
|
|
import { getUser, createAndRedeem, subtractBalance } from '@/lib/sub2api/client';
|
|
|
|
|
import { Prisma } from '@prisma/client';
|
2026-03-01 14:25:12 +08:00
|
|
|
import type { EasyPayNotifyParams } from '@/lib/easy-pay/types';
|
2026-03-01 03:04:24 +08:00
|
|
|
import { deriveOrderState, isRefundStatus } from './status';
|
|
|
|
|
|
|
|
|
|
const MAX_PENDING_ORDERS = 3;
|
|
|
|
|
|
|
|
|
|
export interface CreateOrderInput {
|
|
|
|
|
userId: number;
|
|
|
|
|
amount: number;
|
|
|
|
|
paymentType: 'alipay' | 'wxpay';
|
|
|
|
|
clientIp: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CreateOrderResult {
|
|
|
|
|
orderId: string;
|
|
|
|
|
amount: number;
|
|
|
|
|
status: string;
|
|
|
|
|
paymentType: 'alipay' | 'wxpay';
|
|
|
|
|
userName: string;
|
|
|
|
|
userBalance: number;
|
|
|
|
|
payUrl?: string | null;
|
|
|
|
|
qrCode?: string | null;
|
|
|
|
|
expiresAt: Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createOrder(input: CreateOrderInput): Promise<CreateOrderResult> {
|
|
|
|
|
const env = getEnv();
|
|
|
|
|
|
|
|
|
|
const user = await getUser(input.userId);
|
|
|
|
|
if (user.status !== 'active') {
|
|
|
|
|
throw new OrderError('USER_INACTIVE', 'User account is disabled', 422);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pendingCount = await prisma.order.count({
|
|
|
|
|
where: { userId: input.userId, status: 'PENDING' },
|
|
|
|
|
});
|
|
|
|
|
if (pendingCount >= MAX_PENDING_ORDERS) {
|
|
|
|
|
throw new OrderError('TOO_MANY_PENDING', `Too many pending orders (${MAX_PENDING_ORDERS})`, 429);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expiresAt = new Date(Date.now() + env.ORDER_TIMEOUT_MINUTES * 60 * 1000);
|
|
|
|
|
const order = await prisma.order.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: input.userId,
|
|
|
|
|
userEmail: user.email,
|
|
|
|
|
userName: user.username,
|
|
|
|
|
amount: new Prisma.Decimal(input.amount.toFixed(2)),
|
|
|
|
|
rechargeCode: '',
|
|
|
|
|
status: 'PENDING',
|
|
|
|
|
paymentType: input.paymentType,
|
|
|
|
|
expiresAt,
|
|
|
|
|
clientIp: input.clientIp,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const rechargeCode = generateRechargeCode(order.id);
|
|
|
|
|
await prisma.order.update({
|
|
|
|
|
where: { id: order.id },
|
|
|
|
|
data: { rechargeCode },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const easyPayResult = await createPayment({
|
|
|
|
|
outTradeNo: order.id,
|
|
|
|
|
amount: input.amount.toFixed(2),
|
|
|
|
|
paymentType: input.paymentType,
|
|
|
|
|
clientIp: input.clientIp,
|
|
|
|
|
productName: `${env.PRODUCT_NAME} ${input.amount.toFixed(2)} CNY`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prisma.order.update({
|
|
|
|
|
where: { id: order.id },
|
|
|
|
|
data: {
|
|
|
|
|
zpayTradeNo: easyPayResult.trade_no,
|
|
|
|
|
payUrl: easyPayResult.payurl || null,
|
|
|
|
|
qrCode: easyPayResult.qrcode || null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
action: 'ORDER_CREATED',
|
|
|
|
|
detail: JSON.stringify({ userId: input.userId, amount: input.amount, paymentType: input.paymentType }),
|
|
|
|
|
operator: `user:${input.userId}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
amount: input.amount,
|
|
|
|
|
status: 'PENDING',
|
|
|
|
|
paymentType: input.paymentType,
|
|
|
|
|
userName: user.username,
|
|
|
|
|
userBalance: user.balance,
|
|
|
|
|
payUrl: easyPayResult.payurl,
|
|
|
|
|
qrCode: easyPayResult.qrcode,
|
|
|
|
|
expiresAt,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await prisma.order.delete({ where: { id: order.id } });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function cancelOrder(orderId: string, userId: number): Promise<void> {
|
|
|
|
|
const result = await prisma.order.updateMany({
|
|
|
|
|
where: { id: orderId, userId, status: 'PENDING' },
|
|
|
|
|
data: { status: 'CANCELLED', updatedAt: new Date() },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.count === 0) {
|
|
|
|
|
const order = await prisma.order.findUnique({ where: { id: orderId } });
|
|
|
|
|
if (!order) throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
|
|
|
|
if (order.userId !== userId) throw new OrderError('FORBIDDEN', 'Forbidden', 403);
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Order cannot be cancelled', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId,
|
|
|
|
|
action: 'ORDER_CANCELLED',
|
|
|
|
|
detail: 'User cancelled order',
|
|
|
|
|
operator: `user:${userId}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function adminCancelOrder(orderId: string): Promise<void> {
|
|
|
|
|
const result = await prisma.order.updateMany({
|
|
|
|
|
where: { id: orderId, status: 'PENDING' },
|
|
|
|
|
data: { status: 'CANCELLED', updatedAt: new Date() },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.count === 0) {
|
|
|
|
|
const order = await prisma.order.findUnique({ where: { id: orderId } });
|
|
|
|
|
if (!order) throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Order cannot be cancelled', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId,
|
|
|
|
|
action: 'ORDER_CANCELLED',
|
|
|
|
|
detail: 'Admin cancelled order',
|
|
|
|
|
operator: 'admin',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function handlePaymentNotify(params: EasyPayNotifyParams): Promise<boolean> {
|
|
|
|
|
const env = getEnv();
|
|
|
|
|
|
|
|
|
|
const { sign, ...rest } = params;
|
|
|
|
|
const paramsForSign: Record<string, string> = {};
|
|
|
|
|
for (const [key, value] of Object.entries(rest)) {
|
|
|
|
|
if (value !== undefined && value !== null) {
|
|
|
|
|
paramsForSign[key] = String(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!verifySign(paramsForSign, env.EASY_PAY_PKEY, sign)) {
|
|
|
|
|
console.error('EasyPay notify: invalid signature');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (params.trade_status !== 'TRADE_SUCCESS') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const order = await prisma.order.findUnique({
|
|
|
|
|
where: { id: params.out_trade_no },
|
|
|
|
|
});
|
|
|
|
|
if (!order) {
|
|
|
|
|
console.error('EasyPay notify: order not found:', params.out_trade_no);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let paidAmount: Prisma.Decimal;
|
|
|
|
|
try {
|
|
|
|
|
paidAmount = new Prisma.Decimal(params.money);
|
|
|
|
|
} catch {
|
|
|
|
|
console.error('EasyPay notify: invalid money format:', params.money);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (paidAmount.lte(0)) {
|
|
|
|
|
console.error('EasyPay notify: non-positive money:', params.money);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!paidAmount.equals(order.amount)) {
|
|
|
|
|
console.warn('EasyPay notify: amount changed, use paid amount', order.amount.toString(), params.money);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await prisma.order.updateMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: order.id,
|
|
|
|
|
status: { in: ['PENDING', 'EXPIRED'] },
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
status: 'PAID',
|
|
|
|
|
amount: paidAmount,
|
|
|
|
|
zpayTradeNo: params.trade_no,
|
|
|
|
|
paidAt: new Date(),
|
|
|
|
|
failedAt: null,
|
|
|
|
|
failedReason: null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.count === 0) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
action: 'ORDER_PAID',
|
|
|
|
|
detail: JSON.stringify({
|
|
|
|
|
previous_status: order.status,
|
|
|
|
|
trade_no: params.trade_no,
|
|
|
|
|
expected_amount: order.amount.toString(),
|
|
|
|
|
paid_amount: paidAmount.toString(),
|
|
|
|
|
}),
|
|
|
|
|
operator: 'easy-pay',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Recharge inline to avoid "paid but still recharging" async gaps.
|
|
|
|
|
await executeRecharge(order.id);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Payment has been confirmed, always ack notify to avoid endless retries from gateway.
|
|
|
|
|
console.error('Recharge failed for order:', order.id, err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function executeRecharge(orderId: string): Promise<void> {
|
|
|
|
|
const order = await prisma.order.findUnique({ where: { id: orderId } });
|
|
|
|
|
if (!order) {
|
|
|
|
|
throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
|
|
|
|
}
|
|
|
|
|
if (order.status === 'COMPLETED') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (isRefundStatus(order.status)) {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Refund-related order cannot recharge', 400);
|
|
|
|
|
}
|
|
|
|
|
if (order.status !== 'PAID' && order.status !== 'FAILED') {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', `Order cannot recharge in status ${order.status}`, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await createAndRedeem(
|
|
|
|
|
order.rechargeCode,
|
|
|
|
|
Number(order.amount),
|
|
|
|
|
order.userId,
|
|
|
|
|
`sub2apipay recharge order:${orderId}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await prisma.order.update({
|
|
|
|
|
where: { id: orderId },
|
|
|
|
|
data: { status: 'COMPLETED', completedAt: new Date() },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId,
|
|
|
|
|
action: 'RECHARGE_SUCCESS',
|
|
|
|
|
detail: JSON.stringify({ rechargeCode: order.rechargeCode, amount: Number(order.amount) }),
|
|
|
|
|
operator: 'system',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await prisma.order.update({
|
|
|
|
|
where: { id: orderId },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'FAILED',
|
|
|
|
|
failedAt: new Date(),
|
|
|
|
|
failedReason: error instanceof Error ? error.message : String(error),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId,
|
|
|
|
|
action: 'RECHARGE_FAILED',
|
|
|
|
|
detail: error instanceof Error ? error.message : String(error),
|
|
|
|
|
operator: 'system',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertRetryAllowed(order: { status: string; paidAt: Date | null }): void {
|
|
|
|
|
if (!order.paidAt) {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Order is not paid, retry denied', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isRefundStatus(order.status)) {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Refund-related order cannot retry', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (order.status === 'FAILED' || order.status === 'PAID') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (order.status === 'RECHARGING') {
|
|
|
|
|
throw new OrderError('CONFLICT', 'Order is recharging, retry later', 409);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (order.status === 'COMPLETED') {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Order already completed', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Only paid and failed orders can retry', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function retryRecharge(orderId: string): Promise<void> {
|
|
|
|
|
const order = await prisma.order.findUnique({
|
|
|
|
|
where: { id: orderId },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
status: true,
|
|
|
|
|
paidAt: true,
|
|
|
|
|
completedAt: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!order) {
|
|
|
|
|
throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assertRetryAllowed(order);
|
|
|
|
|
|
|
|
|
|
const result = await prisma.order.updateMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: orderId,
|
|
|
|
|
status: { in: ['FAILED', 'PAID'] },
|
|
|
|
|
paidAt: { not: null },
|
|
|
|
|
},
|
|
|
|
|
data: { status: 'PAID', failedAt: null, failedReason: null },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.count === 0) {
|
|
|
|
|
const latest = await prisma.order.findUnique({
|
|
|
|
|
where: { id: orderId },
|
|
|
|
|
select: {
|
|
|
|
|
status: true,
|
|
|
|
|
paidAt: true,
|
|
|
|
|
completedAt: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!latest) {
|
|
|
|
|
throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const derived = deriveOrderState(latest);
|
|
|
|
|
if (derived.rechargeStatus === 'recharging' || latest.status === 'PAID') {
|
|
|
|
|
throw new OrderError('CONFLICT', 'Order is recharging, retry later', 409);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (derived.rechargeStatus === 'success') {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Order already completed', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isRefundStatus(latest.status)) {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Refund-related order cannot retry', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new OrderError('CONFLICT', 'Order status changed, refresh and retry', 409);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId,
|
|
|
|
|
action: 'RECHARGE_RETRY',
|
|
|
|
|
detail: 'Admin manual retry recharge',
|
|
|
|
|
operator: 'admin',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await executeRecharge(orderId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RefundInput {
|
|
|
|
|
orderId: string;
|
|
|
|
|
reason?: string;
|
|
|
|
|
force?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RefundResult {
|
|
|
|
|
success: boolean;
|
|
|
|
|
warning?: string;
|
|
|
|
|
requireForce?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function processRefund(input: RefundInput): Promise<RefundResult> {
|
|
|
|
|
const order = await prisma.order.findUnique({ where: { id: input.orderId } });
|
|
|
|
|
if (!order) throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
|
|
|
|
if (order.status !== 'COMPLETED') {
|
|
|
|
|
throw new OrderError('INVALID_STATUS', 'Only completed orders can be refunded', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const amount = Number(order.amount);
|
|
|
|
|
|
|
|
|
|
if (!input.force) {
|
|
|
|
|
try {
|
|
|
|
|
const user = await getUser(order.userId);
|
|
|
|
|
if (user.balance < amount) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
warning: `User balance ${user.balance} is lower than refund ${amount}`,
|
|
|
|
|
requireForce: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
warning: 'Cannot fetch user balance, use force=true',
|
|
|
|
|
requireForce: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lockResult = await prisma.order.updateMany({
|
|
|
|
|
where: { id: input.orderId, status: 'COMPLETED' },
|
|
|
|
|
data: { status: 'REFUNDING' },
|
|
|
|
|
});
|
|
|
|
|
if (lockResult.count === 0) {
|
|
|
|
|
throw new OrderError('CONFLICT', 'Order status changed, refresh and retry', 409);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (order.zpayTradeNo) {
|
|
|
|
|
await easyPayRefund(order.zpayTradeNo, order.id, amount.toFixed(2));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await subtractBalance(
|
|
|
|
|
order.userId,
|
|
|
|
|
amount,
|
|
|
|
|
`sub2apipay refund order:${order.id}`,
|
|
|
|
|
`sub2apipay:refund:${order.id}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await prisma.order.update({
|
|
|
|
|
where: { id: input.orderId },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'REFUNDED',
|
|
|
|
|
refundAmount: new Prisma.Decimal(amount.toFixed(2)),
|
|
|
|
|
refundReason: input.reason || null,
|
|
|
|
|
refundAt: new Date(),
|
|
|
|
|
forceRefund: input.force || false,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId: input.orderId,
|
|
|
|
|
action: 'REFUND_SUCCESS',
|
|
|
|
|
detail: JSON.stringify({ amount, reason: input.reason, force: input.force }),
|
|
|
|
|
operator: 'admin',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { success: true };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await prisma.order.update({
|
|
|
|
|
where: { id: input.orderId },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'REFUND_FAILED',
|
|
|
|
|
failedAt: new Date(),
|
|
|
|
|
failedReason: error instanceof Error ? error.message : String(error),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
orderId: input.orderId,
|
|
|
|
|
action: 'REFUND_FAILED',
|
|
|
|
|
detail: error instanceof Error ? error.message : String(error),
|
|
|
|
|
operator: 'admin',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class OrderError extends Error {
|
|
|
|
|
code: string;
|
|
|
|
|
statusCode: number;
|
|
|
|
|
|
|
|
|
|
constructor(code: string, message: string, statusCode: number = 400) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = 'OrderError';
|
|
|
|
|
this.code = code;
|
|
|
|
|
this.statusCode = statusCode;
|
|
|
|
|
}
|
|
|
|
|
}
|