From 4366bf478a5a67381ac5e5b88ea5fc4f7adf64c5 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 8 Jun 2025 16:49:03 +0200 Subject: [PATCH] refactor: mail templates --- .../queries/CreditNoteTransformer.ts | 2 - .../dtos/CommonMailOptions.dto.ts | 37 +++ .../PaymentReceived.application.ts | 17 +- .../PaymentsReceived.controller.ts | 8 +- .../src/modules/PaymentReceived/constants.ts | 21 +- .../PaymentReceived/models/PaymentReceived.ts | 17 +- .../GetPaymentReceivedMailState.service.ts | 48 ++++ ...GetPaymentReceivedMailState.transformer.ts | 185 +++++++++++++++ .../GetPaymentReceivedMailTemplate.service.ts | 62 +++++ ...ntReceivedMailTemplateAttrs.transformer.ts | 149 ++++++++++++ .../queries/GetPaymentReceivedPdf.service.ts | 35 +-- .../types/PaymentReceived.types.ts | 20 +- .../SaleEstimates.application.ts | 21 ++ .../SaleEstimates/SaleEstimates.controller.ts | 11 +- .../SaleEstimates/SaleEstimates.module.ts | 10 +- .../commands/SendSaleEstimateMail.ts | 49 +++- .../src/modules/SaleEstimates/constants.ts | 23 +- .../SaleEstimates/models/SaleEstimate.ts | 7 - ...imateMailTemplateAttributes.transformer.ts | 167 ++++++++++++++ .../GetSaleEstimateMailState.service.ts | 46 ++++ .../GetSaleEstimateMailState.transformer.ts | 171 ++++++++++++++ .../GetSaleEstimateMailTemplate.service.ts | 61 +++++ .../queries/GetSaleEstimatePdf.ts | 27 ++- .../SaleInvoices/SaleInvoices.controller.ts | 5 +- .../queries/GetInvoicePaymentMail.service.ts | 2 +- ...nvoicePaymentMailAttributes.transformer.ts | 37 ++- .../GetSaleInvoiceMailState.transformer.ts | 22 +- .../queries/SaleInvoice.transformer.ts | 37 +++ .../queries/SaleInvoicePdf.service.ts | 1 - .../server/src/modules/SaleInvoices/utils.ts | 5 +- .../SaleReceiptApplication.service.ts | 26 ++- .../SaleReceipts/SaleReceipts.controller.ts | 5 + .../SaleReceipts/SaleReceipts.module.ts | 4 + .../commands/SaleReceiptMailNotification.ts | 19 +- .../src/modules/SaleReceipts/constants.ts | 24 +- .../GetSaleReceiptMailState.service.ts | 37 +++ .../GetSaleReceiptMailState.transformer.ts | 216 ++++++++++++++++++ .../GetSaleReceiptMailTemplate.service.ts | 60 +++++ .../GetSaleReceiptMailTemplate.transformer.ts | 201 ++++++++++++++++ .../queries/SaleReceiptTransformer.ts | 69 ++++++ .../queries/SaleReceiptsPdf.service.ts | 21 +- .../server/src/modules/SaleReceipts/utils.ts | 9 +- packages/webapp/src/hooks/query/estimates.tsx | 2 +- packages/webapp/src/hooks/query/invoices.tsx | 4 +- 44 files changed, 1866 insertions(+), 134 deletions(-) create mode 100644 packages/server/src/modules/MailNotification/dtos/CommonMailOptions.dto.ts create mode 100644 packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.service.ts create mode 100644 packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.transformer.ts create mode 100644 packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplate.service.ts create mode 100644 packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplateAttrs.transformer.ts create mode 100644 packages/server/src/modules/SaleEstimates/queries/GetEstimateMailTemplateAttributes.transformer.ts create mode 100644 packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.service.ts create mode 100644 packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.transformer.ts create mode 100644 packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailTemplate.service.ts create mode 100644 packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.service.ts create mode 100644 packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.transformer.ts create mode 100644 packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.service.ts create mode 100644 packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.transformer.ts diff --git a/packages/server/src/modules/CreditNotes/queries/CreditNoteTransformer.ts b/packages/server/src/modules/CreditNotes/queries/CreditNoteTransformer.ts index a15066b5d..b04b1bb6b 100644 --- a/packages/server/src/modules/CreditNotes/queries/CreditNoteTransformer.ts +++ b/packages/server/src/modules/CreditNotes/queries/CreditNoteTransformer.ts @@ -119,7 +119,6 @@ export class CreditNoteTransformer extends Transformer { /** * Retrieves formatted discount percentage. - * @param credit * @returns {string} */ protected discountPercentageFormatted = (credit): string => { @@ -128,7 +127,6 @@ export class CreditNoteTransformer extends Transformer { /** * Retrieves formatted adjustment amount. - * @param credit * @returns {string} */ protected adjustmentFormatted = (credit): string => { diff --git a/packages/server/src/modules/MailNotification/dtos/CommonMailOptions.dto.ts b/packages/server/src/modules/MailNotification/dtos/CommonMailOptions.dto.ts new file mode 100644 index 000000000..054f0f7db --- /dev/null +++ b/packages/server/src/modules/MailNotification/dtos/CommonMailOptions.dto.ts @@ -0,0 +1,37 @@ +import { ArrayMinSize, IsArray, IsNotEmpty, IsObject, IsString } from "class-validator"; +import { AddressItem } from "../MailNotification.types"; + +export class CommonMailOptionsDto { + @IsArray() + @ArrayMinSize(1) + @IsNotEmpty() + from: Array; + + @IsString() + @IsNotEmpty() + subject: string; + + @IsString() + @IsNotEmpty() + message: string; + + @IsArray() + @ArrayMinSize(1) + @IsNotEmpty() + to: Array; + + @IsArray() + cc?: Array; + + @IsArray() + bcc?: Array; + + @IsObject() + formatArgs?: Record; + + @IsArray() + toOptions: Array; + + @IsArray() + fromOptions: Array; +} \ No newline at end of file diff --git a/packages/server/src/modules/PaymentReceived/PaymentReceived.application.ts b/packages/server/src/modules/PaymentReceived/PaymentReceived.application.ts index 943d419d6..80a7f3a83 100644 --- a/packages/server/src/modules/PaymentReceived/PaymentReceived.application.ts +++ b/packages/server/src/modules/PaymentReceived/PaymentReceived.application.ts @@ -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) { + public async getPaymentsReceived( + filterDTO: Partial, + ) { 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} + */ + public getPaymentReceivedHtml(paymentReceivedId: number) { + return this.getPaymentReceivePdfService.getPaymentReceivedHtml( + paymentReceivedId, + ); + } + /** * Retrieves the create/edit initial state of the payment received. * @returns {Promise} diff --git a/packages/server/src/modules/PaymentReceived/PaymentsReceived.controller.ts b/packages/server/src/modules/PaymentReceived/PaymentsReceived.controller.ts index 0c44d1140..a2a660d94 100644 --- a/packages/server/src/modules/PaymentReceived/PaymentsReceived.controller.ts +++ b/packages/server/src/modules/PaymentReceived/PaymentsReceived.controller.ts @@ -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, diff --git a/packages/server/src/modules/PaymentReceived/constants.ts b/packages/server/src/modules/PaymentReceived/constants.ts index c4b94c2dd..c3929d978 100644 --- a/packages/server/src/modules/PaymentReceived/constants.ts +++ b/packages/server/src/modules/PaymentReceived/constants.ts @@ -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 = ` -

Dear {Customer Name}

-

Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!

-

-Payment Date : {Payment Date}
-Amount : {Payment Amount}
-

+export const DEFAULT_PAYMENT_MAIL_CONTENT = `Dear {Customer Name} -

-Regards
-{Company Name} -

-`; +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', diff --git a/packages/server/src/modules/PaymentReceived/models/PaymentReceived.ts b/packages/server/src/modules/PaymentReceived/models/PaymentReceived.ts index 83efead1a..a360e99e6 100644 --- a/packages/server/src/modules/PaymentReceived/models/PaymentReceived.ts +++ b/packages/server/src/modules/PaymentReceived/models/PaymentReceived.ts @@ -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', + }, + }, }; } diff --git a/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.service.ts b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.service.ts new file mode 100644 index 000000000..a4b830f4e --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.service.ts @@ -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} + */ + public getMailOptions = async ( + paymentId: number, + ): Promise => { + 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; + }; +} diff --git a/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.transformer.ts b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.transformer.ts new file mode 100644 index 000000000..73c93c645 --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailState.transformer.ts @@ -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; + }; +} \ No newline at end of file diff --git a/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplate.service.ts b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplate.service.ts new file mode 100644 index 000000000..1dfdbac90 --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplate.service.ts @@ -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} + */ + public async getMailTemplateAttributes( + paymentReceivedId: number, + ): Promise { + 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} overrideAttributes + * @returns + */ + public async getMailTemplate( + paymentReceivedId: number, + overrideAttributes?: Partial, + ): Promise { + const mailTemplateAttributes = + await this.getMailTemplateAttributes(paymentReceivedId); + const mergedAttributes = { + ...mailTemplateAttributes, + ...overrideAttributes, + }; + return renderPaymentReceivedEmailTemplate(mergedAttributes); + } +} diff --git a/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplateAttrs.transformer.ts b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplateAttrs.transformer.ts new file mode 100644 index 000000000..1e1acb544 --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedMailTemplateAttrs.transformer.ts @@ -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; + } +} \ No newline at end of file diff --git a/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedPdf.service.ts b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedPdf.service.ts index 62d199335..8746304a9 100644 --- a/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedPdf.service.ts +++ b/packages/server/src/modules/PaymentReceived/queries/GetPaymentReceivedPdf.service.ts @@ -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, ) {} + /** + * Retrieves payment received html content. + * @param {number} paymentReceivedId + * @returns {Promise} + */ + public async getPaymentReceivedHtml( + paymentReceivedId: number, + ): Promise { + 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} */ 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), diff --git a/packages/server/src/modules/PaymentReceived/types/PaymentReceived.types.ts b/packages/server/src/modules/PaymentReceived/types/PaymentReceived.types.ts index 2b1b1a209..8da0c3565 100644 --- a/packages/server/src/modules/PaymentReceived/types/PaymentReceived.types.ts +++ b/packages/server/src/modules/PaymentReceived/types/PaymentReceived.types.ts @@ -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; diff --git a/packages/server/src/modules/SaleEstimates/SaleEstimates.application.ts b/packages/server/src/modules/SaleEstimates/SaleEstimates.application.ts index ba06863c9..a8424755a 100644 --- a/packages/server/src/modules/SaleEstimates/SaleEstimates.application.ts +++ b/packages/server/src/modules/SaleEstimates/SaleEstimates.application.ts @@ -18,6 +18,7 @@ import { CreateSaleEstimateDto, EditSaleEstimateDto, } from './dtos/SaleEstimate.dto'; +import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service'; @Injectable() export class SaleEstimatesApplication { @@ -33,6 +34,7 @@ export class SaleEstimatesApplication { private readonly sendEstimateMailService: SendSaleEstimateMail, private readonly getSaleEstimateStateService: GetSaleEstimateState, private readonly saleEstimatesPdfService: GetSaleEstimatePdf, + private readonly getSaleEstimateMailStateService: GetSaleEstimateMailStateService, ) {} /** @@ -172,4 +174,23 @@ export class SaleEstimatesApplication { public getSaleEstimateState() { return this.getSaleEstimateStateService.getSaleEstimateState(); } + + /** + * Retrieves the sale estimate mail state. + * @param {number} saleEstimateId + * @returns {Promise} + */ + public getSaleEstimateMailState(saleEstimateId: number) { + return this.getSaleEstimateMailStateService.getEstimateMailState( + saleEstimateId, + ); + } + + /** + * Retrieve the HTML content of the given sale estimate. + * @param {number} saleEstimateId + */ + public getSaleEstimateHtml(saleEstimateId: number) { + return this.saleEstimatesPdfService.saleEstimateHtml(saleEstimateId); + } } diff --git a/packages/server/src/modules/SaleEstimates/SaleEstimates.controller.ts b/packages/server/src/modules/SaleEstimates/SaleEstimates.controller.ts index e2a1b69f4..f2b04ee4e 100644 --- a/packages/server/src/modules/SaleEstimates/SaleEstimates.controller.ts +++ b/packages/server/src/modules/SaleEstimates/SaleEstimates.controller.ts @@ -208,7 +208,7 @@ export class SaleEstimatesController { } @Get(':id/mail') - @ApiOperation({ summary: 'Retrieves the sale estimate mail details.' }) + @ApiOperation({ summary: 'Retrieves the sale estimate mail state.' }) @ApiParam({ name: 'id', required: true, @@ -218,7 +218,9 @@ export class SaleEstimatesController { public getSaleEstimateMail( @Param('id', ParseIntPipe) saleEstimateId: number, ) { - return this.saleEstimatesApplication.getSaleEstimateMail(saleEstimateId); + return this.saleEstimatesApplication.getSaleEstimateMailState( + saleEstimateId, + ); } @Get(':id') @@ -243,6 +245,11 @@ export class SaleEstimatesController { 'Content-Length': pdfContent.length, }); res.send(pdfContent); + } else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) { + const htmlContent = + await this.saleEstimatesApplication.getSaleEstimateHtml(estimateId); + + return { htmlContent }; } else { return this.saleEstimatesApplication.getSaleEstimate(estimateId); } diff --git a/packages/server/src/modules/SaleEstimates/SaleEstimates.module.ts b/packages/server/src/modules/SaleEstimates/SaleEstimates.module.ts index 2732dcac6..282a8fc67 100644 --- a/packages/server/src/modules/SaleEstimates/SaleEstimates.module.ts +++ b/packages/server/src/modules/SaleEstimates/SaleEstimates.module.ts @@ -37,6 +37,8 @@ import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module'; import { SendSaleEstimateMailQueue } from './types/SaleEstimates.types'; import { SaleEstimatesExportable } from './SaleEstimatesExportable'; import { SaleEstimatesImportable } from './SaleEstimatesImportable'; +import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service'; +import { GetSaleEstimateMailTemplateService } from './queries/GetSaleEstimateMailTemplate.service'; @Module({ imports: [ @@ -78,11 +80,15 @@ import { SaleEstimatesImportable } from './SaleEstimatesImportable'; GetSaleEstimatePdf, SaleEstimatePdfTemplate, SaleEstimatesExportable, - SaleEstimatesImportable + SaleEstimatesImportable, + GetSaleEstimateMailStateService, + GetSaleEstimateMailTemplateService ], exports: [ SaleEstimatesExportable, - SaleEstimatesImportable + SaleEstimatesImportable, + GetSaleEstimateMailStateService, + GetSaleEstimateMailTemplateService ] }) export class SaleEstimatesModule {} diff --git a/packages/server/src/modules/SaleEstimates/commands/SendSaleEstimateMail.ts b/packages/server/src/modules/SaleEstimates/commands/SendSaleEstimateMail.ts index 3ae385a91..9e362f806 100644 --- a/packages/server/src/modules/SaleEstimates/commands/SendSaleEstimateMail.ts +++ b/packages/server/src/modules/SaleEstimates/commands/SendSaleEstimateMail.ts @@ -23,6 +23,7 @@ import { SaleEstimateMailOptions } from '../types/SaleEstimates.types'; import { Mail } from '@/modules/Mail/Mail'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { GetSaleEstimateMailTemplateService } from '../queries/GetSaleEstimateMailTemplate.service'; @Injectable() export class SendSaleEstimateMail { @@ -38,6 +39,7 @@ export class SendSaleEstimateMail { private readonly estimatePdf: GetSaleEstimatePdf, private readonly getSaleEstimateService: GetSaleEstimate, private readonly contactMailNotification: ContactMailNotification, + private readonly getEstimateMailTemplate: GetSaleEstimateMailTemplateService, private readonly eventPublisher: EventEmitter2, private readonly mailTransporter: MailTransporter, @@ -78,7 +80,12 @@ export class SendSaleEstimateMail { */ public formatterArgs = async (estimateId: number) => { const estimate = await this.getSaleEstimateService.getEstimate(estimateId); - return transformEstimateToMailDataArgs(estimate); + const commonArgs = await this.contactMailNotification.getCommonFormatArgs(); + + return { + ...commonArgs, + ...transformEstimateToMailDataArgs(estimate), + }; }; /** @@ -129,9 +136,35 @@ export class SendSaleEstimateMail { mailOptions, formatterArgs, ); - return { ...formattedOptions }; + // Retrieves the estimate mail template. + const message = await this.getEstimateMailTemplate.getMailTemplate( + saleEstimateId, + { + message: formattedOptions.message, + preview: formattedOptions.message, + }, + ); + return { ...formattedOptions, message }; }; + /** + * Retrieves the formatted mail options. + * @param {number} saleEstimateId + * @param {SaleEstimateMailOptionsDTO} messageOptions + * @returns {Promise} + */ + public async getFormattedMailOptions( + saleEstimateId: number, + messageOptions: SaleEstimateMailOptionsDTO, + ): Promise { + const defaultMessageOptions = await this.getMailOptions(saleEstimateId); + const parsedMessageOptions = mergeAndValidateMailOptions( + defaultMessageOptions, + messageOptions, + ); + return this.formatMailOptions(saleEstimateId, parsedMessageOptions); + } + /** * Sends the mail notification of the given sale estimate. * @param {number} saleEstimateId - Sale estimate id. @@ -142,16 +175,9 @@ export class SendSaleEstimateMail { saleEstimateId: number, messageOptions: SaleEstimateMailOptionsDTO, ): Promise { - const localMessageOpts = await this.getMailOptions(saleEstimateId); - // Overrides and validates the given mail options. - const parsedMessageOptions = mergeAndValidateMailOptions( - localMessageOpts, - messageOptions, - ) as SaleEstimateMailOptions; - - const formattedOptions = await this.formatMailOptions( + const formattedOptions = await this.getFormattedMailOptions( saleEstimateId, - parsedMessageOptions, + messageOptions, ); const mail = new Mail() .setSubject(formattedOptions.subject) @@ -173,7 +199,6 @@ export class SendSaleEstimateMail { }, ]); } - const eventPayload = { saleEstimateId, messageOptions, diff --git a/packages/server/src/modules/SaleEstimates/constants.ts b/packages/server/src/modules/SaleEstimates/constants.ts index 798e8f090..fd8738a4d 100644 --- a/packages/server/src/modules/SaleEstimates/constants.ts +++ b/packages/server/src/modules/SaleEstimates/constants.ts @@ -1,18 +1,17 @@ export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT = 'Estimate {Estimate Number} is awaiting your approval'; -export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `

