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,
});
}
}

View File

@@ -0,0 +1,103 @@
import { Service, Inject } from 'typedi';
import { omit, sumBy } from 'lodash';
import * as R from 'ramda';
import moment from 'moment';
import composeAsync from 'async/compose';
import {
ISaleInvoice,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
ICustomer,
ITenantUser,
} from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
import { formatDateFields } from 'utils';
@Service()
export class CommandSaleInvoiceDTOTransformer {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private validators: CommandSaleInvoiceValidators;
@Inject()
private invoiceIncrement: SaleInvoiceIncrement;
/**
* Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
* @param {ISaleInvoice} oldSaleInvoice - Old sale invoice.
* @return {ISaleInvoice}
*/
public async transformDTOToModel(
tenantId: number,
customer: ICustomer,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
authorizedUser: ITenantUser,
oldSaleInvoice?: ISaleInvoice
): Promise<ISaleInvoice> {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
// Retreive the next invoice number.
const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber(tenantId);
// Invoice number.
const invoiceNo =
saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber;
// Validate the invoice is required.
this.validators.validateInvoiceNoRequire(invoiceNo);
const initialEntries = saleInvoiceDTO.entries.map((entry) => ({
referenceType: 'SaleInvoice',
...entry,
}));
const entries = await composeAsync(
// Sets default cost and sell account to invoice items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
)(initialEntries);
const initialDTO = {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
['invoiceDate', 'dueDate']
),
// Avoid rewrite the deliver date in edit mode when already published.
balance,
currencyCode: customer.currencyCode,
exchangeRate: saleInvoiceDTO.exchangeRate || 1,
...(saleInvoiceDTO.delivered &&
!oldSaleInvoice?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
// Avoid override payment amount in edit mode.
...(!oldSaleInvoice && { paymentAmount: 0 }),
...(invoiceNo ? { invoiceNo } : {}),
entries,
userId: authorizedUser.id,
} as ISaleInvoice;
return R.compose(
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
)(initialDTO);
}
}

View File

@@ -0,0 +1,86 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { SaleInvoice } from '@/models';
import { ERRORS } from './constants';
@Service()
export class CommandSaleInvoiceValidators {
@Inject()
private tenancy: HasTenancyService;
/**
* Validates the given invoice is existance.
* @param {SaleInvoice | undefined} invoice
*/
public validateInvoiceExistance(invoice: SaleInvoice | undefined) {
if (!invoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
}
/**
* Validate whether sale invoice number unqiue on the storage.
*/
public async validateInvoiceNumberUnique(
tenantId: number,
invoiceNumber: string,
notInvoiceId?: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findOne('invoice_no', invoiceNumber)
.onBuild((builder) => {
if (notInvoiceId) {
builder.whereNot('id', notInvoiceId);
}
});
if (saleInvoice) {
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE);
}
}
/**
* Validate the invoice amount is bigger than payment amount before edit the invoice.
* @param {number} saleInvoiceAmount
* @param {number} paymentAmount
*/
public validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceAmount: number,
paymentAmount: number
) {
if (saleInvoiceAmount < paymentAmount) {
throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT);
}
}
/**
* Validate the invoice number require.
* @param {ISaleInvoice} saleInvoiceObj
*/
public validateInvoiceNoRequire(invoiceNo: string) {
if (!invoiceNo) {
throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED);
}
}
/**
* Validate the given customer has no sales invoices.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoInvoices(
tenantId: number,
customerId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoices = await SaleInvoice.query().where('customer_id', customerId);
if (invoices.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}

View File

@@ -0,0 +1,147 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ICustomer,
ISaleInvoice,
ISaleInvoiceCreateDTO,
ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod,
ITenantUser,
} from '@/interfaces';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer';
import { SaleEstimateValidators } from '../Estimates/SaleEstimateValidators';
@Service()
export class CreateSaleInvoice {
@Inject()
private tenancy: TenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private validators: CommandSaleInvoiceValidators;
@Inject()
private transformerDTO: CommandSaleInvoiceDTOTransformer;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private commandEstimateValidators: SaleEstimateValidators;
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
* @async
* @param {number} tenantId - Tenant id.
* @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO.
* @return {Promise<ISaleInvoice>}
*/
public createSaleInvoice = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
): Promise<ISaleInvoice> => {
const { SaleInvoice, SaleEstimate, Contact } =
this.tenancy.models(tenantId);
// Validate customer existance.
const customer = await Contact.query()
.modify('customer')
.findById(saleInvoiceDTO.customerId)
.throwIfNotFound();
// Validate the from estimate id exists on the storage.
if (saleInvoiceDTO.fromEstimateId) {
const fromEstimate = await SaleEstimate.query()
.findById(saleInvoiceDTO.fromEstimateId)
.throwIfNotFound();
// Validate the sale estimate is not already converted to invoice.
this.commandEstimateValidators.validateEstimateNotConverted(fromEstimate);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformCreateDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validators.validateInvoiceNumberUnique(
tenantId,
saleInvoiceObj.invoiceNo
);
}
// Creates a new sale invoice and associated transactions under unit of work env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceCreating` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
saleInvoiceDTO,
tenantId,
trx,
} as ISaleInvoiceCreatingPaylaod);
// Create sale invoice graph to the storage.
const saleInvoice = await SaleInvoice.query(trx).upsertGraph(
saleInvoiceObj
);
const eventPayload: ISaleInvoiceCreatedPayload = {
tenantId,
saleInvoice,
saleInvoiceDTO,
saleInvoiceId: saleInvoice.id,
authorizedUser,
trx,
};
// Triggers the event `onSaleInvoiceCreated`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onCreated,
eventPayload
);
return saleInvoice;
});
};
/**
* Transformes create DTO to model.
* @param {number} tenantId -
* @param {ICustomer} customer -
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO -
*/
private transformCreateDTOToModel = async (
tenantId: number,
customer: ICustomer,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
) => {
return this.transformerDTO.transformDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser
);
};
}

View File

@@ -0,0 +1,154 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
ISystemUser,
ISaleInvoiceDeletePayload,
ISaleInvoiceDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import { UnlinkConvertedSaleEstimate } from '../Estimates/UnlinkConvertedSaleEstimate';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class DeleteSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Validate the sale invoice has no payment entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
*/
private async validateInvoiceHasNoPaymentEntries(
tenantId: number,
saleInvoiceId: number
) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
// Retrieve the sale invoice associated payment receive entries.
const entries = await PaymentReceiveEntry.query().where(
'invoice_id',
saleInvoiceId
);
if (entries.length > 0) {
throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
}
/**
* Validate the sale invoice has no applied to credit note transaction.
* @param {number} tenantId
* @param {number} invoiceId
* @returns {Promise<void>}
*/
public validateInvoiceHasNoAppliedToCredit = async (
tenantId: number,
invoiceId: number
): Promise<void> => {
const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId);
const appliedTransactions = await CreditNoteAppliedInvoice.query().where(
'invoiceId',
invoiceId
);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES);
}
};
/**
* Validate whether sale invoice exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async getInvoiceOrThrowError(
tenantId: number,
saleInvoiceId: number
) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const saleInvoice = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries'
);
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
return saleInvoice;
}
/**
* Deletes the given sale invoice with associated entries
* and journal transactions.
* @param {number} tenantId - Tenant id.
* @param {Number} saleInvoiceId - The given sale invoice id.
* @param {ISystemUser} authorizedUser -
*/
public async deleteSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { ItemEntry, SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve the given sale invoice with associated entries
// or throw not found error.
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Validate the sale invoice has no associated payment entries.
await this.validateInvoiceHasNoPaymentEntries(tenantId, saleInvoiceId);
// Validate the sale invoice has applied to credit note transaction.
await this.validateInvoiceHasNoAppliedToCredit(tenantId, saleInvoiceId);
// Deletes sale invoice transaction and associate transactions with UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDelete` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, {
tenantId,
saleInvoice: oldSaleInvoice,
saleInvoiceId,
trx,
} as ISaleInvoiceDeletePayload);
// Unlink the converted sale estimates from the given sale invoice.
await this.unlockEstimateFromInvoice.unlinkConvertedEstimateFromInvoice(
tenantId,
saleInvoiceId,
trx
);
await ItemEntry.query(trx)
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice')
.delete();
await SaleInvoice.query(trx).findById(saleInvoiceId).delete();
// Triggers `onSaleInvoiceDeleted` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, {
tenantId,
oldSaleInvoice,
saleInvoiceId,
authorizedUser,
trx,
} as ISaleInvoiceDeletedPayload);
});
}
}

View File

@@ -0,0 +1,77 @@
import { Knex } from 'knex';
import moment from 'moment';
import { ServiceError } from '@/exceptions';
import {
ISaleInvoiceDeliveringPayload,
ISaleInvoiceEventDeliveredPayload,
ISystemUser,
} from '@/interfaces';
import { ERRORS } from './constants';
import { Inject, Service } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
@Service()
export class DeliverSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: CommandSaleInvoiceValidators;
/**
* Deliver the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
* @return {Promise<void>}
*/
public async deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve details of the given sale invoice id.
const oldSaleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(oldSaleInvoice);
// Throws error in case the sale invoice already published.
if (oldSaleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Update sale invoice transaction with assocaite transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDelivering` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivering, {
tenantId,
oldSaleInvoice,
trx,
} as ISaleInvoiceDeliveringPayload);
// Record the delivered at on the storage.
const saleInvoice = await SaleInvoice.query(trx)
.where({ id: saleInvoiceId })
.update({ deliveredAt: moment().toMySqlDateTime() });
// Triggers `onSaleInvoiceDelivered` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, {
tenantId,
saleInvoiceId,
saleInvoice,
} as ISaleInvoiceEventDeliveredPayload);
});
}
}

View File

@@ -0,0 +1,165 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
ICustomer,
ISaleInvoice,
ISaleInvoiceEditDTO,
ISaleInvoiceEditedPayload,
ISaleInvoiceEditingPayload,
ISystemUser,
ITenantUser,
} from '@/interfaces';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer';
import events from '@/subscribers/events';
@Service()
export class EditSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private validators: CommandSaleInvoiceValidators;
@Inject()
private transformerDTO: CommandSaleInvoiceDTOTransformer;
@Inject()
private uow: UnitOfWork;
/**
* Edit the given sale invoice.
* @async
* @param {number} tenantId - Tenant id.
* @param {Number} saleInvoiceId - Sale invoice id.
* @param {ISaleInvoice} saleInvoice - Sale invoice DTO object.
* @return {Promise<ISaleInvoice>}
*/
public async editSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceDTO: ISaleInvoiceEditDTO,
authorizedUser: ISystemUser
): Promise<ISaleInvoice> {
const { SaleInvoice, Contact } = this.tenancy.models(tenantId);
// Retrieve the sale invoice or throw not found service error.
const oldSaleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphJoined('entries');
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(oldSaleInvoice);
// Validate customer existance.
const customer = await Contact.query()
.findById(saleInvoiceDTO.customerId)
.modify('customer')
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
saleInvoiceId,
'SaleInvoice',
saleInvoiceDTO.entries
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.tranformEditDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
oldSaleInvoice,
authorizedUser
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validators.validateInvoiceNumberUnique(
tenantId,
saleInvoiceObj.invoiceNo,
saleInvoiceId
);
}
// Validate the invoice amount is not smaller than the invoice payment amount.
this.validators.validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceObj.balance,
oldSaleInvoice.paymentAmount
);
// Edit sale invoice transaction in UOW envirment.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceEditing` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, {
trx,
oldSaleInvoice,
tenantId,
saleInvoiceDTO,
} as ISaleInvoiceEditingPayload);
// Upsert the the invoice graph to the storage.
const saleInvoice: ISaleInvoice =
await SaleInvoice.query().upsertGraphAndFetch({
id: saleInvoiceId,
...saleInvoiceObj,
});
// Edit event payload.
const editEventPayload: ISaleInvoiceEditedPayload = {
tenantId,
saleInvoiceId,
saleInvoice,
saleInvoiceDTO,
oldSaleInvoice,
authorizedUser,
trx,
};
// Triggers `onSaleInvoiceEdited` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onEdited,
editEventPayload
);
return saleInvoice;
});
}
/**
* Transformes edit DTO to model.
* @param {number} tennatId -
* @param {ICustomer} customer -
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO -
* @param {ISaleInvoice} oldSaleInvoice
*/
private tranformEditDTOToModel = async (
tenantId: number,
customer: ICustomer,
saleInvoiceDTO: ISaleInvoiceEditDTO,
oldSaleInvoice: ISaleInvoice,
authorizedUser: ITenantUser
) => {
return this.transformerDTO.transformDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser,
oldSaleInvoice
);
};
}

View File

@@ -4,7 +4,7 @@ import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransactio
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class InvoicePaymentsService {
export class GetInvoicePaymentsService {
@Inject()
private tenancy: HasTenancyService;

View File

@@ -0,0 +1,47 @@
import { Inject, Service } from 'typedi';
import { ISaleInvoice, ISystemUser } from '@/interfaces';
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
@Service()
export class GetSaleInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: CommandSaleInvoiceValidators;
/**
* Retrieve sale invoice with associated entries.
* @param {Number} saleInvoiceId -
* @param {ISystemUser} authorizedUser -
* @return {Promise<ISaleInvoice>}
*/
public async getSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch');
// Validates the given sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
return this.transformer.transform(
tenantId,
saleInvoice,
new SaleInvoiceTransformer()
);
}
}

View File

