import { BadRequestException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { CheckRegistrationMode, OrderStatus, Prisma, SettlementType, } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { AdminCreateOrderDto, ChooseSettlementDto, SubmitOrderCheckDto, SubmitOrderDto, SubmitOrderItemDto, UpdateOrderChecksDto, UpdateOrderPricingDto, UpdateOrderStatusDto, } from './dto/order.dto'; import { generateOrderNumber } from '../common/utils/order-number'; import { calculateCheckRass } from '../common/utils/check-rass'; import { earliestCashInvoiceExpiry } from '../common/utils/cash-expiry'; import { resolveAdminStatusFilter, toAdminStatus, } from '../common/utils/order-status'; import { allowedTransitions, assertTransition, } from '../common/utils/order-transitions'; import { resolveMaxMeterage } from '../common/utils/meterage'; import { AgentLevelService } from '../agent/agent-level.service'; import { NotificationsService } from '../notifications/notifications.service'; import { eventForStatus } from '../notifications/notification-events'; import { joinFullName, splitFullName } from '../common/utils/name'; import { isValidPhone, normalizePhone, toEnglishDigits, } from '../common/utils/phone'; const orderInclude = { items: true, checks: { orderBy: { row: 'asc' as const } }, cashReceipt: true, statusHistory: { orderBy: { changedAt: 'asc' as const } }, shipment: true, user: true, company: true, } satisfies Prisma.OrderInclude; type OrderFull = Prisma.OrderGetPayload<{ include: typeof orderInclude }>; /** مبالغ ریالی روی Float ذخیره می‌شوند؛ گرد کردن از انباشت خطای اعشاری جلوگیری می‌کند. */ function roundMoney(value: number): number { return Math.round(value * 100) / 100; } /** بعد از تأیید چک‌ها یا ارسال سفارش، مشتری نباید بتواند چک‌ها را بازنویسی کند. */ const CHECK_EDITABLE_STATUSES: OrderStatus[] = [ OrderStatus.check_in_progress, OrderStatus.awaiting_check_review, OrderStatus.check_needs_fix, OrderStatus.check_rejected, ]; /** * تا وقتی ادمین فاکتور را نفرستاده، مشتری نباید هیچ مبلغی ببیند. * پنهان‌سازی روی خود پاسخ API انجام می‌شود، نه در کلاینت. */ const PRICE_HIDDEN_STATUSES: OrderStatus[] = [ OrderStatus.awaiting_review, OrderStatus.pricing, ]; @Injectable() export class OrdersService { constructor( private readonly prisma: PrismaService, private readonly notifications: NotificationsService, private readonly agentLevelService: AgentLevelService, ) {} async findMyOrders(userId: string) { const orders = await this.prisma.order.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, include: orderInclude, }); return orders.map((o) => this.mapOrder(o)); } async findMyOrder(userId: string, id: string) { const order = await this.getOrder(id); if (order.userId !== userId) { throw new ForbiddenException('دسترسی به این سفارش مجاز نیست'); } return this.mapOrder(order); } async findAdminOrders(filters: { status?: string; q?: string }) { const statuses = 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: toEnglishDigits(q) } }, ], }, }, ], } : {}), }, orderBy: { createdAt: 'desc' }, include: orderInclude, }); return orders.map((o) => this.mapOrder(o, true)); } async findAdminOrder(id: string) { const order = await this.getOrder(id); return this.mapOrder(order, true); } async submitOrder(userId: string, dto: SubmitOrderDto) { const { itemsData, totalMeterage, primaryCompany, companyNames, uniqueProductIds } = await this.resolveOrderLines(dto.items, dto.companyId); // روش تسویه و چک‌ها بعد از قیمت‌گذاری ادمین گرفته می‌شوند، نه اینجا. 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: OrderStatus.awaiting_review, totalMeterage, notes: dto.notes?.trim() || null, items: { create: itemsData }, statusHistory: { create: [ { fromStatus: null, toStatus: 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: string, id: string, dto: ChooseSettlementDto) { const order = await this.getOrder(id); if (order.userId !== userId) { throw new ForbiddenException('دسترسی به این سفارش مجاز نیست'); } if (order.status !== OrderStatus.awaiting_settlement) { throw new BadRequestException( 'انتخاب روش تسویه فقط پس از صدور فاکتور توسط مدیریت امکان‌پذیر است', ); } const isCash = dto.settlementType === SettlementType.cash; const nextStatus = isCash ? OrderStatus.awaiting_cash_payment : OrderStatus.check_in_progress; 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 ?? CheckRegistrationMode.full), cashInvoiceExpiresAt: isCash ? 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: string, dto: AdminCreateOrderDto) { if (!dto.items?.length) { throw new BadRequestException( 'حداقل یک محصول برای ثبت پیش‌سفارش لازم است', ); } let userId = dto.customerId || dto.userId; if (userId) { const existing = await this.prisma.customer.findUnique({ where: { id: userId }, }); if (!existing) { throw new BadRequestException('مشتری انتخاب‌شده یافت نشد'); } } else if (dto.customerPhone) { const phone = normalizePhone(toEnglishDigits(dto.customerPhone)); if (!isValidPhone(phone)) { throw new BadRequestException('شماره موبایل نامعتبر است'); } let customer = await this.prisma.customer.findUnique({ where: { phone }, }); if (!customer) { const names = splitFullName(dto.customerName || 'مشتری جدید'); customer = await this.prisma.customer.create({ data: { phone, firstName: names.firstName, lastName: names.lastName || 'جدید', }, }); } userId = customer.id; } if (!userId) { throw new BadRequestException('انتخاب مشتری الزامی است'); } const { itemsData, totalMeterage, primaryCompany, companyNames } = await this.resolveOrderLines(dto.items, dto.companyId); const ownerId = userId; const order = await this.createWithUniqueOrderNumber((orderNumber) => this.prisma.order.create({ data: { orderNumber, userId: ownerId, companyId: primaryCompany.id, companyName: companyNames.join('، '), settlementType: null, status: OrderStatus.awaiting_review, totalMeterage, notes: dto.notes?.trim() || null, items: { create: itemsData }, statusHistory: { create: { fromStatus: null, toStatus: OrderStatus.awaiting_review, changedBy: `admin:${adminId}`, note: 'پیش‌سفارش توسط ادمین ثبت شد', }, }, }, include: orderInclude, }), ); return this.mapOrder(order, true); } async updatePricing(id: string, adminId: string, dto: UpdateOrderPricingDto) { const order = await this.getOrder(id); const existingIds = new Set(order.items.map((item) => item.id)); const deletedIds = [...new Set(dto.deletedItemIds ?? [])]; for (const delId of deletedIds) { if (!existingIds.has(delId)) { throw new BadRequestException(`آیتم ${delId} یافت نشد`); } } const updates = dto.items.filter((item) => item.id && existingIds.has(item.id)); const creates = dto.items.filter( (item) => item.productId && (!item.id || !existingIds.has(item.id)), ); const unknown = dto.items.find( (item) => item.id && !existingIds.has(item.id) && !item.productId, ); if (unknown?.id) { throw new BadRequestException(`آیتم ${unknown.id} یافت نشد`); } const createdRows: Array<{ productId: string; catalogCode: string; productName: string; productCode: string; color: string; requestedMeterage: number; notes: string | null; companyId: string; companyName: string; confirmedMeterage: number; unitPrice: number; unitDiscount: number; lineTotal: number; }> = []; if (creates.length) { const resolved = await this.resolveOrderLines( creates.map((item) => ({ productId: item.productId!, meterage: item.requestedMeterage ?? item.confirmedMeterage ?? 0, })), order.companyId, ); for (const [index, row] of resolved.itemsData.entries()) { const src = creates[index]!; const unitPrice = src.unitPrice ?? src.supplyPrice ?? 0; const unitDiscount = src.unitDiscount ?? 0; if (unitDiscount > unitPrice) { throw new BadRequestException( 'تخفیف واحد نمی‌تواند از قیمت واحد بیشتر باشد', ); } const confirmed = src.confirmedMeterage ?? row.requestedMeterage; createdRows.push({ ...row, confirmedMeterage: confirmed, unitPrice, unitDiscount, lineTotal: roundMoney(Math.max(0, (unitPrice - unitDiscount) * confirmed)), }); } } await this.prisma.$transaction(async (tx) => { if (deletedIds.length) { await tx.orderItem.deleteMany({ where: { orderId: id, id: { in: deletedIds } }, }); } for (const existing of order.items) { if (deletedIds.includes(existing.id)) continue; const item = updates.find((entry) => entry.id === existing.id); if (!item) continue; 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 BadRequestException( 'تخفیف واحد نمی‌تواند از قیمت واحد بیشتر باشد', ); } const confirmed = item.confirmedMeterage ?? existing.confirmedMeterage ?? existing.requestedMeterage; await tx.orderItem.update({ where: { id: existing.id }, data: { confirmedMeterage: confirmed, unitPrice, unitDiscount, lineTotal: roundMoney(Math.max(0, (unitPrice - unitDiscount) * confirmed)), }, }); } for (const row of createdRows) { await tx.orderItem.create({ data: { orderId: id, ...row }, }); } const remaining = await tx.orderItem.findMany({ where: { orderId: id } }); if (!remaining.length) { throw new BadRequestException('سفارش باید حداقل یک قلم داشته باشد'); } let totalAmount = 0; let totalMeterage = 0; for (const existing of remaining) { const unitPrice = existing.unitPrice ?? 0; const unitDiscount = existing.unitDiscount ?? 0; const confirmed = existing.confirmedMeterage ?? existing.requestedMeterage; totalAmount += roundMoney( Math.max(0, (unitPrice - unitDiscount) * confirmed), ); totalMeterage += confirmed; } 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 === OrderStatus.awaiting_review || order.status === OrderStatus.pricing ? 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 || 'قیمت‌گذاری انجام شد', }, }); } }); await this.agentLevelService.syncAgentForOrder(id); return this.findAdminOrder(id); } async updateStatus(id: string, adminId: string, dto: UpdateOrderStatusDto) { 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 BadRequestException( 'برای تغییر دستی وضعیت، ذکر دلیل الزامی است', ); } } else { assertTransition(order.status, dto.status); } // فاکتور بدون مبلغ نباید برای مشتری ارسال شود if ( dto.status === OrderStatus.awaiting_settlement && (order.finalAmount ?? 0) <= 0 ) { throw new BadRequestException( 'ابتدا سفارش را قیمت‌گذاری کنید تا فاکتور قابل ارسال باشد', ); } const needsCashExpiry = (dto.status === OrderStatus.cash_invoice_issued || dto.status === OrderStatus.awaiting_cash_payment) && !order.cashInvoiceExpiresAt; const cashExpiry = needsCashExpiry ? 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 = eventForStatus(dto.status); if (event) { this.notifications.dispatchOrderEvent(id, event, { note: dto.reason ?? dto.note ?? null, }); } return this.findAdminOrder(id); } async updateChecks(userId: string, id: string, dto: UpdateOrderChecksDto) { const order = await this.getOrder(id); if (order.userId !== userId) { throw new ForbiddenException('دسترسی به این سفارش مجاز نیست'); } if (order.settlementType !== SettlementType.check) { throw new BadRequestException('این سفارش تسویه چکی نیست'); } if (!CHECK_EDITABLE_STATUSES.includes(order.status)) { throw new BadRequestException( 'در وضعیت فعلی سفارش امکان ویرایش چک‌ها وجود ندارد', ); } if (!dto.checks.length) { throw new BadRequestException('حداقل یک چک لازم است'); } const checkMode = dto.checkMode ?? order.checkMode ?? CheckRegistrationMode.full; const rass = calculateCheckRass( dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate })), order.createdAt, ); // ثبت اولیه و اصلاح، هر دو سفارش را به صف بررسی چک می‌برند. const goesToReview = order.status === OrderStatus.check_in_progress || order.status === OrderStatus.check_needs_fix || order.status === OrderStatus.check_rejected; const nextStatus = goesToReview ? OrderStatus.awaiting_check_review : order.status; const isFix = order.status !== 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); } private mapCheckCreate( c: SubmitOrderCheckDto, row: number, defaultMode: CheckRegistrationMode, ) { 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, }; } private async nextOrderNumber(): Promise { for (let i = 0; i < 5; i++) { const orderNumber = generateOrderNumber(); const exists = await this.prisma.order.findUnique({ where: { orderNumber }, }); if (!exists) return orderNumber; } return `${generateOrderNumber()}-${Date.now().toString().slice(-3)}`; } /** بین انتخاب شماره و درج، امکان تصاحب شماره توسط درخواست هم‌زمان هست. */ private async createWithUniqueOrderNumber( create: (orderNumber: string) => Promise, ): Promise { for (let attempt = 0; ; attempt++) { const orderNumber = await this.nextOrderNumber(); try { return await create(orderNumber); } catch (e) { const isDuplicate = e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002'; if (!isDuplicate || attempt >= 4) throw e; } } } private async companiesForOrder(order: OrderFull) { 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] : []; } private async resolveOrderLines( items: SubmitOrderItemDto[], preferredCompanyId?: string, ) { const settings = await this.prisma.systemSettings.findUnique({ where: { id: 'default' }, select: { maxMeteragePerInvoice: true, minOrderRollsPerProduct: true, }, }); const minRolls = settings?.minOrderRollsPerProduct ?? 1; const productIds = items.map((i) => i.productId); const uniqueProductIds = [...new Set(productIds)]; if (uniqueProductIds.length !== productIds.length) { throw new 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 items) { const product = productMap.get(item.productId); if (!product) { throw new BadRequestException( 'یک یا چند محصول نامعتبر است یا دیگر در سیستم وجود ندارد.', ); } if (!product.isActive) { throw new BadRequestException( `محصول «${product.name}» غیرفعال است و قابل سفارش نیست`, ); } if (!product.company?.isActive) { throw new BadRequestException( `شرکت محصول «${product.name}» غیرفعال است`, ); } } const meterageByCompany = new Map< string, { name: string; meterage: number; max: number | null } >(); for (const item of 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 = resolveMaxMeterage( row.max, settings?.maxMeteragePerInvoice, ); if (row.meterage > maxMeterageResolved) { throw new BadRequestException( `حداکثر متراژ مجاز فاکتور برای «${row.name}» برابر ${maxMeterageResolved} متر است`, ); } } let totalMeterage = 0; const itemsData = items.map((item) => { const product = productMap.get(item.productId)!; if (item.meterage < product.minMeterage) { throw new 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 BadRequestException( `متراژ ${product.name} باید مضرب ${product.orderMultiplier} باشد`, ); } const rollMeters = product.rollMeters && product.rollMeters > 0 ? product.rollMeters : product.minMeterage > 0 ? product.minMeterage : 50; if (item.meterage / rollMeters + 1e-9 < minRolls) { throw new BadRequestException( `حداقل سفارش برای ${product.name} برابر ${minRolls} طاقه است`, ); } 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 preferredId = itemsData.some((item) => item.companyId === preferredCompanyId) ? preferredCompanyId : itemsData[0]!.companyId; const primaryCompany = products.find((p) => p.companyId === preferredId) ?.company; if (!primaryCompany) { throw new NotFoundException('شرکت محصولات یافت نشد یا غیرفعال است'); } return { itemsData, totalMeterage, primaryCompany, companyNames, uniqueProductIds, }; } private async getOrder(id: string): Promise { const order = await this.prisma.order.findUnique({ where: { id }, include: orderInclude, }); if (!order) throw new NotFoundException('سفارش یافت نشد'); return order; } mapOrder(order: OrderFull, adminView = false) { const customerName = 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: toAdminStatus(order.status), allowedTransitions: adminView ? 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; } }