Dear {Customer Name}

-

Thank you for your business, You can view or print your estimate from attachements.

-

-Estimate #{Estimate Number}
-Expiration Date : {Estimate Expiration Date}
-Amount : {Estimate Amount}
-

+export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `Hi {Customer Name}, -

-Regards
-{Company Name} -

-`; +Here's estimate # {Estimate Number} for {Estimate Amount} + +This estimate is valid until {Estimate Expiration Date}, and we’re happy to discuss any adjustments you or questions may have. + +Please find your estimate attached to this email for your reference. + +If you have any questions, please let us know. + +Thanks, +{Company Name}`; export const ERRORS = { SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND', diff --git a/packages/server/src/modules/SaleEstimates/models/SaleEstimate.ts b/packages/server/src/modules/SaleEstimates/models/SaleEstimate.ts index 7ba822dd1..727dba04a 100644 --- a/packages/server/src/modules/SaleEstimates/models/SaleEstimate.ts +++ b/packages/server/src/modules/SaleEstimates/models/SaleEstimate.ts @@ -1,6 +1,5 @@ import * as moment from 'moment'; import { Model } from 'objection'; -import { Injectable } from '@nestjs/common'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; import { ImportableModel } from '@/modules/Import/decorators/Import.decorator'; @@ -307,12 +306,6 @@ export class SaleEstimate extends TenantBaseModel { }, }; } - /** - * Model settings. - */ - // static get meta() { - // return SaleEstimateSettings; - // } /** * Retrieve the default custom views, roles and columns. diff --git a/packages/server/src/modules/SaleEstimates/queries/GetEstimateMailTemplateAttributes.transformer.ts b/packages/server/src/modules/SaleEstimates/queries/GetEstimateMailTemplateAttributes.transformer.ts new file mode 100644 index 000000000..5c78ca4c8 --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/queries/GetEstimateMailTemplateAttributes.transformer.ts @@ -0,0 +1,167 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; + +export class GetEstimateMailTemplateAttributesTransformer extends Transformer { + public includeAttributes = (): string[] => { + return [ + 'companyLogoUri', + 'companyName', + + 'estimateAmount', + + 'primaryColor', + + 'estimateAmount', + 'estimateMessage', + + 'dueDate', + 'dueDateLabel', + + 'estimateNumber', + 'estimateNumberLabel', + + 'total', + 'totalLabel', + + 'subtotal', + 'subtotalLabel', + + 'dueAmount', + 'dueAmountLabel', + + 'viewEstimateButtonLabel', + 'viewEstimateButtonUrl', + + '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; + } + + /** + * Estimate number. + * @returns {string} + */ + public estimateNumber(): string { + return this.options.estimate.estimateNumber; + } + + /** + * Estimate number label. + * @returns {string} + */ + public estimateNumberLabel(): string { + return 'Estimate No: {estimateNumber}'; + } + + /** + * Expiration date. + * @returns {string} + */ + public expirationDate(): string { + return this.options.estimate.formattedExpirationDate; + } + + /** + * Expiration date label. + * @returns {string} + */ + public expirationDateLabel(): string { + return 'Expiration Date: {expirationDate}'; + } + + /** + * Estimate total. + */ + public total(): string { + return this.options.estimate.formattedAmount; + } + + /** + * Estimate total label. + * @returns {string} + */ + public totalLabel(): string { + return 'Total'; + } + + /** + * Estimate subtotal. + */ + public subtotal(): string { + return this.options.estimate.formattedAmount; + } + + /** + * Estimate subtotal label. + * @returns {string} + */ + public subtotalLabel(): string { + return 'Subtotal'; + } + + /** + * Estimate mail items attributes. + */ + public items(): any[] { + return this.item( + this.options.estimate.entries, + new GetEstimateMailTemplateEntryAttributesTransformer(), + ); + } +} + +class GetEstimateMailTemplateEntryAttributesTransformer extends Transformer { + public includeAttributes = (): string[] => { + return ['label', 'quantity', 'rate', 'total']; + }; + + public excludeAttributes = (): string[] => { + return ['*']; + }; + + public label(entry): string { + return entry?.item?.name; + } + + public quantity(entry): string { + return entry?.quantity; + } + + public rate(entry): string { + return entry?.rateFormatted; + } + + public total(entry): string { + return entry?.totalFormatted; + } +} diff --git a/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.service.ts b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.service.ts new file mode 100644 index 000000000..f6f5f40a7 --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.service.ts @@ -0,0 +1,46 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { SaleEstimate } from '../models/SaleEstimate'; +import { SendSaleEstimateMail } from '../commands/SendSaleEstimateMail'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { GetSaleEstimateMailStateTransformer } from './GetSaleEstimateMailState.transformer'; + +@Injectable() +export class GetSaleEstimateMailStateService { + constructor( + private readonly estimateMail: SendSaleEstimateMail, + private readonly transformer: TransformerInjectable, + + @Inject(SaleEstimate.name) + private readonly saleEstimateModel: TenantModelProxy + ) {} + + /** + * Retrieves the estimate mail state of the given sale estimate. + * Estimate mail state includes the mail options, branding attributes and the estimate details. + * @param {number} saleEstimateId + * @returns {Promise} + */ + async getEstimateMailState( + saleEstimateId: number + ) { + const saleEstimate = await this.saleEstimateModel().query() + .findById(saleEstimateId) + .withGraphFetched('customer') + .withGraphFetched('entries.item') + .withGraphFetched('pdfTemplate') + .throwIfNotFound(); + + const mailOptions = await this.estimateMail.getMailOptions( + saleEstimateId + ); + const transformed = await this.transformer.transform( + saleEstimate, + new GetSaleEstimateMailStateTransformer(), + { + mailOptions, + } + ); + return transformed; + } +} \ No newline at end of file diff --git a/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.transformer.ts b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.transformer.ts new file mode 100644 index 000000000..fa0ff62a3 --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailState.transformer.ts @@ -0,0 +1,171 @@ +import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer'; +import { SaleEstimateTransfromer } from './SaleEstimate.transformer'; + +export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer { + public excludeAttributes = (): string[] => { + return ['*']; + }; + + public includeAttributes = (): string[] => { + return [ + 'estimateDate', + 'estimateDateFormatted', + + 'expirationDate', + 'expirationDateFormatted', + + 'total', + 'totalFormatted', + + 'subtotal', + 'subtotalFormatted', + + 'estimateNumber', + 'entries', + + 'companyName', + 'companyLogoUri', + + 'primaryColor', + 'customerName', + ]; + }; + + /** + * Retrieves the customer name of the invoice. + * @returns {string} + */ + protected customerName = (invoice) => { + return invoice.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 = (invoice) => { + return invoice.pdfTemplate?.companyLogoUri || null; + }; + + /** + * Retrieves the primary color. + * @returns {string} + */ + protected primaryColor = (invoice) => { + return invoice.pdfTemplate?.attributes?.primaryColor || null; + }; + + /** + * Retrieves the estimate number. + */ + protected estimateDateFormatted = (estimate) => { + return this.formattedEstimateDate(estimate); + }; + + /** + * Retrieves the expiration date of the estimate. + * @param estimate + * @returns {string} + */ + protected expirationDateFormatted = (estimate) => { + return this.formattedExpirationDate(estimate); + }; + + /** + * Retrieves the total amount of the estimate. + * @param estimate + * @returns + */ + protected total(estimate) { + return estimate.amount; + } + + /** + * Retrieves the subtotal amount of the estimate. + * @param estimate + * @returns {number} + */ + protected subtotal(estimate) { + return estimate.amount; + } + + /** + * Retrieves the formatted total of the estimate. + * @param estimate + * @returns {string} + */ + protected totalFormatted(estimate) { + return this.formatMoney(estimate.amount, { + currencyCode: estimate.currencyCode, + money: true, + }); + } + + /** + * Retrieves the formatted subtotal of the estimate. + * @param estimate + * @returns {string} + */ + protected subtotalFormatted = (estimate) => { + return this.formatNumber(estimate.amount, { money: false }); + }; + + /** + * Retrieves the estimate entries. + * @param invoice + * @returns {Array} + */ + protected entries = (invoice) => { + return this.item( + invoice.entries, + new GetSaleEstimateMailStateEntryTransformer(), + { + currencyCode: invoice.currencyCode, + }, + ); + }; + + /** + * Merges the mail options with the invoice object. + */ + public transform = (object: any) => { + return { + ...this.options.mailOptions, + ...object, + }; + }; +} + +class GetSaleEstimateMailStateEntryTransformer extends ItemEntryTransformer { + public excludeAttributes = (): string[] => { + return ['*']; + }; + + /** + * Item name. + * @param entry + * @returns + */ + public name = (entry) => { + return entry.item.name; + }; + + public includeAttributes = (): string[] => { + return [ + 'name', + 'quantity', + 'unitPrice', + 'unitPriceFormatted', + 'total', + 'totalFormatted', + ]; + }; +} diff --git a/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailTemplate.service.ts b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailTemplate.service.ts new file mode 100644 index 000000000..a3a3b2c1e --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimateMailTemplate.service.ts @@ -0,0 +1,61 @@ +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { + renderEstimateEmailTemplate, + EstimatePaymentEmailProps, +} from '@bigcapital/email-components'; +import { Injectable } from '@nestjs/common'; +import { GetSaleEstimate } from './GetSaleEstimate.service'; +import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service'; +import { GetEstimateMailTemplateAttributesTransformer } from './GetEstimateMailTemplateAttributes.transformer'; + +@Injectable() +export class GetSaleEstimateMailTemplateService { + constructor( + private readonly getEstimateService: GetSaleEstimate, + private readonly transformer: TransformerInjectable, + private readonly getBrandingTemplate: GetPdfTemplateService, + ) {} + + /** + * Retrieves the mail template attributes of the given estimate. + * Estimate template attributes are composed of the estimate and branding template attributes. + * @param {number} estimateId - Estimate id. + * @returns {Promise} + */ + public async getMailTemplateAttributes( + estimateId: number, + ): Promise { + const estimate = await this.getEstimateService.getEstimate(estimateId); + const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate( + estimate.pdfTemplateId, + ); + const mailTemplateAttributes = await this.transformer.transform( + estimate, + new GetEstimateMailTemplateAttributesTransformer(), + { + estimate, + brandingTemplate, + }, + ); + return mailTemplateAttributes; + } + + /** + * Rertieves the mail template html content. + * @param {number} tenantId + * @param {number} estimateId + * @param overrideAttributes + * @returns + */ + public async getMailTemplate( + estimateId: number, + overrideAttributes?: Partial, + ): Promise { + const attributes = await this.getMailTemplateAttributes(estimateId); + const mergedAttributes = { + ...attributes, + ...overrideAttributes, + }; + return renderEstimateEmailTemplate(mergedAttributes); + } +} diff --git a/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimatePdf.ts b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimatePdf.ts index 84de0df7e..7b3cbf0c7 100644 --- a/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimatePdf.ts +++ b/packages/server/src/modules/SaleEstimates/queries/GetSaleEstimatePdf.ts @@ -4,18 +4,17 @@ import { GetSaleEstimate } from './GetSaleEstimate.service'; import { transformEstimateToPdfTemplate } from '../utils'; import { EstimatePdfBrandingAttributes } from '../constants'; import { SaleEstimatePdfTemplate } from '@/modules/SaleInvoices/queries/SaleEstimatePdfTemplate.service'; -import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service'; import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; import { events } from '@/common/events/events'; import { SaleEstimate } from '../models/SaleEstimate'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { renderEstimatePaperTemplateHtml } from '@bigcapital/pdf-templates'; @Injectable() export class GetSaleEstimatePdf { constructor( private readonly chromiumlyTenancy: ChromiumlyTenancy, - private readonly templateInjectable: TemplateInjectable, private readonly getSaleEstimate: GetSaleEstimate, private readonly estimatePdfTemplate: SaleEstimatePdfTemplate, private readonly eventPublisher: EventEmitter2, @@ -29,22 +28,29 @@ export class GetSaleEstimatePdf { private readonly saleEstimateModel: TenantModelProxy, ) {} + /** + * Retrieve sale estimate html content. + * @param {number} invoiceId - + */ + public async saleEstimateHtml(estimateId: number): Promise { + const brandingAttributes = + await this.getEstimateBrandingAttributes(estimateId); + + return renderEstimatePaperTemplateHtml({ ...brandingAttributes }); + } + /** * Retrieve sale invoice pdf content. - * @param {number} tenantId - - * @param {ISaleInvoice} saleInvoice - + * @param {ISaleInvoice} saleInvoice - Sale estimate id. */ public async getSaleEstimatePdf( saleEstimateId: number, ): Promise<[Buffer, string]> { const filename = await this.getSaleEstimateFilename(saleEstimateId); - const brandingAttributes = - await this.getEstimateBrandingAttributes(saleEstimateId); - const htmlContent = await this.templateInjectable.render( - 'modules/estimate-regular', - brandingAttributes, - ); + // Retrieves the sale estimate html. + const htmlContent = await this.saleEstimateHtml(saleEstimateId); + const content = await this.chromiumlyTenancy.convertHtmlContent(htmlContent); const eventPayload = { saleEstimateId }; @@ -72,7 +78,6 @@ export class GetSaleEstimatePdf { /** * Retrieves the given estimate branding attributes. - * @param {number} tenantId - Tenant id. * @param {number} estimateId - Estimate id. * @returns {Promise} */ diff --git a/packages/server/src/modules/SaleInvoices/SaleInvoices.controller.ts b/packages/server/src/modules/SaleInvoices/SaleInvoices.controller.ts index 71a6cb94f..f34676cc7 100644 --- a/packages/server/src/modules/SaleInvoices/SaleInvoices.controller.ts +++ b/packages/server/src/modules/SaleInvoices/SaleInvoices.controller.ts @@ -171,6 +171,9 @@ export class SaleInvoicesController { 'Content-Length': pdfContent.length, }); res.send(pdfContent); + } else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) { + const htmlContent = await this.saleInvoiceApplication.saleInvoiceHtml(id); + return { htmlContent }; } else { return this.saleInvoiceApplication.getSaleInvoice(id); } @@ -270,7 +273,7 @@ export class SaleInvoicesController { return this.saleInvoiceApplication.saleInvoiceHtml(id); } - @Get(':id/mail-state') + @Get(':id/mail') @ApiOperation({ summary: 'Retrieves the sale invoice mail state.' }) @ApiResponse({ status: 200, diff --git a/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMail.service.ts b/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMail.service.ts index 99723f8ee..d1ddd175a 100644 --- a/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMail.service.ts +++ b/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMail.service.ts @@ -39,7 +39,7 @@ export class GetInvoicePaymentMail { /** * Retrieves the mail template html content. - * @param {number} invoiceId - Invoice id. + * @param {number} invoiceId - Sale invoice id. */ public async getMailTemplate( invoiceId: number, diff --git a/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMailAttributes.transformer.ts b/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMailAttributes.transformer.ts index 4783288a8..df85a77b3 100644 --- a/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMailAttributes.transformer.ts +++ b/packages/server/src/modules/SaleInvoices/queries/GetInvoicePaymentMailAttributes.transformer.ts @@ -1,4 +1,4 @@ -import { Transformer } from "@/modules/Transformer/Transformer"; +import { Transformer } from '@/modules/Transformer/Transformer'; export class GetInvoicePaymentMailAttributesTransformer extends Transformer { /** @@ -26,6 +26,15 @@ export class GetInvoicePaymentMailAttributesTransformer extends Transformer { 'total', 'totalLabel', + 'subtotal', + 'subtotalLabel', + + 'discount', + 'discountLabel', + + 'adjustment', + 'adjustmentLabel', + 'dueAmount', 'dueAmountLabel', @@ -76,6 +85,30 @@ export class GetInvoicePaymentMailAttributesTransformer extends Transformer { return 'Invoice # {invoiceNumber}'; } + public subtotal(): string { + return this.options.invoice?.subtotalFormatted; + } + + public subtotalLabel(): string { + return 'Subtotal'; + } + + public discount(): string { + return this.options.invoice?.discountAmountFormatted; + } + + public discountLabel(): string { + return 'Discount'; + } + + public adjustment(): string { + return this.options.invoice?.adjustmentFormatted; + } + + public adjustmentLabel(): string { + return 'Adjustment'; + } + public total(): string { return this.options.invoice?.totalFormatted; } @@ -103,7 +136,7 @@ export class GetInvoicePaymentMailAttributesTransformer extends Transformer { public items(): Array { return this.item( this.options.invoice?.entries, - new GetInvoiceMailTemplateItemAttrsTransformer() + new GetInvoiceMailTemplateItemAttrsTransformer(), ); } } diff --git a/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.transformer.ts b/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.transformer.ts index d28b05d6c..e4abaf99b 100644 --- a/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.transformer.ts +++ b/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.transformer.ts @@ -31,6 +31,15 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer { 'subtotal', 'subtotalFormatted', + 'discountAmount', + 'discountAmountFormatted', + 'discountPercentage', + 'discountPercentageFormatted', + 'discountLabel', + + 'adjustment', + 'adjustmentFormatted', + 'invoiceNo', 'entries', @@ -76,6 +85,17 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer { return invoice.pdfTemplate?.attributes?.primaryColor; }; + /** + * Retrieves the discount label of the estimate. + * @param estimate + * @returns {string} + */ + protected discountLabel(invoice) { + return invoice.discountType === 'percentage' + ? `Discount [${invoice.discountPercentageFormatted}]` + : 'Discount'; + } + /** * * @param invoice @@ -87,7 +107,7 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer { new GetSaleInvoiceMailStateEntryTransformer(), { currencyCode: invoice.currencyCode, - } + }, ); }; diff --git a/packages/server/src/modules/SaleInvoices/queries/SaleInvoice.transformer.ts b/packages/server/src/modules/SaleInvoices/queries/SaleInvoice.transformer.ts index ed4de2dc8..6f9459083 100644 --- a/packages/server/src/modules/SaleInvoices/queries/SaleInvoice.transformer.ts +++ b/packages/server/src/modules/SaleInvoices/queries/SaleInvoice.transformer.ts @@ -3,6 +3,7 @@ import { SaleInvoice } from '../models/SaleInvoice'; import { ItemEntryTransformer } from '../../TransactionItemEntry/ItemEntry.transformer'; import { AttachmentTransformer } from '../../Attachments/Attachment.transformer'; import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer'; +import { DiscountType } from '@/common/types/Discount'; export class SaleInvoiceTransformer extends Transformer { /** @@ -25,6 +26,9 @@ export class SaleInvoiceTransformer extends Transformer { 'taxAmountWithheldLocalFormatted', 'totalFormatted', 'totalLocalFormatted', + 'discountAmountFormatted', + 'discountPercentageFormatted', + 'adjustmentFormatted', 'taxes', 'entries', 'attachments', @@ -180,6 +184,39 @@ export class SaleInvoiceTransformer extends Transformer { }); }; + /** + * Retrieves formatted discount amount. + * @param invoice + * @returns {string} + */ + protected discountAmountFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.discountAmount, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieves formatted discount percentage. + * @param invoice + * @returns {string} + */ + protected discountPercentageFormatted = (invoice: SaleInvoice): string => { + return invoice.discountType === DiscountType.Percentage + ? `${invoice.discount}%` + : ''; + }; + + /** + * Retrieves formatted adjustment amount. + * @param invoice + * @returns {string} + */ + protected adjustmentFormatted = (invoice: SaleInvoice): string => { + return this.formatMoney(invoice.adjustment, { + currencyCode: invoice.currencyCode, + }) + } + /** * Retrieve the taxes lines of sale invoice. * @param {ISaleInvoice} invoice diff --git a/packages/server/src/modules/SaleInvoices/queries/SaleInvoicePdf.service.ts b/packages/server/src/modules/SaleInvoices/queries/SaleInvoicePdf.service.ts index ce5ea72f6..bc99defc0 100644 --- a/packages/server/src/modules/SaleInvoices/queries/SaleInvoicePdf.service.ts +++ b/packages/server/src/modules/SaleInvoices/queries/SaleInvoicePdf.service.ts @@ -47,7 +47,6 @@ export class SaleInvoicePdf { */ public async getSaleInvoicePdf(invoiceId: number): Promise<[Buffer, string]> { const filename = await this.getInvoicePdfFilename(invoiceId); - const htmlContent = await this.getSaleInvoiceHtml(invoiceId); // Converts the given html content to pdf document. diff --git a/packages/server/src/modules/SaleInvoices/utils.ts b/packages/server/src/modules/SaleInvoices/utils.ts index da2b16a87..089e8a4ad 100644 --- a/packages/server/src/modules/SaleInvoices/utils.ts +++ b/packages/server/src/modules/SaleInvoices/utils.ts @@ -44,7 +44,10 @@ export const transformInvoiceToPdfTemplate = ( label: tax.name, amount: tax.taxRateAmountFormatted, })), - + discount: invoice.discountAmountFormatted, + discountLabel: invoice.discountPercentageFormatted + ? `Discount [${invoice.discountPercentageFormatted}]` + : 'Discount', customerAddress: contactAddressTextFormat(invoice.customer), }; }; diff --git a/packages/server/src/modules/SaleReceipts/SaleReceiptApplication.service.ts b/packages/server/src/modules/SaleReceipts/SaleReceiptApplication.service.ts index bccd27bbb..4dde26cbc 100644 --- a/packages/server/src/modules/SaleReceipts/SaleReceiptApplication.service.ts +++ b/packages/server/src/modules/SaleReceipts/SaleReceiptApplication.service.ts @@ -17,7 +17,11 @@ import { GetSaleReceiptsService } from './queries/GetSaleReceipts.service'; import { SaleReceipt } from './models/SaleReceipt'; import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; import { SaleReceiptMailNotification } from './commands/SaleReceiptMailNotification'; -import { CreateSaleReceiptDto, EditSaleReceiptDto } from './dtos/SaleReceipt.dto'; +import { + CreateSaleReceiptDto, + EditSaleReceiptDto, +} from './dtos/SaleReceipt.dto'; +import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service'; @Injectable() export class SaleReceiptApplication { @@ -31,6 +35,7 @@ export class SaleReceiptApplication { private getSaleReceiptPdfService: SaleReceiptsPdfService, private getSaleReceiptStateService: GetSaleReceiptState, private saleReceiptNotifyByMailService: SaleReceiptMailNotification, + private getSaleReceiptMailStateService: GetSaleReceiptMailStateService, ) {} /** @@ -172,4 +177,23 @@ export class SaleReceiptApplication { public getSaleReceiptState(): Promise { return this.getSaleReceiptStateService.getSaleReceiptState(); } + + /** + * Retrieves the given sale receipt html. + * @param {number} saleReceiptId + * @returns {Promise} + */ + public getSaleReceiptHtml(saleReceiptId: number) { + return this.getSaleReceiptPdfService.saleReceiptHtml(saleReceiptId); + } + + /** + * Retrieves the mail state of the given sale receipt. + * @param {number} saleReceiptId + */ + public getSaleReceiptMailState( + saleReceiptId: number, + ): Promise { + return this.getSaleReceiptMailStateService.getMailState(saleReceiptId); + } } diff --git a/packages/server/src/modules/SaleReceipts/SaleReceipts.controller.ts b/packages/server/src/modules/SaleReceipts/SaleReceipts.controller.ts index aeb53c5a5..69ed54312 100644 --- a/packages/server/src/modules/SaleReceipts/SaleReceipts.controller.ts +++ b/packages/server/src/modules/SaleReceipts/SaleReceipts.controller.ts @@ -107,6 +107,11 @@ export class SaleReceiptsController { 'Content-Length': pdfContent.length, }); res.send(pdfContent); + } else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) { + const htmlContent = + await this.saleReceiptApplication.getSaleReceiptHtml(id); + + return { htmlContent }; } else { return this.saleReceiptApplication.getSaleReceipt(id); } diff --git a/packages/server/src/modules/SaleReceipts/SaleReceipts.module.ts b/packages/server/src/modules/SaleReceipts/SaleReceipts.module.ts index ceaf6e0a4..86fa2103d 100644 --- a/packages/server/src/modules/SaleReceipts/SaleReceipts.module.ts +++ b/packages/server/src/modules/SaleReceipts/SaleReceipts.module.ts @@ -37,6 +37,8 @@ import { MailModule } from '../Mail/Mail.module'; import { SendSaleReceiptMailQueue } from './constants'; import { SaleReceiptsExportable } from './commands/SaleReceiptsExportable'; import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable'; +import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service'; +import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service'; @Module({ controllers: [SaleReceiptsController], @@ -79,6 +81,8 @@ import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable'; SendSaleReceiptMailProcess, SaleReceiptsExportable, SaleReceiptsImportable, + GetSaleReceiptMailStateService, + GetSaleReceiptMailTemplateService ], }) export class SaleReceiptsModule {} diff --git a/packages/server/src/modules/SaleReceipts/commands/SaleReceiptMailNotification.ts b/packages/server/src/modules/SaleReceipts/commands/SaleReceiptMailNotification.ts index e5a4196f6..ebd542015 100644 --- a/packages/server/src/modules/SaleReceipts/commands/SaleReceiptMailNotification.ts +++ b/packages/server/src/modules/SaleReceipts/commands/SaleReceiptMailNotification.ts @@ -23,8 +23,9 @@ import { import { SaleReceipt } from '../models/SaleReceipt'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { Mail } from '@/modules/Mail/Mail'; -import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { GetSaleReceiptMailTemplateService } from '../queries/GetSaleReceiptMailTemplate.service'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; @Injectable() export class SaleReceiptMailNotification { @@ -42,6 +43,7 @@ export class SaleReceiptMailNotification { private readonly eventEmitter: EventEmitter2, private readonly mailTransporter: MailTransporter, private readonly tenancyContext: TenancyContext, + private readonly getReceiptMailTemplateService: GetSaleReceiptMailTemplateService, @Inject(SaleReceipt.name) private readonly saleReceiptModel: TenantModelProxy, @@ -119,8 +121,12 @@ export class SaleReceiptMailNotification { receiptId: number, ): Promise> => { const receipt = await this.getSaleReceiptService.getSaleReceipt(receiptId); + const commonArgs = await this.contactMailNotification.getCommonFormatArgs(); - return transformReceiptToMailDataArgs(receipt); + return { + ...commonArgs, + ...transformReceiptToMailDataArgs(receipt), + }; }; /** @@ -139,7 +145,14 @@ export class SaleReceiptMailNotification { mailOptions, formatterArgs, )) as SaleReceiptMailOpts; - return formattedOptions; + + const message = await this.getReceiptMailTemplateService.getMailTemplate( + receiptId, + { + message: formattedOptions.message, + }, + ); + return { ...formattedOptions, message }; } /** diff --git a/packages/server/src/modules/SaleReceipts/constants.ts b/packages/server/src/modules/SaleReceipts/constants.ts index 871688b2d..653e373ce 100644 --- a/packages/server/src/modules/SaleReceipts/constants.ts +++ b/packages/server/src/modules/SaleReceipts/constants.ts @@ -1,18 +1,18 @@ export const DEFAULT_RECEIPT_MAIL_SUBJECT = 'Receipt {Receipt Number} from {Company Name}'; -export const DEFAULT_RECEIPT_MAIL_CONTENT = ` -

