mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
refactoring: sales receipts service.
This commit is contained in:
@@ -12,6 +12,7 @@ const ERRORS = {
|
||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||
ENTRIES_IDS_NOT_FOUND: 'ENTRIES_IDS_NOT_FOUND',
|
||||
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
|
||||
NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS'
|
||||
};
|
||||
|
||||
@Service()
|
||||
@@ -98,7 +99,7 @@ export default class ItemsEntriesService {
|
||||
const nonSellableItems = difference(itemsIds, sellableItemsIds);
|
||||
|
||||
if (nonSellableItems.length > 0) {
|
||||
throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS);
|
||||
throw new ServiceError(ERRORS.NOT_SELL_ABLE_ITEMS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
import { omit, difference, sumBy } from 'lodash';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import { ISaleReceipt } from 'interfaces';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import HasItemEntries from 'services/Sales/HasItemsEntries';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { IFilterMeta, IPaginationMeta } from 'interfaces';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
|
||||
|
||||
|
||||
const ERRORS = {
|
||||
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
||||
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
|
||||
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
|
||||
};
|
||||
@Service()
|
||||
export default class SalesReceiptService {
|
||||
@Inject()
|
||||
@@ -24,152 +32,84 @@ export default class SalesReceiptService {
|
||||
journalService: JournalPosterService;
|
||||
|
||||
@Inject()
|
||||
itemsEntriesService: HasItemEntries;
|
||||
itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {number} tenantId -
|
||||
* @param {number} saleReceiptId -
|
||||
*/
|
||||
async getSaleReceiptOrThrowError(tenantId: number, saleReceiptId: number) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
|
||||
const isSaleReceiptExists = await this.saleReceiptService
|
||||
.isSaleReceiptExists(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
);
|
||||
if (!isSaleReceiptExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
this.logger.info('[sale_receipt] trying to validate existance.', { tenantId, saleReceiptId });
|
||||
const foundSaleReceipt = await SaleReceipt.query().findById(saleReceiptId);
|
||||
|
||||
if (!foundSaleReceipt) {
|
||||
this.logger.info('[sale_receipt] not found on the storage.', { tenantId, saleReceiptId });
|
||||
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
|
||||
}
|
||||
next();
|
||||
return foundSaleReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const saleReceipt = { ...req.body };
|
||||
const { Customer } = req.models;
|
||||
|
||||
const foundCustomer = await Customer.query().findById(saleReceipt.customer_id);
|
||||
|
||||
if (!foundCustomer) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt deposit account exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* @param {number} tenantId -
|
||||
* @param {number} accountId -
|
||||
*/
|
||||
async validateReceiptDepositAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
async validateReceiptDepositAccountExistance(tenantId: number, accountId: number) {
|
||||
const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId);
|
||||
const depositAccount = await accountRepository.findById(accountId);
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const isDepositAccountExists = await this.accountsService.isAccountExists(
|
||||
tenantId,
|
||||
saleReceipt.deposit_account_id
|
||||
);
|
||||
if (!isDepositAccountExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
if (!depositAccount) {
|
||||
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
next();
|
||||
}
|
||||
const depositAccountType = await accountTypeRepository.getTypeMeta(depositAccount.accountTypeId);
|
||||
|
||||
/**
|
||||
* Validate whether receipt items ids exist on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptItemsIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id);
|
||||
|
||||
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(
|
||||
tenantId,
|
||||
estimateItemsIds
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }] });
|
||||
if (!depositAccountType || depositAccountType.childRoot === 'current_asset') {
|
||||
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate receipt entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptEntriesIds(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
// Validate the entries IDs that not stored or associated to the sale receipt.
|
||||
const notExistsEntriesIds = await this.saleReceiptService
|
||||
.isSaleReceiptEntriesIDsExists(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
saleReceipt,
|
||||
);
|
||||
if (notExistsEntriesIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{
|
||||
type: 'ENTRIES.IDS.NOT.FOUND',
|
||||
code: 500,
|
||||
}]
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @async
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @return {Object}
|
||||
*/
|
||||
public async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
|
||||
public async createSaleReceipt(tenantId: number, saleReceiptDTO: any): Promise<ISaleReceipt> {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
const saleReceipt = {
|
||||
const saleReceiptObj = {
|
||||
amount,
|
||||
...formatDateFields(saleReceiptDTO, ['receipt_date'])
|
||||
};
|
||||
const storedSaleReceipt = await SaleReceipt.query()
|
||||
.insert({
|
||||
...omit(saleReceipt, ['entries']),
|
||||
entries: saleReceipt.entries.map((entry) => ({
|
||||
await this.validateReceiptDepositAccountExistance(tenantId, saleReceiptDTO.depositAccountId);
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleReceiptDTO.entries);
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleReceiptDTO.entries);
|
||||
|
||||
this.logger.info('[sale_receipt] trying to insert sale receipt graph.', { tenantId, saleReceiptDTO });
|
||||
const saleReceipt = await SaleReceipt.query()
|
||||
.insertGraph({
|
||||
...omit(saleReceiptObj, ['entries']),
|
||||
|
||||
entries: saleReceiptObj.entries.map((entry) => ({
|
||||
reference_type: 'SaleReceipt',
|
||||
reference_id: storedSaleReceipt.id,
|
||||
...omit(entry, ['id', 'amount']),
|
||||
}))
|
||||
});
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated, { tenantId, saleReceipt });
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
|
||||
this.logger.info('[sale_receipt] sale receipt inserted successfully.', { tenantId });
|
||||
|
||||
return saleReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,30 +122,32 @@ export default class SalesReceiptService {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
const saleReceipt = {
|
||||
const saleReceiptObj = {
|
||||
amount,
|
||||
...formatDateFields(saleReceiptDTO, ['receipt_date'])
|
||||
};
|
||||
const updatedSaleReceipt = await SaleReceipt.query()
|
||||
.where('id', saleReceiptId)
|
||||
.update({
|
||||
...omit(saleReceipt, ['entries']),
|
||||
const oldSaleReceipt = await this.getSaleReceiptOrThrowError(tenantId, saleReceiptId);
|
||||
|
||||
await this.validateReceiptDepositAccountExistance(tenantId, saleReceiptDTO.depositAccountId);
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleReceiptDTO.entries);
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleReceiptDTO.entries);
|
||||
|
||||
const saleReceipt = await SaleReceipt.query()
|
||||
.upsertGraph({
|
||||
id: saleReceiptId,
|
||||
...omit(saleReceiptObj, ['entries']),
|
||||
|
||||
entries: saleReceiptObj.entries.map((entry) => ({
|
||||
reference_type: 'SaleReceipt',
|
||||
...omit(entry, ['amount']),
|
||||
}))
|
||||
});
|
||||
const storedSaleReceiptEntries = await ItemEntry.query()
|
||||
.where('reference_id', saleReceiptId)
|
||||
.where('reference_type', 'SaleReceipt');
|
||||
|
||||
// Patch sale receipt items entries.
|
||||
const patchItemsEntries = this.itemsEntriesService.patchItemsEntries(
|
||||
tenantId,
|
||||
saleReceipt.entries,
|
||||
storedSaleReceiptEntries,
|
||||
'SaleReceipt',
|
||||
saleReceiptId,
|
||||
);
|
||||
await Promise.all([patchItemsEntries]);
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
|
||||
this.logger.info('[sale_receipt] edited successfully.', { tenantId, saleReceiptId });
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onEdited, {
|
||||
oldSaleReceipt, tenantId, saleReceiptId, saleReceipt,
|
||||
});
|
||||
return saleReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,66 +157,18 @@ export default class SalesReceiptService {
|
||||
*/
|
||||
public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
const deleteSaleReceiptOper = SaleReceipt.query()
|
||||
.where('id', saleReceiptId)
|
||||
.delete();
|
||||
|
||||
const deleteItemsEntriesOper = ItemEntry.query()
|
||||
await ItemEntry.query()
|
||||
.where('reference_id', saleReceiptId)
|
||||
.where('reference_type', 'SaleReceipt')
|
||||
.delete();
|
||||
|
||||
await SaleReceipt.query().where('id', saleReceiptId).delete();
|
||||
|
||||
// Delete all associated journal transactions to payment receive transaction.
|
||||
const deleteTransactionsOper = this.journalService.deleteJournalTransactions(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
'SaleReceipt'
|
||||
);
|
||||
await Promise.all([
|
||||
deleteItemsEntriesOper,
|
||||
deleteSaleReceiptOper,
|
||||
deleteTransactionsOper,
|
||||
]);
|
||||
|
||||
this.logger.info('[sale_receipt] deleted successfully.', { tenantId, saleReceiptId });
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given sale receipt ID exists.
|
||||
* @param {Integer} saleReceiptId
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
async isSaleReceiptExists(tenantId: number, saleReceiptId: number) {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
const foundSaleReceipt = await SaleReceipt.query()
|
||||
.where('id', saleReceiptId);
|
||||
return foundSaleReceipt.length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the sale receipt entries IDs exists.
|
||||
* @param {Integer} saleReceiptId
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
*/
|
||||
async isSaleReceiptEntriesIDsExists(tenantId: number, saleReceiptId: number, saleReceipt: any) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
const entriesIDs = saleReceipt.entries
|
||||
.filter((e) => e.id)
|
||||
.map((e) => e.id);
|
||||
|
||||
const storedEntries = await ItemEntry.query()
|
||||
.whereIn('id', entriesIDs)
|
||||
.where('reference_id', saleReceiptId)
|
||||
.where('reference_type', 'SaleReceipt');
|
||||
|
||||
const storedEntriesIDs = storedEntries.map((e: any) => e.id);
|
||||
const notFoundEntriesIDs = difference(
|
||||
entriesIDs,
|
||||
storedEntriesIDs
|
||||
);
|
||||
return notFoundEntriesIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sale receipt with associated entries.
|
||||
* @param {Integer} saleReceiptId
|
||||
|
||||
Reference in New Issue
Block a user