From cfd4540a65e7927a1dca9e7e225464a9705015ff Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 14 Dec 2023 23:49:23 +0200 Subject: [PATCH 01/17] feat: wip send an invoice mail the customer email --- .../api/controllers/Sales/SalesInvoices.ts | 110 +++++++++++++++++- packages/server/src/interfaces/SaleInvoice.ts | 8 ++ .../Estimates/SaleEstimatesApplication.ts | 14 +++ .../Sales/Estimates/SendSaleEstimateMail.ts | 6 + .../Invoices/GetSaleInvoiceMailReminder.ts | 3 + .../Sales/Invoices/SaleInvoicesApplication.ts | 58 +++++++++ .../Sales/Invoices/SendSaleInvoiceMail.ts | 12 ++ .../Invoices/SendSaleInvoiceMailReminder.ts | 11 ++ 8 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts create mode 100644 packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailReminder.ts create mode 100644 packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts create mode 100644 packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index d90b94d8d..91c370273 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -1,5 +1,5 @@ import { Router, Request, Response, NextFunction } from 'express'; -import { check, param, query } from 'express-validator'; +import { body, check, param, query } from 'express-validator'; import { Service, Inject } from 'typedi'; import BaseController from '../BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; @@ -145,6 +145,39 @@ export default class SaleInvoicesController extends BaseController { this.handleServiceErrors, this.dynamicListService.handlerErrorsToResponse ); + router.get( + '/:id/mail-reminder', + this.specificSaleInvoiceValidation, + this.validationResult, + asyncMiddleware(this.getSaleInvoiceMailReminder.bind(this)), + this.handleServiceErrors + ); + router.post( + '/:id/mail-reminder', + [ + ...this.specificSaleInvoiceValidation, + body('from').isString().exists(), + body('to').isString().exists(), + body('body').isString().exists(), + body('attach_invoice').exists().isBoolean().toBoolean(), + ], + this.validationResult, + asyncMiddleware(this.sendSaleInvoiceMailReminder.bind(this)), + this.handleServiceErrors + ); + router.post( + '/:id/mail', + [ + ...this.specificSaleInvoiceValidation, + body('from').isString().exists(), + body('to').isString().exists(), + body('body').isString().exists(), + body('attach_invoice').exists().isBoolean().toBoolean(), + ], + this.validationResult, + asyncMiddleware(this.sendSaleInvoiceMail.bind(this)), + this.handleServiceErrors + ); return router; } @@ -630,6 +663,81 @@ export default class SaleInvoicesController extends BaseController { } }; + /** + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async sendSaleInvoiceMail( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { id: invoiceId } = req.params; + + try { + await this.saleInvoiceApplication.sendSaleInvoiceMail( + tenantId, + invoiceId + ); + return res.status(200).send({}); + } catch (error) { + next(error); + } + } + + /** + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async getSaleInvoiceMailReminder( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { id: invoiceId } = req.params; + + try { + await this.saleInvoiceApplication.getSaleInvoiceMailReminder( + tenantId, + invoiceId + ); + return res.status(200).send({}); + } catch (error) { + next(error); + } + } + + /** + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async sendSaleInvoiceMailReminder( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { id: invoiceId } = req.params; + + try { + await this.saleInvoiceApplication.sendSaleInvoiceMailReminder( + tenantId, + invoiceId + ); + return res.status(200).send({}); + } catch (error) { + next(error); + } + } + /** * Handles service errors. * @param {Error} error diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 7ef8fdea2..59090cd03 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -186,3 +186,11 @@ export enum SaleInvoiceAction { Writeoff = 'Writeoff', NotifyBySms = 'NotifyBySms', } + +export interface SendInvoiceMailDTO { + to: string; + from: string; + subject: string; + body: string; + attachInvoice?: boolean; +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 3f63b27de..907693ea0 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -17,6 +17,7 @@ import { ApproveSaleEstimate } from './ApproveSaleEstimate'; import { RejectSaleEstimate } from './RejectSaleEstimate'; import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify'; import { SaleEstimatesPdf } from './SaleEstimatesPdf'; +import { SendSaleEstimateMail } from './SendSaleEstimateMail'; @Service() export class SaleEstimatesApplication { @@ -50,6 +51,9 @@ export class SaleEstimatesApplication { @Inject() private saleEstimatesPdfService: SaleEstimatesPdf; + @Inject() + private sendEstimateMailService: SendSaleEstimateMail; + /** * Create a sale estimate. * @param {number} tenantId - The tenant id. @@ -209,4 +213,14 @@ export class SaleEstimatesApplication { saleEstimate ); } + + /** + * + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns + */ + public sendSaleEstimateMail(tenantId: number, saleEstimateId: number) { + return this.sendEstimateMailService.sendMail(tenantId, saleEstimateId); + } } diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts new file mode 100644 index 000000000..8dca133bb --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -0,0 +1,6 @@ +import { Service } from "typedi"; + +@Service() +export class SendSaleEstimateMail { + sendMail(tenantId: number, saleEstimateId: number) {} +} diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailReminder.ts new file mode 100644 index 000000000..2a65d316e --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailReminder.ts @@ -0,0 +1,3 @@ +export class GetSaleInvoiceMailReminder { + public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {} +} diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index 8a37386f9..eb1a475aa 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -11,6 +11,7 @@ import { ISystemUser, ITenantUser, InvoiceNotificationType, + SendInvoiceMailDTO, } from '@/interfaces'; import { Inject, Service } from 'typedi'; import { CreateSaleInvoice } from './CreateSaleInvoice'; @@ -24,6 +25,9 @@ import { WriteoffSaleInvoice } from './WriteoffSaleInvoice'; import { SaleInvoicePdf } from './SaleInvoicePdf'; import { GetInvoicePaymentsService } from './GetInvoicePaymentsService'; import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms'; +import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder'; +import { SendSaleInvoiceMail } from './SendSaleInvoiceMail'; +import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder'; @Service() export class SaleInvoiceApplication { @@ -60,6 +64,15 @@ export class SaleInvoiceApplication { @Inject() private invoiceSms: SaleInvoiceNotifyBySms; + @Inject() + private sendInvoiceReminderService: SendInvoiceMailReminder; + + @Inject() + private sendSaleInvoiceMailService: SendSaleInvoiceMail; + + @Inject() + private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder; + /** * Creates a new sale invoice with associated GL entries. * @param {number} tenantId @@ -279,4 +292,49 @@ export class SaleInvoiceApplication { invoiceSmsDetailsDTO ); }; + + /** + * Retrieves the metadata of invoice mail reminder. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @returns {} + */ + public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { + return this.getSaleInvoiceReminderService.getInvoiceMailReminder( + tenantId, + saleInvoiceId + ); + } + + /** + * Sends reminder of the given invoice to the invoice's customer. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @returns {} + */ + public sendSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { + return this.sendInvoiceReminderService.sendInvoiceMailReminder( + tenantId, + saleInvoiceId + ); + } + + /** + * + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + * @returns + */ + public sendSaleInvoiceMail( + tenantId: number, + saleInvoiceId: number, + messageDTO: SendInvoiceMailDTO + ) { + return this.sendSaleInvoiceMailService.sendSaleInvoiceMail( + tenantId, + saleInvoiceId, + messageDTO + ); + } } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts new file mode 100644 index 000000000..7d0b89b86 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -0,0 +1,12 @@ +import { Service } from 'typedi'; +import { SendInvoiceMailDTO } from '@/interfaces'; + + +@Service() +export class SendSaleInvoiceMail { + public sendSaleInvoiceMail( + tenantId: number, + saleInvoiceId: number, + messageDTO: SendInvoiceMailDTO + ) {} +} diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts new file mode 100644 index 000000000..6dcf49693 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -0,0 +1,11 @@ +import { Service } from 'typedi'; + +@Service() +export class SendInvoiceMailReminder { + /** + * + * @param {number} tenantId + * @param {number} saleInvoiceId + */ + public sendInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {} +} From cd71900bdd48c831efd6bc54da46b8e286d3d06e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 18 Dec 2023 21:28:53 +0200 Subject: [PATCH 02/17] feat: send invoice through mail --- .../api/controllers/Sales/SalesInvoices.ts | 19 ++++-- packages/server/src/interfaces/SaleInvoice.ts | 6 ++ packages/server/src/loaders/jobs.ts | 4 ++ .../Sales/Invoices/SaleInvoicesApplication.ts | 15 +++-- .../Sales/Invoices/SendSaleInvoiceMail.ts | 65 +++++++++++++++++-- .../Sales/Invoices/SendSaleInvoiceMailJob.ts | 33 ++++++++++ .../Invoices/SendSaleInvoiceMailReminder.ts | 59 ++++++++++++++++- .../SendSaleInvoiceMailReminderJob.ts | 32 +++++++++ packages/server/src/subscribers/events.ts | 5 ++ 9 files changed, 218 insertions(+), 20 deletions(-) create mode 100644 packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts create mode 100644 packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 91c370273..69be4f4a8 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -10,6 +10,7 @@ import { ISaleInvoiceCreateDTO, SaleInvoiceAction, AbilitySubject, + SendInvoiceMailDTO, } from '@/interfaces'; import CheckPolicies from '@/api/middleware/CheckPolicies'; import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication'; @@ -169,10 +170,10 @@ export default class SaleInvoicesController extends BaseController { '/:id/mail', [ ...this.specificSaleInvoiceValidation, - body('from').isString().exists(), - body('to').isString().exists(), - body('body').isString().exists(), - body('attach_invoice').exists().isBoolean().toBoolean(), + body('from').isString().optional(), + body('to').isString().optional(), + body('body').isString().optional(), + body('attach_invoice').optional().isBoolean().toBoolean(), ], this.validationResult, asyncMiddleware(this.sendSaleInvoiceMail.bind(this)), @@ -664,7 +665,7 @@ export default class SaleInvoicesController extends BaseController { }; /** - * + * Sends mail invoice of the given sale invoice. * @param {Request} req * @param {Response} res * @param {NextFunction} next @@ -676,11 +677,13 @@ export default class SaleInvoicesController extends BaseController { ) { const { tenantId } = req; const { id: invoiceId } = req.params; + const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req); try { await this.saleInvoiceApplication.sendSaleInvoiceMail( tenantId, - invoiceId + invoiceId, + invoiceMailDTO ); return res.status(200).send({}); } catch (error) { @@ -726,11 +729,13 @@ export default class SaleInvoicesController extends BaseController { ) { const { tenantId } = req; const { id: invoiceId } = req.params; + const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req); try { await this.saleInvoiceApplication.sendSaleInvoiceMailReminder( tenantId, - invoiceId + invoiceId, + invoiceMailDTO ); return res.status(200).send({}); } catch (error) { diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 59090cd03..7d7633b87 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -194,3 +194,9 @@ export interface SendInvoiceMailDTO { body: string; attachInvoice?: boolean; } + +export interface ISaleInvoiceNotifyPayload { + tenantId: number; + saleInvoiceId: number; + messageDTO: SendInvoiceMailDTO; +} diff --git a/packages/server/src/loaders/jobs.ts b/packages/server/src/loaders/jobs.ts index 4fa3aadb1..9beb7a2bc 100644 --- a/packages/server/src/loaders/jobs.ts +++ b/packages/server/src/loaders/jobs.ts @@ -5,6 +5,8 @@ import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries'; import UserInviteMailJob from 'jobs/UserInviteMail'; import OrganizationSetupJob from 'jobs/OrganizationSetup'; import OrganizationUpgrade from 'jobs/OrganizationUpgrade'; +import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailJob'; +import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob'; export default ({ agenda }: { agenda: Agenda }) => { new ResetPasswordMailJob(agenda); @@ -13,6 +15,8 @@ export default ({ agenda }: { agenda: Agenda }) => { new RewriteInvoicesJournalEntries(agenda); new OrganizationSetupJob(agenda); new OrganizationUpgrade(agenda); + new SendSaleInvoiceMailJob(agenda); + new SendSaleInvoiceReminderMailJob(agenda); agenda.start(); }; diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index eb1a475aa..9ce79b656 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -312,15 +312,20 @@ export class SaleInvoiceApplication { * @param {number} saleInvoiceId * @returns {} */ - public sendSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { - return this.sendInvoiceReminderService.sendInvoiceMailReminder( + public sendSaleInvoiceMailReminder( + tenantId: number, + saleInvoiceId: number, + messageDTO: SendInvoiceMailDTO + ) { + return this.sendInvoiceReminderService.triggerMail( tenantId, - saleInvoiceId + saleInvoiceId, + messageDTO ); } /** - * + * Sends the invoice mail of the given sale invoice. * @param {number} tenantId * @param {number} saleInvoiceId * @param {SendInvoiceMailDTO} messageDTO @@ -331,7 +336,7 @@ export class SaleInvoiceApplication { saleInvoiceId: number, messageDTO: SendInvoiceMailDTO ) { - return this.sendSaleInvoiceMailService.sendSaleInvoiceMail( + return this.sendSaleInvoiceMailService.sendMail( tenantId, saleInvoiceId, messageDTO diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index 7d0b89b86..0fa1aeded 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -1,12 +1,67 @@ -import { Service } from 'typedi'; -import { SendInvoiceMailDTO } from '@/interfaces'; - +import { Inject, Service } from 'typedi'; +import { ISaleInvoiceNotifyPayload, SendInvoiceMailDTO } from '@/interfaces'; +import Mail from '@/lib/Mail'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import events from '@/subscribers/events'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; @Service() export class SendSaleInvoiceMail { - public sendSaleInvoiceMail( + @Inject() + private tenancy: HasTenancyService; + + @Inject('agenda') + private agenda: any; + + /** + * Sends the invoice mail of the given sale invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + */ + public async sendMail( tenantId: number, saleInvoiceId: number, messageDTO: SendInvoiceMailDTO - ) {} + ) { + const payload = { + tenantId, + saleInvoiceId, + messageDTO, + }; + await this.agenda.now('sale-invoice-mail-send', payload); + } + + /** + * Triggers the mail invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + * @returns {Promise} + */ + public async triggerMail( + tenantId: number, + saleInvoiceId: number, + messageDTO: SendInvoiceMailDTO + ) { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findById(saleInvoiceId) + .withGraphFetched('customer'); + + const toEmail = messageDTO.to || saleInvoice.customer.email; + const subject = messageDTO.subject || saleInvoice.invoiceNo; + + if (!toEmail) { + return null; + } + const mail = new Mail() + .setSubject(subject) + .setView('mail/UserInvite.html') + .setTo(toEmail) + .setData({}); + + await mail.send(); + } } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts new file mode 100644 index 000000000..2b73757f3 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts @@ -0,0 +1,33 @@ +import Container, { Service } from 'typedi'; +import events from '@/subscribers/events'; +import { SendSaleInvoiceMail } from './SendSaleInvoiceMail'; + +@Service() +export class SendSaleInvoiceMailJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'sale-invoice-mail-send', + { priority: 'high', concurrency: 1 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, saleInvoiceId, messageDTO } = job.attrs.data; + const sendInvoiceMail = Container.get(SendSaleInvoiceMail); + + try { + await sendInvoiceMail.triggerMail(tenantId, saleInvoiceId, messageDTO); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index 6dcf49693..da210cf41 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -1,11 +1,64 @@ -import { Service } from 'typedi'; +import { Inject, Service } from 'typedi'; +import { SendInvoiceMailDTO } from '@/interfaces'; +import Mail from '@/lib/Mail'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; @Service() export class SendInvoiceMailReminder { + @Inject() + private tenancy: HasTenancyService; + + @Inject('agenda') + private agenda: any; + /** - * + * Triggers the reminder mail of the given sale invoice. * @param {number} tenantId * @param {number} saleInvoiceId */ - public sendInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {} + public async triggerMail( + tenantId: number, + saleInvoiceId: number, + messageDTO: SendInvoiceMailDTO + ) { + const payload = { + tenantId, + saleInvoiceId, + messageDTO, + }; + await this.agenda.now('sale-invoice-reminder-mail-send', payload); + } + + /** + * Triggers the mail invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + * @returns {Promise} + */ + public async sendMail( + tenantId: number, + saleInvoiceId: number, + messageDTO: SendInvoiceMailDTO + ) { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findById(saleInvoiceId) + .withGraphFetched('customer'); + + const toEmail = messageDTO.to || saleInvoice.customer.email; + const subject = messageDTO.subject || saleInvoice.invoiceNo; + + if (!toEmail) { + return null; + } + const mail = new Mail() + .setSubject(subject) + .setView('mail/UserInvite.html') + .setTo(toEmail) + .setData({}); + + await mail.send(); + } } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts new file mode 100644 index 000000000..97a622c7d --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts @@ -0,0 +1,32 @@ +import Container, { Service } from 'typedi'; +import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder'; + +@Service() +export class SendSaleInvoiceReminderMailJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'sale-invoice-reminder-mail-send', + { priority: 'high', concurrency: 1 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, saleInvoiceId, messageDTO } = job.attrs.data; + const sendInvoiceMail = Container.get(SendInvoiceMailReminder); + + try { + await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageDTO); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 24ca0a0a3..e54f48152 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -129,6 +129,9 @@ export default { onNotifySms: 'onSaleInvoiceNotifySms', onNotifiedSms: 'onSaleInvoiceNotifiedSms', + + onNotifyMail: 'onSaleInvoiceNotifyMail', + onNotifyReminderMail: 'onSaleInvoiceNotifyReminderMail' }, /** @@ -160,6 +163,8 @@ export default { onRejecting: 'onSaleEstimateRejecting', onRejected: 'onSaleEstimateRejected', + + onNotifyMail: 'onSaleEstimateNotifyMail' }, /** From d2c63878ed5611e8803832127766355df64dc771 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 21 Dec 2023 22:57:17 +0200 Subject: [PATCH 03/17] feat: send invoices and reminder notifications to the customers --- .../api/controllers/Sales/SalesInvoices.ts | 21 +++--- .../services/Sales/Invoices/GetSaleInvoice.ts | 3 +- .../Invoices/SaleInvoiceMailFormatter.ts | 43 ++++++++++++ .../services/Sales/Invoices/SaleInvoicePdf.ts | 25 ++++++- .../Sales/Invoices/SaleInvoicesApplication.ts | 14 ++-- .../Sales/Invoices/SendSaleInvoiceMail.ts | 67 +++++++++++++++--- .../Sales/Invoices/SendSaleInvoiceMailJob.ts | 4 +- .../Invoices/SendSaleInvoiceMailReminder.ts | 68 ++++++++++++++++--- .../SendSaleInvoiceMailReminderJob.ts | 4 +- .../src/services/Sales/Invoices/constants.ts | 23 +++++++ 10 files changed, 227 insertions(+), 45 deletions(-) create mode 100644 packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 69be4f4a8..daff92f37 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -157,10 +157,11 @@ export default class SaleInvoicesController extends BaseController { '/:id/mail-reminder', [ ...this.specificSaleInvoiceValidation, - body('from').isString().exists(), - body('to').isString().exists(), - body('body').isString().exists(), - body('attach_invoice').exists().isBoolean().toBoolean(), + body('subject').isString().optional(), + body('from').isString().optional(), + body('to').isString().optional(), + body('body').isString().optional(), + body('attach_invoice').optional().isBoolean().toBoolean(), ], this.validationResult, asyncMiddleware(this.sendSaleInvoiceMailReminder.bind(this)), @@ -170,6 +171,7 @@ export default class SaleInvoicesController extends BaseController { '/:id/mail', [ ...this.specificSaleInvoiceValidation, + body('subject').isString().optional(), body('from').isString().optional(), body('to').isString().optional(), body('body').isString().optional(), @@ -677,7 +679,9 @@ export default class SaleInvoicesController extends BaseController { ) { const { tenantId } = req; const { id: invoiceId } = req.params; - const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req); + const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req, { + includeOptionals: false, + }); try { await this.saleInvoiceApplication.sendSaleInvoiceMail( @@ -692,7 +696,7 @@ export default class SaleInvoicesController extends BaseController { } /** - * + * Retreivers the sale invoice reminder options. * @param {Request} req * @param {Response} res * @param {NextFunction} next @@ -729,8 +733,9 @@ export default class SaleInvoicesController extends BaseController { ) { const { tenantId } = req; const { id: invoiceId } = req.params; - const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req); - + const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req, { + includeOptionals: false, + }); try { await this.saleInvoiceApplication.sendSaleInvoiceMailReminder( tenantId, diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts index f2245afef..b57f86ed9 100644 --- a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts @@ -24,8 +24,7 @@ export class GetSaleInvoice { */ public async getSaleInvoice( tenantId: number, - saleInvoiceId: number, - authorizedUser: ISystemUser + saleInvoiceId: number ): Promise { const { SaleInvoice } = this.tenancy.models(tenantId); diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts new file mode 100644 index 000000000..a8c85f260 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts @@ -0,0 +1,43 @@ +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { Inject, Service } from 'typedi'; +import { Tenant } from '@/system/models'; + +@Service() +export class SaleInvoiceMailFormatter { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private getSaleInvoiceService: GetSaleInvoice; + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Sale invoice id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public formatText = async ( + tenantId: number, + invoiceId: number, + text: string + ): Promise => { + const invoice = await this.getSaleInvoiceService.getSaleInvoice( + tenantId, + invoiceId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return text + .replace('{CompanyName}', organization.metadata.name) + .replace('{CustomerName}', invoice.customer.displayName) + .replace('{InvoiceNumber}', invoice.invoiceNo) + .replace('{InvoiceDueAmount}', invoice.dueAmountFormatted) + .replace('{InvoiceDueDate}', invoice.dueDateFormatted) + .replace('{InvoiceDate}', invoice.invoiceDateFormatted) + .replace('{InvoiceAmount}', invoice.totalFormatted); + }; +} diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts index 3d17c699c..9cccf94ef 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts @@ -1,7 +1,8 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; -import { ISaleInvoice } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; @Service() export class SaleInvoicePdf { @@ -11,16 +12,34 @@ export class SaleInvoicePdf { @Inject() private templateInjectable: TemplateInjectable; + @Inject() + private validators: CommandSaleInvoiceValidators; + + @Inject() + private tenancy: HasTenancyService; + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - Tenant Id. * @param {ISaleInvoice} saleInvoice - * @returns {Promise} */ - async saleInvoicePdf( + public async saleInvoicePdf( tenantId: number, - saleInvoice: ISaleInvoice + invoiceId: number ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findById(invoiceId) + .withGraphFetched('entries.item') + .withGraphFetched('entries.tax') + .withGraphFetched('customer') + .withGraphFetched('taxes.taxRate'); + + // Validates the given sale invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); + const htmlContent = await this.templateInjectable.render( tenantId, 'modules/invoice-regular', diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index 9ce79b656..38c2c721f 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -249,13 +249,13 @@ export class SaleInvoiceApplication { }; /** - * - * @param {number} tenantId ] - * @param saleInvoice - * @returns + * Retrieves the pdf buffer of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} saleInvoice + * @returns {Promise} */ - public saleInvoicePdf(tenantId: number, saleInvoice) { - return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice); + public saleInvoicePdf(tenantId: number, saleInvoiceId: number) { + return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoiceId); } /** @@ -336,7 +336,7 @@ export class SaleInvoiceApplication { saleInvoiceId: number, messageDTO: SendInvoiceMailDTO ) { - return this.sendSaleInvoiceMailService.sendMail( + return this.sendSaleInvoiceMailService.triggerMail( tenantId, saleInvoiceId, messageDTO diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index 0fa1aeded..4f3792256 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -1,9 +1,17 @@ import { Inject, Service } from 'typedi'; -import { ISaleInvoiceNotifyPayload, SendInvoiceMailDTO } from '@/interfaces'; +import * as R from 'ramda'; +import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; -import events from '@/subscribers/events'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { SaleInvoicePdf } from './SaleInvoicePdf'; +import { SaleInvoiceMailFormatter } from './SaleInvoiceMailFormatter'; +import { + DEFAULT_INVOICE_MAIL_CONTENT, + DEFAULT_INVOICE_MAIL_SUBJECT, + ERRORS, +} from './constants'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; +import { ServiceError } from '@/exceptions'; @Service() export class SendSaleInvoiceMail { @@ -13,13 +21,22 @@ export class SendSaleInvoiceMail { @Inject('agenda') private agenda: any; + @Inject() + private invoicePdf: SaleInvoicePdf; + + @Inject() + private invoiceFormatter: SaleInvoiceMailFormatter; + + @Inject() + private commandInvoiceValidator: CommandSaleInvoiceValidators; + /** * Sends the invoice mail of the given sale invoice. * @param {number} tenantId * @param {number} saleInvoiceId * @param {SendInvoiceMailDTO} messageDTO */ - public async sendMail( + public async triggerMail( tenantId: number, saleInvoiceId: number, messageDTO: SendInvoiceMailDTO @@ -39,7 +56,7 @@ export class SendSaleInvoiceMail { * @param {SendInvoiceMailDTO} messageDTO * @returns {Promise} */ - public async triggerMail( + public async sendMail( tenantId: number, saleInvoiceId: number, messageDTO: SendInvoiceMailDTO @@ -50,17 +67,45 @@ export class SendSaleInvoiceMail { .findById(saleInvoiceId) .withGraphFetched('customer'); - const toEmail = messageDTO.to || saleInvoice.customer.email; - const subject = messageDTO.subject || saleInvoice.invoiceNo; + this.commandInvoiceValidator.validateInvoiceExistance(saleInvoice); - if (!toEmail) { - return null; + // Parsed message opts with default options. + const parsedMessageOpts = { + attachInvoice: true, + subject: DEFAULT_INVOICE_MAIL_SUBJECT, + body: DEFAULT_INVOICE_MAIL_CONTENT, + to: saleInvoice.customer.email, + ...messageDTO, + }; + // In case there is no email address from the customer or from options, throw an error. + if (!parsedMessageOpts.to) { + throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); + } + const formatter = R.curry(this.invoiceFormatter.formatText)( + tenantId, + saleInvoiceId + ); + const toEmail = parsedMessageOpts.to; + const subject = await formatter(parsedMessageOpts.subject); + const body = await formatter(parsedMessageOpts.body); + const attachments = []; + + if (parsedMessageOpts.attachInvoice) { + // Retrieves document buffer of the invoice pdf document. + const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( + tenantId, + saleInvoiceId + ); + attachments.push({ + filename: 'invoice.pdf', + content: invoicePdfBuffer, + }); } const mail = new Mail() .setSubject(subject) - .setView('mail/UserInvite.html') .setTo(toEmail) - .setData({}); + .setContent(body) + .setAttachments(attachments); await mail.send(); } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts index 2b73757f3..3c1e49a6c 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts @@ -10,7 +10,7 @@ export class SendSaleInvoiceMailJob { constructor(agenda) { agenda.define( 'sale-invoice-mail-send', - { priority: 'high', concurrency: 1 }, + { priority: 'high', concurrency: 2 }, this.handler ); } @@ -23,7 +23,7 @@ export class SendSaleInvoiceMailJob { const sendInvoiceMail = Container.get(SendSaleInvoiceMail); try { - await sendInvoiceMail.triggerMail(tenantId, saleInvoiceId, messageDTO); + await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageDTO); done(); } catch (error) { console.log(error); diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index da210cf41..16baf1273 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -1,7 +1,18 @@ import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { assign } from 'lodash'; import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; +import { SaleInvoiceMailFormatter } from './SaleInvoiceMailFormatter'; +import { + DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, + DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, + ERRORS, +} from './constants'; +import { SaleInvoicePdf } from './SaleInvoicePdf'; +import { ServiceError } from '@/exceptions'; @Service() export class SendInvoiceMailReminder { @@ -11,6 +22,15 @@ export class SendInvoiceMailReminder { @Inject('agenda') private agenda: any; + @Inject() + private commandInvoiceValidator: CommandSaleInvoiceValidators; + + @Inject() + private invoiceFormatter: SaleInvoiceMailFormatter; + + @Inject() + private invoicePdf: SaleInvoicePdf; + /** * Triggers the reminder mail of the given sale invoice. * @param {number} tenantId @@ -19,12 +39,12 @@ export class SendInvoiceMailReminder { public async triggerMail( tenantId: number, saleInvoiceId: number, - messageDTO: SendInvoiceMailDTO + messageOptions: SendInvoiceMailDTO ) { const payload = { tenantId, saleInvoiceId, - messageDTO, + messageOptions, }; await this.agenda.now('sale-invoice-reminder-mail-send', payload); } @@ -33,13 +53,13 @@ export class SendInvoiceMailReminder { * Triggers the mail invoice. * @param {number} tenantId * @param {number} saleInvoiceId - * @param {SendInvoiceMailDTO} messageDTO + * @param {SendInvoiceMailDTO} messageOptions * @returns {Promise} */ public async sendMail( tenantId: number, saleInvoiceId: number, - messageDTO: SendInvoiceMailDTO + messageOptions: SendInvoiceMailDTO ) { const { SaleInvoice } = this.tenancy.models(tenantId); @@ -47,17 +67,45 @@ export class SendInvoiceMailReminder { .findById(saleInvoiceId) .withGraphFetched('customer'); - const toEmail = messageDTO.to || saleInvoice.customer.email; - const subject = messageDTO.subject || saleInvoice.invoiceNo; + // Validates the invoice existance. + this.commandInvoiceValidator.validateInvoiceExistance(saleInvoice); - if (!toEmail) { - return null; + const parsedMessageOptions = { + attachInvoice: true, + subject: DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, + body: DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, + to: saleInvoice.customer.email, + ...messageOptions, + }; + // In case there is no email address from the customer or from options, throw an error. + if (!parsedMessageOptions.to) { + throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); + } + const formatter = R.curry(this.invoiceFormatter.formatText)( + tenantId, + saleInvoiceId + ); + const toEmail = parsedMessageOptions.to; + const subject = await formatter(parsedMessageOptions.subject); + const body = await formatter(parsedMessageOptions.body); + const attachments = []; + + if (parsedMessageOptions.attachInvoice) { + // Retrieves document buffer of the invoice pdf document. + const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( + tenantId, + saleInvoiceId + ); + attachments.push({ + filename: 'invoice.pdf', + content: invoicePdfBuffer, + }); } const mail = new Mail() .setSubject(subject) - .setView('mail/UserInvite.html') .setTo(toEmail) - .setData({}); + .setContent(body) + .setAttachments(attachments); await mail.send(); } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts index 97a622c7d..6570a153f 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts @@ -18,11 +18,11 @@ export class SendSaleInvoiceReminderMailJob { * Triggers sending invoice mail. */ private handler = async (job, done: Function) => { - const { tenantId, saleInvoiceId, messageDTO } = job.attrs.data; + const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data; const sendInvoiceMail = Container.get(SendInvoiceMailReminder); try { - await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageDTO); + await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions); done(); } catch (error) { console.log(error); diff --git a/packages/server/src/services/Sales/Invoices/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts index 018dec027..79bf67c0a 100644 --- a/packages/server/src/services/Sales/Invoices/constants.ts +++ b/packages/server/src/services/Sales/Invoices/constants.ts @@ -1,3 +1,25 @@ +export const DEFAULT_INVOICE_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} from {CompanyName}'; +export const DEFAULT_INVOICE_MAIL_CONTENT = ` +

Dear {CustomerName}

+

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

+

+Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}
+

+`; + +export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} reminder from {CompanyName}'; +export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = ` +

Dear {CustomerName}

+

You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.

+

Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}

+`; + export const ERRORS = { INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE', SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND', @@ -16,6 +38,7 @@ export const ERRORS = { PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF', SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF', + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR', }; export const DEFAULT_VIEW_COLUMNS = []; From 50d5ddba8ef4f2cfe1563ad89a1cbb9928fd7b12 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 22 Dec 2023 02:45:44 +0200 Subject: [PATCH 04/17] feat: send invoice notifications --- .../Invoices/SaleInvoiceMailFormatter.ts | 43 ------- .../Sales/Invoices/SendSaleInvoiceMail.ts | 107 ++++++++++++------ .../Invoices/SendSaleInvoiceMailReminder.ts | 99 ++++++++++------ 3 files changed, 138 insertions(+), 111 deletions(-) delete mode 100644 packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts deleted file mode 100644 index a8c85f260..000000000 --- a/packages/server/src/services/Sales/Invoices/SaleInvoiceMailFormatter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import HasTenancyService from '@/services/Tenancy/TenancyService'; -import { GetSaleInvoice } from './GetSaleInvoice'; -import { Inject, Service } from 'typedi'; -import { Tenant } from '@/system/models'; - -@Service() -export class SaleInvoiceMailFormatter { - @Inject() - private tenancy: HasTenancyService; - - @Inject() - private getSaleInvoiceService: GetSaleInvoice; - - /** - * Retrieves the formatted text of the given sale invoice. - * @param {number} tenantId - Tenant id. - * @param {number} invoiceId - Sale invoice id. - * @param {string} text - The given text. - * @returns {Promise} - */ - public formatText = async ( - tenantId: number, - invoiceId: number, - text: string - ): Promise => { - const invoice = await this.getSaleInvoiceService.getSaleInvoice( - tenantId, - invoiceId - ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - - return text - .replace('{CompanyName}', organization.metadata.name) - .replace('{CustomerName}', invoice.customer.displayName) - .replace('{InvoiceNumber}', invoice.invoiceNo) - .replace('{InvoiceDueAmount}', invoice.dueAmountFormatted) - .replace('{InvoiceDueDate}', invoice.dueDateFormatted) - .replace('{InvoiceDate}', invoice.invoiceDateFormatted) - .replace('{InvoiceAmount}', invoice.totalFormatted); - }; -} diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index 4f3792256..cc8269c56 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -4,31 +4,29 @@ import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { SaleInvoicePdf } from './SaleInvoicePdf'; -import { SaleInvoiceMailFormatter } from './SaleInvoiceMailFormatter'; import { DEFAULT_INVOICE_MAIL_CONTENT, DEFAULT_INVOICE_MAIL_SUBJECT, ERRORS, } from './constants'; -import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; import { ServiceError } from '@/exceptions'; +import { formatSmsMessage } from '@/utils'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { Tenant } from '@/system/models'; @Service() export class SendSaleInvoiceMail { @Inject() private tenancy: HasTenancyService; - @Inject('agenda') - private agenda: any; - + @Inject() + private getSaleInvoiceService: GetSaleInvoice; + @Inject() private invoicePdf: SaleInvoicePdf; - @Inject() - private invoiceFormatter: SaleInvoiceMailFormatter; - - @Inject() - private commandInvoiceValidator: CommandSaleInvoiceValidators; + @Inject('agenda') + private agenda: any; /** * Sends the invoice mail of the given sale invoice. @@ -49,6 +47,58 @@ export class SendSaleInvoiceMail { await this.agenda.now('sale-invoice-mail-send', payload); } + /** + * Retrieves the default invoice mail options. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + const { SaleInvoice } = this.tenancy.models(tenantId); + const saleInvoice = await SaleInvoice.query() + .findById(invoiceId) + .withGraphFetched('customer') + .throwIfNotFound(); + + return { + attachInvoice: true, + subject: DEFAULT_INVOICE_MAIL_SUBJECT, + body: DEFAULT_INVOICE_MAIL_CONTENT, + to: saleInvoice.customer.email, + }; + }; + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Sale invoice id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public textFormatter = async ( + tenantId: number, + invoiceId: number, + text: string + ): Promise => { + const invoice = await this.getSaleInvoiceService.getSaleInvoice( + tenantId, + invoiceId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return formatSmsMessage(text, { + CompanyName: organization.metadata.name, + CustomerName: invoice.customer.displayName, + InvoiceNumber: invoice.invoiceNo, + InvoiceDueAmount: invoice.dueAmountFormatted, + InvoiceDueDate: invoice.dueDateFormatted, + InvoiceDate: invoice.invoiceDateFormatted, + InvoiceAmount: invoice.totalFormatted, + }); + }; + /** * Triggers the mail invoice. * @param {number} tenantId @@ -61,31 +111,20 @@ export class SendSaleInvoiceMail { saleInvoiceId: number, messageDTO: SendInvoiceMailDTO ) { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const saleInvoice = await SaleInvoice.query() - .findById(saleInvoiceId) - .withGraphFetched('customer'); - - this.commandInvoiceValidator.validateInvoiceExistance(saleInvoice); - + const defaultMessageOpts = await this.getDefaultMailOpts( + tenantId, + saleInvoiceId + ); // Parsed message opts with default options. const parsedMessageOpts = { - attachInvoice: true, - subject: DEFAULT_INVOICE_MAIL_SUBJECT, - body: DEFAULT_INVOICE_MAIL_CONTENT, - to: saleInvoice.customer.email, + ...defaultMessageOpts, ...messageDTO, }; // In case there is no email address from the customer or from options, throw an error. if (!parsedMessageOpts.to) { throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); } - const formatter = R.curry(this.invoiceFormatter.formatText)( - tenantId, - saleInvoiceId - ); - const toEmail = parsedMessageOpts.to; + const formatter = R.curry(this.textFormatter)(tenantId, saleInvoiceId); const subject = await formatter(parsedMessageOpts.subject); const body = await formatter(parsedMessageOpts.body); const attachments = []; @@ -96,17 +135,13 @@ export class SendSaleInvoiceMail { tenantId, saleInvoiceId ); - attachments.push({ - filename: 'invoice.pdf', - content: invoicePdfBuffer, - }); + attachments.push({ filename: 'invoice.pdf', content: invoicePdfBuffer }); } - const mail = new Mail() + await new Mail() .setSubject(subject) - .setTo(toEmail) + .setTo(parsedMessageOpts.to) .setContent(body) - .setAttachments(attachments); - - await mail.send(); + .setAttachments(attachments) + .send(); } } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index 16baf1273..886916260 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -1,11 +1,8 @@ import { Inject, Service } from 'typedi'; import * as R from 'ramda'; -import { assign } from 'lodash'; import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; -import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; -import { SaleInvoiceMailFormatter } from './SaleInvoiceMailFormatter'; import { DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, @@ -13,6 +10,9 @@ import { } from './constants'; import { SaleInvoicePdf } from './SaleInvoicePdf'; import { ServiceError } from '@/exceptions'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { Tenant } from '@/system/models'; +import { formatSmsMessage } from '@/utils'; @Service() export class SendInvoiceMailReminder { @@ -22,15 +22,12 @@ export class SendInvoiceMailReminder { @Inject('agenda') private agenda: any; - @Inject() - private commandInvoiceValidator: CommandSaleInvoiceValidators; - - @Inject() - private invoiceFormatter: SaleInvoiceMailFormatter; - @Inject() private invoicePdf: SaleInvoicePdf; + @Inject() + private getSaleInvoiceService: GetSaleInvoice; + /** * Triggers the reminder mail of the given sale invoice. * @param {number} tenantId @@ -49,6 +46,59 @@ export class SendInvoiceMailReminder { await this.agenda.now('sale-invoice-reminder-mail-send', payload); } + /** + * Parses the default message options. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public async getDefaultMailOpts(tenantId: number, invoiceId: number) { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findById(invoiceId) + .withGraphFetched('customer') + .throwIfNotFound(); + + return { + attachInvoice: true, + subject: DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, + body: DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, + to: saleInvoice.customer.email, + }; + } + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Sale invoice id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public formatText = async ( + tenantId: number, + invoiceId: number, + text: string + ): Promise => { + const invoice = await this.getSaleInvoiceService.getSaleInvoice( + tenantId, + invoiceId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return formatSmsMessage(text, { + CompanyName: organization.metadata.name, + CustomerName: invoice.customer.displayName, + InvoiceNumber: invoice.invoiceNo, + InvoiceDueAmount: invoice.dueAmountFormatted, + InvoiceDueDate: invoice.dueDateFormatted, + InvoiceDate: invoice.invoiceDateFormatted, + InvoiceAmount: invoice.totalFormatted, + }); + }; + /** * Triggers the mail invoice. * @param {number} tenantId @@ -61,31 +111,19 @@ export class SendInvoiceMailReminder { saleInvoiceId: number, messageOptions: SendInvoiceMailDTO ) { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const saleInvoice = await SaleInvoice.query() - .findById(saleInvoiceId) - .withGraphFetched('customer'); - - // Validates the invoice existance. - this.commandInvoiceValidator.validateInvoiceExistance(saleInvoice); - + const defaultMessageOpts = await this.getDefaultMailOpts( + tenantId, + saleInvoiceId + ); const parsedMessageOptions = { - attachInvoice: true, - subject: DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, - body: DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, - to: saleInvoice.customer.email, + ...defaultMessageOpts, ...messageOptions, }; // In case there is no email address from the customer or from options, throw an error. if (!parsedMessageOptions.to) { throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); } - const formatter = R.curry(this.invoiceFormatter.formatText)( - tenantId, - saleInvoiceId - ); - const toEmail = parsedMessageOptions.to; + const formatter = R.curry(this.formatText)(tenantId, saleInvoiceId); const subject = await formatter(parsedMessageOptions.subject); const body = await formatter(parsedMessageOptions.body); const attachments = []; @@ -96,14 +134,11 @@ export class SendInvoiceMailReminder { tenantId, saleInvoiceId ); - attachments.push({ - filename: 'invoice.pdf', - content: invoicePdfBuffer, - }); + attachments.push({ filename: 'invoice.pdf', content: invoicePdfBuffer }); } const mail = new Mail() .setSubject(subject) - .setTo(toEmail) + .setTo(parsedMessageOptions.to) .setContent(body) .setAttachments(attachments); From f0e15d43d300df7a4849e439d35fd9b0aead65f3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 22 Dec 2023 23:56:37 +0200 Subject: [PATCH 05/17] feat: send sale estimate mail notification --- .../Estimates/SaleEstimatesApplication.ts | 41 ++++-- .../Sales/Estimates/SaleEstimatesPdf.ts | 13 +- .../Sales/Estimates/SendSaleEstimateMail.ts | 137 +++++++++++++++++- .../Estimates/SendSaleEstimateMailJob.ts | 36 +++++ .../src/services/Sales/Estimates/constants.ts | 11 +- 5 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 907693ea0..6a208746e 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -7,6 +7,7 @@ import { ISaleEstimate, ISaleEstimateDTO, ISalesEstimatesFilter, + SaleEstimateMailOptions, } from '@/interfaces'; import { EditSaleEstimate } from './EditSaleEstimate'; import { DeleteSaleEstimate } from './DeleteSaleEstimate'; @@ -202,25 +203,33 @@ export class SaleEstimatesApplication { }; /** - * - * @param {number} tenantId - * @param {} saleEstimate - * @returns - */ - public getSaleEstimatePdf(tenantId: number, saleEstimate) { - return this.saleEstimatesPdfService.getSaleEstimatePdf( - tenantId, - saleEstimate - ); - } - - /** - * + * Retrieve the PDF content of the given sale estimate. * @param {number} tenantId * @param {number} saleEstimateId * @returns */ - public sendSaleEstimateMail(tenantId: number, saleEstimateId: number) { - return this.sendEstimateMailService.sendMail(tenantId, saleEstimateId); + public getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { + return this.saleEstimatesPdfService.getSaleEstimatePdf( + tenantId, + saleEstimateId + ); + } + + /** + * Send the reminder mail of the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public sendSaleEstimateMail( + tenantId: number, + saleEstimateId: number, + saleEstimateMailOpts: SaleEstimateMailOptions + ) { + return this.sendEstimateMailService.triggerMail( + tenantId, + saleEstimateId, + saleEstimateMailOpts + ); } } diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts index db19743f7..af1d2098c 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; +import { GetSaleEstimate } from './GetSaleEstimate'; @Service() export class SaleEstimatesPdf { @@ -10,11 +11,19 @@ export class SaleEstimatesPdf { @Inject() private templateInjectable: TemplateInjectable; + @Inject() + private getSaleEstimate: GetSaleEstimate; + /** * Retrieve sale invoice pdf content. - * @param {} saleInvoice - + * @param {number} tenantId - + * @param {ISaleInvoice} saleInvoice - */ - async getSaleEstimatePdf(tenantId: number, saleEstimate) { + public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { + const saleEstimate = await this.getSaleEstimate.getEstimate( + tenantId, + saleEstimateId + ); const htmlContent = await this.templateInjectable.render( tenantId, 'modules/estimate-regular', diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts index 8dca133bb..bc558d424 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -1,6 +1,139 @@ -import { Service } from "typedi"; +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import Mail from '@/lib/Mail'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { + DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, +} from './constants'; +import { SaleEstimatesPdf } from './SaleEstimatesPdf'; +import { GetSaleEstimate } from './GetSaleEstimate'; +import { formatSmsMessage } from '@/utils'; +import { SaleEstimateMailOptions } from '@/interfaces'; @Service() export class SendSaleEstimateMail { - sendMail(tenantId: number, saleEstimateId: number) {} + @Inject() + private tenancy: HasTenancyService; + + @Inject('agenda') + private agenda: any; + + @Inject() + private estimatePdf: SaleEstimatesPdf; + + @Inject() + private getSaleEstimateService: GetSaleEstimate; + + /** + * Triggers the reminder mail of the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + * @param messageOptions + */ + public async triggerMail( + tenantId: number, + saleEstimateId: number, + messageOptions: any + ) { + const payload = { + tenantId, + saleEstimateId, + messageOptions, + }; + await this.agenda.now('sale-estimate-mail-send', payload); + } + + /** + * Formates the text of the mail. + * @param {number} tenantId + * @param {number} estimateId + * @param {string} text + */ + public formatText = async ( + tenantId: number, + estimateId: number, + text: string + ) => { + const estimate = await this.getSaleEstimateService.getEstimate( + tenantId, + estimateId + ); + return formatSmsMessage(text, { + CustomerName: estimate.customer.displayName, + EstimateNumber: estimate.estimateNo, + EstimateDate: estimate.estimateDateFormatted, + EstimateAmount: estimate.totalFormatted, + EstimateDueDate: estimate.dueDateFormatted, + EstimateDueAmount: estimate.dueAmountFormatted, + }); + }; + + /** + * Retrieves the default mail options. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public getDefaultMailOpts = async ( + tenantId: number, + saleEstimateId: number + ) => { + const { SaleEstimate } = this.tenancy.models(tenantId); + + const saleEstimate = await SaleEstimate.query() + .findById(saleEstimateId) + .withGraphFetched('customer') + .throwIfNotFound(); + + return { + attachPdf: true, + subject: DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, + body: DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + to: saleEstimate.customer.email, + }; + }; + + /** + * Sends the mail. + * @param {number} tenantId + * @param {number} saleEstimateId + * @param {SaleEstimateMailOptions} messageOptions + */ + public async sendMail( + tenantId: number, + saleEstimateId: number, + messageOptions: SaleEstimateMailOptions + ) { + const defaultMessageOpts = await this.getDefaultMailOpts( + tenantId, + saleEstimateId + ); + const parsedMessageOpts = { + ...defaultMessageOpts, + ...messageOptions, + }; + const formatter = R.curry(this.formatText)(tenantId, saleEstimateId); + const toEmail = parsedMessageOpts.to; + const subject = await formatter(parsedMessageOpts.subject); + const body = await formatter(parsedMessageOpts.body); + const attachments = []; + + if (parsedMessageOpts.to) { + const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf( + tenantId, + saleEstimateId + ); + attachments.push({ + filename: 'estimate.pdf', + content: estimatePdfBuffer, + }); + } + await new Mail() + .setSubject(subject) + .setTo(toEmail) + .setContent(body) + .setAttachments(attachments) + .send(); + } } diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts new file mode 100644 index 000000000..b5e8eda39 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts @@ -0,0 +1,36 @@ +import Container, { Service } from 'typedi'; +import { SendSaleEstimateMail } from './SendSaleEstimateMail'; + +@Service() +export class SendSaleEstimateMailJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'sale-estimate-mail-send', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, saleEstimateId, messageOptions } = job.attrs.data; + const sendSaleEstimateMail = Container.get(SendSaleEstimateMail); + + try { + await sendSaleEstimateMail.sendMail( + tenantId, + saleEstimateId, + messageOptions + ); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Sales/Estimates/constants.ts b/packages/server/src/services/Sales/Estimates/constants.ts index 2b58c74a8..b59350430 100644 --- a/packages/server/src/services/Sales/Estimates/constants.ts +++ b/packages/server/src/services/Sales/Estimates/constants.ts @@ -1,3 +1,12 @@ +export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} reminder from {CompanyName}'; +export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = ` +

Dear {CustomerName}

+

You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.

+

Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}

