canina/backend/src/orders/orders.service.ts
parsa aghaei 62bd8811fa
All checks were successful
Deploy Canina / deploy (push) Successful in 4m58s
style: apply standard ESLint & Prettier formatting across backend
2026-07-29 15:41:53 +03:30

253 lines
7.3 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 { Decimal } from '@prisma/client/runtime/library';
@Injectable()
export class OrdersService {
constructor(private prisma: PrismaService) {}
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_NOT_FOR_USER',
});
}
let discountValue: number;
if (coupon.type === 'percent') {
discountValue = cartTotal * (Number(coupon.value) / 100);
if (coupon.maxCartValue) {
discountValue = Math.min(discountValue, Number(coupon.maxCartValue));
}
} else {
discountValue = Number(coupon.value);
}
discountValue = Math.min(discountValue, cartTotal);
return {
valid: true,
couponId: coupon.id,
code: coupon.code,
type: coupon.type,
discountValue,
message: `کد تخفیف اعمال شد — ${discountValue.toLocaleString('fa-IR')} تومان تخفیف`,
};
}
async create(userId: string, createOrderDto: CreateOrderDto) {
let totalAmount = 0;
let couponId: string | null = null;
const orderItems = [];
for (const item of createOrderDto.items) {
const product = await this.prisma.product.findUnique({
where: { id: item.productId },
});
if (!product) {
throw new NotFoundException(
`محصول با شناسه ${item.productId} یافت نشد`,
);
}
totalAmount += Number(product.priceValue) * item.quantity;
orderItems.push({
productId: product.id,
quantity: item.quantity,
});
}
if (orderItems.length === 0) {
throw new BadRequestException('سبد خرید خالی است');
}
// Apply coupon if provided
let discountAmount = 0;
if (createOrderDto.couponCode) {
try {
const couponResult = await this.validateCoupon(
createOrderDto.couponCode,
totalAmount,
userId,
);
discountAmount = couponResult.discountValue;
couponId = couponResult.couponId;
// Increment usedCount
await this.prisma.coupon.update({
where: { id: couponId },
data: { usedCount: { increment: 1 } },
});
} catch {
// Invalid coupon — ignore and proceed without discount
}
}
const charityAmount = Number(createOrderDto.charityDonation || 0);
const finalAmount = Math.max(
totalAmount - discountAmount + charityAmount,
0,
);
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}`,
},
});
}
// Increment user charity total if charity donation was added
if (charityAmount > 0 && userId) {
try {
await this.prisma.user.update({
where: { id: userId },
data: { charityDonationTotal: { increment: charityAmount } },
});
} catch {
// Ignore if user not found or guest
}
}
return 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 },
},
},
});
}
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;
}
}