feat(server): add pdf template crud endpoints

This commit is contained in:
Ahmed Bouhuolia
2024-09-11 14:54:13 +02:00
parent 5b6270a184
commit c0769662bd
14 changed files with 542 additions and 2 deletions

View File

@@ -0,0 +1,129 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import BaseController from '@/api/controllers/BaseController';
import { PdfTemplateApplication } from '@/services/PdfTemplate/PdfTemplateApplication';
@Service()
export class PdfTemplatesController extends BaseController {
@Inject()
public pdfTemplateApplication: PdfTemplateApplication;
/**
* Router constructor method.
*/
public router() {
const router = Router();
router.delete(
'/:template_id',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.deletePdfTemplate.bind(this)
);
router.put(
'/:template_id',
[
param('template_id').exists().isInt().toInt(),
check('attributes').exists(),
],
this.validationResult,
this.editPdfTemplate.bind(this)
);
router.get(
'/:template_id',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.getPdfTemplate.bind(this)
);
router.get('/', this.getPdfTemplates.bind(this));
router.post(
'/invoices',
[check('template_name').exists(), check('attributes').exists()],
this.validationResult,
this.createPdfInvoiceTemplate.bind(this)
);
return router;
}
async createPdfInvoiceTemplate(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { templateName, attributes } = this.matchedBodyData(req);
try {
const result = await this.pdfTemplateApplication.createPdfTemplate(
tenantId,
templateName,
attributes
);
return res.status(201).send(result);
} catch (error) {
next(error);
}
}
async editPdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
const { attributes } = this.matchedBodyData(req);
try {
const result = await this.pdfTemplateApplication.editPdfTemplate(
tenantId,
Number(templateId),
attributes
);
return res.status(200).send(result);
} catch (error) {
next(error);
}
}
async deletePdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
await this.pdfTemplateApplication.deletePdfTemplate(
tenantId,
Number(templateId)
);
return res.status(204).send();
} catch (error) {
next(error);
}
}
async getPdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
const template = await this.pdfTemplateApplication.getPdfTemplate(
tenantId,
Number(templateId)
);
return res.status(200).send(template);
} catch (error) {
next(error);
}
}
async getPdfTemplates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const templates = await this.pdfTemplateApplication.getPdfTemplates(
tenantId
);
return res.status(200).send(templates);
} catch (error) {
next(error);
}
}
}

View File

@@ -64,6 +64,7 @@ import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
import { PdfTemplatesController } from './controllers/PdfTemplates/PdfTemplatesController';
export default () => {
const app = Router();
@@ -81,7 +82,7 @@ export default () => {
app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router())
app.use('/demo', Container.get(OneClickDemoController).router());
// - Dashboard routes.
// ---------------------------
@@ -147,6 +148,10 @@ export default () => {
dashboard.use('/import', Container.get(ImportController).router());
dashboard.use('/export', Container.get(ExportController).router());
dashboard.use('/attachments', Container.get(AttachmentsController).router());
dashboard.use(
'/pdf_templates',
Container.get(PdfTemplatesController).router()
);
dashboard.use('/', Container.get(ProjectTasksController).router());
dashboard.use('/', Container.get(ProjectTimesController).router());

View File

@@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable('pdf_templates', (table) => {
table.increments('id').primary();
table.text('resource');
table.text('template_name');
table.json('attributes');
table.timestamps();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('pdf_templates');
};

View File

@@ -68,6 +68,7 @@ import { BankRule } from '@/models/BankRule';
import { BankRuleCondition } from '@/models/BankRuleCondition';
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
import { PdfTemplate } from '@/models/PdfTemplate';
export default (knex) => {
const models = {
@@ -139,6 +140,7 @@ export default (knex) => {
BankRuleCondition,
RecognizedBankTransaction,
MatchedBankTransaction,
PdfTemplate
};
return mapValues(models, (model) => model.bindKnex(knex));
};

View File

@@ -0,0 +1,42 @@
import TenantModel from 'models/TenantModel';
export class PdfTemplate extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'pdf_templates';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
templateName: { type: 'string' },
attributes: { type: 'object' }, // JSON field definition
},
};
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
return {};
}
}

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { ICreateInvoicePdfTemplateDTO } from './types';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class CreatePdfTemplate {
@Inject()
private tennacy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a new pdf template.
* @param {number} tenantId
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO
*/
public createPdfTemplate(
tenantId: number,
templateName: string,
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
) {
const { PdfTemplate } = this.tennacy.models(tenantId);
const resource = 'SaleInvoice';
const attributes = invoiceTemplateDTO;
return this.uow.withTransaction(tenantId, async (trx) => {
await PdfTemplate.query(trx).insert({
templateName,
resource,
attributes,
});
await this.eventPublisher.emitAsync(events.pdfTemplate.onInvoiceCreated, {
tenantId,
});
});
}
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class DeletePdfTemplate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes a pdf template.
* @param {number} tenantId
* @param {number} templateId
*/
public deletePdfTemplate(tenantId: number, templateId: number) {
const { PdfTemplate } = this.tenancy.models(tenantId);
return this.uow.withTransaction(tenantId, async (trx) => {
await PdfTemplate.query(trx).deleteById(templateId);
await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleted, {
tenantId,
templateId,
});
});
}
}

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { IEditPdfTemplateDTO } from './types';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class EditPdfTemplate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Edits an existing pdf template.
* @param {number} tenantId
* @param {number} templateId
* @param {IEditPdfTemplateDTO} editTemplateDTO
*/
public editPdfTemplate(
tenantId: number,
templateId: number,
editTemplateDTO: IEditPdfTemplateDTO
) {
const { PdfTemplate } = this.tenancy.models(tenantId);
return this.uow.withTransaction(tenantId, async (trx) => {
await PdfTemplate.query(trx)
.patch({
...editTemplateDTO,
})
.where('id', templateId);
await this.eventPublisher.emitAsync(events.pdfTemplate.onEdited, {
tenantId,
templateId,
});
});
}
}

