231 lines
7.3 KiB
TypeScript
231 lines
7.3 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,
|
|
});
|
|
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;
|
|
}
|
|
}
|