mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
add server to monorepo.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import SaleNotifyBySms from '../SaleNotifyBySms';
|
||||
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
|
||||
import SMSClient from '@/services/SMSClient';
|
||||
import {
|
||||
ICustomer,
|
||||
IPaymentReceiveSmsDetails,
|
||||
ISaleEstimate,
|
||||
SMS_NOTIFICATION_KEY,
|
||||
} from '@/interfaces';
|
||||
import { Tenant, TenantMetadata } from '@/system/models';
|
||||
import { formatNumber, formatSmsMessage } from 'utils';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
const ERRORS = {
|
||||
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class SaleEstimateNotifyBySms {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
saleSmsNotification: SaleNotifyBySms;
|
||||
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
smsNotificationsSettings: SmsNotificationsSettingsService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<ISaleEstimate>}
|
||||
*/
|
||||
public notifyBySms = async (
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<ISaleEstimate> => {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const saleEstimate = await SaleEstimate.query()
|
||||
.findById(saleEstimateId)
|
||||
.withGraphFetched('customer');
|
||||
|
||||
// Validates the estimate transaction existance.
|
||||
this.validateEstimateExistance(saleEstimate);
|
||||
|
||||
// Validate the customer phone number existance and number validation.
|
||||
this.saleSmsNotification.validateCustomerPhoneNumber(
|
||||
saleEstimate.customer.personalPhone
|
||||
);
|
||||
// Triggers `onSaleEstimateNotifySms` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onNotifySms, {
|
||||
tenantId,
|
||||
saleEstimate,
|
||||
});
|
||||
await this.sendSmsNotification(tenantId, saleEstimate);
|
||||
|
||||
// Triggers `onSaleEstimateNotifySms` event.
|
||||
await this.eventPublisher.emitAsync(events.saleEstimate.onNotifiedSms, {
|
||||
tenantId,
|
||||
saleEstimate,
|
||||
});
|
||||
return saleEstimate;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleEstimate} saleEstimate
|
||||
* @returns
|
||||
*/
|
||||
private sendSmsNotification = async (
|
||||
tenantId: number,
|
||||
saleEstimate: ISaleEstimate & { customer: ICustomer }
|
||||
) => {
|
||||
const smsClient = this.tenancy.smsClient(tenantId);
|
||||
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Retrieve the formatted sms notification message for estimate details.
|
||||
const formattedSmsMessage = this.formattedEstimateDetailsMessage(
|
||||
tenantId,
|
||||
saleEstimate,
|
||||
tenantMetadata
|
||||
);
|
||||
const phoneNumber = saleEstimate.customer.personalPhone;
|
||||
|
||||
// Runs the send message job.
|
||||
return smsClient.sendMessageJob(phoneNumber, formattedSmsMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify via SMS message after estimate creation.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public notifyViaSmsNotificationAfterCreation = async (
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<void> => {
|
||||
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||
tenantId,
|
||||
SMS_NOTIFICATION_KEY.SALE_ESTIMATE_DETAILS
|
||||
);
|
||||
// Can't continue if the sms auto-notification is not enabled.
|
||||
if (!notification.isNotificationEnabled) return;
|
||||
|
||||
await this.notifyBySms(tenantId, saleEstimateId);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleEstimate} saleEstimate
|
||||
* @param {TenantMetadata} tenantMetadata
|
||||
* @returns {string}
|
||||
*/
|
||||
private formattedEstimateDetailsMessage = (
|
||||
tenantId: number,
|
||||
saleEstimate: ISaleEstimate,
|
||||
tenantMetadata: TenantMetadata
|
||||
): string => {
|
||||
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||
tenantId,
|
||||
SMS_NOTIFICATION_KEY.SALE_ESTIMATE_DETAILS
|
||||
);
|
||||
return this.formateEstimateDetailsMessage(
|
||||
notification.smsMessage,
|
||||
saleEstimate,
|
||||
tenantMetadata
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formattes the estimate sms notification details message.
|
||||
* @param {string} smsMessage
|
||||
* @param {ISaleEstimate} saleEstimate
|
||||
* @param {TenantMetadata} tenantMetadata
|
||||
* @returns {string}
|
||||
*/
|
||||
private formateEstimateDetailsMessage = (
|
||||
smsMessage: string,
|
||||
saleEstimate: ISaleEstimate & { customer: ICustomer },
|
||||
tenantMetadata: TenantMetadata
|
||||
) => {
|
||||
const formattedAmount = formatNumber(saleEstimate.amount, {
|
||||
currencyCode: saleEstimate.currencyCode,
|
||||
});
|
||||
|
||||
return formatSmsMessage(smsMessage, {
|
||||
EstimateNumber: saleEstimate.estimateNumber,
|
||||
ReferenceNumber: saleEstimate.reference,
|
||||
EstimateDate: moment(saleEstimate.estimateDate).format('YYYY/MM/DD'),
|
||||
ExpirationDate: saleEstimate.expirationDate
|
||||
? moment(saleEstimate.expirationDate).format('YYYY/MM/DD')
|
||||
: '',
|
||||
CustomerName: saleEstimate.customer.displayName,
|
||||
Amount: formattedAmount,
|
||||
CompanyName: tenantMetadata.name,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the SMS details of the given payment receive transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<IPaymentReceiveSmsDetails>}
|
||||
*/
|
||||
public smsDetails = async (
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<IPaymentReceiveSmsDetails> => {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const saleEstimate = await SaleEstimate.query()
|
||||
.findById(saleEstimateId)
|
||||
.withGraphFetched('customer');
|
||||
|
||||
this.validateEstimateExistance(saleEstimate);
|
||||
|
||||
// Retrieve the current tenant metadata.
|
||||
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Retrieve the formatted sms message from the given estimate model.
|
||||
const formattedSmsMessage = this.formattedEstimateDetailsMessage(
|
||||
tenantId,
|
||||
saleEstimate,
|
||||
tenantMetadata
|
||||
);
|
||||
return {
|
||||
customerName: saleEstimate.customer.displayName,
|
||||
customerPhoneNumber: saleEstimate.customer.personalPhone,
|
||||
smsMessage: formattedSmsMessage,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the sale estimate existance.
|
||||
* @param {ISaleEstimate} saleEstimate -
|
||||
*/
|
||||
private validateEstimateExistance(saleEstimate: ISaleEstimate) {
|
||||
if (!saleEstimate) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Service } from 'typedi';
|
||||
import { ISaleEstimate } from '@/interfaces';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
export default class SaleEstimateTransfromer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedAmount',
|
||||
'formattedEstimateDate',
|
||||
'formattedExpirationDate',
|
||||
'formattedDeliveredAtDate',
|
||||
'formattedApprovedAtDate',
|
||||
'formattedRejectedAtDate',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedEstimateDate = (estimate: ISaleEstimate): string => {
|
||||
return this.formatDate(estimate.estimateDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedExpirationDate = (estimate: ISaleEstimate): string => {
|
||||
return this.formatDate(estimate.expirationDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedDeliveredAtDate = (estimate: ISaleEstimate): string => {
|
||||
return this.formatDate(estimate.deliveredAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedApprovedAtDate = (estimate: ISaleEstimate): string => {
|
||||
return this.formatDate(estimate.approvedAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate date.
|
||||
* @param {ISaleEstimate} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedRejectedAtDate = (estimate: ISaleEstimate): string => {
|
||||
return this.formatDate(estimate.rejectedAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ISaleEstimate} estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (estimate: ISaleEstimate): string => {
|
||||
return formatNumber(estimate.amount, {
|
||||
currencyCode: estimate.currencyCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PdfService from '@/services/PDF/PdfService';
|
||||
import { templateRender } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
pdfService: PdfService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
*/
|
||||
async saleEstimatePdf(tenantId: number, saleEstimate) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const htmlContent = templateRender('modules/estimate-regular', {
|
||||
saleEstimate,
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
...i18n,
|
||||
});
|
||||
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
|
||||
|
||||
return pdfContent;
|
||||
}
|
||||
}
|
||||
109
packages/server/src/services/Sales/Estimates/constants.ts
Normal file
109
packages/server/src/services/Sales/Estimates/constants.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
export const ERRORS = {
|
||||
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
|
||||
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE',
|
||||
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
|
||||
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
|
||||
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'
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'Draft',
|
||||
slug: 'draft',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Delivered',
|
||||
slug: 'delivered',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'delivered',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Approved',
|
||||
slug: 'approved',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'approved',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Rejected',
|
||||
slug: 'rejected',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'rejected',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Invoiced',
|
||||
slug: 'invoiced',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'invoiced',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Expired',
|
||||
slug: 'expired',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'expired',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Closed',
|
||||
slug: 'closed',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'closed',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user