refactor: split the services to multiple service classes (#202)

This commit is contained in:
Ahmed Bouhuolia
2023-08-10 20:29:39 +02:00
committed by GitHub
parent ffef627dc3
commit 26c6ca9e36
150 changed files with 7188 additions and 5007 deletions

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import {
ISaleEstimateApprovedEvent,
ISaleEstimateApprovingEvent,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { ERRORS } from './constants';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import moment from 'moment';
@Service()
export class ApproveSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Mark the sale estimate as approved from the customer.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public async approveSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const oldSaleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.throwIfNotFound();
// Throws error in case the sale estimate still not delivered to customer.
if (!oldSaleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
}
// Throws error in case the sale estimate already approved.
if (oldSaleEstimate.isApproved) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_APPROVED);
}
// Triggers `onSaleEstimateApproving` event.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateApproving` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onApproving, {
trx,
tenantId,
oldSaleEstimate,
} as ISaleEstimateApprovingEvent);
// Update estimate as approved.
const saleEstimate = await SaleEstimate.query(trx)
.where('id', saleEstimateId)
.patch({
approvedAt: moment().toMySqlDateTime(),
rejectedAt: null,
});
// Triggers `onSaleEstimateApproved` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onApproved, {
trx,
tenantId,
oldSaleEstimate,
saleEstimate,
} as ISaleEstimateApprovedEvent);
});
}
}

View File

@@ -0,0 +1,46 @@
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { Knex } from 'knex';
import moment from 'moment';
import { Inject, Service } from 'typedi';
@Service()
export class ConvertSaleEstimate {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
/**
* Converts estimate to invoice.
* @param {number} tenantId -
* @param {number} estimateId -
* @return {Promise<void>}
*/
public async convertEstimateToInvoice(
tenantId: number,
estimateId: number,
invoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate.
const saleEstimate = await SaleEstimate.query()
.findById(estimateId)
.throwIfNotFound();
// Marks the estimate as converted from the givne invoice.
await SaleEstimate.query(trx).where('id', estimateId).patch({
convertedToInvoiceId: invoiceId,
convertedToInvoiceAt: moment().toMySqlDateTime(),
});
// Triggers `onSaleEstimateConvertedToInvoice` event.
await this.eventPublisher.emitAsync(
events.saleEstimate.onConvertedToInvoice,
{}
);
}
}

View File

@@ -0,0 +1,102 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import {
ISaleEstimate,
ISaleEstimateCreatedPayload,
ISaleEstimateCreatingPayload,
ISaleEstimateDTO,
} from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer';
import events from '@/subscribers/events';
import { SaleEstimateValidators } from './SaleEstimateValidators';
@Service()
export class CreateSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformerDTO: SaleEstimateDTOTransformer;
@Inject()
private validators: SaleEstimateValidators;
/**
* Creates a new estimate with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public async createEstimate(
tenantId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate, Contact } = this.tenancy.models(tenantId);
// Retrieve the given customer or throw not found service error.
const customer = await Contact.query()
.modify('customer')
.findById(estimateDTO.customerId)
.throwIfNotFound();
// Transform DTO object ot model object.
const estimateObj = await this.transformerDTO.transformDTOToModel(
tenantId,
estimateDTO,
customer
);
// Validate estimate number uniquiness on the storage.
await this.validators.validateEstimateNumberExistance(
tenantId,
estimateObj.estimateNumber
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
estimateDTO.entries
);
// Validate non-sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
estimateDTO.entries
);
// Creates a sale estimate transaction with associated transactions as UOW.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateCreating` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, {
estimateDTO,
tenantId,
trx,
} as ISaleEstimateCreatingPayload);
// Upsert the sale estimate graph to the storage.
const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({
...estimateObj,
});
// Triggers `onSaleEstimateCreated` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
tenantId,
saleEstimate,
saleEstimateId: saleEstimate.id,
saleEstimateDTO: estimateDTO,
trx,
} as ISaleEstimateCreatedPayload);
return saleEstimate;
});
}
}

View File

@@ -0,0 +1,74 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import {
ISaleEstimateDeletedPayload,
ISaleEstimateDeletingPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { ERRORS } from './constants';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
@Service()
export class DeleteSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Deletes the given estimate id with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {IEstimate} estimateId
* @return {void}
*/
public async deleteEstimate(
tenantId: number,
estimateId: number
): Promise<void> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
// Retrieve sale estimate or throw not found service error.
const oldSaleEstimate = await SaleEstimate.query()
.findById(estimateId)
.throwIfNotFound();
// Throw error if the sale estimate converted to sale invoice.
if (oldSaleEstimate.convertedToInvoiceId) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
}
// Deletes the estimate with associated transactions under UOW enivrement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimatedDeleting` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleting, {
trx,
tenantId,
oldSaleEstimate,
} as ISaleEstimateDeletingPayload);
// Delete sale estimate entries.
await ItemEntry.query(trx)
.where('reference_id', estimateId)
.where('reference_type', 'SaleEstimate')
.delete();
// Delete sale estimate transaction.
await SaleEstimate.query(trx).where('id', estimateId).delete();
// Triggers `onSaleEstimatedDeleted` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleted, {
tenantId,
saleEstimateId: estimateId,
oldSaleEstimate,
trx,
} as ISaleEstimateDeletedPayload);
});
}
}

