"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 order_transitions_1 = require("../common/utils/order-transitions"); const meterage_1 = require("../common/utils/meterage"); const notifications_service_1 = require("../notifications/notifications.service"); const notification_events_1 = require("../notifications/notification-events"); 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.check_in_progress, client_1.OrderStatus.awaiting_check_review, client_1.OrderStatus.check_needs_fix, client_1.OrderStatus.check_rejected, ]; const PRICE_HIDDEN_STATUSES = [ client_1.OrderStatus.awaiting_review, client_1.OrderStatus.pricing, ]; let OrdersService = class OrdersService { prisma; notifications; constructor(prisma, notifications) { this.prisma = prisma; this.notifications = notifications; } 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 settings = await this.prisma.systemSettings.findUnique({ where: { id: 'default' }, select: { maxMeteragePerInvoice: true }, }); const productIds = dto.items.map((i) => i.productId); const uniqueProductIds = [...new Set(productIds)]; if (uniqueProductIds.length !== productIds.length) { throw new common_1.BadRequestException('محصول تکراری در سفارش مجاز نیست'); } const products = await this.prisma.product.findMany({ where: { id: { in: uniqueProductIds } }, include: { catalog: true, company: true }, }); const productMap = new Map(products.map((p) => [p.id, p])); for (const item of dto.items) { const product = productMap.get(item.productId); if (!product) { throw new common_1.BadRequestException('یک یا چند محصول نامعتبر است یا دیگر در سیستم وجود ندارد. سبد را خالی کنید و دوباره اضافه کنید.'); } if (!product.isActive) { throw new common_1.BadRequestException(`محصول «${product.name}» غیرفعال است و قابل سفارش نیست`); } if (!product.company?.isActive) { throw new common_1.BadRequestException(`شرکت محصول «${product.name}» غیرفعال است`); } } const meterageByCompany = new Map(); for (const item of dto.items) { const product = productMap.get(item.productId); const current = meterageByCompany.get(product.companyId); if (current) { current.meterage += item.meterage; } else { meterageByCompany.set(product.companyId, { name: product.company.name, meterage: item.meterage, max: product.company.maxMeteragePerInvoice, }); } } for (const row of meterageByCompany.values()) { const maxMeterageResolved = (0, meterage_1.resolveMaxMeterage)(row.max, settings?.maxMeteragePerInvoice); if (row.meterage > maxMeterageResolved) { throw new common_1.BadRequestException(`حداکثر متراژ مجاز فاکتور برای «${row.name}» برابر ${maxMeterageResolved} متر است`); } } 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, notes: item.notes?.trim() || null, companyId: product.companyId, companyName: product.company.name, }; }); const companyNames = [ ...new Set(itemsData.map((item) => item.companyName).filter(Boolean)), ]; const preferredCompanyId = itemsData.some((item) => item.companyId === dto.companyId) ? dto.companyId : itemsData[0].companyId; const primaryCompany = products.find((p) => p.companyId === preferredCompanyId) ?.company; if (!primaryCompany) { throw new common_1.NotFoundException('شرکت محصولات یافت نشد یا غیرفعال است'); } const order = await this.createWithUniqueOrderNumber((orderNumber) => this.prisma.$transaction(async (tx) => { const created = await tx.order.create({ data: { orderNumber, userId, companyId: primaryCompany.id, companyName: companyNames.join('، '), settlementType: null, checkMode: null, status: client_1.OrderStatus.awaiting_review, totalMeterage, notes: dto.notes?.trim() || null, items: { create: itemsData }, statusHistory: { create: [ { fromStatus: null, 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, productId: { in: uniqueProductIds }, }, }); const remaining = await tx.cartItem.findFirst({ where: { cartId: cart.id }, }); await tx.cart.update({ where: { id: cart.id }, data: { companyId: remaining?.companyId ?? null }, }); } return created; })); this.notifications.dispatchOrderEvent(order.id, 'order_submitted'); this.notifications.dispatchAdmin({ title: 'سفارش جدید ثبت شد', body: `سفارش ${order.orderNumber} با ${order.items.length} قلم و ${order.totalMeterage} متر در انتظار قیمت‌گذاری است.`, link: `/orders/${order.id}`, }); return this.mapOrder(order); } async chooseSettlement(userId, id, dto) { const order = await this.getOrder(id); if (order.userId !== userId) { throw new common_1.ForbiddenException('دسترسی به این سفارش مجاز نیست'); } if (order.status !== client_1.OrderStatus.awaiting_settlement) { throw new common_1.BadRequestException('انتخاب روش تسویه فقط پس از صدور فاکتور توسط مدیریت امکان‌پذیر است'); } const isCash = dto.settlementType === client_1.SettlementType.cash; const nextStatus = isCash ? client_1.OrderStatus.awaiting_cash_payment : client_1.OrderStatus.check_in_progress; (0, order_transitions_1.assertTransition)(order.status, nextStatus); const cashCompanies = isCash ? await this.companiesForOrder(order) : []; await this.prisma.$transaction(async (tx) => { await tx.order.update({ where: { id }, data: { settlementType: dto.settlementType, checkMode: isCash ? null : (dto.checkMode ?? client_1.CheckRegistrationMode.full), cashInvoiceExpiresAt: isCash ? (0, cash_expiry_1.earliestCashInvoiceExpiry)(cashCompanies) : null, cashReminderSentAt: null, status: nextStatus, }, }); await tx.orderStatusHistory.create({ data: { orderId: id, fromStatus: order.status, toStatus: nextStatus, changedBy: 'customer', note: isCash ? 'مشتری تسویه نقدی را انتخاب کرد' : 'مشتری تسویه چکی را انتخاب کرد', }, }); }); return this.findMyOrder(userId, id); } async adminCreate(adminId, dto) { const company = await this.prisma.company.findUnique({ where: { id: dto.companyId }, }); if (!company) throw new common_1.NotFoundException('شرکت یافت نشد'); let userId = dto.customerId || dto.userId; if (userId) { const existing = await this.prisma.customer.findUnique({ where: { id: userId }, }); if (!existing) { throw new common_1.BadRequestException('مشتری انتخاب‌شده یافت نشد'); } } else if (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: null, 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); } if (dto.force) { if (!dto.reason?.trim()) { throw new common_1.BadRequestException('برای تغییر دستی وضعیت، ذکر دلیل الزامی است'); } } else { (0, order_transitions_1.assertTransition)(order.status, dto.status); } if (dto.status === client_1.OrderStatus.awaiting_settlement && (order.finalAmount ?? 0) <= 0) { throw new common_1.BadRequestException('ابتدا سفارش را قیمت‌گذاری کنید تا فاکتور قابل ارسال باشد'); } const needsCashExpiry = (dto.status === client_1.OrderStatus.cash_invoice_issued || dto.status === client_1.OrderStatus.awaiting_cash_payment) && !order.cashInvoiceExpiresAt; const cashExpiry = needsCashExpiry ? (0, cash_expiry_1.earliestCashInvoiceExpiry)(await this.companiesForOrder(order)) : null; 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.force ? `تغییر دستی وضعیت${dto.note ? ` — ${dto.note}` : ''}` : dto.note || null, reason: dto.reason || null, }, }); if (cashExpiry) { await tx.order.update({ where: { id }, data: { cashInvoiceExpiresAt: cashExpiry }, }); } }); const event = (0, notification_events_1.eventForStatus)(dto.status); if (event) { this.notifications.dispatchOrderEvent(id, event, { note: dto.reason ?? dto.note ?? null, }); } 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('در وضعیت فعلی سفارش امکان ویرایش چک‌ها وجود ندارد'); } if (!dto.checks.length) { 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); const goesToReview = order.status === client_1.OrderStatus.check_in_progress || order.status === client_1.OrderStatus.check_needs_fix || order.status === client_1.OrderStatus.check_rejected; const nextStatus = goesToReview ? client_1.OrderStatus.awaiting_check_review : order.status; const isFix = order.status !== client_1.OrderStatus.check_in_progress; 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: nextStatus, }, }); if (nextStatus !== order.status) { await tx.orderStatusHistory.create({ data: { orderId: id, fromStatus: order.status, toStatus: nextStatus, changedBy: 'customer', note: isFix ? 'چک‌ها اصلاح و مجدداً ارسال شد' : 'چک‌ها برای بررسی ارسال شد', }, }); } }); if (nextStatus !== order.status) { this.notifications.dispatchAdmin({ title: isFix ? 'چک‌های اصلاح‌شده رسید' : 'چک‌های سفارش ثبت شد', body: `مشتری ${dto.checks.length} چک برای سفارش ${order.orderNumber} ثبت کرد و منتظر بررسی است.`, link: `/orders/${id}/checks`, }); } 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 companiesForOrder(order) { const companyIds = [ ...new Set([ order.companyId, ...order.items.map((item) => item.companyId).filter(Boolean), ].filter(Boolean)), ]; if (!companyIds.length) return order.company ? [order.company] : []; const companies = await this.prisma.company.findMany({ where: { id: { in: companyIds } }, }); return companies.length ? companies : order.company ? [order.company] : []; } 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 hidePrices = !adminView && PRICE_HIDDEN_STATUSES.includes(order.status); 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), allowedTransitions: adminView ? (0, order_transitions_1.allowedTransitions)(order.status) : [], totalMeterage: order.totalMeterage, meterage: order.totalMeterage, totalAmount: hidePrices ? null : order.totalAmount, shippingCost: hidePrices ? null : order.shippingCost, totalDiscount: hidePrices ? null : order.totalDiscount, finalAmount: hidePrices ? null : order.finalAmount, pricesVisible: !hidePrices, 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: hidePrices ? null : item.unitPrice, supplyPrice: hidePrices ? null : item.unitPrice, unitDiscount: hidePrices ? null : (item.unitDiscount ?? 0), discount: hidePrices ? null : (item.unitDiscount ?? 0), lineTotal: hidePrices ? null : item.lineTotal, totalAmount: hidePrices ? null : item.lineTotal, notes: item.notes ?? undefined, companyId: item.companyId, companyName: item.companyName, })), 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(), })), }; return base; } }; exports.OrdersService = OrdersService; exports.OrdersService = OrdersService = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [prisma_service_1.PrismaService, notifications_service_1.NotificationsService]) ], OrdersService); //# sourceMappingURL=orders.service.js.map