@@ -0,0 +1,80 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import {
IFilterMeta,
IPaginationMeta,
ISaleInvoice,
ISalesInvoicesFilter,
} from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
@Service()
export class GetSaleInvoices {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve sales invoices filterable and paginated list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getSaleInvoices(
tenantId: number,
filterDTO: ISalesInvoicesFilter
): Promise<{
salesInvoices: ISaleInvoice[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleInvoice,
filter
);
const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Retrieves the transformed sale invoices.
const salesInvoices = await this.transformer.transform(
tenantId,
results,
new SaleInvoiceTransformer()
);
return {
salesInvoices,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import { ISaleInvoice } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class GetSaleInvoicesPayable {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieve due sales invoices.
* @param {number} tenantId
* @param {number} customerId
*/
public async getPayableInvoices(
tenantId: number,
customerId?: number
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const salesInvoices = await SaleInvoice.query().onBuild((query) => {
query.modify('dueInvoices');
query.modify('delivered');
if (customerId) {
query.where('customer_id', customerId);
}
});
return salesInvoices;
}
}

View File

@@ -88,8 +88,8 @@ export class SaleInvoiceGLEntries {
/**
* Retrieves the given invoice ledger.
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @returns {ILedger}
*/
public getInvoiceGLedger = (
@@ -103,7 +103,7 @@ export class SaleInvoiceGLEntries {
/**
* Retrieves the invoice GL common entry.
* @param {ISaleInvoice} saleInvoice
* @param {ISaleInvoice} saleInvoice
* @returns {Partial<ILedgerEntry>}
*/
private getInvoiceGLCommonEntry = (
@@ -131,8 +131,8 @@ export class SaleInvoiceGLEntries {
/**
* Retrieve receivable entry of the given invoice.
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @returns {ILedgerEntry}
*/
private getInvoiceReceivableEntry = (
@@ -153,9 +153,9 @@ export class SaleInvoiceGLEntries {
/**
* Retrieve item income entry of the given invoice.
* @param {ISaleInvoice} saleInvoice -
* @param {IItemEntry} entry -
* @param {number} index -
* @param {ISaleInvoice} saleInvoice -
* @param {IItemEntry} entry -
* @param {number} index -
* @returns {ILedgerEntry}
*/
private getInvoiceItemEntry = R.curry(
@@ -183,8 +183,8 @@ export class SaleInvoiceGLEntries {
/**
* Retrieves the invoice GL entries.
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @returns {ILedgerEntry[]}
*/
public getInvoiceGLEntries = (

View File

@@ -0,0 +1,77 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ISaleInvoice } from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import InventoryService from '@/services/Inventory/Inventory';
@Service()
export class InvoiceInventoryTransactions {
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private inventoryService: InventoryService;
/**
* Records the inventory transactions of the given sale invoice in case
* the invoice has inventory entries only.
*
* @param {number} tenantId - Tenant id.
* @param {SaleInvoice} saleInvoice - Sale invoice DTO.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {boolean} override - Allow to override old transactions.
* @return {Promise<void>}
*/
public async recordInventoryTranscactions(
tenantId: number,
saleInvoice: ISaleInvoice,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
saleInvoice.entries
);
const transaction = {
transactionId: saleInvoice.id,
transactionType: 'SaleInvoice',
transactionNumber: saleInvoice.invoiceNo,
exchangeRate: saleInvoice.exchangeRate,
warehouseId: saleInvoice.warehouseId,
date: saleInvoice.invoiceDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleInvoice.createdAt,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
override,
trx
);
}
/**
* Reverting the inventory transactions once the invoice deleted.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
// Delete the inventory transaction of the given sale invoice.
const { oldInventoryTransactions } =
await this.inventoryService.deleteInventoryTransactions(
tenantId,
saleInvoiceId,
'SaleInvoice',
trx
);
}
}

View File

@@ -21,6 +21,11 @@ export class InvoicePaymentTransactionTransformer extends Transformer {
});
};
/**
* Formatted payment date.
* @param entry
* @returns {string}
*/
protected formattedPaymentDate = (entry): string => {
return this.formatDate(entry.payment.paymentDate);
};

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex';
import async from 'async';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceiveGLEntries } from '../PaymentReceives/PaymentReceiveGLEntries';
@Service()

View File

@@ -64,7 +64,7 @@ export class SaleInvoiceCostGLEntries {
/**
*
* @param {IInventoryLotCost} inventoryCostLot
* @param {IInventoryLotCost} inventoryCostLot
* @returns {}
*/
private getInvoiceCostGLCommonEntry = (
@@ -91,7 +91,7 @@ export class SaleInvoiceCostGLEntries {
/**
* Retrieves the inventory cost GL entry.
* @param {IInventoryLotCost} inventoryLotCost
* @param {IInventoryLotCost} inventoryLotCost
* @returns {ILedgerEntry[]}
*/
private getInventoryCostGLEntry = R.curry(
@@ -127,10 +127,10 @@ export class SaleInvoiceCostGLEntries {
/**
* Writes journal entries for given sale invoice.
* -------
* -----
* - Cost of goods sold -> Debit -> YYYY
* - Inventory assets -> Credit -> YYYY
* --------
*-----
* @param {ISaleInvoice} saleInvoice
* @param {JournalPoster} journal
*/

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
@Service()
export class SaleInvoiceIncrement {
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
/**
* Retrieves the next unique invoice number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextInvoiceNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'sales_invoices'
);
}
/**
* Increment the invoice next number.
* @param {number} tenantId -
*/
public incrementNextInvoiceNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'sales_invoices'
);
}
}

View File

@@ -2,8 +2,6 @@ import { Service, Inject } from 'typedi';
import moment from 'moment';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import SaleInvoicesService from './SalesInvoices';
import SMSClient from '@/services/SMSClient';
import {
ISaleInvoice,
ISaleInvoiceSmsDetailsDTO,
@@ -15,27 +13,28 @@ import {
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
import { formatSmsMessage, formatNumber } from 'utils';
import { TenantMetadata } from '@/system/models';
import SaleNotifyBySms from './SaleNotifyBySms';
import SaleNotifyBySms from '../SaleNotifyBySms';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ERRORS } from './constants';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
@Service()
export default class SaleInvoiceNotifyBySms {
export class SaleInvoiceNotifyBySms {
@Inject()
invoiceService: SaleInvoicesService;
private tenancy: HasTenancyService;
@Inject()
tenancy: HasTenancyService;
private eventPublisher: EventPublisher;
@Inject()
eventPublisher: EventPublisher;
private smsNotificationsSettings: SmsNotificationsSettingsService;
@Inject()
smsNotificationsSettings: SmsNotificationsSettingsService;
private saleSmsNotification: SaleNotifyBySms;
@Inject()
saleSmsNotification: SaleNotifyBySms;
private validators: CommandSaleInvoiceValidators;
/**
* Notify customer via sms about sale invoice.
@@ -54,6 +53,9 @@ export default class SaleInvoiceNotifyBySms {
.findById(saleInvoiceId)
.withGraphFetched('customer');
// Validates the givne invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the customer phone number existance and number validation.
this.saleSmsNotification.validateCustomerPhoneNumber(
saleInvoice.customer.personalPhone

View File

@@ -5,7 +5,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Tenant } from '@/system/models';
@Service()
export default class SaleInvoicePdf {
export class SaleInvoicePdf {
@Inject()
pdfService: PdfService;

View File

@@ -26,8 +26,8 @@ export class SaleInvoiceWriteoffGLEntries {
/**
* Retrieves the invoice write-off receiveable GL entry.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry}
*/
private getInvoiceWriteoffGLReceivableEntry = (
@@ -50,7 +50,7 @@ export class SaleInvoiceWriteoffGLEntries {
/**
* Retrieves the invoice write-off expense GL entry.
* @param {ISaleInvoice} saleInvoice
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry}
*/
private getInvoiceWriteoffGLExpenseEntry = (
@@ -71,8 +71,8 @@ export class SaleInvoiceWriteoffGLEntries {
/**
* Retrieves the invoice write-off GL entries.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry[]}
*/
public getInvoiceWriteoffGLEntries = (
@@ -89,8 +89,8 @@ export class SaleInvoiceWriteoffGLEntries {
/**
* Retrieves the invoice write-off ledger.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {Ledger}
*/
public getInvoiceWriteoffLedger = (

View File

@@ -0,0 +1,282 @@
import {
IFilterMeta,
IPaginationMeta,
ISaleInvoice,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
ISaleInvoiceSmsDetails,
ISaleInvoiceSmsDetailsDTO,
ISaleInvoiceWriteoffDTO,
ISalesInvoicesFilter,
ISystemUser,
ITenantUser,
InvoiceNotificationType,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { CreateSaleInvoice } from './CreateSaleInvoice';
import { DeleteSaleInvoice } from './DeleteSaleInvoice';
import { GetSaleInvoice } from './GetSaleInvoice';
import { EditSaleInvoice } from './EditSaleInvoice';
import { GetSaleInvoices } from './GetSaleInvoices';
import { DeliverSaleInvoice } from './DeliverSaleInvoice';
import { GetSaleInvoicesPayable } from './GetSaleInvoicesPayable';
import { WriteoffSaleInvoice } from './WriteoffSaleInvoice';
import { SaleInvoicePdf } from './SaleInvoicePdf';
import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
@Service()
export class SaleInvoiceApplication {
@Inject()
private createSaleInvoiceService: CreateSaleInvoice;
@Inject()
private deleteSaleInvoiceService: DeleteSaleInvoice;
@Inject()
private getSaleInvoiceService: GetSaleInvoice;
@Inject()
private getSaleInvoicesService: GetSaleInvoices;
@Inject()
private editSaleInvoiceService: EditSaleInvoice;
@Inject()
private deliverSaleInvoiceService: DeliverSaleInvoice;
@Inject()
private getReceivableSaleInvoicesService: GetSaleInvoicesPayable;
@Inject()
private writeoffInvoiceService: WriteoffSaleInvoice;
@Inject()
private getInvoicePaymentsService: GetInvoicePaymentsService;
@Inject()
private pdfSaleInvoiceService: SaleInvoicePdf;
@Inject()
private invoiceSms: SaleInvoiceNotifyBySms;
/**
* Creates a new sale invoice with associated GL entries.
* @param {number} tenantId
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO
* @param {ITenantUser} authorizedUser
* @returns {Promise<ISaleInvoice>}
*/
public createSaleInvoice(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
): Promise<ISaleInvoice> {
return this.createSaleInvoiceService.createSaleInvoice(
tenantId,
saleInvoiceDTO,
authorizedUser
);
}
/**
* Edits the given sale invoice with associated GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO
* @param {ISystemUser} authorizedUser
* @returns {Promise<ISaleInvoice>}
*/
public editSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceDTO: ISaleInvoiceEditDTO,
authorizedUser: ISystemUser
) {
return this.editSaleInvoiceService.editSaleInvoice(
tenantId,
saleInvoiceId,
saleInvoiceDTO,
authorizedUser
);
}
/**
* Deletes the given sale invoice with given associated GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISystemUser} authorizedUser
* @returns {Promise<void>}
*/
public deleteSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<void> {
return this.deleteSaleInvoiceService.deleteSaleInvoice(
tenantId,
saleInvoiceId,
authorizedUser
);
}
/**
* Retrieves the given sale invoice details.
* @param {number} tenantId
* @param {ISalesInvoicesFilter} filterDTO
* @returns
*/
public getSaleInvoices(
tenantId: number,
filterDTO: ISalesInvoicesFilter
): Promise<{
salesInvoices: ISaleInvoice[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
return this.getSaleInvoicesService.getSaleInvoices(tenantId, filterDTO);
}
/**
* Retrieves sale invoice details.
* @param {number} tenantId -
* @param {number} saleInvoiceId -
* @param {ISystemUser} authorizedUser -
* @return {Promise<ISaleInvoice>}
*/
public getSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
) {
return this.getSaleInvoiceService.getSaleInvoice(
tenantId,
saleInvoiceId,
authorizedUser
);
}
/**
* Mark the given sale invoice as delivered.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISystemUser} authorizedUser
* @returns {}
*/
public deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
) {
return this.deliverSaleInvoiceService.deliverSaleInvoice(
tenantId,
saleInvoiceId,
authorizedUser
);
}
/**
* Retrieves the receivable sale invoices of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @returns
*/
public getReceivableSaleInvoices(tenantId: number, customerId?: number) {
return this.getReceivableSaleInvoicesService.getPayableInvoices(
tenantId,
customerId
);
}
/**
* Writes-off the sale invoice on bad debt expense account.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
* @return {Promise<ISaleInvoice>}
*/
public writeOff = async (
tenantId: number,
saleInvoiceId: number,
writeoffDTO: ISaleInvoiceWriteoffDTO
): Promise<ISaleInvoice> => {
return this.writeoffInvoiceService.writeOff(
tenantId,
saleInvoiceId,
writeoffDTO
);
};
/**
* Cancels the written-off sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns {Promise<ISaleInvoice>}
*/
public cancelWrittenoff = (
tenantId: number,
saleInvoiceId: number
): Promise<ISaleInvoice> => {
return this.writeoffInvoiceService.cancelWrittenoff(
tenantId,
saleInvoiceId
);
};
/**
* Retrieve the invoice assocaited payments transactions.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
*/
public getInvoicePayments = async (tenantId: number, invoiceId: number) => {
return this.getInvoicePaymentsService.getInvoicePayments(
tenantId,
invoiceId
);
};
/**
*
* @param {number} tenantId ]
* @param saleInvoice
* @returns
*/
public saleInvoicePdf(tenantId: number, saleInvoice) {
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice);
}
/**
*
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {InvoiceNotificationType} invoiceNotificationType
*/
public notifySaleInvoiceBySms = async (
tenantId: number,
saleInvoiceId: number,
invoiceNotificationType: InvoiceNotificationType
) => {
return this.invoiceSms.notifyBySms(
tenantId,
saleInvoiceId,
invoiceNotificationType
);
};
/**
* Retrieves the SMS details of the given invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
*/
public getSaleInvoiceSmsDetails = async (
tenantId: number,
saleInvoiceId: number,
invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO
): Promise<ISaleInvoiceSmsDetails> => {
return this.invoiceSms.smsDetails(
tenantId,
saleInvoiceId,
invoiceSmsDetailsDTO
);
};
}

View File

@@ -3,27 +3,22 @@ import { chain } from 'lodash';
import moment from 'moment';
import { Knex } from 'knex';
import InventoryService from '@/services/Inventory/Inventory';
import TenancyService from '@/services/Tenancy/TenancyService';
import {
IInventoryCostLotsGLEntriesWriteEvent,
IInventoryTransaction,
} from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { SaleInvoiceCostGLEntries } from './Invoices/SaleInvoiceCostGLEntries';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class SaleInvoicesCost {
export class SaleInvoicesCost {
@Inject()
private inventoryService: InventoryService;
@Inject()
private uow: UnitOfWork;
@Inject()
private costGLEntries: SaleInvoiceCostGLEntries;
@Inject()
private eventPublisher: EventPublisher;
@@ -122,8 +117,8 @@ export default class SaleInvoicesCost {
/**
* Writes cost GL entries from the inventory cost lots.
* @param {number} tenantId -
* @param {Date} startingDate -
* @param {number} tenantId -
* @param {Date} startingDate -
* @returns {Promise<void>}
*/
public writeCostLotsGLEntries = (tenantId: number, startingDate: Date) => {

View File

@@ -10,29 +10,24 @@ import {
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { ServiceError } from '@/exceptions';
import JournalPosterService from './JournalPosterService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
const ERRORS = {
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF',
};
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { ERRORS } from './constants';
@Service()
export default class SaleInvoiceWriteoff {
export class WriteoffSaleInvoice {
@Inject()
tenancy: HasTenancyService;
private tenancy: HasTenancyService;
@Inject()
eventPublisher: EventPublisher;
private eventPublisher: EventPublisher;
@Inject()
journalService: JournalPosterService;
private uow: UnitOfWork;
@Inject()
uow: UnitOfWork;
private validators: CommandSaleInvoiceValidators;
/**
* Writes-off the sale invoice on bad debt expense account.
@@ -48,16 +43,15 @@ export default class SaleInvoiceWriteoff {
): Promise<ISaleInvoice> => {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Validate the sale invoice existance.
// Retrieve the sale invoice or throw not found service error.
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.throwIfNotFound();
const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the sale invoice whether already written-off.
this.validateSaleInvoiceAlreadyWrittenoff(saleInvoice);
// Saves the invoice write-off transaction with associated transactions
// Saves the invoice write-off transaction with associated transactions
// under unit-of-work envirmenet.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const eventPayload = {
@@ -105,15 +99,16 @@ export default class SaleInvoiceWriteoff {
// Validate the sale invoice existance.
// Retrieve the sale invoice or throw not found service error.
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.throwIfNotFound();
const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
// Validate the sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the sale invoice whether already written-off.
this.validateSaleInvoiceNotWrittenoff(saleInvoice);
// Cancels the invoice written-off and removes the associated transactions.
return this.uow.withTransaction(tenantId, async (trx) => {
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceWrittenoffCancel` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoffCancel,

View File

@@ -11,8 +11,11 @@ export const ERRORS = {
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES: 'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES',
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID'
SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES:
'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES',
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',
};
export const DEFAULT_VIEW_COLUMNS = [];

View File

@@ -0,0 +1,136 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ICustomer,
IPaymentReceiveCreateDTO,
IPaymentReceiveCreatedPayload,
IPaymentReceiveCreatingPayload,
ISystemUser,
} from '@/interfaces';
import { PaymentReceiveValidators } from './PaymentReceiveValidators';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { PaymentReceiveDTOTransformer } from './PaymentReceiveDTOTransformer';
import { TenantMetadata } from '@/system/models';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export class CreatePaymentReceive {
@Inject()
private validators: PaymentReceiveValidators;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformer: PaymentReceiveDTOTransformer;
/**
* Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions.
* @async
* @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive
*/
public async createPaymentReceive(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO,
authorizedUser: ISystemUser
) {
const { PaymentReceive, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Validate customer existance.
const paymentCustomer = await Contact.query()
.modify('customer')
.findById(paymentReceiveDTO.customerId)
.throwIfNotFound();
// Transformes the payment receive DTO to model.
const paymentReceiveObj = await this.transformCreateDTOToModel(
tenantId,
paymentCustomer,
paymentReceiveDTO
);
// Validate payment receive number uniquiness.
await this.validators.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveObj.paymentReceiveNo
);
// Validate the deposit account existance and type.
const depositAccount = await this.validators.getDepositAccountOrThrowError(
tenantId,
paymentReceiveDTO.depositAccountId
);
// Validate payment receive invoices IDs existance.
await this.validators.validateInvoicesIDsExistance(
tenantId,
paymentReceiveDTO.customerId,
paymentReceiveDTO.entries
);
// Validate invoice payment amount.
await this.validators.validateInvoicesPaymentsAmount(
tenantId,
paymentReceiveDTO.entries
);
// Validates the payment account currency code.
this.validators.validatePaymentAccountCurrency(
depositAccount.currencyCode,
paymentCustomer.currencyCode,
tenantMeta.baseCurrency
);
// Creates a payment receive transaction under UOW envirment.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveCreating` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, {
trx,
paymentReceiveDTO,
tenantId,
} as IPaymentReceiveCreatingPayload);
// Inserts the payment receive transaction.
const paymentReceive = await PaymentReceive.query(
trx
).insertGraphAndFetch({
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveCreated` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, {
tenantId,
paymentReceive,
paymentReceiveId: paymentReceive.id,
authorizedUser,
trx,
} as IPaymentReceiveCreatedPayload);
return paymentReceive;
});
}
/**
* Transform the create payment receive DTO.
* @param {number} tenantId
* @param {ICustomer} customer
* @param {IPaymentReceiveCreateDTO} paymentReceiveDTO
* @returns
*/
private transformCreateDTOToModel = async (
tenantId: number,
customer: ICustomer,
paymentReceiveDTO: IPaymentReceiveCreateDTO
) => {
return this.transformer.transformPaymentReceiveDTOToModel(
tenantId,
customer,
paymentReceiveDTO
);
};
}

View File

@@ -0,0 +1,79 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
IPaymentReceiveDeletedPayload,
IPaymentReceiveDeletingPayload,
ISystemUser,
} from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export class DeletePaymentReceive {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Deletes the given payment receive with associated entries
* and journal transactions.
* -----
* - Deletes the payment receive transaction.
* - Deletes the payment receive associated entries.
* - Deletes the payment receive associated journal transactions.
* - Revert the customer balance.
* - Revert the payment amount of the associated invoices.
* @async
* @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId - Payment receive id.
* @param {IPaymentReceive} paymentReceive - Payment receive object.
*/
public async deletePaymentReceive(
tenantId: number,
paymentReceiveId: number,
authorizedUser: ISystemUser
) {
const { PaymentReceive, PaymentReceiveEntry } =
this.tenancy.models(tenantId);
// Retreive payment receive or throw not found service error.
const oldPaymentReceive = await PaymentReceive.query()
.withGraphFetched('entries')
.findById(paymentReceiveId)
.throwIfNotFound();
// Delete payment receive transaction and associate transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveDeleting` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onDeleting, {
tenantId,
oldPaymentReceive,
trx,
} as IPaymentReceiveDeletingPayload);
// Deletes the payment receive associated entries.
await PaymentReceiveEntry.query(trx)
.where('payment_receive_id', paymentReceiveId)
.delete();
// Deletes the payment receive transaction.
await PaymentReceive.query(trx).findById(paymentReceiveId).delete();
// Triggers `onPaymentReceiveDeleted` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onDeleted, {
tenantId,
paymentReceiveId,
oldPaymentReceive,
authorizedUser,
trx,
} as IPaymentReceiveDeletedPayload);
});
}
}

View File

@@ -0,0 +1,177 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ICustomer,
IPaymentReceive,
IPaymentReceiveEditDTO,
IPaymentReceiveEditedPayload,
IPaymentReceiveEditingPayload,
ISystemUser,
} from '@/interfaces';
import { PaymentReceiveDTOTransformer } from './PaymentReceiveDTOTransformer';
import { PaymentReceiveValidators } from './PaymentReceiveValidators';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
@Service()
export class EditPaymentReceive {
@Inject()
private transformer: PaymentReceiveDTOTransformer;
@Inject()
private validators: PaymentReceiveValidators;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
/**
* Edit details the given payment receive with associated entries.
* ------
* - Update the payment receive transactions.
* - Insert the new payment receive entries.
* - Update the given payment receive entries.
* - Delete the not presented payment receive entries.
* - Re-insert the journal transactions and update the different accounts balance.
* - Update the different customer balances.
* - Update the different invoice payment amount.
* @async
* @param {number} tenantId -
* @param {Integer} paymentReceiveId -
* @param {IPaymentReceive} paymentReceive -
*/
public async editPaymentReceive(
tenantId: number,
paymentReceiveId: number,
paymentReceiveDTO: IPaymentReceiveEditDTO,
authorizedUser: ISystemUser
) {
const { PaymentReceive, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Validate the payment receive existance.
const oldPaymentReceive = await PaymentReceive.query()
.withGraphFetched('entries')
.findById(paymentReceiveId);
// Validates the payment existance.
this.validators.validatePaymentExistance(oldPaymentReceive);
// Validate customer existance.
const customer = await Contact.query()
.modify('customer')
.findById(paymentReceiveDTO.customerId)
.throwIfNotFound();
// Transformes the payment receive DTO to model.
const paymentReceiveObj = await this.transformEditDTOToModel(
tenantId,
customer,
paymentReceiveDTO,
oldPaymentReceive
);
// Validate customer whether modified.
this.validators.validateCustomerNotModified(
paymentReceiveDTO,
oldPaymentReceive
);
// Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) {
await this.validators.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveDTO.paymentReceiveNo,
paymentReceiveId
);
}
// Validate the deposit account existance and type.
const depositAccount = await this.validators.getDepositAccountOrThrowError(
tenantId,
paymentReceiveDTO.depositAccountId
);
// Validate the entries ids existance on payment receive type.
await this.validators.validateEntriesIdsExistance(
tenantId,
paymentReceiveId,
paymentReceiveDTO.entries
);
// Validate payment receive invoices IDs existance and associated
// to the given customer id.
await this.validators.validateInvoicesIDsExistance(
tenantId,
oldPaymentReceive.customerId,
paymentReceiveDTO.entries
);
// Validate invoice payment amount.
await this.validators.validateInvoicesPaymentsAmount(
tenantId,
paymentReceiveDTO.entries,
oldPaymentReceive.entries
);
// Validates the payment account currency code.
this.validators.validatePaymentAccountCurrency(
depositAccount.currencyCode,
customer.currencyCode,
tenantMeta.baseCurrency
);
// Creates payment receive transaction under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveEditing` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onEditing, {
trx,
tenantId,
oldPaymentReceive,
paymentReceiveDTO,
} as IPaymentReceiveEditingPayload);
// Update the payment receive transaction.
const paymentReceive = await PaymentReceive.query(
trx
).upsertGraphAndFetch({
id: paymentReceiveId,
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveEdited` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, {
tenantId,
paymentReceiveId,
paymentReceive,
oldPaymentReceive,
authorizedUser,
trx,
} as IPaymentReceiveEditedPayload);
return paymentReceive;
});
}
/**
* Transform the edit payment receive DTO.
* @param {number} tenantId
* @param {ICustomer} customer
* @param {IPaymentReceiveEditDTO} paymentReceiveDTO
* @param {IPaymentReceive} oldPaymentReceive
* @returns
*/
private transformEditDTOToModel = async (
tenantId: number,
customer: ICustomer,
paymentReceiveDTO: IPaymentReceiveEditDTO,
oldPaymentReceive: IPaymentReceive
) => {
return this.transformer.transformPaymentReceiveDTOToModel(
tenantId,
customer,
paymentReceiveDTO,
oldPaymentReceive
);
};
}

View File

@@ -0,0 +1,46 @@
import { ServiceError } from '@/exceptions';
import { IPaymentReceive } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
import { PaymentReceiveTransfromer } from './PaymentReceiveTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetPaymentReceive {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve payment receive details.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<IPaymentReceive>}
*/
public async getPaymentReceive(
tenantId: number,
paymentReceiveId: number
): Promise<IPaymentReceive> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.withGraphFetched('customer')
.withGraphFetched('depositAccount')
.withGraphFetched('entries.invoice')
.withGraphFetched('transactions')
.withGraphFetched('branch')
.findById(paymentReceiveId);
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return this.transformer.transform(
tenantId,
paymentReceive,
new PaymentReceiveTransfromer()
);
}
}

View File

@@ -0,0 +1,41 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { PaymentReceiveValidators } from './PaymentReceiveValidators';
@Service()
export class GetPaymentReceiveInvoices {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: PaymentReceiveValidators;
/**
* Retrieve sale invoices that assocaited to the given payment receive.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<ISaleInvoice>}
*/
public async getPaymentReceiveInvoices(
tenantId: number,
paymentReceiveId: number
) {
const { SaleInvoice, PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findById(paymentReceiveId)
.withGraphFetched('entries');
// Validates the payment receive existance.
this.validators.validatePaymentExistance(paymentReceive);
const paymentReceiveInvoicesIds = paymentReceive.entries.map(
(entry) => entry.invoiceId
);
const saleInvoices = await SaleInvoice.query().whereIn(
'id',
paymentReceiveInvoicesIds
);
return saleInvoices;
}
}

View File

@@ -0,0 +1,77 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import {
IFilterMeta,
IPaginationMeta,
IPaymentReceive,
IPaymentReceivesFilter,
} from '@/interfaces';
import { PaymentReceiveTransfromer } from './PaymentReceiveTransformer';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
@Service()
export class GetPaymentReceives {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve payment receives paginated and filterable list.
* @param {number} tenantId
* @param {IPaymentReceivesFilter} paymentReceivesFilter
*/
public async getPaymentReceives(
tenantId: number,
filterDTO: IPaymentReceivesFilter
): Promise<{
paymentReceives: IPaymentReceive[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { PaymentReceive } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
PaymentReceive,
filter
);
const { results, pagination } = await PaymentReceive.query()
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicList.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformer the payment receives models to POJO.
const transformedPayments = await this.transformer.transform(
tenantId,
results,
new PaymentReceiveTransfromer()
);
return {
paymentReceives: transformedPayments,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**
* Parses payments receive list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -7,10 +7,10 @@ import { Tenant } from '@/system/models';
@Service()
export default class GetPaymentReceivePdf {
@Inject()
pdfService: PdfService;
private pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
private tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.

View File

@@ -0,0 +1,69 @@
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import {
ICustomer,
IPaymentReceive,
IPaymentReceiveCreateDTO,
IPaymentReceiveEditDTO,
} from '@/interfaces';
import { PaymentReceiveValidators } from './PaymentReceiveValidators';
import { PaymentReceiveIncrement } from './PaymentReceiveIncrement';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { formatDateFields } from '@/utils';
@Service()
export class PaymentReceiveDTOTransformer {
@Inject()
private validators: PaymentReceiveValidators;
@Inject()
private increments: PaymentReceiveIncrement;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
/**
* Transformes the create payment receive DTO to model object.
* @param {number} tenantId
* @param {IPaymentReceiveCreateDTO|IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO.
* @param {IPaymentReceive} oldPaymentReceive -
* @return {IPaymentReceive}
*/
public async transformPaymentReceiveDTOToModel(
tenantId: number,
customer: ICustomer,
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
oldPaymentReceive?: IPaymentReceive
): Promise<IPaymentReceive> {
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Retreive the next invoice number.
const autoNextNumber =
this.increments.getNextPaymentReceiveNumber(tenantId);
// Retrieve the next payment receive number.
const paymentReceiveNo =
paymentReceiveDTO.paymentReceiveNo ||
oldPaymentReceive?.paymentReceiveNo ||
autoNextNumber;
this.validators.validatePaymentNoRequire(paymentReceiveNo);
const initialDTO = {
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
'paymentDate',
]),
amount: paymentAmount,
currencyCode: customer.currencyCode,
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
entries: paymentReceiveDTO.entries.map((entry) => ({
...entry,
})),
};
return R.compose(
this.branchDTOTransform.transformDTO<IPaymentReceive>(tenantId)
)(initialDTO);
}
}

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
@Service()
export class PaymentReceiveIncrement {
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
/**
* Retrieve the next unique payment receive number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextPaymentReceiveNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'payment_receives'
);
}
/**
* Increment the payment receive next number.
* @param {number} tenantId
*/
public incrementNextPaymentReceiveNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'payment_receives'
);
}
}

View File

@@ -0,0 +1,48 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IPaymentReceiveEntryDTO } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { entriesAmountDiff } from '@/utils';
@Service()
export class PaymentReceiveInvoiceSync {
@Inject()
private tenancy: HasTenancyService;
/**
* Saves difference changing between old and new invoice payment amount.
* @async
* @param {number} tenantId - Tenant id.
* @param {Array} paymentReceiveEntries
* @param {Array} newPaymentReceiveEntries
* @return {Promise<void>}
*/
public async saveChangeInvoicePaymentAmount(
tenantId: number,
newPaymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[],
trx?: Knex.Transaction
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<void>[] = [];
const diffEntries = entriesAmountDiff(
newPaymentReceiveEntries,
oldPaymentReceiveEntries,
'paymentAmount',
'invoiceId'
);
diffEntries.forEach((diffEntry: any) => {
if (diffEntry.paymentAmount === 0) {
return;
}
const oper = SaleInvoice.changePaymentAmount(
diffEntry.invoiceId,
diffEntry.paymentAmount,
trx
);
opers.push(oper);
});
await Promise.all([...opers]);
}
}

View File

@@ -1,36 +1,35 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import SMSClient from '@/services/SMSClient';
import {
IPaymentReceiveSmsDetails,
SMS_NOTIFICATION_KEY,
IPaymentReceive,
IPaymentReceiveEntry,
} from '@/interfaces';
import PaymentReceiveService from './PaymentsReceives';
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
import { formatNumber, formatSmsMessage } from 'utils';
import { TenantMetadata } from '@/system/models';
import SaleNotifyBySms from '../SaleNotifyBySms';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { PaymentReceiveValidators } from './PaymentReceiveValidators';
@Service()
export default class PaymentReceiveNotifyBySms {
export class PaymentReceiveNotifyBySms {
@Inject()
paymentReceiveService: PaymentReceiveService;
private tenancy: HasTenancyService;
@Inject()
tenancy: HasTenancyService;
private eventPublisher: EventPublisher;
@Inject()
eventPublisher: EventPublisher;
private smsNotificationsSettings: SmsNotificationsSettingsService;
@Inject()
smsNotificationsSettings: SmsNotificationsSettingsService;
private saleSmsNotification: SaleNotifyBySms;
@Inject()
saleSmsNotification: SaleNotifyBySms;
private validators: PaymentReceiveValidators;
/**
* Notify customer via sms about payment receive details.
@@ -46,6 +45,9 @@ export default class PaymentReceiveNotifyBySms {
.withGraphFetched('customer')
.withGraphFetched('entries.invoice');
// Validates the payment existance.
this.validators.validatePaymentExistance(paymentReceive);
// Validate the customer phone number.
this.saleSmsNotification.validateCustomerPhoneNumber(
paymentReceive.customer.personalPhone

View File

@@ -1,7 +1,7 @@
import { IPaymentReceive, IPaymentReceiveEntry } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
import { SaleInvoiceTransformer } from '../SaleInvoiceTransformer';
import { SaleInvoiceTransformer } from '../Invoices/SaleInvoiceTransformer';
export class PaymentReceiveTransfromer extends Transformer {
/**

View File

@@ -0,0 +1,295 @@
import { Inject, Service } from 'typedi';
import { difference, sumBy } from 'lodash';
import {
IAccount,
IPaymentReceive,
IPaymentReceiveEditDTO,
IPaymentReceiveEntry,
IPaymentReceiveEntryDTO,
ISaleInvoice,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
import { PaymentReceive } from '@/models';
@Service()
export class PaymentReceiveValidators {
@Inject()
private tenancy: HasTenancyService;
/**
* Validates the payment existance.
* @param {PaymentReceive | null | undefined} payment
*/
public validatePaymentExistance(payment: PaymentReceive | null | undefined) {
if (!payment) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
}
/**
* Validates the payment receive number existance.
* @param {number} tenantId -
* @param {string} paymentReceiveNo -
*/
public async validatePaymentReceiveNoExistance(
tenantId: number,
paymentReceiveNo: string,
notPaymentReceiveId?: number
): Promise<void> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findOne('payment_receive_no', paymentReceiveNo)
.onBuild((builder) => {
if (notPaymentReceiveId) {
builder.whereNot('id', notPaymentReceiveId);
}
});
if (paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_EXISTS);
}
}
/**
* Validates the invoices IDs existance.
* @param {number} tenantId -
* @param {number} customerId -
* @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries -
*/
public async validateInvoicesIDsExistance(
tenantId: number,
customerId: number,
paymentReceiveEntries: { invoiceId: number }[]
): Promise<ISaleInvoice[]> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoicesIds = paymentReceiveEntries.map(
(e: { invoiceId: number }) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query()
.whereIn('id', invoicesIds)
.where('customer_id', customerId);
const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id);
const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds);
if (notFoundInvoicesIDs.length > 0) {
throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND);
}
// Filters the not delivered invoices.
const notDeliveredInvoices = storedInvoices.filter(
(invoice) => !invoice.isDelivered
);
if (notDeliveredInvoices.length > 0) {
throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, {
notDeliveredInvoices,
});
}
return storedInvoices;
}
/**
* Validates entries invoice payment amount.
* @param {Request} req -
* @param {Response} res -
* @param {Function} next -
*/
public async validateInvoicesPaymentsAmount(
tenantId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentEntries: IPaymentReceiveEntry[] = []
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoicesIds = paymentReceiveEntries.map(
(e: IPaymentReceiveEntryDTO) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds);
const storedInvoicesMap = new Map(
storedInvoices.map((invoice: ISaleInvoice) => {
const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId);
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0;
return [
invoice.id,
{ ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount },
];
})
);
const hasWrongPaymentAmount: any[] = [];
paymentReceiveEntries.forEach(
(entry: IPaymentReceiveEntryDTO, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
const { dueAmount } = entryInvoice;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
}
);
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT);
}
}
/**
* Validate the payment receive number require.
* @param {IPaymentReceive} paymentReceiveObj
*/
public validatePaymentReceiveNoRequire(paymentReceiveObj: IPaymentReceive) {
if (!paymentReceiveObj.paymentReceiveNo) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED);
}
}
/**
* Validate the payment receive entries IDs existance.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries
*/
public async validateEntriesIdsExistance(
tenantId: number,
paymentReceiveId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[]
) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
const entriesIds = paymentReceiveEntries
.filter((entry) => entry.id)
.map((entry) => entry.id);
const storedEntries = await PaymentReceiveEntry.query().where(
'payment_receive_id',
paymentReceiveId
);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS);
}
}
/**
* Validates the payment receive number require.
* @param {string} paymentReceiveNo
*/
public validatePaymentNoRequire(paymentReceiveNo: string) {
if (!paymentReceiveNo) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED);
}
}
/**
* Validate the payment customer whether modified.
* @param {IPaymentReceiveEditDTO} paymentReceiveDTO
* @param {IPaymentReceive} oldPaymentReceive
*/
public validateCustomerNotModified(
paymentReceiveDTO: IPaymentReceiveEditDTO,
oldPaymentReceive: IPaymentReceive
) {
if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) {
throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE);
}
}
/**
* Validates the payment account currency code. The deposit account curreny
* should be equals the customer currency code or the base currency.
* @param {string} paymentAccountCurrency
* @param {string} customerCurrency
* @param {string} baseCurrency
* @throws {ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID)}
*/
public validatePaymentAccountCurrency = (
paymentAccountCurrency: string,
customerCurrency: string,
baseCurrency: string
) => {
if (
paymentAccountCurrency !== customerCurrency &&
paymentAccountCurrency !== baseCurrency
) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID);
}
};
/**
* Validates the payment receive existance.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
*/
async getPaymentReceiveOrThrowError(
tenantId: number,
paymentReceiveId: number
): Promise<IPaymentReceive> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.withGraphFetched('entries')
.findById(paymentReceiveId);
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return paymentReceive;
}
/**
* Validate the deposit account id existance.
* @param {number} tenantId - Tenant id.
* @param {number} depositAccountId - Deposit account id.
* @return {Promise<IAccount>}
*/
async getDepositAccountOrThrowError(
tenantId: number,
depositAccountId: number
): Promise<IAccount> {
const { accountRepository } = this.tenancy.repositories(tenantId);
const depositAccount = await accountRepository.findOneById(
depositAccountId
);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
}
// Detarmines whether the account is cash, bank or other current asset.
if (
!depositAccount.isAccountType([
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
])
) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE);
}
return depositAccount;
}
/**
* Validate the given customer has no payments receives.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoPayments(
tenantId: number,
customerId: number
) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceives = await PaymentReceive.query().where(
'customer_id',
customerId
);
if (paymentReceives.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_PAYMENT_RECEIVES);
}
}
}

View File

@@ -0,0 +1,193 @@
import {
IFilterMeta,
IPaginationMeta,
IPaymentReceive,
IPaymentReceiveCreateDTO,
IPaymentReceiveEditDTO,
IPaymentReceiveSmsDetails,
IPaymentReceivesFilter,
ISystemUser,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { CreatePaymentReceive } from './CreatePaymentReceive';
import { EditPaymentReceive } from './EditPaymentReceive';
import { DeletePaymentReceive } from './DeletePaymentReceive';
import { GetPaymentReceives } from './GetPaymentReceives';
import { GetPaymentReceive } from './GetPaymentReceive';
import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices';
import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify';
import GetPaymentReceivePdf from './GetPaymentReeceivePdf';
import { PaymentReceive } from '@/models';
@Service()
export class PaymentReceivesApplication {
@Inject()
private createPaymentReceiveService: CreatePaymentReceive;
@Inject()
private editPaymentReceiveService: EditPaymentReceive;
@Inject()
private deletePaymentReceiveService: DeletePaymentReceive;
@Inject()
private getPaymentReceivesService: GetPaymentReceives;
@Inject()
private getPaymentReceiveService: GetPaymentReceive;
@Inject()
private getPaymentReceiveInvoicesService: GetPaymentReceiveInvoices;
@Inject()
private paymentSmsNotify: PaymentReceiveNotifyBySms;
@Inject()
private getPaymentReceivePdfService: GetPaymentReceivePdf;
/**
* Creates a new payment receive.
* @param {number} tenantId
* @param {IPaymentReceiveCreateDTO} paymentReceiveDTO
* @param {ISystemUser} authorizedUser
* @returns
*/
public createPaymentReceive(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO,
authorizedUser: ISystemUser
) {
return this.createPaymentReceiveService.createPaymentReceive(
tenantId,
paymentReceiveDTO,
authorizedUser
);
}
/**
* Edit details the given payment receive with associated entries.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {IPaymentReceiveEditDTO} paymentReceiveDTO
* @param {ISystemUser} authorizedUser
* @returns
*/
public editPaymentReceive(
tenantId: number,
paymentReceiveId: number,
paymentReceiveDTO: IPaymentReceiveEditDTO,
authorizedUser: ISystemUser
) {
return this.editPaymentReceiveService.editPaymentReceive(
tenantId,
paymentReceiveId,
paymentReceiveDTO,
authorizedUser
);
}
/**
* deletes the given payment receive.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {ISystemUser} authorizedUser
* @returns
*/
public deletePaymentReceive(
tenantId: number,
paymentReceiveId: number,
authorizedUser: ISystemUser
) {
return this.deletePaymentReceiveService.deletePaymentReceive(
tenantId,
paymentReceiveId,
authorizedUser
);
}
/**
* Retrieve payment receives paginated and filterable.
* @param {number} tenantId
* @param {IPaymentReceivesFilter} filterDTO
* @returns
*/
public async getPaymentReceives(
tenantId: number,
filterDTO: IPaymentReceivesFilter
): Promise<{
paymentReceives: IPaymentReceive[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
return this.getPaymentReceivesService.getPaymentReceives(
tenantId,
filterDTO
);
}
/**
*
* @param {number} tenantId
* @param {number} paymentReceiveId
* @returns {Promise<IPaymentReceive>}
*/
public async getPaymentReceive(
tenantId: number,
paymentReceiveId: number
): Promise<IPaymentReceive> {
return this.getPaymentReceiveService.getPaymentReceive(
tenantId,
paymentReceiveId
);
}
/**
* Retrieves associated sale invoices of the given payment receive.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @returns
*/
public getPaymentReceiveInvoices(tenantId: number, paymentReceiveId: number) {
return this.getPaymentReceiveInvoicesService.getPaymentReceiveInvoices(
tenantId,
paymentReceiveId
);
}
/**
* Notify customer via sms about payment receive details.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveid - Payment receive id.
*/
public notifyPaymentBySms(tenantId: number, paymentReceiveid: number) {
return this.paymentSmsNotify.notifyBySms(tenantId, paymentReceiveid);
}
/**
* Retrieve the SMS details of the given invoice.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveid - Payment receive id.
*/
public getPaymentSmsDetails = async (
tenantId: number,
paymentReceiveId: number
): Promise<IPaymentReceiveSmsDetails> => {
return this.paymentSmsNotify.smsDetails(tenantId, paymentReceiveId);
};
/**
* Retrieve PDF content of the given payment receive.
* @param {number} tenantId
* @param {PaymentReceive} paymentReceive
* @returns
*/
public getPaymentReceivePdf = (
tenantId: number,
paymentReceive: PaymentReceive
) => {
return this.getPaymentReceivePdfService.getPaymentReceivePdf(
tenantId,
paymentReceive
);
};
}

View File

@@ -1,847 +0,0 @@
import { omit, sumBy, difference } from 'lodash';
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IAccount,
IFilterMeta,
IPaginationMeta,
IPaymentReceive,
IPaymentReceiveCreateDTO,
IPaymentReceiveEditDTO,
IPaymentReceiveEntry,
IPaymentReceiveEntryDTO,
IPaymentReceivesFilter,
IPaymentsReceiveService,
IPaymentReceiveCreatedPayload,
ISaleInvoice,
ISystemUser,
IPaymentReceiveEditedPayload,
IPaymentReceiveDeletedPayload,
IPaymentReceiveCreatingPayload,
IPaymentReceiveDeletingPayload,
IPaymentReceiveEditingPayload,
ICustomer,
} from '@/interfaces';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { formatDateFields, entriesAmountDiff } from 'utils';
import { ServiceError } from '@/exceptions';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
import { ERRORS } from './constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { PaymentReceiveTransfromer } from './PaymentReceiveTransformer';
import UnitOfWork from '@/services/UnitOfWork';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TenantMetadata } from '@/system/models';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
/**
* Payment receive service.
* @service
*/
@Service('PaymentReceives')
export default class PaymentReceiveService implements IPaymentsReceiveService {
@Inject()
itemsEntries: ItemsEntriesService;
@Inject()
tenancy: TenancyService;
@Inject()
journalService: JournalPosterService;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject('logger')
logger: any;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
@Inject()
branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
transformer: TransformerInjectable;
/**
* Validates the payment receive number existance.
* @param {number} tenantId -
* @param {string} paymentReceiveNo -
*/
async validatePaymentReceiveNoExistance(
tenantId: number,
paymentReceiveNo: string,
notPaymentReceiveId?: number
): Promise<void> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findOne('payment_receive_no', paymentReceiveNo)
.onBuild((builder) => {
if (notPaymentReceiveId) {
builder.whereNot('id', notPaymentReceiveId);
}
});
if (paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_EXISTS);
}
}
/**
* Validates the payment receive existance.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
*/
async getPaymentReceiveOrThrowError(
tenantId: number,
paymentReceiveId: number
): Promise<IPaymentReceive> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.withGraphFetched('entries')
.findById(paymentReceiveId);
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return paymentReceive;
}
/**
* Validate the deposit account id existance.
* @param {number} tenantId - Tenant id.
* @param {number} depositAccountId - Deposit account id.
* @return {Promise<IAccount>}
*/
async getDepositAccountOrThrowError(
tenantId: number,
depositAccountId: number
): Promise<IAccount> {
const { accountRepository } = this.tenancy.repositories(tenantId);
const depositAccount = await accountRepository.findOneById(
depositAccountId
);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
}
// Detarmines whether the account is cash, bank or other current asset.
if (
!depositAccount.isAccountType([
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
])
) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE);
}
return depositAccount;
}
/**
* Validates the invoices IDs existance.
* @param {number} tenantId -
* @param {number} customerId -
* @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries -
*/
async validateInvoicesIDsExistance(
tenantId: number,
customerId: number,
paymentReceiveEntries: { invoiceId: number }[]
): Promise<ISaleInvoice[]> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoicesIds = paymentReceiveEntries.map(
(e: { invoiceId: number }) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query()
.whereIn('id', invoicesIds)
.where('customer_id', customerId);
const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id);
const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds);
if (notFoundInvoicesIDs.length > 0) {
throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND);
}
// Filters the not delivered invoices.
const notDeliveredInvoices = storedInvoices.filter(
(invoice) => !invoice.isDelivered
);
if (notDeliveredInvoices.length > 0) {
throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, {
notDeliveredInvoices,
});
}
return storedInvoices;
}
/**
* Validates entries invoice payment amount.
* @param {Request} req -
* @param {Response} res -
* @param {Function} next -
*/
async validateInvoicesPaymentsAmount(
tenantId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentEntries: IPaymentReceiveEntry[] = []
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoicesIds = paymentReceiveEntries.map(
(e: IPaymentReceiveEntryDTO) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds);
const storedInvoicesMap = new Map(
storedInvoices.map((invoice: ISaleInvoice) => {
const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId);
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0;
return [
invoice.id,
{ ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount },
];
})
);
const hasWrongPaymentAmount: any[] = [];
paymentReceiveEntries.forEach(
(entry: IPaymentReceiveEntryDTO, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
const { dueAmount } = entryInvoice;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
}
);
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT);
}
}
/**
* Retrieve the next unique payment receive number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
getNextPaymentReceiveNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'payment_receives'
);
}
/**
* Increment the payment receive next number.
* @param {number} tenantId
*/
incrementNextPaymentReceiveNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'payment_receives'
);
}
/**
* Validate the payment receive number require.
* @param {IPaymentReceive} paymentReceiveObj
*/
validatePaymentReceiveNoRequire(paymentReceiveObj: IPaymentReceive) {
if (!paymentReceiveObj.paymentReceiveNo) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED);
}
}
/**
* Validate the payment receive entries IDs existance.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries
*/
private async validateEntriesIdsExistance(
tenantId: number,
paymentReceiveId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[]
) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
const entriesIds = paymentReceiveEntries
.filter((entry) => entry.id)
.map((entry) => entry.id);
const storedEntries = await PaymentReceiveEntry.query().where(
'payment_receive_id',
paymentReceiveId
);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS);
}
}
/**
* Validates the payment receive number require.
* @param {string} paymentReceiveNo
*/
validatePaymentNoRequire(paymentReceiveNo: string) {
if (!paymentReceiveNo) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED);
}
}
/**
* Validate the payment customer whether modified.
* @param {IPaymentReceiveEditDTO} paymentReceiveDTO
* @param {IPaymentReceive} oldPaymentReceive
*/
validateCustomerNotModified(
paymentReceiveDTO: IPaymentReceiveEditDTO,
oldPaymentReceive: IPaymentReceive
) {
if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) {
throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE);
}
}
/**
* Validates the payment account currency code. The deposit account curreny
* should be equals the customer currency code or the base currency.
* @param {string} paymentAccountCurrency
* @param {string} customerCurrency
* @param {string} baseCurrency
* @throws {ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID)}
*/
public validatePaymentAccountCurrency = (
paymentAccountCurrency: string,
customerCurrency: string,
baseCurrency: string
) => {
if (
paymentAccountCurrency !== customerCurrency &&
paymentAccountCurrency !== baseCurrency
) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID);
}
};
/**
* Transformes the create payment receive DTO to model object.
* @param {number} tenantId
* @param {IPaymentReceiveCreateDTO|IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO.
* @param {IPaymentReceive} oldPaymentReceive -
* @return {IPaymentReceive}
*/
async transformPaymentReceiveDTOToModel(
tenantId: number,
customer: ICustomer,
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
oldPaymentReceive?: IPaymentReceive
): Promise<IPaymentReceive> {
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Retreive the next invoice number.
const autoNextNumber = this.getNextPaymentReceiveNumber(tenantId);
// Retrieve the next payment receive number.
const paymentReceiveNo =
paymentReceiveDTO.paymentReceiveNo ||
oldPaymentReceive?.paymentReceiveNo ||
autoNextNumber;
this.validatePaymentNoRequire(paymentReceiveNo);
const initialDTO = {
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
'paymentDate',
]),
amount: paymentAmount,
currencyCode: customer.currencyCode,
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
entries: paymentReceiveDTO.entries.map((entry) => ({
...entry,
})),
};
return R.compose(
this.branchDTOTransform.transformDTO<IPaymentReceive>(tenantId)
)(initialDTO);
}
/**
* Transform the create payment receive DTO.
* @param {number} tenantId
* @param {ICustomer} customer
* @param {IPaymentReceiveCreateDTO} paymentReceiveDTO
* @returns
*/
private transformCreateDTOToModel = async (
tenantId: number,
customer: ICustomer,
paymentReceiveDTO: IPaymentReceiveCreateDTO
) => {
return this.transformPaymentReceiveDTOToModel(
tenantId,
customer,
paymentReceiveDTO
);
};
/**
* Transform the edit payment receive DTO.
* @param {number} tenantId
* @param {ICustomer} customer
* @param {IPaymentReceiveEditDTO} paymentReceiveDTO
* @param {IPaymentReceive} oldPaymentReceive
* @returns
*/
private transformEditDTOToModel = async (
tenantId: number,
customer: ICustomer,
paymentReceiveDTO: IPaymentReceiveEditDTO,
oldPaymentReceive: IPaymentReceive
) => {
return this.transformPaymentReceiveDTOToModel(
tenantId,
customer,
paymentReceiveDTO,
oldPaymentReceive
);
};
/**
* Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions.
* @async
* @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive
*/
public async createPaymentReceive(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO,
authorizedUser: ISystemUser
) {
const { PaymentReceive, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Validate customer existance.
const paymentCustomer = await Contact.query()
.modify('customer')
.findById(paymentReceiveDTO.customerId)
.throwIfNotFound();
// Transformes the payment receive DTO to model.
const paymentReceiveObj = await this.transformCreateDTOToModel(
tenantId,
paymentCustomer,
paymentReceiveDTO
);
// Validate payment receive number uniquiness.
await this.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveObj.paymentReceiveNo
);
// Validate the deposit account existance and type.
const depositAccount = await this.getDepositAccountOrThrowError(
tenantId,
paymentReceiveDTO.depositAccountId
);
// Validate payment receive invoices IDs existance.
await this.validateInvoicesIDsExistance(
tenantId,
paymentReceiveDTO.customerId,
paymentReceiveDTO.entries
);
// Validate invoice payment amount.
await this.validateInvoicesPaymentsAmount(
tenantId,
paymentReceiveDTO.entries
);
// Validates the payment account currency code.
this.validatePaymentAccountCurrency(
depositAccount.currencyCode,
paymentCustomer.currencyCode,
tenantMeta.baseCurrency
);
// Creates a payment receive transaction under UOW envirment.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveCreating` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, {
trx,
paymentReceiveDTO,
tenantId,
} as IPaymentReceiveCreatingPayload);
// Inserts the payment receive transaction.
const paymentReceive = await PaymentReceive.query(
trx
).insertGraphAndFetch({
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveCreated` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, {
tenantId,
paymentReceive,
paymentReceiveId: paymentReceive.id,
authorizedUser,
trx,
} as IPaymentReceiveCreatedPayload);
return paymentReceive;
});
}
/**
* Edit details the given payment receive with associated entries.
* ------
* - Update the payment receive transactions.
* - Insert the new payment receive entries.
* - Update the given payment receive entries.
* - Delete the not presented payment receive entries.
* - Re-insert the journal transactions and update the different accounts balance.
* - Update the different customer balances.
* - Update the different invoice payment amount.
* @async
* @param {number} tenantId -
* @param {Integer} paymentReceiveId -
* @param {IPaymentReceive} paymentReceive -
*/
public async editPaymentReceive(
tenantId: number,
paymentReceiveId: number,
paymentReceiveDTO: IPaymentReceiveEditDTO,
authorizedUser: ISystemUser
) {
const { PaymentReceive, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Validate the payment receive existance.
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(
tenantId,
paymentReceiveId
);
// Validate customer existance.
const customer = await Contact.query()
.modify('customer')
.findById(paymentReceiveDTO.customerId)
.throwIfNotFound();
// Transformes the payment receive DTO to model.
const paymentReceiveObj = await this.transformEditDTOToModel(
tenantId,
customer,
paymentReceiveDTO,
oldPaymentReceive
);
// Validate customer whether modified.
this.validateCustomerNotModified(paymentReceiveDTO, oldPaymentReceive);
// Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) {
await this.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveDTO.paymentReceiveNo,
paymentReceiveId
);
}
// Validate the deposit account existance and type.
const depositAccount = await this.getDepositAccountOrThrowError(
tenantId,
paymentReceiveDTO.depositAccountId
);
// Validate the entries ids existance on payment receive type.
await this.validateEntriesIdsExistance(
tenantId,
paymentReceiveId,
paymentReceiveDTO.entries
);
// Validate payment receive invoices IDs existance and associated
// to the given customer id.
await this.validateInvoicesIDsExistance(
tenantId,
oldPaymentReceive.customerId,
paymentReceiveDTO.entries
);
// Validate invoice payment amount.
await this.validateInvoicesPaymentsAmount(
tenantId,
paymentReceiveDTO.entries,
oldPaymentReceive.entries
);
// Validates the payment account currency code.
this.validatePaymentAccountCurrency(
depositAccount.currencyCode,
customer.currencyCode,
tenantMeta.baseCurrency
);
// Creates payment receive transaction under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveEditing` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onEditing, {
trx,
tenantId,
oldPaymentReceive,
paymentReceiveDTO,
} as IPaymentReceiveEditingPayload);
// Update the payment receive transaction.
const paymentReceive = await PaymentReceive.query(
trx
).upsertGraphAndFetch({
id: paymentReceiveId,
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveEdited` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, {
tenantId,
paymentReceiveId,
paymentReceive,
oldPaymentReceive,
authorizedUser,
trx,
} as IPaymentReceiveEditedPayload);
return paymentReceive;
});
}
/**
* Deletes the given payment receive with associated entries
* and journal transactions.
* -----
* - Deletes the payment receive transaction.
* - Deletes the payment receive associated entries.
* - Deletes the payment receive associated journal transactions.
* - Revert the customer balance.
* - Revert the payment amount of the associated invoices.
* @async
* @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId - Payment receive id.
* @param {IPaymentReceive} paymentReceive - Payment receive object.
*/
public async deletePaymentReceive(
tenantId: number,
paymentReceiveId: number,
authorizedUser: ISystemUser
) {
const { PaymentReceive, PaymentReceiveEntry } =
this.tenancy.models(tenantId);
// Retreive payment receive or throw not found service error.
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(
tenantId,
paymentReceiveId
);
// Delete payment receive transaction and associate transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveDeleting` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onDeleting, {
tenantId,
oldPaymentReceive,
trx,
} as IPaymentReceiveDeletingPayload);
// Deletes the payment receive associated entries.
await PaymentReceiveEntry.query(trx)
.where('payment_receive_id', paymentReceiveId)
.delete();
// Deletes the payment receive transaction.
await PaymentReceive.query(trx).findById(paymentReceiveId).delete();
// Triggers `onPaymentReceiveDeleted` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onDeleted, {
tenantId,
paymentReceiveId,
oldPaymentReceive,
authorizedUser,
trx,
} as IPaymentReceiveDeletedPayload);
});
}
/**
* Retrieve payment receive details.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<IPaymentReceive>}
*/
public async getPaymentReceive(
tenantId: number,
paymentReceiveId: number
): Promise<IPaymentReceive> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.withGraphFetched('customer')
.withGraphFetched('depositAccount')
.withGraphFetched('entries.invoice')
.withGraphFetched('transactions')
.withGraphFetched('branch')
.findById(paymentReceiveId);
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return this.transformer.transform(
tenantId,
paymentReceive,
new PaymentReceiveTransfromer()
);
}
/**
* Retrieve sale invoices that assocaited to the given payment receive.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<ISaleInvoice>}
*/
public async getPaymentReceiveInvoices(
tenantId: number,
paymentReceiveId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const paymentReceive = await this.getPaymentReceiveOrThrowError(
tenantId,
paymentReceiveId
);
const paymentReceiveInvoicesIds = paymentReceive.entries.map(
(entry) => entry.invoiceId
);
const saleInvoices = await SaleInvoice.query().whereIn(
'id',
paymentReceiveInvoicesIds
);
return saleInvoices;
}
/**
* Parses payments receive list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve payment receives paginated and filterable list.
* @param {number} tenantId
* @param {IPaymentReceivesFilter} paymentReceivesFilter
*/
public async listPaymentReceives(
tenantId: number,
filterDTO: IPaymentReceivesFilter
): Promise<{
paymentReceives: IPaymentReceive[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { PaymentReceive } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
PaymentReceive,
filter
);
const { results, pagination } = await PaymentReceive.query()
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicList.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformer the payment receives models to POJO.
const transformedPayments = await this.transformer.transform(
tenantId,
results,
new PaymentReceiveTransfromer()
);
return {
paymentReceives: transformedPayments,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**
* Saves difference changing between old and new invoice payment amount.
* @async
* @param {number} tenantId - Tenant id.
* @param {Array} paymentReceiveEntries
* @param {Array} newPaymentReceiveEntries
* @return {Promise<void>}
*/
public async saveChangeInvoicePaymentAmount(
tenantId: number,
newPaymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[],
trx?: Knex.Transaction
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<void>[] = [];
const diffEntries = entriesAmountDiff(
newPaymentReceiveEntries,
oldPaymentReceiveEntries,
'paymentAmount',
'invoiceId'
);
diffEntries.forEach((diffEntry: any) => {
if (diffEntry.paymentAmount === 0) {
return;
}
const oper = SaleInvoice.changePaymentAmount(
diffEntry.invoiceId,
diffEntry.paymentAmount,
trx
);
opers.push(oper);
});
await Promise.all([...opers]);
}
/**
* Validate the given customer has no payments receives.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoPayments(
tenantId: number,
customerId: number
) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceives = await PaymentReceive.query().where(
'customer_id',
customerId
);
if (paymentReceives.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_PAYMENT_RECEIVES);
}
}
}

View File

@@ -11,8 +11,7 @@ export const ERRORS = {
PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED',
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'
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
};
export const DEFAULT_VIEWS = [];
export const DEFAULT_VIEWS = [];

View File

@@ -0,0 +1,76 @@
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';
import {
ISaleReceiptEventClosedPayload,
ISaleReceiptEventClosingPayload,
} from '@/interfaces';
import { SaleReceiptValidators } from './SaleReceiptValidators';
@Service()
export class CloseSaleReceipt {
@Inject()
private tenancy: TenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: SaleReceiptValidators;
/**
* Mark the given sale receipt as closed.
* @param {number} tenantId
* @param {number} saleReceiptId
* @return {Promise<void>}
*/
public async closeSaleReceipt(
tenantId: number,
saleReceiptId: number
): Promise<void> {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Retrieve sale receipt or throw not found service error.
const oldSaleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries')
.throwIfNotFound();
// Throw service error if the sale receipt already closed.
this.validators.validateReceiptNotClosed(oldSaleReceipt);
// Updates the sale recept transaction under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptClosing` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onClosing, {
tenantId,
oldSaleReceipt,
trx,
} as ISaleReceiptEventClosingPayload);
// Mark the sale receipt as closed on the storage.
const saleReceipt = await SaleReceipt.query(trx)
.findById(saleReceiptId)
.patch({
closedAt: moment().toMySqlDateTime(),
});
// Triggers `onSaleReceiptClosed` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onClosed, {
saleReceiptId,
saleReceipt,
tenantId,
trx,
} as ISaleReceiptEventClosedPayload);
});
}
}

View File

@@ -0,0 +1,106 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
ISaleReceipt,
ISaleReceiptCreatedPayload,
ISaleReceiptCreatingPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer';
import { SaleReceiptValidators } from './SaleReceiptValidators';
@Service()
export class CreateSaleReceipt {
@Inject()
private tenancy: TenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformer: SaleReceiptDTOTransformer;
@Inject()
private validators: SaleReceiptValidators;
/**
* Creates a new sale receipt with associated entries.
* @async
* @param {ISaleReceipt} saleReceipt
* @return {Object}
*/
public async createSaleReceipt(
tenantId: number,
saleReceiptDTO: any
): Promise<ISaleReceipt> {
const { SaleReceipt, Contact } = this.tenancy.models(tenantId);
// Retireves the payment customer model.
const paymentCustomer = await Contact.query()
.modify('customer')
.findById(saleReceiptDTO.customerId)
.throwIfNotFound();
// Transform sale receipt DTO to model.
const saleReceiptObj = await this.transformer.transformDTOToModel(
tenantId,
saleReceiptDTO,
paymentCustomer
);
// Validate receipt deposit account existance and type.
await this.validators.validateReceiptDepositAccountExistance(
tenantId,
saleReceiptDTO.depositAccountId
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleReceiptDTO.entries
);
// Validate the sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleReceiptDTO.entries
);
// Validate sale receipt number uniuqiness.
if (saleReceiptDTO.receiptNumber) {
await this.validators.validateReceiptNumberUnique(
tenantId,
saleReceiptDTO.receiptNumber
);
}
// Creates a sale receipt transaction and associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptCreating` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreating, {
saleReceiptDTO,
tenantId,
trx,
} as ISaleReceiptCreatingPayload);
// Inserts the sale receipt graph to the storage.
const saleReceipt = await SaleReceipt.query().upsertGraph({
...saleReceiptObj,
});
// Triggers `onSaleReceiptCreated` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreated, {
tenantId,
saleReceipt,
saleReceiptId: saleReceipt.id,
trx,
} as ISaleReceiptCreatedPayload);
return saleReceipt;
});
}
}

View File

@@ -0,0 +1,67 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ISaleReceiptDeletingPayload,
ISaleReceiptEventDeletedPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { SaleReceiptValidators } from './SaleReceiptValidators';
@Service()
export class DeleteSaleReceipt {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: SaleReceiptValidators;
/**
* Deletes the sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @return {void}
*/
public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const oldSaleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries');
// Validates the sale receipt existance.
this.validators.validateReceiptExistance(oldSaleReceipt);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptsDeleting` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onDeleting, {
trx,
oldSaleReceipt,
tenantId,
} as ISaleReceiptDeletingPayload);
await ItemEntry.query(trx)
.where('reference_id', saleReceiptId)
.where('reference_type', 'SaleReceipt')
.delete();
// Delete the sale receipt transaction.
await SaleReceipt.query(trx).where('id', saleReceiptId).delete();
// Triggers `onSaleReceiptsDeleted` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onDeleted, {
tenantId,
saleReceiptId,
oldSaleReceipt,
trx,
} as ISaleReceiptEventDeletedPayload);
});
}
}

View File

@@ -0,0 +1,119 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleReceiptEditedPayload,
ISaleReceiptEditingPayload,
} from '@/interfaces';
import { SaleReceiptValidators } from './SaleReceiptValidators';
import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer';
@Service()
export class EditSaleReceipt {
@Inject()
private tenancy: TenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: SaleReceiptValidators;
@Inject()
private DTOTransformer: SaleReceiptDTOTransformer;
/**
* Edit details sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @param {ISaleReceipt} saleReceipt
* @return {void}
*/
public async editSaleReceipt(
tenantId: number,
saleReceiptId: number,
saleReceiptDTO: any
) {
const { SaleReceipt, Contact } = this.tenancy.models(tenantId);
// Retrieve sale receipt or throw not found service error.
const oldSaleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries');
// Validates the sale receipt existance.
this.validators.validateReceiptExistance(oldSaleReceipt);
// Retrieves the payment customer model.
const paymentCustomer = await Contact.query()
.findById(saleReceiptDTO.customerId)
.modify('customer')
.throwIfNotFound();
// Transform sale receipt DTO to model.
const saleReceiptObj = await this.DTOTransformer.transformDTOToModel(
tenantId,
saleReceiptDTO,
paymentCustomer,
oldSaleReceipt
);
// Validate receipt deposit account existance and type.
await this.validators.validateReceiptDepositAccountExistance(
tenantId,
saleReceiptDTO.depositAccountId
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleReceiptDTO.entries
);
// Validate the sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleReceiptDTO.entries
);
// Validate sale receipt number uniuqiness.
if (saleReceiptDTO.receiptNumber) {
await this.validators.validateReceiptNumberUnique(
tenantId,
saleReceiptDTO.receiptNumber,
saleReceiptId
);
}
// Edits the sale receipt tranasctions with associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptsEditing` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onEditing, {
tenantId,
oldSaleReceipt,
saleReceiptDTO,
trx,
} as ISaleReceiptEditingPayload);
// Upsert the receipt graph to the storage.
const saleReceipt = await SaleReceipt.query(trx).upsertGraphAndFetch({
id: saleReceiptId,
...saleReceiptObj,
});
// Triggers `onSaleReceiptEdited` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, {
tenantId,
oldSaleReceipt,
saleReceipt,
saleReceiptId,
trx,
} as ISaleReceiptEditedPayload);
return saleReceipt;
});
}
}

