"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OrdersService = void 0; const common_1 = require("@nestjs/common"); const client_1 = require("@prisma/client"); const prisma_service_1 = require("../prisma/prisma.service"); const order_number_1 = require("../common/utils/order-number"); const check_rass_1 = require("../common/utils/check-rass"); const cash_expiry_1 = require("../common/utils/cash-expiry"); const order_status_1 = require("../common/utils/order-status"); const name_1 = require("../common/utils/name"); const phone_1 = require("../common/utils/phone"); const orderInclude = { items: true, checks: { orderBy: { row: 'asc' } }, cashReceipt: true, statusHistory: { orderBy: { changedAt: 'asc' } }, shipment: true, user: true, company: true, }; function roundMoney(value) { return Math.round(value * 100) / 100; } const CHECK_EDITABLE_STATUSES = [ client_1.OrderStatus.awaiting_review, client_1.OrderStatus.pricing, client_1.OrderStatus.needs_correction, client_1.OrderStatus.awaiting_settlement_fix, client_1.OrderStatus.check_in_progress, client_1.OrderStatus.awaiting_check_review, client_1.OrderStatus.check_needs_fix, client_1.OrderStatus.check_rejected, ]; let OrdersService = class OrdersService { prisma; constructor(prisma) { this.prisma = prisma; } async findMyOrders(userId) { const orders = await this.prisma.order.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, include: orderInclude, }); return orders.map((o) => this.mapOrder(o)); } async findMyOrder(userId, id) { const order = await this.getOrder(id); if (order.userId !== userId) { throw new common_1.ForbiddenException('دسترسی به این سفارش مجاز نیست'); } return this.mapOrder(order); } async findAdminOrders(filters) { const statuses = (0, order_status_1.resolveAdminStatusFilter)(filters.status); const q = filters.q?.trim(); const orders = await this.prisma.order.findMany({ where: { ...(statuses ? { status: { in: statuses } } : {}), ...(q ? { OR: [ { orderNumber: { contains: q, mode: 'insensitive' } }, { companyName: { contains: q, mode: 'insensitive' } }, { user: { OR: [ { firstName: { contains: q, mode: 'insensitive' } }, { lastName: { contains: q, mode: 'insensitive' } }, { phone: { contains: (0, phone_1.toEnglishDigits)(q) } }, ], }, }, ], } : {}), }, orderBy: { createdAt: 'desc' }, include: orderInclude, }); return orders.map((o) => this.mapOrder(o, true)); } async findAdminOrder(id) { const order = await this.getOrder(id); return this.mapOrder(order, true); } async submitOrder(userId, dto) { const company = await this.prisma.company.findUnique({ where: { id: dto.companyId }, }); if (!company || !company.isActive) { throw new common_1.NotFoundException('شرکت یافت نشد یا غیرفعال است'); } const productIds = dto.items.map((i) => i.productId); const products = await this.prisma.product.findMany({ where: { id: { in: productIds }, companyId: company.id, isActive: true }, include: { catalog: true }, }); if (products.length !== productIds.length) { throw new common_1.BadRequestException('یک یا چند محصول نامعتبر است'); } const productMap = new Map(products.map((p) => [p.id, p])); let totalMeterage = 0; const itemsData = dto.items.map((item) => { const product = productMap.get(item.productId); if (item.meterage < product.minMeterage) { throw new common_1.BadRequestException(`حداقل متراژ برای ${product.name} برابر ${product.minMeterage} است`); } const rem = Math.round((item.meterage / product.orderMultiplier) * 1000) / 1000; if (Math.abs(rem - Math.round(rem)) > 1e-6) { throw new common_1.BadRequestException(`متراژ ${product.name} باید مضرب ${product.orderMultiplier} باشد`); } totalMeterage += item.meterage; return { productId: product.id, catalogCode: product.catalog.code, productName: product.name, productCode: product.code, color: product.color, requestedMeterage: item.meterage, unitPrice: product.basePrice, notes: item.notes?.trim() || null, }; }); if (totalMeterage > company.maxMeteragePerInvoice) { throw new common_1.BadRequestException(`حداکثر متراژ مجاز هر فاکتور ${company.maxMeteragePerInvoice} متر است`); } const settlementType = dto.settlementType; if (settlementType === client_1.SettlementType.check) { if (!dto.checks?.length) { throw new common_1.BadRequestException('برای تسویه چکی حداقل یک چک لازم است'); } } const checkMode = settlementType === client_1.SettlementType.check ? (dto.checkMode ?? client_1.CheckRegistrationMode.full) : null; const rass = settlementType === client_1.SettlementType.check && dto.checks?.length ? (0, check_rass_1.calculateCheckRass)(dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate, }))) : null; const cashInvoiceExpiresAt = settlementType === client_1.SettlementType.cash ? (0, cash_expiry_1.computeCashInvoiceExpiry)(company) : null; const order = await this.createWithUniqueOrderNumber((orderNumber) => this.prisma.$transaction(async (tx) => { const created = await tx.order.create({ data: { orderNumber, userId, companyId: company.id, companyName: company.name, settlementType, checkMode, status: client_1.OrderStatus.awaiting_review, totalMeterage, notes: dto.notes?.trim() || null, checkRassDays: rass?.rassDays ?? null, checkRassDate: rass?.rassDate ? new Date(rass.rassDate) : null, cashInvoiceExpiresAt, items: { create: itemsData }, checks: settlementType === client_1.SettlementType.check && dto.checks ? { create: dto.checks.map((c, index) => this.mapCheckCreate(c, index + 1, checkMode)), } : undefined, statusHistory: { create: [ { fromStatus: null, toStatus: client_1.OrderStatus.submitted, changedBy: 'customer', note: 'سفارش ثبت شد', }, { fromStatus: client_1.OrderStatus.submitted, toStatus: client_1.OrderStatus.awaiting_review, changedBy: 'customer', note: 'ارسال برای بررسی مدیریت', }, ], }, }, include: orderInclude, }); const cart = await tx.cart.findFirst({ where: { userId } }); if (cart) { await tx.cartItem.deleteMany({ where: { cartId: cart.id, companyId: company.id }, }); } return created; })); return this.mapOrder(order); } async adminCreate(adminId, dto) { const settlementType = dto.settlementType ?? dto.settlementMethod; if (!settlementType) { throw new common_1.BadRequestException('نوع تسویه الزامی است'); } const company = await this.prisma.company.findUnique({ where: { id: dto.companyId }, }); if (!company) throw new common_1.NotFoundException('شرکت یافت نشد'); let userId = dto.userId; if (!userId && dto.customerPhone) { const phone = (0, phone_1.normalizePhone)((0, phone_1.toEnglishDigits)(dto.customerPhone)); if (!(0, phone_1.isValidPhone)(phone)) { throw new common_1.BadRequestException('شماره موبایل نامعتبر است'); } let customer = await this.prisma.customer.findUnique({ where: { phone }, }); if (!customer) { const names = (0, name_1.splitFullName)(dto.customerName || 'مشتری جدید'); customer = await this.prisma.customer.create({ data: { phone, firstName: names.firstName, lastName: names.lastName || 'جدید', }, }); } userId = customer.id; } if (!userId) { throw new common_1.BadRequestException('شناسه یا شماره مشتری الزامی است'); } const totalMeterage = dto.meterage ?? 0; const ownerId = userId; const order = await this.createWithUniqueOrderNumber((orderNumber) => this.prisma.order.create({ data: { orderNumber, userId: ownerId, companyId: company.id, companyName: company.name, settlementType, status: client_1.OrderStatus.awaiting_review, totalMeterage, notes: dto.notes?.trim() || null, statusHistory: { create: { fromStatus: null, toStatus: client_1.OrderStatus.awaiting_review, changedBy: `admin:${adminId}`, note: 'سفارش توسط ادمین ثبت شد', }, }, }, include: orderInclude, })); return this.mapOrder(order, true); } async updatePricing(id, adminId, dto) { const order = await this.getOrder(id); const unknown = dto.items.find((item) => !order.items.some((i) => i.id === item.id)); if (unknown) { throw new common_1.BadRequestException(`آیتم ${unknown.id} یافت نشد`); } await this.prisma.$transaction(async (tx) => { let totalAmount = 0; let totalMeterage = 0; for (const existing of order.items) { const item = dto.items.find((i) => i.id === existing.id); const unitPrice = item?.unitPrice !== undefined ? item.unitPrice : item?.supplyPrice !== undefined ? item.supplyPrice : (existing.unitPrice ?? 0); const unitDiscount = item?.unitDiscount ?? existing.unitDiscount ?? 0; if (unitDiscount > unitPrice) { throw new common_1.BadRequestException('تخفیف واحد نمی‌تواند از قیمت واحد بیشتر باشد'); } const confirmed = item?.confirmedMeterage ?? existing.confirmedMeterage ?? existing.requestedMeterage; const lineTotal = roundMoney(Math.max(0, (unitPrice - unitDiscount) * confirmed)); totalAmount += lineTotal; totalMeterage += confirmed; if (!item) continue; await tx.orderItem.update({ where: { id: existing.id }, data: { confirmedMeterage: confirmed, unitPrice, unitDiscount, lineTotal, }, }); } totalAmount = roundMoney(totalAmount); const shippingCost = dto.shippingCost ?? order.shippingCost ?? 0; const totalDiscount = dto.totalDiscount ?? order.totalDiscount ?? 0; const finalAmount = roundMoney(Math.max(0, totalAmount + shippingCost - totalDiscount)); const nextStatus = order.status === client_1.OrderStatus.awaiting_review || order.status === client_1.OrderStatus.pricing ? client_1.OrderStatus.pricing : order.status; await tx.order.update({ where: { id }, data: { totalAmount, totalMeterage, shippingCost, totalDiscount, finalAmount, status: nextStatus, }, }); if (nextStatus !== order.status) { await tx.orderStatusHistory.create({ data: { orderId: id, fromStatus: order.status, toStatus: nextStatus, changedBy: `admin:${adminId}`, note: dto.note || 'قیمت‌گذاری انجام شد', }, }); } }); return this.findAdminOrder(id); } async updateStatus(id, adminId, dto) { const order = await this.getOrder(id); if (order.status === dto.status) { return this.mapOrder(order, true); } await this.prisma.$transaction(async (tx) => { await tx.order.update({ where: { id }, data: { status: dto.status }, }); await tx.orderStatusHistory.create({ data: { orderId: id, fromStatus: order.status, toStatus: dto.status, changedBy: `admin:${adminId}`, note: dto.note || null, reason: dto.reason || null, }, }); if (dto.status === client_1.OrderStatus.cash_invoice_issued || dto.status === client_1.OrderStatus.awaiting_cash_payment) { if (!order.cashInvoiceExpiresAt) { await tx.order.update({ where: { id }, data: { cashInvoiceExpiresAt: (0, cash_expiry_1.computeCashInvoiceExpiry)(order.company), }, }); } } }); return this.findAdminOrder(id); } async updateChecks(userId, id, dto) { const order = await this.getOrder(id); if (order.userId !== userId) { throw new common_1.ForbiddenException('دسترسی به این سفارش مجاز نیست'); } if (order.settlementType !== client_1.SettlementType.check) { throw new common_1.BadRequestException('این سفارش تسویه چکی نیست'); } if (!CHECK_EDITABLE_STATUSES.includes(order.status)) { throw new common_1.BadRequestException('در وضعیت فعلی سفارش امکان ویرایش چک‌ها وجود ندارد'); } const checkMode = dto.checkMode ?? order.checkMode ?? client_1.CheckRegistrationMode.full; const rass = (0, check_rass_1.calculateCheckRass)(dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate })), order.createdAt); await this.prisma.$transaction(async (tx) => { await tx.check.deleteMany({ where: { orderId: id } }); await tx.check.createMany({ data: dto.checks.map((c, index) => ({ orderId: id, ...this.mapCheckCreate(c, index + 1, checkMode), })), }); await tx.order.update({ where: { id }, data: { checkMode, checkRassDays: rass.rassDays, checkRassDate: rass.rassDate ? new Date(rass.rassDate) : null, status: order.status === client_1.OrderStatus.check_needs_fix ? client_1.OrderStatus.awaiting_check_review : order.status, }, }); if (order.status === client_1.OrderStatus.check_needs_fix) { await tx.orderStatusHistory.create({ data: { orderId: id, fromStatus: order.status, toStatus: client_1.OrderStatus.awaiting_check_review, changedBy: 'customer', note: 'چک‌ها اصلاح و مجدداً ارسال شد', }, }); } }); return this.findMyOrder(userId, id); } mapCheckCreate(c, row, defaultMode) { return { row, amount: c.amount, dueDate: new Date(c.dueDate), sayadId: c.sayadId?.trim() || null, checkNumber: c.checkNumber?.trim() || null, ownerNationalId: c.ownerNationalId?.trim() || null, ownerFirstName: c.ownerFirstName?.trim() || null, ownerLastName: c.ownerLastName?.trim() || null, bankName: c.bankName?.trim() || null, branchName: c.branchName?.trim() || null, frontImage: c.frontImage || null, backImage: c.backImage || null, notes: c.notes?.trim() || null, mode: c.mode ?? defaultMode, }; } async nextOrderNumber() { for (let i = 0; i < 5; i++) { const orderNumber = (0, order_number_1.generateOrderNumber)(); const exists = await this.prisma.order.findUnique({ where: { orderNumber }, }); if (!exists) return orderNumber; } return `${(0, order_number_1.generateOrderNumber)()}-${Date.now().toString().slice(-3)}`; } async createWithUniqueOrderNumber(create) { for (let attempt = 0;; attempt++) { const orderNumber = await this.nextOrderNumber(); try { return await create(orderNumber); } catch (e) { const isDuplicate = e instanceof client_1.Prisma.PrismaClientKnownRequestError && e.code === 'P2002'; if (!isDuplicate || attempt >= 4) throw e; } } } async getOrder(id) { const order = await this.prisma.order.findUnique({ where: { id }, include: orderInclude, }); if (!order) throw new common_1.NotFoundException('سفارش یافت نشد'); return order; } mapOrder(order, adminView = false) { const customerName = (0, name_1.joinFullName)(order.user.firstName, order.user.lastName); const base = { id: order.id, orderNumber: order.orderNumber, userId: order.userId, companyId: order.companyId, companyName: order.companyName, settlementType: order.settlementType, settlementMethod: order.settlementType, checkMode: order.checkMode ?? undefined, status: order.status, adminStatus: (0, order_status_1.toAdminStatus)(order.status), totalMeterage: order.totalMeterage, meterage: order.totalMeterage, totalAmount: order.totalAmount, shippingCost: order.shippingCost, totalDiscount: order.totalDiscount, finalAmount: order.finalAmount, notes: order.notes ?? undefined, checkRassDays: order.checkRassDays ?? undefined, checkRassDate: order.checkRassDate?.toISOString(), cashInvoiceExpiresAt: order.cashInvoiceExpiresAt?.toISOString(), createdAt: order.createdAt.toISOString(), updatedAt: order.updatedAt.toISOString(), customerName, customerPhone: order.user.phone, customerNationalId: order.user.nationalId ?? undefined, customerType: order.user.customerType, items: order.items.map((item) => ({ id: item.id, productId: item.productId ?? undefined, catalogCode: item.catalogCode, productName: item.productName, productCode: item.productCode ?? undefined, color: item.color, requestedMeterage: item.requestedMeterage, confirmedMeterage: item.confirmedMeterage, unitPrice: item.unitPrice, supplyPrice: item.unitPrice, unitDiscount: item.unitDiscount ?? 0, discount: item.unitDiscount ?? 0, lineTotal: item.lineTotal, totalAmount: item.lineTotal, notes: item.notes ?? undefined, })), checks: order.checks.map((c) => ({ id: c.id, orderId: c.orderId, row: c.row, amount: c.amount, dueDate: c.dueDate.toISOString(), sayadId: c.sayadId ?? undefined, checkNumber: c.checkNumber ?? undefined, ownerNationalId: c.ownerNationalId ?? undefined, ownerFirstName: c.ownerFirstName ?? undefined, ownerLastName: c.ownerLastName ?? undefined, bankName: c.bankName ?? '', branchName: c.branchName ?? undefined, frontImage: c.frontImage ?? undefined, backImage: c.backImage ?? undefined, notes: c.notes ?? undefined, mode: c.mode, status: c.status, createdAt: c.createdAt.toISOString(), updatedAt: c.updatedAt.toISOString(), })), cashReceipt: order.cashReceipt ? { id: order.cashReceipt.id, amount: order.cashReceipt.amount, paidAmount: order.cashReceipt.paidAmount, transferDate: order.cashReceipt.transferDate?.toISOString() ?? null, transferTime: order.cashReceipt.transferTime ?? undefined, trackingNumber: order.cashReceipt.trackingNumber ?? undefined, trackingCode: order.cashReceipt.trackingNumber ?? undefined, destinationAccountId: order.cashReceipt.destinationAccountId ?? undefined, receiptImage: order.cashReceipt.receiptImage ?? undefined, notes: order.cashReceipt.notes ?? undefined, expiresAt: order.cashReceipt.expiresAt?.toISOString() ?? order.cashInvoiceExpiresAt?.toISOString(), status: order.cashReceipt.status, submittedAt: order.cashReceipt.submittedAt?.toISOString(), createdAt: order.cashReceipt.createdAt.toISOString(), } : undefined, shipment: order.shipment ? { id: order.shipment.id, orderId: order.shipment.orderId, carrier: order.shipment.carrier, trackingNumber: order.shipment.trackingNumber ?? '', meterage: order.shipment.meterage, status: order.shipment.status, shippedAt: order.shipment.shippedAt?.toISOString() ?? null, deliveredAt: order.shipment.deliveredAt?.toISOString() ?? null, } : undefined, statusHistory: order.statusHistory.map((h) => ({ id: h.id, fromStatus: h.fromStatus, toStatus: h.toStatus, changedBy: h.changedBy, note: h.note ?? undefined, reason: h.reason ?? undefined, changedAt: h.changedAt.toISOString(), })), }; if (adminView) { return base; } return base; } }; exports.OrdersService = OrdersService; exports.OrdersService = OrdersService = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], OrdersService); //# sourceMappingURL=orders.service.js.map