import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/postgresql'; import { CreateInvoiceDto } from './dto/create-invoice.dto'; import { UpdateInvoiceDto } from './dto/update-invoice.dto'; import { FindInvoicesDto } from './dto/find-invoices.dto'; import { Invoice } from './entities/invoice.entity'; import { InvoiceItem } from './entities/invoice-item.entity'; import { Request } from '../request/entities/request.entity'; import { User } from '../user/entities/user.entity'; import { ProductService } from '../product/providers/product.service'; import { InvoiceRepository } from './repositories/invoice.repository'; const TAX_RATE = 0.1; @Injectable() export class InvoiceService { constructor( private readonly em: EntityManager, private readonly productService: ProductService, private readonly invoiceRepository: InvoiceRepository, ) { } async create(dto: CreateInvoiceDto) { let user: User; let request: Request | undefined; if (dto.requestId) { const requestEntity = await this.em.findOne( Request, { id: dto.requestId }, { populate: ['user'] }, ); if (!requestEntity) { throw new NotFoundException('Request not found'); } user = requestEntity.user; request = requestEntity; } else if (dto.userId) { user = await this.em.findOneOrFail(User, { id: dto.userId }); } else { throw new BadRequestException('Either requestId or userId must be provided'); } const enableTax = dto.enableTax ?? false; const approvalDeadline = dto.approvalDeadline ? new Date(dto.approvalDeadline) : undefined; const discount = dto.items.reduce((acc, item) => acc + (item.discount ?? 0), 0); return this.em.transactional(async (em) => { const invoice = em.create(Invoice, { user, ...(request && { request }), enableTax, approvalDeadline, discount, paidAmount: 0, balance: 0, }); em.persist(invoice); let subTotal = 0; for (const item of dto.items) { const product = await this.productService.findOneOrFail(item.productId); const unitPrice = item.unitPrice; const itemDiscount = item.discount ?? 0; const itemTotal = Math.max(0, unitPrice * item.quantity - itemDiscount); subTotal += itemTotal; const invoiceItem = em.create(InvoiceItem, { invoice, product, quantity: item.quantity, unitPrice: unitPrice, discount: itemDiscount || undefined, description: item.description, }); invoice.items.add(invoiceItem); } const taxAmount = 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 em.flush(); return invoice; }); } findAll(userId: string, dto: FindInvoicesDto) { return this.invoiceRepository.findAllPaginated({ userId, ...dto }); } findAllAsAdmin(dto: FindInvoicesDto) { return this.invoiceRepository.findAllPaginated(dto); } async findOne(id: string, userId: string) { const invoice = await this.em.findOne( Invoice, { id, user: { id: userId } }, { populate: ['user', 'request', 'items', 'items.product'] }, ); if (!invoice) { throw new NotFoundException('Invoice not found'); } return invoice; } async confirmInvoiceItem(id: string, userId: string) { const item = await this.em.findOne( InvoiceItem, { id, invoice: { user: { id: userId } } }, { populate: ['invoice', 'product'] }, ); if (!item) { throw new NotFoundException('Invoice item not found'); } if(item.confirmedAt){ throw new BadRequestException('Invoice item already confirmed'); } item.confirmedAt = new Date(); await this.em.persistAndFlush(item); return item; } async update(id: string, updateInvoiceDto: UpdateInvoiceDto) { const invoice = await this.findOneOrFail(id); if (updateInvoiceDto.enableTax !== undefined) invoice.enableTax = updateInvoiceDto.enableTax; 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; } async remove(id: string) { const invoice = await this.findOneOrFail(id); this.em.remove(invoice); await this.em.flush(); return invoice; } async findOneOrFail(id: string) { const invoice = await this.em.findOne( Invoice, { id }, { populate: ['user', 'request', 'items', 'items.product'] }, ); if (!invoice) { throw new NotFoundException('Invoice not found'); } return invoice; } }