canina/backend/src/orders/orders.service.ts
parsa aghaei a3291cc468
Some checks failed
Deploy Canina / deploy (push) Has been cancelled
feat(agency): implement Farsi error handling, trust seals page, and MeliPayamak SMS integration
2026-07-29 16:40:42 +03:30

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