feat: payment received mail preview

This commit is contained in:
Ahmed Bouhuolia
2024-11-24 13:19:26 +02:00
parent da47418f17
commit 3537a05ea2
19 changed files with 438 additions and 49 deletions

View File

@@ -130,9 +130,18 @@ export default class PaymentReceivesController extends BaseController {
[
...this.paymentReceiveValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.sendPaymentReceiveByMail.bind(this),

View File

@@ -130,8 +130,18 @@ export default class SalesEstimatesController extends BaseController {
[
...this.validateSpecificEstimateSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
@@ -567,8 +577,9 @@ export default class SalesEstimatesController extends BaseController {
const { tenantId } = req;
try {
const data =
await this.saleEstimatesApplication.getSaleEstimateState(tenantId);
const data = await this.saleEstimatesApplication.getSaleEstimateState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);

View File

@@ -56,8 +56,18 @@ export default class SalesReceiptsController extends BaseController {
[
...this.specificReceiptValidationSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('body').isString().optional(),
body('attach_receipt').optional().isBoolean().toBoolean(),
],
@@ -399,8 +409,9 @@ export default class SalesReceiptsController extends BaseController {
// Retrieves receipt in pdf format.
try {
const data =
await this.saleReceiptsApplication.getSaleReceiptState(tenantId);
const data = await this.saleReceiptsApplication.getSaleReceiptState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);

View File

@@ -177,7 +177,9 @@ export type IPaymentReceiveGLCommonEntry = Pick<
| 'branchId'
>;
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
export interface PaymentReceiveMailOpts extends CommonMailOptions {
attachPdf?: boolean;
}
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
@@ -239,7 +241,6 @@ export interface PaymentReceivedPdfTemplateAttributes {
paymentReceivedDateLabel: string;
}
export interface IPaymentReceivedState {
defaultTemplateId: number;
}
}

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import {
renderEstimateEmailTemplate,
EstimatePaymentEmailProps,
} from '@bigcapital/email-components';
import { GetSaleEstimate } from './GetSaleEstimate';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetEstimateMailTemplateAttributesTransformer } from './GetEstimateMailTemplateAttributesTransformer';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
@Service()
export class GetEstimateMailTemplate {
@Inject()
private getEstimateService: GetSaleEstimate;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
/**
* Retrieves the mail template attributes of the given estimate.
* Estimate template attributes are composed of the estimate and branding template attributes.
* @param {number} tenantId
* @param {number} estimateId - Estimate id.
* @returns {Promise<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
estimateId: number
): Promise<EstimatePaymentEmailProps> {
const estimate = await this.getEstimateService.getEstimate(
tenantId,
estimateId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
estimate.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
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(
tenantId: number,
estimateId: number,
overrideAttributes?: Partial<any>
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(
tenantId,
estimateId
);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderEstimateEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,3 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetEstimateMailTemplateAttributesTransformer extends Transformer {}

View File

@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformEstimateToMailDataArgs } from './utils';
import { GetEstimateMailTemplate } from './GetEstimateMailTemplate';
@Service()
export class SendSaleEstimateMail {
@@ -32,12 +33,15 @@ export class SendSaleEstimateMail {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject('agenda')
private agenda: any;
@Inject()
private getEstimateMailTemplate: GetEstimateMailTemplate;
@Inject()
private eventPublisher: EventPublisher;
@Inject('agenda')
private agenda: any;
/**
* Triggers the reminder mail of the given sale estimate.
* @param {number} tenantId -
@@ -132,9 +136,45 @@ export class SendSaleEstimateMail {
mailOptions,
formatterArgs
);
return { ...formattedOptions };
// Retrieves the estimate mail template.
const message = await this.getEstimateMailTemplate.getMailTemplate(
tenantId,
saleEstimateId,
{
message: formattedOptions.message,
preview: formattedOptions.message,
}
);
return { ...formattedOptions, message };
};
/**
* Retrieves the formatted mail options.
* @param {number} tenantId
* @param {number} saleEstimateId
* @param {SaleEstimateMailOptionsDTO} messageOptions
* @returns
*/
public async getFormattedMailOptions(
tenantId: number,
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO
): Promise<SaleEstimateMailOptions> {
const defaultMessageOptions = await this.getMailOptions(
tenantId,
saleEstimateId
);
const parsedMessageOptions = mergeAndValidateMailOptions(
defaultMessageOptions,
messageOptions
);
return this.formatMailOptions(
tenantId,
saleEstimateId,
parsedMessageOptions
);
}
/**
* Sends the mail notification of the given sale estimate.
* @param {number} tenantId
@@ -147,20 +187,10 @@ export class SendSaleEstimateMail {
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO
): Promise<void> {
const localMessageOpts = await this.getMailOptions(
tenantId,
saleEstimateId
);
// Overrides and validates the given mail options.
const parsedMessageOptions = mergeAndValidateMailOptions(
localMessageOpts,
messageOptions
) as SaleEstimateMailOptions;
const formattedOptions = await this.formatMailOptions(
const formattedOptions = await this.getFormattedMailOptions(
tenantId,
saleEstimateId,
parsedMessageOptions
messageOptions
);
const mail = new Mail()
.setSubject(formattedOptions.subject)
@@ -182,7 +212,6 @@ export class SendSaleEstimateMail {
},
]);
}
const eventPayload = {
tenantId,
saleEstimateId,

View File

@@ -4,6 +4,7 @@ import { GetPaymentReceivedMailStateTransformer } from './GetPaymentReceivedMail
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { Inject, Service } from 'typedi';
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
@Service()
export class GetPaymentReceivedMailState {
@@ -11,7 +12,7 @@ export class GetPaymentReceivedMailState {
private tenancy: HasTenancyService;
@Inject()
private contactMailNotification: ContactMailNotification;
private paymentReceivedMail: SendPaymentReceiveMailNotification;
@Inject()
private transformer: TransformerInjectable;
@@ -35,12 +36,10 @@ export class GetPaymentReceivedMailState {
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
paymentReceive.customerId
);
const mailOptions = await this.paymentReceivedMail.getMailOptions(
tenantId,
paymentId
);
const transformed = await this.transformer.transform(
tenantId,
paymentReceive,

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { GetPaymentReceived } from './GetPaymentReceived';
import { GetPaymentReceivedMailTemplateAttrsTransformer } from './GetPaymentReceivedMailTemplateAttrsTransformer';
import {
PaymentReceivedEmailTemplateProps,
renderPaymentReceivedEmailTemplate,
} from '@bigcapital/email-components';
@Service()
export class GetPaymentReceivedMailTemplate {
@Inject()
private getPaymentReceivedService: GetPaymentReceived;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the mail template attributes of the given payment received.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceivedId - Payment received id.
* @returns {Promise<PaymentReceivedEmailTemplateProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
paymentReceivedId: number
): Promise<PaymentReceivedEmailTemplateProps> {
const paymentReceived =
await this.getPaymentReceivedService.getPaymentReceive(
tenantId,
paymentReceivedId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
paymentReceived.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
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(
tenantId: number,
paymentReceivedId: number,
overrideAttributes?: Partial<PaymentReceivedEmailTemplateProps>
): Promise<string> {
const mailTemplateAttributes = await this.getMailTemplateAttributes(
tenantId,
paymentReceivedId
);
const mergedAttributes = {
...mailTemplateAttributes,
...overrideAttributes,
};
return renderPaymentReceivedEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,3 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetPaymentReceivedMailTemplateAttrsTransformer extends Transformer {}

View File

@@ -15,8 +15,9 @@ import { GetPaymentReceived } from './GetPaymentReceived';
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformPaymentReceivedToMailDataArgs } from './utils';
import { GetPaymentReceivedMailTemplate } from './GetPaymentReceivedMailTemplate';
import events from '@/subscribers/events';
@Service()
export class SendPaymentReceiveMailNotification {
@@ -29,12 +30,15 @@ export class SendPaymentReceiveMailNotification {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject('agenda')
private agenda: any;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private paymentMailTemplate: GetPaymentReceivedMailTemplate;
@Inject('agenda')
private agenda: any;
/**
* Sends the mail of the given payment receive.
* @param {number} tenantId
@@ -77,7 +81,82 @@ export class SendPaymentReceiveMailNotification {
tenantId,
invoiceId
);
return transformPaymentReceivedToMailDataArgs(payment);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
const paymentArgs = transformPaymentReceivedToMailDataArgs(payment);
return {
...commonArgs,
...paymentArgs,
};
};
/**
* Retrieves the mail options of the given payment received.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceivedId - Payment received id.
* @param {string} defaultSubject - Default subject of the mail.
* @param {string} defaultContent - Default content of the mail.
* @returns
*/
public getMailOptions = async (
tenantId: number,
paymentReceivedId: number,
defaultSubject: string = DEFAULT_PAYMENT_MAIL_SUBJECT,
defaultContent: string = DEFAULT_PAYMENT_MAIL_CONTENT
): Promise<PaymentReceiveMailOpts> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceived = await PaymentReceive.query().findById(
paymentReceivedId
);
const formatArgs = await this.textFormatter(tenantId, paymentReceivedId);
// Retrieves the default mail options.
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
paymentReceived.customerId
);
return {
...mailOptions,
message: defaultContent,
subject: defaultSubject,
attachPdf: true,
formatArgs,
};
};
/**
* Formats the mail options of the given payment receive.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {PaymentReceiveMailOpts} mailOptions
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public formattedMailOptions = async (
tenantId: number,
paymentReceiveId: number,
mailOptions: PaymentReceiveMailOpts
): Promise<PaymentReceiveMailOpts> => {
const formatterArgs = await this.textFormatter(tenantId, paymentReceiveId);
const formattedOptions =
await this.contactMailNotification.formatMailOptions(
tenantId,
mailOptions,
formatterArgs
);
// Retrieves the mail template.
const message = await this.paymentMailTemplate.getMailTemplate(
tenantId,
paymentReceiveId,
{
message: formattedOptions.message,
preview: formattedOptions.message,
}
);
return { ...formattedOptions, message };
};
/**
@@ -105,10 +184,10 @@ export class SendPaymentReceiveMailNotification {
messageDTO
);
// Formats the message options.
return this.contactMailNotification.formatMailOptions(
return this.formattedMailOptions(
tenantId,
parsedMessageOpts,
formatterArgs
paymentReceiveId,
parsedMessageOpts
);
};

View File

@@ -0,0 +1,75 @@
import {
ReceiptEmailTemplateProps,
renderReceiptEmailTemplate,
} from '@bigcapital/email-components';
import { Inject, Service } from 'typedi';
import { GetSaleReceipt } from './GetSaleReceipt';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetSaleReceiptMailTemplateAttributesTransformer } from './GetSaleReceiptMailTemplateAttributesTransformer';
@Service()
export class GetSaleReceiptMailTemplate {
@Inject()
private getReceiptService: GetSaleReceipt;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
/**
* Retrieves the mail template attributes of the given estimate.
* Estimate template attributes are composed of the estimate and branding template attributes.
* @param {number} tenantId
* @param {number} receiptId - Receipt id.
* @returns {Promise<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
receiptId: number
): Promise<ReceiptEmailTemplateProps> {
const receipt = await this.getReceiptService.getSaleReceipt(
tenantId,
receiptId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
receipt.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
receipt,
new GetSaleReceiptMailTemplateAttributesTransformer(),
{
receipt,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} tenantId
* @param {number} estimateId
* @param overrideAttributes
* @returns
*/
public async getMailTemplate(
tenantId: number,
estimateId: number,
overrideAttributes?: Partial<any>
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(
tenantId,
estimateId
);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderReceiptEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,3 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetSaleReceiptMailTemplateAttributesTransformer extends Transformer {}

View File

@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformReceiptToMailDataArgs } from './utils';
import { GetSaleReceiptMailTemplate } from './GetSaleReceiptMailTemplate';
@Service()
export class SaleReceiptMailNotification {
@@ -32,6 +33,9 @@ export class SaleReceiptMailNotification {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject()
private getReceiptMailTemplate: GetSaleReceiptMailTemplate;
@Inject()
private eventPublisher: EventPublisher;
@@ -133,7 +137,15 @@ export class SaleReceiptMailNotification {
mailOptions,
formatterArgs
)) as SaleReceiptMailOpts;
return formattedOptions;
const message = await this.getReceiptMailTemplate.getMailTemplate(
tenantId,
receiptId,
{
message: formattedOptions.message,
}
);
return { ...formattedOptions, message };
}
/**

View File

@@ -40,7 +40,7 @@ export function EstimateSendMailForm({ children }: EstimateSendMailFormProps) {
{ setSubmitting }: FormikHelpers<EstimateSendMailFormValues>,
) => {
setSubmitting(true);
sendEstimateMail({ id: estimateId, values: { ...values } })
sendEstimateMail([estimateId, values])
.then(() => {
AppToaster.show({
message: 'The invoice mail has been sent to the customer.',

View File

@@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import { Intent } from '@blueprintjs/core';
import { PaymentReceivedSendMailFormSchema } from './_types';
import { AppToaster } from '@/components';
import { useSendSaleInvoiceMail } from '@/hooks/query';
import { useSendPaymentReceiveMail, } from '@/hooks/query';
import { usePaymentReceivedSendMailBoot } from './PaymentReceivedMailBoot';
import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
@@ -27,7 +27,7 @@ interface PaymentReceivedSendMailFormProps {
export function PaymentReceivedSendMailForm({
children,
}: PaymentReceivedSendMailFormProps) {
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail();
const { mutateAsync: sendPaymentMail } = useSendPaymentReceiveMail();
const { paymentReceivedId, paymentReceivedMailState } =
usePaymentReceivedSendMailBoot();
@@ -43,7 +43,7 @@ export function PaymentReceivedSendMailForm({
{ setSubmitting }: FormikHelpers<PaymentReceivedSendMailFormValues>,
) => {
setSubmitting(true);
sendInvoiceMail({ id: paymentReceivedId, values: { ...values } })
sendPaymentMail([paymentReceivedId, values])
.then(() => {
AppToaster.show({
message: 'The invoice mail has been sent to the customer.',

View File

@@ -40,7 +40,7 @@ export function ReceiptSendMailForm({ children }: ReceiptSendMailFormProps) {
{ setSubmitting }: FormikHelpers<ReceiptSendMailFormValues>,
) => {
setSubmitting(true);
sendReceiptMail({ id: receiptId, values: { ...values } })
sendReceiptMail([receiptId, values])
.then(() => {
AppToaster.show({
message: 'The receipt mail has been sent to the customer.',

View File

@@ -244,7 +244,7 @@ export function usePdfPaymentReceive(paymentReceiveId) {
return useRequestPdf({ url: `sales/payment_receives/${paymentReceiveId}` });
}
export function useSendPaymentReceiveMail(props) {
export function useSendPaymentReceiveMail(props?: any) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();

View File

@@ -1 +1,5 @@
export * from './InvoicePaymentEmail';
export * from './EstimatePaymentEmail';
export * from './ReceiptPaymentEmail';
export * from './CreditNoteEmailTemplate';
export * from './PaymentReceivedEmailTemplate';