279 lines
8.2 KiB
TypeScript
279 lines
8.2 KiB
TypeScript
import {
|
|
Injectable,
|
|
NotFoundException,
|
|
BadRequestException,
|
|
} from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { PaginationDto } from '../common/dto/pagination.dto';
|
|
import { CreateOrderDto } from './dto/create-order.dto';
|
|
import { SmsService } from '../common/services/sms.service';
|
|
|
|
@Injectable()
|
|
export class OrdersService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private smsService: SmsService,
|
|
) {}
|
|
|
|
private generateTrackingNumber(): string {
|
|
const date = new Date();
|
|
const dateStr = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`;
|
|
const random = Math.floor(10000 + Math.random() * 90000);
|
|
return `CN-${dateStr}-${random}`;
|
|
}
|
|
|
|
async validateCoupon(code: string, cartTotal: number, userId: string) {
|
|
const coupon = await this.prisma.coupon.findUnique({
|
|
where: { code: code.toUpperCase().trim() },
|
|
include: { targets: true },
|
|
});
|
|
|
|
if (!coupon || !coupon.isActive) {
|
|
throw new BadRequestException({
|
|
message: 'کد تخفیف معتبر نیست یا منقضی شده است',
|
|
error: 'COUPON_INVALID',
|
|
});
|
|
}
|
|
|
|
if (coupon.expiresAt && new Date() > coupon.expiresAt) {
|
|
throw new BadRequestException({
|
|
message: 'کد تخفیف منقضی شده است',
|
|
error: 'COUPON_EXPIRED',
|
|
});
|
|
}
|
|
|
|
if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) {
|
|
throw new BadRequestException({
|
|
message: 'ظرفیت استفاده از این کد تخفیف تکمیل شده است',
|
|
error: 'COUPON_LIMIT_REACHED',
|
|
});
|
|
}
|
|
|
|
if (coupon.minCartValue && cartTotal < Number(coupon.minCartValue)) {
|
|
throw new BadRequestException({
|
|
message: `حداقل مبلغ سبد خرید برای استفاده از این کد ${Number(coupon.minCartValue).toLocaleString('fa-IR')} تومان است`,
|
|
error: 'COUPON_MIN_CART',
|
|
});
|
|
}
|
|
|
|
// Check user-specific targets
|
|
const userTargets = coupon.targets.filter((t) => t.targetType === 'USER');
|
|
if (
|
|
userTargets.length > 0 &&
|
|
!userTargets.some((t) => t.targetId === userId)
|
|
) {
|
|
throw new BadRequestException({
|
|
message: 'این کد تخفیف برای حساب کاربری شما فعال نیست',
|
|
error: 'COUPON_USER_MISMATCH',
|
|
});
|
|
}
|
|
|
|
let discountAmount = 0;
|
|
if (coupon.type === 'percent' || coupon.type === 'PERCENTAGE') {
|
|
discountAmount = (cartTotal * Number(coupon.value)) / 100;
|
|
if (coupon.maxCartValue && discountAmount > Number(coupon.maxCartValue)) {
|
|
discountAmount = Number(coupon.maxCartValue);
|
|
}
|
|
} else {
|
|
discountAmount = Number(coupon.value);
|
|
}
|
|
|
|
return {
|
|
couponId: coupon.id,
|
|
code: coupon.code,
|
|
discountAmount: Math.min(discountAmount, cartTotal),
|
|
};
|
|
}
|
|
|
|
async create(userId: string, createOrderDto: CreateOrderDto) {
|
|
if (!createOrderDto.items || createOrderDto.items.length === 0) {
|
|
throw new BadRequestException('سبد خرید خالی است');
|
|
}
|
|
|
|
// Verify stock and build items
|
|
let cartTotal = 0;
|
|
const orderItems: Array<{ productId: string; quantity: number; unitPrice: number; totalPrice: number }> = [];
|
|
|
|
for (const item of createOrderDto.items) {
|
|
const product = await this.prisma.product.findUnique({
|
|
where: { id: item.productId },
|
|
});
|
|
if (!product) {
|
|
throw new NotFoundException(`محصول یافت نشد`);
|
|
}
|
|
const itemPrice = Number(product.priceValue);
|
|
const totalItemPrice = itemPrice * item.quantity;
|
|
cartTotal += totalItemPrice;
|
|
orderItems.push({
|
|
productId: item.productId,
|
|
quantity: item.quantity,
|
|
unitPrice: itemPrice,
|
|
totalPrice: totalItemPrice,
|
|
});
|
|
}
|
|
|
|
let discountAmount = 0;
|
|
let couponId: string | undefined = undefined;
|
|
|
|
if (createOrderDto.couponCode) {
|
|
const couponResult = await this.validateCoupon(
|
|
createOrderDto.couponCode,
|
|
cartTotal,
|
|
userId,
|
|
);
|
|
discountAmount = couponResult.discountAmount;
|
|
couponId = couponResult.couponId;
|
|
}
|
|
|
|
const charityAmount = createOrderDto.charityDonation || 0;
|
|
const finalAmount = Math.max(0, cartTotal - discountAmount) + charityAmount;
|
|
|
|
const trackingNumber = this.generateTrackingNumber();
|
|
|
|
// Deduct user wallet balance if payment method is wallet
|
|
if (createOrderDto.paymentMethod === 'wallet' && userId) {
|
|
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
|
if (!user) {
|
|
throw new NotFoundException('کاربر یافت نشد');
|
|
}
|
|
const userBalance = Number(user.walletBalance || 0);
|
|
if (userBalance < finalAmount) {
|
|
throw new BadRequestException(
|
|
'موجودی کیف پول برای پرداخت این سفارش کافی نیست',
|
|
);
|
|
}
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
walletBalance: { decrement: finalAmount },
|
|
},
|
|
});
|
|
await this.prisma.walletTransaction.create({
|
|
data: {
|
|
userId,
|
|
amount: finalAmount,
|
|
type: 'withdrawal',
|
|
status: 'completed',
|
|
description: `پرداخت سفارش ${trackingNumber}`,
|
|
},
|
|
});
|
|
}
|
|
|
|
const createdOrder = await this.prisma.order.create({
|
|
data: {
|
|
userId,
|
|
couponId,
|
|
totalAmount: finalAmount,
|
|
charityDonation: charityAmount,
|
|
isRefill: Boolean(createOrderDto.isRefill),
|
|
refillIntervalDays: createOrderDto.refillIntervalDays || 60,
|
|
trackingNumber,
|
|
status: 'processing',
|
|
orderItems: {
|
|
create: orderItems,
|
|
},
|
|
} as any,
|
|
include: {
|
|
orderItems: {
|
|
include: { product: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
// Send Order Confirmation SMS
|
|
if (userId) {
|
|
this.prisma.user.findUnique({ where: { id: userId } }).then((user) => {
|
|
if (user && user.mobile) {
|
|
this.smsService.sendOrderConfirmation(
|
|
user.mobile,
|
|
trackingNumber,
|
|
finalAmount.toLocaleString('fa-IR'),
|
|
).catch(() => {});
|
|
}
|
|
}).catch(() => {});
|
|
}
|
|
|
|
return createdOrder;
|
|
}
|
|
|
|
async updateStatus(orderId: string, status: string, trackingCode?: string) {
|
|
const order = await this.prisma.order.findUnique({
|
|
where: { id: orderId },
|
|
include: { user: true },
|
|
});
|
|
|
|
if (!order) {
|
|
throw new NotFoundException('سفارش یافت نشد');
|
|
}
|
|
|
|
const updatedOrder = await this.prisma.order.update({
|
|
where: { id: orderId },
|
|
data: {
|
|
status,
|
|
...(trackingCode ? { trackingNumber: trackingCode } : {}),
|
|
},
|
|
include: { user: true },
|
|
});
|
|
|
|
// Send Shipping SMS with Tracking Code
|
|
const mobile = updatedOrder.user?.mobile;
|
|
const trackingNum = updatedOrder.trackingNumber || trackingCode || '';
|
|
if (status === 'shipped' && mobile && trackingCode) {
|
|
this.smsService.sendShippingNotification(
|
|
mobile,
|
|
trackingNum,
|
|
trackingCode,
|
|
).catch(() => {});
|
|
}
|
|
|
|
return updatedOrder;
|
|
}
|
|
|
|
async findAllByUser(userId: string, filters: PaginationDto) {
|
|
const {
|
|
page = 1,
|
|
limit = 10,
|
|
sortBy = 'createdAt',
|
|
sortOrder = 'desc',
|
|
} = filters;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.order.findMany({
|
|
where: { userId },
|
|
skip,
|
|
take: limit,
|
|
orderBy: { [sortBy]: sortOrder },
|
|
include: {
|
|
orderItems: { include: { product: true } },
|
|
},
|
|
}),
|
|
this.prisma.order.count({ where: { userId } }),
|
|
]);
|
|
|
|
return {
|
|
data,
|
|
meta: {
|
|
total,
|
|
page,
|
|
lastPage: Math.ceil(total / limit),
|
|
limit,
|
|
},
|
|
};
|
|
}
|
|
|
|
async findOne(id: string, userId: string) {
|
|
const order = await this.prisma.order.findFirst({
|
|
where: { id, userId },
|
|
include: {
|
|
orderItems: { include: { product: true } },
|
|
},
|
|
});
|
|
|
|
if (!order) {
|
|
throw new NotFoundException('سفارش یافت نشد');
|
|
}
|
|
return order;
|
|
}
|
|
}
|