mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
refactor: split the services to multiple service classes (#202)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
123
packages/server/src/services/Sales/Estimates/EditSaleEstimate.ts
Normal file
123
packages/server/src/services/Sales/Estimates/EditSaleEstimate.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, {});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts
Normal file
147
packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts
Normal 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
|
||||
);
|
||||
};
|
||||
}
|
||||
154
packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts
Normal file
154
packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
165
packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts
Normal file
165
packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts
Normal 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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = (
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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) => {
|
||||
@@ -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,
|
||||
@@ -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 = [];
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
106
packages/server/src/services/Sales/Receipts/CreateSaleReceipt.ts
Normal file
106
packages/server/src/services/Sales/Receipts/CreateSaleReceipt.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
119
packages/server/src/services/Sales/Receipts/EditSaleReceipt.ts
Normal file
119
packages/server/src/services/Sales/Receipts/EditSaleReceipt.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,7 +17,6 @@ export default class SaleNotifyBySms {
|
||||
if (!personalPhone) {
|
||||
throw new ServiceError(ERRORS.CUSTOMER_HAS_NO_PHONE_NUMBER);
|
||||
}
|
||||
|
||||
this.validateCustomerPhoneNumberLocally(personalPhone);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user