View File

@@ -0,0 +1,71 @@
import { ServiceError } from '@/exceptions';
import {
ISaleEstimateEventDeliveredPayload,
ISaleEstimateEventDeliveringPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import moment from 'moment';
@Service()
export class DeliverSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Mark the sale estimate as delivered.
* @param {number} tenantId - Tenant id.
* @param {number} saleEstimateId - Sale estimate id.
*/
public async deliverSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const oldSaleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.throwIfNotFound();
// Throws error in case the sale estimate already published.
if (oldSaleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED);
}
// Updates the sale estimate transaction with assocaited transactions
// under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateDelivering` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivering, {
oldSaleEstimate,
trx,
tenantId,
} as ISaleEstimateEventDeliveringPayload);
// Record the delivered at on the storage.
const saleEstimate = await SaleEstimate.query(trx).patchAndFetchById(
saleEstimateId,
{
deliveredAt: moment().toMySqlDateTime(),
}
);
// Triggers `onSaleEstimateDelivered` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivered, {
tenantId,
saleEstimate,
trx,
} as ISaleEstimateEventDeliveredPayload);
});
}
}

View File

@@ -0,0 +1,123 @@
import { Inject, Service } from 'typedi';
import {
ISaleEstimate,
ISaleEstimateDTO,
ISaleEstimateEditedPayload,
ISaleEstimateEditingPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimateValidators } from './SaleEstimateValidators';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import events from '@/subscribers/events';
@Service()
export class EditSaleEstimate {
@Inject()
private validators: SaleEstimateValidators;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformerDTO: SaleEstimateDTOTransformer;
/**
* Edit details of the given estimate with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {Integer} estimateId
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public async editEstimate(
tenantId: number,
estimateId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate, Contact } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const oldSaleEstimate = await SaleEstimate.query().findById(estimateId);
// Validates the given estimate existance.
this.validators.validateEstimateExistance(oldSaleEstimate);
// Retrieve the given customer or throw not found service error.
const customer = await Contact.query()
.modify('customer')
.findById(estimateDTO.customerId)
.throwIfNotFound();
// Transform DTO object ot model object.
const estimateObj = await this.transformerDTO.transformDTOToModel(
tenantId,
estimateDTO,
oldSaleEstimate,
customer
);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
await this.validators.validateEstimateNumberExistance(
tenantId,
estimateDTO.estimateNumber,
estimateId
);
}
// Validate sale estimate entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
estimateId,
'SaleEstimate',
estimateDTO.entries
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
estimateDTO.entries
);
// Validate non-sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
estimateDTO.entries
);
// Edits estimate transaction with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx) => {
// Trigger `onSaleEstimateEditing` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onEditing, {
tenantId,
oldSaleEstimate,
estimateDTO,
trx,
} as ISaleEstimateEditingPayload);
// Upsert the estimate graph to the storage.
const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({
id: estimateId,
...estimateObj,
});
// Trigger `onSaleEstimateEdited` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onEdited, {
tenantId,
estimateId,
saleEstimate,
oldSaleEstimate,
trx,
} as ISaleEstimateEditedPayload);
return saleEstimate;
});
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
import { SaleEstimateValidators } from './SaleEstimateValidators';
@Service()
export class GetSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: SaleEstimateValidators;
/**
* Retrieve the estimate details with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {Integer} estimateId
*/
public async getEstimate(tenantId: number, estimateId: number) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const estimate = await SaleEstimate.query()
.findById(estimateId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch');
// Validates the estimate existance.
this.validators.validateEstimateExistance(estimate);
// Transformes sale estimate model to POJO.
return this.transformer.transform(
tenantId,
estimate,
new SaleEstimateTransfromer()
);
}
}

