Add invoice bank account templates API and migration.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
503b70a690
commit
2a01873c95
@@ -297,6 +297,65 @@ export class UpdateInvoiceItemTemplateDto {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class CreateInvoiceAccountTemplateDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(255)
|
||||
bankName!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
accountHolderName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
cardNumber?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
iban?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class UpdateInvoiceAccountTemplateDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(255)
|
||||
bankName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
accountHolderName?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
cardNumber?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
iban?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class InvoiceTemplateItemInputDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -20,9 +20,11 @@ import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import {
|
||||
CreateInvoiceDto,
|
||||
CreateInvoiceAccountTemplateDto,
|
||||
CreateInvoiceItemTemplateDto,
|
||||
CreateInvoiceTemplateDto,
|
||||
ListInvoicesDto,
|
||||
UpdateInvoiceAccountTemplateDto,
|
||||
UpdateInvoiceDto,
|
||||
UpdateInvoiceItemTemplateDto,
|
||||
UpdateInvoiceStatusDto,
|
||||
@@ -63,6 +65,41 @@ export class InvoicesController {
|
||||
return this.service.deletePlatformTemplate(templateId, user);
|
||||
}
|
||||
|
||||
// Platform invoice account templates (settings)
|
||||
@Get('invoice-account-templates')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
listAccountTemplates(@CurrentUser() user: AuthUser) {
|
||||
return this.service.listPlatformAccountTemplates(user);
|
||||
}
|
||||
|
||||
@Post('invoice-account-templates')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
createAccountTemplate(
|
||||
@Body() dto: CreateInvoiceAccountTemplateDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.createPlatformAccountTemplate(dto, user);
|
||||
}
|
||||
|
||||
@Patch('invoice-account-templates/:templateId')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
updateAccountTemplate(
|
||||
@Param('templateId') templateId: string,
|
||||
@Body() dto: UpdateInvoiceAccountTemplateDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.updatePlatformAccountTemplate(templateId, dto, user);
|
||||
}
|
||||
|
||||
@Delete('invoice-account-templates/:templateId')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
deleteAccountTemplate(
|
||||
@Param('templateId') templateId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.deletePlatformAccountTemplate(templateId, user);
|
||||
}
|
||||
|
||||
// Platform invoice templates (settings)
|
||||
@Get('invoice-templates')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -237,6 +274,47 @@ export class BusinessInvoicesController {
|
||||
return this.service.deleteBusinessItemTemplate(businessId, templateId, user);
|
||||
}
|
||||
|
||||
// Invoice account templates (business-scoped)
|
||||
@Get('invoice-account-templates')
|
||||
@RequireBusinessPermission('invoice_templates.read')
|
||||
listAccountTemplatesBusiness(
|
||||
@Param('businessId') businessId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.listBusinessAccountTemplates(businessId, user);
|
||||
}
|
||||
|
||||
@Post('invoice-account-templates')
|
||||
@RequireBusinessPermission('invoice_templates.manage')
|
||||
createAccountTemplateBusiness(
|
||||
@Param('businessId') businessId: string,
|
||||
@Body() dto: CreateInvoiceAccountTemplateDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.createBusinessAccountTemplate(businessId, dto, user);
|
||||
}
|
||||
|
||||
@Patch('invoice-account-templates/:templateId')
|
||||
@RequireBusinessPermission('invoice_templates.manage')
|
||||
updateAccountTemplateBusiness(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('templateId') templateId: string,
|
||||
@Body() dto: UpdateInvoiceAccountTemplateDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.updateBusinessAccountTemplate(businessId, templateId, dto, user);
|
||||
}
|
||||
|
||||
@Delete('invoice-account-templates/:templateId')
|
||||
@RequireBusinessPermission('invoice_templates.manage')
|
||||
deleteAccountTemplateBusiness(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('templateId') templateId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.deleteBusinessAccountTemplate(businessId, templateId, user);
|
||||
}
|
||||
|
||||
// Invoice templates (business-scoped)
|
||||
@Get('invoice-templates')
|
||||
@RequireBusinessPermission('invoice_templates.read')
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PermissionsService } from '../auth/permissions.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
CreateInvoiceDto,
|
||||
CreateInvoiceAccountTemplateDto,
|
||||
CreateInvoiceItemTemplateDto,
|
||||
CreateInvoiceTemplateDto,
|
||||
InvoiceAccountInputDto,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
InvoicePaymentMethodInputDto,
|
||||
InvoiceTemplateItemInputDto,
|
||||
ListInvoicesDto,
|
||||
UpdateInvoiceAccountTemplateDto,
|
||||
UpdateInvoiceDto,
|
||||
UpdateInvoiceItemTemplateDto,
|
||||
UpdateInvoiceStatusDto,
|
||||
@@ -232,6 +234,34 @@ export class InvoicesService {
|
||||
};
|
||||
}
|
||||
|
||||
private serializeAccountTemplate(row: {
|
||||
id: bigint;
|
||||
ownerScope: InvoiceOwnerScope;
|
||||
businessId: bigint | null;
|
||||
bankName: string;
|
||||
accountHolderName: string | null;
|
||||
cardNumber: string | null;
|
||||
iban: string | null;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}) {
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
ownerScope: row.ownerScope,
|
||||
businessId: row.businessId?.toString() ?? null,
|
||||
bankName: row.bankName,
|
||||
accountHolderName: row.accountHolderName,
|
||||
cardNumber: row.cardNumber,
|
||||
iban: row.iban,
|
||||
sortOrder: row.sortOrder,
|
||||
isActive: row.isActive,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly invoiceInclude = {
|
||||
business: {
|
||||
select: {
|
||||
@@ -794,6 +824,83 @@ export class InvoicesService {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// --- Account templates (platform settings) ---
|
||||
|
||||
async listPlatformAccountTemplates(actor: AuthUser) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
const rows = await this.prisma.invoiceAccountTemplate.findMany({
|
||||
where: { ownerScope: InvoiceOwnerScope.platform },
|
||||
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
return { items: rows.map((row) => this.serializeAccountTemplate(row)) };
|
||||
}
|
||||
|
||||
async createPlatformAccountTemplate(
|
||||
dto: CreateInvoiceAccountTemplateDto,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
const bankName = dto.bankName.trim();
|
||||
if (!bankName) {
|
||||
throw new BadRequestException('Bank name is required');
|
||||
}
|
||||
const row = await this.prisma.invoiceAccountTemplate.create({
|
||||
data: {
|
||||
ownerScope: InvoiceOwnerScope.platform,
|
||||
bankName,
|
||||
accountHolderName: dto.accountHolderName?.trim() || null,
|
||||
cardNumber: dto.cardNumber?.trim() || null,
|
||||
iban: dto.iban?.trim() || null,
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
},
|
||||
});
|
||||
return this.serializeAccountTemplate(row);
|
||||
}
|
||||
|
||||
async updatePlatformAccountTemplate(
|
||||
templateIdRaw: string,
|
||||
dto: UpdateInvoiceAccountTemplateDto,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
const templateId = BigInt(templateIdRaw);
|
||||
const existing = await this.prisma.invoiceAccountTemplate.findFirst({
|
||||
where: { id: templateId, ownerScope: InvoiceOwnerScope.platform },
|
||||
});
|
||||
if (!existing) {
|
||||
throw new NotFoundException('Invoice account template not found');
|
||||
}
|
||||
const row = await this.prisma.invoiceAccountTemplate.update({
|
||||
where: { id: templateId },
|
||||
data: {
|
||||
...(dto.bankName !== undefined ? { bankName: dto.bankName.trim() } : {}),
|
||||
...(dto.accountHolderName !== undefined
|
||||
? { accountHolderName: dto.accountHolderName?.trim() || null }
|
||||
: {}),
|
||||
...(dto.cardNumber !== undefined
|
||||
? { cardNumber: dto.cardNumber?.trim() || null }
|
||||
: {}),
|
||||
...(dto.iban !== undefined ? { iban: dto.iban?.trim() || null } : {}),
|
||||
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
|
||||
...(dto.isActive !== undefined ? { isActive: dto.isActive } : {}),
|
||||
},
|
||||
});
|
||||
return this.serializeAccountTemplate(row);
|
||||
}
|
||||
|
||||
async deletePlatformAccountTemplate(templateIdRaw: string, actor: AuthUser) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
const templateId = BigInt(templateIdRaw);
|
||||
const existing = await this.prisma.invoiceAccountTemplate.findFirst({
|
||||
where: { id: templateId, ownerScope: InvoiceOwnerScope.platform },
|
||||
});
|
||||
if (!existing) {
|
||||
throw new NotFoundException('Invoice account template not found');
|
||||
}
|
||||
await this.prisma.invoiceAccountTemplate.delete({ where: { id: templateId } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// --- Invoice templates (platform settings) ---
|
||||
|
||||
async listPlatformInvoiceTemplates(actor: AuthUser) {
|
||||
@@ -1129,6 +1236,105 @@ export class InvoicesService {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// --- Account templates (business-scoped) ---
|
||||
|
||||
async listBusinessAccountTemplates(businessIdRaw: string, actor: AuthUser) {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
await this.assertBusinessExists(businessId);
|
||||
await this.assertPermission(businessId, actor.id, 'invoice_templates.read');
|
||||
|
||||
const rows = await this.prisma.invoiceAccountTemplate.findMany({
|
||||
where: { ownerScope: InvoiceOwnerScope.business, businessId },
|
||||
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
return { items: rows.map((row) => this.serializeAccountTemplate(row)) };
|
||||
}
|
||||
|
||||
async createBusinessAccountTemplate(
|
||||
businessIdRaw: string,
|
||||
dto: CreateInvoiceAccountTemplateDto,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
await this.assertBusinessExists(businessId);
|
||||
await this.assertPermission(businessId, actor.id, 'invoice_templates.manage');
|
||||
|
||||
const bankName = dto.bankName.trim();
|
||||
if (!bankName) {
|
||||
throw new BadRequestException('Bank name is required');
|
||||
}
|
||||
|
||||
const row = await this.prisma.invoiceAccountTemplate.create({
|
||||
data: {
|
||||
ownerScope: InvoiceOwnerScope.business,
|
||||
businessId,
|
||||
bankName,
|
||||
accountHolderName: dto.accountHolderName?.trim() || null,
|
||||
cardNumber: dto.cardNumber?.trim() || null,
|
||||
iban: dto.iban?.trim() || null,
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
},
|
||||
});
|
||||
return this.serializeAccountTemplate(row);
|
||||
}
|
||||
|
||||
async updateBusinessAccountTemplate(
|
||||
businessIdRaw: string,
|
||||
templateIdRaw: string,
|
||||
dto: UpdateInvoiceAccountTemplateDto,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
await this.assertBusinessExists(businessId);
|
||||
await this.assertPermission(businessId, actor.id, 'invoice_templates.manage');
|
||||
|
||||
const templateId = BigInt(templateIdRaw);
|
||||
const existing = await this.prisma.invoiceAccountTemplate.findFirst({
|
||||
where: { id: templateId, ownerScope: InvoiceOwnerScope.business, businessId },
|
||||
});
|
||||
if (!existing) {
|
||||
throw new NotFoundException('Invoice account template not found');
|
||||
}
|
||||
|
||||
const row = await this.prisma.invoiceAccountTemplate.update({
|
||||
where: { id: templateId },
|
||||
data: {
|
||||
...(dto.bankName !== undefined ? { bankName: dto.bankName.trim() } : {}),
|
||||
...(dto.accountHolderName !== undefined
|
||||
? { accountHolderName: dto.accountHolderName?.trim() || null }
|
||||
: {}),
|
||||
...(dto.cardNumber !== undefined
|
||||
? { cardNumber: dto.cardNumber?.trim() || null }
|
||||
: {}),
|
||||
...(dto.iban !== undefined ? { iban: dto.iban?.trim() || null } : {}),
|
||||
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
|
||||
...(dto.isActive !== undefined ? { isActive: dto.isActive } : {}),
|
||||
},
|
||||
});
|
||||
return this.serializeAccountTemplate(row);
|
||||
}
|
||||
|
||||
async deleteBusinessAccountTemplate(
|
||||
businessIdRaw: string,
|
||||
templateIdRaw: string,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
await this.assertBusinessExists(businessId);
|
||||
await this.assertPermission(businessId, actor.id, 'invoice_templates.manage');
|
||||
|
||||
const templateId = BigInt(templateIdRaw);
|
||||
const existing = await this.prisma.invoiceAccountTemplate.findFirst({
|
||||
where: { id: templateId, ownerScope: InvoiceOwnerScope.business, businessId },
|
||||
});
|
||||
if (!existing) {
|
||||
throw new NotFoundException('Invoice account template not found');
|
||||
}
|
||||
|
||||
await this.prisma.invoiceAccountTemplate.delete({ where: { id: templateId } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// --- Invoice templates (business-scoped) ---
|
||||
|
||||
async listBusinessInvoiceTemplates(businessIdRaw: string, actor: AuthUser) {
|
||||
|
||||
Reference in New Issue
Block a user