Enhance sales document flow with new service and related functionalities
- Introduced `SalesDocumentFlowService` to manage the lifecycle of sales documents, including packing slips and invoices. - Implemented methods for processing orders, generating invoices, and handling packing slips based on order status. - Updated `SalesOrdersService`, `PackingSlipsService`, and `SalesInvoicesService` to integrate with the new document flow service. - Added new methods for marking drafts processed and checking if invoices are on sales plans. - Enhanced existing services and repositories to support new functionalities, including status transitions and related document management. - Updated DTOs to include new fields for packing slip and invoice IDs in sales order responses. - Added unit tests for the new service and updated existing tests to cover new functionalities.
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<ReturnType<PackingSlipsService['toDetail']>> {
|
||||
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<void> {
|
||||
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<string, Decimal>();
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user