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

@@ -119,7 +119,6 @@ export class CreditNoteTransformer extends Transformer {
/** /**
* Retrieves formatted discount percentage. * Retrieves formatted discount percentage.
* @param credit
* @returns {string} * @returns {string}
*/ */
protected discountPercentageFormatted = (credit): string => { protected discountPercentageFormatted = (credit): string => {
@@ -128,7 +127,6 @@ export class CreditNoteTransformer extends Transformer {
/** /**
* Retrieves formatted adjustment amount. * Retrieves formatted adjustment amount.
* @param credit
* @returns {string} * @returns {string}
*/ */
protected adjustmentFormatted = (credit): string => { protected adjustmentFormatted = (credit): string => {

View File

@@ -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<string>;
@IsString()
@IsNotEmpty()
subject: string;
@IsString()
@IsNotEmpty()
message: string;
@IsArray()
@ArrayMinSize(1)
@IsNotEmpty()
to: Array<string>;
@IsArray()
cc?: Array<string>;
@IsArray()
bcc?: Array<string>;
@IsObject()
formatArgs?: Record<string, any>;
@IsArray()
toOptions: Array<AddressItem>;
@IsArray()
fromOptions: Array<AddressItem>;
}

View File

@@ -1,6 +1,4 @@
import { import {
IPaymentReceivedCreateDTO,
IPaymentReceivedEditDTO,
IPaymentsReceivedFilter, IPaymentsReceivedFilter,
PaymentReceiveMailOptsDTO, PaymentReceiveMailOptsDTO,
} from './types/PaymentReceived.types'; } from './types/PaymentReceived.types';
@@ -79,7 +77,9 @@ export class PaymentReceivesApplication {
* @param {IPaymentsReceivedFilter} filterDTO * @param {IPaymentsReceivedFilter} filterDTO
* @returns * @returns
*/ */
public async getPaymentsReceived(filterDTO: Partial<IPaymentsReceivedFilter>) { public async getPaymentsReceived(
filterDTO: Partial<IPaymentsReceivedFilter>,
) {
return this.getPaymentsReceivedService.getPaymentReceives(filterDTO); 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. * Retrieves the create/edit initial state of the payment received.
* @returns {Promise<IPaymentReceivedState>} * @returns {Promise<IPaymentReceivedState>}

View File

@@ -144,7 +144,7 @@ export class PaymentReceivesController {
description: description:
'The payment received details have been successfully retrieved.', 'The payment received details have been successfully retrieved.',
}) })
public getPaymentReceive( public async getPaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number, @Param('id', ParseIntPipe) paymentReceiveId: number,
@Headers('accept') acceptHeader: string, @Headers('accept') acceptHeader: string,
) { ) {
@@ -152,6 +152,12 @@ export class PaymentReceivesController {
return this.paymentReceivesApplication.getPaymentReceivePdf( return this.paymentReceivesApplication.getPaymentReceivePdf(
paymentReceiveId, paymentReceiveId,
); );
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent =
await this.paymentReceivesApplication.getPaymentReceivedHtml(
paymentReceiveId,
);
return { htmlContent };
} else { } else {
return this.paymentReceivesApplication.getPaymentReceive( return this.paymentReceivesApplication.getPaymentReceive(
paymentReceiveId, 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 = export const DEFAULT_PAYMENT_MAIL_SUBJECT =
'Payment Received for {Customer Name} from {Company Name}'; 'Payment Received for {Customer Name} from {Company Name}';
export const DEFAULT_PAYMENT_MAIL_CONTENT = ` export const DEFAULT_PAYMENT_MAIL_CONTENT = `Dear {Customer Name}
<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>
<p> Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!
<i>Regards</i><br />
<i>{Company Name}</i> Payment Transaction: {Payment Number}
</p> Payment Date : {Payment Date}
`; Amount : {Payment Amount}
Regards,
{Company Name}`;
export const ERRORS = { export const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', 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 { Customer } = require('../../Customers/models/Customer');
const { Account } = require('../../Accounts/models/Account.model'); const { Account } = require('../../Accounts/models/Account.model');
const { Branch } = require('../../Branches/models/Branch.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 { return {
customer: { customer: {
@@ -154,6 +157,18 @@ export class PaymentReceived extends TenantBaseModel {
query.where('model_ref', 'PaymentReceive'); 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 { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates';
import { GetPaymentReceivedService } from './GetPaymentReceived.service'; import { GetPaymentReceivedService } from './GetPaymentReceived.service';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate.service'; import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate.service';
import { transformPaymentReceivedToPdfTemplate } from '../utils'; import { transformPaymentReceivedToPdfTemplate } from '../utils';
import { PaymentReceived } from '../models/PaymentReceived'; import { PaymentReceived } from '../models/PaymentReceived';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; 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 { PaymentReceivedPdfTemplateAttributes } from '../types/PaymentReceived.types';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { events } from '@/common/events/events';
@Injectable() @Injectable()
export class GetPaymentReceivedPdfService { export class GetPaymentReceivedPdfService {
constructor( constructor(
private chromiumlyTenancy: ChromiumlyTenancy, private chromiumlyTenancy: ChromiumlyTenancy,
private templateInjectable: TemplateInjectable,
private getPaymentService: GetPaymentReceivedService, private getPaymentService: GetPaymentReceivedService,
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate, private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate,
private eventPublisher: EventEmitter2, private eventPublisher: EventEmitter2,
@@ -28,23 +26,31 @@ export class GetPaymentReceivedPdfService {
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>, 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. * Retrieve sale invoice pdf content.
* @param {number} tenantId - * @param {number} paymentReceivedId - Payment received id.
* @param {IPaymentReceived} paymentReceive -
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
async getPaymentReceivePdf( async getPaymentReceivePdf(
paymentReceivedId: number, paymentReceivedId: number,
): Promise<[Buffer, string]> { ): Promise<[Buffer, string]> {
const brandingAttributes = const htmlContent = await this.getPaymentReceivedHtml(paymentReceivedId);
await this.getPaymentBrandingAttributes(paymentReceivedId);
const htmlContent = await this.templateInjectable.render(
'modules/payment-receive-standard',
brandingAttributes,
);
const filename = await this.getPaymentReceivedFilename(paymentReceivedId); const filename = await this.getPaymentReceivedFilename(paymentReceivedId);
// Converts the given html content to pdf document. // Converts the given html content to pdf document.
const content = const content =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent); await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
@@ -98,7 +104,6 @@ export class GetPaymentReceivedPdfService {
await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate( await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate(
templateId, templateId,
); );
return { return {
...brandingTemplate.attributes, ...brandingTemplate.attributes,
...transformPaymentReceivedToPdfTemplate(paymentReceived), ...transformPaymentReceivedToPdfTemplate(paymentReceived),

View File

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

View File

@@ -18,6 +18,7 @@ import {
CreateSaleEstimateDto, CreateSaleEstimateDto,
EditSaleEstimateDto, EditSaleEstimateDto,
} from './dtos/SaleEstimate.dto'; } from './dtos/SaleEstimate.dto';
import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service';
@Injectable() @Injectable()
export class SaleEstimatesApplication { export class SaleEstimatesApplication {
@@ -33,6 +34,7 @@ export class SaleEstimatesApplication {
private readonly sendEstimateMailService: SendSaleEstimateMail, private readonly sendEstimateMailService: SendSaleEstimateMail,
private readonly getSaleEstimateStateService: GetSaleEstimateState, private readonly getSaleEstimateStateService: GetSaleEstimateState,
private readonly saleEstimatesPdfService: GetSaleEstimatePdf, private readonly saleEstimatesPdfService: GetSaleEstimatePdf,
private readonly getSaleEstimateMailStateService: GetSaleEstimateMailStateService,
) {} ) {}
/** /**
@@ -172,4 +174,23 @@ export class SaleEstimatesApplication {
public getSaleEstimateState() { public getSaleEstimateState() {
return this.getSaleEstimateStateService.getSaleEstimateState(); return this.getSaleEstimateStateService.getSaleEstimateState();
} }
/**
* Retrieves the sale estimate mail state.
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailOptions>}
*/
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);
}
} }

View File

@@ -208,7 +208,7 @@ export class SaleEstimatesController {
} }
@Get(':id/mail') @Get(':id/mail')
@ApiOperation({ summary: 'Retrieves the sale estimate mail details.' }) @ApiOperation({ summary: 'Retrieves the sale estimate mail state.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
required: true, required: true,
@@ -218,7 +218,9 @@ export class SaleEstimatesController {
public getSaleEstimateMail( public getSaleEstimateMail(
@Param('id', ParseIntPipe) saleEstimateId: number, @Param('id', ParseIntPipe) saleEstimateId: number,
) { ) {
return this.saleEstimatesApplication.getSaleEstimateMail(saleEstimateId); return this.saleEstimatesApplication.getSaleEstimateMailState(
saleEstimateId,
);
} }
@Get(':id') @Get(':id')
@@ -243,6 +245,11 @@ export class SaleEstimatesController {
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
}); });
res.send(pdfContent); res.send(pdfContent);
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent =
await this.saleEstimatesApplication.getSaleEstimateHtml(estimateId);
return { htmlContent };
} else { } else {
return this.saleEstimatesApplication.getSaleEstimate(estimateId); return this.saleEstimatesApplication.getSaleEstimate(estimateId);
} }

View File

@@ -37,6 +37,8 @@ import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { SendSaleEstimateMailQueue } from './types/SaleEstimates.types'; import { SendSaleEstimateMailQueue } from './types/SaleEstimates.types';
import { SaleEstimatesExportable } from './SaleEstimatesExportable'; import { SaleEstimatesExportable } from './SaleEstimatesExportable';
import { SaleEstimatesImportable } from './SaleEstimatesImportable'; import { SaleEstimatesImportable } from './SaleEstimatesImportable';
import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service';
import { GetSaleEstimateMailTemplateService } from './queries/GetSaleEstimateMailTemplate.service';
@Module({ @Module({
imports: [ imports: [
@@ -78,11 +80,15 @@ import { SaleEstimatesImportable } from './SaleEstimatesImportable';
GetSaleEstimatePdf, GetSaleEstimatePdf,
SaleEstimatePdfTemplate, SaleEstimatePdfTemplate,
SaleEstimatesExportable, SaleEstimatesExportable,
SaleEstimatesImportable SaleEstimatesImportable,
GetSaleEstimateMailStateService,
GetSaleEstimateMailTemplateService
], ],
exports: [ exports: [
SaleEstimatesExportable, SaleEstimatesExportable,
SaleEstimatesImportable SaleEstimatesImportable,
GetSaleEstimateMailStateService,
GetSaleEstimateMailTemplateService
] ]
}) })
export class SaleEstimatesModule {} export class SaleEstimatesModule {}

View File

@@ -23,6 +23,7 @@ import { SaleEstimateMailOptions } from '../types/SaleEstimates.types';
import { Mail } from '@/modules/Mail/Mail'; import { Mail } from '@/modules/Mail/Mail';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { GetSaleEstimateMailTemplateService } from '../queries/GetSaleEstimateMailTemplate.service';
@Injectable() @Injectable()
export class SendSaleEstimateMail { export class SendSaleEstimateMail {
@@ -38,6 +39,7 @@ export class SendSaleEstimateMail {
private readonly estimatePdf: GetSaleEstimatePdf, private readonly estimatePdf: GetSaleEstimatePdf,
private readonly getSaleEstimateService: GetSaleEstimate, private readonly getSaleEstimateService: GetSaleEstimate,
private readonly contactMailNotification: ContactMailNotification, private readonly contactMailNotification: ContactMailNotification,
private readonly getEstimateMailTemplate: GetSaleEstimateMailTemplateService,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
private readonly mailTransporter: MailTransporter, private readonly mailTransporter: MailTransporter,
@@ -78,7 +80,12 @@ export class SendSaleEstimateMail {
*/ */
public formatterArgs = async (estimateId: number) => { public formatterArgs = async (estimateId: number) => {
const estimate = await this.getSaleEstimateService.getEstimate(estimateId); 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, mailOptions,
formatterArgs, 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<SaleEstimateMailOptions>}
*/
public async getFormattedMailOptions(
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO,
): Promise<SaleEstimateMailOptions> {
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. * Sends the mail notification of the given sale estimate.
* @param {number} saleEstimateId - Sale estimate id. * @param {number} saleEstimateId - Sale estimate id.
@@ -142,16 +175,9 @@ export class SendSaleEstimateMail {
saleEstimateId: number, saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO, messageOptions: SaleEstimateMailOptionsDTO,
): Promise<void> { ): Promise<void> {
const localMessageOpts = await this.getMailOptions(saleEstimateId); const formattedOptions = await this.getFormattedMailOptions(
// Overrides and validates the given mail options.
const parsedMessageOptions = mergeAndValidateMailOptions(
localMessageOpts,
messageOptions,
) as SaleEstimateMailOptions;
const formattedOptions = await this.formatMailOptions(
saleEstimateId, saleEstimateId,
parsedMessageOptions, messageOptions,
); );
const mail = new Mail() const mail = new Mail()
.setSubject(formattedOptions.subject) .setSubject(formattedOptions.subject)
@@ -173,7 +199,6 @@ export class SendSaleEstimateMail {
}, },
]); ]);
} }
const eventPayload = { const eventPayload = {
saleEstimateId, saleEstimateId,
messageOptions, messageOptions,

View File

@@ -1,18 +1,17 @@
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT = export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
'Estimate {Estimate Number} is awaiting your approval'; 'Estimate {Estimate Number} is awaiting your approval';
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {Customer Name}</p> export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `Hi {Customer Name},
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
<p>
Estimate <strong>#{Estimate Number}</strong><br />
Expiration Date : <strong>{Estimate Expiration Date}</strong><br />
Amount : <strong>{Estimate Amount}</strong></br />
</p>
<p> Here's estimate # {Estimate Number} for {Estimate Amount}
<i>Regards</i><br />
<i>{Company Name}</i> This estimate is valid until {Estimate Expiration Date}, and were happy to discuss any adjustments you or questions may have.
</p>
`; 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 = { export const ERRORS = {
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND', SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',

View File

@@ -1,6 +1,5 @@
import * as moment from 'moment'; import * as moment from 'moment';
import { Model } from 'objection'; import { Model } from 'objection';
import { Injectable } from '@nestjs/common';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator';
import { ImportableModel } from '@/modules/Import/decorators/Import.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. * Retrieve the default custom views, roles and columns.

View File

@@ -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;
}
}

View File

@@ -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<typeof SaleEstimate>
) {}
/**
* 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<SaleEstimateMailState>}
*/
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;
}
}

View File

@@ -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',
];
};
}

View File

@@ -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<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
estimateId: number,
): Promise<EstimatePaymentEmailProps> {
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<any>,
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(estimateId);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderEstimateEmailTemplate(mergedAttributes);
}
}

View File

@@ -4,18 +4,17 @@ import { GetSaleEstimate } from './GetSaleEstimate.service';
import { transformEstimateToPdfTemplate } from '../utils'; import { transformEstimateToPdfTemplate } from '../utils';
import { EstimatePdfBrandingAttributes } from '../constants'; import { EstimatePdfBrandingAttributes } from '../constants';
import { SaleEstimatePdfTemplate } from '@/modules/SaleInvoices/queries/SaleEstimatePdfTemplate.service'; import { SaleEstimatePdfTemplate } from '@/modules/SaleInvoices/queries/SaleEstimatePdfTemplate.service';
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { SaleEstimate } from '../models/SaleEstimate'; import { SaleEstimate } from '../models/SaleEstimate';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { renderEstimatePaperTemplateHtml } from '@bigcapital/pdf-templates';
@Injectable() @Injectable()
export class GetSaleEstimatePdf { export class GetSaleEstimatePdf {
constructor( constructor(
private readonly chromiumlyTenancy: ChromiumlyTenancy, private readonly chromiumlyTenancy: ChromiumlyTenancy,
private readonly templateInjectable: TemplateInjectable,
private readonly getSaleEstimate: GetSaleEstimate, private readonly getSaleEstimate: GetSaleEstimate,
private readonly estimatePdfTemplate: SaleEstimatePdfTemplate, private readonly estimatePdfTemplate: SaleEstimatePdfTemplate,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
@@ -29,22 +28,29 @@ export class GetSaleEstimatePdf {
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>, private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
) {} ) {}
/**
* Retrieve sale estimate html content.
* @param {number} invoiceId -
*/
public async saleEstimateHtml(estimateId: number): Promise<string> {
const brandingAttributes =
await this.getEstimateBrandingAttributes(estimateId);
return renderEstimatePaperTemplateHtml({ ...brandingAttributes });
}
/** /**
* Retrieve sale invoice pdf content. * Retrieve sale invoice pdf content.
* @param {number} tenantId - * @param {ISaleInvoice} saleInvoice - Sale estimate id.
* @param {ISaleInvoice} saleInvoice -
*/ */
public async getSaleEstimatePdf( public async getSaleEstimatePdf(
saleEstimateId: number, saleEstimateId: number,
): Promise<[Buffer, string]> { ): Promise<[Buffer, string]> {
const filename = await this.getSaleEstimateFilename(saleEstimateId); const filename = await this.getSaleEstimateFilename(saleEstimateId);
const brandingAttributes =
await this.getEstimateBrandingAttributes(saleEstimateId);
const htmlContent = await this.templateInjectable.render( // Retrieves the sale estimate html.
'modules/estimate-regular', const htmlContent = await this.saleEstimateHtml(saleEstimateId);
brandingAttributes,
);
const content = const content =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent); await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
const eventPayload = { saleEstimateId }; const eventPayload = { saleEstimateId };
@@ -72,7 +78,6 @@ export class GetSaleEstimatePdf {
/** /**
* Retrieves the given estimate branding attributes. * Retrieves the given estimate branding attributes.
* @param {number} tenantId - Tenant id.
* @param {number} estimateId - Estimate id. * @param {number} estimateId - Estimate id.
* @returns {Promise<EstimatePdfBrandingAttributes>} * @returns {Promise<EstimatePdfBrandingAttributes>}
*/ */

View File

@@ -171,6 +171,9 @@ export class SaleInvoicesController {
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
}); });
res.send(pdfContent); res.send(pdfContent);
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent = await this.saleInvoiceApplication.saleInvoiceHtml(id);
return { htmlContent };
} else { } else {
return this.saleInvoiceApplication.getSaleInvoice(id); return this.saleInvoiceApplication.getSaleInvoice(id);
} }
@@ -270,7 +273,7 @@ export class SaleInvoicesController {
return this.saleInvoiceApplication.saleInvoiceHtml(id); return this.saleInvoiceApplication.saleInvoiceHtml(id);
} }
@Get(':id/mail-state') @Get(':id/mail')
@ApiOperation({ summary: 'Retrieves the sale invoice mail state.' }) @ApiOperation({ summary: 'Retrieves the sale invoice mail state.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -39,7 +39,7 @@ export class GetInvoicePaymentMail {
/** /**
* Retrieves the mail template html content. * Retrieves the mail template html content.
* @param {number} invoiceId - Invoice id. * @param {number} invoiceId - Sale invoice id.
*/ */
public async getMailTemplate( public async getMailTemplate(
invoiceId: number, invoiceId: number,

View File

@@ -1,4 +1,4 @@
import { Transformer } from "@/modules/Transformer/Transformer"; import { Transformer } from '@/modules/Transformer/Transformer';
export class GetInvoicePaymentMailAttributesTransformer extends Transformer { export class GetInvoicePaymentMailAttributesTransformer extends Transformer {
/** /**
@@ -26,6 +26,15 @@ export class GetInvoicePaymentMailAttributesTransformer extends Transformer {
'total', 'total',
'totalLabel', 'totalLabel',
'subtotal',
'subtotalLabel',
'discount',
'discountLabel',
'adjustment',
'adjustmentLabel',
'dueAmount', 'dueAmount',
'dueAmountLabel', 'dueAmountLabel',
@@ -76,6 +85,30 @@ export class GetInvoicePaymentMailAttributesTransformer extends Transformer {
return 'Invoice # {invoiceNumber}'; 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 { public total(): string {
return this.options.invoice?.totalFormatted; return this.options.invoice?.totalFormatted;
} }
@@ -103,7 +136,7 @@ export class GetInvoicePaymentMailAttributesTransformer extends Transformer {
public items(): Array<any> { public items(): Array<any> {
return this.item( return this.item(
this.options.invoice?.entries, this.options.invoice?.entries,
new GetInvoiceMailTemplateItemAttrsTransformer() new GetInvoiceMailTemplateItemAttrsTransformer(),
); );
} }
} }

View File

@@ -31,6 +31,15 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
'subtotal', 'subtotal',
'subtotalFormatted', 'subtotalFormatted',
'discountAmount',
'discountAmountFormatted',
'discountPercentage',
'discountPercentageFormatted',
'discountLabel',
'adjustment',
'adjustmentFormatted',
'invoiceNo', 'invoiceNo',
'entries', 'entries',
@@ -76,6 +85,17 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
return invoice.pdfTemplate?.attributes?.primaryColor; 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 * @param invoice
@@ -87,7 +107,7 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
new GetSaleInvoiceMailStateEntryTransformer(), new GetSaleInvoiceMailStateEntryTransformer(),
{ {
currencyCode: invoice.currencyCode, currencyCode: invoice.currencyCode,
} },
); );
}; };

View File

@@ -3,6 +3,7 @@ import { SaleInvoice } from '../models/SaleInvoice';
import { ItemEntryTransformer } from '../../TransactionItemEntry/ItemEntry.transformer'; import { ItemEntryTransformer } from '../../TransactionItemEntry/ItemEntry.transformer';
import { AttachmentTransformer } from '../../Attachments/Attachment.transformer'; import { AttachmentTransformer } from '../../Attachments/Attachment.transformer';
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer'; import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer';
import { DiscountType } from '@/common/types/Discount';
export class SaleInvoiceTransformer extends Transformer { export class SaleInvoiceTransformer extends Transformer {
/** /**
@@ -25,6 +26,9 @@ export class SaleInvoiceTransformer extends Transformer {
'taxAmountWithheldLocalFormatted', 'taxAmountWithheldLocalFormatted',
'totalFormatted', 'totalFormatted',
'totalLocalFormatted', 'totalLocalFormatted',
'discountAmountFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'taxes', 'taxes',
'entries', 'entries',
'attachments', '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. * Retrieve the taxes lines of sale invoice.
* @param {ISaleInvoice} invoice * @param {ISaleInvoice} invoice

View File

@@ -47,7 +47,6 @@ export class SaleInvoicePdf {
*/ */
public async getSaleInvoicePdf(invoiceId: number): Promise<[Buffer, string]> { public async getSaleInvoicePdf(invoiceId: number): Promise<[Buffer, string]> {
const filename = await this.getInvoicePdfFilename(invoiceId); const filename = await this.getInvoicePdfFilename(invoiceId);
const htmlContent = await this.getSaleInvoiceHtml(invoiceId); const htmlContent = await this.getSaleInvoiceHtml(invoiceId);
// Converts the given html content to pdf document. // Converts the given html content to pdf document.

View File

@@ -44,7 +44,10 @@ export const transformInvoiceToPdfTemplate = (
label: tax.name, label: tax.name,
amount: tax.taxRateAmountFormatted, amount: tax.taxRateAmountFormatted,
})), })),
discount: invoice.discountAmountFormatted,
discountLabel: invoice.discountPercentageFormatted
? `Discount [${invoice.discountPercentageFormatted}]`
: 'Discount',
customerAddress: contactAddressTextFormat(invoice.customer), customerAddress: contactAddressTextFormat(invoice.customer),
}; };
}; };

View File

@@ -17,7 +17,11 @@ import { GetSaleReceiptsService } from './queries/GetSaleReceipts.service';
import { SaleReceipt } from './models/SaleReceipt'; import { SaleReceipt } from './models/SaleReceipt';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { SaleReceiptMailNotification } from './commands/SaleReceiptMailNotification'; 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() @Injectable()
export class SaleReceiptApplication { export class SaleReceiptApplication {
@@ -31,6 +35,7 @@ export class SaleReceiptApplication {
private getSaleReceiptPdfService: SaleReceiptsPdfService, private getSaleReceiptPdfService: SaleReceiptsPdfService,
private getSaleReceiptStateService: GetSaleReceiptState, private getSaleReceiptStateService: GetSaleReceiptState,
private saleReceiptNotifyByMailService: SaleReceiptMailNotification, private saleReceiptNotifyByMailService: SaleReceiptMailNotification,
private getSaleReceiptMailStateService: GetSaleReceiptMailStateService,
) {} ) {}
/** /**
@@ -172,4 +177,23 @@ export class SaleReceiptApplication {
public getSaleReceiptState(): Promise<ISaleReceiptState> { public getSaleReceiptState(): Promise<ISaleReceiptState> {
return this.getSaleReceiptStateService.getSaleReceiptState(); return this.getSaleReceiptStateService.getSaleReceiptState();
} }
/**
* Retrieves the given sale receipt html.
* @param {number} saleReceiptId
* @returns {Promise<string>}
*/
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<ISaleReceiptState> {
return this.getSaleReceiptMailStateService.getMailState(saleReceiptId);
}
} }

View File

@@ -107,6 +107,11 @@ export class SaleReceiptsController {
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
}); });
res.send(pdfContent); res.send(pdfContent);
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent =
await this.saleReceiptApplication.getSaleReceiptHtml(id);
return { htmlContent };
} else { } else {
return this.saleReceiptApplication.getSaleReceipt(id); return this.saleReceiptApplication.getSaleReceipt(id);
} }

View File

@@ -37,6 +37,8 @@ import { MailModule } from '../Mail/Mail.module';
import { SendSaleReceiptMailQueue } from './constants'; import { SendSaleReceiptMailQueue } from './constants';
import { SaleReceiptsExportable } from './commands/SaleReceiptsExportable'; import { SaleReceiptsExportable } from './commands/SaleReceiptsExportable';
import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable'; import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable';
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service';
@Module({ @Module({
controllers: [SaleReceiptsController], controllers: [SaleReceiptsController],
@@ -79,6 +81,8 @@ import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable';
SendSaleReceiptMailProcess, SendSaleReceiptMailProcess,
SaleReceiptsExportable, SaleReceiptsExportable,
SaleReceiptsImportable, SaleReceiptsImportable,
GetSaleReceiptMailStateService,
GetSaleReceiptMailTemplateService
], ],
}) })
export class SaleReceiptsModule {} export class SaleReceiptsModule {}

View File

@@ -23,8 +23,9 @@ import {
import { SaleReceipt } from '../models/SaleReceipt'; import { SaleReceipt } from '../models/SaleReceipt';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { Mail } from '@/modules/Mail/Mail'; import { Mail } from '@/modules/Mail/Mail';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { GetSaleReceiptMailTemplateService } from '../queries/GetSaleReceiptMailTemplate.service';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable() @Injectable()
export class SaleReceiptMailNotification { export class SaleReceiptMailNotification {
@@ -42,6 +43,7 @@ export class SaleReceiptMailNotification {
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly mailTransporter: MailTransporter, private readonly mailTransporter: MailTransporter,
private readonly tenancyContext: TenancyContext, private readonly tenancyContext: TenancyContext,
private readonly getReceiptMailTemplateService: GetSaleReceiptMailTemplateService,
@Inject(SaleReceipt.name) @Inject(SaleReceipt.name)
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>, private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@@ -119,8 +121,12 @@ export class SaleReceiptMailNotification {
receiptId: number, receiptId: number,
): Promise<Record<string, string>> => { ): Promise<Record<string, string>> => {
const receipt = await this.getSaleReceiptService.getSaleReceipt(receiptId); 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, mailOptions,
formatterArgs, formatterArgs,
)) as SaleReceiptMailOpts; )) as SaleReceiptMailOpts;
return formattedOptions;
const message = await this.getReceiptMailTemplateService.getMailTemplate(
receiptId,
{
message: formattedOptions.message,
},
);
return { ...formattedOptions, message };
} }
/** /**

View File

@@ -1,18 +1,18 @@
export const DEFAULT_RECEIPT_MAIL_SUBJECT = export const DEFAULT_RECEIPT_MAIL_SUBJECT =
'Receipt {Receipt Number} from {Company Name}'; 'Receipt {Receipt Number} from {Company Name}';
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
<p>
Receipt <strong>#{Receipt Number}</strong><br />
Amount : <strong>{Receipt Amount}</strong></br />
</p>
<p> export const DEFAULT_RECEIPT_MAIL_CONTENT = `Hi {Customer Name},
<i>Regards</i><br />
<i>{Company Name}</i> Here's receipt # {Receipt Number} for Receipt {Receipt Amount}
</p>
`; 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 SendSaleReceiptMailQueue = 'SendSaleReceiptMailQueue';
export const SendSaleReceiptMailJob = 'SendSaleReceiptMailJob'; export const SendSaleReceiptMailJob = 'SendSaleReceiptMailJob';

View File

@@ -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<typeof SaleReceipt>
) {}
/**
* 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 },
);
}
}

View File

@@ -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',
];
};
}

View File

@@ -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<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
receiptId: number,
): Promise<ReceiptEmailTemplateProps> {
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<any>,
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(receiptId);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderReceiptEmailTemplate(mergedAttributes);
}
}

View File

@@ -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;
}
}

View File

@@ -15,6 +15,10 @@ export class SaleReceiptTransformer extends Transformer {
'formattedReceiptDate', 'formattedReceiptDate',
'formattedClosedAtDate', 'formattedClosedAtDate',
'formattedCreatedAt', 'formattedCreatedAt',
'subtotalFormatted',
'subtotalLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'entries', 'entries',
'attachments', 'attachments',
]; ];
@@ -47,6 +51,40 @@ export class SaleReceiptTransformer extends Transformer {
return this.formatDate(receipt.createdAt); 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. * Retrieves the estimate formatted subtotal.
* @param {ISaleReceipt} receipt * @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. * Retrieves the entries of the credit note.
* @param {ISaleReceipt} credit * @param {ISaleReceipt} credit

View File

@@ -4,12 +4,12 @@ import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate.servi
import { transformReceiptToBrandingTemplateAttributes } from '../utils'; import { transformReceiptToBrandingTemplateAttributes } from '../utils';
import { SaleReceipt } from '../models/SaleReceipt'; import { SaleReceipt } from '../models/SaleReceipt';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { ISaleReceiptBrandingTemplateAttributes } from '../types/SaleReceipts.types'; import { ISaleReceiptBrandingTemplateAttributes } from '../types/SaleReceipts.types';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { renderReceiptPaperTemplateHtml } from '@bigcapital/pdf-templates';
@Injectable() @Injectable()
export class SaleReceiptsPdfService { export class SaleReceiptsPdfService {
@@ -24,7 +24,6 @@ export class SaleReceiptsPdfService {
*/ */
constructor( constructor(
private readonly chromiumlyTenancy: ChromiumlyTenancy, private readonly chromiumlyTenancy: ChromiumlyTenancy,
private readonly templateInjectable: TemplateInjectable,
private readonly getSaleReceiptService: GetSaleReceipt, private readonly getSaleReceiptService: GetSaleReceipt,
private readonly saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate, private readonly saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate,
private readonly eventPublisher: EventEmitter2, 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. * Retrieves sale invoice pdf content.
* @param {number} saleReceiptId - Sale receipt identifier. * @param {number} saleReceiptId - Sale receipt identifier.
@@ -47,14 +56,8 @@ export class SaleReceiptsPdfService {
saleReceiptId: number, saleReceiptId: number,
): Promise<[Buffer, string]> { ): Promise<[Buffer, string]> {
const filename = await this.getSaleReceiptFilename(saleReceiptId); 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. // Renders the html content to pdf document.
const content = const content =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent); await this.chromiumlyTenancy.convertHtmlContent(htmlContent);

View File

@@ -9,8 +9,8 @@ export const transformReceiptToBrandingTemplateAttributes = (
saleReceipt: ISaleReceipt saleReceipt: ISaleReceipt
): Partial<ISaleReceiptBrandingTemplateAttributes> => { ): Partial<ISaleReceiptBrandingTemplateAttributes> => {
return { return {
total: saleReceipt.formattedAmount, total: saleReceipt.totalFormatted,
subtotal: saleReceipt.formattedSubtotal, subtotal: saleReceipt.subtotalFormatted,
lines: saleReceipt.entries?.map((entry) => ({ lines: saleReceipt.entries?.map((entry) => ({
item: entry.item.name, item: entry.item.name,
description: entry.description, description: entry.description,
@@ -20,6 +20,11 @@ export const transformReceiptToBrandingTemplateAttributes = (
})), })),
receiptNumber: saleReceipt.receiptNumber, receiptNumber: saleReceipt.receiptNumber,
receiptDate: saleReceipt.formattedReceiptDate, receiptDate: saleReceipt.formattedReceiptDate,
adjustment: saleReceipt.adjustmentFormatted,
discount: saleReceipt.discountAmountFormatted,
discountLabel: saleReceipt.discountPercentageFormatted
? `Discount [${saleReceipt.discountPercentageFormatted}]`
: 'Discount',
customerAddress: contactAddressTextFormat(saleReceipt.customer), customerAddress: contactAddressTextFormat(saleReceipt.customer),
}; };
}; };

View File

@@ -320,7 +320,7 @@ export function useSaleEstimateMailState(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useQuery([t.SALE_ESTIMATE_MAIL_OPTIONS, estimateId], () => return useQuery([t.SALE_ESTIMATE_MAIL_OPTIONS, estimateId], () =>
apiRequest apiRequest
.get(`sale-estimates/${estimateId}/mail/state`) .get(`sale-estimates/${estimateId}/mail`)
.then((res) => transformToCamelCase(res.data)), .then((res) => transformToCamelCase(res.data)),
); );
} }

View File

@@ -460,8 +460,8 @@ export function useSaleInvoiceMailState(
[t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId], [t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId],
() => () =>
apiRequest apiRequest
.get(`/sale-invoices/${invoiceId}/mail/state`) .get(`/sale-invoices/${invoiceId}/mail`)
.then((res) => transformToCamelCase(res.data?.data)), .then((res) => transformToCamelCase(res.data)),
options, options,
); );
} }