From da448f445cc3de81861fabb66a9a3a04d1d0a1e7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 24 Oct 2020 20:17:46 +0200 Subject: [PATCH] refactoring: sales receipts service. --- .../api/controllers/Sales/SalesReceipts.ts | 104 +++++-- server/src/api/controllers/Sales/index.ts | 6 +- server/src/interfaces/index.ts | 1 + server/src/models/SaleReceipt.js | 13 + .../src/services/Items/ItemsEntriesService.ts | 3 +- server/src/services/Sales/SalesReceipts.ts | 258 ++++++------------ 6 files changed, 174 insertions(+), 211 deletions(-) diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts index ea410320e..eb5a42258 100644 --- a/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/server/src/api/controllers/Sales/SalesReceipts.ts @@ -2,11 +2,11 @@ import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query } from 'express-validator'; import { Inject, Service } from 'typedi'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import AccountsService from 'services/Accounts/AccountsService'; -import ItemsService from 'services/Items/ItemsService'; import SaleReceiptService from 'services/Sales/SalesReceipts'; import BaseController from '../BaseController'; import { ISaleReceiptDTO } from 'interfaces/SaleReceipt'; +import { ServiceError } from 'exceptions'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class SalesReceiptsController extends BaseController{ @@ -14,10 +14,7 @@ export default class SalesReceiptsController extends BaseController{ saleReceiptService: SaleReceiptService; @Inject() - accountsService: AccountsService; - - @Inject() - itemsService: ItemsService; + dynamicListService: DynamicListingService; /** * Router constructor. @@ -31,34 +28,30 @@ export default class SalesReceiptsController extends BaseController{ ...this.salesReceiptsValidationSchema, ], this.validationResult, - asyncMiddleware(this.validateSaleReceiptExistance.bind(this)), - asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)), - asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)), - asyncMiddleware(this.validateReceiptItemsIdsExistance.bind(this)), - asyncMiddleware(this.validateReceiptEntriesIds.bind(this)), - asyncMiddleware(this.editSaleReceipt.bind(this)) + asyncMiddleware(this.editSaleReceipt.bind(this)), + this.handleServiceErrors, ); router.post( '/', this.salesReceiptsValidationSchema, this.validationResult, - asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)), - asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)), - asyncMiddleware(this.validateReceiptItemsIdsExistance.bind(this)), - asyncMiddleware(this.newSaleReceipt.bind(this)) + asyncMiddleware(this.newSaleReceipt.bind(this)), + this.handleServiceErrors, ); router.delete( '/:id', this.specificReceiptValidationSchema, this.validationResult, - asyncMiddleware(this.validateSaleReceiptExistance.bind(this)), - asyncMiddleware(this.deleteSaleReceipt.bind(this)) + asyncMiddleware(this.deleteSaleReceipt.bind(this)), + this.handleServiceErrors, ); router.get( '/', this.listSalesReceiptsValidationSchema, this.validationResult, - asyncMiddleware(this.listingSalesReceipts.bind(this)) + asyncMiddleware(this.getSalesReceipts.bind(this)), + this.handleServiceErrors, + this.dynamicListService.handlerErrorsToResponse, ); return router; } @@ -163,13 +156,13 @@ export default class SalesReceiptsController extends BaseController{ /** * Edit the sale receipt details with associated entries and re-write * journal transaction on the same date. - * @param {Request} req - * @param {Response} res + * @param {Request} req - + * @param {Response} res - */ async editSaleReceipt(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: saleReceiptId } = req.params; - const saleReceipt = { ...req.body }; + const saleReceipt = this.matchedBodyData(req); try { // Update the given sale receipt details. @@ -179,6 +172,7 @@ export default class SalesReceiptsController extends BaseController{ saleReceipt, ); return res.status(200).send({ + id: saleReceiptId, message: 'Sale receipt has been edited successfully.', }); } catch (error) { @@ -191,19 +185,79 @@ export default class SalesReceiptsController extends BaseController{ * @param {Request} req * @param {Response} res */ - async getSalesReceipts(req: Request, res: Response) { + async getSalesReceipts(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const filter = { + filterRoles: [], sortOrder: 'asc', + columnSortBy: 'created_at', page: 1, pageSize: 12, - ...this.matchedBodyData(req), + ...this.matchedQueryData(req), }; + if (filter.stringifiedFilterRoles) { + filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); + } try { - + const { salesReceipts, pagination, filterMeta } = await this.saleReceiptService + .salesReceiptsList(tenantId, filter); + + return res.status(200).send({ + sale_receipts: salesReceipts, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } } + + + handleServiceErrors(error: Error, req: Request, res: Response, next: Next) { + if (error instanceof ServiceError) { + if (error.errorType === 'SALE_RECEIPT_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'SALE_RECEIPT_NOT_FOUND', code: 100 }], + }) + } + if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'DEPOSIT_ACCOUNT_NOT_FOUND', code: 200 }], + }) + } + if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET') { + return res.boom.badRequest(null, { + errors: [{ type: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET', code: 300 }], + }) + } + if (error.errorType === 'ITEMS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEMS_NOT_FOUND', code: 400, }], + }); + } + if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 500, }], + }); + } + if (error.errorType === 'NOT_SELL_ABLE_ITEMS') { + return res.boom.badRequest(null, { + errors: [{ type: 'NOT_SELL_ABLE_ITEMS', code: 600, }], + }); + } + if (error.errorType === 'SALE.RECEIPT.NOT.FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 700 }], + }); + } + if (error.errorType === 'DEPOSIT.ACCOUNT.NOT.EXISTS') { + return res.boom.badRequest(null, { + errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 800 }], + }); + } + } + console.log(error); + next(error); + } }; diff --git a/server/src/api/controllers/Sales/index.ts b/server/src/api/controllers/Sales/index.ts index a447d0420..fe930ef3a 100644 --- a/server/src/api/controllers/Sales/index.ts +++ b/server/src/api/controllers/Sales/index.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import { Router } from 'express'; import { Container, Service } from 'typedi'; import SalesEstimates from './SalesEstimates'; import SalesReceipts from './SalesReceipts'; @@ -11,11 +11,11 @@ export default class SalesController { * Router constructor. */ router() { - const router = express.Router(); + const router = Router(); // router.use('/invoices', Container.get(SalesInvoices).router()); router.use('/estimates', Container.get(SalesEstimates).router()); - // router.use('/receipts', Container.get(SalesReceipts).router()); + router.use('/receipts', Container.get(SalesReceipts).router()); // router.use('/payment_receives', Container.get(PaymentReceives).router()); return router; diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index d54dba3ee..f5f5f970b 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -10,6 +10,7 @@ export * from './License'; export * from './ItemCategory'; export * from './Payment'; export * from './SaleInvoice'; +export * from './SaleReceipt'; export * from './PaymentReceive'; export * from './SaleEstimate'; export * from './Authentication'; diff --git a/server/src/models/SaleReceipt.js b/server/src/models/SaleReceipt.js index 4fe84b9f0..9ec70b2ec 100644 --- a/server/src/models/SaleReceipt.js +++ b/server/src/models/SaleReceipt.js @@ -72,4 +72,17 @@ export default class SaleReceipt extends TenantModel { } }; } + + /** + * Model defined fields. + */ + static get fields() { + return { + created_at: { + label: 'Created at', + column: 'created_at', + columnType: 'date', + }, + }; + } } diff --git a/server/src/services/Items/ItemsEntriesService.ts b/server/src/services/Items/ItemsEntriesService.ts index 7516a6c27..cc2cec1d2 100644 --- a/server/src/services/Items/ItemsEntriesService.ts +++ b/server/src/services/Items/ItemsEntriesService.ts @@ -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); } } } \ No newline at end of file diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index a877d6775..02040f238 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -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 { 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