+`; export const ERRORS = { SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND', @@ -8,7 +17,7 @@ export const ERRORS = { CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES', SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED', SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED', - SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED' + SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED', }; export const DEFAULT_VIEW_COLUMNS = []; From b6d99b1d4b47c36ebd30a726478d727ecb18cfd3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 24 Dec 2023 21:49:59 +0200 Subject: [PATCH 06/17] feat: send mail notifications of sale receipts --- .../Sales/Receipts/SaleReceiptApplication.ts | 22 +++ .../Receipts/SaleReceiptMailNotification.ts | 147 ++++++++++++++++++ .../SaleReceiptMailNotificationJob.ts | 36 +++++ .../Sales/Receipts/SaleReceiptsPdfService.ts | 16 +- .../src/services/Sales/Receipts/constants.ts | 13 ++ 5 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptMailNotificationJob.ts diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index 8dfdd4c75..9fdfb548a 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -5,6 +5,7 @@ import { IPaginationMeta, ISaleReceipt, ISalesReceiptsFilter, + SaleReceiptMailOpts, } from '@/interfaces'; import { EditSaleReceipt } from './EditSaleReceipt'; import { GetSaleReceipt } from './GetSaleReceipt'; @@ -13,6 +14,7 @@ import { GetSaleReceipts } from './GetSaleReceipts'; import { CloseSaleReceipt } from './CloseSaleReceipt'; import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms'; +import { SaleReceiptMailNotification } from './SaleReceiptMailNotification'; @Service() export class SaleReceiptApplication { @@ -40,6 +42,9 @@ export class SaleReceiptApplication { @Inject() private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms; + @Inject() + private saleReceiptNotifyByMailService: SaleReceiptMailNotification; + /** * Creates a new sale receipt with associated entries. * @param {number} tenantId @@ -166,4 +171,21 @@ export class SaleReceiptApplication { saleReceiptId ); } + + /** + * Sends the receipt mail of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + */ + public sendSaleReceiptMail( + tenantId: number, + saleReceiptId: number, + messageOpts: SaleReceiptMailOpts + ) { + return this.saleReceiptNotifyByMailService.triggerMail( + tenantId, + saleReceiptId, + messageOpts + ); + } } diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts new file mode 100644 index 000000000..bf6c4c893 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts @@ -0,0 +1,147 @@ +import * as R from 'ramda'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { Tenant } from '@/system/models'; +import { formatSmsMessage } from '@/utils'; +import { ServiceError } from '@/exceptions'; +import Mail from '@/lib/Mail'; +import { GetSaleReceipt } from './GetSaleReceipt'; +import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; +import { + DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, +} from '../Estimates/constants'; +import { ERRORS } from './constants'; +import { SaleReceiptMailOpts } from '@/interfaces'; + +@Service() +export class SaleReceiptMailNotification { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private getSaleReceiptService: GetSaleReceipt; + + @Inject() + private receiptPdfService: SaleReceiptsPdf; + + @Inject('agenda') + private agenda: any; + + /** + * Sends the receipt mail of the given sale receipt. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + */ + public async triggerMail( + tenantId: number, + saleReceiptId: number, + messageOpts: SaleReceiptMailOpts + ) { + const payload = { + tenantId, + saleReceiptId, + messageOpts, + }; + await this.agenda.now('sale-receipt-mail-send', payload); + } + + /** + * Retrieves the default receipt mail options. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + const { SaleReceipt } = this.tenancy.models(tenantId); + const saleReceipt = await SaleReceipt.query() + .findById(invoiceId) + .withGraphFetched('customer') + .throwIfNotFound(); + + return { + attachInvoice: true, + subject: DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, + body: DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + to: saleReceipt.customer.email, + }; + }; + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} receiptId - Sale receipt id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public textFormatter = async ( + tenantId: number, + receiptId: number, + text: string + ): Promise => { + const invoice = await this.getSaleReceiptService.getSaleReceipt( + tenantId, + receiptId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return formatSmsMessage(text, { + CompanyName: organization.metadata.name, + CustomerName: invoice.customer.displayName, + InvoiceNumber: invoice.invoiceNo, + InvoiceDueAmount: invoice.dueAmountFormatted, + InvoiceDueDate: invoice.dueDateFormatted, + InvoiceDate: invoice.invoiceDateFormatted, + InvoiceAmount: invoice.totalFormatted, + }); + }; + + /** + * Triggers the mail invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + * @returns {Promise} + */ + public async sendMail( + tenantId: number, + saleReceiptId: number, + messageOpts: SaleReceiptMailOpts + ) { + const defaultMessageOpts = await this.getDefaultMailOpts( + tenantId, + saleReceiptId + ); + // Parsed message opts with default options. + const parsedMessageOpts = { + ...defaultMessageOpts, + ...messageOpts, + }; + // In case there is no email address from the customer or from options, throw an error. + if (!parsedMessageOpts.to) { + throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); + } + const formatter = R.curry(this.textFormatter)(tenantId, saleReceiptId); + const body = await formatter(parsedMessageOpts.body); + const subject = await formatter(parsedMessageOpts.subject); + const attachments = []; + + if (parsedMessageOpts.attachInvoice) { + // Retrieves document buffer of the invoice pdf document. + const receiptPdfBuffer = await this.receiptPdfService.saleReceiptPdf( + tenantId, + saleReceiptId + ); + attachments.push({ filename: 'invoice.pdf', content: receiptPdfBuffer }); + } + await new Mail() + .setSubject(subject) + .setTo(parsedMessageOpts.to) + .setContent(body) + .setAttachments(attachments) + .send(); + } +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotificationJob.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotificationJob.ts new file mode 100644 index 000000000..f32325114 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotificationJob.ts @@ -0,0 +1,36 @@ +import Container, { Service } from 'typedi'; +import { SaleReceiptMailNotification } from './SaleReceiptMailNotification'; + +@Service() +export class SaleReceiptMailNotificationJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'sale-receipt-mail-send', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, saleReceiptId, messageOpts } = job.attrs.data; + const receiveMailNotification = Container.get(SaleReceiptMailNotification); + + try { + await receiveMailNotification.sendMail( + tenantId, + saleReceiptId, + messageOpts + ); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts index c06263212..cad2b5f93 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; +import { GetSaleReceipt } from './GetSaleReceipt'; @Service() export class SaleReceiptsPdf { @@ -10,11 +11,20 @@ export class SaleReceiptsPdf { @Inject() private templateInjectable: TemplateInjectable; + @Inject() + private getSaleReceiptService: GetSaleReceipt; + /** - * Retrieve sale invoice pdf content. - * @param {} saleInvoice - + * Retrieves sale invoice pdf content. + * @param {number} tenantId - + * @param {number} saleInvoiceId - + * @returns {Promise} */ - public async saleReceiptPdf(tenantId: number, saleReceipt) { + public async saleReceiptPdf(tenantId: number, saleReceiptId: number) { + const saleReceipt = await this.getSaleReceiptService.getSaleReceipt( + tenantId, + saleReceiptId + ); const htmlContent = await this.templateInjectable.render( tenantId, 'modules/receipt-regular', diff --git a/packages/server/src/services/Sales/Receipts/constants.ts b/packages/server/src/services/Sales/Receipts/constants.ts index bf0cdef18..977a5d9b1 100644 --- a/packages/server/src/services/Sales/Receipts/constants.ts +++ b/packages/server/src/services/Sales/Receipts/constants.ts @@ -1,3 +1,15 @@ +export const DEFAULT_RECEIPT_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} from {CompanyName}'; +export const DEFAULT_RECEIPT_MAIL_CONTENT = ` +

Dear {CustomerName}

+

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

+

+Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}
+