View File

@@ -0,0 +1,77 @@
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
import {
IFilterMeta,
IPaginationMeta,
ISaleEstimate,
ISalesEstimatesFilter,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
@Service()
export class GetSaleEstimates {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves estimates filterable and paginated list.
* @param {number} tenantId -
* @param {IEstimatesFilter} estimatesFilter -
*/
public async getEstimates(
tenantId: number,
filterDTO: ISalesEstimatesFilter
): Promise<{
salesEstimates: ISaleEstimate[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleEstimate,
filter
);
const { results, pagination } = await SaleEstimate.query()
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
const transformedEstimates = await this.transformer.transform(
tenantId,
results,
new SaleEstimateTransfromer()
);
return {
salesEstimates: transformedEstimates,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Parses the sale receipts list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,57 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export class RejectSaleEstimate {
@Inject()
private tenancy: TenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Mark the sale estimate as rejected from the customer.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public async rejectSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const saleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.throwIfNotFound();
// Throws error in case the sale estimate still not delivered to customer.
if (!saleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
}
// Throws error in case the sale estimate already rejected.
if (saleEstimate.isRejected) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_REJECTED);
}
//
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Mark the sale estimate as reject on the storage.
await SaleEstimate.query(trx).where('id', saleEstimateId).patch({
rejectedAt: moment().toMySqlDateTime(),
approvedAt: null,
});
// Triggers `onSaleEstimateRejected` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onRejected, {});
});
}
}

View File

@@ -0,0 +1,104 @@
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces';
import { SaleEstimateValidators } from './SaleEstimateValidators';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { formatDateFields } from '@/utils';
import moment from 'moment';
import { SaleEstimateIncrement } from './SaleEstimateIncrement';
@Service()
export class SaleEstimateDTOTransformer {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: SaleEstimateValidators;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private estimateIncrement: SaleEstimateIncrement;
/**
* Transform create DTO object ot model object.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO.
* @return {ISaleEstimate}
*/
async transformDTOToModel(
tenantId: number,
estimateDTO: ISaleEstimateDTO,
paymentCustomer: ICustomer,
oldSaleEstimate?: ISaleEstimate
): Promise<ISaleEstimate> {
const { ItemEntry, Contact } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e));
// Retreive the next invoice number.
const autoNextNumber =
this.estimateIncrement.getNextEstimateNumber(tenantId);
// Retreive the next estimate number.
const estimateNumber =
estimateDTO.estimateNumber ||
oldSaleEstimate?.estimateNumber ||
autoNextNumber;
// Validate the sale estimate number require.
this.validators.validateEstimateNoRequire(estimateNumber);
const initialDTO = {
amount,
...formatDateFields(omit(estimateDTO, ['delivered', 'entries']), [
'estimateDate',
'expirationDate',
]),
currencyCode: paymentCustomer.currencyCode,
exchangeRate: estimateDTO.exchangeRate || 1,
...(estimateNumber ? { estimateNumber } : {}),
entries: estimateDTO.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...entry,
})),
// Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
};
return R.compose(
this.branchDTOTransform.transformDTO<ISaleEstimate>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleEstimate>(tenantId)
)(initialDTO);
}
/**
* Retrieve estimate number to object model.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO
* @param {ISaleEstimate} oldSaleEstimate
*/
public transformEstimateNumberToModel(
tenantId: number,
saleEstimateDTO: ISaleEstimateDTO,
oldSaleEstimate?: ISaleEstimate
): string {
// Retreive the next invoice number.
const autoNextNumber =
this.estimateIncrement.getNextEstimateNumber(tenantId);
if (saleEstimateDTO.estimateNumber) {
return saleEstimateDTO.estimateNumber;
}
return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber;
}
}

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
@Service()
export class SaleEstimateIncrement {
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
/**
* Retrieve the next unique estimate number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextEstimateNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'sales_estimates'
);
}
/**
* Increment the estimate next number.
* @param {number} tenantId -
*/
public incrementNextEstimateNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'sales_estimates'
);
}
}