View File

@@ -0,0 +1,42 @@
import { Inject, Service } from 'typedi';
import { SaleReceiptTransformer } from './SaleReceiptTransformer';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { SaleReceiptValidators } from './SaleReceiptValidators';
@Service()
export class GetSaleReceipt {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: SaleReceiptValidators;
/**
* Retrieve sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @return {ISaleReceipt}
*/
public async getSaleReceipt(tenantId: number, saleReceiptId: number) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('depositAccount')
.withGraphFetched('branch');
// Valdiates the sale receipt existance.
this.validators.validateReceiptExistance(saleReceipt);
return this.transformer.transform(
tenantId,
saleReceipt,
new SaleReceiptTransformer()
);
}
}

View File

@@ -0,0 +1,80 @@
import * as R from 'ramda';
import {
IFilterMeta,
IPaginationMeta,
ISaleReceipt,
ISalesReceiptsFilter,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { SaleReceiptTransformer } from './SaleReceiptTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
@Service()
export class GetSaleReceipts {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private dynamicListService: DynamicListingService;
/**
* Retrieve sales receipts paginated and filterable list.
* @param {number} tenantId
* @param {ISaleReceiptFilter} salesReceiptsFilter
*/
public async getSaleReceipts(
tenantId: number,
filterDTO: ISalesReceiptsFilter
): Promise<{
data: ISaleReceipt[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleReceipt,
filter
);
const { results, pagination } = await SaleReceipt.query()
.onBuild((builder) => {
builder.withGraphFetched('depositAccount');
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformes the estimates models to POJO.
const salesEstimates = await this.transformer.transform(
tenantId,
results,
new SaleReceiptTransformer()
);
return {
data: salesEstimates,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,169 @@
import { Inject, Service } from 'typedi';
import { CreateSaleReceipt } from './CreateSaleReceipt';
import {
IFilterMeta,
IPaginationMeta,
ISaleReceipt,
ISalesReceiptsFilter,
} from '@/interfaces';
import { EditSaleReceipt } from './EditSaleReceipt';
import { GetSaleReceipt } from './GetSaleReceipt';
import { DeleteSaleReceipt } from './DeleteSaleReceipt';
import { GetSaleReceipts } from './GetSaleReceipts';
import { CloseSaleReceipt } from './CloseSaleReceipt';
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
@Service()
export class SaleReceiptApplication {
@Inject()
private createSaleReceiptService: CreateSaleReceipt;
@Inject()
private editSaleReceiptService: EditSaleReceipt;
@Inject()
private getSaleReceiptService: GetSaleReceipt;
@Inject()
private deleteSaleReceiptService: DeleteSaleReceipt;
@Inject()
private getSaleReceiptsService: GetSaleReceipts;
@Inject()
private closeSaleReceiptService: CloseSaleReceipt;
@Inject()
private getSaleReceiptPdfService: SaleReceiptsPdf;
@Inject()
private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms;
/**
* Creates a new sale receipt with associated entries.
* @param {number} tenantId
* @param {} saleReceiptDTO
* @returns {Promise<ISaleReceipt>}
*/
public async createSaleReceipt(
tenantId: number,
saleReceiptDTO: any
): Promise<ISaleReceipt> {
return this.createSaleReceiptService.createSaleReceipt(
tenantId,
saleReceiptDTO
);
}
/**
* Edit details sale receipt with associated entries.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {} saleReceiptDTO
* @returns
*/
public async editSaleReceipt(
tenantId: number,
saleReceiptId: number,
saleReceiptDTO: any
) {
return this.editSaleReceiptService.editSaleReceipt(
tenantId,
saleReceiptId,
saleReceiptDTO
);
}
/**
* Retrieve sale receipt with associated entries.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public async getSaleReceipt(tenantId: number, saleReceiptId: number) {
return this.getSaleReceiptService.getSaleReceipt(tenantId, saleReceiptId);
}
/**
* Deletes the sale receipt with associated entries.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
return this.deleteSaleReceiptService.deleteSaleReceipt(
tenantId,
saleReceiptId
);
}
/**
* Retrieve sales receipts paginated and filterable list.
* @param {number} tenantId
* @param {ISalesReceiptsFilter} filterDTO
* @returns
*/
public async getSaleReceipts(
tenantId: number,
filterDTO: ISalesReceiptsFilter
): Promise<{
data: ISaleReceipt[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
return this.getSaleReceiptsService.getSaleReceipts(tenantId, filterDTO);
}
/**
* Closes the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns {Promise<void>}
*/
public async closeSaleReceipt(tenantId: number, saleReceiptId: number) {
return this.closeSaleReceiptService.closeSaleReceipt(
tenantId,
saleReceiptId
);
}
/**
* Retrieves the given sale receipt pdf.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public getSaleReceiptPdf(tenantId: number, saleReceiptId: number) {
return this.getSaleReceiptPdfService.saleReceiptPdf(
tenantId,
saleReceiptId
);
}
/**
* Notify receipt customer by SMS of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public saleReceiptNotifyBySms(tenantId: number, saleReceiptId: number) {
return this.saleReceiptNotifyBySmsService.notifyBySms(
tenantId,
saleReceiptId
);
}
/**
* Retrieves sms details of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public getSaleReceiptSmsDetails(tenantId: number, saleReceiptId: number) {
return this.saleReceiptNotifyBySmsService.smsDetails(
tenantId,
saleReceiptId
);
}
}

View File

@@ -0,0 +1,89 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { sumBy, omit } from 'lodash';
import composeAsync from 'async/compose';
import moment from 'moment';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { SaleReceiptValidators } from './SaleReceiptValidators';
import { ICustomer, ISaleReceipt, ISaleReceiptDTO } from '@/interfaces';
import { formatDateFields } from '@/utils';
import { SaleReceiptIncrement } from './SaleReceiptIncrement';
import { ItemEntry } from '@/models';
@Service()
export class SaleReceiptDTOTransformer {
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private validators: SaleReceiptValidators;
@Inject()
private receiptIncrement: SaleReceiptIncrement;
/**
* Transform create DTO object to model object.
* @param {ISaleReceiptDTO} saleReceiptDTO -
* @param {ISaleReceipt} oldSaleReceipt -
* @returns {ISaleReceipt}
*/
async transformDTOToModel(
tenantId: number,
saleReceiptDTO: ISaleReceiptDTO,
paymentCustomer: ICustomer,
oldSaleReceipt?: ISaleReceipt
): Promise<ISaleReceipt> {
const amount = sumBy(saleReceiptDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
// Retreive the next invoice number.
const autoNextNumber = this.receiptIncrement.getNextReceiptNumber(tenantId);
// Retreive the receipt number.
const receiptNumber =
saleReceiptDTO.receiptNumber ||
oldSaleReceipt?.receiptNumber ||
autoNextNumber;
// Validate receipt number require.
this.validators.validateReceiptNoRequire(receiptNumber);
const initialEntries = saleReceiptDTO.entries.map((entry) => ({
reference_type: 'SaleReceipt',
...entry,
}));
const entries = await composeAsync(
// Sets default cost and sell account to receipt items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
)(initialEntries);
const initialDTO = {
amount,
...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [
'receiptDate',
]),
currencyCode: paymentCustomer.currencyCode,
exchangeRate: saleReceiptDTO.exchangeRate || 1,
receiptNumber,
// Avoid rewrite the deliver date in edit mode when already published.
...(saleReceiptDTO.closed &&
!oldSaleReceipt?.closedAt && {
closedAt: moment().toMySqlDateTime(),
}),
entries,
};
return R.compose(
this.branchDTOTransform.transformDTO<ISaleReceipt>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleReceipt>(tenantId)
)(initialDTO);
}
}

View File

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

View File

@@ -0,0 +1,72 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { ISaleReceipt } from '@/interfaces';
import InventoryService from '@/services/Inventory/Inventory';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@Service()
export class SaleReceiptInventoryTransactions {
@Inject()
private inventoryService: InventoryService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async recordInventoryTransactions(
tenantId: number,
saleReceipt: ISaleReceipt,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
saleReceipt.entries
);
const transaction = {
transactionId: saleReceipt.id,
transactionType: 'SaleReceipt',
transactionNumber: saleReceipt.receiptNumber,
exchangeRate: saleReceipt.exchangeRate,
date: saleReceipt.receiptDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleReceipt.createdAt,
warehouseId: saleReceipt.warehouseId,
};
return this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
override,
trx
);
}
/**
* Reverts the inventory transactions of the given bill id.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
receiptId: number,
trx?: Knex.Transaction
) {
return this.inventoryService.deleteInventoryTransactions(
tenantId,
receiptId,
'SaleReceipt',
trx
);
}
}

View File

@@ -1,38 +1,33 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import SMSClient from '@/services/SMSClient';
import {
ISaleReceiptSmsDetails,
ISaleReceipt,
SMS_NOTIFICATION_KEY,
ICustomer,
} from '@/interfaces';
import SalesReceiptService from './SalesReceipts';
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
import { formatNumber, formatSmsMessage } from 'utils';
import { TenantMetadata } from '@/system/models';
import SaleNotifyBySms from './SaleNotifyBySms';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './Receipts/constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import SaleNotifyBySms from '../SaleNotifyBySms';
import { ERRORS } from './constants';
@Service()
export default class SaleReceiptNotifyBySms {
export class SaleReceiptNotifyBySms {
@Inject()
receiptsService: SalesReceiptService;
private tenancy: HasTenancyService;
@Inject()
tenancy: HasTenancyService;
private eventPublisher: EventPublisher;
@Inject()
eventPublisher: EventPublisher;
private smsNotificationsSettings: SmsNotificationsSettingsService;
@Inject()
smsNotificationsSettings: SmsNotificationsSettingsService;
@Inject()
saleSmsNotification: SaleNotifyBySms;
private saleSmsNotification: SaleNotifyBySms;
/**
* Notify customer via sms about sale receipt.

View File

@@ -0,0 +1,106 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ACCOUNT_PARENT_TYPE } from '@/data/AccountTypes';
import { ERRORS } from './constants';
import { SaleEstimate, SaleReceipt } from '@/models';
@Service()
export class SaleReceiptValidators {
@Inject()
private tenancy: HasTenancyService;
/**
* Validates the sale receipt existance.
* @param {SaleEstimate | undefined | null} estimate
*/
public validateReceiptExistance(receipt: SaleReceipt | undefined | null) {
if (!receipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
}
}
/**
* Validates the receipt not closed.
* @param {SaleReceipt} receipt
*/
public validateReceiptNotClosed(receipt: SaleReceipt) {
if (receipt.isClosed) {
throw new ServiceError(ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED);
}
}
/**
* Validate whether sale receipt deposit account exists on the storage.
* @param {number} tenantId - Tenant id.
* @param {number} accountId - Account id.
*/
public async validateReceiptDepositAccountExistance(
tenantId: number,
accountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const depositAccount = await accountRepository.findOneById(accountId);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
}
if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET);
}
}
/**
* Validate sale receipt number uniquiness on the storage.
* @param {number} tenantId -
* @param {string} receiptNumber -
* @param {number} notReceiptId -
*/
public async validateReceiptNumberUnique(
tenantId: number,
receiptNumber: string,
notReceiptId?: number
) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const saleReceipt = await SaleReceipt.query()
.findOne('receipt_number', receiptNumber)
.onBuild((builder) => {
if (notReceiptId) {
builder.whereNot('id', notReceiptId);
}
});
if (saleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NUMBER_NOT_UNIQUE);
}
}
/**
* Validate the sale receipt number require.
* @param {ISaleReceipt} saleReceipt
*/
public validateReceiptNoRequire(receiptNumber: string) {
if (!receiptNumber) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED);
}
}
/**
* Validate the given customer has no sales receipts.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoReceipts(
tenantId: number,
customerId: number
) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const receipts = await SaleReceipt.query().where('customer_id', customerId);
if (receipts.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}

View File

@@ -5,7 +5,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Tenant } from '@/system/models';
@Service()
export default class SaleReceiptsPdf {
export class SaleReceiptsPdf {
@Inject()
pdfService: PdfService;

View File

@@ -22,7 +22,7 @@ export class SaleReceiptCostGLEntriesSubscriber {
* Writes the receipts cost GL entries once the inventory cost lots be written.
* @param {IInventoryCostLotsGLEntriesWriteEvent}
*/
writeJournalEntriesOnceWriteoffCreate = async ({
private writeJournalEntriesOnceWriteoffCreate = async ({
trx,
startingDate,
tenantId,

View File

@@ -17,7 +17,6 @@ export default class SaleNotifyBySms {
if (!personalPhone) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_NO_PHONE_NUMBER);
}
this.validateCustomerPhoneNumberLocally(personalPhone);
};

View File

@@ -1,718 +0,0 @@
import { omit, sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import { Knex } from 'knex';
import {
IEstimatesFilter,
IFilterMeta,
IPaginationMeta,
ISaleEstimate,
ISaleEstimateApprovedEvent,
ISaleEstimateCreatedPayload,
ISaleEstimateCreatingPayload,
ISaleEstimateDeletedPayload,
ISaleEstimateDeletingPayload,
ISaleEstimateDTO,
ISaleEstimateEditedPayload,
ISaleEstimateEditingPayload,
ISaleEstimateEventDeliveredPayload,
ISaleEstimateEventDeliveringPayload,
ISaleEstimateApprovingEvent,
ISalesEstimatesService,
ICustomer,
} from '@/interfaces';
import { formatDateFields } from 'utils';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import events from '@/subscribers/events';
import { ServiceError } from '@/exceptions';
import moment from 'moment';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import SaleEstimateTransformer from './Estimates/SaleEstimateTransformer';
import { ERRORS } from './Estimates/constants';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
/**
* Sale estimate service.
* @Service
*/
@Service('SalesEstimates')
export default class SaleEstimateService implements ISalesEstimatesService {
@Inject()
tenancy: TenancyService;
@Inject()
itemsEntriesService: ItemsEntriesService;
@Inject('logger')
logger: any;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
eventPublisher: EventPublisher;
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
uow: UnitOfWork;
@Inject()
branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
transformer: TransformerInjectable;
/**
* Retrieve sale estimate or throw service error.
* @param {number} tenantId
* @return {ISaleEstimate}
*/
async getSaleEstimateOrThrowError(tenantId: number, saleEstimateId: number) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const foundSaleEstimate = await SaleEstimate.query().findById(
saleEstimateId
);
if (!foundSaleEstimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
}
return foundSaleEstimate;
}
/**
* Validate the estimate number unique on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
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 -
*/
validateEstimateNotConverted(saleEstimate: ISaleEstimate) {
if (saleEstimate.isConvertedToInvoice) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
}
}
/**
* Retrieve the next unique estimate number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
getNextEstimateNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'sales_estimates'
);
}
/**
* Increment the estimate next number.
* @param {number} tenantId -
*/
incrementNextEstimateNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'sales_estimates'
);
}
/**
* Retrieve estimate number to object model.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO
* @param {ISaleEstimate} oldSaleEstimate
*/
transformEstimateNumberToModel(
tenantId: number,
saleEstimateDTO: ISaleEstimateDTO,
oldSaleEstimate?: ISaleEstimate
): string {
// Retreive the next invoice number.
const autoNextNumber = this.getNextEstimateNumber(tenantId);
if (saleEstimateDTO.estimateNumber) {
return saleEstimateDTO.estimateNumber;
}
return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber;
}
/**
* 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.getNextEstimateNumber(tenantId);
// Retreive the next estimate number.
const estimateNumber =
estimateDTO.estimateNumber ||
oldSaleEstimate?.estimateNumber ||
autoNextNumber;
// Validate the sale estimate number require.
this.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);
}
/**
* Validate the sale estimate number require.
* @param {ISaleEstimate} saleInvoiceObj
*/
validateEstimateNoRequire(estimateNumber: string) {
if (!estimateNumber) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED);
}
}
/**
* 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.transformDTOToModel(
tenantId,
estimateDTO,
customer
);
// Validate estimate number uniquiness on the storage.
await this.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;
});
}
/**
* Edit details of the given estimate with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {Integer} estimateId
* @param {EstimateDTO} estimate
* @return {void}
*/
public async editEstimate(
tenantId: number,
estimateId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate, Contact } = this.tenancy.models(tenantId);
const oldSaleEstimate = await this.getSaleEstimateOrThrowError(
tenantId,
estimateId
);
// 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.transformDTOToModel(
tenantId,
estimateDTO,
oldSaleEstimate,
customer
);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
await this.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;
});
}
/**
* 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 this.getSaleEstimateOrThrowError(
tenantId,
estimateId
);
// 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);
});
}
/**
* 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');
if (!estimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
}
// Transformes sale estimate model to POJO.
return this.transformer.transform(
tenantId,
estimate,
new SaleEstimateTransformer()
);
}
/**
* Parses estimates list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieves estimates filterable and paginated list.
* @param {number} tenantId -
* @param {IEstimatesFilter} estimatesFilter -
*/
public async estimatesList(
tenantId: number,
filterDTO: IEstimatesFilter
): 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 SaleEstimateTransformer()
);
return {
salesEstimates: transformedEstimates,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Converts estimate to invoice.
* @param {number} tenantId -
* @param {number} estimateId -
* @return {Promise<void>}
*/
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 this.getSaleEstimateOrThrowError(
tenantId,
estimateId
);
// 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,
{}
);
}
/**
* Unlink the converted sale estimates from the given sale invoice.
* @param {number} tenantId -
* @param {number} invoiceId -
* @return {Promise<void>}
*/
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,
});
}
/**
* 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 this.getSaleEstimateOrThrowError(
tenantId,
saleEstimateId
);
// 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);
});
}
/**
* 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 this.getSaleEstimateOrThrowError(
tenantId,
saleEstimateId
);
// 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);
});
}
/**
* 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 this.getSaleEstimateOrThrowError(
tenantId,
saleEstimateId
);
// 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, {});
});
}
/**
* 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

@@ -1,799 +0,0 @@
import { Service, Inject } from 'typedi';
import { omit, sumBy } from 'lodash';
import * as R from 'ramda';
import moment from 'moment';
import { Knex } from 'knex';
import composeAsync from 'async/compose';
import {
ISaleInvoice,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
ISalesInvoicesFilter,
IPaginationMeta,
IFilterMeta,
ISystemUser,
ISalesInvoicesService,
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletePayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEventDeliveredPayload,
ISaleInvoiceEditedPayload,
ISaleInvoiceCreatingPaylaod,
ISaleInvoiceEditingPayload,
ISaleInvoiceDeliveringPayload,
ICustomer,
ITenantUser,
} from '@/interfaces';
import events from '@/subscribers/events';
import InventoryService from '@/services/Inventory/Inventory';
import TenancyService from '@/services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { ServiceError } from '@/exceptions';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import SaleEstimateService from '@/services/Sales/SalesEstimate';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import { ERRORS } from './constants';
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
/**
* Sales invoices service
* @service
*/
@Service('SalesInvoices')
export default class SaleInvoicesService implements ISalesInvoicesService {
@Inject()
tenancy: TenancyService;
@Inject()
inventoryService: InventoryService;
@Inject()
itemsEntriesService: ItemsEntriesService;
@Inject('logger')
logger: any;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
private saleEstimatesService: SaleEstimateService;
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private transformer: TransformerInjectable;
/**
* Validate whether sale invoice number unqiue on the storage.
*/
async validateInvoiceNumberUnique(
tenantId: number,
invoiceNumber: string,
notInvoiceId?: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findOne('invoice_no', invoiceNumber)
.onBuild((builder) => {
if (notInvoiceId) {
builder.whereNot('id', notInvoiceId);
}
});
if (saleInvoice) {
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE);
}
}
/**
* Validate the sale invoice has no payment entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
*/
async validateInvoiceHasNoPaymentEntries(
tenantId: number,
saleInvoiceId: number
) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
// Retrieve the sale invoice associated payment receive entries.
const entries = await PaymentReceiveEntry.query().where(
'invoice_id',
saleInvoiceId
);
if (entries.length > 0) {
throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
}
/**
* Validate the invoice amount is bigger than payment amount before edit the invoice.
* @param {number} saleInvoiceAmount
* @param {number} paymentAmount
*/
validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceAmount: number,
paymentAmount: number
) {
if (saleInvoiceAmount < paymentAmount) {
throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT);
}
}
/**
* Validate whether sale invoice exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async getInvoiceOrThrowError(tenantId: number, saleInvoiceId: number) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const saleInvoice = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries'
);
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
return saleInvoice;
}
/**
* Retrieve the next unique invoice number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
getNextInvoiceNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'sales_invoices'
);
}
/**
* Increment the invoice next number.
* @param {number} tenantId -
*/
incrementNextInvoiceNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'sales_invoices'
);
}
/**
* Transformes edit DTO to model.
* @param {number} tennatId -
* @param {ICustomer} customer -
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO -
* @param {ISaleInvoice} oldSaleInvoice
*/
private tranformEditDTOToModel = async (
tenantId: number,
customer: ICustomer,
saleInvoiceDTO: ISaleInvoiceEditDTO,
oldSaleInvoice: ISaleInvoice,
authorizedUser: ITenantUser
) => {
return this.transformDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser,
oldSaleInvoice
);
};
/**
* Transformes create DTO to model.
* @param {number} tenantId -
* @param {ICustomer} customer -
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO -
*/
private transformCreateDTOToModel = async (
tenantId: number,
customer: ICustomer,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
) => {
return this.transformDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser
);
};
/**
* Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
* @param {ISaleInvoice} oldSaleInvoice - Old sale invoice.
* @return {ISaleInvoice}
*/
private async transformDTOToModel(
tenantId: number,
customer: ICustomer,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
authorizedUser: ITenantUser,
oldSaleInvoice?: ISaleInvoice
): Promise<ISaleInvoice> {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
// Retreive the next invoice number.
const autoNextNumber = this.getNextInvoiceNumber(tenantId);
// Invoice number.
const invoiceNo =
saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber;
// Validate the invoice is required.
this.validateInvoiceNoRequire(invoiceNo);
const initialEntries = saleInvoiceDTO.entries.map((entry) => ({
referenceType: 'SaleInvoice',
...entry,
}));
const entries = await composeAsync(
// Sets default cost and sell account to invoice items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
)(initialEntries);
const initialDTO = {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
['invoiceDate', 'dueDate']
),
// Avoid rewrite the deliver date in edit mode when already published.
balance,
currencyCode: customer.currencyCode,
exchangeRate: saleInvoiceDTO.exchangeRate || 1,
...(saleInvoiceDTO.delivered &&
!oldSaleInvoice?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
// Avoid override payment amount in edit mode.
...(!oldSaleInvoice && { paymentAmount: 0 }),
...(invoiceNo ? { invoiceNo } : {}),
entries,
userId: authorizedUser.id,
} as ISaleInvoice;
return R.compose(
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
)(initialDTO);
}
/**
* Validate the invoice number require.
* @param {ISaleInvoice} saleInvoiceObj
*/
validateInvoiceNoRequire(invoiceNo: string) {
if (!invoiceNo) {
throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED);
}
}
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
* @async
* @param {number} tenantId - Tenant id.
* @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO.
* @return {Promise<ISaleInvoice>}
*/
public createSaleInvoice = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
): Promise<ISaleInvoice> => {
const { SaleInvoice, Contact } = this.tenancy.models(tenantId);
// Validate customer existance.
const customer = await Contact.query()
.modify('customer')
.findById(saleInvoiceDTO.customerId)
.throwIfNotFound();
// Validate the from estimate id exists on the storage.
if (saleInvoiceDTO.fromEstimateId) {
const fromEstimate =
await this.saleEstimatesService.getSaleEstimateOrThrowError(
tenantId,
saleInvoiceDTO.fromEstimateId
);
// Validate the sale estimate is not already converted to invoice.
this.saleEstimatesService.validateEstimateNotConverted(fromEstimate);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformCreateDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
authorizedUser
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceObj.invoiceNo
);
}
// Creates a new sale invoice and associated transactions under unit of work env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceCreating` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
saleInvoiceDTO,
tenantId,
trx,
} as ISaleInvoiceCreatingPaylaod);
// Create sale invoice graph to the storage.
const saleInvoice = await SaleInvoice.query(trx).upsertGraph(
saleInvoiceObj
);
const eventPayload: ISaleInvoiceCreatedPayload = {
tenantId,
saleInvoice,
saleInvoiceDTO,
saleInvoiceId: saleInvoice.id,
authorizedUser,
trx,
};
// Triggers the event `onSaleInvoiceCreated`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onCreated,
eventPayload
);
return saleInvoice;
});
};
/**
* Edit the given sale invoice.
* @async
* @param {number} tenantId - Tenant id.
* @param {Number} saleInvoiceId - Sale invoice id.
* @param {ISaleInvoice} saleInvoice - Sale invoice DTO object.
* @return {Promise<ISaleInvoice>}
*/
public async editSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceDTO: ISaleInvoiceEditDTO,
authorizedUser: ISystemUser
): Promise<ISaleInvoice> {
const { SaleInvoice, Contact } = this.tenancy.models(tenantId);
// Retrieve the sale invoice or throw not found service error.
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Validate customer existance.
const customer = await Contact.query()
.findById(saleInvoiceDTO.customerId)
.modify('customer')
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
saleInvoiceId,
'SaleInvoice',
saleInvoiceDTO.entries
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.tranformEditDTOToModel(
tenantId,
customer,
saleInvoiceDTO,
oldSaleInvoice,
authorizedUser
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceObj.invoiceNo,
saleInvoiceId
);
}
// Validate the invoice amount is not smaller than the invoice payment amount.
this.validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceObj.balance,
oldSaleInvoice.paymentAmount
);
// Edit sale invoice transaction in UOW envirment.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceEditing` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, {
trx,
oldSaleInvoice,
tenantId,
saleInvoiceDTO,
} as ISaleInvoiceEditingPayload);
// Upsert the the invoice graph to the storage.
const saleInvoice: ISaleInvoice =
await SaleInvoice.query().upsertGraphAndFetch({
id: saleInvoiceId,
...saleInvoiceObj,
});
// Edit event payload.
const editEventPayload: ISaleInvoiceEditedPayload = {
tenantId,
saleInvoiceId,
saleInvoice,
saleInvoiceDTO,
oldSaleInvoice,
authorizedUser,
trx,
};
// Triggers `onSaleInvoiceEdited` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onEdited,
editEventPayload
);
return saleInvoice;
});
}
/**
* Deliver the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
* @return {Promise<void>}
*/
public async deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve details of the given sale invoice id.
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Throws error in case the sale invoice already published.
if (oldSaleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Update sale invoice transaction with assocaite transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDelivering` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivering, {
tenantId,
oldSaleInvoice,
trx,
} as ISaleInvoiceDeliveringPayload);
// Record the delivered at on the storage.
const saleInvoice = await SaleInvoice.query(trx)
.where({ id: saleInvoiceId })
.update({ deliveredAt: moment().toMySqlDateTime() });
// Triggers `onSaleInvoiceDelivered` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, {
tenantId,
saleInvoiceId,
saleInvoice,
} as ISaleInvoiceEventDeliveredPayload);
});
}
/**
* Deletes the given sale invoice with associated entries
* and journal transactions.
* @param {number} tenantId - Tenant id.
* @param {Number} saleInvoiceId - The given sale invoice id.
* @param {ISystemUser} authorizedUser -
*/
public async deleteSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<void> {
const { ItemEntry, SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve the given sale invoice with associated entries
// or throw not found error.
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Validate the sale invoice has no associated payment entries.
await this.validateInvoiceHasNoPaymentEntries(tenantId, saleInvoiceId);
// Validate the sale invoice has applied to credit note transaction.
await this.validateInvoiceHasNoAppliedToCredit(tenantId, saleInvoiceId);
// Deletes sale invoice transaction and associate transactions with UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDelete` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, {
tenantId,
saleInvoice: oldSaleInvoice,
saleInvoiceId,
trx,
} as ISaleInvoiceDeletePayload);
// Unlink the converted sale estimates from the given sale invoice.
await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice(
tenantId,
saleInvoiceId,
trx
);
await ItemEntry.query(trx)
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice')
.delete();
await SaleInvoice.query(trx).findById(saleInvoiceId).delete();
// Triggers `onSaleInvoiceDeleted` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, {
tenantId,
oldSaleInvoice,
saleInvoiceId,
authorizedUser,
trx,
} as ISaleInvoiceDeletedPayload);
});
}
/**
* Records the inventory transactions of the given sale invoice in case
* the invoice has inventory entries only.
*
* @param {number} tenantId - Tenant id.
* @param {SaleInvoice} saleInvoice - Sale invoice DTO.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {boolean} override - Allow to override old transactions.
* @return {Promise<void>}
*/
public async recordInventoryTranscactions(
tenantId: number,
saleInvoice: ISaleInvoice,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
saleInvoice.entries
);
const transaction = {
transactionId: saleInvoice.id,
transactionType: 'SaleInvoice',
transactionNumber: saleInvoice.invoiceNo,
exchangeRate: saleInvoice.exchangeRate,
warehouseId: saleInvoice.warehouseId,
date: saleInvoice.invoiceDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleInvoice.createdAt,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
override,
trx
);
}
/**
* Reverting the inventory transactions once the invoice deleted.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
// Delete the inventory transaction of the given sale invoice.
const { oldInventoryTransactions } =
await this.inventoryService.deleteInventoryTransactions(
tenantId,
saleInvoiceId,
'SaleInvoice',
trx
);
}
/**
* Retrieve sale invoice with associated entries.
* @param {Number} saleInvoiceId -
* @param {ISystemUser} authorizedUser -
* @return {Promise<ISaleInvoice>}
*/
public async getSaleInvoice(
tenantId: number,
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch');
return this.transformer.transform(
tenantId,
saleInvoice,
new SaleInvoiceTransformer()
);
}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve sales invoices filterable and paginated list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async salesInvoicesList(
tenantId: number,
filterDTO: ISalesInvoicesFilter
): Promise<{
salesInvoices: ISaleInvoice[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleInvoice,
filter
);
const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Retrieves the transformed sale invoices.
const salesInvoices = await this.transformer.transform(
tenantId,
results,
new SaleInvoiceTransformer()
);
return {
salesInvoices,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Retrieve due sales invoices.
* @param {number} tenantId
* @param {number} customerId
*/
public async getPayableInvoices(
tenantId: number,
customerId?: number
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const salesInvoices = await SaleInvoice.query().onBuild((query) => {
query.modify('dueInvoices');
query.modify('delivered');
if (customerId) {
query.where('customer_id', customerId);
}
});
return salesInvoices;
}
/**
* Validate the given customer has no sales invoices.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoInvoices(
tenantId: number,
customerId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoices = await SaleInvoice.query().where('customer_id', customerId);
if (invoices.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
/**
* Validate the sale invoice has no applied to credit note transaction.
* @param {number} tenantId
* @param {number} invoiceId
* @returns {Promise<void>}
*/
public validateInvoiceHasNoAppliedToCredit = async (
tenantId: number,
invoiceId: number
): Promise<void> => {
const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId);
const appliedTransactions = await CreditNoteAppliedInvoice.query().where(
'invoiceId',
invoiceId
);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES);
}
};
}

