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; } }