import { BadRequestException, Inject, Injectable, NotFoundException, forwardRef, } from '@nestjs/common'; import { Decimal } from '../../../common/value-objects/decimal/decimal'; import { Status } from '../../../common/value-objects/status/status'; import { PackingSlipsRepository } from '../packing-slips/packing-slips.repository'; import { PackingSlipsService } from '../packing-slips/packing-slips.service'; import type { PackingSlip } from '../packing-slips/packing-slip'; import { SalesInvoicesRepository } from '../sales-invoices/sales-invoices.repository'; import { SalesInvoicesService } from '../sales-invoices/sales-invoices.service'; import { SalesOrdersService } from '../sales-orders/sales-orders.service'; import { PACKING_SLIP_STATUSES, SALES_INVOICE_STATUSES } from './sales-fields'; export type DeliveredLineBody = { readonly productId: string; readonly quantity: string; }; @Injectable() export class SalesDocumentFlowService { constructor( @Inject(forwardRef(() => SalesOrdersService)) private readonly salesOrdersService: SalesOrdersService, @Inject(forwardRef(() => PackingSlipsService)) private readonly packingSlipsService: PackingSlipsService, @Inject(forwardRef(() => SalesInvoicesService)) private readonly salesInvoicesService: SalesInvoicesService, private readonly packingSlipsRepository: PackingSlipsRepository, private readonly salesInvoicesRepository: SalesInvoicesRepository, ) {} async relatedDocumentIds(orderId: string): Promise<{ packingSlipIds: string[]; invoiceIds: string[]; }> { const [packing, invoices] = await Promise.all([ this.packingSlipsRepository.listBySalesOrderId(orderId), this.salesInvoicesRepository.listBySalesOrderId(orderId), ]); return { packingSlipIds: packing.map((item) => item.id), invoiceIds: invoices.map((item) => item.id), }; } async onOrderProcessed( orderId: string, userId: string, generatePackingSlip: boolean, ): Promise { const existing = await this.salesInvoicesRepository.listBySalesOrderId(orderId); if (existing.length > 0) { throw new BadRequestException('Sales order already has an invoice'); } await this.salesInvoicesService.create({ salesOrderId: orderId, userId, }); if (generatePackingSlip) { await this.packingSlipsService.create({ salesOrderId: orderId, status: 'processed', userId, }); return; } await this.salesOrdersService.completeFromSystem(orderId, userId); } async cancelOrderDocuments(orderId: string, userId: string): Promise { const [packing, invoices] = await Promise.all([ this.packingSlipsRepository.listBySalesOrderId(orderId), this.salesInvoicesRepository.listBySalesOrderId(orderId), ]); for (const slip of packing) { if (slip.status.value !== 'cancelled') { await this.packingSlipsRepository.updateStatus( slip.id, Status.create('cancelled', PACKING_SLIP_STATUSES), userId, ); } } for (const invoice of invoices) { if (invoice.status.value !== 'cancelled') { await this.salesInvoicesRepository.updateStatus( invoice.id, Status.create('cancelled', SALES_INVOICE_STATUSES), userId, ); } } } async completePacking( id: string, userId: string, products?: DeliveredLineBody[], ): Promise> { const packing = await this.packingSlipsRepository.findById(id); if (!packing) { throw new NotFoundException('Packing slip not found'); } const delivered = this.splitDelivered(packing, products); if (delivered.remaining.length > 0) { await this.packingSlipsRepository.update(id, { products: delivered.delivered, userId, }); } await this.packingSlipsRepository.updateStatus( id, Status.create('completed', PACKING_SLIP_STATUSES), userId, ); if (delivered.remaining.length > 0) { await this.packingSlipsService.create({ salesOrderId: packing.salesOrderId ?? undefined, date: packing.date.format(), customerId: packing.customerId, address: packing.address, latitude: packing.latitude, longitude: packing.longitude, notes: packing.notes, products: delivered.remaining.map((line) => ({ productId: line.productId, quantity: line.quantity.value, price: line.price.value, })), status: 'processed', userId, }); } if (packing.salesOrderId) { await this.completeOrderIfPackingDone(packing.salesOrderId, userId); } return this.packingSlipsService.findById(id); } async completeOrderIfPackingDone( orderId: string, userId: string, ): Promise { const slips = await this.packingSlipsRepository.listBySalesOrderId(orderId); const blocking = slips.filter( (item) => item.status.value !== 'cancelled' && item.status.value !== 'completed', ); if (blocking.length === 0) { await this.salesOrdersService.completeFromSystem(orderId, userId); } } private splitDelivered( packing: PackingSlip, products?: DeliveredLineBody[], ): { delivered: Array<{ productId: string; quantity: Decimal; price: Decimal; }>; remaining: Array<{ productId: string; quantity: Decimal; price: Decimal; }>; } { const requested = new Map(); for (const line of products ?? []) { let quantity: Decimal; try { quantity = Decimal.create(line.quantity); } catch { throw new BadRequestException('Invalid delivered quantity'); } requested.set(line.productId, quantity); } for (const productId of requested.keys()) { if (!packing.products.some((line) => line.productId === productId)) { throw new BadRequestException('Invalid delivered quantity'); } } const delivered: Array<{ productId: string; quantity: Decimal; price: Decimal; }> = []; const remaining: Array<{ productId: string; quantity: Decimal; price: Decimal; }> = []; for (const line of packing.products) { const qty = requested.get(line.productId) ?? line.quantity; if (!qty.isPositive() || qty.compare(line.quantity) > 0) { throw new BadRequestException('Invalid delivered quantity'); } delivered.push({ productId: line.productId, quantity: qty, price: line.price, }); const leftover = line.quantity.subtract(qty); if (leftover.isPositive()) { remaining.push({ productId: line.productId, quantity: leftover, price: line.price, }); } } return { delivered, remaining }; } }