View File

@@ -1,629 +0,0 @@
import { omit, sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import moment from 'moment';
import * as R from 'ramda';
import { Knex } from 'knex';
import composeAsync from 'async/compose';
import events from '@/subscribers/events';
import {
IFilterMeta,
IPaginationMeta,
ISaleReceipt,
ISaleReceiptDTO,
ISalesReceiptsService,
ISaleReceiptCreatedPayload,
ISaleReceiptEditedPayload,
ISaleReceiptEventClosedPayload,
ISaleReceiptEventDeletedPayload,
ISaleReceiptCreatingPayload,
ISaleReceiptDeletingPayload,
ISaleReceiptEditingPayload,
ISaleReceiptEventClosingPayload,
ICustomer,
} from '@/interfaces';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import TenancyService from '@/services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { ServiceError } from '@/exceptions';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import { ItemEntry } from 'models';
import InventoryService from '@/services/Inventory/Inventory';
import { ACCOUNT_PARENT_TYPE } from '@/data/AccountTypes';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import { ERRORS } from './Receipts/constants';
import { SaleReceiptTransformer } from './Receipts/SaleReceiptTransformer';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service('SalesReceipts')
export default class SalesReceiptService implements ISalesReceiptsService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
journalService: JournalPosterService;
@Inject()
itemsEntriesService: ItemsEntriesService;
@Inject()
inventoryService: InventoryService;
@Inject()
eventPublisher: EventPublisher;
@Inject('logger')
logger: any;
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
uow: UnitOfWork;
@Inject()
branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
transformer: TransformerInjectable;
/**
* Validate whether sale receipt exists on the storage.
* @param {number} tenantId -
* @param {number} saleReceiptId -
*/
async getSaleReceiptOrThrowError(tenantId: number, saleReceiptId: number) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const foundSaleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries');
if (!foundSaleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
}
return foundSaleReceipt;
}
/**
* Validate whether sale receipt deposit account exists on the storage.
* @param {number} tenantId - Tenant id.
* @param {number} accountId - Account id.
*/
async validateReceiptDepositAccountExistance(
tenantId: number,
accountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const depositAccount = await accountRepository.findOneById(accountId);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
}
if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET);
}
}
/**
* Validate sale receipt number uniquiness on the storage.
* @param {number} tenantId -
* @param {string} receiptNumber -
* @param {number} notReceiptId -
*/
async validateReceiptNumberUnique(
tenantId: number,
receiptNumber: string,
notReceiptId?: number
) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const saleReceipt = await SaleReceipt.query()
.findOne('receipt_number', receiptNumber)
.onBuild((builder) => {
if (notReceiptId) {
builder.whereNot('id', notReceiptId);
}
});
if (saleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NUMBER_NOT_UNIQUE);
}
}
/**
* Validate the sale receipt number require.
* @param {ISaleReceipt} saleReceipt
*/
validateReceiptNoRequire(receiptNumber: string) {
if (!receiptNumber) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED);
}
}
/**
* Retrieve the next unique receipt number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
getNextReceiptNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'sales_receipts'
);
}
/**
* Increment the receipt next number.
* @param {number} tenantId -
*/
incrementNextReceiptNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'sales_receipts'
);
}
/**
* Transform create DTO object to model object.
* @param {ISaleReceiptDTO} saleReceiptDTO -
* @param {ISaleReceipt} oldSaleReceipt -
* @returns {ISaleReceipt}
*/
async transformDTOToModel(
tenantId: number,
saleReceiptDTO: ISaleReceiptDTO,
paymentCustomer: ICustomer,
oldSaleReceipt?: ISaleReceipt
): Promise<ISaleReceipt> {
const amount = sumBy(saleReceiptDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
// Retreive the next invoice number.
const autoNextNumber = this.getNextReceiptNumber(tenantId);
// Retreive the receipt number.
const receiptNumber =
saleReceiptDTO.receiptNumber ||
oldSaleReceipt?.receiptNumber ||
autoNextNumber;
// Validate receipt number require.
this.validateReceiptNoRequire(receiptNumber);
const initialEntries = saleReceiptDTO.entries.map((entry) => ({
reference_type: 'SaleReceipt',
...entry,
}));
const entries = await composeAsync(
// Sets default cost and sell account to receipt items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
)(initialEntries);
const initialDTO = {
amount,
...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [
'receiptDate',
]),
currencyCode: paymentCustomer.currencyCode,
exchangeRate: saleReceiptDTO.exchangeRate || 1,
receiptNumber,
// Avoid rewrite the deliver date in edit mode when already published.
...(saleReceiptDTO.closed &&
!oldSaleReceipt?.closedAt && {
closedAt: moment().toMySqlDateTime(),
}),
entries,
};
return R.compose(
this.branchDTOTransform.transformDTO<ISaleReceipt>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleReceipt>(tenantId)
)(initialDTO);
}
/**
* Creates a new sale receipt with associated entries.
* @async
* @param {ISaleReceipt} saleReceipt
* @return {Object}
*/
public async createSaleReceipt(
tenantId: number,
saleReceiptDTO: any
): Promise<ISaleReceipt> {
const { SaleReceipt, Contact } = this.tenancy.models(tenantId);
// Retireves the payment customer model.
const paymentCustomer = await Contact.query()
.modify('customer')
.findById(saleReceiptDTO.customerId)
.throwIfNotFound();
// Transform sale receipt DTO to model.
const saleReceiptObj = await this.transformDTOToModel(
tenantId,
saleReceiptDTO,
paymentCustomer
);
// Validate receipt deposit account existance and type.
await this.validateReceiptDepositAccountExistance(
tenantId,
saleReceiptDTO.depositAccountId
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleReceiptDTO.entries
);
// Validate the sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleReceiptDTO.entries
);
// Validate sale receipt number uniuqiness.
if (saleReceiptDTO.receiptNumber) {
await this.validateReceiptNumberUnique(
tenantId,
saleReceiptDTO.receiptNumber
);
}
// Creates a sale receipt transaction and associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptCreating` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreating, {
saleReceiptDTO,
tenantId,
trx,
} as ISaleReceiptCreatingPayload);
// Inserts the sale receipt graph to the storage.
const saleReceipt = await SaleReceipt.query().upsertGraph({
...saleReceiptObj,
});
// Triggers `onSaleReceiptCreated` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreated, {
tenantId,
saleReceipt,
saleReceiptId: saleReceipt.id,
trx,
} as ISaleReceiptCreatedPayload);
return saleReceipt;
});
}
/**
* Edit details sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @param {ISaleReceipt} saleReceipt
* @return {void}
*/
public async editSaleReceipt(
tenantId: number,
saleReceiptId: number,
saleReceiptDTO: any
) {
const { SaleReceipt, Contact } = this.tenancy.models(tenantId);
// Retrieve sale receipt or throw not found service error.
const oldSaleReceipt = await this.getSaleReceiptOrThrowError(
tenantId,
saleReceiptId
);
// Retrieves the payment customer model.
const paymentCustomer = await Contact.query()
.findById(saleReceiptId)
.modify('customer')
.throwIfNotFound();
// Transform sale receipt DTO to model.
const saleReceiptObj = await this.transformDTOToModel(
tenantId,
saleReceiptDTO,
paymentCustomer,
oldSaleReceipt
);
// Validate receipt deposit account existance and type.
await this.validateReceiptDepositAccountExistance(
tenantId,
saleReceiptDTO.depositAccountId
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleReceiptDTO.entries
);
// Validate the sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleReceiptDTO.entries
);
// Validate sale receipt number uniuqiness.
if (saleReceiptDTO.receiptNumber) {
await this.validateReceiptNumberUnique(
tenantId,
saleReceiptDTO.receiptNumber,
saleReceiptId
);
}
// Edits the sale receipt tranasctions with associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptsEditing` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onEditing, {
tenantId,
oldSaleReceipt,
saleReceiptDTO,
trx,
} as ISaleReceiptEditingPayload);
// Upsert the receipt graph to the storage.
const saleReceipt = await SaleReceipt.query(trx).upsertGraphAndFetch({
id: saleReceiptId,
...saleReceiptObj,
});
// Triggers `onSaleReceiptEdited` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, {
tenantId,
oldSaleReceipt,
saleReceipt,
saleReceiptId,
trx,
} as ISaleReceiptEditedPayload);
return saleReceipt;
});
}
/**
* Deletes the sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @return {void}
*/
public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const oldSaleReceipt = await this.getSaleReceiptOrThrowError(
tenantId,
saleReceiptId
);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptsDeleting` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onDeleting, {
trx,
oldSaleReceipt,
tenantId,
} as ISaleReceiptDeletingPayload);
//
await ItemEntry.query(trx)
.where('reference_id', saleReceiptId)
.where('reference_type', 'SaleReceipt')
.delete();
// Delete the sale receipt transaction.
await SaleReceipt.query(trx).where('id', saleReceiptId).delete();
// Triggers `onSaleReceiptsDeleted` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onDeleted, {
tenantId,
saleReceiptId,
oldSaleReceipt,
trx,
} as ISaleReceiptEventDeletedPayload);
});
}
/**
* Retrieve sale receipt with associated entries.
* @param {Integer} saleReceiptId
* @return {ISaleReceipt}
*/
async getSaleReceipt(tenantId: number, saleReceiptId: number) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('depositAccount')
.withGraphFetched('branch');
if (!saleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
}
return this.transformer.transform(
tenantId,
saleReceipt,
new SaleReceiptTransformer()
);
}
/**
* Parses the sale receipts list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve sales receipts paginated and filterable list.
* @param {number} tenantId
* @param {ISaleReceiptFilter} salesReceiptsFilter
*/
public async salesReceiptsList(
tenantId: number,
filterDTO: ISaleReceiptFilter
): Promise<{
data: ISaleReceipt[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleReceipt,
filter
);
const { results, pagination } = await SaleReceipt.query()
.onBuild((builder) => {
builder.withGraphFetched('depositAccount');
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformes the estimates models to POJO.
const salesEstimates = await this.transformer.transform(
tenantId,
results,
new SaleReceiptTransformer()
);
return {
data: salesEstimates,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Mark the given sale receipt as closed.
* @param {number} tenantId
* @param {number} saleReceiptId
* @return {Promise<void>}
*/
async closeSaleReceipt(
tenantId: number,
saleReceiptId: number
): Promise<void> {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Retrieve sale receipt or throw not found service error.
const oldSaleReceipt = await this.getSaleReceiptOrThrowError(
tenantId,
saleReceiptId
);
// Throw service error if the sale receipt already closed.
if (oldSaleReceipt.isClosed) {
throw new ServiceError(ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED);
}
// Updates the sale recept transaction under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptClosing` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onClosing, {
tenantId,
oldSaleReceipt,
trx,
} as ISaleReceiptEventClosingPayload);
// Mark the sale receipt as closed on the storage.
const saleReceipt = await SaleReceipt.query(trx)
.findById(saleReceiptId)
.patch({
closedAt: moment().toMySqlDateTime(),
});
// Triggers `onSaleReceiptClosed` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onClosed, {
saleReceiptId,
saleReceipt,
tenantId,
trx,
} as ISaleReceiptEventClosedPayload);
});
}
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async recordInventoryTransactions(
tenantId: number,
saleReceipt: ISaleReceipt,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
saleReceipt.entries
);
const transaction = {
transactionId: saleReceipt.id,
transactionType: 'SaleReceipt',
transactionNumber: saleReceipt.receiptNumber,
exchangeRate: saleReceipt.exchangeRate,
date: saleReceipt.receiptDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleReceipt.createdAt,
warehouseId: saleReceipt.warehouseId,
};
return this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
override,
trx
);
}
/**
* Reverts the inventory transactions of the given bill id.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
receiptId: number,
trx?: Knex.Transaction
) {
return this.inventoryService.deleteInventoryTransactions(
tenantId,
receiptId,
'SaleReceipt',
trx
);
}
/**
* Validate the given customer has no sales receipts.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoReceipts(
tenantId: number,
customerId: number
) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const receipts = await SaleReceipt.query().where('customer_id', customerId);
if (receipts.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}