Files
negareh-api/src/modules/invoice/invoice.service.ts
T
2026-02-21 12:27:15 +03:30

234 lines
7.4 KiB
TypeScript

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,
attachments: [],
paymentMethod: dto.paymentMethod,
invoiceNumber:20
});
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;
}
}