diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index 076693a5d..95b81a983 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -35,6 +35,7 @@ import { AccountsModule } from '../Accounts/Accounts.module'; import { ExpensesModule } from '../Expenses/Expenses.module'; import { ItemCategoryModule } from '../ItemCategories/ItemCategory.module'; import { TaxRatesModule } from '../TaxRates/TaxRate.module'; +import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module'; @Module({ imports: [ @@ -92,7 +93,8 @@ import { TaxRatesModule } from '../TaxRates/TaxRate.module'; ItemCategoryModule, AccountsModule, ExpensesModule, - TaxRatesModule + TaxRatesModule, + PdfTemplatesModule, ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/PdfTemplate/BrandingTemplateDTOTransformer.ts b/packages/server-nest/src/modules/PdfTemplate/BrandingTemplateDTOTransformer.ts new file mode 100644 index 000000000..01ef57a91 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/BrandingTemplateDTOTransformer.ts @@ -0,0 +1,37 @@ +// import { Inject, Service } from 'typedi'; +// import { isNil } from 'lodash'; +// import HasTenancyService from '../Tenancy/TenancyService'; + +// @Service() +// export class BrandingTemplateDTOTransformer { +// @Inject() +// private tenancy: HasTenancyService; + +// /** +// * Associates the default branding template id. +// * @param {number} tenantId +// * @param {string} resource +// * @param {Record} object +// * @param {string} attributeName +// * @returns +// */ +// public assocDefaultBrandingTemplate = +// (tenantId: number, resource: string) => +// async (object: Record) => { +// const { PdfTemplate } = this.tenancy.models(tenantId); +// const attributeName = 'pdfTemplateId'; + +// const defaultTemplate = await PdfTemplate.query() +// .modify('default') +// .findOne({ resource }); + +// // If the default template is not found OR the given object has no defined template id. +// if (!defaultTemplate || !isNil(object[attributeName])) { +// return object; +// } +// return { +// ...object, +// [attributeName]: defaultTemplate.id, +// }; +// }; +// } diff --git a/packages/server-nest/src/modules/PdfTemplate/GetPdfTemplateBrandingState.ts b/packages/server-nest/src/modules/PdfTemplate/GetPdfTemplateBrandingState.ts new file mode 100644 index 000000000..711d381c5 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/GetPdfTemplateBrandingState.ts @@ -0,0 +1,15 @@ +// import { Inject, Service } from 'typedi'; +// import { GetOrganizationBrandingAttributes } from './queries/GetOrganizationBrandingAttributes.service'; + +// @Service() +// export class GetPdfTemplateBrandingState { +// @Inject() +// private getOrgBrandingAttributes: GetOrganizationBrandingAttributes; + +// getBrandingState(tenantId: number) { +// const brandingAttributes = +// this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(tenantId); + +// return brandingAttributes; +// } +// } diff --git a/packages/server-nest/src/modules/PdfTemplate/PdfTemplate.application.ts b/packages/server-nest/src/modules/PdfTemplate/PdfTemplate.application.ts new file mode 100644 index 000000000..459595ffa --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/PdfTemplate.application.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; +import { ICreateInvoicePdfTemplateDTO, IEditPdfTemplateDTO } from './types'; +import { CreatePdfTemplateService } from './commands/CreatePdfTemplate.service'; +import { DeletePdfTemplateService } from './commands/DeletePdfTemplate.service'; +import { GetPdfTemplateService } from './queries/GetPdfTemplate.service'; +import { EditPdfTemplateService } from './commands/EditPdfTemplate.service'; +import { AssignPdfTemplateDefaultService } from './commands/AssignPdfTemplateDefault.service'; +import { GetOrganizationBrandingAttributesService } from './queries/GetOrganizationBrandingAttributes.service'; + +@Injectable() +export class PdfTemplateApplication { + constructor( + private readonly createPdfTemplateService: CreatePdfTemplateService, + private readonly deletePdfTemplateService: DeletePdfTemplateService, + private readonly getPdfTemplateService: GetPdfTemplateService, + // private readonly getPdfTemplatesService: GetPdfTemplatesService, + private readonly editPdfTemplateService: EditPdfTemplateService, + private readonly assignPdfTemplateDefaultService: AssignPdfTemplateDefaultService, + // private readonly getPdfTemplateBrandingStateService: GetPdfTemplateBrandingStateService, + // private readonly getOrganizationBrandingAttributesService: GetOrganizationBrandingAttributesService, + ) {} + + /** + * Creates a new PDF template. + * @param {string} templateName - The name of the PDF template to create. + * @param {string} resource - The resource type associated with the PDF template. + * @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO - The data transfer object containing the details for the new PDF template. + * @returns {Promise} + */ + public async createPdfTemplate( + templateName: string, + resource: string, + invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO, + ) { + return this.createPdfTemplateService.createPdfTemplate( + templateName, + resource, + invoiceTemplateDTO, + ); + } + + /** + * Deletes a PDF template. + * @param {number} templateId - The ID of the template to delete. + */ + public async deletePdfTemplate(templateId: number) { + return this.deletePdfTemplateService.deletePdfTemplate(templateId); + } + + /** + * Retrieves a specific PDF template. + * @param {number} templateId - The ID of the template to retrieve. + */ + public async getPdfTemplate(templateId: number) { + return this.getPdfTemplateService.getPdfTemplate(templateId); + } + + /** + * Retrieves all PDF templates. + * @param {string} resource - The resource type to filter templates. + */ + public async getPdfTemplates(resource: string) { + // return this.getPdfTemplatesService.execute(resource); + } + + /** + * Edits an existing PDF template. + * @param {number} templateId - The ID of the template to edit. + * @param {IEditPdfTemplateDTO} editDTO - The data transfer object containing the updates. + */ + public async editPdfTemplate( + templateId: number, + editDTO: IEditPdfTemplateDTO, + ) { + return this.editPdfTemplateService.editPdfTemplate(templateId, editDTO); + } + + /** + * Gets the PDF template branding state. + * @param {number} tenantId - The tenant ID. + */ + public async getPdfTemplateBrandingState(tenantId: number) { + // return this.getPdfTemplateBrandingStateService.execute(tenantId); + } + + /** + * Assigns a PDF template as the default template. + * @param {number} templateId - The ID of the PDF template to assign as default. + * @returns {Promise} + */ + public async assignPdfTemplateAsDefault(templateId: number) { + return this.assignPdfTemplateDefaultService.assignDefaultTemplate( + templateId, + ); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/PdfTemplates.controller.ts b/packages/server-nest/src/modules/PdfTemplate/PdfTemplates.controller.ts new file mode 100644 index 000000000..142f5e355 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/PdfTemplates.controller.ts @@ -0,0 +1,51 @@ +import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; +import { PdfTemplateApplication } from './PdfTemplate.application'; +import { ICreateInvoicePdfTemplateDTO, IEditPdfTemplateDTO } from './types'; +import { PublicRoute } from '../Auth/Jwt.guard'; + +@Controller('pdf-templates') +@PublicRoute() +export class PdfTemplatesController { + constructor(private readonly pdfTemplateApplication: PdfTemplateApplication) {} + + @Post() + async createPdfTemplate( + @Body('templateName') templateName: string, + @Body('resource') resource: string, + @Body() invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO, + ) { + return this.pdfTemplateApplication.createPdfTemplate( + templateName, + resource, + invoiceTemplateDTO, + ); + } + + @Delete(':id') + async deletePdfTemplate(@Param('id') templateId: number) { + return this.pdfTemplateApplication.deletePdfTemplate(templateId); + } + + @Get(':id') + async getPdfTemplate(@Param('id') templateId: number) { + return this.pdfTemplateApplication.getPdfTemplate(templateId); + } + + @Get() + async getPdfTemplates(@Body('resource') resource: string) { + return this.pdfTemplateApplication.getPdfTemplates(resource); + } + + @Put(':id') + async editPdfTemplate( + @Param('id') templateId: number, + @Body() editDTO: IEditPdfTemplateDTO, + ) { + return this.pdfTemplateApplication.editPdfTemplate(templateId, editDTO); + } + + @Put(':id/assign-default') + async assignPdfTemplateAsDefault(@Param('id') templateId: number) { + return this.pdfTemplateApplication.assignPdfTemplateAsDefault(templateId); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/PdfTemplates.module.ts b/packages/server-nest/src/modules/PdfTemplate/PdfTemplates.module.ts new file mode 100644 index 000000000..c58765230 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/PdfTemplates.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module'; +import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; +import { AssignPdfTemplateDefaultService } from './commands/AssignPdfTemplateDefault.service'; +import { CreatePdfTemplateService } from './commands/CreatePdfTemplate.service'; +import { DeletePdfTemplateService } from './commands/DeletePdfTemplate.service'; +import { EditPdfTemplateService } from './commands/EditPdfTemplate.service'; +import { PdfTemplateApplication } from './PdfTemplate.application'; +import { PdfTemplatesController } from './PdfTemplates.controller'; +import { GetPdfTemplateService } from './queries/GetPdfTemplate.service'; + +@Module({ + imports: [TenancyDatabaseModule], + controllers: [PdfTemplatesController], + providers: [ + PdfTemplateApplication, + CreatePdfTemplateService, + DeletePdfTemplateService, + GetPdfTemplateService, + EditPdfTemplateService, + AssignPdfTemplateDefaultService, + TenancyContext, + TransformerInjectable, + ], +}) +export class PdfTemplatesModule {} diff --git a/packages/server-nest/src/modules/PdfTemplate/commands/AssignPdfTemplateDefault.service.ts b/packages/server-nest/src/modules/PdfTemplate/commands/AssignPdfTemplateDefault.service.ts new file mode 100644 index 000000000..2b35066a9 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/commands/AssignPdfTemplateDefault.service.ts @@ -0,0 +1,55 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { PdfTemplateModel } from '../models/PdfTemplate'; +import { events } from '@/common/events/events'; + +@Injectable() +export class AssignPdfTemplateDefaultService { + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Assigns a default PDF template. + * @param {number} templateId - The ID of the template to be set as the default. + * @returns {Promise} A promise that resolves when the operation is complete. + * @throws {Error} Throws an error if the specified template is not found. + */ + public async assignDefaultTemplate(templateId: number) { + const oldPdfTemplate = await this.pdfTemplateModel.query() + .findById(templateId) + .throwIfNotFound(); + + return this.uow.withTransaction( + async (trx?: Knex.Transaction) => { + // Triggers `onPdfTemplateAssigningDefault` event. + await this.eventPublisher.emitAsync( + events.pdfTemplate.onAssigningDefault, + { + templateId, + } + ); + await this.pdfTemplateModel.query(trx) + .where('resource', oldPdfTemplate.resource) + .patch({ default: false }); + + await this.pdfTemplateModel.query(trx) + .findById(templateId) + .patch({ default: true }); + + // Triggers `onPdfTemplateAssignedDefault` event. + await this.eventPublisher.emitAsync( + events.pdfTemplate.onAssignedDefault, + { + templateId, + } + ); + } + ); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/commands/CreatePdfTemplate.service.ts b/packages/server-nest/src/modules/PdfTemplate/commands/CreatePdfTemplate.service.ts new file mode 100644 index 000000000..9d4c3d9fc --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/commands/CreatePdfTemplate.service.ts @@ -0,0 +1,47 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { ICreateInvoicePdfTemplateDTO } from '../types'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { PdfTemplateModel } from '../models/PdfTemplate'; + +@Injectable() +export class CreatePdfTemplateService { + constructor( + private readonly uow: UnitOfWork, + private readonly eventEmitter: EventEmitter2, + + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Creates a new pdf template. + * @param {string} templateName - Pdf template name. + * @param {string} resource - Pdf template resource. + * @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO - Invoice template data. + */ + public createPdfTemplate( + templateName: string, + resource: string, + invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO, + ) { + const attributes = invoiceTemplateDTO; + + return this.uow.withTransaction(async (trx) => { + // Triggers `onPdfTemplateCreating` event. + await this.eventEmitter.emitAsync(events.pdfTemplate.onCreating, {}); + + const pdfTemplate = await this.pdfTemplateModel.query(trx).insert({ + templateName, + resource, + attributes, + }); + + // Triggers `onPdfTemplateCreated` event. + await this.eventEmitter.emitAsync(events.pdfTemplate.onCreated, {}); + + return pdfTemplate; + }); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/commands/DeletePdfTemplate.service.ts b/packages/server-nest/src/modules/PdfTemplate/commands/DeletePdfTemplate.service.ts new file mode 100644 index 000000000..d3b501ddf --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/commands/DeletePdfTemplate.service.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ERRORS } from '../types'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { ServiceError } from '../../Items/ServiceError'; +import { PdfTemplateModel } from '../models/PdfTemplate'; + +@Injectable() +export class DeletePdfTemplateService { + constructor( + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + private readonly uow: UnitOfWork, + private readonly eventEmitter: EventEmitter2, + ) {} + + /** + * Deletes a pdf template. + * @param {number} templateId - Pdf template id. + */ + public async deletePdfTemplate(templateId: number) { + const oldPdfTemplate = await this.pdfTemplateModel.query() + .findById(templateId) + .throwIfNotFound(); + + // Cannot delete the predefined pdf templates. + if (oldPdfTemplate.predefined) { + throw new ServiceError(ERRORS.CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE); + } + + return this.uow.withTransaction(async (trx) => { + // Triggers `onPdfTemplateDeleting` event. + await this.eventEmitter.emitAsync(events.pdfTemplate.onDeleting, { + templateId, + oldPdfTemplate, + trx, + }); + await this.pdfTemplateModel.query(trx).deleteById(templateId); + + // Triggers `onPdfTemplateDeleted` event. + await this.eventEmitter.emitAsync(events.pdfTemplate.onDeleted, { + templateId, + oldPdfTemplate, + trx, + }); + }); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/commands/EditPdfTemplate.service.ts b/packages/server-nest/src/modules/PdfTemplate/commands/EditPdfTemplate.service.ts new file mode 100644 index 000000000..b6febaf9a --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/commands/EditPdfTemplate.service.ts @@ -0,0 +1,51 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { IEditPdfTemplateDTO } from '../types'; +import { PdfTemplateModel } from '../models/PdfTemplate'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; + +@Injectable() +export class EditPdfTemplateService { + constructor( + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + private readonly uow: UnitOfWork, + private readonly eventEmitter: EventEmitter2, + ) {} + + /** + * Edits an existing pdf template. + * @param {number} templateId - Template id. + * @param {IEditPdfTemplateDTO} editTemplateDTO + */ + public async editPdfTemplate( + templateId: number, + editTemplateDTO: IEditPdfTemplateDTO + ) { + const oldPdfTemplate = await this.pdfTemplateModel.query() + .findById(templateId) + .throwIfNotFound(); + + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onPdfTemplateEditing` event. + await this.eventEmitter.emitAsync(events.pdfTemplate.onEditing, { + templateId, + }); + + const pdfTemplate = await this.pdfTemplateModel.query(trx) + .where('id', templateId) + .update({ + templateName: editTemplateDTO.templateName, + attributes: editTemplateDTO.attributes, + }); + + // Triggers `onPdfTemplatedEdited` event. + await this.eventEmitter.emitAsync(events.pdfTemplate.onEdited, { + templateId, + }); + return pdfTemplate; + }); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/models/PdfTemplate.ts b/packages/server-nest/src/modules/PdfTemplate/models/PdfTemplate.ts new file mode 100644 index 000000000..9c315aedd --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/models/PdfTemplate.ts @@ -0,0 +1,77 @@ +// import { getUploadedObjectUri } from '@/services/Attachments/utils'; +import { BaseModel } from '@/models/Model'; +// import TenantModel from 'models/TenantModel'; + +export class PdfTemplateModel extends BaseModel { + public resource!: string; + public templateName!: string; + public predefined!: boolean; + public default!: boolean; + public attributes!: Record; + + /** + * Table name. + */ + static get tableName() { + return 'pdf_templates'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Json schema. + */ + static get jsonSchema() { + return { + type: 'object', + properties: { + id: { type: 'integer' }, + templateName: { type: 'string' }, + attributes: { type: 'object' }, // JSON field definition + }, + }; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Filters the due invoices. + */ + default(query) { + query.where('default', true); + }, + }; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['companyLogoUri']; + } + + /** + * Retrieves the company logo uri. + * @returns {string} + */ + // get companyLogoUri() { + // return this.attributes?.companyLogoKey + // ? getUploadedObjectUri(this.attributes.companyLogoKey) + // : ''; + // } + + /** + * Relationship mapping. + */ + static get relationMappings() { + return {}; + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service.ts b/packages/server-nest/src/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service.ts new file mode 100644 index 000000000..7722e3b41 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { CommonOrganizationBrandingAttributes } from '../types'; +import { TenancyContext } from '../../Tenancy/TenancyContext.service'; + +@Injectable() +export class GetOrganizationBrandingAttributesService { + constructor(private readonly tenancyContext: TenancyContext) {} + + /** + * Retrieves the given organization branding attributes initial state. + * @returns {Promise} + */ + public async getOrganizationBrandingAttributes(): Promise { + const tenant = await this.tenancyContext.getTenant(true); + const tenantMetadata = tenant.metadata; + + const companyName = tenantMetadata?.name; + const primaryColor = tenantMetadata?.primaryColor; + const companyLogoKey = tenantMetadata?.logoKey; + const companyLogoUri = tenantMetadata?.logoUri; + const companyAddress = tenantMetadata?.addressTextFormatted; + + return { + companyName, + companyAddress, + companyLogoUri, + companyLogoKey, + primaryColor, + }; + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplate.service.ts b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplate.service.ts new file mode 100644 index 000000000..edbef0206 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplate.service.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { GetPdfTemplateTransformer } from './GetPdfTemplate.transformer'; +import { PdfTemplateModel } from '../models/PdfTemplate'; +import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service'; + +@Injectable() +export class GetPdfTemplateService { + constructor( + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + private readonly transformer: TransformerInjectable, + ) {} + + /** + * Retrieves a pdf template by its ID. + * @param {number} templateId - The ID of the pdf template to retrieve. + * @return {Promise} - The retrieved pdf template. + */ + async getPdfTemplate( + templateId: number, + trx?: Knex.Transaction + ): Promise { + const template = await this.pdfTemplateModel.query(trx) + .findById(templateId) + .throwIfNotFound(); + + return this.transformer.transform( + template, + new GetPdfTemplateTransformer() + ); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplate.transformer.ts b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplate.transformer.ts new file mode 100644 index 000000000..986c0d496 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplate.transformer.ts @@ -0,0 +1,52 @@ +// import { getTransactionTypeLabel } from '@/utils/transactions-types'; +import { Transformer } from "../../Transformer/Transformer"; + +export class GetPdfTemplateTransformer extends Transformer { + /** + * Included attributes. + * @returns {string[]} + */ + public includeAttributes = (): string[] => { + return ['createdAtFormatted', 'resourceFormatted', 'attributes']; + }; + + /** + * Formats the creation date of the PDF template. + * @param {Object} template + * @returns {string} A formatted string representing the creation date of the template. + */ + protected createdAtFormatted = (template) => { + return this.formatDate(template.createdAt); + }; + + /** + * Formats the creation date of the PDF template. + * @param {Object} template - + * @returns {string} A formatted string representing the creation date of the template. + */ + protected resourceFormatted = (template) => { + // return getTransactionTypeLabel(template.resource); + }; + + /** + * Retrieves transformed brand attributes. + * @param {} template + * @returns + */ + protected attributes = (template) => { + return this.item( + template.attributes, + new GetPdfTemplateAttributesTransformer() + ); + }; +} + +class GetPdfTemplateAttributesTransformer extends Transformer { + /** + * Included attributes. + * @returns {string[]} + */ + public includeAttributes = (): string[] => { + return []; + }; +} diff --git a/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplates.service.ts b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplates.service.ts new file mode 100644 index 000000000..4fd7b8dc2 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplates.service.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { GetPdfTemplatesTransformer } from './GetPdfTemplates.transformer'; +import { PdfTemplateModel } from '../models/PdfTemplate'; +import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service'; + +@Injectable() +export class GetPdfTemplates { + constructor( + private readonly transformInjectable: TransformerInjectable, + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Retrieves a list of PDF templates for a specified tenant. + * @param {Object} [query] - Optional query parameters to filter the templates. + * @param {string} [query.resource] - The resource type to filter the templates by. + * @returns {Promise} - A promise that resolves to the transformed list of PDF templates. + */ + async getPdfTemplates(query?: { resource?: string }) { + const templates = await this.pdfTemplateModel.query().onBuild((q) => { + if (query?.resource) { + q.where('resource', query?.resource); + } + q.orderBy('createdAt', 'ASC'); + }); + + return this.transformInjectable.transform( + templates, + new GetPdfTemplatesTransformer(), + ); + } +} diff --git a/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplates.transformer.ts b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplates.transformer.ts new file mode 100644 index 000000000..d727dd661 --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/queries/GetPdfTemplates.transformer.ts @@ -0,0 +1,38 @@ +// import { getTransactionTypeLabel } from '@/utils/transactions-types'; +import { Transformer } from "../../Transformer/Transformer"; + +export class GetPdfTemplatesTransformer extends Transformer { + /** + * Exclude attributes. + * @returns {string[]} + */ + public excludeAttributes = (): string[] => { + return ['attributes']; + }; + + /** + * Includeded attributes. + * @returns {string[]} + */ + public includeAttributes = (): string[] => { + return ['createdAtFormatted', 'resourceFormatted']; + }; + + /** + * Formats the creation date of the PDF template. + * @param {Object} template + * @returns {string} A formatted string representing the creation date of the template. + */ + protected createdAtFormatted = (template) => { + return this.formatDate(template.createdAt); + }; + + /** + * Formats the creation date of the PDF template. + * @param {Object} template - + * @returns {string} A formatted string representing the creation date of the template. + */ + protected resourceFormatted = (template) => { + // return getTransactionTypeLabel(template.resource); + }; +} diff --git a/packages/server-nest/src/modules/PdfTemplate/types.ts b/packages/server-nest/src/modules/PdfTemplate/types.ts new file mode 100644 index 000000000..de1029afa --- /dev/null +++ b/packages/server-nest/src/modules/PdfTemplate/types.ts @@ -0,0 +1,75 @@ +export enum ERRORS { + CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE = 'CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE', +} + +export interface IEditPdfTemplateDTO { + templateName: string; + attributes: Record; +} + +export interface ICreateInvoicePdfTemplateDTO { + // Colors + primaryColor?: string; + secondaryColor?: string; + + // Company Logo + showCompanyLogo?: boolean; + companyLogo?: string; + + // Top details. + showInvoiceNumber?: boolean; + invoiceNumberLabel?: string; + + showDateIssue?: boolean; + dateIssueLabel?: string; + + showDueDate?: boolean; + dueDateLabel?: string; + + // Company name + companyName?: string; + + // Addresses + showBilledFromAddress?: boolean; + showBillingToAddress?: boolean; + billedToLabel?: string; + + // Entries + itemNameLabel?: string; + itemDescriptionLabel?: string; + itemRateLabel?: string; + itemTotalLabel?: string; + + // Totals + showSubtotal?: boolean; + subtotalLabel?: string; + + showDiscount?: boolean; + discountLabel?: string; + + showTaxes?: boolean; + + showTotal?: boolean; + totalLabel?: string; + + paymentMadeLabel?: string; + showPaymentMade?: boolean; + + dueAmountLabel?: string; + showDueAmount?: boolean; + + // Footer paragraphs. + termsConditionsLabel?: string; + showTermsConditions?: boolean; + + statementLabel?: string; + showStatement?: boolean; +} + +export interface CommonOrganizationBrandingAttributes { + companyName?: string; + primaryColor?: string; + companyLogoKey?: string; + companyLogoUri?: string; + companyAddress?: string; +} diff --git a/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts b/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts index c453e86a4..9829dccc3 100644 --- a/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts +++ b/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts @@ -10,6 +10,7 @@ import { Expense } from '@/modules/Expenses/models/Expense.model'; import ExpenseCategory from '@/modules/Expenses/models/ExpenseCategory.model'; import { ItemCategory } from '@/modules/ItemCategories/models/ItemCategory.model'; import { TaxRateModel } from '@/modules/TaxRates/models/TaxRate.model'; +import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; const models = [ Item, @@ -19,7 +20,8 @@ const models = [ Expense, ExpenseCategory, ItemCategory, - TaxRateModel + TaxRateModel, + PdfTemplateModel ]; const modelProviders = models.map((model) => { diff --git a/packages/server-nest/test/pdf-templates.e2e-spec.ts b/packages/server-nest/test/pdf-templates.e2e-spec.ts new file mode 100644 index 000000000..008ca9966 --- /dev/null +++ b/packages/server-nest/test/pdf-templates.e2e-spec.ts @@ -0,0 +1,68 @@ +import * as request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { app } from './init-app-test'; + +describe('Pdf Templates (e2e)', () => { + it('/pdf-templates (POST)', () => { + return request(app.getHttpServer()) + .post('/pdf-templates') + .set('organization-id', '4064541lv40nhca') + .send({ + template_name: 'Standard', + resource: 'SaleInvoice', + attributes: {}, + }) + .expect(201); + }); + + it('/pdf-templates (DELETE)', async () => { + const response = await request(app.getHttpServer()) + .post('/pdf-templates') + .set('organization-id', '4064541lv40nhca') + .send({ + template_name: 'Standard', + resource: 'SaleInvoice', + attributes: {}, + }); + const pdfTemplateId = response.body.id; + + return request(app.getHttpServer()) + .delete(`/pdf-templates/${pdfTemplateId}`) + .set('organization-id', '4064541lv40nhca') + .expect(200); + }); + + it('/pdf-templates/:id (GET)', async () => { + const response = await request(app.getHttpServer()) + .post('/pdf-templates') + .set('organization-id', '4064541lv40nhca') + .send({ + template_name: 'Standard', + resource: 'SaleInvoice', + attributes: {}, + }); + const pdfTemplateId = response.body.id; + + return request(app.getHttpServer()) + .get(`/pdf-templates/${pdfTemplateId}`) + .set('organization-id', '4064541lv40nhca') + .expect(200); + }); + + it('/pdf-templates/:id/assign-default (POST)', async () => { + const response = await request(app.getHttpServer()) + .post('/pdf-templates') + .set('organization-id', '4064541lv40nhca') + .send({ + template_name: 'Standard', + resource: 'SaleInvoice', + attributes: {}, + }); + const pdfTemplateId = response.body.id; + + return request(app.getHttpServer()) + .put(`/pdf-templates/${pdfTemplateId}/assign-default`) + .set('organization-id', '4064541lv40nhca') + .expect(200); + }); +});