mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-12 06:40:58 +04:30
Add approved invoice status with lock after customer acceptance.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
9455cd9fab
commit
7244b70e90
@@ -18,6 +18,7 @@ import {
|
||||
InvoiceKeyPointInputDto,
|
||||
InvoiceTemplateItemInputDto,
|
||||
ListInvoicesDto,
|
||||
UpdateInvoiceDto,
|
||||
UpdateInvoiceItemTemplateDto,
|
||||
UpdateInvoiceStatusDto,
|
||||
UpdateInvoiceTemplateDto,
|
||||
@@ -743,7 +744,7 @@ export class InvoicesService {
|
||||
where: {
|
||||
publicId,
|
||||
ownerScope: InvoiceOwnerScope.platform,
|
||||
status: { in: [InvoiceStatus.issued, InvoiceStatus.paid] },
|
||||
status: { in: [InvoiceStatus.issued, InvoiceStatus.approved, InvoiceStatus.paid] },
|
||||
},
|
||||
include: this.invoiceInclude,
|
||||
});
|
||||
@@ -930,6 +931,17 @@ export class InvoicesService {
|
||||
throw new NotFoundException('Invoice not found');
|
||||
}
|
||||
|
||||
if (
|
||||
existing.status === InvoiceStatus.approved &&
|
||||
dto.status !== InvoiceStatus.approved &&
|
||||
dto.status !== InvoiceStatus.paid &&
|
||||
dto.status !== InvoiceStatus.cancelled
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Approved invoices can only be marked paid or cancelled',
|
||||
);
|
||||
}
|
||||
|
||||
const row = await this.prisma.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: {
|
||||
@@ -942,6 +954,166 @@ export class InvoicesService {
|
||||
return this.serializeInvoice(row);
|
||||
}
|
||||
|
||||
async updateContent(
|
||||
businessIdRaw: string,
|
||||
invoiceIdRaw: string,
|
||||
dto: UpdateInvoiceDto,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
const invoiceId = BigInt(invoiceIdRaw);
|
||||
|
||||
const existing = await this.prisma.invoice.findFirst({
|
||||
where: {
|
||||
id: invoiceId,
|
||||
businessId,
|
||||
ownerScope: InvoiceOwnerScope.platform,
|
||||
},
|
||||
});
|
||||
if (!existing) {
|
||||
throw new NotFoundException('Invoice not found');
|
||||
}
|
||||
if (existing.status === InvoiceStatus.approved) {
|
||||
throw new BadRequestException('Approved invoices cannot be edited');
|
||||
}
|
||||
|
||||
const items = dto.items.map((item) => this.normalizeItemInput(item));
|
||||
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
||||
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
||||
|
||||
let invoiceTemplateId: bigint | null | undefined;
|
||||
if (dto.invoiceTemplateId === null) {
|
||||
invoiceTemplateId = null;
|
||||
} else if (dto.invoiceTemplateId !== undefined) {
|
||||
const trimmed = dto.invoiceTemplateId.trim();
|
||||
invoiceTemplateId = trimmed ? BigInt(trimmed) : null;
|
||||
if (invoiceTemplateId !== null) {
|
||||
const template = await this.prisma.invoiceTemplate.findFirst({
|
||||
where: { id: invoiceTemplateId, ownerScope: InvoiceOwnerScope.platform },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!template) {
|
||||
throw new BadRequestException('Invoice template not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const row = await this.prisma.$transaction(async (tx) => {
|
||||
await tx.invoiceItem.deleteMany({ where: { invoiceId } });
|
||||
await tx.invoiceKeyPoint.deleteMany({ where: { invoiceId } });
|
||||
await tx.invoiceAccount.deleteMany({ where: { invoiceId } });
|
||||
|
||||
return tx.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name?.trim() || null } : {}),
|
||||
...(dto.topText !== undefined ? { topText: dto.topText?.trim() || null } : {}),
|
||||
...(dto.notes !== undefined ? { notes: dto.notes?.trim() || null } : {}),
|
||||
...(invoiceTemplateId !== undefined ? { invoiceTemplateId } : {}),
|
||||
items: {
|
||||
create: items.map((item, index) => ({
|
||||
templateId: item.templateId,
|
||||
title: item.title,
|
||||
duration: item.duration,
|
||||
worktime: item.worktime,
|
||||
description: item.description,
|
||||
price: item.price,
|
||||
discountedPrice: item.discountedPrice,
|
||||
sortOrder: index,
|
||||
})),
|
||||
},
|
||||
keyPoints: {
|
||||
create: keyPoints.map((kp, index) => ({
|
||||
text: kp.text,
|
||||
sortOrder: index,
|
||||
})),
|
||||
},
|
||||
accounts: {
|
||||
create: accounts.map((acc, index) => ({
|
||||
bankName: acc.bankName,
|
||||
accountHolderName: acc.accountHolderName,
|
||||
cardNumber: acc.cardNumber,
|
||||
iban: acc.iban,
|
||||
sortOrder: index,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: this.invoiceInclude,
|
||||
});
|
||||
});
|
||||
|
||||
return this.serializeInvoice(row);
|
||||
}
|
||||
|
||||
async approvePublicInvoice(publicIdRaw: string) {
|
||||
const publicId = publicIdRaw.trim();
|
||||
if (!/^\d{6,32}$/.test(publicId)) {
|
||||
throw new NotFoundException('Invoice not found');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.invoice.findFirst({
|
||||
where: {
|
||||
publicId,
|
||||
ownerScope: InvoiceOwnerScope.platform,
|
||||
},
|
||||
});
|
||||
if (
|
||||
!existing ||
|
||||
(existing.status !== InvoiceStatus.issued &&
|
||||
existing.status !== InvoiceStatus.approved &&
|
||||
existing.status !== InvoiceStatus.paid)
|
||||
) {
|
||||
throw new NotFoundException('Invoice not found');
|
||||
}
|
||||
|
||||
if (existing.status === InvoiceStatus.approved || existing.status === InvoiceStatus.paid) {
|
||||
const row = await this.prisma.invoice.findFirst({
|
||||
where: { id: existing.id },
|
||||
include: this.invoiceInclude,
|
||||
});
|
||||
if (!row) throw new NotFoundException('Invoice not found');
|
||||
const serialized = this.serializeInvoice(row, true);
|
||||
return {
|
||||
publicId: serialized.publicId,
|
||||
status: serialized.status,
|
||||
name: serialized.name,
|
||||
topText: serialized.topText,
|
||||
issuedAt: serialized.issuedAt,
|
||||
business: serialized.business,
|
||||
items: serialized.items,
|
||||
keyPoints: serialized.keyPoints,
|
||||
accounts: serialized.accounts,
|
||||
subtotal: serialized.subtotal,
|
||||
total: serialized.total,
|
||||
publicUrl: serialized.publicUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const row = await this.prisma.invoice.update({
|
||||
where: { id: existing.id },
|
||||
data: { status: InvoiceStatus.approved },
|
||||
include: this.invoiceInclude,
|
||||
});
|
||||
|
||||
const serialized = this.serializeInvoice(row, true);
|
||||
return {
|
||||
publicId: serialized.publicId,
|
||||
status: serialized.status,
|
||||
name: serialized.name,
|
||||
topText: serialized.topText,
|
||||
issuedAt: serialized.issuedAt,
|
||||
business: serialized.business,
|
||||
items: serialized.items,
|
||||
keyPoints: serialized.keyPoints,
|
||||
accounts: serialized.accounts,
|
||||
subtotal: serialized.subtotal,
|
||||
total: serialized.total,
|
||||
publicUrl: serialized.publicUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteInvoice(businessIdRaw: string, invoiceIdRaw: string, actor: AuthUser) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user