mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
refactor: mail templates
This commit is contained in:
@@ -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 => {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 we’re 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',
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user