+`; + export const ERRORS = { SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND', DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', @@ -6,6 +18,7 @@ export const ERRORS = { SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED', SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED', CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES', + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR' }; export const DEFAULT_VIEW_COLUMNS = []; From 13c6e7a62d13ff715c5866df365c06d9a29fbb95 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 24 Dec 2023 21:51:23 +0200 Subject: [PATCH 07/17] feat: send mail notifications of payment --- .../PaymentReceives/GetPaymentReeceivePdf.ts | 11 +- .../PaymentReceiveMailNotification.ts | 131 ++++++++++++++++++ .../PaymentReceiveMailNotificationJob.ts | 35 +++++ .../PaymentReceivesApplication.ts | 31 ++++- .../Sales/PaymentReceives/constants.ts | 13 ++ 5 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts diff --git a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts index e05937f76..e3d3cfb26 100644 --- a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts +++ b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts @@ -1,7 +1,7 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; -import { IPaymentReceive } from '@/interfaces'; +import { GetPaymentReceive } from './GetPaymentReceive'; @Service() export default class GetPaymentReceivePdf { @@ -11,6 +11,9 @@ export default class GetPaymentReceivePdf { @Inject() private templateInjectable: TemplateInjectable; + @Inject() + private getPaymentService: GetPaymentReceive; + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - @@ -19,8 +22,12 @@ export default class GetPaymentReceivePdf { */ async getPaymentReceivePdf( tenantId: number, - paymentReceive: IPaymentReceive + paymentReceiveId: number ): Promise { + const paymentReceive = await this.getPaymentService.getPaymentReceive( + tenantId, + paymentReceiveId + ); const htmlContent = await this.templateInjectable.render( tenantId, 'modules/payment-receive-standard', diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts new file mode 100644 index 000000000..3b92c2ece --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts @@ -0,0 +1,131 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { IPaymentReceiveMailOpts, SendInvoiceMailDTO } from '@/interfaces'; +import Mail from '@/lib/Mail'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { + DEFAULT_PAYMENT_MAIL_CONTENT, + DEFAULT_PAYMENT_MAIL_SUBJECT, + ERRORS, +} from './constants'; +import { ServiceError } from '@/exceptions'; +import { formatSmsMessage } from '@/utils'; +import { Tenant } from '@/system/models'; +import { GetPaymentReceive } from './GetPaymentReceive'; + +@Service() +export class SendPaymentReceiveMailNotification { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private getPaymentService: GetPaymentReceive; + + @Inject('agenda') + private agenda: any; + + /** + * Sends the mail of the given payment receive. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {SendInvoiceMailDTO} messageDTO + */ + public async triggerMail( + tenantId: number, + paymentReceiveId: number, + messageDTO: IPaymentReceiveMailOpts + ) { + const payload = { + tenantId, + paymentReceiveId, + messageDTO, + }; + await this.agenda.now('payment-receive-mail-send', payload); + } + + /** + * Retrieves the default payment mail options. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + const { PaymentReceive } = this.tenancy.models(tenantId); + const paymentReceive = await PaymentReceive.query() + .findById(invoiceId) + .withGraphFetched('customer') + .throwIfNotFound(); + + return { + attachInvoice: true, + subject: DEFAULT_PAYMENT_MAIL_SUBJECT, + body: DEFAULT_PAYMENT_MAIL_CONTENT, + to: paymentReceive.customer.email, + }; + }; + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Sale invoice id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public textFormatter = async ( + tenantId: number, + invoiceId: number, + text: string + ): Promise => { + const payment = await this.getPaymentService.getPaymentReceive( + tenantId, + invoiceId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return formatSmsMessage(text, { + CompanyName: organization.metadata.name, + CustomerName: payment.customer.displayName, + PaymentNumber: payment.invoiceNo, + PaymentDate: payment.dueAmountFormatted, + PaymentAmount: payment.dueAmountFormatted, + }); + }; + + /** + * Triggers the mail invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + * @returns {Promise} + */ + public async sendMail( + tenantId: number, + paymentReceiveId: number, + messageDTO: SendInvoiceMailDTO + ): Promise { + const defaultMessageOpts = await this.getDefaultMailOpts( + tenantId, + paymentReceiveId + ); + // Parsed message opts with default options. + const parsedMessageOpts = { + ...defaultMessageOpts, + ...messageDTO, + }; + // In case there is no email address from the customer or from options, throw an error. + if (!parsedMessageOpts.to) { + throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); + } + const formatter = R.curry(this.textFormatter)(tenantId, paymentReceiveId); + const subject = await formatter(parsedMessageOpts.subject); + const body = await formatter(parsedMessageOpts.body); + + await new Mail() + .setSubject(subject) + .setTo(parsedMessageOpts.to) + .setContent(body) + .send(); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts new file mode 100644 index 000000000..236a33758 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts @@ -0,0 +1,35 @@ +import Container, { Service } from 'typedi'; +import events from '@/subscribers/events'; +import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification'; + +@Service() +export class PaymentReceiveMailNotificationJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'payment-receive-mail-send', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending payment notification via mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data; + const paymentMail = Container.get(SendPaymentReceiveMailNotification); + + console.log(tenantId, paymentReceiveId, messageDTO); + + try { + await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts index afeca6010..bbf88b1ca 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts @@ -4,6 +4,7 @@ import { IPaymentReceive, IPaymentReceiveCreateDTO, IPaymentReceiveEditDTO, + IPaymentReceiveMailOpts, IPaymentReceiveSmsDetails, IPaymentReceivesFilter, ISystemUser, @@ -17,7 +18,7 @@ import { GetPaymentReceive } from './GetPaymentReceive'; import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices'; import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify'; import GetPaymentReceivePdf from './GetPaymentReeceivePdf'; -import { PaymentReceive } from '@/models'; +import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification'; @Service() export class PaymentReceivesApplication { @@ -42,6 +43,9 @@ export class PaymentReceivesApplication { @Inject() private paymentSmsNotify: PaymentReceiveNotifyBySms; + @Inject() + private paymentMailNotify: SendPaymentReceiveMailNotification; + @Inject() private getPaymentReceivePdfService: GetPaymentReceivePdf; @@ -176,18 +180,37 @@ export class PaymentReceivesApplication { }; /** - * Retrieve PDF content of the given payment receive. + * Notify customer via mail about payment receive details. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {IPaymentReceiveMailOpts} messageOpts + * @returns {Promise} + */ + public notifyPaymentByMail( + tenantId: number, + paymentReceiveId: number, + messageOpts: IPaymentReceiveMailOpts + ) { + return this.paymentMailNotify.triggerMail( + tenantId, + paymentReceiveId, + messageOpts + ); + } + + /** + * Retrieve pdf content of the given payment receive. * @param {number} tenantId * @param {PaymentReceive} paymentReceive * @returns */ public getPaymentReceivePdf = ( tenantId: number, - paymentReceive: PaymentReceive + paymentReceiveId: number ) => { return this.getPaymentReceivePdfService.getPaymentReceivePdf( tenantId, - paymentReceive + paymentReceiveId ); }; } diff --git a/packages/server/src/services/Sales/PaymentReceives/constants.ts b/packages/server/src/services/Sales/PaymentReceives/constants.ts index ccd8d75ee..e11060670 100644 --- a/packages/server/src/services/Sales/PaymentReceives/constants.ts +++ b/packages/server/src/services/Sales/PaymentReceives/constants.ts @@ -1,3 +1,15 @@ +export const DEFAULT_PAYMENT_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} from {CompanyName}'; +export const DEFAULT_PAYMENT_MAIL_CONTENT = ` +

Dear {CustomerName}

+

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

+

+Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}
+

+`; + export const ERRORS = { PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS', @@ -12,6 +24,7 @@ export const ERRORS = { PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES', PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR' }; export const DEFAULT_VIEWS = []; From 6356cb5e636e39c085ec8d533b8c1554b041f34c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 24 Dec 2023 21:53:37 +0200 Subject: [PATCH 08/17] feat: send mail notifications of sale transactions --- .../api/controllers/Sales/PaymentReceives.ts | 68 +++++++++++++++--- .../api/controllers/Sales/SalesEstimates.ts | 63 ++++++++++++++--- .../api/controllers/Sales/SalesReceipts.ts | 69 +++++++++++++++---- .../server/src/interfaces/PaymentReceive.ts | 4 ++ .../server/src/interfaces/SaleEstimate.ts | 8 +++ packages/server/src/interfaces/SaleReceipt.ts | 4 ++ packages/server/src/loaders/jobs.ts | 6 ++ .../Sales/Estimates/SendSaleEstimateMail.ts | 7 +- .../Sales/Invoices/SaleInvoicesApplication.ts | 2 +- 9 files changed, 195 insertions(+), 36 deletions(-) diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 7cfa93a00..8604682ba 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -1,9 +1,11 @@ import { Inject, Service } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; -import { check, param, query, ValidationChain } from 'express-validator'; +import { body, check, param, query, ValidationChain } from 'express-validator'; import { AbilitySubject, IPaymentReceiveDTO, + IPaymentReceiveMailOpts, + // IPaymentReceiveMailOpts, PaymentReceiveAction, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; @@ -117,6 +119,19 @@ export default class PaymentReceivesController extends BaseController { asyncMiddleware(this.deletePaymentReceive.bind(this)), this.handleServiceErrors ); + router.post( + '/:id/mail', + [ + ...this.paymentReceiveValidation, + body('subject').isString().optional(), + body('from').isString().optional(), + body('to').isString().optional(), + body('body').isString().optional(), + body('attach_invoice').optional().isBoolean().toBoolean(), + ], + this.sendPaymentReceiveByMail.bind(this), + this.handleServiceErrors + ); return router; } @@ -416,27 +431,26 @@ export default class PaymentReceivesController extends BaseController { const { id: paymentReceiveId } = req.params; try { - const paymentReceive = - await this.paymentReceiveApplication.getPaymentReceive( - tenantId, - paymentReceiveId - ); - const ACCEPT_TYPE = { APPLICATION_PDF: 'application/pdf', APPLICATION_JSON: 'application/json', }; res.format({ - [ACCEPT_TYPE.APPLICATION_JSON]: () => { + [ACCEPT_TYPE.APPLICATION_JSON]: async () => { + const paymentReceive = + await this.paymentReceiveApplication.getPaymentReceive( + tenantId, + paymentReceiveId + ); return res.status(200).send({ - payment_receive: this.transfromToResponse(paymentReceive), + payment_receive: paymentReceive, }); }, [ACCEPT_TYPE.APPLICATION_PDF]: async () => { const pdfContent = await this.paymentReceiveApplication.getPaymentReceivePdf( tenantId, - paymentReceive + paymentReceiveId ); res.set({ 'Content-Type': 'application/pdf', @@ -507,6 +521,38 @@ export default class PaymentReceivesController extends BaseController { } }; + /** + * Sends mail invoice of the given sale invoice. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns + */ + public sendPaymentReceiveByMail = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + const { tenantId } = req; + const { id: paymentReceiveId } = req.params; + const paymentMailDTO: IPaymentReceiveMailOpts = this.matchedBodyData(req, { + includeOptionals: false, + }); + try { + await this.paymentReceiveApplication.notifyPaymentByMail( + tenantId, + paymentReceiveId, + paymentMailDTO + ); + return res.status(200).send({ + code: 200, + message: 'The payment notification has been sent successfully.', + }); + } catch (error) { + next(error); + } + }; + /** * Handles service errors. * @param error @@ -514,7 +560,7 @@ export default class PaymentReceivesController extends BaseController { * @param res * @param next */ - handleServiceErrors( + private handleServiceErrors( error: Error, req: Request, res: Response, diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index 9f3cf3719..c896ecbfd 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -1,10 +1,11 @@ import { Router, Request, Response, NextFunction } from 'express'; -import { check, param, query } from 'express-validator'; +import { body, check, param, query } from 'express-validator'; import { Inject, Service } from 'typedi'; import { AbilitySubject, ISaleEstimateDTO, SaleEstimateAction, + SaleEstimateMailOptions, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; @@ -121,6 +122,20 @@ export default class SalesEstimatesController extends BaseController { this.handleServiceErrors, this.dynamicListService.handlerErrorsToResponse ); + router.post( + '/:id/mail', + [ + ...this.validateSpecificEstimateSchema, + body('subject').isString().optional(), + body('from').isString().optional(), + body('to').isString().optional(), + body('body').isString().optional(), + body('attach_invoice').optional().isBoolean().toBoolean(), + ], + this.validationResult, + asyncMiddleware(this.sendSaleEstimateMail.bind(this)), + this.handleServiceErrors + ); return router; } @@ -362,22 +377,22 @@ export default class SalesEstimatesController extends BaseController { const { tenantId } = req; try { - const estimate = await this.saleEstimatesApplication.getSaleEstimate( - tenantId, - estimateId - ); // Response formatter. res.format({ // JSON content type. - [ACCEPT_TYPE.APPLICATION_JSON]: () => { - return res.status(200).send(this.transfromToResponse({ estimate })); + [ACCEPT_TYPE.APPLICATION_JSON]: async () => { + const estimate = await this.saleEstimatesApplication.getSaleEstimate( + tenantId, + estimateId + ); + return res.status(200).send({ estimate }); }, // PDF content type. [ACCEPT_TYPE.APPLICATION_PDF]: async () => { const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf( tenantId, - estimate + estimateId ); res.set({ 'Content-Type': 'application/pdf', @@ -478,6 +493,38 @@ export default class SalesEstimatesController extends BaseController { } }; + /** + * Send the sale estimate mail. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + private sendSaleEstimateMail = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + const { tenantId } = req; + const { id: invoiceId } = req.params; + const saleEstimateDTO: SaleEstimateMailOptions = this.matchedBodyData(req, { + includeOptionals: false, + }); + + try { + await this.saleEstimatesApplication.sendSaleEstimateMail( + tenantId, + invoiceId, + saleEstimateDTO + ); + return res.status(200).send({ + code: 200, + message: 'The sale estimate mail has been sent successfully.', + }); + } catch (error) { + next(error); + } + }; + /** * Handles service errors. * @param {Error} error diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index 3eabcf84e..89d7edef2 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -1,9 +1,9 @@ import { Router, Request, Response, NextFunction } from 'express'; -import { check, param, query } from 'express-validator'; +import { body, check, param, query } from 'express-validator'; import { Inject, Service } from 'typedi'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import BaseController from '../BaseController'; -import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt'; +import { ISaleReceiptDTO, SaleReceiptMailOpts } from '@/interfaces/SaleReceipt'; import { ServiceError } from '@/exceptions'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import CheckPolicies from '@/api/middleware/CheckPolicies'; @@ -46,6 +46,20 @@ export default class SalesReceiptsController extends BaseController { this.saleReceiptSmsDetails, this.handleServiceErrors ); + router.post( + '/:id/mail', + [ + ...this.specificReceiptValidationSchema, + body('subject').isString().optional(), + body('from').isString().optional(), + body('to').isString().optional(), + body('body').isString().optional(), + body('attach_invoice').optional().isBoolean().toBoolean(), + ], + this.validationResult, + asyncMiddleware(this.sendSaleReceiptMail.bind(this)), + this.handleServiceErrors + ); router.post( '/:id', CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt), @@ -314,26 +328,24 @@ export default class SalesReceiptsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async getSaleReceipt(req: Request, res: Response, next: NextFunction) { + public async getSaleReceipt(req: Request, res: Response, next: NextFunction) { const { id: saleReceiptId } = req.params; const { tenantId } = req; try { - const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt( - tenantId, - saleReceiptId - ); res.format({ - 'application/json': () => { - return res - .status(200) - .send(this.transfromToResponse({ saleReceipt })); + 'application/json': async () => { + const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt( + tenantId, + saleReceiptId + ); + return res.status(200).send({ saleReceipt }); }, 'application/pdf': async () => { const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf( tenantId, - saleReceipt + saleReceiptId ); res.set({ 'Content-Type': 'application/pdf', @@ -405,6 +417,39 @@ export default class SalesReceiptsController extends BaseController { } }; + /** + * Sends mail notification of the given sale receipt. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public sendSaleReceiptMail = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + const { tenantId } = req; + const { id: receiptId } = req.params; + const receiptMailDTO: SaleReceiptMailOpts = this.matchedBodyData(req, { + includeOptionals: false, + }); + + try { + await this.saleReceiptsApplication.sendSaleReceiptMail( + tenantId, + receiptId, + receiptMailDTO + ); + return res.status(200).send({ + code: 200, + message: + 'The sale receipt notification via sms has been sent successfully.', + }); + } catch (error) { + next(error); + } + }; + /** * Handles service errors. * @param {Error} error diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts index 6f8d8552a..c919182ae 100644 --- a/packages/server/src/interfaces/PaymentReceive.ts +++ b/packages/server/src/interfaces/PaymentReceive.ts @@ -165,3 +165,7 @@ export type IPaymentReceiveGLCommonEntry = Pick< | 'createdAt' | 'branchId' >; + +export interface IPaymentReceiveMailOpts { + +} \ No newline at end of file diff --git a/packages/server/src/interfaces/SaleEstimate.ts b/packages/server/src/interfaces/SaleEstimate.ts index f2a820e98..3a503e1fd 100644 --- a/packages/server/src/interfaces/SaleEstimate.ts +++ b/packages/server/src/interfaces/SaleEstimate.ts @@ -124,3 +124,11 @@ export interface ISaleEstimateApprovedEvent { saleEstimate: ISaleEstimate; trx: Knex.Transaction; } + +export interface SaleEstimateMailOptions { + to: string; + from: string; + subject: string; + body: string; + attachInvoice?: boolean; +} \ No newline at end of file diff --git a/packages/server/src/interfaces/SaleReceipt.ts b/packages/server/src/interfaces/SaleReceipt.ts index 4d319ec4f..102513f7e 100644 --- a/packages/server/src/interfaces/SaleReceipt.ts +++ b/packages/server/src/interfaces/SaleReceipt.ts @@ -134,3 +134,7 @@ export interface ISaleReceiptDeletingPayload { oldSaleReceipt: ISaleReceipt; trx: Knex.Transaction; } + +export interface SaleReceiptMailOpts { + +} \ No newline at end of file diff --git a/packages/server/src/loaders/jobs.ts b/packages/server/src/loaders/jobs.ts index 9beb7a2bc..74c7d6b6e 100644 --- a/packages/server/src/loaders/jobs.ts +++ b/packages/server/src/loaders/jobs.ts @@ -7,6 +7,9 @@ import OrganizationSetupJob from 'jobs/OrganizationSetup'; import OrganizationUpgrade from 'jobs/OrganizationUpgrade'; import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailJob'; import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob'; +import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEstimateMailJob'; +import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob'; +import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob'; export default ({ agenda }: { agenda: Agenda }) => { new ResetPasswordMailJob(agenda); @@ -17,6 +20,9 @@ export default ({ agenda }: { agenda: Agenda }) => { new OrganizationUpgrade(agenda); new SendSaleInvoiceMailJob(agenda); new SendSaleInvoiceReminderMailJob(agenda); + new SendSaleEstimateMailJob(agenda); + new SaleReceiptMailNotificationJob(agenda); + new PaymentReceiveMailNotificationJob(agenda); agenda.start(); }; diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts index bc558d424..b30a7a013 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -29,12 +29,12 @@ export class SendSaleEstimateMail { * Triggers the reminder mail of the given sale estimate. * @param {number} tenantId * @param {number} saleEstimateId - * @param messageOptions + * @param {SaleEstimateMailOptions} messageOptions */ public async triggerMail( tenantId: number, saleEstimateId: number, - messageOptions: any + messageOptions: SaleEstimateMailOptions ) { const payload = { tenantId, @@ -114,7 +114,6 @@ export class SendSaleEstimateMail { ...messageOptions, }; const formatter = R.curry(this.formatText)(tenantId, saleEstimateId); - const toEmail = parsedMessageOpts.to; const subject = await formatter(parsedMessageOpts.subject); const body = await formatter(parsedMessageOpts.body); const attachments = []; @@ -131,7 +130,7 @@ export class SendSaleEstimateMail { } await new Mail() .setSubject(subject) - .setTo(toEmail) + .setTo(parsedMessageOpts.to) .setContent(body) .setAttachments(attachments) .send(); diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index 38c2c721f..4e00bc813 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -329,7 +329,7 @@ export class SaleInvoiceApplication { * @param {number} tenantId * @param {number} saleInvoiceId * @param {SendInvoiceMailDTO} messageDTO - * @returns + * @returns {Promise} */ public sendSaleInvoiceMail( tenantId: number, From 657400c6713fa908210999ef9f1d7812ef3553a3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 24 Dec 2023 23:41:04 +0200 Subject: [PATCH 09/17] feat: tweak mail notifications content --- .../Sales/Estimates/SendSaleEstimateMail.ts | 9 ++++----- .../src/services/Sales/Estimates/constants.ts | 20 ++++++++++++------- .../PaymentReceiveMailNotification.ts | 6 +++--- .../Sales/PaymentReceives/constants.ts | 17 +++++++++------- .../Receipts/SaleReceiptMailNotification.ts | 18 ++++++++--------- .../src/services/Sales/Receipts/constants.ts | 12 +++++++---- 6 files changed, 46 insertions(+), 36 deletions(-) diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts index b30a7a013..9ae6fa2f8 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -61,11 +61,10 @@ export class SendSaleEstimateMail { ); return formatSmsMessage(text, { CustomerName: estimate.customer.displayName, - EstimateNumber: estimate.estimateNo, - EstimateDate: estimate.estimateDateFormatted, - EstimateAmount: estimate.totalFormatted, - EstimateDueDate: estimate.dueDateFormatted, - EstimateDueAmount: estimate.dueAmountFormatted, + EstimateNumber: estimate.estimateNumber, + EstimateDate: estimate.formattedEstimateDate, + EstimateAmount: estimate.formattedAmount, + EstimateExpirationDate: estimate.formattedExpirationDate, }); }; diff --git a/packages/server/src/services/Sales/Estimates/constants.ts b/packages/server/src/services/Sales/Estimates/constants.ts index b59350430..6b689a0e1 100644 --- a/packages/server/src/services/Sales/Estimates/constants.ts +++ b/packages/server/src/services/Sales/Estimates/constants.ts @@ -1,11 +1,17 @@ export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT = - 'Invoice {InvoiceNumber} reminder from {CompanyName}'; -export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = ` -

Dear {CustomerName}

-

You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.

-

Invoice #{InvoiceNumber}
-Due Date : {InvoiceDueDate}
-Amount : {InvoiceAmount}

