refactor: mail templates

This commit is contained in:
Ahmed Bouhuolia
2025-06-08 16:49:03 +02:00
parent 0a57b6e20e
commit 4366bf478a
44 changed files with 1866 additions and 134 deletions

View File

@@ -1,6 +1,4 @@
import {
IPaymentReceivedCreateDTO,
IPaymentReceivedEditDTO,
IPaymentsReceivedFilter,
PaymentReceiveMailOptsDTO,
} from './types/PaymentReceived.types';
@@ -79,7 +77,9 @@ export class PaymentReceivesApplication {
* @param {IPaymentsReceivedFilter} filterDTO
* @returns
*/
public async getPaymentsReceived(filterDTO: Partial<IPaymentsReceivedFilter>) {
public async getPaymentsReceived(
filterDTO: Partial<IPaymentsReceivedFilter>,
) {
return this.getPaymentsReceivedService.getPaymentReceives(filterDTO);
}
@@ -142,6 +142,17 @@ export class PaymentReceivesApplication {
);
}
/**
* Retrieves html content of the given payment receive.
* @param {number} paymentReceivedId
* @returns {Promise<string>}
*/
public getPaymentReceivedHtml(paymentReceivedId: number) {
return this.getPaymentReceivePdfService.getPaymentReceivedHtml(
paymentReceivedId,
);
}
/**
* Retrieves the create/edit initial state of the payment received.
* @returns {Promise<IPaymentReceivedState>}

View File

@@ -144,7 +144,7 @@ export class PaymentReceivesController {
description:
'The payment received details have been successfully retrieved.',
})
public getPaymentReceive(
public async getPaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number,
@Headers('accept') acceptHeader: string,
) {
@@ -152,6 +152,12 @@ export class PaymentReceivesController {
return this.paymentReceivesApplication.getPaymentReceivePdf(
paymentReceiveId,
);
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent =
await this.paymentReceivesApplication.getPaymentReceivedHtml(
paymentReceiveId,
);
return { htmlContent };
} else {
return this.paymentReceivesApplication.getPaymentReceive(
paymentReceiveId,

View File

@@ -4,19 +4,16 @@ export const SEND_PAYMENT_RECEIVED_MAIL_JOB = 'SEND_PAYMENT_RECEIVED_MAIL_JOB';
export const DEFAULT_PAYMENT_MAIL_SUBJECT =
'Payment Received for {Customer Name} from {Company Name}';
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
<p>
Payment Date : <strong>{Payment Date}</strong><br />
Amount : <strong>{Payment Amount}</strong></br />
</p>
export const DEFAULT_PAYMENT_MAIL_CONTENT = `Dear {Customer Name}
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!
Payment Transaction: {Payment Number}
Payment Date : {Payment Date}
Amount : {Payment Amount}
Regards,
{Company Name}`;
export const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',

View File

@@ -76,7 +76,10 @@ export class PaymentReceived extends TenantBaseModel {
const { Customer } = require('../../Customers/models/Customer');
const { Account } = require('../../Accounts/models/Account.model');
const { Branch } = require('../../Branches/models/Branch.model');
const { DocumentModel } = require('../../Attachments/models/Document.model');
const {
DocumentModel,
} = require('../../Attachments/models/Document.model');
const { PdfTemplateModel } = require('../../PdfTemplate/models/PdfTemplate');
return {
customer: {
@@ -154,6 +157,18 @@ export class PaymentReceived extends TenantBaseModel {
query.where('model_ref', 'PaymentReceive');
},
},
/**
* Payment received may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplateModel,
join: {
from: 'payment_receives.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}

View File

@@ -0,0 +1,48 @@
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { GetPaymentReceivedMailStateTransformer } from './GetPaymentReceivedMailState.transformer';
import { SendPaymentReceiveMailNotification } from '../commands/PaymentReceivedMailNotification';
import { PaymentReceived } from '../models/PaymentReceived';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { PaymentReceiveMailOpts } from '../types/PaymentReceived.types';
@Injectable()
export class GetPaymentReceivedMailState {
constructor(
private readonly paymentReceivedMail: SendPaymentReceiveMailNotification,
private readonly transformer: TransformerInjectable,
@Inject(PaymentReceived.name)
private readonly paymentReceivedModel: TenantModelProxy<
typeof PaymentReceived
>,
) {}
/**
* Retrieves the default payment mail options.
* @param {number} paymentReceiveId - Payment receive id.
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public getMailOptions = async (
paymentId: number,
): Promise<PaymentReceiveMailOpts> => {
const paymentReceive = await this.paymentReceivedModel()
.query()
.findById(paymentId)
.withGraphFetched('customer')
.withGraphFetched('entries.invoice')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions =
await this.paymentReceivedMail.getMailOptions(paymentId);
const transformed = await this.transformer.transform(
paymentReceive,
new GetPaymentReceivedMailStateTransformer(),
{
mailOptions,
},
);
return transformed;
};
}

View File

@@ -0,0 +1,185 @@
import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer';
import { PaymentReceivedEntryTransfromer } from './PaymentReceivedEntryTransformer';
export class GetPaymentReceivedMailStateTransformer extends PaymentReceiveTransfromer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'paymentDate',
'paymentDateFormatted',
'paymentAmount',
'paymentAmountFormatted',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'paymentNumber',
'entries',
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
];
};
/**
* Retrieves the customer name of the payment.
* @returns {string}
*/
protected customerName = (payment) => {
return payment.customer.displayName;
};
/**
* Retrieves the company name.
* @returns {string}
*/
protected companyName = () => {
return this.context.organization.name;
};
/**
* Retrieves the company logo uri.
* @returns {string | null}
*/
protected companyLogoUri = (payment) => {
return payment.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (payment) => {
return payment.pdfTemplate?.attributes?.primaryColor;
};
/**
* Retrieves the formatted payment date.
* @returns {string}
*/
protected paymentDateFormatted = (payment) => {
return this.formatDate(payment.paymentDate);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected total = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
*/
protected totalFormatted = (payment) => {
return this.formatMoney(payment.amount);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected subtotal = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
*/
protected subtotalFormatted = (payment) => {
return this.formatMoney(payment.amount);
};
/**
* Retrieves the payment number.
* @param payment
* @returns {string}
*/
protected paymentNumber = (payment) => {
return payment.paymentReceiveNo;
};
/**
* Retrieves the payment entries.
* @param {IPaymentReceived} payment
* @returns {IPaymentReceivedEntry[]}
*/
protected entries = (payment) => {
return this.item(payment.entries, new GetPaymentReceivedEntryMailState());
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
export class GetPaymentReceivedEntryMailState extends PaymentReceivedEntryTransfromer {
/**
* Include these attributes to payment receive entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['paidAmount', 'invoiceNumber'];
};
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieves the paid amount.
* @param entry
* @returns {string}
*/
public paidAmount = (entry) => {
return this.paymentAmountFormatted(entry);
};
/**
* Retrieves the invoice number.
* @param entry
* @returns {string}
*/
public invoiceNumber = (entry) => {
return entry.invoice.invoiceNo;
};
}

View File

@@ -0,0 +1,62 @@
import {
PaymentReceivedEmailTemplateProps,
renderPaymentReceivedEmailTemplate,
} from '@bigcapital/email-components';
import { Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
import { GetPaymentReceivedService } from './GetPaymentReceived.service';
import { GetPaymentReceivedMailTemplateAttrsTransformer } from './GetPaymentReceivedMailTemplateAttrs.transformer';
@Injectable()
export class GetPaymentReceivedMailTemplate {
constructor(
private readonly getPaymentReceivedService: GetPaymentReceivedService,
private readonly getBrandingTemplate: GetPdfTemplateService,
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieves the mail template attributes of the given payment received.
* @param {number} paymentReceivedId - Payment received id.
* @returns {Promise<PaymentReceivedEmailTemplateProps>}
*/
public async getMailTemplateAttributes(
paymentReceivedId: number,
): Promise<PaymentReceivedEmailTemplateProps> {
const paymentReceived =
await this.getPaymentReceivedService.getPaymentReceive(paymentReceivedId);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
paymentReceived.pdfTemplateId,
);
const mailTemplateAttributes = await this.transformer.transform(
paymentReceived,
new GetPaymentReceivedMailTemplateAttrsTransformer(),
{
paymentReceived,
brandingTemplate,
},
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @param {Partial<PaymentReceivedEmailTemplateProps>} overrideAttributes
* @returns
*/
public async getMailTemplate(
paymentReceivedId: number,
overrideAttributes?: Partial<PaymentReceivedEmailTemplateProps>,
): Promise<string> {
const mailTemplateAttributes =
await this.getMailTemplateAttributes(paymentReceivedId);
const mergedAttributes = {
...mailTemplateAttributes,
...overrideAttributes,
};
return renderPaymentReceivedEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,149 @@
import { Transformer } from "@/modules/Transformer/Transformer";
export class GetPaymentReceivedMailTemplateAttrsTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'primaryColor',
'total',
'totalLabel',
'subtotal',
'subtotalLabel',
'paymentNumberLabel',
'paymentNumber',
'items',
];
};
/**
* Exclude all attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Company logo uri.
* @returns {string}
*/
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
/**
* Company name.
* @returns {string}
*/
public companyName(): string {
return this.context.organization.name;
}
/**
* Primary color
* @returns {string}
*/
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
/**
* Total.
* @returns {string}
*/
public total(): string {
return this.options.paymentReceived.formattedAmount;
}
/**
* Total label.
* @returns {string}
*/
public totalLabel(): string {
return 'Total';
}
/**
* Subtotal.
* @returns {string}
*/
public subtotal(): string {
return this.options.paymentReceived.formattedAmount;
}
/**
* Subtotal label.
* @returns {string}
*/
public subtotalLabel(): string {
return 'Subtotal';
}
/**
* Payment number label.
* @returns {string}
*/
public paymentNumberLabel(): string {
return 'Payment # {paymentNumber}';
}
/**
* Payment number.
* @returns {string}
*/
public paymentNumber(): string {
return this.options.paymentReceived.paymentReceiveNumber;
}
/**
* Items.
* @returns
*/
public items() {
return this.item(
this.options.paymentReceived.entries,
new GetPaymentReceivedMailTemplateItemAttrsTransformer()
);
}
}
class GetPaymentReceivedMailTemplateItemAttrsTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = () => {
return ['label', 'total'];
};
/**
* Excluded attributes.
* @returns {string[]}
*/
public excludeAttributes = () => {
return ['*'];
};
/**
*
* @param entry
* @returns
*/
public label(entry) {
return entry.invoice.invoiceNo;
}
/**
*
* @param entry
* @returns
*/
public total(entry) {
return entry.paymentAmountFormatted;
}
}

View File

@@ -1,22 +1,20 @@
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates';
import { GetPaymentReceivedService } from './GetPaymentReceived.service';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate.service';
import { transformPaymentReceivedToPdfTemplate } from '../utils';
import { PaymentReceived } from '../models/PaymentReceived';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
import { PaymentReceivedPdfTemplateAttributes } from '../types/PaymentReceived.types';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { events } from '@/common/events/events';
@Injectable()
export class GetPaymentReceivedPdfService {
constructor(
private chromiumlyTenancy: ChromiumlyTenancy,
private templateInjectable: TemplateInjectable,
private getPaymentService: GetPaymentReceivedService,
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate,
private eventPublisher: EventEmitter2,
@@ -28,23 +26,31 @@ export class GetPaymentReceivedPdfService {
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
) {}
/**
* Retrieves payment received html content.
* @param {number} paymentReceivedId
* @returns {Promise<string>}
*/
public async getPaymentReceivedHtml(
paymentReceivedId: number,
): Promise<string> {
const brandingAttributes =
await this.getPaymentBrandingAttributes(paymentReceivedId);
return renderPaymentReceivedPaperTemplateHtml(brandingAttributes);
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
* @param {IPaymentReceived} paymentReceive -
* @param {number} paymentReceivedId - Payment received id.
* @returns {Promise<Buffer>}
*/
async getPaymentReceivePdf(
paymentReceivedId: number,
): Promise<[Buffer, string]> {
const brandingAttributes =
await this.getPaymentBrandingAttributes(paymentReceivedId);
const htmlContent = await this.templateInjectable.render(
'modules/payment-receive-standard',
brandingAttributes,
);
const htmlContent = await this.getPaymentReceivedHtml(paymentReceivedId);
const filename = await this.getPaymentReceivedFilename(paymentReceivedId);
// Converts the given html content to pdf document.
const content =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
@@ -98,7 +104,6 @@ export class GetPaymentReceivedPdfService {
await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate(
templateId,
);
return {
...brandingTemplate.attributes,
...transformPaymentReceivedToPdfTemplate(paymentReceived),

View File

@@ -130,23 +130,9 @@ export enum PaymentReceiveAction {
NotifyBySms = 'NotifyBySms',
}
// export type IPaymentReceiveGLCommonEntry = Pick<
// ILedgerEntry,
// | 'debit'
// | 'credit'
// | 'currencyCode'
// | 'exchangeRate'
// | 'transactionId'
// | 'transactionType'
// | 'transactionNumber'
// | 'referenceNumber'
// | 'date'
// | 'userId'
// | 'createdAt'
// | 'branchId'
// >;
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
export interface PaymentReceiveMailOpts extends CommonMailOptions {
attachPdf?: boolean;
}
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
export interface PaymentReceiveMailPresendEvent {
paymentReceivedId: number;