View File

@@ -4,7 +4,6 @@ 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,
@@ -21,18 +20,18 @@ const ERRORS = {
};
@Service()
export default class SaleEstimateNotifyBySms {
export class SaleEstimateNotifyBySms {
@Inject()
tenancy: HasTenancyService;
private tenancy: HasTenancyService;
@Inject()
saleSmsNotification: SaleNotifyBySms;
private saleSmsNotification: SaleNotifyBySms;
@Inject()
eventPublisher: EventPublisher;
private eventPublisher: EventPublisher;
@Inject()
smsNotificationsSettings: SmsNotificationsSettingsService;
private smsNotificationsSettings: SmsNotificationsSettingsService;
/**
*
@@ -187,6 +186,7 @@ export default class SaleEstimateNotifyBySms {
.findById(saleEstimateId)
.withGraphFetched('customer');
// Validates the estimate existance.
this.validateEstimateExistance(saleEstimate);
// Retrieve the current tenant metadata.

View File

@@ -3,7 +3,7 @@ import { ISaleEstimate } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export default class SaleEstimateTransfromer extends Transformer {
export class SaleEstimateTransfromer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}

View File

@@ -0,0 +1,87 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ISaleEstimate } from '@/interfaces';
import { ERRORS } from './constants';
import { SaleEstimate } from '@/models';
@Service()
export class SaleEstimateValidators {
@Inject()
private tenancy: HasTenancyService;
/**
* Validates the given estimate existance.
* @param {SaleEstimate | undefined | null} estimate -
*/
public validateEstimateExistance(estimate: SaleEstimate | undefined | null) {
if (!estimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
}
}
/**
* Validate the estimate number unique on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
public async validateEstimateNumberExistance(
tenantId: number,
estimateNumber: string,
notEstimateId?: number
) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const foundSaleEstimate = await SaleEstimate.query()
.findOne('estimate_number', estimateNumber)
.onBuild((builder) => {
if (notEstimateId) {
builder.whereNot('id', notEstimateId);
}
});
if (foundSaleEstimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE);
}
}
/**
* Validates the given sale estimate not already converted to invoice.
* @param {ISaleEstimate} saleEstimate -
*/
public validateEstimateNotConverted(saleEstimate: ISaleEstimate) {
if (saleEstimate.isConvertedToInvoice) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
}
}
/**
* Validate the sale estimate number require.
* @param {ISaleEstimate} saleInvoiceObj
*/
public validateEstimateNoRequire(estimateNumber: string) {
if (!estimateNumber) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED);
}
}
/**
* Validate the given customer has no sales estimates.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoEstimates(
tenantId: number,
customerId: number
) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const estimates = await SaleEstimate.query().where(
'customer_id',
customerId
);
if (estimates.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_ESTIMATES);
}
}
}

View File

@@ -0,0 +1,212 @@
import { Inject, Service } from 'typedi';
import { CreateSaleEstimate } from './CreateSaleEstimate';
import {
IFilterMeta,
IPaginationMeta,
IPaymentReceiveSmsDetails,
ISaleEstimate,
ISaleEstimateDTO,
ISalesEstimatesFilter,
} from '@/interfaces';
import { EditSaleEstimate } from './EditSaleEstimate';
import { DeleteSaleEstimate } from './DeleteSaleEstimate';
import { GetSaleEstimate } from './GetSaleEstimate';
import { GetSaleEstimates } from './GetSaleEstimates';
import { DeliverSaleEstimate } from './DeliverSaleEstimate';
import { ApproveSaleEstimate } from './ApproveSaleEstimate';
import { RejectSaleEstimate } from './RejectSaleEstimate';
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
@Service()
export class SaleEstimatesApplication {
@Inject()
private createSaleEstimateService: CreateSaleEstimate;
@Inject()
private editSaleEstimateService: EditSaleEstimate;
@Inject()
private deleteSaleEstimateService: DeleteSaleEstimate;
@Inject()
private getSaleEstimateService: GetSaleEstimate;
@Inject()
private getSaleEstimatesService: GetSaleEstimates;
@Inject()
private deliverSaleEstimateService: DeliverSaleEstimate;
@Inject()
private approveSaleEstimateService: ApproveSaleEstimate;
@Inject()
private rejectSaleEstimateService: RejectSaleEstimate;
@Inject()
private saleEstimateNotifyBySmsService: SaleEstimateNotifyBySms;
@Inject()
private saleEstimatesPdfService: SaleEstimatesPdf;
/**
* Create a sale estimate.
* @param {number} tenantId - The tenant id.
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public createSaleEstimate(
tenantId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
return this.createSaleEstimateService.createEstimate(tenantId, estimateDTO);
}
/**
* Edit the given sale estimate.
* @param {number} tenantId - The tenant id.
* @param {Integer} estimateId
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public editSaleEstimate(
tenantId: number,
estimateId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
return this.editSaleEstimateService.editEstimate(
tenantId,
estimateId,
estimateDTO
);
}
/**
* Deletes the given sale estimate.
* @param {number} tenantId -
* @param {number} estimateId -
* @return {Promise<void>}
*/
public deleteSaleEstimate(
tenantId: number,
estimateId: number
): Promise<void> {
return this.deleteSaleEstimateService.deleteEstimate(tenantId, estimateId);
}
/**
* Retrieves the given sale estimate.
* @param {number} tenantId
* @param {number} estimateId
*/
public getSaleEstimate(tenantId: number, estimateId: number) {
return this.getSaleEstimateService.getEstimate(tenantId, estimateId);
}
/**
* Retrieves the sale estimate.
* @param {number} tenantId
* @param {ISalesEstimatesFilter} filterDTO
* @returns
*/
public getSaleEstimates(
tenantId: number,
filterDTO: ISalesEstimatesFilter
): Promise<{
salesEstimates: ISaleEstimate[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
return this.getSaleEstimatesService.getEstimates(tenantId, filterDTO);
}
/**
* Deliver the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<void>}
*/
public deliverSaleEstimate(tenantId: number, saleEstimateId: number) {
return this.deliverSaleEstimateService.deliverSaleEstimate(
tenantId,
saleEstimateId
);
}
/**
* Approve the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<void>}
*/
public approveSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
return this.approveSaleEstimateService.approveSaleEstimate(
tenantId,
saleEstimateId
);
}
/**
* Mark the sale estimate as rejected from the customer.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public async rejectSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
return this.rejectSaleEstimateService.rejectSaleEstimate(
tenantId,
saleEstimateId
);
}
/**
* Notify the customer of the given sale estimate by SMS.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<ISaleEstimate>}
*/
public notifySaleEstimateBySms = async (
tenantId: number,
saleEstimateId: number
): Promise<ISaleEstimate> => {
return this.saleEstimateNotifyBySmsService.notifyBySms(
tenantId,
saleEstimateId
);
};
/**
* Retrieve the SMS details of the given payment receive transaction.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<IPaymentReceiveSmsDetails>}
*/
public getSaleEstimateSmsDetails = (
tenantId: number,
saleEstimateId: number
): Promise<IPaymentReceiveSmsDetails> => {
return this.saleEstimateNotifyBySmsService.smsDetails(
tenantId,
saleEstimateId
);
};
/**
*
* @param {number} tenantId
* @param {} saleEstimate
* @returns
*/
public getSaleEstimatePdf(tenantId: number, saleEstimate) {
return this.saleEstimatesPdfService.getSaleEstimatePdf(
tenantId,
saleEstimate
);
}
}

View File

@@ -5,18 +5,18 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Tenant } from '@/system/models';
@Service()
export default class SaleEstimatesPdf {
export class SaleEstimatesPdf {
@Inject()
pdfService: PdfService;
private pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
private tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleEstimatePdf(tenantId: number, saleEstimate) {
async getSaleEstimatePdf(tenantId: number, saleEstimate) {
const i18n = this.tenancy.i18n(tenantId);
const organization = await Tenant.query()

View File

@@ -0,0 +1,32 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class UnlinkConvertedSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
/**
* Unlink the converted sale estimates from the given sale invoice.
* @param {number} tenantId -
* @param {number} invoiceId -
* @return {Promise<void>}
*/
public async unlinkConvertedEstimateFromInvoice(
tenantId: number,
invoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
await SaleEstimate.query(trx)
.where({
convertedToInvoiceId: invoiceId,
})
.patch({
convertedToInvoiceId: null,
convertedToInvoiceAt: null,
});
}
}