View File

@@ -0,0 +1,29 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class GetPdfTemplate {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves a pdf template by its ID.
* @param {number} tenantId - The ID of the tenant.
* @param {number} templateId - The ID of the pdf template to retrieve.
* @return {Promise<any>} - The retrieved pdf template.
*/
async getPdfTemplate(
tenantId: number,
templateId: number,
trx?: Knex.Transaction
): Promise<any> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const template = await PdfTemplate.query(trx)
.findById(templateId)
.throwIfNotFound();
return template;
}
}

View File

@@ -0,0 +1,37 @@
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '../Tenancy/TenancyService';
import { GetPdfTemplatesTransformer } from './GetPdfTemplatesTransformer';
import { Inject, Service } from 'typedi';
@Service()
export class GetPdfTemplates {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformInjectable: TransformerInjectable;
/**
* Retrieves a list of PDF templates for a specified tenant.
* @param {number} tenantId - The ID of the tenant for which to retrieve templates.
* @param {Object} [query] - Optional query parameters to filter the templates.
* @param {string} [query.resource] - The resource type to filter the templates by.
* @returns {Promise<any>} - A promise that resolves to the transformed list of PDF templates.
*/
async getPdfTemplates(tenantId: number, query?: { resource?: string }) {
const { PdfTemplate } = this.tenancy.models(tenantId);
const templates = await PdfTemplate.query().onBuild((q) => {
if (query?.resource) {
q.where('resource', query?.resource);
}
q.orderBy('createdAt', 'ASC');
});
return this.transformInjectable.transform(
tenantId,
templates,
new GetPdfTemplatesTransformer()
);
}
}

View File

@@ -0,0 +1,13 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetPdfTemplatesTransformer extends Transformer {
// Empty transformer with no additional methods or attributes
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['attributes'];
};
}

View File

@@ -0,0 +1,67 @@
import { Inject, Service } from 'typedi';
import { ICreateInvoicePdfTemplateDTO } from './types';
import { CreatePdfTemplate } from './CreatePdfTemplate';
import { DeletePdfTemplate } from './DeletePdfTemplate';
import { GetPdfTemplate } from './GetPdfTemplate';
import { GetPdfTemplates } from './GetPdfTemplates';
import { EditPdfTemplate } from './EditPdfTemplate';
@Service()
export class PdfTemplateApplication {
@Inject()
private createPdfTemplateService: CreatePdfTemplate;
@Inject()
private deletePdfTemplateService: DeletePdfTemplate;
@Inject()
private getPdfTemplateService: GetPdfTemplate;
@Inject()
private getPdfTemplatesService: GetPdfTemplates;
@Inject()
private editPdfTemplateService: EditPdfTemplate;
public async createPdfTemplate(
tenantId: number,
templateName: string,
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
) {
return this.createPdfTemplateService.createPdfTemplate(
tenantId,
templateName,
invoiceTemplateDTO
);
}
public async editPdfTemplate(
tenantId: number,
templateId: number,
editTemplateDTO: IEditPdfTemplateDTO
) {
return this.editPdfTemplateService.editPdfTemplate(
tenantId,
templateId,
editTemplateDTO
);
}
public async deletePdfTemplate(tenantId: number, templateId: number) {
return this.deletePdfTemplateService.deletePdfTemplate(
tenantId,
templateId
);
}
public async getPdfTemplate(tenantId: number, templateId: number) {
return this.getPdfTemplateService.getPdfTemplate(tenantId, templateId);
}
public async getPdfTemplates(
tenantId: number,
query?: { resource?: string }
) {
return this.getPdfTemplatesService.getPdfTemplates(tenantId, query);
}
}

View File

@@ -0,0 +1,58 @@
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;
}

View File

@@ -58,7 +58,7 @@ export default {
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed'
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed',
},
/**
@@ -684,4 +684,16 @@ export default {
import: {
onImportCommitted: 'onImportFileCommitted',
},
pdfTemplate: {
onCreating: 'onPdfTemplateCreating',
onCreated: 'onPdfTemplateCreated',
onEditing: 'onPdfTemplateEditing',
onEdited: 'onPdfTemplatedEdited',
onDeleted: 'onPdfTemplateDeleted',
onInvoiceCreated: 'onInvoicePdfTemplateCreated',
},
};