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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,184 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import {
AccountNormal,
ILedgerEntry,
ISaleReceipt,
IItemEntry,
} from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export class SaleReceiptGLEntries {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private ledgerStorage: LedgerStorageService;
/**
* Creates income GL entries.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {Knex.Transaction} trx
*/
public writeIncomeGLEntries = async (
tenantId: number,
saleReceiptId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { SaleReceipt } = this.tenancy.models(tenantId);
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries.item');
// Retrieve the income entries ledger.
const incomeLedger = this.getIncomeEntriesLedger(saleReceipt);
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(tenantId, incomeLedger, trx);
};
/**
* Reverts the receipt GL entries.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public revertReceiptGLEntries = async (
tenantId: number,
saleReceiptId: number,
trx?: Knex.Transaction
): Promise<void> => {
await this.ledgerStorage.deleteByReference(
tenantId,
saleReceiptId,
'SaleReceipt',
trx
);
};
/**
* Rewrites the receipt GL entries.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public rewriteReceiptGLEntries = async (
tenantId: number,
saleReceiptId: number,
trx?: Knex.Transaction
): Promise<void> => {
// Reverts the receipt GL entries.
await this.revertReceiptGLEntries(tenantId, saleReceiptId, trx);
// Writes the income GL entries.
await this.writeIncomeGLEntries(tenantId, saleReceiptId, trx);
};
/**
* Retrieves the income GL ledger.
* @param {ISaleReceipt} saleReceipt
* @returns {Ledger}
*/
private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => {
const entries = this.getIncomeGLEntries(saleReceipt);
return new Ledger(entries);
};
/**
* Retireves the income GL common entry.
* @param {ISaleReceipt} saleReceipt -
*/
private getIncomeGLCommonEntry = (saleReceipt: ISaleReceipt) => {
return {
currencyCode: saleReceipt.currencyCode,
exchangeRate: saleReceipt.exchangeRate,
transactionType: 'SaleReceipt',
transactionId: saleReceipt.id,
date: saleReceipt.receiptDate,
transactionNumber: saleReceipt.receiptNumber,
referenceNumber: saleReceipt.referenceNo,
createdAt: saleReceipt.createdAt,
credit: 0,
debit: 0,
userId: saleReceipt.userId,
branchId: saleReceipt.branchId,
};
};
/**
* Retrieve receipt income item GL entry.
* @param {ISaleReceipt} saleReceipt -
* @param {IItemEntry} entry -
* @param {number} index -
* @returns {ILedgerEntry}
*/
private getReceiptIncomeItemEntry = R.curry(
(
saleReceipt: ISaleReceipt,
entry: IItemEntry,
index: number
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
const itemIncome = entry.amount * saleReceipt.exchangeRate;
return {
...commonEntry,
credit: itemIncome,
accountId: entry.item.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
itemQuantity: entry.quantity,
accountNormal: AccountNormal.CREDIT,
};
}
);
/**
* Retrieves the receipt deposit GL deposit entry.
* @param {ISaleReceipt} saleReceipt
* @returns {ILedgerEntry}
*/
private getReceiptDepositEntry = (
saleReceipt: ISaleReceipt
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
return {
...commonEntry,
debit: saleReceipt.localAmount,
accountId: saleReceipt.depositAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the income GL entries.
* @param {ISaleReceipt} saleReceipt -
* @returns {ILedgerEntry[]}
*/
private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => {
const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt);
const creditEntries = saleReceipt.entries.map(getItemEntry);
const depositEntry = this.getReceiptDepositEntry(saleReceipt);
return [depositEntry, ...creditEntries];
};
}

View File

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

View File

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

View File

@@ -0,0 +1,206 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import {
ISaleReceiptSmsDetails,
ISaleReceipt,
SMS_NOTIFICATION_KEY,
ICustomer,
} from '@/interfaces';
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
import { formatNumber, formatSmsMessage } from 'utils';
import { TenantMetadata } from '@/system/models';
import { ServiceError } from '@/exceptions';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import SaleNotifyBySms from '../SaleNotifyBySms';
import { ERRORS } from './constants';
@Service()
export class SaleReceiptNotifyBySms {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private smsNotificationsSettings: SmsNotificationsSettingsService;
@Inject()
private saleSmsNotification: SaleNotifyBySms;
/**
* Notify customer via sms about sale receipt.
* @param {number} tenantId - Tenant id.
* @param {number} saleReceiptId - Sale receipt id.
*/
public async notifyBySms(tenantId: number, saleReceiptId: number) {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Retrieve the sale receipt or throw not found service error.
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('customer');
// Validates the receipt receipt existance.
this.validateSaleReceiptExistance(saleReceipt);
// Validate the customer phone number.
this.saleSmsNotification.validateCustomerPhoneNumber(
saleReceipt.customer.personalPhone
);
// Triggers `onSaleReceiptNotifySms` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onNotifySms, {
tenantId,
saleReceipt,
});
// Sends the payment receive sms notification to the given customer.
await this.sendSmsNotification(tenantId, saleReceipt);
// Triggers `onSaleReceiptNotifiedSms` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onNotifiedSms, {
tenantId,
saleReceipt,
});
return saleReceipt;
}
/**
* Sends SMS notification.
* @param {ISaleReceipt} invoice
* @param {ICustomer} customer
* @returns
*/
public sendSmsNotification = async (
tenantId: number,
saleReceipt: ISaleReceipt & { customer: ICustomer }
) => {
const smsClient = this.tenancy.smsClient(tenantId);
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// Retrieve formatted sms notification message of receipt details.
const formattedSmsMessage = this.formattedReceiptDetailsMessage(
tenantId,
saleReceipt,
tenantMetadata
);
const phoneNumber = saleReceipt.customer.personalPhone;
// Run the send sms notification message job.
return smsClient.sendMessageJob(phoneNumber, formattedSmsMessage);
};
/**
* Notify via SMS message after receipt creation.
* @param {number} tenantId
* @param {number} receiptId
* @returns {Promise<void>}
*/
public notifyViaSmsAfterCreation = async (
tenantId: number,
receiptId: number
): Promise<void> => {
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
tenantId,
SMS_NOTIFICATION_KEY.SALE_RECEIPT_DETAILS
);
// Can't continue if the sms auto-notification is not enabled.
if (!notification.isNotificationEnabled) return;
await this.notifyBySms(tenantId, receiptId);
};
/**
* Retrieve the formatted sms notification message of the given sale receipt.
* @param {number} tenantId
* @param {ISaleReceipt} saleReceipt
* @param {TenantMetadata} tenantMetadata
* @returns {string}
*/
private formattedReceiptDetailsMessage = (
tenantId: number,
saleReceipt: ISaleReceipt & { customer: ICustomer },
tenantMetadata: TenantMetadata
): string => {
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
tenantId,
SMS_NOTIFICATION_KEY.SALE_RECEIPT_DETAILS
);
return this.formatReceiptDetailsMessage(
notification.smsMessage,
saleReceipt,
tenantMetadata
);
};
/**
* Formattes the receipt sms notification message.
* @param {string} smsMessage
* @param {ISaleReceipt} saleReceipt
* @param {TenantMetadata} tenantMetadata
* @returns {string}
*/
private formatReceiptDetailsMessage = (
smsMessage: string,
saleReceipt: ISaleReceipt & { customer: ICustomer },
tenantMetadata: TenantMetadata
): string => {
// Format the receipt amount.
const formattedAmount = formatNumber(saleReceipt.amount, {
currencyCode: saleReceipt.currencyCode,
});
return formatSmsMessage(smsMessage, {
ReceiptNumber: saleReceipt.receiptNumber,
ReferenceNumber: saleReceipt.referenceNo,
CustomerName: saleReceipt.customer.displayName,
Amount: formattedAmount,
CompanyName: tenantMetadata.name,
});
};
/**
* Retrieve the SMS details of the given invoice.
* @param {number} tenantId -
* @param {number} saleReceiptId - Sale receipt id.
*/
public smsDetails = async (
tenantId: number,
saleReceiptId: number
): Promise<ISaleReceiptSmsDetails> => {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Retrieve the sale receipt or throw not found service error.
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('customer');
// Validates the receipt receipt existance.
this.validateSaleReceiptExistance(saleReceipt);
// Current tenant metadata.
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// Retrieve the sale receipt formatted sms notification message.
const formattedSmsMessage = this.formattedReceiptDetailsMessage(
tenantId,
saleReceipt,
tenantMetadata
);
return {
customerName: saleReceipt.customer.displayName,
customerPhoneNumber: saleReceipt.customer.personalPhone,
smsMessage: formattedSmsMessage,
};
};
/**
* Validates the receipt receipt existance.
* @param {ISaleReceipt|null} saleReceipt
*/
private validateSaleReceiptExistance(saleReceipt: ISaleReceipt | null) {
if (!saleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
}
}
}

View File

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

View File

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

View File

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