+ 'Estimate {EstimateNumber} is awaiting your approval'; +export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `

Dear {CustomerName}

+

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

+

+Estimate #{EstimateNumber}
+Expiration Date : {EstimateExpirationDate}
+Amount : {EstimateAmount}
+

+ +

+Regards
+{CompanyName} +

`; export const ERRORS = { diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts index 3b92c2ece..42069c5f3 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts @@ -87,9 +87,9 @@ export class SendPaymentReceiveMailNotification { return formatSmsMessage(text, { CompanyName: organization.metadata.name, CustomerName: payment.customer.displayName, - PaymentNumber: payment.invoiceNo, - PaymentDate: payment.dueAmountFormatted, - PaymentAmount: payment.dueAmountFormatted, + PaymentNumber: payment.payment_receive_no, + PaymentDate: payment.formattedPaymentDate, + PaymentAmount: payment.formattedAmount, }); }; diff --git a/packages/server/src/services/Sales/PaymentReceives/constants.ts b/packages/server/src/services/Sales/PaymentReceives/constants.ts index e11060670..405939617 100644 --- a/packages/server/src/services/Sales/PaymentReceives/constants.ts +++ b/packages/server/src/services/Sales/PaymentReceives/constants.ts @@ -1,12 +1,15 @@ -export const DEFAULT_PAYMENT_MAIL_SUBJECT = - 'Invoice {InvoiceNumber} from {CompanyName}'; +export const DEFAULT_PAYMENT_MAIL_SUBJECT = 'Payment Received by {CompanyName}'; export const DEFAULT_PAYMENT_MAIL_CONTENT = `

Dear {CustomerName}

-

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

+

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

-Invoice #{InvoiceNumber}
-Due Date : {InvoiceDueDate}
-Amount : {InvoiceAmount}
+Payment Date : {PaymentDate}
+Amount : {PaymentAmount}
+

+ +

+Regards
+{CompanyName}

`; @@ -24,7 +27,7 @@ export const ERRORS = { PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES', PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', - NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR' + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR', }; export const DEFAULT_VIEWS = []; diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts index bf6c4c893..0d93c2b65 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts @@ -8,9 +8,9 @@ import Mail from '@/lib/Mail'; import { GetSaleReceipt } from './GetSaleReceipt'; import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; import { - DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, - DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, -} from '../Estimates/constants'; + DEFAULT_RECEIPT_MAIL_CONTENT, + DEFAULT_RECEIPT_MAIL_SUBJECT, +} from './constants'; import { ERRORS } from './constants'; import { SaleReceiptMailOpts } from '@/interfaces'; @@ -62,8 +62,8 @@ export class SaleReceiptMailNotification { return { attachInvoice: true, - subject: DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, - body: DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + subject: DEFAULT_RECEIPT_MAIL_SUBJECT, + body: DEFAULT_RECEIPT_MAIL_CONTENT, to: saleReceipt.customer.email, }; }; @@ -91,11 +91,9 @@ export class SaleReceiptMailNotification { return formatSmsMessage(text, { CompanyName: organization.metadata.name, CustomerName: invoice.customer.displayName, - InvoiceNumber: invoice.invoiceNo, - InvoiceDueAmount: invoice.dueAmountFormatted, - InvoiceDueDate: invoice.dueDateFormatted, - InvoiceDate: invoice.invoiceDateFormatted, - InvoiceAmount: invoice.totalFormatted, + ReceiptNumber: invoice.receiptNumber, + ReceiptDate: invoice.formattedReceiptDate, + ReceiptAmount: invoice.formattedAmount, }); }; diff --git a/packages/server/src/services/Sales/Receipts/constants.ts b/packages/server/src/services/Sales/Receipts/constants.ts index 977a5d9b1..ae1a2e388 100644 --- a/packages/server/src/services/Sales/Receipts/constants.ts +++ b/packages/server/src/services/Sales/Receipts/constants.ts @@ -2,11 +2,15 @@ export const DEFAULT_RECEIPT_MAIL_SUBJECT = 'Invoice {InvoiceNumber} from {CompanyName}'; export const DEFAULT_RECEIPT_MAIL_CONTENT = `

Dear {CustomerName}

-

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

+

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

-Invoice #{InvoiceNumber}
-Due Date : {InvoiceDueDate}
-Amount : {InvoiceAmount}
+Receipt #{ReceiptNumber}
+Amount : {ReceiptAmount}
+

+ +

+Regards
+{CompanyName}

`; From 3c8c3d82536357da480e399e9fbff5d96ff5ef86 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 25 Dec 2023 18:21:02 +0200 Subject: [PATCH 10/17] feat: add endpoints to retrieving the default mail options --- .../api/controllers/Sales/PaymentReceives.ts | 31 +++++++++++++++++ .../api/controllers/Sales/SalesEstimates.ts | 32 +++++++++++++++++ .../api/controllers/Sales/SalesInvoices.ts | 32 +++++++++++++++++ .../api/controllers/Sales/SalesReceipts.ts | 34 +++++++++++++++++++ .../Estimates/SaleEstimatesApplication.ts | 13 +++++++ .../Sales/Invoices/SaleInvoicesApplication.ts | 13 +++++++ .../PaymentReceivesApplication.ts | 13 +++++++ .../Sales/Receipts/SaleReceiptApplication.ts | 13 +++++++ 8 files changed, 181 insertions(+) diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 8604682ba..74657a703 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -132,6 +132,12 @@ export default class PaymentReceivesController extends BaseController { this.sendPaymentReceiveByMail.bind(this), this.handleServiceErrors ); + router.get( + '/:id/mail', + [...this.paymentReceiveValidation], + asyncMiddleware(this.getPaymentDefaultMail.bind(this)), + this.handleServiceErrors + ); return router; } @@ -553,6 +559,31 @@ export default class PaymentReceivesController extends BaseController { } }; + /** + * Retrieves the default mail options of the given payment transaction. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public getPaymentDefaultMail = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + const { tenantId } = req; + const { id: paymentReceiveId } = req.params; + + try { + const data = await this.paymentReceiveApplication.getPaymentDefaultMail( + tenantId, + paymentReceiveId + ); + return res.status(200).send({ data }); + } catch (error) { + next(error); + } + }; + /** * Handles service errors. * @param error diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index c896ecbfd..f78719de9 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -136,6 +136,13 @@ export default class SalesEstimatesController extends BaseController { asyncMiddleware(this.sendSaleEstimateMail.bind(this)), this.handleServiceErrors ); + router.get( + '/:id/mail', + [...this.validateSpecificEstimateSchema], + this.validationResult, + asyncMiddleware(this.getSaleEstimateMail.bind(this)), + this.handleServiceErrors + ); return router; } @@ -525,6 +532,31 @@ export default class SalesEstimatesController extends BaseController { } }; + /** + * Retrieves the default mail options of the given sale estimate. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + private getSaleEstimateMail = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + const { tenantId } = req; + const { id: invoiceId } = req.params; + + try { + const data = await this.saleEstimatesApplication.getSaleEstimateMail( + tenantId, + invoiceId + ); + return res.status(200).send({ data }); + } catch (error) { + next(error); + } + }; + /** * Handles service errors. * @param {Error} error diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index daff92f37..cd7f5bfec 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -181,6 +181,13 @@ export default class SaleInvoicesController extends BaseController { asyncMiddleware(this.sendSaleInvoiceMail.bind(this)), this.handleServiceErrors ); + router.get( + '/:id/mail', + [...this.specificSaleInvoiceValidation], + this.validationResult, + asyncMiddleware(this.getSaleInvoiceMail.bind(this)), + this.handleServiceErrors + ); return router; } @@ -748,6 +755,31 @@ export default class SaleInvoicesController extends BaseController { } } + /** + * Retrieves the default mail options of the given sale invoice. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async getSaleInvoiceMail( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { id: invoiceId } = req.params; + + try { + const data = await this.saleInvoiceApplication.getSaleInvoiceMail( + tenantId, + invoiceId + ); + return res.status(200).send({ data }); + } catch (error) { + next(error); + } + } + /** * Handles service errors. * @param {Error} error diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index 89d7edef2..c8635fe3c 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -60,6 +60,15 @@ export default class SalesReceiptsController extends BaseController { asyncMiddleware(this.sendSaleReceiptMail.bind(this)), this.handleServiceErrors ); + router.get( + '/:id/mail', + [ + ...this.specificReceiptValidationSchema, + ], + this.validationResult, + asyncMiddleware(this.getSaleReceiptMail.bind(this)), + this.handleServiceErrors + ); router.post( '/:id', CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt), @@ -450,6 +459,31 @@ export default class SalesReceiptsController extends BaseController { } }; + /** + * Retrieves the default mail options of the given sale receipt. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public getSaleReceiptMail = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + const { tenantId } = req; + const { id: receiptId } = req.params; + + try { + const data = await this.saleReceiptsApplication.getSaleReceiptMail( + tenantId, + receiptId + ); + return res.status(200).send({ data }); + } catch (error) { + next(error); + } + }; + /** * Handles service errors. * @param {Error} error diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 6a208746e..68cd8e601 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -232,4 +232,17 @@ export class SaleEstimatesApplication { saleEstimateMailOpts ); } + + /** + * Retrieves the default mail options of the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {} + */ + public getSaleEstimateMail(tenantId: number, saleEstimateId: number) { + return this.sendEstimateMailService.getDefaultMailOpts( + tenantId, + saleEstimateId + ); + } } diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index 4e00bc813..9b3c19d33 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -342,4 +342,17 @@ export class SaleInvoiceApplication { messageDTO ); } + + /** + * Retrieves the default mail options of the given sale invoice. + * @param {number} tenantId + * @param {number} saleInvoiceid + * @returns {Promise} + */ + public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) { + return this.sendInvoiceReminderService.getDefaultMailOpts( + tenantId, + saleInvoiceid + ); + } } diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts index bbf88b1ca..6092664e6 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts @@ -198,6 +198,19 @@ export class PaymentReceivesApplication { ); } + /** + * Retrieves the default mail options of the given payment transaction. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @returns {Promise} + */ + public getPaymentDefaultMail(tenantId: number, paymentReceiveId: number) { + return this.paymentMailNotify.getDefaultMailOpts( + tenantId, + paymentReceiveId + ); + } + /** * Retrieve pdf content of the given payment receive. * @param {number} tenantId diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index 9fdfb548a..6fe03b2a1 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -188,4 +188,17 @@ export class SaleReceiptApplication { messageOpts ); } + + /** + * Retrieves the default mail options of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + public getSaleReceiptMail(tenantId: number, saleReceiptId: number) { + return this.saleReceiptNotifyByMailService.getDefaultMailOpts( + tenantId, + saleReceiptId + ); + } } From de1b7f132ca94610352b9c33761606e1588eccb3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 26 Dec 2023 15:52:38 +0200 Subject: [PATCH 11/17] feat(webapp): send mail notification dialogs --- .../src/components/DialogsContainer.tsx | 8 ++++ packages/webapp/src/constants/dialogs.ts | 4 ++ .../EstimateDetailActionsBar.tsx | 14 ++++++ .../InvoiceDetailActionsBar.tsx | 12 +++++ .../PaymentReceiveActionsBar.tsx | 12 +++++ .../ReceiptDetailActionBar.tsx | 11 +++++ .../EstimateMailDialog/EstimateMailDialog.tsx | 36 +++++++++++++++ .../EstimateMailDialogBoot.tsx | 43 ++++++++++++++++++ .../EstimateMailDialogContent.tsx | 18 ++++++++ .../EstimateMailDialogForm.tsx | 31 +++++++++++++ .../Estimates/EstimateMailDialog/index.ts | 1 + .../InvoiceMailDialog/InvoiceMailDialog.tsx | 36 +++++++++++++++ .../InvoiceMailDialogBoot.tsx | 43 ++++++++++++++++++ .../InvoiceMailDialogContent.tsx | 17 +++++++ .../InvoiceMailDialogForm.tsx | 29 ++++++++++++ .../Sales/Invoices/InvoiceMailDialog/index.ts | 1 + .../PaymentMailDialog/PaymentMailDialog.tsx | 36 +++++++++++++++ .../PaymentMailDialogBoot.tsx | 44 +++++++++++++++++++ .../PaymentMailDialogContent.tsx | 17 +++++++ .../PaymentMailDialogForm.tsx | 29 ++++++++++++ .../PaymentMailDialog/index.ts | 1 + .../ReceiptMailDialog/ReceiptMailDialog.tsx | 36 +++++++++++++++ .../ReceiptMailDialogBoot.tsx | 44 +++++++++++++++++++ .../ReceiptMailDialogContent.tsx | 18 ++++++++ .../ReceiptMailDialogForm.tsx | 29 ++++++++++++ .../Receipts/ReceiptMailDialog/index.tsx | 1 + .../SendMailNotificationForm.tsx | 36 +++++++++++++++ .../containers/SendMailNotification/index.ts | 1 + packages/webapp/src/hooks/query/estimates.tsx | 30 +++++++++++++ packages/webapp/src/hooks/query/invoices.tsx | 31 +++++++++++++ .../src/hooks/query/paymentReceives.tsx | 31 +++++++++++++ packages/webapp/src/hooks/query/receipts.tsx | 33 ++++++++++++++ packages/webapp/src/hooks/query/types.tsx | 4 ++ 33 files changed, 737 insertions(+) create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogBoot.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogContent.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogForm.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/index.ts create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog.tsx create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBoot.tsx create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogContent.tsx create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/index.ts create mode 100644 packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog.tsx create mode 100644 packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx create mode 100644 packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogContent.tsx create mode 100644 packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx create mode 100644 packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/index.ts create mode 100644 packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog.tsx create mode 100644 packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogBoot.tsx create mode 100644 packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogContent.tsx create mode 100644 packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx create mode 100644 packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/index.tsx create mode 100644 packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx create mode 100644 packages/webapp/src/containers/SendMailNotification/index.ts diff --git a/packages/webapp/src/components/DialogsContainer.tsx b/packages/webapp/src/components/DialogsContainer.tsx index 2ee579980..b301e0492 100644 --- a/packages/webapp/src/components/DialogsContainer.tsx +++ b/packages/webapp/src/components/DialogsContainer.tsx @@ -47,6 +47,10 @@ import ProjectInvoicingFormDialog from '@/containers/Projects/containers/Project import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog'; import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog'; import { DialogsName } from '@/constants/dialogs'; +import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog'; +import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog'; +import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog'; +import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog'; /** * Dialogs container. @@ -137,6 +141,10 @@ export default function DialogsContainer() { dialogName={DialogsName.ProjectBillableEntriesForm} /> + + + + ); } diff --git a/packages/webapp/src/constants/dialogs.ts b/packages/webapp/src/constants/dialogs.ts index 115c25af2..5378efb24 100644 --- a/packages/webapp/src/constants/dialogs.ts +++ b/packages/webapp/src/constants/dialogs.ts @@ -48,4 +48,8 @@ export enum DialogsName { ProjectBillableEntriesForm = 'project-billable-entries', InvoiceNumberSettings = 'InvoiceNumberSettings', TaxRateForm = 'tax-rate-form', + InvoiceMail = 'invoice-mail', + EstimateMail = 'estimate-mail', + ReceiptMail = 'receipt-mail', + PaymentMail = 'payment-mail', } diff --git a/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.tsx b/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.tsx index 6f1ca3e91..65821afd8 100644 --- a/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.tsx +++ b/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.tsx @@ -26,6 +26,7 @@ import { import { compose } from '@/utils'; import { DRAWERS } from '@/constants/drawers'; +import { DialogsName } from '@/constants/dialogs'; /** * Estimate read-only details actions bar of the drawer. @@ -65,6 +66,10 @@ function EstimateDetailActionsBar({ const handleNotifyViaSMS = () => { openDialog('notify-estimate-via-sms', { estimateId }); }; + // Handles the estimate mail dialog. + const handleMailEstimate = () => { + openDialog(DialogsName.EstimateMail, { estimateId }); + }; return ( @@ -86,6 +91,15 @@ function EstimateDetailActionsBar({ onClick={handlePrintEstimate} /> + + + + + + + ); } + +const HeaderBox = styled('div')` + border-top-right-radius: 5px; + border-top-left-radius: 5px; + border: 1px solid #dddfe9; + padding: 15px; + + .bp4-form-group { + margin: 0; + padding-top: 12px; + padding-bottom: 12px; + + &:not(:last-of-type) { + border-bottom: 1px solid #dddfe9; + } + &:first-of-type { + padding-top: 0; + } + &:last-of-type { + padding-bottom: 0; + } + } + + .bp4-form-content { + flex: 1 0; + } + + .bp4-label { + min-width: 65px; + color: #738091; + } + + .bp4-input { + } +`; diff --git a/packages/webapp/src/static/json/icons.tsx b/packages/webapp/src/static/json/icons.tsx index b13e9ffa5..e60213bb8 100644 --- a/packages/webapp/src/static/json/icons.tsx +++ b/packages/webapp/src/static/json/icons.tsx @@ -561,8 +561,14 @@ export default { }, 'content-copy': { path: [ - 'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z' + 'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z', ], - viewBox: '0 0 16 16' - } + viewBox: '0 0 16 16', + }, + envelope: { + path: [ + 'M0 4.01v11.91l6.27-6.27L0 4.01zm18.91-1.03H1.09L10 10.97l8.91-7.99zm-5.18 6.66L20 15.92V4.01l-6.27 5.63zm-3.23 2.9c-.13.12-.31.19-.5.19s-.37-.07-.5-.19l-2.11-1.89-6.33 6.33h17.88l-6.33-6.33-2.11 1.89z', + ], + viewBox: '0 0 20 20', + }, }; From a676e095376f311be6f457e41d112b06fa223359 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 27 Dec 2023 23:56:34 +0200 Subject: [PATCH 13/17] feat: rich editor component --- .../src/components/Forms/FRichEditor.tsx | 52 ++++++++++++ .../webapp/src/components/Forms/index.tsx | 3 +- .../RichEditor/RichEditor.style.scss | 66 +++++++++++++++ .../src/components/RichEditor/RichEditor.tsx | 58 ++++++++++++++ .../webapp/src/components/RichEditor/index.ts | 1 + .../SendMailNotification/RichEditor.tsx | 63 +++++++++++++++ .../SendMailNotificationForm.tsx | 80 +++++++++++++++++-- 7 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 packages/webapp/src/components/Forms/FRichEditor.tsx create mode 100644 packages/webapp/src/components/RichEditor/RichEditor.style.scss create mode 100644 packages/webapp/src/components/RichEditor/RichEditor.tsx create mode 100644 packages/webapp/src/components/RichEditor/index.ts create mode 100644 packages/webapp/src/containers/SendMailNotification/RichEditor.tsx diff --git a/packages/webapp/src/components/Forms/FRichEditor.tsx b/packages/webapp/src/components/Forms/FRichEditor.tsx new file mode 100644 index 000000000..d490f87f5 --- /dev/null +++ b/packages/webapp/src/components/Forms/FRichEditor.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { FieldConfig, FieldProps } from 'formik'; +import { Field } from '@blueprintjs-formik/core'; +import { RichEditor, RichEditorProps } from '../../components/RichEditor'; + +export interface FRichEditorProps + extends Omit, + RichEditorProps { + name: string; + value?: string; +} + +interface FieldToRichEditorProps + extends FieldProps, + Omit {} + +/** + * Transformes the field props to `RichEditor` props. + * @param {FieldToRichEditorProps} + * @returns {HTMLSelectProps} + */ +function fieldToRichEditor({ + field: { onBlur: onFieldBlur, ...field }, + form: { touched, errors, ...form }, + ...props +}: FieldToRichEditorProps): RichEditorProps { + return { + ...field, + ...props, + onChange: (value: string) => { + form.setFieldValue(field.name, value); + }, + }; +} + +/** + * Transformes field props to `RichEditor` props. + * @param {FieldToRichEditorProps} + * @returns {JSX.Element} + */ +function FieldToRichEditor({ ...props }: FieldToRichEditorProps): JSX.Element { + return ; +} + +/** + * Rich editor wrapper to bind with Formik. + * @param {FRichEditorProps} props - + * @returns {JSX.Element} + */ +export function FRichEditor({ ...props }: FRichEditorProps): JSX.Element { + return ; +} diff --git a/packages/webapp/src/components/Forms/index.tsx b/packages/webapp/src/components/Forms/index.tsx index d4fb2aec0..c638ac029 100644 --- a/packages/webapp/src/components/Forms/index.tsx +++ b/packages/webapp/src/components/Forms/index.tsx @@ -4,4 +4,5 @@ export * from './FMoneyInputGroup'; export * from './BlueprintFormik'; export * from './InputPrependText'; export * from './InputPrependButton'; -export * from './MoneyInputGroup'; \ No newline at end of file +export * from './MoneyInputGroup'; +export * from './FRichEditor'; \ No newline at end of file diff --git a/packages/webapp/src/components/RichEditor/RichEditor.style.scss b/packages/webapp/src/components/RichEditor/RichEditor.style.scss new file mode 100644 index 000000000..942fdf81e --- /dev/null +++ b/packages/webapp/src/components/RichEditor/RichEditor.style.scss @@ -0,0 +1,66 @@ +/* Basic editor styles */ +.tiptap { + color: #222; + + &:focus-visible { + outline: none; + } + + >*+* { + margin-top: 0.75em; + } + + ul, + ol { + padding: 0 1rem; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + } + + code { + background: rgba(#ffffff, 0.1); + color: rgba(#ffffff, 0.6); + border: 1px solid rgba(#ffffff, 0.1); + border-radius: 0.5rem; + padding: 0.2rem; + } + + pre { + background: rgba(#ffffff, 0.1); + font-family: "JetBrainsMono", monospace; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + + code { + color: inherit; + padding: 0; + background: none; + font-size: 0.8rem; + border: none; + } + } + + img { + max-width: 100%; + height: auto; + } + + blockquote { + margin-left: 0; + padding-left: 1rem; + border-left: 2px solid rgba(#ffffff, 0.4); + + hr { + border: none; + border-top: 2px solid rgba(#ffffff, 0.1); + margin: 2rem 0; + } + } +} \ No newline at end of file diff --git a/packages/webapp/src/components/RichEditor/RichEditor.tsx b/packages/webapp/src/components/RichEditor/RichEditor.tsx new file mode 100644 index 000000000..da82fb09f --- /dev/null +++ b/packages/webapp/src/components/RichEditor/RichEditor.tsx @@ -0,0 +1,58 @@ +// @ts-nocheck +import { Color } from '@tiptap/extension-color'; +import ListItem from '@tiptap/extension-list-item'; +import TextStyle from '@tiptap/extension-text-style'; +import { EditorProvider } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import { useUncontrolled } from '@/hooks/useUncontrolled'; +import { Box } from '../Layout/Box'; +import './RichEditor.style.scss'; + +const extensions = [ + Color.configure({ types: [TextStyle.name, ListItem.name] }), + TextStyle.configure({ types: [ListItem.name] }), + StarterKit.configure({ + bulletList: { + keepMarks: true, + keepAttributes: false, + }, + orderedList: { + keepMarks: true, + keepAttributes: false, + }, + }), +]; + +export interface RichEditorProps { + value?: string; + initialValue?: string; + onChange?: (value: string) => void; + className?: string; +} +export const RichEditor = ({ + value, + initialValue, + onChange, + className, +}: RichEditorProps) => { + const [content, handleChange] = useUncontrolled({ + value, + initialValue, + onChange, + finalValue: '', + }); + + const handleBlur = ({ editor }) => { + handleChange(editor.getHTML()); + }; + + return ( + + + + ); +}; diff --git a/packages/webapp/src/components/RichEditor/index.ts b/packages/webapp/src/components/RichEditor/index.ts new file mode 100644 index 000000000..226b701f3 --- /dev/null +++ b/packages/webapp/src/components/RichEditor/index.ts @@ -0,0 +1 @@ +export * from './RichEditor'; \ No newline at end of file diff --git a/packages/webapp/src/containers/SendMailNotification/RichEditor.tsx b/packages/webapp/src/containers/SendMailNotification/RichEditor.tsx new file mode 100644 index 000000000..e540fd6da --- /dev/null +++ b/packages/webapp/src/containers/SendMailNotification/RichEditor.tsx @@ -0,0 +1,63 @@ +// @ts-nocheck +import './styles.scss'; +import { Color } from '@tiptap/extension-color'; +import ListItem from '@tiptap/extension-list-item'; +import TextStyle from '@tiptap/extension-text-style'; +import { EditorProvider } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import { Box } from '@/components'; +import styled from 'styled-components'; +import { useUncontrolled } from '@/hooks/useUncontrolled'; + +const extensions = [ + Color.configure({ types: [TextStyle.name, ListItem.name] }), + TextStyle.configure({ types: [ListItem.name] }), + StarterKit.configure({ + bulletList: { + keepMarks: true, + keepAttributes: false, + }, + orderedList: { + keepMarks: true, + keepAttributes: false, + }, + }), +]; + +export interface RichEditorProps { + value?: string; + initialValue?: string; + onChange?: (value: string) => void; + className?: string; +} +export const RichEditor = ({ + value, + initialValue, + onChange, + className, +}: RichEditorProps) => { + const [content, handleChange] = useUncontrolled({ + value, + initialValue, + finalValue: '', + onChange, + }); + + return ( + + + + ); +}; + +const Root = styled(Box)` + padding: 15px; + border: 1px solid #dedfe9; + border-top: 0; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +`; diff --git a/packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx b/packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx index a23b108af..ec5263c2f 100644 --- a/packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx +++ b/packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx @@ -1,8 +1,15 @@ // @ts-nocheck import { Form, useFormikContext } from 'formik'; -import { FFormGroup, FInputGroup, FMultiSelect } from '@/components'; +import { + FFormGroup, + FInputGroup, + FMultiSelect, + FRichEditor, + FSwitch, + Hint, +} from '@/components'; import styled from 'styled-components'; -import { Button, Classes, Intent } from '@blueprintjs/core'; +import { Button, Classes, Intent, Position } from '@blueprintjs/core'; import { saveInvoke } from '@/utils'; interface SendMailNotificationFormProps { @@ -24,25 +31,47 @@ export function SendMailNotificationForm({ + } name={'from'} inline={true} fastField={true} > @@ -56,6 +85,12 @@ export function SendMailNotificationForm({ + + + + + +
@@ -82,16 +117,33 @@ export function SendMailNotificationForm({ ); } +const AttachFormGroup = styled(FFormGroup)` + background: #f8f9fb; + margin-top: 0.6rem; + padding: 4px 14px; + border-radius: 5px; + border: 1px solid #dcdcdd; +`; + +const MailMessageEditor = styled(FRichEditor)` + padding: 15px; + border: 1px solid #dedfe9; + border-top: 0; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +`; + const HeaderBox = styled('div')` border-top-right-radius: 5px; border-top-left-radius: 5px; border: 1px solid #dddfe9; - padding: 15px; + border-bottom: 2px solid #eaeaef; + padding: 6px 15px; .bp4-form-group { margin: 0; - padding-top: 12px; - padding-bottom: 12px; + padding-top: 8px; + padding-bottom: 8px; &:not(:last-of-type) { border-bottom: 1px solid #dddfe9; @@ -114,5 +166,19 @@ const HeaderBox = styled('div')` } .bp4-input { + border-color: transparent; + padding: 0; + + &:focus, + &.bp4-active { + box-shadow: 0 0 0 0; + } + } + + .bp4-input-ghost { + margin-top: 5px; + } + .bp4-tag-input-values { + margin: 0; } `; From dc762567b5bbd60a733f75672a6cc8fc1a40c4cf Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 28 Dec 2023 17:53:51 +0200 Subject: [PATCH 14/17] feat(webapp): send mail notififcation of sale transactions --- packages/webapp/package.json | 7 + .../EstimateMailDialog/EstimateMailDialog.tsx | 2 +- .../EstimateMailDialogContent.tsx | 1 - .../EstimateMailDialogForm.tsx | 15 +- .../EstimateMailDialogFormContent.tsx | 62 ++ .../InvoiceMailDialogForm.schema.ts | 9 + .../InvoiceMailDialogForm.tsx | 18 +- .../InvoiceMailDialogFormContent.tsx | 62 ++ .../PaymentMailDialogForm.tsx | 12 +- .../PaymentMailDialogFormContent.tsx | 62 ++ .../ReceiptMailDialogForm.tsx | 10 +- .../ReceiptMailDialogFormContent.tsx | 62 ++ .../MailNotificationForm.tsx | 134 +++++ .../SendMailNotificationForm.tsx | 184 ------ pnpm-lock.yaml | 555 ++++++++++++++++++ 15 files changed, 987 insertions(+), 208 deletions(-) create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx create mode 100644 packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx create mode 100644 packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx create mode 100644 packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx delete mode 100644 packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx diff --git a/packages/webapp/package.json b/packages/webapp/package.json index d4e1c65d1..c1c2415aa 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -20,6 +20,13 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.4.0", "@testing-library/user-event": "^7.2.1", + "@tiptap/extension-color": "latest", + "@tiptap/extension-text-style": "2.1.13", + "@tiptap/core": "2.1.13", + "@tiptap/pm": "2.1.13", + "@tiptap/extension-list-item": "2.1.13", + "@tiptap/react": "2.1.13", + "@tiptap/starter-kit": "2.1.13", "@types/jest": "^26.0.15", "@types/js-money": "^0.6.1", "@types/lodash": "^4.14.172", diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog.tsx index de6082222..9965db833 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog.tsx @@ -19,7 +19,7 @@ function EstimateMailDialog({ return ( - + ); } diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx new file mode 100644 index 000000000..d299d5b16 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx @@ -0,0 +1,62 @@ +// @ts-nocheck +import { Form, useFormikContext } from 'formik'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import styled from 'styled-components'; +import { FFormGroup, FSwitch } from '@/components'; +import { MailNotificationForm } from '@/containers/SendMailNotification'; +import { saveInvoke } from '@/utils'; + +interface EstimateMailDialogFormContentProps { + onClose?: () => void; +} + +export function EstimateMailDialogFormContent({ + onClose, +}: EstimateMailDialogFormContentProps) { + const { isSubmitting } = useFormikContext(); + + const handleClose = () => { + saveInvoke(onClose); + }; + + return ( +
+
+ + + + + +
+ +
+
+ + + +
+
+
+ ); +} + +const AttachFormGroup = styled(FFormGroup)` + background: #f8f9fb; + margin-top: 0.6rem; + padding: 4px 14px; + border-radius: 5px; + border: 1px solid #dcdcdd; +`; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts new file mode 100644 index 000000000..1c365ac4a --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import * as Yup from 'yup'; + +export const InvoiceMailFormSchema = Yup.object().shape({ + from: Yup.array().required().min(1).max(5).label('From address'), + to: Yup.array().required().min(1).max(5).label('To address'), + subject: Yup.string().required().label('Mail subject'), + body: Yup.string().required().label('Mail body'), +}); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx index fed8bee61..6aa62cfba 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx @@ -2,25 +2,27 @@ import { Formik } from 'formik'; import { castArray } from 'lodash'; import * as R from 'ramda'; -import { SendMailNotificationForm } from '@/containers/SendMailNotification'; import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot'; import { transformToForm } from '@/utils'; -import withDialogActions from '@/containers/Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { useSendSaleInvoiceMail } from '@/hooks/query'; +import { InvoiceMailDialogFormContent } from './InvoiceMailDialogFormContent'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { InvoiceMailFormSchema } from './InvoiceMailDialogForm.schema'; const initialFormValues = { from: [], to: [], subject: '', - message: '', + body: '', + attachInvoice: true, }; interface InvoiceMailFormValues { from: string[]; to: string[]; subject: string; - message: string; + body: string; attachInvoice: boolean; } @@ -54,8 +56,12 @@ function InvoiceMailDialogFormRoot({ }; return ( - - + + ); } diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx new file mode 100644 index 000000000..2038d5379 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx @@ -0,0 +1,62 @@ +// @ts-nocheck +import { Form, useFormikContext } from 'formik'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import styled from 'styled-components'; +import { FFormGroup, FSwitch } from '@/components'; +import { MailNotificationForm } from '@/containers/SendMailNotification'; +import { saveInvoke } from '@/utils'; + +interface SendMailNotificationFormProps { + onClose?: () => void; +} + +export function InvoiceMailDialogFormContent({ + onClose, +}: SendMailNotificationFormProps) { + const { isSubmitting } = useFormikContext(); + + const handleClose = () => { + saveInvoke(onClose); + }; + + return ( +
+
+ + + + + +
+ +
+
+ + + +
+
+
+ ); +} + +const AttachFormGroup = styled(FFormGroup)` + background: #f8f9fb; + margin-top: 0.6rem; + padding: 4px 14px; + border-radius: 5px; + border: 1px solid #dcdcdd; +`; diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx index 395aff4f2..04906185d 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx @@ -2,25 +2,27 @@ import { Formik, FormikBag } from 'formik'; import { castArray } from 'lodash'; import * as R from 'ramda'; -import { SendMailNotificationForm } from '@/containers/SendMailNotification'; import { usePaymentMailDialogBoot } from './PaymentMailDialogBoot'; -import { transformToForm } from '@/utils'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { useSendPaymentReceiveMail } from '@/hooks/query'; +import { PaymentMailDialogFormContent } from './PaymentMailDialogFormContent'; +import { transformToForm } from '@/utils'; const initialFormValues = { from: [], to: [], subject: '', - message: '', + body: '', + attachPayment: true, }; interface PaymentMailFormValue { from: string[]; to: string[]; subject: string; - message: string; + body: string; + attachPayment: boolean; } export function PaymentMailDialogFormRoot({ @@ -57,7 +59,7 @@ export function PaymentMailDialogFormRoot({ return ( - + ); } diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx new file mode 100644 index 000000000..172494c40 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx @@ -0,0 +1,62 @@ +// @ts-nocheck +import { Form, useFormikContext } from 'formik'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import styled from 'styled-components'; +import { FFormGroup, FSwitch } from '@/components'; +import { MailNotificationForm } from '@/containers/SendMailNotification'; +import { saveInvoke } from '@/utils'; + +interface PaymentMailDialogFormContentProps { + onClose?: () => void; +} + +export function PaymentMailDialogFormContent({ + onClose, +}: PaymentMailDialogFormContentProps) { + const { isSubmitting } = useFormikContext(); + + const handleClose = () => { + saveInvoke(onClose); + }; + + return ( +
+
+ + + + + +
+ +
+
+ + + +
+
+
+ ); +} + +const AttachFormGroup = styled(FFormGroup)` + background: #f8f9fb; + margin-top: 0.6rem; + padding: 4px 14px; + border-radius: 5px; + border: 1px solid #dcdcdd; +`; diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx index 76b8cef7b..2d5a3bcf1 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx @@ -2,24 +2,26 @@ import { Formik, FormikBag } from 'formik'; import { castArray } from 'lodash'; import * as R from 'ramda'; -import { SendMailNotificationForm } from '@/containers/SendMailNotification'; import { useReceiptMailDialogBoot } from './ReceiptMailDialogBoot'; import { transformToForm } from '@/utils'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { useSendSaleReceiptMail } from '@/hooks/query'; +import { ReceiptMailDialogFormContent } from './ReceiptMailDialogFormContent'; const initialFormValues = { from: [], to: [], subject: '', - message: '', + body: '', + attachReceipt: true, }; interface ReceiptMailFormValues { from: string[]; to: string[]; subject: string; - message: string; + body: string; + attachReceipt: boolean; } function ReceiptMailDialogFormRoot({ closeDialog }) { @@ -52,7 +54,7 @@ function ReceiptMailDialogFormRoot({ closeDialog }) { return ( - + ); } diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx new file mode 100644 index 000000000..381160f09 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx @@ -0,0 +1,62 @@ +// @ts-nocheck +import { Form, useFormikContext } from 'formik'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import styled from 'styled-components'; +import { FFormGroup, FSwitch } from '@/components'; +import { MailNotificationForm } from '@/containers/SendMailNotification'; +import { saveInvoke } from '@/utils'; + +interface SendMailNotificationFormProps { + onClose?: () => void; +} + +export function ReceiptMailDialogFormContent({ + onClose, +}: SendMailNotificationFormProps) { + const { isSubmitting } = useFormikContext(); + + const handleClose = () => { + saveInvoke(onClose); + }; + + return ( +
+
+ + + + + +
+ +
+
+ + + +
+
+
+ ); +} + +const AttachFormGroup = styled(FFormGroup)` + background: #f8f9fb; + margin-top: 0.6rem; + padding: 4px 14px; + border-radius: 5px; + border: 1px solid #dcdcdd; +`; diff --git a/packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx b/packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx new file mode 100644 index 000000000..6b5053dd5 --- /dev/null +++ b/packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx @@ -0,0 +1,134 @@ +// @ts-nocheck +import { + Box, + FFormGroup, + FInputGroup, + FMultiSelect, + FRichEditor, + Hint, +} from '@/components'; +import styled from 'styled-components'; +import { Position } from '@blueprintjs/core'; +import { SelectOptionProps } from '@blueprintjs-formik/select'; + +interface MailNotificationFormProps { + fromAddresses: SelectOptionProps[]; + toAddresses: SelectOptionProps[]; +} + +export function MailNotificationForm({ + fromAddresses, + toAddresses, +}: MailNotificationFormProps) { + return ( + + + + } + name={'from'} + inline={true} + fastField={true} + > + + + + + + + + + + + + + + + ); +} + +const MailMessageEditor = styled(FRichEditor)` + padding: 15px; + border: 1px solid #dedfe9; + border-top: 0; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +`; + +const HeaderBox = styled('div')` + border-top-right-radius: 5px; + border-top-left-radius: 5px; + border: 1px solid #dddfe9; + border-bottom: 2px solid #eaeaef; + padding: 6px 15px; + + .bp4-form-group { + margin: 0; + padding-top: 8px; + padding-bottom: 8px; + + &:not(:last-of-type) { + border-bottom: 1px solid #dddfe9; + } + &:first-of-type { + padding-top: 0; + } + &:last-of-type { + padding-bottom: 0; + } + } + + .bp4-form-content { + flex: 1 0; + } + + .bp4-label { + min-width: 65px; + color: #738091; + } + + .bp4-input { + border-color: transparent; + padding: 0; + + &:focus, + &.bp4-active { + box-shadow: 0 0 0 0; + } + } + + .bp4-input-ghost { + margin-top: 5px; + } + .bp4-tag-input-values { + margin: 0; + } +`; diff --git a/packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx b/packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx deleted file mode 100644 index ec5263c2f..000000000 --- a/packages/webapp/src/containers/SendMailNotification/SendMailNotificationForm.tsx +++ /dev/null @@ -1,184 +0,0 @@ -// @ts-nocheck -import { Form, useFormikContext } from 'formik'; -import { - FFormGroup, - FInputGroup, - FMultiSelect, - FRichEditor, - FSwitch, - Hint, -} from '@/components'; -import styled from 'styled-components'; -import { Button, Classes, Intent, Position } from '@blueprintjs/core'; -import { saveInvoke } from '@/utils'; - -interface SendMailNotificationFormProps { - onClose?: () => void; -} - -export function SendMailNotificationForm({ - onClose, -}: SendMailNotificationFormProps) { - const { isSubmitting } = useFormikContext(); - - const handleClose = () => { - saveInvoke(onClose); - }; - - return ( -
-
- - - } - name={'from'} - inline={true} - fastField={true} - > - - - - - - - - - - - - - - - - - -
- -
-
- - - -
-
-
- ); -} - -const AttachFormGroup = styled(FFormGroup)` - background: #f8f9fb; - margin-top: 0.6rem; - padding: 4px 14px; - border-radius: 5px; - border: 1px solid #dcdcdd; -`; - -const MailMessageEditor = styled(FRichEditor)` - padding: 15px; - border: 1px solid #dedfe9; - border-top: 0; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; -`; - -const HeaderBox = styled('div')` - border-top-right-radius: 5px; - border-top-left-radius: 5px; - border: 1px solid #dddfe9; - border-bottom: 2px solid #eaeaef; - padding: 6px 15px; - - .bp4-form-group { - margin: 0; - padding-top: 8px; - padding-bottom: 8px; - - &:not(:last-of-type) { - border-bottom: 1px solid #dddfe9; - } - &:first-of-type { - padding-top: 0; - } - &:last-of-type { - padding-bottom: 0; - } - } - - .bp4-form-content { - flex: 1 0; - } - - .bp4-label { - min-width: 65px; - color: #738091; - } - - .bp4-input { - border-color: transparent; - padding: 0; - - &:focus, - &.bp4-active { - box-shadow: 0 0 0 0; - } - } - - .bp4-input-ghost { - margin-top: 5px; - } - .bp4-tag-input-values { - margin: 0; - } -`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9482a23bf..a3cdf6aa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,6 +471,27 @@ importers: '@testing-library/user-event': specifier: ^7.2.1 version: 7.2.1(@testing-library/dom@8.20.0) + '@tiptap/core': + specifier: 2.1.13 + version: 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/extension-color': + specifier: latest + version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/extension-text-style@2.1.13) + '@tiptap/extension-list-item': + specifier: 2.1.13 + version: 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-text-style': + specifier: 2.1.13 + version: 2.1.13(@tiptap/core@2.1.13) + '@tiptap/pm': + specifier: 2.1.13 + version: 2.1.13 + '@tiptap/react': + specifier: 2.1.13 + version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(react-dom@18.2.0)(react@18.2.0) + '@tiptap/starter-kit': + specifier: 2.1.13 + version: 2.1.13(@tiptap/pm@2.1.13) '@types/jest': specifier: ^26.0.15 version: 26.0.24 @@ -5690,6 +5711,34 @@ packages: reselect: 4.1.7 dev: false + /@remirror/core-constants@2.0.2: + resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==} + dev: false + + /@remirror/core-helpers@3.0.0: + resolution: {integrity: sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A==} + dependencies: + '@remirror/core-constants': 2.0.2 + '@remirror/types': 1.0.1 + '@types/object.omit': 3.0.3 + '@types/object.pick': 1.3.4 + '@types/throttle-debounce': 2.1.0 + case-anything: 2.1.13 + dash-get: 1.0.2 + deepmerge: 4.3.1 + fast-deep-equal: 3.1.3 + make-error: 1.3.6 + object.omit: 3.0.0 + object.pick: 1.3.0 + throttle-debounce: 3.0.1 + dev: false + + /@remirror/types@1.0.1: + resolution: {integrity: sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==} + dependencies: + type-fest: 2.19.0 + dev: false + /@rollup/plugin-babel@5.3.1(@babel/core@7.20.12)(rollup@2.79.1): resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -5968,6 +6017,273 @@ packages: '@testing-library/dom': 8.20.0 dev: false + /@tiptap/core@2.1.13(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-cMC8bgTN63dj1Mv82iDeeLl6sa9kY0Pug8LSalxVEptRmyFVsVxGgu2/6Y3T+9aCYScxfS06EkA8SdzFMAwYTQ==} + peerDependencies: + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/pm': 2.1.13 + dev: false + + /@tiptap/extension-blockquote@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-oe6wSQACmODugoP9XH3Ouffjy4BsOBWfTC+dETHNCG6ZED6ShHN3CB9Vr7EwwRgmm2WLaKAjMO1sVumwH+Z1rg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-bold@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-6cHsQTh/rUiG4jkbJer3vk7g60I5tBwEBSGpdxmEHh83RsvevD8+n92PjA24hYYte5RNlATB011E1wu8PVhSvw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-bubble-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-Hm7e1GX3AI6lfaUmr6WqsS9MMyXIzCkhh+VQi6K8jj4Q4s8kY4KPoAyD/c3v9pZ/dieUtm2TfqrOCkbHzsJQBg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + tippy.js: 6.3.7 + dev: false + + /@tiptap/extension-bullet-list@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-NkWlQ5bLPUlcROj6G/d4oqAxMf3j3wfndGOPp0z8OoXJtVbVoXl/aMSlLbVgE6n8r6CS8MYxKhXNxrb7Ll2foA==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-code-block@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-E3tweNExPOV+t1ODKX0MDVsS0aeHGWc1ECt+uyp6XwzsN0bdF2A5+pttQqM7sTcMnQkVACGFbn9wDeLRRcfyQg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + dev: false + + /@tiptap/extension-code@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-color@2.1.13(@tiptap/core@2.1.13)(@tiptap/extension-text-style@2.1.13): + resolution: {integrity: sha512-T3tJXCIfFxzIlGOhvbPVIZa3y36YZRPYIo2TKsgkTz8LiMob6hRXXNFjsrFDp2Fnu3DrBzyvrorsW7767s4eYg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/extension-text-style': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/extension-text-style': 2.1.13(@tiptap/core@2.1.13) + dev: false + + /@tiptap/extension-document@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-wLwiTWsVmZTGIE5duTcHRmW4ulVxNW4nmgfpk95+mPn1iKyNGtrVhGWleLhBlTj+DWXDtcfNWZgqZkZNzhkqYQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-dropcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-NAyJi4BJxH7vl/2LNS1X0ndwFKjEtX+cRgshXCnMyh7qNpIRW6Plczapc/W1OiMncOEhZJfpZfkRSfwG01FWFg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + dev: false + + /@tiptap/extension-floating-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-9Oz7pk1Nts2+EyY+rYfnREGbLzQ5UFazAvRhF6zAJdvyuDmAYm0Jp6s0GoTrpV0/dJEISoFaNpPdMJOb9EBNRw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + tippy.js: 6.3.7 + dev: false + + /@tiptap/extension-gapcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-Cl5apsoTcyPPCgE3ThufxQxZ1wyqqh+9uxUN9VF9AbeTkid6oPZvKXwaILf6AFnkSy+SuKrb9kZD2iaezxpzXw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + dev: false + + /@tiptap/extension-hard-break@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-TGkMzMQayuKg+vN4du0x1ahEItBLcCT1jdWeRsjdM8gHfzbPLdo4PQhVsvm1I0xaZmbJZelhnVsUwRZcIu1WNA==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-heading@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-PEmc19QLmlVUTiHWoF0hpgNTNPNU0nlaFmMKskzO+cx5Df4xvHmv/UqoIwp7/UFbPMkfVJT1ozQU7oD1IWn9Hg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-history@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-1ouitThGTBUObqw250aDwGLMNESBH5PRXIGybsCFO1bktdmWtEw7m72WY41EuX2BH8iKJpcYPerl3HfY1vmCNw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + dev: false + + /@tiptap/extension-horizontal-rule@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-7OgjgNqZXvBejgULNdMSma2M1nzv4bbZG+FT5XMFZmEOxR9IB1x/RzChjPdeicff2ZK2sfhMBc4Y9femF5XkUg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + dev: false + + /@tiptap/extension-italic@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-list-item@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-6e8iiCWXOiJTl1XOwVW2tc0YG18h70HUtEHFCx2m5HspOGFKsFEaSS3qYxOheM9HxlmQeDt8mTtqftRjEFRxPQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-ordered-list@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-UO4ZAL5Vrr1WwER5VjgmeNIWHpqy9cnIRo1En07gZ0OWTjs1eITPcu+4TCn1ZG6DhoFvAQzE5DTxxdhIotg+qw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-paragraph@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-cEoZBJrsQn69FPpUMePXG/ltGXtqKISgypj70PEHXt5meKDjpmMVSY4/8cXvFYEYsI9GvIwyAK0OrfAHiSoROA==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-strike@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-VN6zlaCNCbyJUCDyBFxavw19XmQ4LkCh8n20M8huNqW77lDGXA2A7UcWLHaNBpqAijBRu9mWI8l4Bftyf2fcAw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-text-style@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-K9/pNHxpZKQoc++crxrsppVUSeHv8YevfY2FkJ4YMaekGcX+q4BRrHR0tOfii4izAUPJF2L0/PexLQaWXtAY1w==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/extension-text@2.1.13(@tiptap/core@2.1.13): + resolution: {integrity: sha512-zzsTTvu5U67a8WjImi6DrmpX2Q/onLSaj+LRWPh36A1Pz2WaxW5asZgaS+xWCnR+UrozlCALWa01r7uv69jq0w==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + dev: false + + /@tiptap/pm@2.1.13: + resolution: {integrity: sha512-zNbA7muWsHuVg12GrTgN/j119rLePPq5M8dZgkKxUwdw8VmU3eUyBp1SihPEXJ2U0MGdZhNhFX7Y74g11u66sg==} + dependencies: + prosemirror-changeset: 2.2.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.5.2 + prosemirror-dropcursor: 1.8.1 + prosemirror-gapcursor: 1.3.2 + prosemirror-history: 1.3.2 + prosemirror-inputrules: 1.3.0 + prosemirror-keymap: 1.2.2 + prosemirror-markdown: 1.12.0 + prosemirror-menu: 1.2.4 + prosemirror-model: 1.19.4 + prosemirror-schema-basic: 1.2.2 + prosemirror-schema-list: 1.3.0 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.3.5 + prosemirror-trailing-node: 2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7) + prosemirror-transform: 1.8.0 + prosemirror-view: 1.32.7 + dev: false + + /@tiptap/react@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Dq3f8EtJnpImP3iDtJo+7bulnN9SJZRZcVVzxHXccLcC2MxtmDdlPGZjP+wxO800nd8toSIOd5734fPNf/YcfA==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/extension-bubble-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + '@tiptap/extension-floating-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + '@tiptap/pm': 2.1.13 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@tiptap/starter-kit@2.1.13(@tiptap/pm@2.1.13): + resolution: {integrity: sha512-ph/mUR/OwPtPkZ5rNHINxubpABn8fHnvJSdhXFrY/q6SKoaO11NZXgegRaiG4aL7O6Sz4LsZVw6Sm0Ae+GJmrg==} + dependencies: + '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/extension-blockquote': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-bold': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-bullet-list': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-code': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-code-block': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + '@tiptap/extension-document': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-dropcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + '@tiptap/extension-gapcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + '@tiptap/extension-hard-break': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-heading': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-history': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + '@tiptap/extension-horizontal-rule': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + '@tiptap/extension-italic': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-list-item': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-ordered-list': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-paragraph': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-strike': 2.1.13(@tiptap/core@2.1.13) + '@tiptap/extension-text': 2.1.13(@tiptap/core@2.1.13) + transitivePeerDependencies: + - '@tiptap/pm' + dev: false + /@tootallnate/once@1.1.2: resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -6276,6 +6592,14 @@ packages: /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + /@types/object.omit@3.0.3: + resolution: {integrity: sha512-xrq4bQTBGYY2cw+gV4PzoG2Lv3L0pjZ1uXStRRDQoATOYW1lCsFQHhQ+OkPhIcQoqLjAq7gYif7D14Qaa6Zbew==} + dev: false + + /@types/object.pick@1.3.4: + resolution: {integrity: sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==} + dev: false + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} @@ -6473,6 +6797,10 @@ packages: pretty-format: 25.5.0 dev: false + /@types/throttle-debounce@2.1.0: + resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==} + dev: false + /@types/triple-beam@1.3.2: resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} dev: false @@ -8839,6 +9167,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /case-anything@2.1.13: + resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} + engines: {node: '>=12.13'} + dev: false + /case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} @@ -9780,6 +10113,10 @@ packages: /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + /crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + dev: false + /cron-parser@3.5.0: resolution: {integrity: sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==} engines: {node: '>=0.8'} @@ -10174,6 +10511,10 @@ packages: engines: {node: '>=8'} dev: true + /dash-get@1.0.2: + resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} + dev: false + /dashdash@1.14.1: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} @@ -10945,6 +11286,11 @@ packages: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} dev: false + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + /env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -16314,6 +16660,12 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + dependencies: + uc.micro: 2.0.0 + dev: false + /load-json-file@1.1.0: resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==} engines: {node: '>=0.10.0'} @@ -16722,6 +17074,18 @@ packages: object-visit: 1.0.1 dev: false + /markdown-it@14.0.0: + resolution: {integrity: sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.0.0 + dev: false + /match-sorter@4.2.1: resolution: {integrity: sha512-s+3h9TiZU9U1pWhIERHf8/f4LmBN6IXaRgo2CI17+XGByGS1GvG5VvXK9pcGyCjGe3WM3mSYRC3ipGrd5UEVgw==} dependencies: @@ -16785,6 +17149,10 @@ packages: resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} dev: false + /mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -18069,6 +18437,13 @@ packages: make-iterator: 1.0.1 dev: false + /object.omit@3.0.0: + resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 1.0.1 + dev: false + /object.pick@1.3.0: resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} engines: {node: '>=0.10.0'} @@ -18245,6 +18620,10 @@ packages: readable-stream: 2.3.7 dev: false + /orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + dev: false + /os-browserify@0.3.0: resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} dev: true @@ -19825,6 +20204,149 @@ packages: resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==} dev: false + /prosemirror-changeset@2.2.1: + resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + dependencies: + prosemirror-transform: 1.8.0 + dev: false + + /prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + dependencies: + prosemirror-state: 1.4.3 + dev: false + + /prosemirror-commands@1.5.2: + resolution: {integrity: sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==} + dependencies: + prosemirror-model: 1.19.4 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.8.0 + dev: false + + /prosemirror-dropcursor@1.8.1: + resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==} + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.8.0 + prosemirror-view: 1.32.7 + dev: false + + /prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.19.4 + prosemirror-state: 1.4.3 + prosemirror-view: 1.32.7 + dev: false + + /prosemirror-history@1.3.2: + resolution: {integrity: sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==} + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.8.0 + prosemirror-view: 1.32.7 + rope-sequence: 1.3.4 + dev: false + + /prosemirror-inputrules@1.3.0: + resolution: {integrity: sha512-z1GRP2vhh5CihYMQYsJSa1cOwXb3SYxALXOIfAkX8nZserARtl9LiL+CEl+T+OFIsXc3mJIHKhbsmRzC0HDAXA==} + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.8.0 + dev: false + + /prosemirror-keymap@1.2.2: + resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + dev: false + + /prosemirror-markdown@1.12.0: + resolution: {integrity: sha512-6F5HS8Z0HDYiS2VQDZzfZP6A0s/I0gbkJy8NCzzDMtcsz3qrfqyroMMeoSjAmOhDITyon11NbXSzztfKi+frSQ==} + dependencies: + markdown-it: 14.0.0 + prosemirror-model: 1.19.4 + dev: false + + /prosemirror-menu@1.2.4: + resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.5.2 + prosemirror-history: 1.3.2 + prosemirror-state: 1.4.3 + dev: false + + /prosemirror-model@1.19.4: + resolution: {integrity: sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==} + dependencies: + orderedmap: 2.1.1 + dev: false + + /prosemirror-schema-basic@1.2.2: + resolution: {integrity: sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==} + dependencies: + prosemirror-model: 1.19.4 + dev: false + + /prosemirror-schema-list@1.3.0: + resolution: {integrity: sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==} + dependencies: + prosemirror-model: 1.19.4 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.8.0 + dev: false + + /prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + dependencies: + prosemirror-model: 1.19.4 + prosemirror-transform: 1.8.0 + prosemirror-view: 1.32.7 + dev: false + + /prosemirror-tables@1.3.5: + resolution: {integrity: sha512-JSZ2cCNlApu/ObAhdPyotrjBe2cimniniTpz60YXzbL0kZ+47nEYk2LWbfKU2lKpBkUNquta2PjteoNi4YCluQ==} + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.19.4 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.8.0 + prosemirror-view: 1.32.7 + dev: false + + /prosemirror-trailing-node@2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7): + resolution: {integrity: sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==} + peerDependencies: + prosemirror-model: ^1.19.0 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.31.2 + dependencies: + '@remirror/core-constants': 2.0.2 + '@remirror/core-helpers': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.19.4 + prosemirror-state: 1.4.3 + prosemirror-view: 1.32.7 + dev: false + + /prosemirror-transform@1.8.0: + resolution: {integrity: sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==} + dependencies: + prosemirror-model: 1.19.4 + dev: false + + /prosemirror-view@1.32.7: + resolution: {integrity: sha512-pvxiOoD4shW41X5bYDjRQk3DSG4fMqxh36yPMt7VYgU3dWRmqFzWJM/R6zeo1KtC8nyk717ZbQND3CC9VNeptw==} + dependencies: + prosemirror-model: 1.19.4 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.8.0 + dev: false + /proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} dev: true @@ -19979,6 +20501,11 @@ packages: pump: 2.0.1 dev: false + /punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + dev: false + /punycode@1.3.2: resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} dev: true @@ -21500,6 +22027,10 @@ packages: fsevents: 2.3.2 dev: false + /rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + dev: false + /rsvp@4.8.5: resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} engines: {node: 6.* || >= 7.*} @@ -23119,6 +23650,11 @@ packages: engines: {node: '>=8'} dev: false + /throttle-debounce@3.0.1: + resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} + engines: {node: '>=10'} + dev: false + /through2-filter@3.0.0: resolution: {integrity: sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==} dependencies: @@ -23185,6 +23721,12 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false + /tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + dependencies: + '@popperjs/core': 2.11.8 + dev: false + /tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} dependencies: @@ -23569,6 +24111,11 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + /type-is@1.6.15: resolution: {integrity: sha512-0uqZYZDiBICTVXEsNcDLueZLPgZ8FgGe8lmVDQ0FcVFUeaxsPbFWiz60ZChVw8VELIt7iGuCehOrZSYjYteWKQ==} engines: {node: '>= 0.6'} @@ -23661,6 +24208,10 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + /uc.micro@2.0.0: + resolution: {integrity: sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==} + dev: false + /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} @@ -24125,6 +24676,10 @@ packages: browser-process-hrtime: 1.0.0 dev: false + /w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + dev: false + /w3c-xmlserializer@1.1.2: resolution: {integrity: sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==} dependencies: From 2a85fe2f3ca203f18719065360cf1dbadac7579e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 29 Dec 2023 17:31:51 +0200 Subject: [PATCH 15/17] feat(webapp): the mail notifications dialogs --- .../EstimateMailDialogForm.tsx | 45 +++++++------ .../EstimateMailDialogFormContent.tsx | 8 ++- .../InvoiceMailDialogForm.tsx | 46 ++++++++------ .../InvoiceMailDialogFormContent.tsx | 8 ++- .../PaymentMailDialogForm.tsx | 47 ++++++++------ .../PaymentMailDialogFormContent.tsx | 8 ++- .../ReceiptMailDialogForm.tsx | 50 +++++++++------ .../ReceiptMailDialogFormContent.tsx | 8 ++- .../MailNotificationForm.tsx | 11 +++- .../SendMailNotification/RichEditor.tsx | 63 ------------------- .../containers/SendMailNotification/index.ts | 2 +- .../containers/SendMailNotification/utils.ts | 44 +++++++++++++ packages/webapp/src/hooks/query/receipts.tsx | 2 +- 13 files changed, 192 insertions(+), 150 deletions(-) delete mode 100644 packages/webapp/src/containers/SendMailNotification/RichEditor.tsx create mode 100644 packages/webapp/src/containers/SendMailNotification/utils.ts diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogForm.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogForm.tsx index 5f00c996e..f8811cdbb 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogForm.tsx @@ -1,27 +1,26 @@ // @ts-nocheck import { Formik } from 'formik'; import * as R from 'ramda'; -import { castArray } from 'lodash'; import { useEstimateMailDialogBoot } from './EstimateMailDialogBoot'; -import { transformToForm } from '@/utils'; import { DialogsName } from '@/constants/dialogs'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import { useSendSaleEstimateMail } from '@/hooks/query'; import { EstimateMailDialogFormContent } from './EstimateMailDialogFormContent'; +import { + initialMailNotificationValues, + MailNotificationFormValues, + transformMailFormToInitialValues, + transformMailFormToRequest, +} from '@/containers/SendMailNotification/utils'; +import { Intent } from '@blueprintjs/core'; +import { AppToaster } from '@/components'; const initialFormValues = { - from: [], - to: [], - subject: '', - body: '', + ...initialMailNotificationValues, attachEstimate: true, }; -interface EstimateMailFormValues { - from: string[]; - to: string[]; - subject: string; - body: string; +interface EstimateMailFormValues extends MailNotificationFormValues { attachEstimate: boolean; } @@ -32,21 +31,31 @@ function EstimateMailDialogFormRoot({ const { mutateAsync: sendEstimateMail } = useSendSaleEstimateMail(); const { mailOptions, saleEstimateId } = useEstimateMailDialogBoot(); - const initialValues = { - ...initialFormValues, - ...transformToForm(mailOptions, initialFormValues), - from: mailOptions.from ? castArray(mailOptions.from) : [], - to: mailOptions.to ? castArray(mailOptions.to) : [], - }; + const initialValues = transformMailFormToInitialValues( + mailOptions, + initialFormValues, + ); // Handle the form submitting. const handleSubmit = (values: EstimateMailFormValues, { setSubmitting }) => { + const reqValues = transformMailFormToRequest(values); + setSubmitting(true); - sendEstimateMail([saleEstimateId, values]) + sendEstimateMail([saleEstimateId, reqValues]) .then(() => { + AppToaster.show({ + message: 'The mail notification has been sent successfully.', + intent: Intent.SUCCESS, + }); + closeDialog(DialogsName.EstimateMail); setSubmitting(false); }) .catch((error) => { setSubmitting(false); + closeDialog(DialogsName.EstimateMail); + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); }); }; diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx index d299d5b16..668c7a4c9 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialogFormContent.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { FFormGroup, FSwitch } from '@/components'; import { MailNotificationForm } from '@/containers/SendMailNotification'; import { saveInvoke } from '@/utils'; +import { useEstimateMailDialogBoot } from './EstimateMailDialogBoot'; interface EstimateMailDialogFormContentProps { onClose?: () => void; @@ -14,6 +15,7 @@ export function EstimateMailDialogFormContent({ onClose, }: EstimateMailDialogFormContentProps) { const { isSubmitting } = useFormikContext(); + const { mailOptions } = useEstimateMailDialogBoot(); const handleClose = () => { saveInvoke(onClose); @@ -22,8 +24,10 @@ export function EstimateMailDialogFormContent({ return (
- - + diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx index 6aa62cfba..794ed890d 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx @@ -1,28 +1,27 @@ // @ts-nocheck import { Formik } from 'formik'; -import { castArray } from 'lodash'; import * as R from 'ramda'; +import { Intent } from '@blueprintjs/core'; import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot'; -import { transformToForm } from '@/utils'; import { DialogsName } from '@/constants/dialogs'; +import { AppToaster } from '@/components'; import { useSendSaleInvoiceMail } from '@/hooks/query'; -import { InvoiceMailDialogFormContent } from './InvoiceMailDialogFormContent'; import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { InvoiceMailDialogFormContent } from './InvoiceMailDialogFormContent'; import { InvoiceMailFormSchema } from './InvoiceMailDialogForm.schema'; +import { + MailNotificationFormValues, + initialMailNotificationValues, + transformMailFormToRequest, + transformMailFormToInitialValues, +} from '@/containers/SendMailNotification/utils'; const initialFormValues = { - from: [], - to: [], - subject: '', - body: '', + ...initialMailNotificationValues, attachInvoice: true, }; -interface InvoiceMailFormValues { - from: string[]; - to: string[]; - subject: string; - body: string; +interface InvoiceMailFormValues extends MailNotificationFormValues { attachInvoice: boolean; } @@ -33,20 +32,29 @@ function InvoiceMailDialogFormRoot({ const { mailOptions, saleInvoiceId } = useInvoiceMailDialogBoot(); const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail(); - const initialValues = { - ...initialFormValues, - ...transformToForm(mailOptions, initialFormValues), - from: mailOptions.from ? castArray(mailOptions.from) : [], - to: mailOptions.to ? castArray(mailOptions.to) : [], - }; + const initialValues = transformMailFormToInitialValues( + mailOptions, + initialFormValues, + ); // Handle the form submitting. const handleSubmit = (values: InvoiceMailFormValues, { setSubmitting }) => { + const reqValues = transformMailFormToRequest(values); + setSubmitting(true); - sendInvoiceMail([saleInvoiceId, values]) + sendInvoiceMail([saleInvoiceId, reqValues]) .then(() => { + AppToaster.show({ + message: 'The mail notification has been sent successfully.', + intent: Intent.SUCCESS, + }); + closeDialog(DialogsName.InvoiceMail); setSubmitting(false); }) .catch(() => { + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); setSubmitting(false); }); }; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx index 2038d5379..07e104027 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { FFormGroup, FSwitch } from '@/components'; import { MailNotificationForm } from '@/containers/SendMailNotification'; import { saveInvoke } from '@/utils'; +import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot'; interface SendMailNotificationFormProps { onClose?: () => void; @@ -14,6 +15,7 @@ export function InvoiceMailDialogFormContent({ onClose, }: SendMailNotificationFormProps) { const { isSubmitting } = useFormikContext(); + const { mailOptions } = useInvoiceMailDialogBoot(); const handleClose = () => { saveInvoke(onClose); @@ -22,8 +24,10 @@ export function InvoiceMailDialogFormContent({ return (
- - + diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx index 04906185d..075c2ee8b 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx @@ -1,27 +1,26 @@ // @ts-nocheck import { Formik, FormikBag } from 'formik'; -import { castArray } from 'lodash'; import * as R from 'ramda'; +import { Intent } from '@blueprintjs/core'; import { usePaymentMailDialogBoot } from './PaymentMailDialogBoot'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { useSendPaymentReceiveMail } from '@/hooks/query'; import { PaymentMailDialogFormContent } from './PaymentMailDialogFormContent'; -import { transformToForm } from '@/utils'; +import { + MailNotificationFormValues, + initialMailNotificationValues, + transformMailFormToRequest, + transformMailFormToInitialValues, +} from '@/containers/SendMailNotification/utils'; +import { AppToaster } from '@/components'; const initialFormValues = { - from: [], - to: [], - subject: '', - body: '', + ...initialMailNotificationValues, attachPayment: true, }; -interface PaymentMailFormValue { - from: string[]; - to: string[]; - subject: string; - body: string; +interface PaymentMailFormValue extends MailNotificationFormValues { attachPayment: boolean; } @@ -32,24 +31,34 @@ export function PaymentMailDialogFormRoot({ const { mailOptions, paymentId } = usePaymentMailDialogBoot(); const { mutateAsync: sendPaymentMail } = useSendPaymentReceiveMail(); - const initialValues = { - ...initialFormValues, - ...transformToForm(mailOptions, initialFormValues), - from: mailOptions.from ? castArray(mailOptions.from) : [], - to: mailOptions.to ? castArray(mailOptions.to) : [], - }; + const initialValues = transformMailFormToInitialValues( + mailOptions, + initialFormValues, + ); // Handles the form submitting. const handleSubmit = ( values: PaymentMailFormValue, { setSubmitting }: FormikBag, ) => { + const reqValues = transformMailFormToRequest(values); + setSubmitting(true); - sendPaymentMail([paymentId, values]) + sendPaymentMail([paymentId, reqValues]) .then(() => { + AppToaster.show({ + message: 'The mail notification has been sent successfully.', + intent: Intent.SUCCESS, + }); setSubmitting(false); + closeDialog(DialogsName.PaymentMail); }) - .catch((error) => { + .catch(() => { + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); setSubmitting(false); + closeDialog(DialogsName.PaymentMail); }); }; diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx index 172494c40..5a04f0f28 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogFormContent.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { FFormGroup, FSwitch } from '@/components'; import { MailNotificationForm } from '@/containers/SendMailNotification'; import { saveInvoke } from '@/utils'; +import { usePaymentMailDialogBoot } from './PaymentMailDialogBoot'; interface PaymentMailDialogFormContentProps { onClose?: () => void; @@ -13,6 +14,7 @@ interface PaymentMailDialogFormContentProps { export function PaymentMailDialogFormContent({ onClose, }: PaymentMailDialogFormContentProps) { + const { mailOptions } = usePaymentMailDialogBoot(); const { isSubmitting } = useFormikContext(); const handleClose = () => { @@ -22,8 +24,10 @@ export function PaymentMailDialogFormContent({ return (
- - + diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx index 2d5a3bcf1..fb9b845af 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogForm.tsx @@ -1,26 +1,25 @@ // @ts-nocheck import { Formik, FormikBag } from 'formik'; -import { castArray } from 'lodash'; import * as R from 'ramda'; +import { Intent } from '@blueprintjs/core'; import { useReceiptMailDialogBoot } from './ReceiptMailDialogBoot'; -import { transformToForm } from '@/utils'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { useSendSaleReceiptMail } from '@/hooks/query'; import { ReceiptMailDialogFormContent } from './ReceiptMailDialogFormContent'; +import { + initialMailNotificationValues, + MailNotificationFormValues, + transformMailFormToInitialValues, + transformMailFormToRequest, +} from '@/containers/SendMailNotification/utils'; +import { AppToaster } from '@/components'; const initialFormValues = { - from: [], - to: [], - subject: '', - body: '', + ...initialMailNotificationValues, attachReceipt: true, }; -interface ReceiptMailFormValues { - from: string[]; - to: string[]; - subject: string; - body: string; +interface ReceiptMailFormValues extends MailNotificationFormValues { attachReceipt: boolean; } @@ -28,26 +27,37 @@ function ReceiptMailDialogFormRoot({ closeDialog }) { const { mailOptions, saleReceiptId } = useReceiptMailDialogBoot(); const { mutateAsync: sendReceiptMail } = useSendSaleReceiptMail(); - const initialValues = { - ...initialFormValues, - ...transformToForm(mailOptions, initialFormValues), - from: mailOptions.from ? castArray(mailOptions.from) : [], - to: mailOptions.to ? castArray(mailOptions.to) : [], - }; + // Transformes mail options to initial form values. + const initialValues = transformMailFormToInitialValues( + mailOptions, + initialFormValues, + ); + // Handle the form submitting. const handleSubmit = ( values: ReceiptMailFormValues, { setSubmitting }: FormikBag, ) => { + const reqValues = transformMailFormToRequest(values); + setSubmitting(true); - sendReceiptMail([saleReceiptId, values]) + sendReceiptMail([saleReceiptId, reqValues]) .then(() => { + AppToaster.show({ + message: 'The mail notification has been sent successfully.', + intent: Intent.SUCCESS, + }); + closeDialog(DialogsName.ReceiptMail); setSubmitting(false); }) - .catch((error) => { + .catch(() => { + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); setSubmitting(false); }); }; - + // Handle the close button click. const handleClose = () => { closeDialog(DialogsName.ReceiptMail); }; diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx index 381160f09..d824d35af 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialogFormContent.tsx @@ -4,6 +4,7 @@ import { Button, Classes, Intent } from '@blueprintjs/core'; import styled from 'styled-components'; import { FFormGroup, FSwitch } from '@/components'; import { MailNotificationForm } from '@/containers/SendMailNotification'; +import { useReceiptMailDialogBoot } from './ReceiptMailDialogBoot'; import { saveInvoke } from '@/utils'; interface SendMailNotificationFormProps { @@ -13,6 +14,7 @@ interface SendMailNotificationFormProps { export function ReceiptMailDialogFormContent({ onClose, }: SendMailNotificationFormProps) { + const { mailOptions } = useReceiptMailDialogBoot(); const { isSubmitting } = useFormikContext(); const handleClose = () => { @@ -22,8 +24,10 @@ export function ReceiptMailDialogFormContent({ return (
- - + diff --git a/packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx b/packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx index 6b5053dd5..b7e578b91 100644 --- a/packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx +++ b/packages/webapp/src/containers/SendMailNotification/MailNotificationForm.tsx @@ -16,6 +16,14 @@ interface MailNotificationFormProps { toAddresses: SelectOptionProps[]; } +const commonAddressSelect = { + placeholder: '', + labelAccessor: '', + valueAccessor: 'mail', + tagAccessor: (item) => `<${item.label}> (${item.mail})`, + textAccessor: (item) => `<${item.label}> (${item.mail})`, +}; + export function MailNotificationForm({ fromAddresses, toAddresses, @@ -38,12 +46,12 @@ export function MailNotificationForm({ @@ -57,6 +65,7 @@ export function MailNotificationForm({ tagProps: { round: true, minimal: true, large: true }, }} fill={true} + {...commonAddressSelect} /> diff --git a/packages/webapp/src/containers/SendMailNotification/RichEditor.tsx b/packages/webapp/src/containers/SendMailNotification/RichEditor.tsx deleted file mode 100644 index e540fd6da..000000000 --- a/packages/webapp/src/containers/SendMailNotification/RichEditor.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// @ts-nocheck -import './styles.scss'; -import { Color } from '@tiptap/extension-color'; -import ListItem from '@tiptap/extension-list-item'; -import TextStyle from '@tiptap/extension-text-style'; -import { EditorProvider } from '@tiptap/react'; -import StarterKit from '@tiptap/starter-kit'; -import { Box } from '@/components'; -import styled from 'styled-components'; -import { useUncontrolled } from '@/hooks/useUncontrolled'; - -const extensions = [ - Color.configure({ types: [TextStyle.name, ListItem.name] }), - TextStyle.configure({ types: [ListItem.name] }), - StarterKit.configure({ - bulletList: { - keepMarks: true, - keepAttributes: false, - }, - orderedList: { - keepMarks: true, - keepAttributes: false, - }, - }), -]; - -export interface RichEditorProps { - value?: string; - initialValue?: string; - onChange?: (value: string) => void; - className?: string; -} -export const RichEditor = ({ - value, - initialValue, - onChange, - className, -}: RichEditorProps) => { - const [content, handleChange] = useUncontrolled({ - value, - initialValue, - finalValue: '', - onChange, - }); - - return ( - - - - ); -}; - -const Root = styled(Box)` - padding: 15px; - border: 1px solid #dedfe9; - border-top: 0; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; -`; diff --git a/packages/webapp/src/containers/SendMailNotification/index.ts b/packages/webapp/src/containers/SendMailNotification/index.ts index 6e5788dcc..5662fe7c9 100644 --- a/packages/webapp/src/containers/SendMailNotification/index.ts +++ b/packages/webapp/src/containers/SendMailNotification/index.ts @@ -1 +1 @@ -export * from './SendMailNotificationForm'; \ No newline at end of file +export * from './MailNotificationForm'; \ No newline at end of file diff --git a/packages/webapp/src/containers/SendMailNotification/utils.ts b/packages/webapp/src/containers/SendMailNotification/utils.ts new file mode 100644 index 000000000..59d0f6420 --- /dev/null +++ b/packages/webapp/src/containers/SendMailNotification/utils.ts @@ -0,0 +1,44 @@ +import { castArray, first } from 'lodash'; +import { transformToForm } from '@/utils'; + +export const initialMailNotificationValues = { + from: [], + to: [], + subject: '', + body: '', +}; + +export interface MailNotificationFormValues { + from: string[]; + to: string[]; + subject: string; + body: string; +} + +export const transformMailFormToRequest = ( + values: MailNotificationFormValues, +) => { + return { + ...values, + from: first(values.from), + to: values.to?.join(', '), + }; +}; + +/** + * Transformes the mail options response values to form initial values. + * @param {any} mailOptions + * @param {MailNotificationFormValues} initialValues + * @returns {MailNotificationFormValues} + */ +export const transformMailFormToInitialValues = ( + mailOptions: any, + initialValues: MailNotificationFormValues, +): MailNotificationFormValues => { + return { + ...initialValues, + ...transformToForm(mailOptions, initialValues), + from: mailOptions.from ? castArray(mailOptions.from) : [], + to: mailOptions.to ? castArray(mailOptions.to) : [], + }; +}; diff --git a/packages/webapp/src/hooks/query/receipts.tsx b/packages/webapp/src/hooks/query/receipts.tsx index 60b309589..7a6ae2ce9 100644 --- a/packages/webapp/src/hooks/query/receipts.tsx +++ b/packages/webapp/src/hooks/query/receipts.tsx @@ -216,7 +216,7 @@ export function useSendSaleReceiptMail(props) { const apiRequest = useApiRequest(); return useMutation( - (id, values) => apiRequest.post(`sales/receipts/${id}/mail`, values), + ([id, values]) => apiRequest.post(`sales/receipts/${id}/mail`, values), { onSuccess: () => { // Invalidate queries. From 0d15c16d40b731cba142d2f2cbc915df32294b9d Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 29 Dec 2023 17:35:34 +0200 Subject: [PATCH 16/17] feat(server): contact mail notification service --- .../api/controllers/Sales/SalesInvoices.ts | 16 ++- packages/server/src/config/index.ts | 1 + packages/server/src/interfaces/SaleInvoice.ts | 14 ++- packages/server/src/models/Customer.ts | 16 +++ .../ContactMailNotification.ts | 78 ++++++++++++ .../src/services/MailTenancy/MailTenancy.ts | 25 ++++ .../Estimates/SaleEstimatesApplication.ts | 2 +- .../Sales/Estimates/SendSaleEstimateMail.ts | 114 +++++++++-------- .../Sales/Invoices/SaleInvoicesApplication.ts | 14 +-- .../Invoices/SendInvoiceInvoiceMailCommon.ts | 115 ++++++++++++++++++ .../Sales/Invoices/SendSaleInvoiceMail.ts | 107 ++++------------ .../Invoices/SendSaleInvoiceMailReminder.ts | 105 ++++------------ .../src/services/Sales/Invoices/constants.ts | 10 ++ .../PaymentReceiveMailNotification.ts | 50 ++++---- .../PaymentReceivesApplication.ts | 5 +- .../Sales/Receipts/SaleReceiptApplication.ts | 8 +- .../Receipts/SaleReceiptMailNotification.ts | 71 ++++++----- 17 files changed, 441 insertions(+), 310 deletions(-) create mode 100644 packages/server/src/services/MailNotification/ContactMailNotification.ts create mode 100644 packages/server/src/services/MailTenancy/MailTenancy.ts create mode 100644 packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index cd7f5bfec..9e5ac8d25 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -696,7 +696,10 @@ export default class SaleInvoicesController extends BaseController { invoiceId, invoiceMailDTO ); - return res.status(200).send({}); + return res.status(200).send({ + code: 200, + message: 'The sale invoice mail has been sent successfully.', + }); } catch (error) { next(error); } @@ -717,18 +720,18 @@ export default class SaleInvoicesController extends BaseController { const { id: invoiceId } = req.params; try { - await this.saleInvoiceApplication.getSaleInvoiceMailReminder( + const data = await this.saleInvoiceApplication.getSaleInvoiceMailReminder( tenantId, invoiceId ); - return res.status(200).send({}); + return res.status(200).send(data); } catch (error) { next(error); } } /** - * + * Sends mail invoice of the given sale invoice. * @param {Request} req * @param {Response} res * @param {NextFunction} next @@ -749,7 +752,10 @@ export default class SaleInvoicesController extends BaseController { invoiceId, invoiceMailDTO ); - return res.status(200).send({}); + return res.status(200).send({ + code: 200, + message: 'The sale invoice mail reminder has been sent successfully.', + }); } catch (error) { next(error); } diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index bc6833130..0dc9d9676 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -58,6 +58,7 @@ module.exports = { secure: !!parseInt(process.env.MAIL_SECURE, 10), username: process.env.MAIL_USERNAME, password: process.env.MAIL_PASSWORD, + from: process.env.MAIL_FROM_ADDRESS, }, /** diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 7d7633b87..d660b17ca 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -1,5 +1,5 @@ import { Knex } from 'knex'; -import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces'; +import { ISystemUser, IAccount, ITaxTransaction, AddressItem } from '@/interfaces'; import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; @@ -187,8 +187,18 @@ export enum SaleInvoiceAction { NotifyBySms = 'NotifyBySms', } +export interface SaleInvoiceMailOptions { + toAddresses: AddressItem[]; + fromAddresses: AddressItem[]; + from: string; + to: string | string[]; + subject: string; + body: string; + attachInvoice: boolean; +} + export interface SendInvoiceMailDTO { - to: string; + to: string | string[]; from: string; subject: string; body: string; diff --git a/packages/server/src/models/Customer.ts b/packages/server/src/models/Customer.ts index 690b77d55..631763b71 100644 --- a/packages/server/src/models/Customer.ts +++ b/packages/server/src/models/Customer.ts @@ -24,6 +24,9 @@ export default class Customer extends mixin(TenantModel, [ CustomViewBaseModel, ModelSearchable, ]) { + email: string; + displayName: string; + /** * Query builder. */ @@ -76,6 +79,19 @@ export default class Customer extends mixin(TenantModel, [ return 'debit'; } + /** + * + */ + get contactAddresses() { + return [ + { + mail: this.email, + label: this.displayName, + primary: true + }, + ].filter((c) => c.mail); + } + /** * Model modifiers. */ diff --git a/packages/server/src/services/MailNotification/ContactMailNotification.ts b/packages/server/src/services/MailNotification/ContactMailNotification.ts new file mode 100644 index 000000000..21c745a96 --- /dev/null +++ b/packages/server/src/services/MailNotification/ContactMailNotification.ts @@ -0,0 +1,78 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { SaleInvoiceMailOptions } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { MailTenancy } from '@/services/MailTenancy/MailTenancy'; +import { formatSmsMessage } from '@/utils'; + +@Service() +export class ContactMailNotification { + @Inject() + private mailTenancy: MailTenancy; + + @Inject() + private tenancy: HasTenancyService; + + /** + * Parses the default message options. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public async getDefaultMailOptions( + tenantId: number, + contactId: number, + subject: string = '', + body: string = '' + ): Promise { + const { Contact, Customer } = this.tenancy.models(tenantId); + const contact = await Customer.query().findById(contactId).throwIfNotFound(); + + const toAddresses = contact.contactAddresses; + const fromAddresses = await this.mailTenancy.senders(tenantId); + + const toAddress = toAddresses.find((a) => a.primary); + const fromAddress = fromAddresses.find((a) => a.primary); + + const to = toAddress?.mail || ''; + const from = fromAddress?.mail || ''; + + return { + subject, + body, + to, + from, + fromAddresses, + toAddresses, + }; + } + + /** + * Retrieves the mail options. + * @param {number} + * @param {number} invoiceId + * @returns {} + */ + public async getMailOptions( + tenantId: number, + contactId: number, + defaultSubject?: string, + defaultBody?: string, + formatterData?: Record + ): Promise { + const mailOpts = await this.getDefaultMailOptions( + tenantId, + contactId, + defaultSubject, + defaultBody + ); + const subject = formatSmsMessage(mailOpts.subject, formatterData); + const body = formatSmsMessage(mailOpts.body, formatterData); + + return { + ...mailOpts, + subject, + body, + }; + } +} diff --git a/packages/server/src/services/MailTenancy/MailTenancy.ts b/packages/server/src/services/MailTenancy/MailTenancy.ts new file mode 100644 index 000000000..6f8e82e11 --- /dev/null +++ b/packages/server/src/services/MailTenancy/MailTenancy.ts @@ -0,0 +1,25 @@ +import config from '@/config'; +import { Tenant } from "@/system/models"; +import { Service } from 'typedi'; + + +@Service() +export class MailTenancy { + /** + * Retrieves the senders mails of the given tenant. + * @param {number} tenantId + */ + public async senders(tenantId: number) { + const tenant = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return [ + { + mail: config.mail.from, + label: tenant.metadata.name, + primary: true, + } + ].filter((item) => item.mail) + } +} \ No newline at end of file diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 68cd8e601..1ceb2bbcc 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -240,7 +240,7 @@ export class SaleEstimatesApplication { * @returns {} */ public getSaleEstimateMail(tenantId: number, saleEstimateId: number) { - return this.sendEstimateMailService.getDefaultMailOpts( + return this.sendEstimateMailService.getMailOptions( tenantId, saleEstimateId ); diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts index 9ae6fa2f8..5777ab55a 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -1,5 +1,4 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { @@ -8,28 +7,31 @@ import { } from './constants'; import { SaleEstimatesPdf } from './SaleEstimatesPdf'; import { GetSaleEstimate } from './GetSaleEstimate'; -import { formatSmsMessage } from '@/utils'; import { SaleEstimateMailOptions } from '@/interfaces'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; @Service() export class SendSaleEstimateMail { @Inject() private tenancy: HasTenancyService; - @Inject('agenda') - private agenda: any; - @Inject() private estimatePdf: SaleEstimatesPdf; @Inject() private getSaleEstimateService: GetSaleEstimate; + @Inject() + private contactMailNotification: ContactMailNotification; + + @Inject('agenda') + private agenda: any; + /** * Triggers the reminder mail of the given sale estimate. - * @param {number} tenantId - * @param {number} saleEstimateId - * @param {SaleEstimateMailOptions} messageOptions + * @param {number} tenantId - + * @param {number} saleEstimateId - + * @param {SaleEstimateMailOptions} messageOptions - */ public async triggerMail( tenantId: number, @@ -50,51 +52,50 @@ export class SendSaleEstimateMail { * @param {number} estimateId * @param {string} text */ - public formatText = async ( - tenantId: number, - estimateId: number, - text: string - ) => { + public formatterData = async (tenantId: number, estimateId: number) => { const estimate = await this.getSaleEstimateService.getEstimate( tenantId, estimateId ); - return formatSmsMessage(text, { + return { CustomerName: estimate.customer.displayName, EstimateNumber: estimate.estimateNumber, EstimateDate: estimate.formattedEstimateDate, EstimateAmount: estimate.formattedAmount, EstimateExpirationDate: estimate.formattedExpirationDate, - }); - }; - - /** - * Retrieves the default mail options. - * @param {number} tenantId - * @param {number} saleEstimateId - * @returns {Promise} - */ - public getDefaultMailOpts = async ( - tenantId: number, - saleEstimateId: number - ) => { - const { SaleEstimate } = this.tenancy.models(tenantId); - - const saleEstimate = await SaleEstimate.query() - .findById(saleEstimateId) - .withGraphFetched('customer') - .throwIfNotFound(); - - return { - attachPdf: true, - subject: DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, - body: DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, - to: saleEstimate.customer.email, }; }; /** - * Sends the mail. + * Retrieves the mail options. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns + */ + public getMailOptions = async (tenantId: number, saleEstimateId: number) => { + const { SaleEstimate } = this.tenancy.models(tenantId); + + const saleEstimate = await SaleEstimate.query() + .findById(saleEstimateId) + .throwIfNotFound(); + + const formatterData = await this.formatterData(tenantId, saleEstimateId); + + const mailOptions = await this.contactMailNotification.getMailOptions( + tenantId, + saleEstimate.customerId, + DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, + DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + formatterData + ); + return { + ...mailOptions, + data: formatterData, + }; + }; + + /** + * Sends the mail notification of the given sale estimate. * @param {number} tenantId * @param {number} saleEstimateId * @param {SaleEstimateMailOptions} messageOptions @@ -104,34 +105,31 @@ export class SendSaleEstimateMail { saleEstimateId: number, messageOptions: SaleEstimateMailOptions ) { - const defaultMessageOpts = await this.getDefaultMailOpts( + const localMessageOpts = await this.getMailOptions( tenantId, saleEstimateId ); - const parsedMessageOpts = { - ...defaultMessageOpts, + const messageOpts = { + ...localMessageOpts, ...messageOptions, }; - const formatter = R.curry(this.formatText)(tenantId, saleEstimateId); - const subject = await formatter(parsedMessageOpts.subject); - const body = await formatter(parsedMessageOpts.body); - const attachments = []; + const mail = new Mail() + .setSubject(messageOpts.subject) + .setTo(messageOpts.to) + .setContent(messageOpts.body); - if (parsedMessageOpts.to) { + if (messageOpts.to) { const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf( tenantId, saleEstimateId ); - attachments.push({ - filename: 'estimate.pdf', - content: estimatePdfBuffer, - }); + mail.setAttachments([ + { + filename: messageOpts.data?.EstimateNumber || 'estimate.pdf', + content: estimatePdfBuffer, + }, + ]); } - await new Mail() - .setSubject(subject) - .setTo(parsedMessageOpts.to) - .setContent(body) - .setAttachments(attachments) - .send(); + await mail.send(); } } diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index 9b3c19d33..b175d9546 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -300,10 +300,7 @@ export class SaleInvoiceApplication { * @returns {} */ public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { - return this.getSaleInvoiceReminderService.getInvoiceMailReminder( - tenantId, - saleInvoiceId - ); + return this.sendInvoiceReminderService.getMailOpts(tenantId, saleInvoiceId); } /** @@ -345,14 +342,11 @@ export class SaleInvoiceApplication { /** * Retrieves the default mail options of the given sale invoice. - * @param {number} tenantId - * @param {number} saleInvoiceid + * @param {number} tenantId + * @param {number} saleInvoiceid * @returns {Promise} */ public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) { - return this.sendInvoiceReminderService.getDefaultMailOpts( - tenantId, - saleInvoiceid - ); + return this.sendSaleInvoiceMailService.getMailOpts(tenantId, saleInvoiceid); } } diff --git a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts new file mode 100644 index 000000000..97be8de87 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts @@ -0,0 +1,115 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { SaleInvoiceMailOptions } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { + DEFAULT_INVOICE_MAIL_CONTENT, + DEFAULT_INVOICE_MAIL_SUBJECT, +} from './constants'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { Tenant } from '@/system/models'; +import { ServiceError } from '@/exceptions'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; + +@Service() +export class SendSaleInvoiceMailCommon { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private getSaleInvoiceService: GetSaleInvoice; + + @Inject() + private contactMailNotification: ContactMailNotification; + + /** + * Retrieves the mail options. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Invoice id. + * @param {string} defaultSubject - Subject text. + * @param {string} defaultBody - Subject body. + * @returns {} + */ + public async getMailOpts( + tenantId: number, + invoiceId: number, + defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT, + defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findById(invoiceId) + .throwIfNotFound(); + + const formatterData = await this.formatText(tenantId, invoiceId); + + return this.contactMailNotification.getMailOptions( + tenantId, + saleInvoice.customerId, + defaultSubject, + defaultBody, + formatterData + ); + } + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Sale invoice id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public formatText = async ( + tenantId: number, + invoiceId: number + ): Promise> => { + const invoice = await this.getSaleInvoiceService.getSaleInvoice( + tenantId, + invoiceId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return { + CompanyName: organization.metadata.name, + CustomerName: invoice.customer.displayName, + InvoiceNumber: invoice.invoiceNo, + InvoiceDueAmount: invoice.dueAmountFormatted, + InvoiceDueDate: invoice.dueDateFormatted, + InvoiceDate: invoice.invoiceDateFormatted, + InvoiceAmount: invoice.totalFormatted, + OverdueDays: invoice.overdueDays, + }; + }; + + /** + * Validates the mail notification options before sending it. + * @param {Partial} mailNotificationOpts + * @throws {ServiceError} + */ + public validateMailNotification( + mailNotificationOpts: Partial + ) { + if (isEmpty(mailNotificationOpts.from)) { + throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND); + } + if (isEmpty(mailNotificationOpts.to)) { + throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND); + } + if (isEmpty(mailNotificationOpts.subject)) { + throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND); + } + if (isEmpty(mailNotificationOpts.body)) { + throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND); + } + } +} + +const ERRORS = { + MAIL_FROM_NOT_FOUND: 'Mail from address not found', + MAIL_TO_NOT_FOUND: 'Mail to address not found', + MAIL_SUBJECT_NOT_FOUND: 'Mail subject not found', + MAIL_BODY_NOT_FOUND: 'Mail body not found', +}; diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index cc8269c56..a5bc744e1 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -1,29 +1,20 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; -import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SendInvoiceMailDTO } from '@/interfaces'; import { SaleInvoicePdf } from './SaleInvoicePdf'; +import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon'; import { DEFAULT_INVOICE_MAIL_CONTENT, DEFAULT_INVOICE_MAIL_SUBJECT, - ERRORS, } from './constants'; -import { ServiceError } from '@/exceptions'; -import { formatSmsMessage } from '@/utils'; -import { GetSaleInvoice } from './GetSaleInvoice'; -import { Tenant } from '@/system/models'; @Service() export class SendSaleInvoiceMail { @Inject() - private tenancy: HasTenancyService; + private invoicePdf: SaleInvoicePdf; @Inject() - private getSaleInvoiceService: GetSaleInvoice; - - @Inject() - private invoicePdf: SaleInvoicePdf; + private invoiceMail: SendSaleInvoiceMailCommon; @Inject('agenda') private agenda: any; @@ -48,56 +39,19 @@ export class SendSaleInvoiceMail { } /** - * Retrieves the default invoice mail options. + * Retrieves the mail options of the given sale invoice. * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} saleInvoiceId + * @returns {Promise} */ - public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { - const { SaleInvoice } = this.tenancy.models(tenantId); - const saleInvoice = await SaleInvoice.query() - .findById(invoiceId) - .withGraphFetched('customer') - .throwIfNotFound(); - - return { - attachInvoice: true, - subject: DEFAULT_INVOICE_MAIL_SUBJECT, - body: DEFAULT_INVOICE_MAIL_CONTENT, - to: saleInvoice.customer.email, - }; - }; - - /** - * Retrieves the formatted text of the given sale invoice. - * @param {number} tenantId - Tenant id. - * @param {number} invoiceId - Sale invoice id. - * @param {string} text - The given text. - * @returns {Promise} - */ - public textFormatter = async ( - tenantId: number, - invoiceId: number, - text: string - ): Promise => { - const invoice = await this.getSaleInvoiceService.getSaleInvoice( + public async getMailOpts(tenantId: number, saleInvoiceId: number) { + return this.invoiceMail.getMailOpts( tenantId, - invoiceId + saleInvoiceId, + DEFAULT_INVOICE_MAIL_SUBJECT, + DEFAULT_INVOICE_MAIL_CONTENT ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - - return formatSmsMessage(text, { - CompanyName: organization.metadata.name, - CustomerName: invoice.customer.displayName, - InvoiceNumber: invoice.invoiceNo, - InvoiceDueAmount: invoice.dueAmountFormatted, - InvoiceDueDate: invoice.dueDateFormatted, - InvoiceDate: invoice.invoiceDateFormatted, - InvoiceAmount: invoice.totalFormatted, - }); - }; + } /** * Triggers the mail invoice. @@ -111,37 +65,30 @@ export class SendSaleInvoiceMail { saleInvoiceId: number, messageDTO: SendInvoiceMailDTO ) { - const defaultMessageOpts = await this.getDefaultMailOpts( - tenantId, - saleInvoiceId - ); + const defaultMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); + // Parsed message opts with default options. - const parsedMessageOpts = { + const messageOpts = { ...defaultMessageOpts, ...messageDTO, }; - // In case there is no email address from the customer or from options, throw an error. - if (!parsedMessageOpts.to) { - throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); - } - const formatter = R.curry(this.textFormatter)(tenantId, saleInvoiceId); - const subject = await formatter(parsedMessageOpts.subject); - const body = await formatter(parsedMessageOpts.body); - const attachments = []; + this.invoiceMail.validateMailNotification(messageOpts); - if (parsedMessageOpts.attachInvoice) { + const mail = new Mail() + .setSubject(messageOpts.subject) + .setTo(messageOpts.to) + .setContent(messageOpts.body); + + if (messageOpts.attachInvoice) { // Retrieves document buffer of the invoice pdf document. const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( tenantId, saleInvoiceId ); - attachments.push({ filename: 'invoice.pdf', content: invoicePdfBuffer }); + mail.setAttachments([ + { filename: 'invoice.pdf', content: invoicePdfBuffer }, + ]); } - await new Mail() - .setSubject(subject) - .setTo(parsedMessageOpts.to) - .setContent(body) - .setAttachments(attachments) - .send(); + await mail.send(); } } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index 886916260..09463bddc 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -1,24 +1,15 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SaleInvoicePdf } from './SaleInvoicePdf'; +import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon'; import { DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, - ERRORS, } from './constants'; -import { SaleInvoicePdf } from './SaleInvoicePdf'; -import { ServiceError } from '@/exceptions'; -import { GetSaleInvoice } from './GetSaleInvoice'; -import { Tenant } from '@/system/models'; -import { formatSmsMessage } from '@/utils'; @Service() export class SendInvoiceMailReminder { - @Inject() - private tenancy: HasTenancyService; - @Inject('agenda') private agenda: any; @@ -26,7 +17,7 @@ export class SendInvoiceMailReminder { private invoicePdf: SaleInvoicePdf; @Inject() - private getSaleInvoiceService: GetSaleInvoice; + private invoiceCommonMail: SendSaleInvoiceMailCommon; /** * Triggers the reminder mail of the given sale invoice. @@ -47,57 +38,19 @@ export class SendInvoiceMailReminder { } /** - * Parses the default message options. + * Retrieves the mail options of the given sale invoice. * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} saleInvoiceId + * @returns {Promise} */ - public async getDefaultMailOpts(tenantId: number, invoiceId: number) { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const saleInvoice = await SaleInvoice.query() - .findById(invoiceId) - .withGraphFetched('customer') - .throwIfNotFound(); - - return { - attachInvoice: true, - subject: DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, - body: DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, - to: saleInvoice.customer.email, - }; - } - - /** - * Retrieves the formatted text of the given sale invoice. - * @param {number} tenantId - Tenant id. - * @param {number} invoiceId - Sale invoice id. - * @param {string} text - The given text. - * @returns {Promise} - */ - public formatText = async ( - tenantId: number, - invoiceId: number, - text: string - ): Promise => { - const invoice = await this.getSaleInvoiceService.getSaleInvoice( + public async getMailOpts(tenantId: number, saleInvoiceId: number) { + return this.invoiceCommonMail.getMailOpts( tenantId, - invoiceId + saleInvoiceId, + DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, + DEFAULT_INVOICE_REMINDER_MAIL_CONTENT ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - - return formatSmsMessage(text, { - CompanyName: organization.metadata.name, - CustomerName: invoice.customer.displayName, - InvoiceNumber: invoice.invoiceNo, - InvoiceDueAmount: invoice.dueAmountFormatted, - InvoiceDueDate: invoice.dueDateFormatted, - InvoiceDate: invoice.invoiceDateFormatted, - InvoiceAmount: invoice.totalFormatted, - }); - }; + } /** * Triggers the mail invoice. @@ -111,37 +64,27 @@ export class SendInvoiceMailReminder { saleInvoiceId: number, messageOptions: SendInvoiceMailDTO ) { - const defaultMessageOpts = await this.getDefaultMailOpts( - tenantId, - saleInvoiceId - ); - const parsedMessageOptions = { - ...defaultMessageOpts, + const localMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); + + const messageOpts = { + ...localMessageOpts, ...messageOptions, }; - // In case there is no email address from the customer or from options, throw an error. - if (!parsedMessageOptions.to) { - throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); - } - const formatter = R.curry(this.formatText)(tenantId, saleInvoiceId); - const subject = await formatter(parsedMessageOptions.subject); - const body = await formatter(parsedMessageOptions.body); - const attachments = []; + const mail = new Mail() + .setSubject(messageOpts.subject) + .setTo(messageOpts.to) + .setContent(messageOpts.body); - if (parsedMessageOptions.attachInvoice) { + if (messageOpts.attachInvoice) { // Retrieves document buffer of the invoice pdf document. const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( tenantId, saleInvoiceId ); - attachments.push({ filename: 'invoice.pdf', content: invoicePdfBuffer }); + mail.setAttachments([ + { filename: 'invoice.pdf', content: invoicePdfBuffer }, + ]); } - const mail = new Mail() - .setSubject(subject) - .setTo(parsedMessageOptions.to) - .setContent(body) - .setAttachments(attachments); - await mail.send(); } } diff --git a/packages/server/src/services/Sales/Invoices/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts index 79bf67c0a..404b7e613 100644 --- a/packages/server/src/services/Sales/Invoices/constants.ts +++ b/packages/server/src/services/Sales/Invoices/constants.ts @@ -8,6 +8,11 @@ Invoice #{InvoiceNumber}
Due Date : {InvoiceDueDate}
Amount : {InvoiceAmount}

+ +

+Regards
+{CompanyName} +

`; export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT = @@ -18,6 +23,11 @@ export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `

Invoice #{InvoiceNumber}
Due Date : {InvoiceDueDate}
Amount : {InvoiceAmount}

+ +

+Regards
+{CompanyName} +

`; export const ERRORS = { diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts index 42069c5f3..79dfe2392 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts @@ -1,17 +1,14 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; import { IPaymentReceiveMailOpts, SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { DEFAULT_PAYMENT_MAIL_CONTENT, DEFAULT_PAYMENT_MAIL_SUBJECT, - ERRORS, } from './constants'; -import { ServiceError } from '@/exceptions'; -import { formatSmsMessage } from '@/utils'; import { Tenant } from '@/system/models'; import { GetPaymentReceive } from './GetPaymentReceive'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; @Service() export class SendPaymentReceiveMailNotification { @@ -21,6 +18,9 @@ export class SendPaymentReceiveMailNotification { @Inject() private getPaymentService: GetPaymentReceive; + @Inject() + private contactMailNotification: ContactMailNotification; + @Inject('agenda') private agenda: any; @@ -49,19 +49,22 @@ export class SendPaymentReceiveMailNotification { * @param {number} invoiceId * @returns {Promise} */ - public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + public getMailOptions = async (tenantId: number, invoiceId: number) => { const { PaymentReceive } = this.tenancy.models(tenantId); + const paymentReceive = await PaymentReceive.query() .findById(invoiceId) - .withGraphFetched('customer') .throwIfNotFound(); - return { - attachInvoice: true, - subject: DEFAULT_PAYMENT_MAIL_SUBJECT, - body: DEFAULT_PAYMENT_MAIL_CONTENT, - to: paymentReceive.customer.email, - }; + const formatterData = await this.textFormatter(tenantId, invoiceId); + + return this.contactMailNotification.getMailOptions( + tenantId, + paymentReceive.customerId, + DEFAULT_PAYMENT_MAIL_SUBJECT, + DEFAULT_PAYMENT_MAIL_CONTENT, + formatterData + ); }; /** @@ -73,9 +76,8 @@ export class SendPaymentReceiveMailNotification { */ public textFormatter = async ( tenantId: number, - invoiceId: number, - text: string - ): Promise => { + invoiceId: number + ): Promise> => { const payment = await this.getPaymentService.getPaymentReceive( tenantId, invoiceId @@ -84,13 +86,13 @@ export class SendPaymentReceiveMailNotification { .findById(tenantId) .withGraphFetched('metadata'); - return formatSmsMessage(text, { + return { CompanyName: organization.metadata.name, CustomerName: payment.customer.displayName, PaymentNumber: payment.payment_receive_no, PaymentDate: payment.formattedPaymentDate, PaymentAmount: payment.formattedAmount, - }); + }; }; /** @@ -105,7 +107,7 @@ export class SendPaymentReceiveMailNotification { paymentReceiveId: number, messageDTO: SendInvoiceMailDTO ): Promise { - const defaultMessageOpts = await this.getDefaultMailOpts( + const defaultMessageOpts = await this.getMailOptions( tenantId, paymentReceiveId ); @@ -114,18 +116,10 @@ export class SendPaymentReceiveMailNotification { ...defaultMessageOpts, ...messageDTO, }; - // In case there is no email address from the customer or from options, throw an error. - if (!parsedMessageOpts.to) { - throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); - } - const formatter = R.curry(this.textFormatter)(tenantId, paymentReceiveId); - const subject = await formatter(parsedMessageOpts.subject); - const body = await formatter(parsedMessageOpts.body); - await new Mail() - .setSubject(subject) + .setSubject(parsedMessageOpts.subject) .setTo(parsedMessageOpts.to) - .setContent(body) + .setContent(parsedMessageOpts.body) .send(); } } diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts index 6092664e6..bf1e2da3f 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts @@ -205,10 +205,7 @@ export class PaymentReceivesApplication { * @returns {Promise} */ public getPaymentDefaultMail(tenantId: number, paymentReceiveId: number) { - return this.paymentMailNotify.getDefaultMailOpts( - tenantId, - paymentReceiveId - ); + return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId); } /** diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index 6fe03b2a1..0f790ec6f 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -191,12 +191,12 @@ export class SaleReceiptApplication { /** * Retrieves the default mail options of the given sale receipt. - * @param {number} tenantId - * @param {number} saleReceiptId - * @returns + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns */ public getSaleReceiptMail(tenantId: number, saleReceiptId: number) { - return this.saleReceiptNotifyByMailService.getDefaultMailOpts( + return this.saleReceiptNotifyByMailService.getMailOptions( tenantId, saleReceiptId ); diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts index 0d93c2b65..20bfc4073 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts @@ -2,8 +2,6 @@ import * as R from 'ramda'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Inject, Service } from 'typedi'; import { Tenant } from '@/system/models'; -import { formatSmsMessage } from '@/utils'; -import { ServiceError } from '@/exceptions'; import Mail from '@/lib/Mail'; import { GetSaleReceipt } from './GetSaleReceipt'; import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; @@ -11,8 +9,8 @@ import { DEFAULT_RECEIPT_MAIL_CONTENT, DEFAULT_RECEIPT_MAIL_SUBJECT, } from './constants'; -import { ERRORS } from './constants'; import { SaleReceiptMailOpts } from '@/interfaces'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; @Service() export class SaleReceiptMailNotification { @@ -25,6 +23,9 @@ export class SaleReceiptMailNotification { @Inject() private receiptPdfService: SaleReceiptsPdf; + @Inject() + private contactMailNotification: ContactMailNotification; + @Inject('agenda') private agenda: any; @@ -48,25 +49,28 @@ export class SaleReceiptMailNotification { } /** - * Retrieves the default receipt mail options. + * Retrieves the mail options of the given sale receipt. * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} saleReceiptId + * @returns */ - public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + public async getMailOptions(tenantId: number, saleReceiptId: number) { const { SaleReceipt } = this.tenancy.models(tenantId); + const saleReceipt = await SaleReceipt.query() - .findById(invoiceId) - .withGraphFetched('customer') + .findById(saleReceiptId) .throwIfNotFound(); - return { - attachInvoice: true, - subject: DEFAULT_RECEIPT_MAIL_SUBJECT, - body: DEFAULT_RECEIPT_MAIL_CONTENT, - to: saleReceipt.customer.email, - }; - }; + const formattedData = await this.textFormatter(tenantId, saleReceiptId); + + return this.contactMailNotification.getMailOptions( + tenantId, + saleReceipt.customerId, + DEFAULT_RECEIPT_MAIL_SUBJECT, + DEFAULT_RECEIPT_MAIL_CONTENT, + formattedData + ); + } /** * Retrieves the formatted text of the given sale invoice. @@ -77,9 +81,8 @@ export class SaleReceiptMailNotification { */ public textFormatter = async ( tenantId: number, - receiptId: number, - text: string - ): Promise => { + receiptId: number + ): Promise> => { const invoice = await this.getSaleReceiptService.getSaleReceipt( tenantId, receiptId @@ -88,13 +91,13 @@ export class SaleReceiptMailNotification { .findById(tenantId) .withGraphFetched('metadata'); - return formatSmsMessage(text, { + return { CompanyName: organization.metadata.name, CustomerName: invoice.customer.displayName, ReceiptNumber: invoice.receiptNumber, ReceiptDate: invoice.formattedReceiptDate, ReceiptAmount: invoice.formattedAmount, - }); + }; }; /** @@ -109,7 +112,7 @@ export class SaleReceiptMailNotification { saleReceiptId: number, messageOpts: SaleReceiptMailOpts ) { - const defaultMessageOpts = await this.getDefaultMailOpts( + const defaultMessageOpts = await this.getMailOptions( tenantId, saleReceiptId ); @@ -118,14 +121,11 @@ export class SaleReceiptMailNotification { ...defaultMessageOpts, ...messageOpts, }; - // In case there is no email address from the customer or from options, throw an error. - if (!parsedMessageOpts.to) { - throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); - } - const formatter = R.curry(this.textFormatter)(tenantId, saleReceiptId); - const body = await formatter(parsedMessageOpts.body); - const subject = await formatter(parsedMessageOpts.subject); - const attachments = []; + + const mail = new Mail() + .setSubject(parsedMessageOpts.subject) + .setTo(parsedMessageOpts.to) + .setContent(parsedMessageOpts.body); if (parsedMessageOpts.attachInvoice) { // Retrieves document buffer of the invoice pdf document. @@ -133,13 +133,10 @@ export class SaleReceiptMailNotification { tenantId, saleReceiptId ); - attachments.push({ filename: 'invoice.pdf', content: receiptPdfBuffer }); + mail.setAttachments([ + { filename: 'invoice.pdf', content: receiptPdfBuffer }, + ]); } - await new Mail() - .setSubject(subject) - .setTo(parsedMessageOpts.to) - .setContent(body) - .setAttachments(attachments) - .send(); + await mail.send(); } } From ab7abfea352ec38f2883e2d3b20c245875340a91 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 30 Dec 2023 17:49:02 +0200 Subject: [PATCH 17/17] feat: mail notifications of sales transactions --- .../api/controllers/Sales/PaymentReceives.ts | 14 ++-- .../api/controllers/Sales/SalesEstimates.ts | 12 ++-- .../api/controllers/Sales/SalesReceipts.ts | 6 +- packages/server/src/interfaces/Mailable.ts | 41 +++++++++-- .../server/src/interfaces/PaymentReceive.ts | 14 ++-- .../server/src/interfaces/SaleEstimate.ts | 13 ++-- packages/server/src/interfaces/SaleInvoice.ts | 17 ++--- packages/server/src/interfaces/SaleReceipt.ts | 11 ++- packages/server/src/lib/Mail/index.ts | 47 ++++++++----- .../ContactMailNotification.ts | 56 +++++++++++---- .../services/MailNotification/constants.ts | 6 ++ .../src/services/MailNotification/utils.ts | 33 +++++++++ .../Estimates/SaleEstimatesApplication.ts | 16 +++-- .../Sales/Estimates/SendSaleEstimateMail.ts | 43 +++++++----- .../Sales/Invoices/SaleInvoicesApplication.ts | 10 ++- .../Invoices/SendInvoiceInvoiceMailCommon.ts | 50 +++----------- .../Sales/Invoices/SendSaleInvoiceMail.ts | 23 ++++--- .../Invoices/SendSaleInvoiceMailReminder.ts | 6 +- .../PaymentReceiveMailNotification.ts | 43 ++++++------ .../PaymentReceivesApplication.ts | 8 +-- .../Sales/Receipts/SaleReceiptApplication.ts | 13 ++-- .../Receipts/SaleReceiptMailNotification.ts | 68 +++++++++---------- .../src/services/Sales/Receipts/constants.ts | 2 +- .../PaymentMailDialogBoot.tsx | 1 + .../PaymentMailDialogForm.tsx | 4 +- 25 files changed, 336 insertions(+), 221 deletions(-) create mode 100644 packages/server/src/services/MailNotification/constants.ts create mode 100644 packages/server/src/services/MailNotification/utils.ts diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 74657a703..0bef1e60d 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -4,9 +4,8 @@ import { body, check, param, query, ValidationChain } from 'express-validator'; import { AbilitySubject, IPaymentReceiveDTO, - IPaymentReceiveMailOpts, - // IPaymentReceiveMailOpts, PaymentReceiveAction, + PaymentReceiveMailOptsDTO, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; @@ -541,9 +540,12 @@ export default class PaymentReceivesController extends BaseController { ) => { const { tenantId } = req; const { id: paymentReceiveId } = req.params; - const paymentMailDTO: IPaymentReceiveMailOpts = this.matchedBodyData(req, { - includeOptionals: false, - }); + const paymentMailDTO: PaymentReceiveMailOptsDTO = this.matchedBodyData( + req, + { + includeOptionals: false, + } + ); try { await this.paymentReceiveApplication.notifyPaymentByMail( tenantId, @@ -574,7 +576,7 @@ export default class PaymentReceivesController extends BaseController { const { id: paymentReceiveId } = req.params; try { - const data = await this.paymentReceiveApplication.getPaymentDefaultMail( + const data = await this.paymentReceiveApplication.getPaymentMailOptions( tenantId, paymentReceiveId ); diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index f78719de9..b34c1ccf2 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -5,7 +5,7 @@ import { AbilitySubject, ISaleEstimateDTO, SaleEstimateAction, - SaleEstimateMailOptions, + SaleEstimateMailOptionsDTO, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; @@ -513,10 +513,12 @@ export default class SalesEstimatesController extends BaseController { ) => { const { tenantId } = req; const { id: invoiceId } = req.params; - const saleEstimateDTO: SaleEstimateMailOptions = this.matchedBodyData(req, { - includeOptionals: false, - }); - + const saleEstimateDTO: SaleEstimateMailOptionsDTO = this.matchedBodyData( + req, + { + includeOptionals: false, + } + ); try { await this.saleEstimatesApplication.sendSaleEstimateMail( tenantId, diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index c8635fe3c..6151561f8 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -3,7 +3,7 @@ import { body, check, param, query } from 'express-validator'; import { Inject, Service } from 'typedi'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import BaseController from '../BaseController'; -import { ISaleReceiptDTO, SaleReceiptMailOpts } from '@/interfaces/SaleReceipt'; +import { ISaleReceiptDTO, SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces/SaleReceipt'; import { ServiceError } from '@/exceptions'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import CheckPolicies from '@/api/middleware/CheckPolicies'; @@ -54,7 +54,7 @@ export default class SalesReceiptsController extends BaseController { body('from').isString().optional(), body('to').isString().optional(), body('body').isString().optional(), - body('attach_invoice').optional().isBoolean().toBoolean(), + body('attach_receipt').optional().isBoolean().toBoolean(), ], this.validationResult, asyncMiddleware(this.sendSaleReceiptMail.bind(this)), @@ -439,7 +439,7 @@ export default class SalesReceiptsController extends BaseController { ) => { const { tenantId } = req; const { id: receiptId } = req.params; - const receiptMailDTO: SaleReceiptMailOpts = this.matchedBodyData(req, { + const receiptMailDTO: SaleReceiptMailOptsDTO = this.matchedBodyData(req, { includeOptionals: false, }); diff --git a/packages/server/src/interfaces/Mailable.ts b/packages/server/src/interfaces/Mailable.ts index 36cc3c81f..5682f2529 100644 --- a/packages/server/src/interfaces/Mailable.ts +++ b/packages/server/src/interfaces/Mailable.ts @@ -1,9 +1,17 @@ +export type IMailAttachment = MailAttachmentPath | MailAttachmentContent; + +export interface MailAttachmentPath { + filename: string; + path: string; + cid: string; +} +export interface MailAttachmentContent { + filename: string; + content: Buffer; +} export interface IMailable { - constructor( - view: string, - data?: { [key: string]: string | number }, - ); + constructor(view: string, data?: { [key: string]: string | number }); send(): Promise; build(): void; setData(data: { [key: string]: string | number }): IMailable; @@ -13,4 +21,27 @@ export interface IMailable { setView(view: string): IMailable; render(data?: { [key: string]: string | number }): string; getViewContent(): string; -} \ No newline at end of file +} + +export interface AddressItem { + label: string; + mail: string; + primary?: boolean; +} + +export interface CommonMailOptions { + toAddresses: AddressItem[]; + fromAddresses: AddressItem[]; + from: string; + to: string | string[]; + subject: string; + body: string; + data?: Record; +} + +export interface CommonMailOptionsDTO { + to?: string | string[]; + from?: string; + subject?: string; + body?: string; +} diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts index c919182ae..2926d923c 100644 --- a/packages/server/src/interfaces/PaymentReceive.ts +++ b/packages/server/src/interfaces/PaymentReceive.ts @@ -1,5 +1,9 @@ import { Knex } from 'knex'; -import { ISystemUser } from '@/interfaces'; +import { + CommonMailOptions, + CommonMailOptionsDTO, + ISystemUser, +} from '@/interfaces'; import { ILedgerEntry } from './Ledger'; import { ISaleInvoice } from './SaleInvoice'; @@ -19,7 +23,7 @@ export interface IPaymentReceive { createdAt: Date; updatedAt: Date; localAmount?: number; - branchId?: number + branchId?: number; } export interface IPaymentReceiveCreateDTO { customerId: number; @@ -166,6 +170,6 @@ export type IPaymentReceiveGLCommonEntry = Pick< | 'branchId' >; -export interface IPaymentReceiveMailOpts { - -} \ No newline at end of file +export interface PaymentReceiveMailOpts extends CommonMailOptions {} + +export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {} diff --git a/packages/server/src/interfaces/SaleEstimate.ts b/packages/server/src/interfaces/SaleEstimate.ts index 3a503e1fd..171c8a0d1 100644 --- a/packages/server/src/interfaces/SaleEstimate.ts +++ b/packages/server/src/interfaces/SaleEstimate.ts @@ -1,6 +1,7 @@ import { Knex } from 'knex'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter'; +import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; export interface ISaleEstimate { id?: number; @@ -125,10 +126,10 @@ export interface ISaleEstimateApprovedEvent { trx: Knex.Transaction; } -export interface SaleEstimateMailOptions { - to: string; - from: string; - subject: string; - body: string; - attachInvoice?: boolean; +export interface SaleEstimateMailOptions extends CommonMailOptions { + attachEstimate?: boolean; +} + +export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO { + attachEstimate?: boolean; } \ No newline at end of file diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index d660b17ca..394319e86 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -1,5 +1,6 @@ import { Knex } from 'knex'; -import { ISystemUser, IAccount, ITaxTransaction, AddressItem } from '@/interfaces'; +import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces'; +import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; @@ -187,21 +188,11 @@ export enum SaleInvoiceAction { NotifyBySms = 'NotifyBySms', } -export interface SaleInvoiceMailOptions { - toAddresses: AddressItem[]; - fromAddresses: AddressItem[]; - from: string; - to: string | string[]; - subject: string; - body: string; +export interface SaleInvoiceMailOptions extends CommonMailOptions { attachInvoice: boolean; } -export interface SendInvoiceMailDTO { - to: string | string[]; - from: string; - subject: string; - body: string; +export interface SendInvoiceMailDTO extends CommonMailOptionsDTO { attachInvoice?: boolean; } diff --git a/packages/server/src/interfaces/SaleReceipt.ts b/packages/server/src/interfaces/SaleReceipt.ts index 102513f7e..1e8ffa98e 100644 --- a/packages/server/src/interfaces/SaleReceipt.ts +++ b/packages/server/src/interfaces/SaleReceipt.ts @@ -1,5 +1,6 @@ import { Knex } from 'knex'; import { IItemEntry } from './ItemEntry'; +import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; export interface ISaleReceipt { id?: number; @@ -135,6 +136,10 @@ export interface ISaleReceiptDeletingPayload { trx: Knex.Transaction; } -export interface SaleReceiptMailOpts { - -} \ No newline at end of file +export interface SaleReceiptMailOpts extends CommonMailOptions { + attachReceipt: boolean; +} + +export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO { + attachReceipt?: boolean; +} diff --git a/packages/server/src/lib/Mail/index.ts b/packages/server/src/lib/Mail/index.ts index dd79c934b..015ca02a8 100644 --- a/packages/server/src/lib/Mail/index.ts +++ b/packages/server/src/lib/Mail/index.ts @@ -2,18 +2,13 @@ import fs from 'fs'; import Mustache from 'mustache'; import { Container } from 'typedi'; import path from 'path'; -import { IMailable } from '@/interfaces'; - -interface IMailAttachment { - filename: string; - path: string; - cid: string; -} +import { IMailAttachment } from '@/interfaces'; export default class Mail { view: string; - subject: string; - to: string; + subject: string = ''; + content: string = ''; + to: string | string[]; from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`; data: { [key: string]: string | number }; attachments: IMailAttachment[]; @@ -21,16 +16,24 @@ export default class Mail { /** * Mail options. */ - private get mailOptions() { + public get mailOptions() { return { to: this.to, from: this.from, subject: this.subject, - html: this.render(this.data), + html: this.html, attachments: this.attachments, }; } + /** + * Retrieves the html content of the mail. + * @returns {string} + */ + public get html() { + return this.view ? Mail.render(this.view, this.data) : this.content; + } + /** * Sends the given mail to the target address. */ @@ -52,7 +55,7 @@ export default class Mail { * Set send mail to address. * @param {string} to - */ - setTo(to: string) { + setTo(to: string | string[]) { this.to = to; return this; } @@ -62,11 +65,16 @@ export default class Mail { * @param {string} from * @return {} */ - private setFrom(from: string) { + setFrom(from: string) { this.from = from; return this; } + /** + * Set attachments to the mail. + * @param {IMailAttachment[]} attachments + * @returns {Mail} + */ setAttachments(attachments: IMailAttachment[]) { this.attachments = attachments; return this; @@ -95,21 +103,26 @@ export default class Mail { return this; } + setContent(content: string) { + this.content = content; + return this; + } + /** * Renders the view template with the given data. * @param {object} data * @return {string} */ - render(data): string { - const viewContent = this.getViewContent(); + static render(view: string, data: Record): string { + const viewContent = Mail.getViewContent(view); return Mustache.render(viewContent, data); } /** * Retrieve view content from the view directory. */ - private getViewContent(): string { - const filePath = path.join(global.__views_dir, `/${this.view}`); + static getViewContent(view: string): string { + const filePath = path.join(global.__views_dir, `/${view}`); return fs.readFileSync(filePath, 'utf8'); } } diff --git a/packages/server/src/services/MailNotification/ContactMailNotification.ts b/packages/server/src/services/MailNotification/ContactMailNotification.ts index 21c745a96..e1e733a79 100644 --- a/packages/server/src/services/MailNotification/ContactMailNotification.ts +++ b/packages/server/src/services/MailNotification/ContactMailNotification.ts @@ -1,9 +1,9 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; -import { SaleInvoiceMailOptions } from '@/interfaces'; +import { CommonMailOptions } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { MailTenancy } from '@/services/MailTenancy/MailTenancy'; import { formatSmsMessage } from '@/utils'; +import { Tenant } from '@/system/models'; @Service() export class ContactMailNotification { @@ -15,8 +15,10 @@ export class ContactMailNotification { /** * Parses the default message options. - * @param {number} tenantId - * @param {number} invoiceId + * @param {number} tenantId - + * @param {number} invoiceId - + * @param {string} subject - + * @param {string} body - * @returns {Promise} */ public async getDefaultMailOptions( @@ -24,9 +26,11 @@ export class ContactMailNotification { contactId: number, subject: string = '', body: string = '' - ): Promise { - const { Contact, Customer } = this.tenancy.models(tenantId); - const contact = await Customer.query().findById(contactId).throwIfNotFound(); + ): Promise { + const { Customer } = this.tenancy.models(tenantId); + const contact = await Customer.query() + .findById(contactId) + .throwIfNotFound(); const toAddresses = contact.contactAddresses; const fromAddresses = await this.mailTenancy.senders(tenantId); @@ -48,10 +52,12 @@ export class ContactMailNotification { } /** - * Retrieves the mail options. - * @param {number} - * @param {number} invoiceId - * @returns {} + * Retrieves the mail options of the given contact. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Invoice id. + * @param {string} defaultSubject - Default subject text. + * @param {string} defaultBody - Default body text. + * @returns {Promise} */ public async getMailOptions( tenantId: number, @@ -59,15 +65,20 @@ export class ContactMailNotification { defaultSubject?: string, defaultBody?: string, formatterData?: Record - ): Promise { + ): Promise { const mailOpts = await this.getDefaultMailOptions( tenantId, contactId, defaultSubject, defaultBody ); - const subject = formatSmsMessage(mailOpts.subject, formatterData); - const body = formatSmsMessage(mailOpts.body, formatterData); + const commonFormatArgs = await this.getCommonFormatArgs(tenantId); + const formatArgs = { + ...commonFormatArgs, + ...formatterData, + }; + const subject = formatSmsMessage(mailOpts.subject, formatArgs); + const body = formatSmsMessage(mailOpts.body, formatArgs); return { ...mailOpts, @@ -75,4 +86,21 @@ export class ContactMailNotification { body, }; } + + /** + * Retrieves the common format args. + * @param {number} tenantId + * @returns {Promise>} + */ + public async getCommonFormatArgs( + tenantId: number + ): Promise> { + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return { + CompanyName: organization.metadata.name, + }; + } } diff --git a/packages/server/src/services/MailNotification/constants.ts b/packages/server/src/services/MailNotification/constants.ts new file mode 100644 index 000000000..95b720d70 --- /dev/null +++ b/packages/server/src/services/MailNotification/constants.ts @@ -0,0 +1,6 @@ +export const ERRORS = { + MAIL_FROM_NOT_FOUND: 'Mail from address not found', + MAIL_TO_NOT_FOUND: 'Mail to address not found', + MAIL_SUBJECT_NOT_FOUND: 'Mail subject not found', + MAIL_BODY_NOT_FOUND: 'Mail body not found', +}; diff --git a/packages/server/src/services/MailNotification/utils.ts b/packages/server/src/services/MailNotification/utils.ts new file mode 100644 index 000000000..b9e37b297 --- /dev/null +++ b/packages/server/src/services/MailNotification/utils.ts @@ -0,0 +1,33 @@ +import { isEmpty } from 'lodash'; +import { ServiceError } from '@/exceptions'; +import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces'; +import { ERRORS } from './constants'; + +/** + * Merges the mail options with incoming options. + * @param {Partial} mailOptions + * @param {Partial} overridedOptions + * @throws {ServiceError} + */ +export function parseAndValidateMailOptions( + mailOptions: Partial, + overridedOptions: Partial +) { + const mergedMessageOptions = { + ...mailOptions, + ...overridedOptions, + }; + if (isEmpty(mergedMessageOptions.from)) { + throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND); + } + if (isEmpty(mergedMessageOptions.to)) { + throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND); + } + if (isEmpty(mergedMessageOptions.subject)) { + throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND); + } + if (isEmpty(mergedMessageOptions.body)) { + throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND); + } + return mergedMessageOptions; +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 1ceb2bbcc..f1c7b3cdf 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -8,6 +8,7 @@ import { ISaleEstimateDTO, ISalesEstimatesFilter, SaleEstimateMailOptions, + SaleEstimateMailOptionsDTO, } from '@/interfaces'; import { EditSaleEstimate } from './EditSaleEstimate'; import { DeleteSaleEstimate } from './DeleteSaleEstimate'; @@ -224,8 +225,8 @@ export class SaleEstimatesApplication { public sendSaleEstimateMail( tenantId: number, saleEstimateId: number, - saleEstimateMailOpts: SaleEstimateMailOptions - ) { + saleEstimateMailOpts: SaleEstimateMailOptionsDTO + ): Promise { return this.sendEstimateMailService.triggerMail( tenantId, saleEstimateId, @@ -235,11 +236,14 @@ export class SaleEstimatesApplication { /** * Retrieves the default mail options of the given sale estimate. - * @param {number} tenantId - * @param {number} saleEstimateId - * @returns {} + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} */ - public getSaleEstimateMail(tenantId: number, saleEstimateId: number) { + public getSaleEstimateMail( + tenantId: number, + saleEstimateId: number + ): Promise { return this.sendEstimateMailService.getMailOptions( tenantId, saleEstimateId diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts index 5777ab55a..258496306 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -7,8 +7,12 @@ import { } from './constants'; import { SaleEstimatesPdf } from './SaleEstimatesPdf'; import { GetSaleEstimate } from './GetSaleEstimate'; -import { SaleEstimateMailOptions } from '@/interfaces'; +import { + SaleEstimateMailOptions, + SaleEstimateMailOptionsDTO, +} from '@/interfaces'; import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SendSaleEstimateMail { @@ -31,13 +35,14 @@ export class SendSaleEstimateMail { * Triggers the reminder mail of the given sale estimate. * @param {number} tenantId - * @param {number} saleEstimateId - - * @param {SaleEstimateMailOptions} messageOptions - + * @param {SaleEstimateMailOptionsDTO} messageOptions - + * @returns {Promise} */ public async triggerMail( tenantId: number, saleEstimateId: number, - messageOptions: SaleEstimateMailOptions - ) { + messageOptions: SaleEstimateMailOptionsDTO + ): Promise { const payload = { tenantId, saleEstimateId, @@ -48,9 +53,9 @@ export class SendSaleEstimateMail { /** * Formates the text of the mail. - * @param {number} tenantId - * @param {number} estimateId - * @param {string} text + * @param {number} tenantId - Tenant id. + * @param {number} estimateId - Estimate id. + * @returns {Promise>} */ public formatterData = async (tenantId: number, estimateId: number) => { const estimate = await this.getSaleEstimateService.getEstimate( @@ -70,9 +75,12 @@ export class SendSaleEstimateMail { * Retrieves the mail options. * @param {number} tenantId * @param {number} saleEstimateId - * @returns + * @returns {Promise} */ - public getMailOptions = async (tenantId: number, saleEstimateId: number) => { + public getMailOptions = async ( + tenantId: number, + saleEstimateId: number + ): Promise => { const { SaleEstimate } = this.tenancy.models(tenantId); const saleEstimate = await SaleEstimate.query() @@ -91,6 +99,7 @@ export class SendSaleEstimateMail { return { ...mailOptions, data: formatterData, + attachEstimate: true }; }; @@ -99,26 +108,28 @@ export class SendSaleEstimateMail { * @param {number} tenantId * @param {number} saleEstimateId * @param {SaleEstimateMailOptions} messageOptions + * @returns {Promise} */ public async sendMail( tenantId: number, saleEstimateId: number, - messageOptions: SaleEstimateMailOptions - ) { + messageOptions: SaleEstimateMailOptionsDTO + ): Promise { const localMessageOpts = await this.getMailOptions( tenantId, saleEstimateId ); - const messageOpts = { - ...localMessageOpts, - ...messageOptions, - }; + // Overrides and validates the given mail options. + const messageOpts = parseAndValidateMailOptions( + localMessageOpts, + messageOptions + ); const mail = new Mail() .setSubject(messageOpts.subject) .setTo(messageOpts.to) .setContent(messageOpts.body); - if (messageOpts.to) { + if (messageOpts.attachEstimate) { const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf( tenantId, saleEstimateId diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index b175d9546..bc3f8c24b 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -300,7 +300,10 @@ export class SaleInvoiceApplication { * @returns {} */ public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { - return this.sendInvoiceReminderService.getMailOpts(tenantId, saleInvoiceId); + return this.sendInvoiceReminderService.getMailOption( + tenantId, + saleInvoiceId + ); } /** @@ -347,6 +350,9 @@ export class SaleInvoiceApplication { * @returns {Promise} */ public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) { - return this.sendSaleInvoiceMailService.getMailOpts(tenantId, saleInvoiceid); + return this.sendSaleInvoiceMailService.getMailOption( + tenantId, + saleInvoiceid + ); } } diff --git a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts index 97be8de87..52ef46a59 100644 --- a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts +++ b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts @@ -1,15 +1,12 @@ import { Inject, Service } from 'typedi'; -import { isEmpty } from 'lodash'; import { SaleInvoiceMailOptions } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; import { DEFAULT_INVOICE_MAIL_CONTENT, DEFAULT_INVOICE_MAIL_SUBJECT, } from './constants'; -import { GetSaleInvoice } from './GetSaleInvoice'; -import { Tenant } from '@/system/models'; -import { ServiceError } from '@/exceptions'; -import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; @Service() export class SendSaleInvoiceMailCommon { @@ -28,9 +25,9 @@ export class SendSaleInvoiceMailCommon { * @param {number} invoiceId - Invoice id. * @param {string} defaultSubject - Subject text. * @param {string} defaultBody - Subject body. - * @returns {} + * @returns {Promise} */ - public async getMailOpts( + public async getMailOption( tenantId: number, invoiceId: number, defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT, @@ -44,13 +41,17 @@ export class SendSaleInvoiceMailCommon { const formatterData = await this.formatText(tenantId, invoiceId); - return this.contactMailNotification.getMailOptions( + const mailOptions = await this.contactMailNotification.getMailOptions( tenantId, saleInvoice.customerId, defaultSubject, defaultBody, formatterData ); + return { + ...mailOptions, + attachInvoice: true, + }; } /** @@ -68,12 +69,8 @@ export class SendSaleInvoiceMailCommon { tenantId, invoiceId ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); return { - CompanyName: organization.metadata.name, CustomerName: invoice.customer.displayName, InvoiceNumber: invoice.invoiceNo, InvoiceDueAmount: invoice.dueAmountFormatted, @@ -83,33 +80,4 @@ export class SendSaleInvoiceMailCommon { OverdueDays: invoice.overdueDays, }; }; - - /** - * Validates the mail notification options before sending it. - * @param {Partial} mailNotificationOpts - * @throws {ServiceError} - */ - public validateMailNotification( - mailNotificationOpts: Partial - ) { - if (isEmpty(mailNotificationOpts.from)) { - throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND); - } - if (isEmpty(mailNotificationOpts.to)) { - throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND); - } - if (isEmpty(mailNotificationOpts.subject)) { - throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND); - } - if (isEmpty(mailNotificationOpts.body)) { - throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND); - } - } } - -const ERRORS = { - MAIL_FROM_NOT_FOUND: 'Mail from address not found', - MAIL_TO_NOT_FOUND: 'Mail to address not found', - MAIL_SUBJECT_NOT_FOUND: 'Mail subject not found', - MAIL_BODY_NOT_FOUND: 'Mail body not found', -}; diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index a5bc744e1..05db4f73e 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -7,6 +7,7 @@ import { DEFAULT_INVOICE_MAIL_CONTENT, DEFAULT_INVOICE_MAIL_SUBJECT, } from './constants'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SendSaleInvoiceMail { @@ -44,8 +45,8 @@ export class SendSaleInvoiceMail { * @param {number} saleInvoiceId * @returns {Promise} */ - public async getMailOpts(tenantId: number, saleInvoiceId: number) { - return this.invoiceMail.getMailOpts( + public async getMailOption(tenantId: number, saleInvoiceId: number) { + return this.invoiceMail.getMailOption( tenantId, saleInvoiceId, DEFAULT_INVOICE_MAIL_SUBJECT, @@ -65,15 +66,15 @@ export class SendSaleInvoiceMail { saleInvoiceId: number, messageDTO: SendInvoiceMailDTO ) { - const defaultMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); - - // Parsed message opts with default options. - const messageOpts = { - ...defaultMessageOpts, - ...messageDTO, - }; - this.invoiceMail.validateMailNotification(messageOpts); - + const defaultMessageOpts = await this.getMailOption( + tenantId, + saleInvoiceId + ); + // Merge message opts with default options and validate the incoming options. + const messageOpts = parseAndValidateMailOptions( + defaultMessageOpts, + messageDTO + ); const mail = new Mail() .setSubject(messageOpts.subject) .setTo(messageOpts.to) diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index 09463bddc..b5389a8a0 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -43,8 +43,8 @@ export class SendInvoiceMailReminder { * @param {number} saleInvoiceId * @returns {Promise} */ - public async getMailOpts(tenantId: number, saleInvoiceId: number) { - return this.invoiceCommonMail.getMailOpts( + public async getMailOption(tenantId: number, saleInvoiceId: number) { + return this.invoiceCommonMail.getMailOption( tenantId, saleInvoiceId, DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, @@ -64,7 +64,7 @@ export class SendInvoiceMailReminder { saleInvoiceId: number, messageOptions: SendInvoiceMailDTO ) { - const localMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); + const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId); const messageOpts = { ...localMessageOpts, diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts index 79dfe2392..acb1ea7a1 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts @@ -1,14 +1,18 @@ import { Inject, Service } from 'typedi'; -import { IPaymentReceiveMailOpts, SendInvoiceMailDTO } from '@/interfaces'; +import { + PaymentReceiveMailOpts, + PaymentReceiveMailOptsDTO, + SendInvoiceMailDTO, +} from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { DEFAULT_PAYMENT_MAIL_CONTENT, DEFAULT_PAYMENT_MAIL_SUBJECT, } from './constants'; -import { Tenant } from '@/system/models'; import { GetPaymentReceive } from './GetPaymentReceive'; import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SendPaymentReceiveMailNotification { @@ -28,13 +32,14 @@ export class SendPaymentReceiveMailNotification { * Sends the mail of the given payment receive. * @param {number} tenantId * @param {number} paymentReceiveId - * @param {SendInvoiceMailDTO} messageDTO + * @param {PaymentReceiveMailOptsDTO} messageDTO + * @returns {Promise} */ public async triggerMail( tenantId: number, paymentReceiveId: number, - messageDTO: IPaymentReceiveMailOpts - ) { + messageDTO: PaymentReceiveMailOptsDTO + ): Promise { const payload = { tenantId, paymentReceiveId, @@ -45,18 +50,21 @@ export class SendPaymentReceiveMailNotification { /** * Retrieves the default payment mail options. - * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment receive id. + * @returns {Promise} */ - public getMailOptions = async (tenantId: number, invoiceId: number) => { + public getMailOptions = async ( + tenantId: number, + paymentId: number + ): Promise => { const { PaymentReceive } = this.tenancy.models(tenantId); const paymentReceive = await PaymentReceive.query() - .findById(invoiceId) + .findById(paymentId) .throwIfNotFound(); - const formatterData = await this.textFormatter(tenantId, invoiceId); + const formatterData = await this.textFormatter(tenantId, paymentId); return this.contactMailNotification.getMailOptions( tenantId, @@ -82,12 +90,7 @@ export class SendPaymentReceiveMailNotification { tenantId, invoiceId ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - return { - CompanyName: organization.metadata.name, CustomerName: payment.customer.displayName, PaymentNumber: payment.payment_receive_no, PaymentDate: payment.formattedPaymentDate, @@ -112,10 +115,10 @@ export class SendPaymentReceiveMailNotification { paymentReceiveId ); // Parsed message opts with default options. - const parsedMessageOpts = { - ...defaultMessageOpts, - ...messageDTO, - }; + const parsedMessageOpts = parseAndValidateMailOptions( + defaultMessageOpts, + messageDTO + ); await new Mail() .setSubject(parsedMessageOpts.subject) .setTo(parsedMessageOpts.to) diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts index bf1e2da3f..0d5669bf8 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts @@ -4,10 +4,10 @@ import { IPaymentReceive, IPaymentReceiveCreateDTO, IPaymentReceiveEditDTO, - IPaymentReceiveMailOpts, IPaymentReceiveSmsDetails, IPaymentReceivesFilter, ISystemUser, + PaymentReceiveMailOptsDTO, } from '@/interfaces'; import { Inject, Service } from 'typedi'; import { CreatePaymentReceive } from './CreatePaymentReceive'; @@ -189,8 +189,8 @@ export class PaymentReceivesApplication { public notifyPaymentByMail( tenantId: number, paymentReceiveId: number, - messageOpts: IPaymentReceiveMailOpts - ) { + messageOpts: PaymentReceiveMailOptsDTO + ): Promise { return this.paymentMailNotify.triggerMail( tenantId, paymentReceiveId, @@ -204,7 +204,7 @@ export class PaymentReceivesApplication { * @param {number} paymentReceiveId * @returns {Promise} */ - public getPaymentDefaultMail(tenantId: number, paymentReceiveId: number) { + public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) { return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId); } diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index 0f790ec6f..459d9c62e 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -6,6 +6,7 @@ import { ISaleReceipt, ISalesReceiptsFilter, SaleReceiptMailOpts, + SaleReceiptMailOptsDTO, } from '@/interfaces'; import { EditSaleReceipt } from './EditSaleReceipt'; import { GetSaleReceipt } from './GetSaleReceipt'; @@ -176,12 +177,13 @@ export class SaleReceiptApplication { * Sends the receipt mail of the given sale receipt. * @param {number} tenantId * @param {number} saleReceiptId + * @returns {Promise} */ public sendSaleReceiptMail( tenantId: number, saleReceiptId: number, - messageOpts: SaleReceiptMailOpts - ) { + messageOpts: SaleReceiptMailOptsDTO + ): Promise { return this.saleReceiptNotifyByMailService.triggerMail( tenantId, saleReceiptId, @@ -193,9 +195,12 @@ export class SaleReceiptApplication { * Retrieves the default mail options of the given sale receipt. * @param {number} tenantId * @param {number} saleReceiptId - * @returns + * @returns {Promise} */ - public getSaleReceiptMail(tenantId: number, saleReceiptId: number) { + public getSaleReceiptMail( + tenantId: number, + saleReceiptId: number + ): Promise { return this.saleReceiptNotifyByMailService.getMailOptions( tenantId, saleReceiptId diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts index 20bfc4073..572bed2f8 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts @@ -1,7 +1,5 @@ -import * as R from 'ramda'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Inject, Service } from 'typedi'; -import { Tenant } from '@/system/models'; import Mail from '@/lib/Mail'; import { GetSaleReceipt } from './GetSaleReceipt'; import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; @@ -9,8 +7,9 @@ import { DEFAULT_RECEIPT_MAIL_CONTENT, DEFAULT_RECEIPT_MAIL_SUBJECT, } from './constants'; -import { SaleReceiptMailOpts } from '@/interfaces'; +import { SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces'; import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SaleReceiptMailNotification { @@ -32,13 +31,13 @@ export class SaleReceiptMailNotification { /** * Sends the receipt mail of the given sale receipt. * @param {number} tenantId - * @param {number} saleInvoiceId - * @param {SendInvoiceMailDTO} messageDTO + * @param {number} saleReceiptId + * @param {SaleReceiptMailOptsDTO} messageDTO */ public async triggerMail( tenantId: number, saleReceiptId: number, - messageOpts: SaleReceiptMailOpts + messageOpts: SaleReceiptMailOptsDTO ) { const payload = { tenantId, @@ -52,9 +51,12 @@ export class SaleReceiptMailNotification { * Retrieves the mail options of the given sale receipt. * @param {number} tenantId * @param {number} saleReceiptId - * @returns + * @returns {Promise} */ - public async getMailOptions(tenantId: number, saleReceiptId: number) { + public async getMailOptions( + tenantId: number, + saleReceiptId: number + ): Promise { const { SaleReceipt } = this.tenancy.models(tenantId); const saleReceipt = await SaleReceipt.query() @@ -63,17 +65,21 @@ export class SaleReceiptMailNotification { const formattedData = await this.textFormatter(tenantId, saleReceiptId); - return this.contactMailNotification.getMailOptions( + const mailOpts = await this.contactMailNotification.getMailOptions( tenantId, saleReceipt.customerId, DEFAULT_RECEIPT_MAIL_SUBJECT, DEFAULT_RECEIPT_MAIL_CONTENT, formattedData ); + return { + ...mailOpts, + attachReceipt: true, + }; } /** - * Retrieves the formatted text of the given sale invoice. + * Retrieves the formatted text of the given sale receipt. * @param {number} tenantId - Tenant id. * @param {number} receiptId - Sale receipt id. * @param {string} text - The given text. @@ -83,58 +89,52 @@ export class SaleReceiptMailNotification { tenantId: number, receiptId: number ): Promise> => { - const invoice = await this.getSaleReceiptService.getSaleReceipt( + const receipt = await this.getSaleReceiptService.getSaleReceipt( tenantId, receiptId ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - return { - CompanyName: organization.metadata.name, - CustomerName: invoice.customer.displayName, - ReceiptNumber: invoice.receiptNumber, - ReceiptDate: invoice.formattedReceiptDate, - ReceiptAmount: invoice.formattedAmount, + CustomerName: receipt.customer.displayName, + ReceiptNumber: receipt.receiptNumber, + ReceiptDate: receipt.formattedReceiptDate, + ReceiptAmount: receipt.formattedAmount, }; }; /** - * Triggers the mail invoice. - * @param {number} tenantId - * @param {number} saleInvoiceId - * @param {SendInvoiceMailDTO} messageDTO + * Triggers the mail notification of the given sale receipt. + * @param {number} tenantId - Tenant id. + * @param {number} saleReceiptId - Sale receipt id. + * @param {SaleReceiptMailOpts} messageDTO - Overrided message options. * @returns {Promise} */ public async sendMail( tenantId: number, saleReceiptId: number, - messageOpts: SaleReceiptMailOpts + messageOpts: SaleReceiptMailOptsDTO ) { const defaultMessageOpts = await this.getMailOptions( tenantId, saleReceiptId ); - // Parsed message opts with default options. - const parsedMessageOpts = { - ...defaultMessageOpts, - ...messageOpts, - }; - + // Merges message opts with default options. + const parsedMessageOpts = parseAndValidateMailOptions( + defaultMessageOpts, + messageOpts + ); const mail = new Mail() .setSubject(parsedMessageOpts.subject) .setTo(parsedMessageOpts.to) .setContent(parsedMessageOpts.body); - if (parsedMessageOpts.attachInvoice) { - // Retrieves document buffer of the invoice pdf document. + if (parsedMessageOpts.attachReceipt) { + // Retrieves document buffer of the receipt pdf document. const receiptPdfBuffer = await this.receiptPdfService.saleReceiptPdf( tenantId, saleReceiptId ); mail.setAttachments([ - { filename: 'invoice.pdf', content: receiptPdfBuffer }, + { filename: 'receipt.pdf', content: receiptPdfBuffer }, ]); } await mail.send(); diff --git a/packages/server/src/services/Sales/Receipts/constants.ts b/packages/server/src/services/Sales/Receipts/constants.ts index ae1a2e388..084af9214 100644 --- a/packages/server/src/services/Sales/Receipts/constants.ts +++ b/packages/server/src/services/Sales/Receipts/constants.ts @@ -1,5 +1,5 @@ export const DEFAULT_RECEIPT_MAIL_SUBJECT = - 'Invoice {InvoiceNumber} from {CompanyName}'; + 'Receipt {ReceiptNumber} from {CompanyName}'; export const DEFAULT_RECEIPT_MAIL_CONTENT = `

Dear {CustomerName}

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

diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx index c70a28896..aa08bd2e1 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx @@ -29,6 +29,7 @@ function PaymentMailDialogBoot({ const provider = { mailOptions, isMailOptionsLoading, + paymentReceiveId }; return ( diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx index 075c2ee8b..bf0aa578b 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx @@ -28,7 +28,7 @@ export function PaymentMailDialogFormRoot({ // #withDialogActions closeDialog, }) { - const { mailOptions, paymentId } = usePaymentMailDialogBoot(); + const { mailOptions, paymentReceiveId } = usePaymentMailDialogBoot(); const { mutateAsync: sendPaymentMail } = useSendPaymentReceiveMail(); const initialValues = transformMailFormToInitialValues( @@ -43,7 +43,7 @@ export function PaymentMailDialogFormRoot({ const reqValues = transformMailFormToRequest(values); setSubmitting(true); - sendPaymentMail([paymentId, reqValues]) + sendPaymentMail([paymentReceiveId, reqValues]) .then(() => { AppToaster.show({ message: 'The mail notification has been sent successfully.',