Dear {Customer Name}

-

Thank you for your business, You can view or print your receipt from attachements.

-

-Receipt #{Receipt Number}
-Amount : {Receipt Amount}
-

+ +export const DEFAULT_RECEIPT_MAIL_CONTENT = `Hi {Customer Name}, -

-Regards
-{Company Name} -

-`; +Here's receipt # {Receipt Number} for Receipt {Receipt Amount} + +The receipt paid on {Receipt Date}, and the total amount paid is {Receipt Amount}. + +Please find your sale receipt attached to this email for your reference + +If you have any questions, please let us know. + +Thanks, +{Company Name}`; export const SendSaleReceiptMailQueue = 'SendSaleReceiptMailQueue'; export const SendSaleReceiptMailJob = 'SendSaleReceiptMailJob'; diff --git a/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.service.ts b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.service.ts new file mode 100644 index 000000000..9fc6fef22 --- /dev/null +++ b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.service.ts @@ -0,0 +1,37 @@ +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { Inject, Injectable } from '@nestjs/common'; +import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { GetSaleReceiptMailStateTransformer } from './GetSaleReceiptMailState.transformer'; + +@Injectable() +export class GetSaleReceiptMailStateService { + constructor( + private readonly transformer: TransformerInjectable, + private readonly receiptMail: SaleReceiptMailNotification, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: TenantModelProxy + ) {} + + /** + * Retrieves the sale receipt mail state of the given sale receipt. + * @param {number} saleReceiptId + */ + public async getMailState(saleReceiptId: number) { + const saleReceipt = await this.saleReceiptModel().query() + .findById(saleReceiptId) + .withGraphFetched('entries.item') + .withGraphFetched('customer') + .throwIfNotFound(); + + const mailOptions = await this.receiptMail.getMailOptions(saleReceiptId); + + return this.transformer.transform( + saleReceipt, + new GetSaleReceiptMailStateTransformer(), + { mailOptions }, + ); + } +} diff --git a/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.transformer.ts b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.transformer.ts new file mode 100644 index 000000000..ecbe42f00 --- /dev/null +++ b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailState.transformer.ts @@ -0,0 +1,216 @@ +import { DiscountType } from '@/common/types/Discount'; +import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer'; +import { Transformer } from '@/modules/Transformer/Transformer'; + +export class GetSaleReceiptMailStateTransformer extends Transformer { + /** + * Exclude these attributes from user object. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['*']; + }; + + /** + * Included attributes. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'companyName', + 'companyLogoUri', + 'primaryColor', + 'customerName', + 'total', + 'totalFormatted', + 'subtotal', + 'subtotalFormatted', + 'receiptDate', + 'receiptDateFormatted', + 'closedAtDate', + 'closedAtDateFormatted', + 'receiptNumber', + + 'discountAmount', + 'discountAmountFormatted', + 'discountPercentage', + 'discountPercentageFormatted', + 'discountLabel', + + 'adjustment', + 'adjustmentFormatted', + + 'entries', + ]; + }; + + /** + * Retrieves the customer name of the invoice. + * @returns {string} + */ + protected customerName = (receipt) => { + return receipt.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 = (receipt) => { + return receipt.pdfTemplate?.companyLogoUri; + }; + + /** + * Retrieves the primary color. + * @returns {string} + */ + protected primaryColor = (receipt) => { + return receipt.pdfTemplate?.attributes?.primaryColor; + }; + + /** + * + * @param receipt + * @returns + */ + protected total = (receipt) => { + return receipt.amount; + }; + + /** + * + * @param receipt + * @returns + */ + protected totalFormatted = (receipt) => { + return this.formatMoney(receipt.amount, { + currencyCode: receipt.currencyCode, + }); + }; + + /** + * + * @param receipt + * @returns + */ + protected subtotal = (receipt) => { + return receipt.amount; + }; + + /** + * + * @param receipt + * @returns + */ + protected subtotalFormatted = (receipt) => { + return this.formatMoney(receipt.amount, { + currencyCode: receipt.currencyCode, + }); + }; + + /** + * + * @param receipt + * @returns + */ + protected receiptDate = (receipt): string => { + return receipt.receiptDate; + }; + + /** + * + * @param {ISaleReceipt} invoice + * @returns {string} + */ + protected receiptDateFormatted = (receipt): string => { + return this.formatDate(receipt.receiptDate); + }; + + /** + * Retrieves the discount label of the estimate. + * @param estimate + * @returns {string} + */ + protected discountLabel(receipt) { + return receipt.discountType === DiscountType.Percentage + ? `Discount [${receipt.discountPercentageFormatted}]` + : 'Discount'; + } + + /** + * + * @param receipt + * @returns + */ + protected closedAtDate = (receipt): string => { + return receipt.closedAt; + }; + + /** + * Retrieve formatted estimate closed at date. + * @param {ISaleReceipt} invoice + * @returns {String} + */ + protected closedAtDateFormatted = (receipt): string => { + return this.formatDate(receipt.closedAt); + }; + + /** + * + * @param invoice + * @returns + */ + protected entries = (receipt) => { + return this.item( + receipt.entries, + new GetSaleReceiptEntryMailStateTransformer(), + { + currencyCode: receipt.currencyCode, + }, + ); + }; + + /** + * Merges the mail options with the invoice object. + */ + public transform = (object: any) => { + return { + ...this.options.mailOptions, + ...object, + }; + }; +} + +class GetSaleReceiptEntryMailStateTransformer extends ItemEntryTransformer { + /** + * Exclude these attributes from user object. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['*']; + }; + + public name = (entry) => { + return entry.item.name; + }; + + public includeAttributes = (): string[] => { + return [ + 'name', + 'quantity', + 'quantityFormatted', + 'rate', + 'rateFormatted', + 'total', + 'totalFormatted', + ]; + }; +} diff --git a/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.service.ts b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.service.ts new file mode 100644 index 000000000..b8ff61c09 --- /dev/null +++ b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.service.ts @@ -0,0 +1,60 @@ +import { + ReceiptEmailTemplateProps, + renderReceiptEmailTemplate, +} from '@bigcapital/email-components'; +import { Injectable } from '@nestjs/common'; +import { GetSaleReceipt } from './GetSaleReceipt.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service'; +import { GetSaleReceiptMailTemplateAttributesTransformer } from './GetSaleReceiptMailTemplate.transformer'; + +@Injectable() +export class GetSaleReceiptMailTemplateService { + constructor( + private readonly getReceiptService: GetSaleReceipt, + private readonly transformer: TransformerInjectable, + private readonly getBrandingTemplate: GetPdfTemplateService, + ) {} + + /** + * Retrieves the mail template attributes of the given estimate. + * Estimate template attributes are composed of the estimate and branding template attributes. + * @param {number} receiptId - Receipt id. + * @returns {Promise} + */ + public async getMailTemplateAttributes( + receiptId: number, + ): Promise { + const receipt = await this.getReceiptService.getSaleReceipt(receiptId); + const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate( + receipt.pdfTemplateId, + ); + const mailTemplateAttributes = await this.transformer.transform( + receipt, + new GetSaleReceiptMailTemplateAttributesTransformer(), + { + receipt, + brandingTemplate, + }, + ); + return mailTemplateAttributes; + } + + /** + * Retrieves the mail template html content. + * @param {number} receiptId + * @param overrideAttributes + * @returns + */ + public async getMailTemplate( + receiptId: number, + overrideAttributes?: Partial, + ): Promise { + const attributes = await this.getMailTemplateAttributes(receiptId); + const mergedAttributes = { + ...attributes, + ...overrideAttributes, + }; + return renderReceiptEmailTemplate(mergedAttributes); + } +} diff --git a/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.transformer.ts b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.transformer.ts new file mode 100644 index 000000000..4ab22e3da --- /dev/null +++ b/packages/server/src/modules/SaleReceipts/queries/GetSaleReceiptMailTemplate.transformer.ts @@ -0,0 +1,201 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; + +export class GetSaleReceiptMailTemplateAttributesTransformer extends Transformer { + public includeAttributes = (): string[] => { + return [ + 'companyLogoUri', + 'companyName', + + 'primaryColor', + + 'receiptAmount', + 'receiptMessage', + + 'date', + 'dateLabel', + + 'receiptNumber', + 'receiptNumberLabel', + + 'total', + 'totalLabel', + + 'subtotal', + 'subtotalLabel', + + 'paidAmount', + 'paidAmountLabel', + + 'discount', + 'discountLabel', + + 'adjustment', + 'adjustmentLabel', + + '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; + } + + /** + * Receipt number. + * @returns {string} + */ + public receiptNumber(): string { + return this.options.receipt.receiptNumber; + } + + /** + * Receipt number label. + * @returns {string} + */ + public receiptNumberLabel(): string { + return 'Receipt # {receiptNumber}'; + } + + /** + * Date. + * @returns {string} + */ + public date(): string { + return this.options.receipt.date; + } + + /** + * Date label. + * @returns {string} + */ + public dateLabel(): string { + return 'Date'; + } + + /** + * Receipt total. + */ + public total(): string { + return this.options.receipt.totalFormatted; + } + + /** + * Receipt total label. + * @returns {string} + */ + public totalLabel(): string { + return 'Total'; + } + + /** + * Receipt discount. + * @returns {string} + */ + public discount(): string { + return this.options.receipt?.discountAmountFormatted; + } + + /** + * Receipt discount label. + * @returns {string} + */ + public discountLabel(): string { + return 'Discount'; + } + + /** + * Receipt adjustment. + * @returns {string} + */ + public adjustment(): string { + return this.options.receipt?.adjustmentFormatted; + } + + /** + * Receipt adjustment label. + * @returns {string} + */ + public adjustmentLabel(): string { + return 'Adjustment'; + } + + /** + * Receipt subtotal. + * @returns {string} + */ + public subtotal(): string { + return this.options.receipt.subtotalFormatted; + } + + /** + * Receipt subtotal label. + * @returns {string} + */ + public subtotalLabel(): string { + return 'Subtotal'; + } + + /** + * Receipt mail items attributes. + */ + public items(): any[] { + return this.item( + this.options.receipt.entries, + new GetSaleReceiptMailTemplateEntryAttributesTransformer(), + ); + } +} + +class GetSaleReceiptMailTemplateEntryAttributesTransformer extends Transformer { + public includeAttributes = (): string[] => { + return ['label', 'quantity', 'rate', 'total']; + }; + + public excludeAttributes = (): string[] => { + return ['*']; + }; + + public label(entry): string { + return entry?.item?.name; + } + + public quantity(entry): string { + return entry?.quantity; + } + + public rate(entry): string { + return entry?.rateFormatted; + } + + public total(entry): string { + return entry?.totalFormatted; + } +} diff --git a/packages/server/src/modules/SaleReceipts/queries/SaleReceiptTransformer.ts b/packages/server/src/modules/SaleReceipts/queries/SaleReceiptTransformer.ts index 407d52eed..f339c4798 100644 --- a/packages/server/src/modules/SaleReceipts/queries/SaleReceiptTransformer.ts +++ b/packages/server/src/modules/SaleReceipts/queries/SaleReceiptTransformer.ts @@ -15,6 +15,10 @@ export class SaleReceiptTransformer extends Transformer { 'formattedReceiptDate', 'formattedClosedAtDate', 'formattedCreatedAt', + 'subtotalFormatted', + 'subtotalLocalFormatted', + 'totalFormatted', + 'totalLocalFormatted', 'entries', 'attachments', ]; @@ -47,6 +51,40 @@ export class SaleReceiptTransformer extends Transformer { return this.formatDate(receipt.createdAt); }; + /** + * Retrieves the formatted subtotal. + */ + protected subtotalFormatted = (receipt: SaleReceipt): string => { + return this.formatNumber(receipt.subtotal, { money: false }); + }; + + /** + * Retrieves the estimate formatted subtotal in local currency. + * @param {ISaleReceipt} receipt + * @returns {string} + */ + protected subtotalLocalFormatted = (receipt: SaleReceipt): string => { + return this.formatNumber(receipt.subtotalLocal, { + currencyCode: receipt.currencyCode, + }); + }; + + /** + * Retrieves the receipt formatted total. + * @returns {string} + */ + protected totalFormatted = (receipt: SaleReceipt): string => { + return this.formatNumber(receipt.total, { money: false }); + }; + + /** + * Retrieves the receipt formatted total in local currency. + * @returns {string} + */ + protected totalLocalFormatted = (receipt: SaleReceipt): string => { + return this.formatNumber(receipt.totalLocal, { money: false }); + }; + /** * Retrieves the estimate formatted subtotal. * @param {ISaleReceipt} receipt @@ -67,6 +105,37 @@ export class SaleReceiptTransformer extends Transformer { }); }; + /** + * Retrieves formatted discount amount. + * @param receipt + * @returns {string} + */ + protected discountAmountFormatted = (receipt: SaleReceipt): string => { + return this.formatNumber(receipt.discountAmount, { + currencyCode: receipt.currencyCode, + }); + }; + + /** + * Retrieves formatted discount percentage. + * @param receipt + * @returns {string} + */ + protected discountPercentageFormatted = (receipt: SaleReceipt): string => { + return receipt.discountPercentage ? `${receipt.discountPercentage}%` : ''; + }; + + /** + * Retrieves formatted adjustment amount. + * @param receipt + * @returns {string} + */ + protected adjustmentFormatted = (receipt: SaleReceipt): string => { + return this.formatMoney(receipt.adjustment, { + currencyCode: receipt.currencyCode, + }); + }; + /** * Retrieves the entries of the credit note. * @param {ISaleReceipt} credit diff --git a/packages/server/src/modules/SaleReceipts/queries/SaleReceiptsPdf.service.ts b/packages/server/src/modules/SaleReceipts/queries/SaleReceiptsPdf.service.ts index 0ce8e126b..b999c07a6 100644 --- a/packages/server/src/modules/SaleReceipts/queries/SaleReceiptsPdf.service.ts +++ b/packages/server/src/modules/SaleReceipts/queries/SaleReceiptsPdf.service.ts @@ -4,12 +4,12 @@ import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate.servi import { transformReceiptToBrandingTemplateAttributes } from '../utils'; import { SaleReceipt } from '../models/SaleReceipt'; import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; -import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; import { ISaleReceiptBrandingTemplateAttributes } from '../types/SaleReceipts.types'; import { events } from '@/common/events/events'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { renderReceiptPaperTemplateHtml } from '@bigcapital/pdf-templates'; @Injectable() export class SaleReceiptsPdfService { @@ -24,7 +24,6 @@ export class SaleReceiptsPdfService { */ constructor( private readonly chromiumlyTenancy: ChromiumlyTenancy, - private readonly templateInjectable: TemplateInjectable, private readonly getSaleReceiptService: GetSaleReceipt, private readonly saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate, private readonly eventPublisher: EventEmitter2, @@ -38,6 +37,16 @@ export class SaleReceiptsPdfService { >, ) {} + /** + * Retrieves sale receipt html content. + * @param {number} saleReceiptId + */ + public async saleReceiptHtml(saleReceiptId: number) { + const brandingAttributes = + await this.getReceiptBrandingAttributes(saleReceiptId); + return renderReceiptPaperTemplateHtml(brandingAttributes); + } + /** * Retrieves sale invoice pdf content. * @param {number} saleReceiptId - Sale receipt identifier. @@ -47,14 +56,8 @@ export class SaleReceiptsPdfService { saleReceiptId: number, ): Promise<[Buffer, string]> { const filename = await this.getSaleReceiptFilename(saleReceiptId); + const htmlContent = await this.saleReceiptHtml(saleReceiptId); - const brandingAttributes = - await this.getReceiptBrandingAttributes(saleReceiptId); - // Converts the receipt template to html content. - const htmlContent = await this.templateInjectable.render( - 'modules/receipt-regular', - brandingAttributes, - ); // Renders the html content to pdf document. const content = await this.chromiumlyTenancy.convertHtmlContent(htmlContent); diff --git a/packages/server/src/modules/SaleReceipts/utils.ts b/packages/server/src/modules/SaleReceipts/utils.ts index bc971e1d0..e70ac5e72 100644 --- a/packages/server/src/modules/SaleReceipts/utils.ts +++ b/packages/server/src/modules/SaleReceipts/utils.ts @@ -9,8 +9,8 @@ export const transformReceiptToBrandingTemplateAttributes = ( saleReceipt: ISaleReceipt ): Partial => { return { - total: saleReceipt.formattedAmount, - subtotal: saleReceipt.formattedSubtotal, + total: saleReceipt.totalFormatted, + subtotal: saleReceipt.subtotalFormatted, lines: saleReceipt.entries?.map((entry) => ({ item: entry.item.name, description: entry.description, @@ -20,6 +20,11 @@ export const transformReceiptToBrandingTemplateAttributes = ( })), receiptNumber: saleReceipt.receiptNumber, receiptDate: saleReceipt.formattedReceiptDate, + adjustment: saleReceipt.adjustmentFormatted, + discount: saleReceipt.discountAmountFormatted, + discountLabel: saleReceipt.discountPercentageFormatted + ? `Discount [${saleReceipt.discountPercentageFormatted}]` + : 'Discount', customerAddress: contactAddressTextFormat(saleReceipt.customer), }; }; diff --git a/packages/webapp/src/hooks/query/estimates.tsx b/packages/webapp/src/hooks/query/estimates.tsx index 7ed31e34a..5102a8dcd 100644 --- a/packages/webapp/src/hooks/query/estimates.tsx +++ b/packages/webapp/src/hooks/query/estimates.tsx @@ -320,7 +320,7 @@ export function useSaleEstimateMailState( const apiRequest = useApiRequest(); return useQuery([t.SALE_ESTIMATE_MAIL_OPTIONS, estimateId], () => apiRequest - .get(`sale-estimates/${estimateId}/mail/state`) + .get(`sale-estimates/${estimateId}/mail`) .then((res) => transformToCamelCase(res.data)), ); } diff --git a/packages/webapp/src/hooks/query/invoices.tsx b/packages/webapp/src/hooks/query/invoices.tsx index 80a7bcb91..c5e0859b4 100644 --- a/packages/webapp/src/hooks/query/invoices.tsx +++ b/packages/webapp/src/hooks/query/invoices.tsx @@ -460,8 +460,8 @@ export function useSaleInvoiceMailState( [t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId], () => apiRequest - .get(`/sale-invoices/${invoiceId}/mail/state`) - .then((res) => transformToCamelCase(res.data?.data)), + .get(`/sale-invoices/${invoiceId}/mail`) + .then((res) => transformToCamelCase(res.data)), options, ); }