From d32367f37d6c765e31684cf5b671585122fa61d3 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 19 Feb 2026 11:00:06 +0330 Subject: [PATCH] invoice update --- .../invoice/dto/update-invoice-item.dto.ts | 11 +++ src/modules/invoice/dto/update-invoice.dto.ts | 23 ++++++- src/modules/invoice/invoice.service.ts | 68 ++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/modules/invoice/dto/update-invoice-item.dto.ts diff --git a/src/modules/invoice/dto/update-invoice-item.dto.ts b/src/modules/invoice/dto/update-invoice-item.dto.ts new file mode 100644 index 0000000..2937347 --- /dev/null +++ b/src/modules/invoice/dto/update-invoice-item.dto.ts @@ -0,0 +1,11 @@ +import { PartialType } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { CreateInvoiceItemDto } from './create-invoice.dto'; + +export class UpdateInvoiceItemDto extends PartialType(CreateInvoiceItemDto) { + @IsOptional() + @IsString() + @ApiPropertyOptional({ description: 'Invoice item ID - when provided, updates existing item; when omitted, creates new item' }) + id?: string; +} diff --git a/src/modules/invoice/dto/update-invoice.dto.ts b/src/modules/invoice/dto/update-invoice.dto.ts index 39983d1..a563e53 100644 --- a/src/modules/invoice/dto/update-invoice.dto.ts +++ b/src/modules/invoice/dto/update-invoice.dto.ts @@ -1,4 +1,23 @@ -import { PartialType } from '@nestjs/swagger'; +import { IntersectionType, OmitType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsOptional } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { CreateInvoiceDto } from './create-invoice.dto'; +import { UpdateInvoiceItemDto } from './update-invoice-item.dto'; -export class UpdateInvoiceDto extends PartialType(CreateInvoiceDto) {} +class UpdateInvoiceBaseDto extends PartialType( + OmitType(CreateInvoiceDto, ['items']), +) {} + +class UpdateInvoiceItemsDto { + @IsOptional() + @IsArray() + @Type(() => UpdateInvoiceItemDto) + @ApiPropertyOptional({ type: [UpdateInvoiceItemDto] }) + items?: UpdateInvoiceItemDto[]; +} + +export class UpdateInvoiceDto extends IntersectionType( + UpdateInvoiceBaseDto, + UpdateInvoiceItemsDto, +) {} diff --git a/src/modules/invoice/invoice.service.ts b/src/modules/invoice/invoice.service.ts index 30808a8..22929cd 100644 --- a/src/modules/invoice/invoice.service.ts +++ b/src/modules/invoice/invoice.service.ts @@ -9,7 +9,7 @@ import { Request } from '../request/entities/request.entity'; import { ProductService } from '../product/providers/product.service'; import { InvoiceRepository } from './repositories/invoice.repository'; -const TAX_RATE = 0.09; +const TAX_RATE = 0.1; @Injectable() export class InvoiceService { @@ -127,6 +127,72 @@ export class InvoiceService { if (updateInvoiceDto.approvalDeadline !== undefined) { invoice.approvalDeadline = new Date(updateInvoiceDto.approvalDeadline); } + if (updateInvoiceDto.items !== undefined && updateInvoiceDto.items.length > 0) { + for (const item of updateInvoiceDto.items) { + if (item.id) { + const existingItem = await this.em.findOne(InvoiceItem, { + id: item.id, + invoice: { id: invoice.id }, + }); + if (!existingItem) { + throw new NotFoundException(`Invoice item ${item.id} not found`); + } + if (item.productId !== undefined) { + existingItem.product = await this.productService.findOneOrFail( + item.productId, + ); + } + if (item.quantity !== undefined) existingItem.quantity = item.quantity; + if (item.unitPrice !== undefined) + existingItem.unitPrice = item.unitPrice; + if (item.discount !== undefined) existingItem.discount = item.discount; + if (item.description !== undefined) + existingItem.description = item.description; + } else { + if ( + item.productId === undefined || + item.quantity === undefined || + item.unitPrice === undefined + ) { + throw new BadRequestException( + 'New invoice items must have productId, quantity and unitPrice', + ); + } + const product = await this.productService.findOneOrFail(item.productId); + const itemDiscount = item.discount ?? 0; + const invoiceItem = this.em.create(InvoiceItem, { + invoice, + product, + quantity: item.quantity, + unitPrice: item.unitPrice, + discount: itemDiscount || undefined, + description: item.description, + }); + invoice.items.add(invoiceItem); + } + } + + const allItems = invoice.items.getItems(); + let subTotal = 0; + let discount = 0; + for (const item of allItems) { + const itemTotal = Math.max( + 0, + item.unitPrice * item.quantity - (item.discount ?? 0), + ); + subTotal += itemTotal; + discount += item.discount ?? 0; + } + invoice.discount = discount; + const taxAmount = invoice.enableTax + ? Math.round((subTotal - discount) * TAX_RATE) + : 0; + const total = Math.max(0, subTotal - discount + taxAmount); + invoice.subTotal = subTotal; + invoice.taxAmount = taxAmount; + invoice.total = total; + invoice.balance = total - invoice.paidAmount; + } await this.em.flush(); return invoice; }