From 899ea7a52d2ec86b21ee4ab094b05fe7fa2bc805 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 15 Oct 2020 15:10:41 +0200 Subject: [PATCH 1/4] refactoring: bills service. refactoring: bills payments made service. --- .../src/api/controllers/Contacts/Customers.ts | 80 ++-- .../src/api/controllers/Contacts/Vendors.ts | 84 ++-- server/src/api/controllers/Purchases/Bills.ts | 331 ++++++-------- .../controllers/Purchases/BillsPayments.ts | 365 +++++---------- server/src/api/controllers/Purchases/index.ts | 4 +- .../api/controllers/Sales/SalesEstimates.ts | 136 +----- .../api/controllers/Sales/SalesInvoices.ts | 57 +-- .../api/controllers/Sales/SalesReceipts.ts | 71 +-- server/src/api/index.ts | 4 +- ...2647_create_accounts_transactions_table.js | 2 +- .../20200719152005_create_bills_table.js | 1 + .../seeds/core/20200810121807_seed_views.js | 2 +- server/src/interfaces/Bill.ts | 47 +- server/src/interfaces/BillPayment.ts | 24 +- server/src/interfaces/ItemEntry.ts | 4 + server/src/interfaces/SaleEstimate.ts | 25 +- server/src/interfaces/SaleInvoice.ts | 5 +- server/src/interfaces/index.ts | 4 +- server/src/loaders/events.ts | 8 +- .../src/repositories/AccountTypeRepository.ts | 13 +- server/src/repositories/ContactRepository.ts | 10 +- server/src/repositories/VendorRepository.ts | 28 ++ .../src/services/Contacts/ContactsService.ts | 2 +- .../src/services/Contacts/CustomersService.ts | 89 ++-- .../src/services/Contacts/VendorsService.ts | 91 ++-- .../src/services/Expenses/ExpensesService.ts | 9 +- server/src/services/Purchases/BillPayments.ts | 428 ++++++++++++------ server/src/services/Purchases/Bills.ts | 420 +++++++++-------- .../services/Sales/JournalPosterService.ts | 21 +- server/src/services/Sales/PaymentsReceives.ts | 192 +++++++- server/src/services/Sales/SalesEstimate.ts | 155 ++++++- server/src/services/Sales/SalesInvoices.ts | 132 ++++-- server/src/services/Sales/SalesReceipts.ts | 148 +++++- server/src/subscribers/bills.ts | 83 ++++ server/src/subscribers/customers.ts | 46 ++ server/src/subscribers/events.ts | 88 +++- server/src/subscribers/paymentMades.ts | 108 +++++ server/src/subscribers/saleInvoices.ts | 22 + server/src/subscribers/vendors.ts | 46 ++ 39 files changed, 2192 insertions(+), 1193 deletions(-) create mode 100644 server/src/subscribers/bills.ts create mode 100644 server/src/subscribers/customers.ts create mode 100644 server/src/subscribers/paymentMades.ts create mode 100644 server/src/subscribers/saleInvoices.ts create mode 100644 server/src/subscribers/vendors.ts diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index 0ee03b2bc..33e16079e 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -24,7 +24,8 @@ export default class CustomersController extends ContactsController { ...this.customerDTOSchema, ], this.validationResult, - asyncMiddleware(this.newCustomer.bind(this)) + asyncMiddleware(this.newCustomer.bind(this)), + this.handlerServiceErrors ); router.post('/:id', [ ...this.contactDTOSchema, @@ -32,19 +33,22 @@ export default class CustomersController extends ContactsController { ...this.customerDTOSchema, ], this.validationResult, - asyncMiddleware(this.editCustomer.bind(this)) + asyncMiddleware(this.editCustomer.bind(this)), + this.handlerServiceErrors, ); router.delete('/:id', [ ...this.specificContactSchema, ], this.validationResult, - asyncMiddleware(this.deleteCustomer.bind(this)) + asyncMiddleware(this.deleteCustomer.bind(this)), + this.handlerServiceErrors, ); router.delete('/', [ ...this.bulkContactsSchema, ], this.validationResult, - asyncMiddleware(this.deleteBulkCustomers.bind(this)) + asyncMiddleware(this.deleteBulkCustomers.bind(this)), + this.handlerServiceErrors, ); router.get('/', [ @@ -56,7 +60,8 @@ export default class CustomersController extends ContactsController { ...this.specificContactSchema, ], this.validationResult, - asyncMiddleware(this.getCustomer.bind(this)) + asyncMiddleware(this.getCustomer.bind(this)), + this.handlerServiceErrors ); return router; } @@ -104,13 +109,6 @@ export default class CustomersController extends ContactsController { await this.customersService.editCustomer(tenantId, contactId, contactDTO); return res.status(200).send({ id: contactId }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contact_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }], - }); - } - } next(error); } } @@ -129,18 +127,6 @@ export default class CustomersController extends ContactsController { await this.customersService.deleteCustomer(tenantId, contactId) return res.status(200).send({ id: contactId }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contact_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }], - }); - } - if (error.errorType === 'customer_has_invoices') { - return res.boom.badRequest(null, { - errors: [{ type: 'CUSTOMER.HAS.SALES_INVOICES', code: 200 }], - }); - } - } next(error); } } @@ -159,13 +145,6 @@ export default class CustomersController extends ContactsController { const contact = await this.customersService.getCustomer(tenantId, contactId) return res.status(200).send({ contact }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contact_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'CONTACT.NOT.FOUND', code: 100 }], - }); - } - } next(error); } } @@ -184,18 +163,6 @@ export default class CustomersController extends ContactsController { await this.customersService.deleteBulkCustomers(tenantId, contactsIds) return res.status(200).send({ ids: contactsIds }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contacts_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 100 }], - }); - } - if (error.errorType === 'some_customers_have_invoices') { - return res.boom.badRequest(null, { - errors: [{ type: 'SOME.CUSTOMERS.HAVE.SALES_INVOICES', code: 200 }], - }); - } - } next(error); } } @@ -210,4 +177,31 @@ export default class CustomersController extends ContactsController { next(error); } } + + /** + * Handles service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'contacts_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'some_customers_have_invoices') { + return res.boom.badRequest(null, { + errors: [{ type: 'SOME.CUSTOMERS.HAVE.SALES_INVOICES', code: 200 }], + }); + } + if (error.errorType === 'customer_has_invoices') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMER.HAS.SALES_INVOICES', code: 200 }], + }); + } + } + } } \ No newline at end of file diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index 95f848fdd..a6b3b6786 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -24,7 +24,8 @@ export default class VendorsController extends ContactsController { ...this.vendorDTOSchema, ], this.validationResult, - asyncMiddleware(this.newVendor.bind(this)) + asyncMiddleware(this.newVendor.bind(this)), + this.handlerServiceErrors, ); router.post('/:id', [ ...this.contactDTOSchema, @@ -32,25 +33,29 @@ export default class VendorsController extends ContactsController { ...this.vendorDTOSchema, ], this.validationResult, - asyncMiddleware(this.editVendor.bind(this)) + asyncMiddleware(this.editVendor.bind(this)), + this.handlerServiceErrors, ); router.delete('/:id', [ ...this.specificContactSchema, ], this.validationResult, - asyncMiddleware(this.deleteVendor.bind(this)) + asyncMiddleware(this.deleteVendor.bind(this)), + this.handlerServiceErrors, ); router.delete('/', [ ...this.bulkContactsSchema, ], this.validationResult, - asyncMiddleware(this.deleteBulkVendors.bind(this)) + asyncMiddleware(this.deleteBulkVendors.bind(this)), + this.handlerServiceErrors, ); router.get('/:id', [ ...this.specificContactSchema, ], this.validationResult, - asyncMiddleware(this.getVendor.bind(this)) + asyncMiddleware(this.getVendor.bind(this)), + this.handlerServiceErrors, ); router.get('/', [ ...this.vendorsListSchema, @@ -99,8 +104,8 @@ export default class VendorsController extends ContactsController { const { tenantId } = req; try { - const contact = await this.vendorsService.newVendor(tenantId, contactDTO); - return res.status(200).send({ id: contact.id }); + const vendor = await this.vendorsService.newVendor(tenantId, contactDTO); + return res.status(200).send({ id: vendor.id }); } catch (error) { next(error); } @@ -121,13 +126,6 @@ export default class VendorsController extends ContactsController { await this.vendorsService.editVendor(tenantId, contactId, contactDTO); return res.status(200).send({ id: contactId }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contact_not_found') { - return res.status(400).send({ - errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }], - }); - } - } next(error); } } @@ -146,18 +144,6 @@ export default class VendorsController extends ContactsController { await this.vendorsService.deleteVendor(tenantId, contactId) return res.status(200).send({ id: contactId }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contact_not_found') { - return res.status(400).send({ - errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }], - }); - } - if (error.errorType === 'vendor_has_bills') { - return res.status(400).send({ - errors: [{ type: 'VENDOR.HAS.BILLS', code: 200 }], - }); - } - } next(error); } } @@ -176,13 +162,6 @@ export default class VendorsController extends ContactsController { const vendor = await this.vendorsService.getVendor(tenantId, vendorId) return res.status(200).send({ vendor }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contact_not_found') { - return res.status(400).send({ - errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }], - }); - } - } next(error); } } @@ -201,18 +180,6 @@ export default class VendorsController extends ContactsController { await this.vendorsService.deleteBulkVendors(tenantId, contactsIds) return res.status(200).send({ ids: contactsIds }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'contacts_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'VENDORS.NOT.FOUND', code: 100 }], - }); - } - if (error.errorType === 'some_vendors_have_bills') { - return res.boom.badRequest(null, { - errors: [{ type: 'SOME.VENDORS.HAVE.BILLS', code: 200 }], - }); - } - } next(error); } } @@ -236,4 +203,31 @@ export default class VendorsController extends ContactsController { next(error); } } + + /** + * Handle service errors. + * @param {Error} error - + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + handlerServiceErrors(error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'contacts_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'VENDORS.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'some_vendors_have_bills') { + return res.boom.badRequest(null, { + errors: [{ type: 'SOME.VENDORS.HAVE.BILLS', code: 200 }], + }); + } + if (error.errorType === 'vendor_has_bills') { + return res.status(400).send({ + errors: [{ type: 'VENDOR.HAS.BILLS', code: 200 }], + }); + } + } + } } \ No newline at end of file diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 83168506e..f7f0324b7 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -1,13 +1,13 @@ -import { Router, Request, Response } from 'express'; -import { check, param, query, matchedData } from 'express-validator'; +import { Router, Request, Response, NextFunction } from 'express'; +import { check, param, query } from 'express-validator'; import { Service, Inject } from 'typedi'; -import { difference } from 'lodash'; -import { BillOTD } from 'interfaces'; +import { IBillDTO, IBillEditDTO } from 'interfaces'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import BillsService from 'services/Purchases/Bills'; import BaseController from 'api/controllers/BaseController'; import ItemsService from 'services/Items/ItemsService'; import TenancyService from 'services/Tenancy/TenancyService'; +import { ServiceError } from 'exceptions'; @Service() export default class BillsController extends BaseController { @@ -27,45 +27,49 @@ export default class BillsController extends BaseController { const router = Router(); router.post( - '/', - [...this.billValidationSchema], + '/', [ + ...this.billValidationSchema + ], this.validationResult, - asyncMiddleware(this.validateVendorExistance.bind(this)), - asyncMiddleware(this.validateItemsIds.bind(this)), - asyncMiddleware(this.validateBillNumberExists.bind(this)), - asyncMiddleware(this.validateNonPurchasableEntriesItems.bind(this)), - asyncMiddleware(this.newBill.bind(this)) + asyncMiddleware(this.newBill.bind(this)), + this.handleServiceError, ); router.post( - '/:id', - [...this.billValidationSchema, ...this.specificBillValidationSchema], + '/:id', [ + ...this.billValidationSchema, + ...this.specificBillValidationSchema, + ], this.validationResult, - asyncMiddleware(this.validateBillExistance.bind(this)), - asyncMiddleware(this.validateVendorExistance.bind(this)), - asyncMiddleware(this.validateItemsIds.bind(this)), - asyncMiddleware(this.validateEntriesIdsExistance.bind(this)), - asyncMiddleware(this.validateNonPurchasableEntriesItems.bind(this)), asyncMiddleware(this.editBill.bind(this)) ); router.get( - '/:id', - [...this.specificBillValidationSchema], + '/:id', [ + ...this.specificBillValidationSchema + ], this.validationResult, - asyncMiddleware(this.validateBillExistance.bind(this)), - asyncMiddleware(this.getBill.bind(this)) - ); - router.get( - '/', - [...this.billsListingValidationSchema], - this.validationResult, - asyncMiddleware(this.listingBills.bind(this)) + asyncMiddleware(this.getBill.bind(this)), ); + + // router.get( + // '/:id', + // [...this.specificBillValidationSchema], + // this.validationResult, + // asyncMiddleware(this.getBill.bind(this)), + // this.handleServiceError, + // ); + // router.get( + // '/', + // [...this.billsListingValidationSchema], + // this.validationResult, + // asyncMiddleware(this.listingBills.bind(this)), + // this.handleServiceError, + // ); router.delete( '/:id', [...this.specificBillValidationSchema], this.validationResult, - asyncMiddleware(this.validateBillExistance.bind(this)), - asyncMiddleware(this.deleteBill.bind(this)) + asyncMiddleware(this.deleteBill.bind(this)), + this.handleServiceError, ); return router; } @@ -92,6 +96,28 @@ export default class BillsController extends BaseController { ]; } + /** + * Common validation schema. + */ + get billEditValidationSchema() { + return [ + // check('bill_number').exists().trim().escape(), + check('bill_date').exists().isISO8601(), + check('due_date').optional().isISO8601(), + // check('vendor_id').exists().isNumeric().toInt(), + check('note').optional().trim().escape(), + check('entries').isArray({ min: 1 }), + + check('entries.*.id').optional().isNumeric().toInt(), + check('entries.*.index').exists().isNumeric().toInt(), + check('entries.*.item_id').exists().isNumeric().toInt(), + check('entries.*.rate').exists().isNumeric().toFloat(), + check('entries.*.quantity').exists().isNumeric().toFloat(), + check('entries.*.discount').optional().isNumeric().toFloat(), + check('entries.*.description').optional().trim().escape(), + ]; + } + /** * Bill validation schema. */ @@ -112,162 +138,23 @@ export default class BillsController extends BaseController { query('sort_order').optional().isIn(['desc', 'asc']), ]; } - - /** - * Validates whether the vendor is exist. - * @async - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateVendorExistance(req: Request, res: Response, next: Function) { - const { tenantId } = req; - const { Vendor } = req.models; - - const isVendorExists = await Vendor.query().findById(req.body.vendor_id); - - if (!isVendorExists) { - return res.status(400).send({ - errors: [{ type: 'VENDOR.ID.NOT.FOUND', code: 300 }], - }); - } - next(); - } - - /** - * Validates the given bill existance. - * @async - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateBillExistance(req: Request, res: Response, next: Function) { - const billId: number = req.params.id; - const { tenantId } = req; - - const isBillExists = await this.billsService.isBillExists(tenantId, billId); - - if (!isBillExists) { - return res.status(400).send({ - errors: [{ type: 'BILL.NOT.FOUND', code: 200 }], - }); - } - next(); - } - - /** - * Validates the entries items ids. - * @async - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateItemsIds(req: Request, res: Response, next: Function) { - const { tenantId } = req; - const itemsIds = req.body.entries.map((e) => e.item_id); - - const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, itemsIds); - - if (notFoundItemsIds.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }], - }); - } - next(); - } - - /** - * Validates the bill number existance. - * @async - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateBillNumberExists(req: Request, res: Response, next: Function) { - const { tenantId } = req; - - const isBillNoExists = await this.billsService.isBillNoExists( - tenantId, req.body.bill_number, - ); - if (isBillNoExists) { - return res.status(400).send({ - errors: [{ type: 'BILL.NUMBER.EXISTS', code: 500 }], - }); - } - next(); - } - - /** - * Validates the entries ids existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEntriesIdsExistance(req: Request, res: Response, next: Function) { - const { id: billId } = req.params; - const bill = { ...req.body }; - const { ItemEntry } = req.models; - - const entriesIds = bill.entries.filter((e) => e.id).map((e) => e.id); - - const storedEntries = await ItemEntry.query() - .whereIn('reference_id', [billId]) - .whereIn('reference_type', ['Bill']); - - const storedEntriesIds = storedEntries.map((entry) => entry.id); - const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); - - if (notFoundEntriesIds.length > 0) { - return res.status(400).send({ - errors: [{ type: 'BILL.ENTRIES.IDS.NOT.FOUND', code: 600 }], - }); - } - next(); - } - - /** - * Validate the entries items that not purchase-able. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateNonPurchasableEntriesItems(req: Request, res: Response, next: Function) { - const { Item } = req.models; - const bill = { ...req.body }; - const itemsIds = bill.entries.map(e => e.item_id); - - const purchasbleItems = await Item.query() - .where('purchasable', true) - .whereIn('id', itemsIds); - - const purchasbleItemsIds = purchasbleItems.map((item) => item.id); - const notPurchasableItems = difference(itemsIds, purchasbleItemsIds); - - if (notPurchasableItems.length > 0) { - return res.status(400).send({ - errors: [{ type: 'NOT.PURCHASE.ABLE.ITEMS', code: 600 }], - }); - } - next(); - } - + /** * Creates a new bill and records journal transactions. * @param {Request} req * @param {Response} res * @param {Function} next */ - async newBill(req: Request, res: Response, next: Function) { - const { tenantId } = req; - const { ItemEntry } = req.models; + async newBill(req: Request, res: Response, next: NextFunction) { + const { tenantId, user } = req; + const billDTO: IBillDTO = this.matchedBodyData(req); - const billOTD: BillOTD = matchedData(req, { - locations: ['body'], - includeOptionals: true - }); - const storedBill = await this.billsService.createBill(tenantId, billOTD); - - return res.status(200).send({ id: storedBill.id }); + try { + const storedBill = await this.billsService.createBill(tenantId, billDTO, user); + return res.status(200).send({ id: storedBill.id }); + } catch (error) { + next(error); + } } /** @@ -275,18 +162,17 @@ export default class BillsController extends BaseController { * @param {Request} req * @param {Response} res */ - async editBill(req: Request, res: Response) { + async editBill(req: Request, res: Response, next: NextFunction) { const { id: billId } = req.params; - const { ItemEntry } = req.models; const { tenantId } = req; + const billDTO: IBillEditDTO = this.matchedBodyData(req); - const billOTD: BillOTD = matchedData(req, { - locations: ['body'], - includeOptionals: true - }); - const editedBill = await this.billsService.editBill(tenantId, billId, billOTD); - - return res.status(200).send({ id: billId }); + try { + const editedBill = await this.billsService.editBill(tenantId, billId, billDTO); + return res.status(200).send({ id: billId }); + } catch (error) { + next(error); + } } /** @@ -295,13 +181,17 @@ export default class BillsController extends BaseController { * @param {Response} res * @return {Response} */ - async getBill(req: Request, res: Response) { + async getBill(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: billId } = req.params; - const bill = await this.billsService.getBillWithMetadata(tenantId, billId); + try { + const bill = await this.billsService.getBillWithMetadata(tenantId, billId); - return res.status(200).send({ bill }); + return res.status(200).send({ bill }); + } catch (error) { + next(error); + } } /** @@ -310,13 +200,19 @@ export default class BillsController extends BaseController { * @param {Response} res - * @return {Response} */ - async deleteBill(req: Request, res: Response) { + async deleteBill(req: Request, res: Response, next: NextFunction) { const billId = req.params.id; const { tenantId } = req; - await this.billsService.deleteBill(tenantId, billId); - - return res.status(200).send({ id: billId }); + try { + await this.billsService.deleteBill(tenantId, billId); + return res.status(200).send({ + id: billId, + message: 'The given bill deleted successfully.', + }); + } catch (error) { + next(error); + } } /** @@ -325,7 +221,50 @@ export default class BillsController extends BaseController { * @param {Response} res - * @return {Response} */ - async listingBills(req: Request, res: Response) { + async billsList(req: Request, res: Response) { + + } + /** + * Handles service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'BILL_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'BILL_NOT_FOUND', code: 100 }], + }); + } + if (error.errorType === 'BILL_NUMBER_EXISTS') { + return res.status(400).send({ + errors: [{ type: 'BILL.NUMBER.EXISTS', code: 500 }], + }); + } + if (error.errorType === 'BILL_VENDOR_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'BILL_VENDOR_NOT_FOUND', code: 600 }], + }); + } + if (error.errorType === 'BILL_ITEMS_NOT_PURCHASABLE') { + return res.status(400).send({ + errors: [{ type: 'BILL_ITEMS_NOT_PURCHASABLE', code: 700 }] + }) + } + if (error.errorType === 'NOT_PURCHASE_ABLE_ITEMS') { + return res.status(400).send({ + errors: [{ type: 'NOT_PURCHASE_ABLE_ITEMS', code: 800 }], + }); + } + if (error.errorType === 'BILL_ITEMS_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }], + }); + } + } + next(error); } } diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index 1b72a99d6..d6782af98 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -1,10 +1,9 @@ -import { Router, Request, Response } from 'express'; +import { Router, Request, Response, NextFunction } from 'express'; import { Service, Inject } from 'typedi'; -import { check, param, query, ValidationChain, matchedData } from 'express-validator'; -import { difference } from 'lodash'; +import { check, param, query, ValidationChain } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import validateMiddleware from 'api/middleware/validateMiddleware'; +import { ServiceError } from 'exceptions'; import BaseController from 'api/controllers/BaseController'; import BillPaymentsService from 'services/Purchases/BillPayments'; import AccountsService from 'services/Accounts/AccountsService'; @@ -31,43 +30,34 @@ export default class BillsPayments extends BaseController { ...this.billPaymentSchemaValidation, ], this.validationResult, - asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)), - asyncMiddleware(this.validatePaymentAccount.bind(this)), - asyncMiddleware(this.validatePaymentNumber.bind(this)), - asyncMiddleware(this.validateEntriesBillsExistance.bind(this)), - asyncMiddleware(this.validateVendorsDueAmount.bind(this)), asyncMiddleware(this.createBillPayment.bind(this)), + this.handleServiceError, ); router.post('/:id', [ ...this.billPaymentSchemaValidation, ...this.specificBillPaymentValidateSchema, ], this.validationResult, - asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)), - asyncMiddleware(this.validatePaymentAccount.bind(this)), - asyncMiddleware(this.validatePaymentNumber.bind(this)), - asyncMiddleware(this.validateEntriesIdsExistance.bind(this)), - asyncMiddleware(this.validateEntriesBillsExistance.bind(this)), - asyncMiddleware(this.validateVendorsDueAmount.bind(this)), asyncMiddleware(this.editBillPayment.bind(this)), + this.handleServiceError, ) - router.delete('/:id', - this.specificBillPaymentValidateSchema, + router.delete('/:id', [ + ...this.specificBillPaymentValidateSchema, + ], this.validationResult, - asyncMiddleware(this.validateBillPaymentExistance.bind(this)), asyncMiddleware(this.deleteBillPayment.bind(this)), + this.handleServiceError, ); - router.get('/:id', - this.specificBillPaymentValidateSchema, - this.validationResult, - asyncMiddleware(this.validateBillPaymentExistance.bind(this)), - asyncMiddleware(this.getBillPayment.bind(this)), - ); - router.get('/', - this.listingValidationSchema, - this.validationResult, - asyncMiddleware(this.getBillsPayments.bind(this)) - ); + // router.get('/:id', + // this.specificBillPaymentValidateSchema, + // this.validationResult, + // asyncMiddleware(this.getBillPayment.bind(this)), + // ); + // router.get('/', + // this.listingValidationSchema, + // this.validationResult, + // asyncMiddleware(this.getBillsPayments.bind(this)) + // ); return router; } @@ -112,186 +102,6 @@ export default class BillsPayments extends BaseController { ]; } - /** - * Validate whether the bill payment vendor exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateBillPaymentVendorExistance(req: Request, res: Response, next: any ) { - const billPayment = req.body; - const { Vendor } = req.models; - const isVendorExists = await Vendor.query().findById(billPayment.vendor_id); - - if (!isVendorExists) { - return res.status(400).send({ - errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }], - }); - } - next(); - } - - /** - * Validates the bill payment existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateBillPaymentExistance(req: Request, res: Response, next: any ) { - const { id: billPaymentId } = req.params; - const { BillPayment } = req.models; - const foundBillPayment = await BillPayment.query().findById(billPaymentId); - - if (!foundBillPayment) { - return res.status(404).send({ - errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }], - }); - } - next(); - } - - /** - * Validates the payment account. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validatePaymentAccount(req: Request, res: Response, next: any) { - const { tenantId } = req; - const billPayment = { ...req.body }; - - const isAccountExists = await this.accountsService.isAccountExists( - tenantId, - billPayment.payment_account_id - ); - - if (!isAccountExists) { - return res.status(400).send({ - errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }], - }); - } - next(); - } - - /** - * Validates the payment number uniqness. - * @param {Request} req - * @param {Response} res - * @param {Function} res - */ - async validatePaymentNumber(req: Request, res: Response, next: any) { - const billPayment = { ...req.body }; - const { id: billPaymentId } = req.params; - const { BillPayment } = req.models; - - const foundBillPayment = await BillPayment.query() - .onBuild((builder: any) => { - builder.where('payment_number', billPayment.payment_number) - if (billPaymentId) { - builder.whereNot('id', billPaymentId); - } - }) - .first(); - - if (foundBillPayment) { - return res.status(400).send({ - errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }], - }); - } - next(); - } - - /** - * Validate whether the entries bills ids exist on the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async validateEntriesBillsExistance(req: Request, res: Response, next: any) { - const { Bill } = req.models; - const billPayment = { ...req.body }; - const entriesBillsIds = billPayment.entries.map((e: any) => e.bill_id); - - // Retrieve not found bills that associated to the given vendor id. - const notFoundBillsIds = await Bill.getNotFoundBills( - entriesBillsIds, - billPayment.vendor_id, - ); - if (notFoundBillsIds.length > 0) { - return res.status(400).send({ - errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 600 }], - }); - } - next(); - } - - /** - * Validate wether the payment amount bigger than the payable amount. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - * @return {void} - */ - async validateVendorsDueAmount(req: Request, res: Response, next: Function) { - const { Bill } = req.models; - const billsIds = req.body.entries.map((entry: any) => entry.bill_id); - const storedBills = await Bill.query() - .whereIn('id', billsIds); - - const storedBillsMap = new Map( - storedBills.map((bill: any) => [bill.id, bill]), - ); - interface invalidPaymentAmountError{ - index: number, - due_amount: number - }; - const hasWrongPaymentAmount: invalidPaymentAmountError[] = []; - const { entries } = req.body; - - entries.forEach((entry: any, index: number) => { - const entryBill = storedBillsMap.get(entry.bill_id); - const { dueAmount } = entryBill; - - if (dueAmount < entry.payment_amount) { - hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); - } - }); - if (hasWrongPaymentAmount.length > 0) { - return res.status(400).send({ - errors: [{ type: 'INVALID.BILL.PAYMENT.AMOUNT', code: 400, indexes: hasWrongPaymentAmount }] - }); - } - next(); - } - - /** - * Validate the payment receive entries IDs existance. - * @param {Request} req - * @param {Response} res - * @return {Response} - */ - async validateEntriesIdsExistance(req: Request, res: Response, next: Function) { - const { BillPaymentEntry } = req.models; - - const billPayment = { id: req.params.id, ...req.body }; - const entriesIds = billPayment.entries - .filter((entry: any) => entry.id) - .map((entry: any) => entry.id); - - const storedEntries = await BillPaymentEntry.query() - .where('bill_payment_id', billPayment.id); - - const storedEntriesIds = storedEntries.map((entry: any) => entry.id); - const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); - - if (notFoundEntriesIds.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }], - }); - } - next(); - } - /** * Creates a bill payment. * @async @@ -299,17 +109,21 @@ export default class BillsPayments extends BaseController { * @param {Response} res * @param {Response} res */ - async createBillPayment(req: Request, res: Response) { + async createBillPayment(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const billPaymentDTO = this.matchedBodyData(req); - const billPayment = matchedData(req, { - locations: ['body'], - includeOptionals: true, - }); - const storedPayment = await this.billPaymentService - .createBillPayment(tenantId, billPayment); + try { + const billPayment = await this.billPaymentService.createBillPayment(tenantId, billPaymentDTO); - return res.status(200).send({ id: storedPayment.id }); + return res.status(200).send({ + id: billPayment.id, + message: 'Payment made has been created successfully.', + }); + } catch (error) { + console.log(error); + next(error); + } } /** @@ -317,28 +131,24 @@ export default class BillsPayments extends BaseController { * @param {Request} req * @param {Response} res */ - async editBillPayment(req: Request, res: Response) { + async editBillPayment(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - - const billPayment = matchedData(req, { - locations: ['body'], - includeOptionals: true, - }); + const billPaymentDTO = this.matchedBodyData(req); const { id: billPaymentId } = req.params; - const { BillPayment } = req.models; - const oldBillPayment = await BillPayment.query() - .where('id', billPaymentId) - .withGraphFetched('entries') - .first(); - - await this.billPaymentService.editBillPayment( - tenantId, - billPaymentId, - billPayment, - oldBillPayment, - ); - return res.status(200).send({ id: 1 }); + try { + const paymentMade = await this.billPaymentService.editBillPayment( + tenantId, + billPaymentId, + billPaymentDTO + ); + return res.status(200).send({ + id: paymentMade.id, + message: 'Payment made has been edited successfully.', + }); + } catch (error) { + next(error); + } } /** @@ -348,15 +158,20 @@ export default class BillsPayments extends BaseController { * @param {Response} res - * @return {Response} res - */ - async deleteBillPayment(req: Request, res: Response) { + async deleteBillPayment(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const { id: billPaymentId } = req.params; - const billPayment = req.body; - await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId); + try { + await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId); - return res.status(200).send({ id: billPaymentId }); + return res.status(200).send({ + id: billPaymentId, + message: 'Payment made has been deleted successfully.', + }); + } catch (error) { + next(error); + } } /** @@ -364,7 +179,7 @@ export default class BillsPayments extends BaseController { * @param {Request} req * @param {Response} res */ - async getBillPayment(req: Request, res: Response) { + async getBillPayment(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: billPaymentId } = req.params; @@ -380,7 +195,7 @@ export default class BillsPayments extends BaseController { * @param {Response} res - * @return {Response} */ - async getBillsPayments(req: Request, res: Response) { + async getBillsPayments(req: Request, res: Response, next: NextFunction) { const { tenantId } = req.params; const billPaymentsFilter = this.matchedQueryData(req); @@ -397,4 +212,68 @@ export default class BillsPayments extends BaseController { next(error); } } + + /** + * Handle service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'PAYMENT_MADE_NOT_FOUND') { + return res.status(404).send({ + message: 'Payment made not found.', + }); + } + if (error.errorType === 'VENDOR_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }], + }); + } + if (error.errorType === 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') { + return res.status(400).send({ + errors: [{ type: 'PAYMENT_ACCOUNT.NOT.CURRENT_ASSET.TYPE', code: 100 }], + }); + } + if (error.errorType === 'BILL_PAYMENT_NUMBER_NOT_UNQIUE') { + return res.status(400).send({ + errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }], + }); + } + if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }], + }); + } + if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }], + }); + } + if (error.errorType === '') { + return res.status(400).send({ + errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 600 }], + }); + } + if (error.errorType === 'BILL_PAYMENT_ENTRIES_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }], + }); + } + if (error.errorType === 'INVALID_BILL_PAYMENT_AMOUNT') { + return res.status(400).send({ + errors: [{ type: 'INVALID_BILL_PAYMENT_AMOUNT', code: 100 }], + }); + } + if (error.errorType === 'BILL_ENTRIES_IDS_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'BILLS_NOT_FOUND', code: 100 }], + }) + } + } + console.log(error); + next(error); + } } \ No newline at end of file diff --git a/server/src/api/controllers/Purchases/index.ts b/server/src/api/controllers/Purchases/index.ts index 622c575e7..a56ac1261 100644 --- a/server/src/api/controllers/Purchases/index.ts +++ b/server/src/api/controllers/Purchases/index.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import { Router } from 'express'; import { Container, Service } from 'typedi'; import Bills from 'api/controllers/Purchases/Bills' import BillPayments from 'api/controllers/Purchases/BillsPayments'; @@ -7,7 +7,7 @@ import BillPayments from 'api/controllers/Purchases/BillsPayments'; export default class PurchasesController { router() { - const router = express.Router(); + const router = Router(); router.use('/bills', Container.get(Bills).router()); router.use('/bill_payments', Container.get(BillPayments).router()); diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index dd5f589ec..8f512fa09 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response } from 'express'; +import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, matchedData } from 'express-validator'; import { Inject, Service } from 'typedi'; import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces'; @@ -25,9 +25,9 @@ export default class SalesEstimatesController extends BaseController { '/', this.estimateValidationSchema, this.validationResult, - asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), - asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), - asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), + // asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), + // asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), + // asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), asyncMiddleware(this.newEstimate.bind(this)) ); router.post( @@ -36,11 +36,11 @@ export default class SalesEstimatesController extends BaseController { ...this.estimateValidationSchema, ], this.validationResult, - asyncMiddleware(this.validateEstimateIdExistance.bind(this)), - asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), - asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), - asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), - asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)), + // asyncMiddleware(this.validateEstimateIdExistance.bind(this)), + // asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), + // asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), + // asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), + // asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)), asyncMiddleware(this.editEstimate.bind(this)) ); router.delete( @@ -48,14 +48,14 @@ export default class SalesEstimatesController extends BaseController { this.validateSpecificEstimateSchema, ], this.validationResult, - asyncMiddleware(this.validateEstimateIdExistance.bind(this)), + // asyncMiddleware(this.validateEstimateIdExistance.bind(this)), asyncMiddleware(this.deleteEstimate.bind(this)) ); router.get( '/:id', this.validateSpecificEstimateSchema, this.validationResult, - asyncMiddleware(this.validateEstimateIdExistance.bind(this)), + // asyncMiddleware(this.validateEstimateIdExistance.bind(this)), asyncMiddleware(this.getEstimate.bind(this)) ); router.get( @@ -114,120 +114,6 @@ export default class SalesEstimatesController extends BaseController { ] } - /** - * Validate whether the estimate customer exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) { - const estimate = { ...req.body }; - const { Customer } = req.models - - const foundCustomer = await Customer.query().findById(estimate.customer_id); - - if (!foundCustomer) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }], - }); - } - next(); - } - - /** - * Validate the estimate number unique on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEstimateNumberExistance(req: Request, res: Response, next: Function) { - const estimate = { ...req.body }; - const { tenantId } = req; - - const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique( - tenantId, - estimate.estimate_number, - req.params.id, - ); - if (isEstNumberUnqiue) { - return res.boom.badRequest(null, { - errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }], - }); - } - next(); - } - - /** - * Validate the estimate entries items ids existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) { - const tenantId = req.tenantId; - const estimate = { ...req.body }; - const estimateItemsIds = estimate.entries.map(e => e.item_id); - - // Validate items ids in estimate entries exists. - const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds); - - if (notFoundItemsIds.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }], - }); - } - next(); - } - - /** - * Validate whether the sale estimate id exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEstimateIdExistance(req: Request, res: Response, next: Function) { - const { id: estimateId } = req.params; - const { tenantId } = req; - - const storedEstimate = await this.saleEstimateService - .getEstimate(tenantId, estimateId); - - if (!storedEstimate) { - return res.status(404).send({ - errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }], - }); - } - next(); - } - - /** - * Validate sale invoice entries ids existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) { - const { ItemEntry } = req.models; - - const { id: saleInvoiceId } = req.params; - const saleInvoice = { ...req.body }; - const entriesIds = saleInvoice.entries - .filter(e => e.id) - .map((e) => e.id); - - const foundEntries = await ItemEntry.query() - .whereIn('id', entriesIds) - .where('reference_type', 'SaleInvoice') - .where('reference_id', saleInvoiceId); - - if (foundEntries.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }], - }); - } - next(); - } - /** * Handle create a new estimate with associated entries. * @param {Request} req - diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index db4ce0a16..c6141b69f 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -32,7 +32,7 @@ export default class SaleInvoicesController extends BaseController{ this.saleInvoiceValidationSchema, this.validationResult, asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), - asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), + // asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)), asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)), asyncMiddleware(this.newSaleInvoice.bind(this)) @@ -46,7 +46,7 @@ export default class SaleInvoicesController extends BaseController{ this.validationResult, asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), - asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), + // asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)), asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)), asyncMiddleware(this.validateEntriesIdsExistance.bind(this)), @@ -312,18 +312,19 @@ export default class SaleInvoicesController extends BaseController{ * @param {Response} res * @param {Function} next */ - async newSaleInvoice(req: Request, res: Response) { + async newSaleInvoice(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, { - locations: ['body'], - includeOptionals: true - }); + const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req); - // Creates a new sale invoice with associated entries. - const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice( - tenantId, saleInvoiceOTD, - ); - return res.status(200).send({ id: storedSaleInvoice.id }); + try { + // Creates a new sale invoice with associated entries. + const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice( + tenantId, saleInvoiceOTD, + ); + return res.status(200).send({ id: storedSaleInvoice.id }); + } catch (error) { + next(error) + } } /** @@ -332,18 +333,18 @@ export default class SaleInvoicesController extends BaseController{ * @param {Response} res * @param {Function} next */ - async editSaleInvoice(req: Request, res: Response) { + async editSaleInvoice(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: saleInvoiceId } = req.params; + const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req); - const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, { - locations: ['body'], - includeOptionals: true - }); - // Update the given sale invoice details. - await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD); - - return res.status(200).send({ id: saleInvoiceId }); + try { + // Update the given sale invoice details. + await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD); + return res.status(200).send({ id: saleInvoiceId }); + } catch (error) { + next(error); + } } /** @@ -352,14 +353,18 @@ export default class SaleInvoicesController extends BaseController{ * @param {Response} res * @param {Function} next */ - async deleteSaleInvoice(req: Request, res: Response) { + async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) { const { id: saleInvoiceId } = req.params; const { tenantId } = req; - // Deletes the sale invoice with associated entries and journal transaction. - await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId); - - return res.status(200).send({ id: saleInvoiceId }); + try { + // Deletes the sale invoice with associated entries and journal transaction. + await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId); + + return res.status(200).send({ id: saleInvoiceId }); + } catch (error) { + next(error); + } } /** diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts index 53823def1..8fca3aaaa 100644 --- a/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/server/src/api/controllers/Sales/SalesReceipts.ts @@ -1,11 +1,12 @@ -import { Router, Request, Response } from 'express'; -import { check, param, query, matchedData } from 'express-validator'; +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'; @Service() export default class SalesReceiptsController extends BaseController{ @@ -232,20 +233,21 @@ export default class SalesReceiptsController extends BaseController{ * @param {Request} req * @param {Response} res */ - async newSaleReceipt(req: Request, res: Response) { + async newSaleReceipt(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req); - const saleReceipt = matchedData(req, { - locations: ['body'], - includeOptionals: true, - }); - // Store the given sale receipt details with associated entries. - const storedSaleReceipt = await this.saleReceiptService - .createSaleReceipt( - tenantId, - saleReceipt, - ); - return res.status(200).send({ id: storedSaleReceipt.id }); + try { + // Store the given sale receipt details with associated entries. + const storedSaleReceipt = await this.saleReceiptService + .createSaleReceipt( + tenantId, + saleReceiptDTO, + ); + return res.status(200).send({ id: storedSaleReceipt.id }); + } catch (error) { + next(error); + } } /** @@ -253,14 +255,18 @@ export default class SalesReceiptsController extends BaseController{ * @param {Request} req * @param {Response} res */ - async deleteSaleReceipt(req: Request, res: Response) { + async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: saleReceiptId } = req.params; - // Deletes the sale receipt. - await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId); - - return res.status(200).send({ id: saleReceiptId }); + try { + // Deletes the sale receipt. + await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId); + + return res.status(200).send({ id: saleReceiptId }); + } catch (error) { + next(error); + } } /** @@ -269,25 +275,22 @@ export default class SalesReceiptsController extends BaseController{ * @param {Request} req * @param {Response} res */ - async editSaleReceipt(req: Request, res: Response) { + async editSaleReceipt(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const { id: saleReceiptId } = req.params; const saleReceipt = { ...req.body }; - const errorReasons = []; - - // Handle all errors with reasons messages. - if (errorReasons.length > 0) { - return res.boom.badRequest(null, { errors: errorReasons }); - } - // Update the given sale receipt details. - await this.saleReceiptService.editSaleReceipt( - tenantId, - saleReceiptId, - saleReceipt, - ); - return res.status(200).send(); + try { + // Update the given sale receipt details. + await this.saleReceiptService.editSaleReceipt( + tenantId, + saleReceiptId, + saleReceipt, + ); + return res.status(200).send(); + } catch (error) { + next(error); + } } /** diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 12dc5138b..69bebb022 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -29,7 +29,7 @@ import Settings from 'api/controllers/Settings'; import Currencies from 'api/controllers/Currencies'; import Customers from 'api/controllers/Contacts/Customers'; import Vendors from 'api/controllers/Contacts/Vendors'; -import Sales from 'api/controllers/Sales' +// import Sales from 'api/controllers/Sales' import Purchases from 'api/controllers/Purchases'; import Resources from './controllers/Resources'; import ExchangeRates from 'api/controllers/ExchangeRates'; @@ -95,7 +95,7 @@ export default () => { dashboard.use('/customers', Container.get(Customers).router()); dashboard.use('/vendors', Container.get(Vendors).router()); // dashboard.use('/sales', Container.get(Sales).router()); - // dashboard.use('/purchases', Container.get(Purchases).router()); + dashboard.use('/purchases', Container.get(Purchases).router()); dashboard.use('/resources', Container.get(Resources).router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); dashboard.use('/media', Container.get(Media).router()); diff --git a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js index 42adc70fb..f4e2e3902 100644 --- a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js +++ b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js @@ -9,7 +9,7 @@ exports.up = function(knex) { table.integer('reference_id').index(); table.integer('account_id').unsigned().index().references('id').inTable('accounts'); table.string('contact_type').nullable().index(); - table.integer('contact_id').unsigned().nullable().index().references('id').inTable('contacts'); + table.integer('contact_id').unsigned().nullable().index(); table.string('note'); table.boolean('draft').defaultTo(false); table.integer('user_id').unsigned().index(); diff --git a/server/src/database/migrations/20200719152005_create_bills_table.js b/server/src/database/migrations/20200719152005_create_bills_table.js index efc81c7d6..6e34f3c5a 100644 --- a/server/src/database/migrations/20200719152005_create_bills_table.js +++ b/server/src/database/migrations/20200719152005_create_bills_table.js @@ -14,6 +14,7 @@ exports.up = function(knex) { table.decimal('payment_amount', 13, 3).defaultTo(0); table.string('inv_lot_number').index(); + table.integer('user_id').unsigned(); table.timestamps(); }); }; diff --git a/server/src/database/seeds/core/20200810121807_seed_views.js b/server/src/database/seeds/core/20200810121807_seed_views.js index 19da37d7b..f47b01b23 100644 --- a/server/src/database/seeds/core/20200810121807_seed_views.js +++ b/server/src/database/seeds/core/20200810121807_seed_views.js @@ -1,5 +1,5 @@ import Container from 'typedi'; -import TenancyService from 'services/Tenancy/TenancyService' +import TenancyService from 'services/Tenancy/TenancyService'; exports.up = (knex) => { const tenancyService = Container.get(TenancyService); diff --git a/server/src/interfaces/Bill.ts b/server/src/interfaces/Bill.ts index a448bd536..b8bffe78b 100644 --- a/server/src/interfaces/Bill.ts +++ b/server/src/interfaces/Bill.ts @@ -1,3 +1,44 @@ -export interface IBillOTD {}; -export interface IBill {}; - \ No newline at end of file +import { IItemEntry, IItemEntryDTO } from "./ItemEntry"; + +export interface IBillDTO { + vendorId: number, + billNumber: string, + billDate: Date, + dueDate: Date, + referenceNo: string, + status: string, + note: string, + amount: number, + paymentAmount: number, + entries: IItemEntryDTO[], +}; + +export interface IBillEditDTO { + billDate: Date, + dueDate: Date, + referenceNo: string, + status: string, + note: string, + amount: number, + paymentAmount: number, + entries: IItemEntryDTO[], +}; + +export interface IBill { + id?: number, + + vendorId: number, + billNumber: string, + billDate: Date, + dueDate: Date, + referenceNo: string, + status: string, + note: string, + amount: number, + paymentAmount: number, + + invLotNumber: string, + + entries: IItemEntry[], +}; + \ No newline at end of file diff --git a/server/src/interfaces/BillPayment.ts b/server/src/interfaces/BillPayment.ts index a08e716a2..51fab3a64 100644 --- a/server/src/interfaces/BillPayment.ts +++ b/server/src/interfaces/BillPayment.ts @@ -1,15 +1,35 @@ export interface IBillPaymentEntry { + id?: number, + billPaymentId: number, billId: number, paymentAmount: number, }; export interface IBillPayment { + id?: number, + vendorId: number, amount: number, reference: string, - billNo: string, + paymentAccountId: number, + paymentNumber: string, + paymentDate: Date, + userId: number, entries: IBillPaymentEntry[], } -export interface IBillPaymentOTD {}; \ No newline at end of file +export interface IBillPaymentEntryDTO { + billId: number, + paymentAmount: number, +}; + +export interface IBillPaymentDTO { + vendorId: number, + paymentAccountId: number, + paymentNumber: string, + paymentDate: Date, + description: string, + reference: string, + entries: IBillPaymentEntryDTO[], +}; \ No newline at end of file diff --git a/server/src/interfaces/ItemEntry.ts b/server/src/interfaces/ItemEntry.ts index 7b519c55f..eed0cb7fd 100644 --- a/server/src/interfaces/ItemEntry.ts +++ b/server/src/interfaces/ItemEntry.ts @@ -11,4 +11,8 @@ export interface IItemEntry { discount: number, quantity: number, rate: number, +} + +export interface IItemEntryDTO { + } \ No newline at end of file diff --git a/server/src/interfaces/SaleEstimate.ts b/server/src/interfaces/SaleEstimate.ts index f8fd64b43..0f623bb93 100644 --- a/server/src/interfaces/SaleEstimate.ts +++ b/server/src/interfaces/SaleEstimate.ts @@ -1,4 +1,25 @@ +import { IItemEntry } from "./ItemEntry"; -export interface ISaleEstimate {}; -export interface ISaleEstimateOTD {}; \ No newline at end of file +export interface ISaleEstimate { + id?: number, + amount: number, + customerId: number, + estimateDate: Date, + reference: string, + note: string, + termsConditions: string, + userId: number, + entries: IItemEntry[], + + createdAt?: Date, +}; +export interface ISaleEstimateDTO { + customerId: number, + estimateDate?: Date, + reference: string, + estimateNumber: string, + entries: IItemEntry[], + note: string, + termsConditions: string, +}; \ No newline at end of file diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index ac8850243..e3ef2060b 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -1,3 +1,4 @@ +import { IItemEntry, IItemEntryDTO } from "./ItemEntry"; export interface ISaleInvoice { id: number, @@ -5,7 +6,7 @@ export interface ISaleInvoice { paymentAmount: number, invoiceDate: Date, dueDate: Date, - entries: any[], + entries: IItemEntry[], } export interface ISaleInvoiceOTD { @@ -14,7 +15,7 @@ export interface ISaleInvoiceOTD { referenceNo: string, invoiceMessage: string, termsConditions: string, - entries: any[], + entries: IItemEntryDTO[], } export interface ISalesInvoicesFilter{ diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 61d02b690..d54dba3ee 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -2,6 +2,7 @@ export * from './Model'; export * from './InventoryTransaction'; export * from './BillPayment'; +export * from './Bill'; export * from './InventoryCostMethod'; export * from './ItemEntry'; export * from './Item'; @@ -25,4 +26,5 @@ export * from './View'; export * from './ManualJournal'; export * from './Currency'; export * from './ExchangeRate'; -export * from './Media'; \ No newline at end of file +export * from './Media'; +export * from './SaleEstimate'; \ No newline at end of file diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index e81cf6659..1c3847b02 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -1,7 +1,11 @@ -import { Container } from 'typedi'; // Here we import all events. import 'subscribers/authentication'; import 'subscribers/organization'; import 'subscribers/manualJournals'; -import 'subscribers/expenses'; \ No newline at end of file +import 'subscribers/expenses'; +import 'subscribers/bills'; +// import 'subscribers/saleInvoices'; +import 'subscribers/customers'; +import 'subscribers/vendors'; +import 'subscribers/paymentMades'; \ No newline at end of file diff --git a/server/src/repositories/AccountTypeRepository.ts b/server/src/repositories/AccountTypeRepository.ts index 21c93ead5..f1421c061 100644 --- a/server/src/repositories/AccountTypeRepository.ts +++ b/server/src/repositories/AccountTypeRepository.ts @@ -70,13 +70,24 @@ export default class AccountTypeRepository extends TenantRepository { * @param {string} rootType * @return {IAccountType[]} */ - getByRootType(rootType: string): IAccountType[] { + getByRootType(rootType: string): Promise { const { AccountType } = this.models; return this.cache.get(`accountType.rootType.${rootType}`, () => { return AccountType.query().where('root_type', rootType); }); } + /** + * Retrieve accounts types of the given child type. + * @param {string} childType + */ + getByChildType(childType: string): Promise { + const { AccountType } = this.models; + return this.cache.get(`accountType.childType.${childType}`, () => { + return AccountType.query().where('child_type', childType); + }); + } + /** * Flush repository cache. */ diff --git a/server/src/repositories/ContactRepository.ts b/server/src/repositories/ContactRepository.ts index 712e357e1..ee5e903be 100644 --- a/server/src/repositories/ContactRepository.ts +++ b/server/src/repositories/ContactRepository.ts @@ -1,6 +1,5 @@ import TenantRepository from 'repositories/TenantRepository'; import { IContact } from 'interfaces'; -import Contact from 'models/Contact'; export default class ContactRepository extends TenantRepository { cache: any; @@ -45,9 +44,11 @@ export default class ContactRepository extends TenantRepository { * Inserts a new contact model. * @param contact */ - async insert(contact) { - await Contact.query().insert({ ...contact }) + async insert(contactInput: IContact) { + const { Contact } = this.models; + const contact = await Contact.query().insert({ ...contactInput }) this.flushCache(); + return contact; } /** @@ -56,6 +57,7 @@ export default class ContactRepository extends TenantRepository { * @param {IContact} contact - Contact input. */ async update(contactId: number, contact: IContact) { + const { Contact } = this.models; await Contact.query().findById(contactId).patch({ ...contact }); this.flushCache(); } @@ -66,6 +68,7 @@ export default class ContactRepository extends TenantRepository { * @return {Promise} */ async deleteById(contactId: number): Promise { + const { Contact } = this.models; await Contact.query().where('id', contactId).delete(); this.flushCache(); } @@ -75,6 +78,7 @@ export default class ContactRepository extends TenantRepository { * @param {number[]} contactsIds */ async bulkDelete(contactsIds: number[]) { + const { Contact } = this.models; await Contact.query().whereIn('id', contactsIds); this.flushCache(); } diff --git a/server/src/repositories/VendorRepository.ts b/server/src/repositories/VendorRepository.ts index 264c158c8..e7856ada2 100644 --- a/server/src/repositories/VendorRepository.ts +++ b/server/src/repositories/VendorRepository.ts @@ -17,6 +17,15 @@ export default class VendorRepository extends TenantRepository { this.cache = this.tenancy.cache(tenantId); } + /** + * Retrieve vendor details of the given id. + * @param {number} vendorId - Vendor id. + */ + findById(vendorId: number) { + const { Contact } = this.models; + return Contact.query().findById(vendorId); + } + /** * Retrieve the bill that associated to the given vendor id. * @param {number} vendorId - Vendor id. @@ -49,4 +58,23 @@ export default class VendorRepository extends TenantRepository { .whereIn('id', vendorIds) .withGraphFetched('bills'); } + + changeBalance(vendorId: number, amount: number) { + const { Contact } = this.models; + const changeMethod = (amount > 0) ? 'increment' : 'decrement'; + + return Contact.query() + .where('id', vendorId) + [changeMethod]('balance', Math.abs(amount)); + } + + + changeDiffBalance( + vendorId: number, + amount: number, + oldAmount: number, + oldVendorId?: number, + ) { + + } } diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts index 6c98474ec..20cd25895 100644 --- a/server/src/services/Contacts/ContactsService.ts +++ b/server/src/services/Contacts/ContactsService.ts @@ -26,7 +26,7 @@ export default class ContactsService { * @param {TContactService} contactService * @return {Promise} */ - private async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) { + public async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) { const { Contact } = this.tenancy.models(tenantId); this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId }); diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index be63d6f93..bec4b1d50 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -1,5 +1,9 @@ import { Inject, Service } from 'typedi'; import { omit, difference } from 'lodash'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import JournalPoster from "services/Accounting/JournalPoster"; import JournalCommands from "services/Accounting/JournalCommands"; import ContactsService from 'services/Contacts/ContactsService'; @@ -13,6 +17,7 @@ import { import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import events from 'subscribers/events'; @Service() export default class CustomersService { @@ -25,6 +30,12 @@ export default class CustomersService { @Inject() dynamicListService: DynamicListingService; + @Inject('logger') + logger: any; + + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Converts customer to contact DTO. * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO @@ -43,31 +54,44 @@ export default class CustomersService { * Creates a new customer. * @param {number} tenantId * @param {ICustomerNewDTO} customerDTO - * @return {Promise} + * @return {Promise} */ - public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) { + public async newCustomer( + tenantId: number, + customerDTO: ICustomerNewDTO + ): Promise { + this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO }); + const contactDTO = this.customerToContactDTO(customerDTO) const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer'); - // Writes the customer opening balance journal entries. - if (customer.openingBalance) { - await this.writeCustomerOpeningBalanceJournal( - tenantId, - customer.id, - customer.openingBalance, - ); - } + this.logger.info('[customer] created successfully.', { tenantId, customerDTO }); + await this.eventDispatcher.dispatch(events.customers.onCreated); + return customer; } /** * Edits details of the given customer. * @param {number} tenantId + * @param {number} customerId * @param {ICustomerEditDTO} customerDTO + * @return {Promise} */ - public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) { + public async editCustomer( + tenantId: number, + customerId: number, + customerDTO: ICustomerEditDTO, + ): Promise { const contactDTO = this.customerToContactDTO(customerDTO); - return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer'); + + this.logger.info('[customer] trying to edit customer.', { tenantId, customerId, customerDTO }); + const customer = this.contactService.editContact(tenantId, customerId, contactDTO, 'customer'); + + this.eventDispatcher.dispatch(events.customers.onEdited); + this.logger.info('[customer] edited successfully.', { tenantId, customerId }); + + return customer; } /** @@ -76,16 +100,31 @@ export default class CustomersService { * @param {number} customerId * @return {Promise} */ - public async deleteCustomer(tenantId: number, customerId: number) { + public async deleteCustomer(tenantId: number, customerId: number): Promise { const { Contact } = this.tenancy.models(tenantId); + this.logger.info('[customer] trying to delete customer.', { tenantId, customerId }); await this.getCustomerByIdOrThrowError(tenantId, customerId); await this.customerHasNoInvoicesOrThrowError(tenantId, customerId); await Contact.query().findById(customerId).delete(); + await this.eventDispatcher.dispatch(events.customers.onDeleted); + this.logger.info('[customer] deleted successfully.', { tenantId, customerId }); + } + + /** + * Reverts customer opening balance journal entries. + * @param {number} tenantId - + * @param {number} customerId - + * @return {Promise} + */ + public async revertOpeningBalanceEntries(tenantId: number, customerId: number|number[]) { + const id = Array.isArray(customerId) ? customerId : [customerId]; + + this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId }); await this.contactService.revertJEntriesContactsOpeningBalance( - tenantId, [customerId], 'customer', + tenantId, id, 'customer', ); } @@ -129,7 +168,7 @@ export default class CustomersService { * @param {number} openingBalance * @return {Promise} */ - async writeCustomerOpeningBalanceJournal( + public async writeCustomerOpeningBalanceJournal( tenantId: number, customerId: number, openingBalance: number, @@ -150,7 +189,7 @@ export default class CustomersService { * @param {number} tenantId * @param {number} customerId */ - getCustomerByIdOrThrowError(tenantId: number, customerId: number) { + private getCustomerByIdOrThrowError(tenantId: number, customerId: number) { return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer'); } @@ -159,7 +198,7 @@ export default class CustomersService { * @param {numebr} tenantId * @param {number[]} customersIds */ - getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) { + private getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) { return this.contactService.getContactsOrThrowErrorNotFound(tenantId, customersIds, 'customer'); } @@ -169,19 +208,14 @@ export default class CustomersService { * @param {number[]} customersIds * @return {Promise} */ - async deleteBulkCustomers(tenantId: number, customersIds: number[]) { + public async deleteBulkCustomers(tenantId: number, customersIds: number[]) { const { Contact } = this.tenancy.models(tenantId); await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds); await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds); await Contact.query().whereIn('id', customersIds).delete(); - - await this.contactService.revertJEntriesContactsOpeningBalance( - tenantId, - customersIds, - 'Customer' - ); + await this.eventDispatcher.dispatch(events.customers.onBulkDeleted); } /** @@ -189,8 +223,10 @@ export default class CustomersService { * or throw service error. * @param {number} tenantId * @param {number} customerId + * @throws {ServiceError} + * @return {Promise} */ - async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) { + private async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) { const { customerRepository } = this.tenancy.repositories(tenantId); const salesInvoice = await customerRepository.getSalesInvoices(customerId); @@ -204,8 +240,9 @@ export default class CustomersService { * @param {number} tenantId * @param {number[]} customersIds * @throws {ServiceError} + * @return {Promise} */ - async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) { + private async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) { const { customerRepository } = this.tenancy.repositories(tenantId); const customersWithInvoices = await customerRepository.customersWithSalesInvoices( diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index 2f49e315d..4957656a5 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -1,5 +1,9 @@ import { Inject, Service } from 'typedi'; import { difference, rest } from 'lodash'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import JournalPoster from "services/Accounting/JournalPoster"; import JournalCommands from "services/Accounting/JournalCommands"; import ContactsService from 'services/Contacts/ContactsService'; @@ -12,6 +16,7 @@ import { import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; +import events from 'subscribers/events'; @Service() export default class VendorsService { @@ -24,12 +29,18 @@ export default class VendorsService { @Inject() dynamicListService: DynamicListingService; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + @Inject('logger') + logger: any; + /** * Converts vendor to contact DTO. * @param {IVendorNewDTO|IVendorEditDTO} vendorDTO * @returns {IContactDTO} */ - vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) { + private vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) { return { ...vendorDTO, active: (typeof vendorDTO.active === 'undefined') ? @@ -43,19 +54,15 @@ export default class VendorsService { * @param {IVendorNewDTO} vendorDTO * @return {Promise} */ - async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) { - const contactDTO = this.vendorToContactDTO(vendorDTO); + public async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) { + this.logger.info('[vendor] trying create a new vendor.', { tenantId, vendorDTO }); + const contactDTO = this.vendorToContactDTO(vendorDTO); const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor'); - // Writes the vendor opening balance journal entries. - if (vendor.openingBalance) { - await this.writeVendorOpeningBalanceJournal( - tenantId, - vendor.id, - vendor.openingBalance, - ); - } + await this.eventDispatcher.dispatch(events.vendors.onCreated, { + tenantId, vendorId: vendor.id, vendor, + }); return vendor; } @@ -64,9 +71,13 @@ export default class VendorsService { * @param {number} tenantId * @param {IVendorEditDTO} vendorDTO */ - async editVendor(tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO) { + public async editVendor(tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO) { const contactDTO = this.vendorToContactDTO(vendorDTO); - return this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor'); + const vendor = await this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor'); + + await this.eventDispatcher.dispatch(events.vendors.onEdited); + + return vendor; } /** @@ -74,7 +85,7 @@ export default class VendorsService { * @param {number} tenantId * @param {number} customerId */ - getVendorByIdOrThrowError(tenantId: number, customerId: number) { + private getVendorByIdOrThrowError(tenantId: number, customerId: number) { return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor'); } @@ -84,17 +95,17 @@ export default class VendorsService { * @param {number} vendorId * @return {Promise} */ - async deleteVendor(tenantId: number, vendorId: number) { + public async deleteVendor(tenantId: number, vendorId: number) { const { Contact } = this.tenancy.models(tenantId); - + await this.getVendorByIdOrThrowError(tenantId, vendorId); await this.vendorHasNoBillsOrThrowError(tenantId, vendorId); + this.logger.info('[vendor] trying to delete vendor.', { tenantId, vendorId }); await Contact.query().findById(vendorId).delete(); - await this.contactService.revertJEntriesContactsOpeningBalance( - tenantId, [vendorId], 'vendor', - ); + await this.eventDispatcher.dispatch(events.vendors.onDeleted, { tenantId, vendorId }); + this.logger.info('[vendor] deleted successfully.', { tenantId, vendorId }); } /** @@ -102,7 +113,7 @@ export default class VendorsService { * @param {number} tenantId * @param {number} vendorId */ - async getVendor(tenantId: number, vendorId: number) { + public async getVendor(tenantId: number, vendorId: number) { return this.contactService.getContact(tenantId, vendorId, 'vendor'); } @@ -113,7 +124,7 @@ export default class VendorsService { * @param {number} openingBalance * @return {Promise} */ - async writeVendorOpeningBalanceJournal( + public async writeVendorOpeningBalanceJournal( tenantId: number, vendorId: number, openingBalance: number, @@ -121,20 +132,36 @@ export default class VendorsService { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); + this.logger.info('[vendor] writing opening balance journal entries.', { tenantId, vendorId }); await journalCommands.vendorOpeningBalance(vendorId, openingBalance) - + await Promise.all([ journal.saveBalance(), journal.saveEntries(), ]); } + /** + * Reverts vendor opening balance journal entries. + * @param {number} tenantId - + * @param {number} vendorId - + * @return {Promise} + */ + public async revertOpeningBalanceEntries(tenantId: number, vendorId: number|number[]) { + const id = Array.isArray(vendorId) ? vendorId : [vendorId]; + + this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId }); + await this.contactService.revertJEntriesContactsOpeningBalance( + tenantId, id, 'vendor', + ); + } + /** * Retrieve the given vendors or throw error if one of them not found. * @param {numebr} tenantId * @param {number[]} vendorsIds */ - getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) { + private getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) { return this.contactService.getContactsOrThrowErrorNotFound(tenantId, vendorsIds, 'vendor'); } @@ -144,17 +171,19 @@ export default class VendorsService { * @param {number[]} vendorsIds * @return {Promise} */ - async deleteBulkVendors(tenantId: number, vendorsIds: number[]) { + public async deleteBulkVendors( + tenantId: number, + vendorsIds: number[] + ): Promise { const { Contact } = this.tenancy.models(tenantId); - + await this.getVendorsOrThrowErrorNotFound(tenantId, vendorsIds); await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds); await Contact.query().whereIn('id', vendorsIds).delete(); + await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, { tenantId, vendorsIds }); - await this.contactService.revertJEntriesContactsOpeningBalance( - tenantId, vendorsIds, 'vendor', - ); + this.logger.info('[vendor] bulk deleted successfully.', { tenantId, vendorsIds }); } /** @@ -162,7 +191,7 @@ export default class VendorsService { * @param {number} tenantId * @param {number} vendorId */ - async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) { + private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) { const { vendorRepository } = this.tenancy.repositories(tenantId); const bills = await vendorRepository.getBills(vendorId); @@ -177,7 +206,7 @@ export default class VendorsService { * @param {number[]} customersIds * @throws {ServiceError} */ - async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) { + private async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) { const { vendorRepository } = this.tenancy.repositories(tenantId); const vendorsWithBills = await vendorRepository.vendorsWithBills(vendorsIds); @@ -197,7 +226,7 @@ export default class VendorsService { * @param {number} tenantId - Tenant id. * @param {IVendorsFilter} vendorsFilter - Vendors filter. */ - async getVendorsList(tenantId: number, vendorsFilter: IVendorsFilter) { + public async getVendorsList(tenantId: number, vendorsFilter: IVendorsFilter) { const { Vendor } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Vendor, vendorsFilter); diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index cc9b083a2..beb2fa24e 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -143,16 +143,21 @@ export default class ExpensesService implements IExpensesService { } } + /** + * Reverts expense journal entries. + * @param {number} tenantId + * @param {number} expenseId + */ public async revertJournalEntries( tenantId: number, expenseId: number|number[], - ) { + ): Promise { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); await journalCommands.revertJournalEntries(expenseId, 'Expense'); - return Promise.all([ + await Promise.all([ journal.saveBalance(), journal.deleteEntries(), ]); diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index f3825488c..4020c86dd 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -1,15 +1,41 @@ import { Inject, Service } from 'typedi'; -import { omit, sumBy } from 'lodash'; +import { entries, omit, sumBy, difference } from 'lodash'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import moment from 'moment'; -import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces'; -import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; +import events from 'subscribers/events'; +import { + IBill, + IBillPaymentDTO, + IBillPaymentEntryDTO, + IBillPayment, + IBillPaymentsFilter, + IPaginationMeta, + IFilterMeta, + IBillPaymentEntry, +} from 'interfaces'; import AccountsService from 'services/Accounts/AccountsService'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; +import JournalCommands from 'services/Accounting/JournalCommands'; import JournalPosterService from 'services/Sales/JournalPosterService'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; +import { ServiceError } from 'exceptions'; + +const ERRORS = { + BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND', + PAYMENT_MADE_NOT_FOUND: 'PAYMENT_MADE_NOT_FOUND', + BILL_PAYMENT_NUMBER_NOT_UNQIUE: 'BILL_PAYMENT_NUMBER_NOT_UNQIUE', + PAYMENT_ACCOUNT_NOT_FOUND: 'PAYMENT_ACCOUNT_NOT_FOUND', + PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', + BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', + BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND', + INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT', +}; /** * Bill payments service. @@ -29,6 +55,177 @@ export default class BillPaymentsService { @Inject() dynamicListService: DynamicListingService; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + @Inject('logger') + logger: any; + + /** + * Validate whether the bill payment vendor exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + private async getVendorOrThrowError(tenantId: number, vendorId: number) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + const vendor = await vendorRepository.findById(vendorId); + + if (!vendor) { + throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND) + } + return vendor; + } + + /** + * Validates the bill payment existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + private async getPaymentMadeOrThrowError(tenantid: number, paymentMadeId: number) { + const { BillPayment } = this.tenancy.models(tenantid); + const billPayment = await BillPayment.query().findById(paymentMadeId); + + if (!billPayment) { + throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); + } + return billPayment; + } + + /** + * Validates the payment account. + * @param {number} tenantId - + * @param {number} paymentAccountId + * @return {Promise} + */ + private async getPaymentAccountOrThrowError(tenantId: number, paymentAccountId: number) { + const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + + const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset'); + const paymentAccount = await accountRepository.findById(paymentAccountId); + + const currentAssetTypesIds = currentAssetTypes.map(type => type.id); + + if (!paymentAccount) { + throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); + } + if (currentAssetTypesIds.indexOf(paymentAccount.accountTypeId) === -1) { + throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE); + } + return paymentAccount; + } + + /** + * Validates the payment number uniqness. + * @param {number} tenantId - + * @param {string} paymentMadeNumber - + * @return {Promise} + */ + private async validatePaymentNumber(tenantId: number, paymentMadeNumber: string, notPaymentMadeId?: string) { + const { BillPayment } = this.tenancy.models(tenantId); + + const foundBillPayment = await BillPayment.query() + .onBuild((builder: any) => { + builder.findOne('payment_number', paymentMadeNumber); + + if (notPaymentMadeId) { + builder.whereNot('id', notPaymentMadeId); + } + }); + + if (foundBillPayment) { + throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE) + } + return foundBillPayment; + } + + /** + * Validate whether the entries bills ids exist on the storage. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + private async validateBillsExistance(tenantId: number, billPaymentEntries: IBillPaymentEntry[], notVendorId?: number) { + const { Bill } = this.tenancy.models(tenantId); + const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId); + + const storedBills = await Bill.query().onBuild((builder) => { + builder.whereIn('id', entriesBillsIds); + + if (notVendorId) { + builder.where('vendor_id', notVendorId); + } + }); + const storedBillsIds = storedBills.map((t: IBill) => t.id); + const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds); + + if (notFoundBillsIds.length > 0) { + throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND) + } + } + + /** + * Validate wether the payment amount bigger than the payable amount. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @return {void} + */ + private async validateBillsDueAmount(tenantId: number, billPaymentEntries: IBillPaymentEntryDTO[]) { + const { Bill } = this.tenancy.models(tenantId); + const billsIds = billPaymentEntries.map((entry: IBillPaymentEntryDTO) => entry.billId); + + const storedBills = await Bill.query().whereIn('id', billsIds); + const storedBillsMap = new Map( + storedBills.map((bill: any) => [bill.id, bill]), + ); + interface invalidPaymentAmountError{ + index: number, + due_amount: number + }; + const hasWrongPaymentAmount: invalidPaymentAmountError[] = []; + + billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => { + const entryBill = storedBillsMap.get(entry.billId); + const { dueAmount } = entryBill; + + if (dueAmount < entry.paymentAmount) { + hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); + } + }); + if (hasWrongPaymentAmount.length > 0) { + throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT); + } + } + + /** + * Validate the payment receive entries IDs existance. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + private async validateEntriesIdsExistance( + tenantId: number, + billPaymentId: number, + billPaymentEntries: IBillPaymentEntry[] + ) { + const { BillPaymentEntry } = this.tenancy.models(tenantId); + + const entriesIds = billPaymentEntries + .filter((entry: any) => entry.id) + .map((entry: any) => entry.id); + + const storedEntries = await BillPaymentEntry.query().where('bill_payment_id', billPaymentId); + + const storedEntriesIds = storedEntries.map((entry: any) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND); + } + } + /** * Creates a new bill payment transcations and store it to the storage * with associated bills entries and journal transactions. @@ -40,53 +237,39 @@ export default class BillPaymentsService { * - Increment the payment amount of the given vendor bills. * - Decrement the vendor balance. * - Records payment journal entries. + * ------ * @param {number} tenantId - Tenant id. * @param {BillPaymentDTO} billPayment - Bill payment object. */ - public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) { - const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); + public async createBillPayment( + tenantId: number, + billPaymentDTO: IBillPaymentDTO + ): Promise { + this.logger.info('[paymentDate] trying to save payment made.', { tenantId, billPaymentDTO }); + const { BillPayment } = this.tenancy.models(tenantId); - const billPayment = { - amount: sumBy(billPaymentDTO.entries, 'payment_amount'), - ...formatDateFields(billPaymentDTO, ['payment_date']), - } - const storedBillPayment = await BillPayment.query() - .insert({ - ...omit(billPayment, ['entries']), + const billPaymentObj = { + amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), + ...formatDateFields(billPaymentDTO, ['paymentDate']), + }; + await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId); + await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId); + await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber); + await this.validateBillsExistance(tenantId, billPaymentObj.entries); + await this.validateBillsDueAmount(tenantId, billPaymentObj.entries); + + const billPayment = await BillPayment.query() + .insertGraph({ + ...omit(billPaymentObj, ['entries']), + entries: billPaymentDTO.entries, }); - const storeOpers: Promise[] = []; - billPayment.entries.forEach((entry) => { - const oper = BillPaymentEntry.query() - .insert({ - bill_payment_id: storedBillPayment.id, - ...entry, - }); - // Increment the bill payment amount. - const billOper = Bill.changePaymentAmount( - entry.bill_id, - entry.payment_amount, - ); - storeOpers.push(billOper); - storeOpers.push(oper); + await this.eventDispatcher.dispatch(events.billPayments.onCreated, { + tenantId, billPayment, billPaymentId: billPayment.id, }); - // Decrement the vendor balance after bills payments. - const vendorDecrementOper = Vendor.changeBalance( - billPayment.vendor_id, - billPayment.amount * -1, - ); - // Records the journal transactions after bills payment - // and change diff acoount balance. - const recordJournalTransaction = this.recordPaymentReceiveJournalEntries(tenantId, { - id: storedBillPayment.id, - ...billPayment, - }); - await Promise.all([ - ...storeOpers, - recordJournalTransaction, - vendorDecrementOper, - ]); - return storedBillPayment; + this.logger.info('[payment_made] inserted successfully.', { tenantId, billPaymentId: billPayment.id, }); + + return billPayment; } /** @@ -110,63 +293,31 @@ export default class BillPaymentsService { tenantId: number, billPaymentId: number, billPaymentDTO, - oldBillPayment, - ) { - const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); - const billPayment = { - amount: sumBy(billPaymentDTO.entries, 'payment_amount'), - ...formatDateFields(billPaymentDTO, ['payment_date']), - }; - const updateBillPayment = await BillPayment.query() - .where('id', billPaymentId) - .update({ - ...omit(billPayment, ['entries']), - }); - const opers = []; - const entriesHasIds = billPayment.entries.filter((i) => i.id); - const entriesHasNoIds = billPayment.entries.filter((e) => !e.id); + ): Promise { + const { BillPayment } = this.tenancy.models(tenantId); - const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( - oldBillPayment.entries, - entriesHasIds - ); - if (entriesIdsShouldDelete.length > 0) { - const deleteOper = BillPaymentEntry.query() - .bulkDelete(entriesIdsShouldDelete); - opers.push(deleteOper); - } - // Entries that should be update to the storage. - if (entriesHasIds.length > 0) { - const updateOper = BillPaymentEntry.query() - .bulkUpdate(entriesHasIds, { where: 'id' }); - opers.push(updateOper); - } - // Entries that should be inserted to the storage. - if (entriesHasNoIds.length > 0) { - const insertOper = BillPaymentEntry.query() - .bulkInsert( - entriesHasNoIds.map((e) => ({ ...e, bill_payment_id: billPaymentId })) - ); - opers.push(insertOper); - } - // Records the journal transactions after bills payment and change - // different acoount balance. - const recordJournalTransaction = this.recordPaymentReceiveJournalEntries(tenantId, { - id: storedBillPayment.id, - ...billPayment, - }); - // Change the different vendor balance between the new and old one. - const changeDiffBalance = Vendor.changeDiffBalance( - billPayment.vendor_id, - oldBillPayment.vendorId, - billPayment.amount * -1, - oldBillPayment.amount * -1, - ); - await Promise.all([ - ...opers, - recordJournalTransaction, - changeDiffBalance, - ]); + const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId); + + const billPaymentObj = { + amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), + ...formatDateFields(billPaymentDTO, ['paymentDate']), + }; + + await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId); + await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId); + await this.validateEntriesIdsExistance(tenantId, billPaymentId, billPaymentObj.entries); + await this.validateBillsExistance(tenantId, billPaymentObj.entries); + await this.validateBillsDueAmount(tenantId, billPaymentObj.entries); + + const billPayment = await BillPayment.query() + .upsertGraph({ + id: billPaymentId, + ...omit(billPaymentObj, ['entries']), + }); + await this.eventDispatcher.dispatch(events.billPayments.onEdited); + this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldPaymentMade }); + + return billPayment; } /** @@ -176,29 +327,16 @@ export default class BillPaymentsService { * @return {Promise} */ public async deleteBillPayment(tenantId: number, billPaymentId: number) { - const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); - const billPayment = await BillPayment.query().where('id', billPaymentId).first(); + const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId); + + this.logger.info('[bill_payment] trying to delete.', { tenantId, billPaymentId }); + const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId); - await BillPayment.query() - .where('id', billPaymentId) - .delete(); + await BillPaymentEntry.query().where('bill_payment_id', billPaymentId).delete(); + await BillPayment.query().where('id', billPaymentId).delete(); - await BillPaymentEntry.query() - .where('bill_payment_id', billPaymentId) - .delete(); - - const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions( - billPaymentId, - 'BillPayment', - ); - const revertVendorBalanceOper = Vendor.changeBalance( - billPayment.vendorId, - billPayment.amount, - ); - return Promise.all([ - deleteTransactionsOper, - revertVendorBalanceOper, - ]); + await this.eventDispatcher.dispatch(events.billPayments.onDeleted, { tenantId, billPaymentId, oldPaymentMade }); + this.logger.info('[bill_payment] deleted successfully.', { tenantId, billPaymentId }); } /** @@ -207,15 +345,13 @@ export default class BillPaymentsService { * @param {BillPayment} billPayment * @param {Integer} billPaymentId */ - private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) { - const { AccountTransaction, Account } = this.tenancy.models(tenantId); + public async recordJournalEntries(tenantId: number, billPayment: IBillPayment) { + const { AccountTransaction } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); - const paymentAmount = sumBy(billPayment.entries, 'payment_amount'); - const formattedDate = moment(billPayment.payment_date).format('YYYY-MM-DD'); - const payableAccount = await this.accountsService.getAccountByType( - tenantId, - 'accounts_payable' - ); + const paymentAmount = sumBy(billPayment.entries, 'paymentAmount'); + const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD'); + const payableAccount = await accountRepository.getBySlug('accounts-payable'); const journal = new JournalPoster(tenantId); const commonJournal = { @@ -238,13 +374,13 @@ export default class BillPaymentsService { ...commonJournal, debit: paymentAmount, contactType: 'Vendor', - contactId: billPayment.vendor_id, + contactId: billPayment.vendorId, account: payableAccount.id, }); const creditPaymentAccount = new JournalEntry({ ...commonJournal, credit: paymentAmount, - account: billPayment.payment_account_id, + account: billPayment.paymentAccountId, }); journal.debit(debitReceivable); journal.credit(creditPaymentAccount); @@ -256,6 +392,24 @@ export default class BillPaymentsService { ]); } + /** + * Reverts bill payment journal entries. + * @param {number} tenantId + * @param {number} billPaymentId + * @return {Promise} + */ + public async revertJournalEntries(tenantId: number, billPaymentId: number) { + const journal = new JournalPoster(tenantId); + const journalCommands = new JournalCommands(journal); + + await journalCommands.revertJournalEntries(billPaymentId, 'BillPayment'); + + return Promise.all([ + journal.saveBalance(), + journal.deleteEntries(), + ]); + } + /** * Retrieve bill payment paginted and filterable list. * @param {number} tenantId @@ -301,18 +455,4 @@ export default class BillPaymentsService { return billPayment; } - - /** - * Detarmines whether the bill payment exists on the storage. - * @param {Integer} billPaymentId - * @return {boolean} - */ - async isBillPaymentExists(tenantId: number, billPaymentId: number) { - const { BillPayment } = this.tenancy.models(tenantId); - const billPayment = await BillPayment.query() - .where('id', billPaymentId) - .first(); - - return (billPayment.length > 0); - } } diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 0cbb66903..378248d6f 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -1,16 +1,39 @@ -import { omit, sumBy, pick } from 'lodash'; +import { omit, sumBy, pick, difference } from 'lodash'; import moment from 'moment'; import { Inject, Service } from 'typedi'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import events from 'subscribers/events'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; import AccountsService from 'services/Accounts/AccountsService'; -import JournalPosterService from 'services/Sales/JournalPosterService'; import InventoryService from 'services/Inventory/Inventory'; -import HasItemsEntries from 'services/Sales/HasItemsEntries'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; import { formatDateFields } from 'utils'; -import{ IBillOTD, IBill, IItem } from 'interfaces'; +import { + IBillDTO, + IBill, + IItem, + ISystemUser, + IItemEntry, + IItemEntryDTO, + IBillEditDTO, +} from 'interfaces'; +import { ServiceError } from 'exceptions'; +import ItemsService from 'services/Items/ItemsService'; + +const ERRORS = { + BILL_NOT_FOUND: 'BILL_NOT_FOUND', + BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND', + BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE', + BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS', + BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND', + BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', + NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS', +}; /** * Vendor bills services. @@ -24,9 +47,138 @@ export default class BillsService extends SalesInvoicesCost { @Inject() accountsService: AccountsService; + @Inject() + itemsService: ItemsService; + @Inject() tenancy: TenancyService; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + @Inject('logger') + logger: any; + + /** + * Validates whether the vendor is exist. + * @async + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + private async getVendorOrThrowError(tenantId: number, vendorId: number) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[bill] trying to get vendor.', { tenantId, vendorId }); + const foundVendor = await vendorRepository.findById(vendorId); + + if (!foundVendor) { + this.logger.info('[bill] the given vendor not found.', { tenantId, vendorId }); + throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND); + } + return foundVendor; + } + + /** + * Validates the given bill existance. + * @async + * @param {number} tenantId - + * @param {number} billId - + */ + private async getBillOrThrowError(tenantId: number, billId: number) { + const { Bill } = this.tenancy.models(tenantId); + + this.logger.info('[bill] trying to get bill.', { tenantId, billId }); + const foundBill = await Bill.query().findById(billId).withGraphFetched('entries'); + + if (!foundBill) { + this.logger.info('[bill] the given bill not found.', { tenantId, billId }); + throw new ServiceError(ERRORS.BILL_NOT_FOUND); + } + return foundBill; + } + + /** + * Validates the entries items ids. + * @async + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + private async validateItemsIdsExistance(tenantId: number, billEntries: IItemEntryDTO[]) { + const { Item } = this.tenancy.models(tenantId); + const itemsIds = billEntries.map((e) => e.itemId); + + const foundItems = await Item.query().whereIn('id', itemsIds); + + const foundItemsIds = foundItems.map((item: IItem) => item.id); + const notFoundItemsIds = difference(itemsIds, foundItemsIds); + + if (notFoundItemsIds.length > 0) { + throw new ServiceError(ERRORS.BILL_ITEMS_NOT_FOUND); + } + } + + /** + * Validates the bill number existance. + * @async + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + private async validateBillNumberExists(tenantId: number, billNumber: string) { + const { Bill } = this.tenancy.models(tenantId); + const foundBills = await Bill.query().where('bill_number', billNumber); + + if (foundBills.length > 0) { + throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS); + } + } + + /** + * Validates the entries ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validateEntriesIdsExistance(tenantId: number, billId: number, billEntries: any) { + const { ItemEntry } = this.tenancy.models(tenantId); + const entriesIds = billEntries.filter((e) => e.id).map((e) => e.id); + + const storedEntries = await ItemEntry.query() + .whereIn('reference_id', [billId]) + .whereIn('reference_type', ['Bill']); + + const storedEntriesIds = storedEntries.map((entry) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND) + } + } + + /** + * Validate the entries items that not purchase-able. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + private async validateNonPurchasableEntriesItems(tenantId: number, billEntries: any) { + const { Item } = this.tenancy.models(tenantId); + const itemsIds = billEntries.map((e: IItemEntry) => e.itemId); + + const purchasbleItems = await Item.query() + .where('purchasable', true) + .whereIn('id', itemsIds); + + const purchasbleItemsIds = purchasbleItems.map((item: IItem) => item.id); + const notPurchasableItems = difference(itemsIds, purchasbleItemsIds); + + if (notPurchasableItems.length > 0) { + throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS); + } + } + /** * Converts bill DTO to model. * @param {number} tenantId @@ -35,13 +187,13 @@ export default class BillsService extends SalesInvoicesCost { * * @returns {IBill} */ - async billDTOToModel(tenantId: number, billDTO: IBillOTD, oldBill?: IBill) { + private async billDTOToModel(tenantId: number, billDTO: IBillDTO, oldBill?: IBill) { const { ItemEntry } = this.tenancy.models(tenantId); let invLotNumber = oldBill?.invLotNumber; - if (!invLotNumber) { - invLotNumber = await this.inventoryService.nextLotNumber(tenantId); - } + // if (!invLotNumber) { + // invLotNumber = await this.inventoryService.nextLotNumber(tenantId); + // } const entries = billDTO.entries.map((entry) => ({ ...entry, amount: ItemEntry.calcAmount(entry), @@ -58,7 +210,7 @@ export default class BillsService extends SalesInvoicesCost { /** * Creates a new bill and stored it to the storage. - * + * ---- * Precedures. * ---- * - Insert bill transactions to the storage. @@ -67,55 +219,43 @@ export default class BillsService extends SalesInvoicesCost { * - Record bill journal transactions on the given accounts. * - Record bill items inventory transactions. * ---- - * @param {number} tenantId - The given tenant id. - * @param {IBillOTD} billDTO - - * @return {void} + * @param {number} tenantId - The given tenant id. + * @param {IBillDTO} billDTO - + * @return {Promise} */ - async createBill(tenantId: number, billDTO: IBillOTD) { - const { Vendor, Bill, ItemEntry } = this.tenancy.models(tenantId); + public async createBill( + tenantId: number, + billDTO: IBillDTO, + authorizedUser: ISystemUser + ): Promise { + const { Bill } = this.tenancy.models(tenantId); - const bill = await this.billDTOToModel(tenantId, billDTO); - const saveEntriesOpers = []; + this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO }); + const billObj = await this.billDTOToModel(tenantId, billDTO); - const storedBill = await Bill.query() - .insert({ - ...omit(bill, ['entries']), - }); - bill.entries.forEach((entry) => { - const oper = ItemEntry.query() - .insertAndFetch({ + await this.getVendorOrThrowError(tenantId, billDTO.vendorId); + await this.validateBillNumberExists(tenantId, billDTO.billNumber); + + await this.validateItemsIdsExistance(tenantId, billDTO.entries); + await this.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); + + const bill = await Bill.query() + .insertGraph({ + ...omit(billObj, ['entries']), + userId: authorizedUser.id, + entries: billDTO.entries.map((entry) => ({ reference_type: 'Bill', - reference_id: storedBill.id, - ...omit(entry, ['amount']), - }).then((itemEntry) => { - entry.id = itemEntry.id; - }); - saveEntriesOpers.push(oper); + ...omit(entry, ['amount', 'id']), + })), + }); + + // Triggers `onBillCreated` event. + await this.eventDispatcher.dispatch(events.bills.onCreated, { + tenantId, bill, billId: bill.id, }); - // Await save all bill entries operations. - await Promise.all([...saveEntriesOpers]); + this.logger.info('[bill] bill inserted successfully.', { tenantId, billId: bill.id }); - // Increments vendor balance. - const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount); - - // Rewrite the inventory transactions for inventory items. - const writeInvTransactionsOper = this.recordInventoryTransactions( - tenantId, bill, storedBill.id - ); - // Writes the journal entries for the given bill transaction. - const writeJEntriesOper = this.recordJournalTransactions(tenantId, { - id: storedBill.id, ...bill, - }); - await Promise.all([ - incrementOper, - writeInvTransactionsOper, - writeJEntriesOper, - ]); - // Schedule bill re-compute based on the item cost - // method and starting date. - await this.scheduleComputeBillItemsCost(tenantId, bill); - - return storedBill; + return bill; } /** @@ -132,55 +272,34 @@ export default class BillsService extends SalesInvoicesCost { * * @param {number} tenantId - The given tenant id. * @param {Integer} billId - The given bill id. - * @param {billDTO} billDTO - The given new bill details. + * @param {IBillEditDTO} billDTO - The given new bill details. + * @return {Promise} */ - async editBill(tenantId: number, billId: number, billDTO: billDTO) { - const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId); - - const oldBill = await Bill.query().findById(billId); - const bill = this.billDTOToModel(tenantId, billDTO, oldBill); + public async editBill( + tenantId: number, + billId: number, + billDTO: IBillEditDTO, + ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + this.logger.info('[bill] trying to edit bill.', { tenantId, billId }); + const oldBill = await this.getBillOrThrowError(tenantId, billId); + const billObj = this.billDTOToModel(tenantId, billDTO, oldBill); // Update the bill transaction. - const updatedBill = await Bill.query() - .where('id', billId) - .update({ - ...omit(bill, ['entries', 'invLotNumber']) + const bill = await Bill.query() + .upsertGraph({ + id: billId, + ...omit(billObj, ['entries', 'invLotNumber']), + entries: billDTO.entries.map((entry) => ({ + reference_type: 'Bill', + ...omit(entry, ['amount']), + })) }); - // Old stored entries. - const storedEntries = await ItemEntry.query() - .where('reference_id', billId) - .where('reference_type', 'Bill'); + // Triggers event `onBillEdited`. + await this.eventDispatcher.dispatch(events.bills.onEdited, { tenantId, billId, oldBill, bill }); - // Patch the bill entries. - const patchEntriesOper = HasItemsEntries.patchItemsEntries( - bill.entries, storedEntries, 'Bill', billId, - ); - // Changes the diff vendor balance between old and new amount. - const changeVendorBalanceOper = Vendor.changeDiffBalance( - bill.vendor_id, - oldBill.vendorId, - bill.amount, - oldBill.amount, - ); - // Re-write the inventory transactions for inventory items. - const writeInvTransactionsOper = this.recordInventoryTransactions( - tenantId, bill, billId, true - ); - // Writes the journal entries for the given bill transaction. - const writeJEntriesOper = this.recordJournalTransactions(tenantId, { - id: billId, - ...bill, - }, billId); - - await Promise.all([ - patchEntriesOper, - changeVendorBalanceOper, - writeInvTransactionsOper, - writeJEntriesOper, - ]); - // Schedule sale invoice re-compute based on the item cost - // method and starting date. - await this.scheduleComputeBillItemsCost(tenantId, bill); + return bill; } /** @@ -188,13 +307,10 @@ export default class BillsService extends SalesInvoicesCost { * @param {Integer} billId * @return {void} */ - async deleteBill(tenantId: number, billId: number) { - const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId); + public async deleteBill(tenantId: number, billId: number) { + const { Bill, ItemEntry } = this.tenancy.models(tenantId); - const bill = await Bill.query() - .where('id', billId) - .withGraphFetched('entries') - .first(); + const oldBill = await this.getBillOrThrowError(tenantId, billId); // Delete all associated bill entries. const deleteBillEntriesOper = ItemEntry.query() @@ -205,28 +321,10 @@ export default class BillsService extends SalesInvoicesCost { // Delete the bill transaction. const deleteBillOper = Bill.query().where('id', billId).delete(); - // Delete associated bill journal transactions. - const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions( - billId, - 'Bill' - ); - // Delete bill associated inventory transactions. - const deleteInventoryTransOper = this.inventoryService.deleteInventoryTransactions( - tenantId, billId, 'Bill' - ); - // Revert vendor balance. - const revertVendorBalance = Vendor.changeBalance(bill.vendorId, bill.amount * -1); - - await Promise.all([ - deleteBillOper, - deleteBillEntriesOper, - deleteTransactionsOper, - deleteInventoryTransOper, - revertVendorBalance, - ]); - // Schedule sale invoice re-compute based on the item cost - // method and starting date. - await this.scheduleComputeBillItemsCost(tenantId, bill); + await Promise.all([deleteBillEntriesOper, deleteBillOper]); + + // Triggers `onBillDeleted` event. + await this.eventDispatcher.dispatch(events.bills.onDeleted, { tenantId, billId, oldBill }); } /** @@ -262,20 +360,18 @@ export default class BillsService extends SalesInvoicesCost { * @param {IBill} bill * @param {Integer} billId */ - async recordJournalTransactions(tenantId: number, bill: any, billId?: number) { - const { AccountTransaction, Item } = this.tenancy.models(tenantId); + async recordJournalTransactions(tenantId: number, bill: IBill, billId?: number) { + const { AccountTransaction, Item, ItemEntry } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); - const entriesItemsIds = bill.entries.map((entry) => entry.item_id); - const payableTotal = sumBy(bill.entries, 'amount'); - const formattedDate = moment(bill.bill_date).format('YYYY-MM-DD'); + const entriesItemsIds = bill.entries.map((entry) => entry.itemId); + const formattedDate = moment(bill.billDate).format('YYYY-MM-DD'); - const storedItems = await Item.query() - .whereIn('id', entriesItemsIds); + const storedItems = await Item.query().whereIn('id', entriesItemsIds); const storedItemsMap = new Map(storedItems.map((item) => [item.id, item])); - const payableAccount = await this.accountsService.getAccountByType( - tenantId, 'accounts_payable' - ); + const payableAccount = await accountRepository.getBySlug('accounts-payable'); + const journal = new JournalPoster(tenantId); const commonJournalMeta = { @@ -284,7 +380,7 @@ export default class BillsService extends SalesInvoicesCost { referenceId: bill.id, referenceType: 'Bill', date: formattedDate, - accural: true, + userId: bill.userId, }; if (billId) { const transactions = await AccountTransaction.query() @@ -297,23 +393,26 @@ export default class BillsService extends SalesInvoicesCost { } const payableEntry = new JournalEntry({ ...commonJournalMeta, - credit: payableTotal, + credit: bill.amount, account: payableAccount.id, - contactId: bill.vendor_id, + contactId: bill.vendorId, contactType: 'Vendor', + index: 1, }); journal.credit(payableEntry); - bill.entries.forEach((entry) => { - const item: IItem = storedItemsMap.get(entry.item_id); + bill.entries.forEach((entry, index) => { + const item: IItem = storedItemsMap.get(entry.itemId); + const amount = ItemEntry.calcAmount(entry); const debitEntry = new JournalEntry({ ...commonJournalMeta, - debit: entry.amount, + debit: amount, account: ['inventory'].indexOf(item.type) !== -1 ? item.inventoryAccountId : item.costAccountId, + index: index + 2, }); journal.debit(debitEntry); }); @@ -324,44 +423,6 @@ export default class BillsService extends SalesInvoicesCost { ]); } - /** - * Detarmines whether the bill exists on the storage. - * @param {number} tenantId - The given tenant id. - * @param {Integer} billId - The given bill id. - * @return {Boolean} - */ - async isBillExists(tenantId: number, billId: number) { - const { Bill } = this.tenancy.models(tenantId); - - const foundBills = await Bill.query().where('id', billId); - return foundBills.length > 0; - } - - /** - * Detarmines whether the given bills exist on the storage in bulk. - * @param {Array} billsIds - * @return {Boolean} - */ - async isBillsExist(tenantId: number, billsIds: number[]) { - const { Bill } = this.tenancy.models(tenantId); - - const bills = await Bill.query().whereIn('id', billsIds); - return bills.length > 0; - } - - /** - * Detarmines whether the given bill id exists on the storage. - * @param {number} tenantId - * @param {Integer} billNumber - * @return {boolean} - */ - async isBillNoExists(tenantId: number, billNumber : string) { - const { Bill } = this.tenancy.models(tenantId); - - const foundBills = await Bill.query() - .where('bill_number', billNumber); - return foundBills.length > 0; - } /** * Retrieve the given bill details with associated items entries. @@ -370,8 +431,7 @@ export default class BillsService extends SalesInvoicesCost { */ getBill(tenantId: number, billId: number) { const { Bill } = this.tenancy.models(tenantId); - - return Bill.query().where('id', billId).first(); + return Bill.query().findById(billId).withGraphFetched('entries'); } /** diff --git a/server/src/services/Sales/JournalPosterService.ts b/server/src/services/Sales/JournalPosterService.ts index d0e4d95b8..622f043e2 100644 --- a/server/src/services/Sales/JournalPosterService.ts +++ b/server/src/services/Sales/JournalPosterService.ts @@ -1,6 +1,7 @@ import { Service, Inject } from 'typedi'; import JournalPoster from 'services/Accounting/JournalPoster'; import TenancyService from 'services/Tenancy/TenancyService'; +import JournalCommands from 'services/Accounting/JournalCommands'; @Service() export default class JournalPosterService { @@ -19,20 +20,14 @@ export default class JournalPosterService { referenceId: number, referenceType: string ) { - const { Account, AccountTransaction } = this.tenancy.models(tenantId); + const journal = new JournalPoster(tenantId); + const journalCommand = new JournalCommands(journal); - const transactions = await AccountTransaction.tenant() - .query() - .whereIn('reference_type', [referenceType]) - .where('reference_id', referenceId) - .withGraphFetched('account.type'); + await journalCommand.revertJournalEntries(referenceId, referenceType); - const accountsDepGraph = await Account.tenant().depGraph().query(); - const journal = new JournalPoster(accountsDepGraph); - - journal.loadEntries(transactions); - journal.removeEntries(); - - await Promise.all([journal.deleteEntries(), journal.saveBalance()]); + await Promise.all([ + journal.deleteEntries(), + journal.saveBalance() + ]); } } \ No newline at end of file diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index f0f27a262..c7f568be4 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -1,6 +1,11 @@ import { omit, sumBy, chain } from 'lodash'; import moment from 'moment'; import { Service, Inject } from 'typedi'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import events from 'subscribers/events'; import { IPaymentReceiveOTD } from 'interfaces'; import AccountsService from 'services/Accounts/AccountsService'; import JournalPoster from 'services/Accounting/JournalPoster'; @@ -34,6 +39,186 @@ export default class PaymentReceiveService { @Inject('logger') logger: any; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + + + /** + * Validates the payment receive number existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validatePaymentReceiveNoExistance(req: Request, res: Response, next: Function) { + const tenantId = req.tenantId; + const isPaymentNoExists = await this.paymentReceiveService.isPaymentReceiveNoExists( + tenantId, + req.body.payment_receive_no, + req.params.id, + ); + if (isPaymentNoExists) { + return res.status(400).send({ + errors: [{ type: 'PAYMENT.RECEIVE.NUMBER.EXISTS', code: 400 }], + }); + } + next(); + } + + /** + * Validates the payment receive existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validatePaymentReceiveExistance(req: Request, res: Response, next: Function) { + const tenantId = req.tenantId; + const isPaymentNoExists = await this.paymentReceiveService + .isPaymentReceiveExists( + tenantId, + req.params.id + ); + if (!isPaymentNoExists) { + return res.status(400).send({ + errors: [{ type: 'PAYMENT.RECEIVE.NOT.EXISTS', code: 600 }], + }); + } + next(); + } + + /** + * Validate the deposit account id existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validateDepositAccount(req: Request, res: Response, next: Function) { + const tenantId = req.tenantId; + const isDepositAccExists = await this.accountsService.isAccountExists( + tenantId, + req.body.deposit_account_id + ); + if (!isDepositAccExists) { + return res.status(400).send({ + errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }], + }); + } + next(); + } + + /** + * Validates the `customer_id` existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validateCustomerExistance(req: Request, res: Response, next: Function) { + const { Customer } = req.models; + + const isCustomerExists = await Customer.query().findById(req.body.customer_id); + + if (!isCustomerExists) { + return res.status(400).send({ + errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }], + }); + } + next(); + } + + /** + * Validates the invoices IDs existance. + * @param {Request} req - + * @param {Response} res - + * @param {Function} next - + */ + async validateInvoicesIDs(req: Request, res: Response, next: Function) { + const paymentReceive = { ...req.body }; + const { tenantId } = req; + const invoicesIds = paymentReceive.entries + .map((e) => e.invoice_id); + + const notFoundInvoicesIDs = await this.saleInvoiceService.isInvoicesExist( + tenantId, + invoicesIds, + paymentReceive.customer_id, + ); + if (notFoundInvoicesIDs.length > 0) { + return res.status(400).send({ + errors: [{ type: 'INVOICES.IDS.NOT.FOUND', code: 500 }], + }); + } + next(); + } + + /** + * Validates entries invoice payment amount. + * @param {Request} req - + * @param {Response} res - + * @param {Function} next - + */ + async validateInvoicesPaymentsAmount(req: Request, res: Response, next: Function) { + const { SaleInvoice } = req.models; + const invoicesIds = req.body.entries.map((e) => e.invoice_id); + + const storedInvoices = await SaleInvoice.query() + .whereIn('id', invoicesIds); + + const storedInvoicesMap = new Map( + storedInvoices.map((invoice) => [invoice.id, invoice]) + ); + const hasWrongPaymentAmount: any[] = []; + + req.body.entries.forEach((entry, index: number) => { + const entryInvoice = storedInvoicesMap.get(entry.invoice_id); + const { dueAmount } = entryInvoice; + + if (dueAmount < entry.payment_amount) { + hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); + } + }); + if (hasWrongPaymentAmount.length > 0) { + return res.status(400).send({ + errors: [ + { + type: 'INVOICE.PAYMENT.AMOUNT', + code: 200, + indexes: hasWrongPaymentAmount, + }, + ], + }); + } + next(); + } + + /** + * Validate the payment receive entries IDs existance. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + async validateEntriesIdsExistance(req: Request, res: Response, next: Function) { + const paymentReceive = { id: req.params.id, ...req.body }; + const entriesIds = paymentReceive.entries + .filter(entry => entry.id) + .map(entry => entry.id); + + const { PaymentReceiveEntry } = req.models; + + const storedEntries = await PaymentReceiveEntry.query() + .where('payment_receive_id', paymentReceive.id); + + const storedEntriesIds = storedEntries.map((entry) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }], + }); + } + next(); + } + + /** * Creates a new payment receive and store it to the storage * with associated invoices payment and journal transactions. @@ -53,9 +238,10 @@ export default class PaymentReceiveService { this.logger.info('[payment_receive] inserting to the storage.'); const storedPaymentReceive = await PaymentReceive.query() - .insert({ + .insertGraph({ amount: paymentAmount, ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), + entries: paymentReceive.entries.map((entry) => ({ ...entry })), }); const storeOpers: Array = []; @@ -92,6 +278,8 @@ export default class PaymentReceiveService { customerIncrementOper, recordJournalTransactions, ]); + await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated); + return storedPaymentReceive; } @@ -186,6 +374,7 @@ export default class PaymentReceiveService { changeCustomerBalance, diffInvoicePaymentAmount, ]); + await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited); } /** @@ -239,6 +428,7 @@ export default class PaymentReceiveService { revertCustomerBalance, revertInvoicesPaymentAmount, ]); + await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted); } /** diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index 8ec782d8f..44526fb46 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -1,10 +1,15 @@ import { omit, difference, sumBy, mixin } from 'lodash'; import { Service, Inject } from 'typedi'; -import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces'; +import { IEstimatesFilter, IFilterMeta, IPaginationMeta, ISaleEstimate, ISaleEstimateDTO } from 'interfaces'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import HasItemsEntries from 'services/Sales/HasItemsEntries'; import { formatDateFields } from 'utils'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import events from 'subscribers/events'; /** * Sale estimate service. @@ -24,14 +29,132 @@ export default class SaleEstimateService { @Inject() dynamicListService: DynamicListingService; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + + /** + * Validate whether the estimate customer exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) { + const estimate = { ...req.body }; + const { Customer } = req.models + + const foundCustomer = await Customer.query().findById(estimate.customer_id); + + if (!foundCustomer) { + return res.status(404).send({ + errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }], + }); + } + next(); + } + + /** + * Validate the estimate number unique on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validateEstimateNumberExistance(req: Request, res: Response, next: Function) { + const estimate = { ...req.body }; + const { tenantId } = req; + + const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique( + tenantId, + estimate.estimate_number, + req.params.id, + ); + if (isEstNumberUnqiue) { + return res.boom.badRequest(null, { + errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }], + }); + } + next(); + } + + /** + * Validate the estimate entries items ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) { + const tenantId = req.tenantId; + const estimate = { ...req.body }; + const estimateItemsIds = estimate.entries.map(e => e.item_id); + + // Validate items ids in estimate entries exists. + const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds); + + if (notFoundItemsIds.length > 0) { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }], + }); + } + next(); + } + + /** + * Validate whether the sale estimate id exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async validateEstimateIdExistance(req: Request, res: Response, next: Function) { + const { id: estimateId } = req.params; + const { tenantId } = req; + + const storedEstimate = await this.saleEstimateService + .getEstimate(tenantId, estimateId); + + if (!storedEstimate) { + return res.status(404).send({ + errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }], + }); + } + next(); + } + + /** + * Validate sale invoice entries ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) { + const { ItemEntry } = req.models; + + const { id: saleInvoiceId } = req.params; + const saleInvoice = { ...req.body }; + const entriesIds = saleInvoice.entries + .filter(e => e.id) + .map((e) => e.id); + + const foundEntries = await ItemEntry.query() + .whereIn('id', entriesIds) + .where('reference_type', 'SaleInvoice') + .where('reference_id', saleInvoiceId); + + if (foundEntries.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }], + }); + } + next(); + } + /** * Creates a new estimate with associated entries. * @async * @param {number} tenantId - The tenant id. * @param {EstimateDTO} estimate - * @return {void} + * @return {Promise} */ - async createEstimate(tenantId: number, estimateDTO: any) { + async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise { const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e)); @@ -44,22 +167,15 @@ export default class SaleEstimateService { const storedEstimate = await SaleEstimate.query() .insert({ ...omit(estimate, ['entries']), - }); - const storeEstimateEntriesOpers: any[] = []; - - this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.'); - estimate.entries.forEach((entry: any) => { - const oper = ItemEntry.query() - .insert({ + entries: estimate.entries.map((entry) => ({ reference_type: 'SaleEstimate', reference_id: storedEstimate.id, ...omit(entry, ['total', 'amount', 'id']), - }); - storeEstimateEntriesOpers.push(oper); - }); - await Promise.all([...storeEstimateEntriesOpers]); + })) + }); this.logger.info('[sale_estimate] insert sale estimated success.'); + await this.eventDispatcher.dispatch(events.saleEstimates.onCreated); return storedEstimate; } @@ -72,7 +188,7 @@ export default class SaleEstimateService { * @param {EstimateDTO} estimate * @return {void} */ - async editEstimate(tenantId: number, estimateId: number, estimateDTO: any) { + async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise { const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e)); @@ -89,16 +205,14 @@ export default class SaleEstimateService { .where('reference_id', estimateId) .where('reference_type', 'SaleEstimate'); - const patchItemsEntries = this.itemsEntriesService.patchItemsEntries( + await this.itemsEntriesService.patchItemsEntries( tenantId, estimate.entries, storedEstimateEntries, 'SaleEstimate', estimateId, ); - return Promise.all([ - patchItemsEntries, - ]); + await this.eventDispatcher.dispatch(events.saleEstimates.onEdited); } /** @@ -118,6 +232,9 @@ export default class SaleEstimateService { .delete(); await SaleEstimate.query().where('id', estimateId).delete(); + this.logger.info('[sale_estimate] deleted successfully.', { tenantId, estimateId }); + + await this.eventDispatcher.dispatch(events.saleEstimates.onDeleted); } /** diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 1dc960e5b..160cb20ed 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -4,7 +4,15 @@ import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; -import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces'; +import { + ISaleInvoice, + ISaleInvoiceOTD, + IItemEntry, + ISalesInvoicesFilter, + IPaginationMeta, + IFilterMeta +} from 'interfaces'; +import events from 'subscribers/events'; import JournalPoster from 'services/Accounting/JournalPoster'; import HasItemsEntries from 'services/Sales/HasItemsEntries'; import InventoryService from 'services/Inventory/Inventory'; @@ -12,6 +20,16 @@ import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; +import { ServiceError } from 'exceptions'; +import ItemsService from 'services/Items/ItemsService'; + + +const ERRORS = { + SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND', + ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', + NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS', + SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE' +} /** * Sales invoices service @@ -37,6 +55,81 @@ export default class SaleInvoicesService extends SalesInvoicesCost { @EventDispatcher() eventDispatcher: EventDispatcherInterface; + @Inject() + itemsService: ItemsService; + + /** + * Retrieve sale invoice or throw not found error. + * @param {number} tenantId + * @param {number} saleInvoiceId + */ + private async getSaleInvoiceOrThrowError(tenantId: number, saleInvoiceId: number): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + const saleInvoice = await SaleInvoice.query().where('id', saleInvoiceId); + + if (!saleInvoice) { + throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); + } + return saleInvoice; + } + + /** + * Validate whether sale invoice number unqiue on the storage. + * @param {number} tenantId + * @param {number} saleInvoiceNo + * @param {number} notSaleInvoiceId + */ + private async validateSaleInvoiceNoUniquiness(tenantId: number, saleInvoiceNo: string, notSaleInvoiceId: number) { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const foundSaleInvoice = await SaleInvoice.query() + .onBuild((query: any) => { + query.where('invoice_no', saleInvoiceNo); + + if (notSaleInvoiceId) { + query.whereNot('id', notSaleInvoiceId); + } + return query; + }); + + if (foundSaleInvoice.length > 0) { + throw new ServiceError(ERRORS.SALE_INVOICE_NO_NOT_UNIQUE); + } + } + + /** + * Validates sale invoice items that not sellable. + */ + private async validateNonSellableEntriesItems(tenantId: number, saleInvoiceEntries: any) { + const { Item } = this.tenancy.models(tenantId); + const itemsIds = saleInvoiceEntries.map(e => e.itemId); + + const sellableItems = await Item.query().where('sellable', true).whereIn('id', itemsIds); + + const sellableItemsIds = sellableItems.map((item) => item.id); + const notSellableItems = difference(itemsIds, sellableItemsIds); + + if (notSellableItems.length > 0) { + throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); + } + } + + /** + * + * @param {number} tenantId + * @param {} saleInvoiceEntries + */ + validateEntriesIdsExistance(tenantId: number, saleInvoiceEntries: any) { + const entriesItemsIds = saleInvoiceEntries.map((e) => e.item_id); + + const isItemsIdsExists = await this.itemsService.isItemsIdsExists( + tenantId, entriesItemsIds, + ); + if (isItemsIdsExists.length > 0) { + throw new ServiceError(ERRORS.ENTRIES_ITEMS_IDS_NOT_EXISTS); + } + } + /** * Creates a new sale invoices and store it to the storage * with associated to entries and journal transactions. @@ -58,6 +151,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost { invLotNumber, }; + await this.validateSaleInvoiceNoUniquiness(tenantId, saleInvoiceDTO.invoiceNo); + await this.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries); + this.logger.info('[sale_invoice] inserting sale invoice to the storage.'); const storedInvoice = await SaleInvoice.query() .insert({ @@ -95,6 +191,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost { // method and starting date. await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id); + await this.eventDispatcher.dispatch(events.saleInvoice.onCreated); + return storedInvoice; } @@ -131,30 +229,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost { .where('reference_type', 'SaleInvoice'); // Patch update the sale invoice items entries. - const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries( + await this.itemsEntriesService.patchItemsEntries( tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId, ); - - this.logger.info('[sale_invoice] change customer different balance.'); - // Changes the diff customer balance between old and new amount. - const changeCustomerBalanceOper = Customer.changeDiffBalance( - saleInvoice.customer_id, - oldSaleInvoice.customerId, - balance, - oldSaleInvoice.balance, - ); - // Records the inventory transactions for inventory items. - const recordInventoryTransOper = this.recordInventoryTranscactions( - tenantId, saleInvoice, saleInvoiceId, true, - ); - await Promise.all([ - patchItemsEntriesOper, - changeCustomerBalanceOper, - recordInventoryTransOper, - ]); - // Schedule sale invoice re-compute based on the item cost - // method and starting date. - await this.scheduleComputeInvoiceItemsCost(tenantId, saleInvoiceId, true); + // Triggers `onSaleInvoiceEdited` event. + await this.eventDispatcher.dispatch(events.saleInvoice.onEdited); } /** @@ -168,14 +247,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost { SaleInvoice, ItemEntry, Customer, - Account, InventoryTransaction, AccountTransaction, } = this.tenancy.models(tenantId); - const oldSaleInvoice = await SaleInvoice.query() - .findById(saleInvoiceId) - .withGraphFetched('entries'); + const oldSaleInvoice = await this.getSaleInvoiceOrThrowError(tenantId, saleInvoiceId); this.logger.info('[sale_invoice] delete sale invoice with entries.'); await SaleInvoice.query().where('id', saleInvoiceId).delete(); @@ -218,6 +294,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost { // Schedule sale invoice re-compute based on the item cost // method and starting date. await this.scheduleComputeItemsCost(tenantId, oldSaleInvoice) + + await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted); } /** diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index 96a481136..a877d6775 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -1,5 +1,10 @@ import { omit, difference, sumBy } from 'lodash'; import { Service, Inject } from 'typedi'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import events from 'subscribers/events'; import JournalPosterService from 'services/Sales/JournalPosterService'; import HasItemEntries from 'services/Sales/HasItemsEntries'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -21,6 +26,125 @@ export default class SalesReceiptService { @Inject() itemsEntriesService: HasItemEntries; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + /** + * Validate whether sale receipt exists on the storage. + * @param {Request} req + * @param {Response} res + */ + async getSaleReceiptOrThrowError(tenantId: number, saleReceiptId: number) { + const { tenantId } = req; + const { id: saleReceiptId } = req.params; + + const isSaleReceiptExists = await this.saleReceiptService + .isSaleReceiptExists( + tenantId, + saleReceiptId, + ); + if (!isSaleReceiptExists) { + return res.status(404).send({ + errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }], + }); + } + next(); + } + + /** + * 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 + */ + async validateReceiptDepositAccountExistance(req: Request, res: Response, next: Function) { + const { tenantId } = req; + + 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 }], + }); + } + next(); + } + + /** + * 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 }] }); + } + 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 @@ -38,20 +162,14 @@ export default class SalesReceiptService { const storedSaleReceipt = await SaleReceipt.query() .insert({ ...omit(saleReceipt, ['entries']), - }); - const storeSaleReceiptEntriesOpers: Array = []; - - saleReceipt.entries.forEach((entry: any) => { - const oper = ItemEntry.query() - .insert({ + entries: saleReceipt.entries.map((entry) => ({ reference_type: 'SaleReceipt', reference_id: storedSaleReceipt.id, ...omit(entry, ['id', 'amount']), - }); - storeSaleReceiptEntriesOpers.push(oper); - }); - await Promise.all([...storeSaleReceiptEntriesOpers]); - return storedSaleReceipt; + })) + }); + + await this.eventDispatcher.dispatch(events.saleReceipts.onCreated); } /** @@ -85,7 +203,9 @@ export default class SalesReceiptService { 'SaleReceipt', saleReceiptId, ); - return Promise.all([patchItemsEntries]); + await Promise.all([patchItemsEntries]); + + await this.eventDispatcher.dispatch(events.saleReceipts.onCreated); } /** @@ -110,11 +230,13 @@ export default class SalesReceiptService { saleReceiptId, 'SaleReceipt' ); - return Promise.all([ + await Promise.all([ deleteItemsEntriesOper, deleteSaleReceiptOper, deleteTransactionsOper, ]); + + await this.eventDispatcher.dispatch(events.saleReceipts.onDeleted); } /** diff --git a/server/src/subscribers/bills.ts b/server/src/subscribers/bills.ts new file mode 100644 index 000000000..4ee297266 --- /dev/null +++ b/server/src/subscribers/bills.ts @@ -0,0 +1,83 @@ +import { Container, Inject, Service } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; +import BillsService from 'services/Purchases/Bills'; +import JournalPosterService from 'services/Sales/JournalPosterService'; +import VendorRepository from 'repositories/VendorRepository'; + +@EventSubscriber() +export default class BillSubscriber { + tenancy: TenancyService; + billsService: BillsService; + logger: any; + journalPosterService: JournalPosterService; + + constructor() { + this.tenancy = Container.get(TenancyService); + this.billsService = Container.get(BillsService); + this.logger = Container.get('logger'); + + this.journalPosterService = Container.get(JournalPosterService); + } + + /** + * Handles vendor balance increment once bill created. + */ + @On(events.bills.onCreated) + async handleVendorBalanceIncrement({ tenantId, billId, bill }) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + // Increments vendor balance. + this.logger.info('[bill] trying to increment vendor balance.', { tenantId, billId }); + await vendorRepository.changeBalance(bill.vendorId, bill.amount); + } + + /** + * Handles writing journal entries once bill created. + */ + @On(events.bills.onCreated) + @On(events.bills.onEdited) + async handlerWriteJournalEntries({ tenantId, billId, bill }) { + // Writes the journal entries for the given bill transaction. + this.logger.info('[bill] writing bill journal entries.', { tenantId }); + await this.billsService.recordJournalTransactions(tenantId, bill); + } + + /** + * Handles vendor balance decrement once bill deleted. + */ + @On(events.bills.onDeleted) + async handleVendorBalanceDecrement({ tenantId, billId, oldBill }) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + // Decrements vendor balance. + this.logger.info('[bill] trying to decrement vendor balance.', { tenantId, billId }); + await vendorRepository.changeBalance(oldBill.vendorId, oldBill.amount * -1); + } + + /** + * Handles revert journal entries on bill deleted. + */ + @On(events.bills.onDeleted) + async handlerDeleteJournalEntries({ tenantId, billId }) { + // Delete associated bill journal transactions. + this.logger.info('[bill] trying to delete journal entries.', { tenantId, billId }); + await this.journalPosterService.revertJournalTransactions(tenantId, billId, 'Bill'); + } + + + @On(events.bills.onEdited) + async handleCustomerBalanceDiffChange({ tenantId, billId, oldBill, bill }) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + // Changes the diff vendor balance between old and new amount. + this.logger.info('[bill[ change vendor the different balance.', { tenantId, billId }); + await vendorRepository.changeDiffBalance( + bill.vendorId, + oldBill.vendorId, + bill.amount, + oldBill.amount, + ); + } +} \ No newline at end of file diff --git a/server/src/subscribers/customers.ts b/server/src/subscribers/customers.ts new file mode 100644 index 000000000..7eac98037 --- /dev/null +++ b/server/src/subscribers/customers.ts @@ -0,0 +1,46 @@ +import { Container, Inject, Service } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; +import CustomersService from 'services/Contacts/CustomersService'; + +@EventSubscriber() +export default class CustomersSubscriber { + logger: any; + tenancy: TenancyService; + customersService: CustomersService; + + constructor() { + this.logger = Container.get('logger'); + this.customersService = Container.get(CustomersService); + } + + @On(events.customers.onCreated) + async handleWriteOpenBalanceEntries({ tenantId, customerId, customer }) { + + // Writes the customer opening balance journal entries. + if (customer.openingBalance) { + await this.customersService.writeCustomerOpeningBalanceJournal( + tenantId, + customer.id, + customer.openingBalance, + ); + } + } + + @On(events.customers.onDeleted) + async handleRevertOpeningBalanceEntries({ tenantId, customerId }) { + + await this.customersService.revertOpeningBalanceEntries( + tenantId, customerId, + ); + } + + @On(events.customers.onBulkDeleted) + async handleBulkRevertOpeningBalanceEntries({ tenantId, customersIds }) { + + await this.customersService.revertOpeningBalanceEntries( + tenantId, customersIds, + ); + } +} \ No newline at end of file diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 13a990f23..62c6d2998 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -1,5 +1,4 @@ - export default { /** * Authentication service. @@ -72,5 +71,90 @@ export default { onBulkDeleted: 'onExpenseBulkDeleted', onBulkPublished: 'onBulkPublished', - } + }, + + /** + * Sales invoices service. + */ + saleInvoice: { + onCreated: 'onSaleInvoiceCreated', + onEdited: 'onSaleInvoiceEdited', + onDeleted: 'onSaleInvoiceDeleted', + onBulkDelete: 'onSaleInvoiceBulkDeleted', + onPublished: 'onSaleInvoicePublished', + }, + + /** + * Sales estimates service. + */ + saleEstimates: { + onCreated: 'onSaleEstimateCreated', + onEdited: 'onSaleEstimateEdited', + onDeleted: 'onSaleEstimatedDeleted', + onBulkDelete: 'onSaleEstimatedBulkDeleted', + onPublished: 'onSaleEstimatedPublished', + }, + + /** + * Sales receipts service. + */ + saleReceipts: { + onCreated: 'onSaleReceiptsCreated', + onEdited: 'onSaleReceiptsEdited', + onDeleted: 'onSaleReceiptsDeleted', + onBulkDeleted: 'onSaleReceiptsBulkDeleted', + onPublished: 'onSaleReceiptPublished', + }, + + /** + * Payment receipts service. + */ + paymentReceipts: { + onCreated: 'onPaymentReceiveCreated', + onEdited: 'onPaymentReceiveEdited', + onDeleted: 'onPaymentReceiveDeleted', + onPublished: 'onPaymentReceiptPublished', + }, + + /** + * Bills service. + */ + bills: { + onCreated: 'onBillCreated', + onEdited: 'onBillEdited', + onDeleted: 'onBillDeleted', + onBulkDeleted: 'onBillBulkDeleted', + onPublished: 'onBillPublished', + }, + + /** + * Bill payments service. + */ + billPayments: { + onCreated: 'onBillPaymentCreated', + onEdited: 'onBillPaymentEdited', + onDeleted: 'onBillPaymentDeleted', + onBulkDeleted: 'onBillPaymentsBulkDeleted', + onPublished: 'onBillPaymentPublished', + }, + + /** + * Customers services. + */ + customers: { + onCreated: 'onCustomerCreated', + onEdited: 'onCustomerEdited', + onDeleted: 'onCustomerDeleted', + onBulkDeleted: 'onBulkDeleted', + }, + + /** + * Vendors services. + */ + vendors: { + onCreated: 'onVendorCreated', + onEdited: 'onVendorEdited', + onDeleted: 'onVendorDeleted', + onBulkDeleted: 'onVendorBulkDeleted', + }, } diff --git a/server/src/subscribers/paymentMades.ts b/server/src/subscribers/paymentMades.ts new file mode 100644 index 000000000..e4b98ec36 --- /dev/null +++ b/server/src/subscribers/paymentMades.ts @@ -0,0 +1,108 @@ +import { Container, Inject, Service } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import BillPaymentsService from 'services/Purchases/BillPayments'; +import TenancyService from 'services/Tenancy/TenancyService'; + +@EventSubscriber() +export default class PaymentMadesSubscriber { + tenancy: TenancyService; + billPaymentsService: BillPaymentsService; + logger: any; + + constructor() { + this.tenancy = Container.get(TenancyService); + this.billPaymentsService = Container.get(BillPaymentsService); + this.logger = Container.get('logger'); + } + + /** + * Handles bills payment amount increment once payment made created. + */ + @On(events.billPayments.onCreated) + async handleBillsIncrement({ tenantId, billPayment, billPaymentId }) { + const { Bill } = this.tenancy.models(tenantId); + const storeOpers = []; + + billPayment.entries.forEach((entry) => { + this.logger.info('[bill_payment] increment bill payment amount.', { + tenantId, billPaymentId, + billId: entry.billId, + amount: entry.paymentAmount, + }) + // Increment the bill payment amount. + const billOper = Bill.changePaymentAmount( + entry.billId, + entry.paymentAmount, + ); + storeOpers.push(billOper); + }); + await Promise.all(storeOpers); + } + + /** + * Handle vendor balance increment once payment made created. + */ + @On(events.billPayments.onCreated) + async handleVendorIncrement({ tenantId, billPayment, billPaymentId }) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + // Increment the vendor balance after bills payments. + this.logger.info('[bill_payment] trying to increment vendor balance.', { tenantId }); + await vendorRepository.changeBalance( + billPayment.vendorId, + billPayment.amount, + ); + } + + /** + * Handle bill payment writing journal entries once created. + */ + @On(events.billPayments.onCreated) + async handleWriteJournalEntries({ tenantId, billPayment }) { + // Records the journal transactions after bills payment + // and change diff acoount balance. + this.logger.info('[bill_payment] trying to write journal entries.', { tenantId, billPaymentId: billPayment.id }); + await this.billPaymentsService.recordJournalEntries(tenantId, billPayment); + } + + /** + * Decrements the vendor balance once bill payment deleted. + */ + @On(events.billPayments.onDeleted) + async handleVendorDecrement({ tenantId, paymentMadeId, oldPaymentMade }) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + await vendorRepository.changeBalance( + oldPaymentMade.vendorId, + oldPaymentMade.amount * -1, + ); + } + + /** + * Reverts journal entries once bill payment deleted. + */ + @On(events.billPayments.onDeleted) + async handleRevertJournalEntries({ tenantId, billPaymentId }) { + await this.billPaymentsService.revertJournalEntries( + tenantId, billPaymentId, + ); + } + + /** + * Change the vendor balance different between old and new once + * bill payment edited. + */ + @On(events.billPayments.onEdited) + async handleVendorChangeDiffBalance({ tenantId, paymentMadeId, billPayment, oldBillPayment }) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + // Change the different vendor balance between the new and old one. + await vendorRepository.changeDiffBalance( + billPayment.vendor_id, + oldBillPayment.vendorId, + billPayment.amount * -1, + oldBillPayment.amount * -1, + ); + } +} \ No newline at end of file diff --git a/server/src/subscribers/saleInvoices.ts b/server/src/subscribers/saleInvoices.ts new file mode 100644 index 000000000..bf5f1da4e --- /dev/null +++ b/server/src/subscribers/saleInvoices.ts @@ -0,0 +1,22 @@ +import { Container } from 'typedi'; +import { On, EventSubscriber } from "event-dispatch"; +import events from 'subscribers/events'; + +@EventSubscriber() +export default class SaleInvoiceSubscriber { + + @On(events.saleInvoice.onCreated) + public onSaleInvoiceCreated(payload) { + + } + + @On(events.saleInvoice.onEdited) + public onSaleInvoiceEdited(payload) { + + } + + @On(events.saleInvoice.onDeleted) + public onSaleInvoiceDeleted(payload) { + + } +} \ No newline at end of file diff --git a/server/src/subscribers/vendors.ts b/server/src/subscribers/vendors.ts new file mode 100644 index 000000000..52a510213 --- /dev/null +++ b/server/src/subscribers/vendors.ts @@ -0,0 +1,46 @@ +import { Container, Inject, Service } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; +import VendorsService from 'services/Contacts/VendorsService'; + +@EventSubscriber() +export default class VendorsSubscriber { + logger: any; + tenancy: TenancyService; + vendorsService: VendorsService; + + /** + * Constructor method. + */ + constructor() { + this.logger = Container.get('logger'); + this.vendorsService = Container.get(VendorsService); + } + + @On(events.vendors.onCreated) + async handleWriteOpeningBalanceEntries({ tenantId, vendorId, vendor }) { + // Writes the vendor opening balance journal entries. + if (vendor.openingBalance) { + await this.vendorsService.writeVendorOpeningBalanceJournal( + tenantId, + vendor.id, + vendor.openingBalance, + ); + } + } + + @On(events.vendors.onDeleted) + async handleRevertOpeningBalanceEntries({ tenantId, vendorId }) { + await this.vendorsService.revertOpeningBalanceEntries( + tenantId, vendorId, + ); + } + + @On(events.vendors.onBulkDeleted) + async handleBulkRevertOpeningBalanceEntries({ tenantId, vendorsIds }) { + await this.vendorsService.revertOpeningBalanceEntries( + tenantId, vendorsIds, + ); + } +} \ No newline at end of file From 7397afe2a933794e7116b2d1542353d9e5ce091d Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 15 Oct 2020 21:27:51 +0200 Subject: [PATCH 2/4] feat: listing vendors and customers. feat: items service events. --- .../src/api/controllers/Contacts/Customers.ts | 50 +++++++++++++++++-- .../src/api/controllers/Contacts/Vendors.ts | 10 +++- .../src/api/controllers/Subscription/index.ts | 6 ++- server/src/interfaces/ManualJournal.ts | 2 +- server/src/models/Contact.js | 9 ++++ .../src/services/Accounts/AccountsService.ts | 3 -- .../src/services/Contacts/CustomersService.ts | 9 ++-- .../src/services/Contacts/VendorsService.ts | 29 ++++++++--- .../ItemCategories/ItemCategoriesService.ts | 16 ++++++ .../ManualJournals/ManualJournalsService.ts | 4 +- server/src/subscribers/events.ts | 7 +++ 11 files changed, 119 insertions(+), 26 deletions(-) diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index 33e16079e..7b0322110 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -1,17 +1,21 @@ import { Request, Response, Router, NextFunction } from 'express'; import { Service, Inject } from 'typedi'; -import { check } from 'express-validator'; +import { check, query } from 'express-validator'; import ContactsController from 'api/controllers/Contacts/Contacts'; import CustomersService from 'services/Contacts/CustomersService'; import { ServiceError } from 'exceptions'; import { ICustomerNewDTO, ICustomerEditDTO } from 'interfaces'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class CustomersController extends ContactsController { @Inject() customersService: CustomersService; + @Inject() + dynamicListService: DynamicListingService; + /** * Express router. */ @@ -51,10 +55,11 @@ export default class CustomersController extends ContactsController { this.handlerServiceErrors, ); router.get('/', [ - + ...this.validateListQuerySchema, ], this.validationResult, - asyncMiddleware(this.getCustomersList.bind(this)) + asyncMiddleware(this.getCustomersList.bind(this)), + this.dynamicListService.handlerErrorsToResponse, ); router.get('/:id', [ ...this.specificContactSchema, @@ -76,6 +81,19 @@ export default class CustomersController extends ContactsController { ]; } + get validateListQuerySchema() { + return [ + query('column_sort_by').optional().trim().escape(), + query('sort_order').optional().isIn(['desc', 'asc']), + + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + ]; + } + /** * Creates a new customer. * @param {Request} req @@ -167,12 +185,34 @@ export default class CustomersController extends ContactsController { } } - + /** + * Retrieve customers paginated and filterable list. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ async getCustomersList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const filter = { + filterRoles: [], + sortOrder: 'asc', + columnSortBy: 'created_at', + page: 1, + pageSize: 12, + ...this.matchedQueryData(req), + }; + if (filter.stringifiedFilterRoles) { + filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); + } try { - await this.customersService.getCustomersList(tenantId) + const { customers, pagination, filterMeta } = await this.customersService.getCustomersList(tenantId, filter); + + return res.status(200).send({ + customers, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index a6b3b6786..50e3e5b05 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -196,9 +196,15 @@ export default class VendorsController extends ContactsController { filterRoles: [], ...this.matchedBodyData(req), }; + try { - const vendors = await this.vendorsService.getVendorsList(tenantId, vendorsFilter); - return res.status(200).send({ vendors }); + const { vendors, pagination, filterMeta } = await this.vendorsService.getVendorsList(tenantId, vendorsFilter); + + return res.status(200).send({ + vendors, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } diff --git a/server/src/api/controllers/Subscription/index.ts b/server/src/api/controllers/Subscription/index.ts index 1485e7249..77e727193 100644 --- a/server/src/api/controllers/Subscription/index.ts +++ b/server/src/api/controllers/Subscription/index.ts @@ -22,8 +22,10 @@ export default class SubscriptionController { router.use(AttachCurrentTenantUser); router.use(TenancyMiddleware); - router.use('/license', Container.get(PaymentViaLicenseController).router()); - + router.use( + '/license', + Container.get(PaymentViaLicenseController).router() + ); router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)) ); diff --git a/server/src/interfaces/ManualJournal.ts b/server/src/interfaces/ManualJournal.ts index 2a1e0fc05..4a861d47a 100644 --- a/server/src/interfaces/ManualJournal.ts +++ b/server/src/interfaces/ManualJournal.ts @@ -41,7 +41,7 @@ export interface IManualJournalsFilter extends IDynamicListFilterDTO { pageSize: number, } -export interface IManuaLJournalsService { +export interface IManualJournalsService { makeJournalEntries(tenantId: number, manualJournalDTO: IManualJournalDTO, authorizedUser: ISystemUser): Promise<{ manualJournal: IManualJournal }>; editJournalEntries(tenantId: number, manualJournalId: number, manualJournalDTO: IManualJournalDTO, authorizedUser): Promise<{ manualJournal: IManualJournal }>; deleteManualJournal(tenantId: number, manualJournalId: number): Promise; diff --git a/server/src/models/Contact.js b/server/src/models/Contact.js index 21505c9da..9fd718ecd 100644 --- a/server/src/models/Contact.js +++ b/server/src/models/Contact.js @@ -121,4 +121,13 @@ export default class Contact extends TenantModel { } return Promise.all(asyncOpers); } + + + static get fields() { + return { + created_at: { + column: 'created_at', + } + }; + } } diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 2c5635073..525be6c56 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -10,9 +10,6 @@ import { } from 'decorators/eventDispatcher'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; -import JournalPoster from 'services/Accounting/JournalPoster'; -import { Account } from 'models'; -import AccountRepository from 'repositories/AccountRepository'; @Service() export default class AccountsService { diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index bec4b1d50..b98356a60 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -144,15 +144,18 @@ export default class CustomersService { */ public async getCustomersList( tenantId: number, - filter: ICustomersFilter + customersFilter: ICustomersFilter ): Promise<{ customers: ICustomer[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { const { Contact } = this.tenancy.models(tenantId); - const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, filter); + const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, customersFilter); const { results, pagination } = await Contact.query().onBuild((query) => { query.modify('customer'); dynamicList.buildQuery()(query); - }); + }).pagination( + customersFilter.page - 1, + customersFilter.pageSize, + ); return { customers: results, diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index 4957656a5..8b5bb4862 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -11,7 +11,9 @@ import { IVendorNewDTO, IVendorEditDTO, IVendor, - IVendorsFilter + IVendorsFilter, + IPaginationMeta, + IFilterMeta } from 'interfaces'; import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; @@ -226,14 +228,25 @@ export default class VendorsService { * @param {number} tenantId - Tenant id. * @param {IVendorsFilter} vendorsFilter - Vendors filter. */ - public async getVendorsList(tenantId: number, vendorsFilter: IVendorsFilter) { - const { Vendor } = this.tenancy.models(tenantId); + public async getVendorsList( + tenantId: number, + vendorsFilter: IVendorsFilter + ): Promise<{ vendors: IVendor[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { Contact } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Contact, vendorsFilter); - const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Vendor, vendorsFilter); - - const vendors = await Vendor.query().onBuild((builder) => { + const { results, pagination } = await Contact.query().onBuild((builder) => { + builder.modify('vendor'); dynamicFilter.buildQuery()(builder); - }); - return vendors; + }).pagination( + vendorsFilter.page - 1, + vendorsFilter.pageSize, + ); + + return { + vendors: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; } } diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index cacd7a52e..14bbee307 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -1,5 +1,9 @@ import { Inject } from 'typedi'; import { difference } from 'lodash'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import { ServiceError } from 'exceptions'; import { IItemCategory, @@ -10,6 +14,7 @@ import { } from "interfaces"; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; +import events from 'subscribers/events'; const ERRORS = { ITEM_CATEGORIES_NOT_FOUND: 'ITEM_CATEGORIES_NOT_FOUND', @@ -33,6 +38,9 @@ export default class ItemCategoriesService implements IItemCategoriesService { @Inject('logger') logger: any; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Retrieve item category or throw not found error. * @param {number} tenantId @@ -92,6 +100,8 @@ export default class ItemCategoriesService implements IItemCategoriesService { const itemCategoryObj = this.transformOTDToObject(itemCategoryOTD, authorizedUser); const itemCategory = await ItemCategory.query().insert({ ...itemCategoryObj }); + + await this.eventDispatcher.dispatch(events.items.onCreated); this.logger.info('[item_category] item category inserted successfully.', { tenantId, itemCategoryOTD }); return itemCategory; @@ -188,6 +198,8 @@ export default class ItemCategoriesService implements IItemCategoriesService { const itemCategoryObj = this.transformOTDToObject(itemCategoryOTD, authorizedUser); const itemCategory = await ItemCategory.query().patchAndFetchById(itemCategoryId, { ...itemCategoryObj }); + + await this.eventDispatcher.dispatch(events.items.onEdited); this.logger.info('[item_category] edited successfully.', { tenantId, itemCategoryId, itemCategoryOTD }); return itemCategory; @@ -207,6 +219,8 @@ export default class ItemCategoriesService implements IItemCategoriesService { const { ItemCategory } = this.tenancy.models(tenantId); await ItemCategory.query().findById(itemCategoryId).delete(); this.logger.info('[item_category] deleted successfully.', { tenantId, itemCategoryId }); + + await this.eventDispatcher.dispatch(events.items.onDeleted); } /** @@ -267,6 +281,8 @@ export default class ItemCategoriesService implements IItemCategoriesService { await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds); await ItemCategory.query().whereIn('id', itemCategoriesIds).delete(); + + await this.eventDispatcher.dispatch(events.items.onBulkDeleted); this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds }); } } \ No newline at end of file diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index 91864a82f..29159fe5b 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -4,7 +4,7 @@ import moment from 'moment'; import { ServiceError } from "exceptions"; import { IManualJournalDTO, - IManuaLJournalsService, + IManualJournalsService, IManualJournalsFilter, ISystemUser, IManualJournal, @@ -33,7 +33,7 @@ const ERRORS = { }; @Service() -export default class ManualJournalsService implements IManuaLJournalsService { +export default class ManualJournalsService implements IManualJournalsService { @Inject() tenancy: TenancyService; diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 62c6d2998..c12e58303 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -157,4 +157,11 @@ export default { onDeleted: 'onVendorDeleted', onBulkDeleted: 'onVendorBulkDeleted', }, + + items: { + onCreated: 'onItemCreated', + onEdited: 'onItemEdited', + onDeleted: 'onItemDeleted', + onBulkDeleted: 'onItemBulkDeleted', + } } From 27ec0e91fa166a0464950857e0b1bf1970f7797c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 17 Oct 2020 15:00:22 +0200 Subject: [PATCH 3/4] refactoring: retrieve resources fields. fix: issue in create a new custom view. --- client/src/components/ErrorBoundary/index.js | 34 ++++++++ server/package.json | 1 + server/src/api/controllers/Resources.ts | 42 ++++++++-- .../core/20190423085242_seed_accounts.js | 2 - server/src/interfaces/Account.ts | 1 + server/src/lib/ViewRolesBuilder/index.ts | 29 ++++++- server/src/locales/en.json | 14 +++- server/src/models/Account.js | 7 +- server/src/models/AccountType.js | 24 +++++- server/src/repositories/ViewRepository.ts | 11 ++- .../Accounts/AccountsTypesServices.ts | 14 +++- .../src/services/Resource/ResourceService.ts | 83 +++++++++---------- server/src/services/Views/ViewsService.ts | 49 ++++++----- 13 files changed, 228 insertions(+), 83 deletions(-) create mode 100644 client/src/components/ErrorBoundary/index.js diff --git a/client/src/components/ErrorBoundary/index.js b/client/src/components/ErrorBoundary/index.js new file mode 100644 index 000000000..5a0789e50 --- /dev/null +++ b/client/src/components/ErrorBoundary/index.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function ErrorBoundary({ + error, + errorInfo, + children +}) { + + if (errorInfo) { + return ( +
+

Something went wrong.

+ +
+ {error && error.toString()} +
+ {errorInfo.componentStack} +
+
+ ); + } + return children; +} + +ErrorBoundary.defaultProps = { + children: null, +}; + +ErrorBoundary.propTypes = { + children: PropTypes.node, +}; + +export default ErrorBoundary; \ No newline at end of file diff --git a/server/package.json b/server/package.json index 953825d3c..ea4e47bce 100644 --- a/server/package.json +++ b/server/package.json @@ -59,6 +59,7 @@ "nodemon": "^1.19.1", "objection": "^2.0.10", "objection-soft-delete": "^1.0.7", + "pluralize": "^8.0.0", "reflect-metadata": "^0.1.13", "ts-transformer-keys": "^0.4.2", "tsyringe": "^4.3.0", diff --git a/server/src/api/controllers/Resources.ts b/server/src/api/controllers/Resources.ts index 91f0d8955..32795dda5 100644 --- a/server/src/api/controllers/Resources.ts +++ b/server/src/api/controllers/Resources.ts @@ -1,3 +1,4 @@ +import { Service, Inject } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; import { param, @@ -5,20 +6,26 @@ import { } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import BaseController from './BaseController'; -import { Service } from 'typedi'; -import ResourceFieldsKeys from 'data/ResourceFieldsKeys'; +import { ServiceError } from 'exceptions'; +import ResourceService from 'services/Resource/ResourceService'; @Service() export default class ResourceController extends BaseController{ + @Inject() + resourcesService: ResourceService; + /** * Router constructor. */ router() { const router = Router(); - router.get('/:resource_model/fields', - this.resourceModelParamSchema, - asyncMiddleware(this.resourceFields.bind(this)) + router.get( + '/:resource_model/fields', [ + ...this.resourceModelParamSchema, + ], + asyncMiddleware(this.resourceFields.bind(this)), + this.handleServiceErrors ); return router; } @@ -31,14 +38,39 @@ export default class ResourceController extends BaseController{ /** * Retrieve resource fields of the given resource. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ resourceFields(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; const { resource_model: resourceModel } = req.params; try { + const resourceFields = this.resourcesService.getResourceFields(tenantId, resourceModel); + return res.status(200).send({ + resource_fields: this.transfromToResponse(resourceFields), + }); } catch (error) { next(error); } } + + /** + * Handles service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'RESOURCE.MODEL.NOT.FOUND', code: 100 }], + }); + } + } + } }; diff --git a/server/src/database/seeds/core/20190423085242_seed_accounts.js b/server/src/database/seeds/core/20190423085242_seed_accounts.js index 58c29f3cc..270ad5504 100644 --- a/server/src/database/seeds/core/20190423085242_seed_accounts.js +++ b/server/src/database/seeds/core/20190423085242_seed_accounts.js @@ -5,8 +5,6 @@ exports.up = function (knex) { const tenancyService = Container.get(TenancyService); const i18n = tenancyService.i18n(knex.userParams.tenantId); - console.log(i18n); - return knex('accounts').then(() => { // Inserts seed entries return knex('accounts').insert([ diff --git a/server/src/interfaces/Account.ts b/server/src/interfaces/Account.ts index 398b48275..76dcc40a4 100644 --- a/server/src/interfaces/Account.ts +++ b/server/src/interfaces/Account.ts @@ -29,6 +29,7 @@ export interface IAccountsFilter extends IDynamicListFilterDTO { export interface IAccountType { id: number, key: string, + label: string, normal: string, rootType: string, childType: string, diff --git a/server/src/lib/ViewRolesBuilder/index.ts b/server/src/lib/ViewRolesBuilder/index.ts index f8eeff373..89606fd4a 100644 --- a/server/src/lib/ViewRolesBuilder/index.ts +++ b/server/src/lib/ViewRolesBuilder/index.ts @@ -268,4 +268,31 @@ export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRo return filterRoles.filter((filterRole: IFilterRole) => { return !validateFieldKeyExistance(model, filterRole.fieldKey); }); -} \ No newline at end of file +} + +/** + * Retrieve model fields keys. + * @param {IModel} Model + * @return {string[]} + */ +export function getModelFieldsKeys(Model: IModel) { + const fields = Object.keys(Model.fields); + + return fields.sort((a, b) => { + if (a < b) { return -1; } + if (a > b) { return 1; } + return 0; + }); +} + +export function getModelFields(Model: IModel) { + const fieldsKey = this.getModelFieldsKeys(Model); + + return fieldsKey.map((fieldKey) => { + const field = Model.fields[fieldKey]; + return { + ...field, + key: fieldKey, + }; + }) +} diff --git a/server/src/locales/en.json b/server/src/locales/en.json index 13a8df5e2..69522edf2 100644 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -1,7 +1,5 @@ { - "Empty": "", - "Hello": "Hello", - "Petty Cash": "Petty Cash 2", + "Petty Cash": "Petty Cash", "Bank": "Bank", "Other Income": "Other Income", "Interest Income": "Interest Income", @@ -30,4 +28,14 @@ "Assets": "Assets", "Liabilities": "Liabilities", "Expenses": "Expenses", + "Account name": "Account name", + "Account type": "Account type", + "Account normal": "Account normal", + "Description": "Description", + "Account code": "Account code", + "Currency": "Currency", + "Balance": "Balance", + "Active": "Active", + "Created at": "Created at", + "fixed_asset": "Fixed asset" } \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 6fa7625ac..26e38183e 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -121,7 +121,7 @@ export default class Account extends TenantModel { static get fields() { return { name: { - label: 'Name', + label: 'Account name', column: 'name', }, type: { @@ -145,20 +145,25 @@ export default class Account extends TenantModel { relationColumn: 'account_types.root_type', }, created_at: { + label: 'Created at', column: 'created_at', columnType: 'date', }, active: { + label: 'Active', column: 'active', }, balance: { + label: 'Balance', column: 'amount', columnType: 'number' }, currency: { + label: 'Currency', column: 'currency_code', }, normal: { + label: 'Account normal', column: 'account_type_id', relation: 'account_types.id', relationColumn: 'account_types.normal' diff --git a/server/src/models/AccountType.js b/server/src/models/AccountType.js index b0babeed6..a93b3d688 100644 --- a/server/src/models/AccountType.js +++ b/server/src/models/AccountType.js @@ -4,7 +4,7 @@ import TenantModel from 'models/TenantModel'; export default class AccountType extends TenantModel { /** - * Table name + * Table name. */ static get tableName() { return 'account_types'; @@ -30,4 +30,26 @@ export default class AccountType extends TenantModel { }, }; } + + /** + * Accounts types labels. + */ + static get labels() { + return { + fixed_asset: 'Fixed asset', + current_asset: "Current asset", + long_term_liability: "Long term liability", + current_liability: "Current liability", + equity: "Equity", + expense: "Expense", + income: "Income", + accounts_receivable: "Accounts receivable", + accounts_payable: "Accounts payable", + other_expense: "Other expense", + other_income: "Other income", + cost_of_goods_sold: "Cost of goods sold (COGS)", + other_liability: "Other liability", + other_asset: 'Other asset', + }; + } } diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts index 0d124d24b..b7488da99 100644 --- a/server/src/repositories/ViewRepository.ts +++ b/server/src/repositories/ViewRepository.ts @@ -1,5 +1,4 @@ import { IView } from 'interfaces'; -import { View } from 'models'; import TenantRepository from 'repositories/TenantRepository'; export default class ViewRepository extends TenantRepository { @@ -50,13 +49,23 @@ export default class ViewRepository extends TenantRepository { * @param {IView} view */ async insert(view: IView): Promise { + const { View } = this.models; const insertedView = await View.query().insertGraph({ ...view }); this.flushCache(); return insertedView; } + async update(viewId: number, view: IView): Promise { + const { View } = this.models; + const updatedView = await View.query().upsertGraph({ + id: viewId, + ...view + }); + this.flushCache(); + return updatedView; + } /** * Flushes repository cache. diff --git a/server/src/services/Accounts/AccountsTypesServices.ts b/server/src/services/Accounts/AccountsTypesServices.ts index b2b6d02d6..c21126989 100644 --- a/server/src/services/Accounts/AccountsTypesServices.ts +++ b/server/src/services/Accounts/AccountsTypesServices.ts @@ -1,4 +1,5 @@ import { Inject, Service } from 'typedi'; +import { omit } from 'lodash'; import TenancyService from 'services/Tenancy/TenancyService'; import { IAccountsTypesService, IAccountType } from 'interfaces'; @@ -12,8 +13,17 @@ export default class AccountsTypesService implements IAccountsTypesService{ * @param {number} tenantId - * @return {Promise} */ - getAccountsTypes(tenantId: number): Promise { + async getAccountsTypes(tenantId: number): Promise { const { accountTypeRepository } = this.tenancy.repositories(tenantId); - return accountTypeRepository.all(); + const { AccountType } = this.tenancy.models(tenantId); + const { __ } = this.tenancy.i18n(tenantId); + + const allAccountsTypes = await accountTypeRepository.all(); + + return allAccountsTypes.map((_accountType: IAccountType) => ({ + id: _accountType.id, + label: __(AccountType.labels[_accountType.key]), + ...omit(_accountType, ['id']), + })); } } \ No newline at end of file diff --git a/server/src/services/Resource/ResourceService.ts b/server/src/services/Resource/ResourceService.ts index cd2364b46..99d25c6c0 100644 --- a/server/src/services/Resource/ResourceService.ts +++ b/server/src/services/Resource/ResourceService.ts @@ -1,78 +1,71 @@ import { Service, Inject } from 'typedi'; -import { camelCase, upperFirst } from 'lodash' +import { camelCase, upperFirst } from 'lodash'; +import pluralize from 'pluralize'; import { IModel } from 'interfaces'; -import resourceFieldsKeys from 'data/ResourceFieldsKeys'; +import { + getModelFields, +} from 'lib/ViewRolesBuilder' import TenancyService from 'services/Tenancy/TenancyService'; +import { ServiceError } from 'exceptions'; + +const ERRORS = { + RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND', +}; @Service() export default class ResourceService { @Inject() tenancy: TenancyService; - /** - * - * @param {string} resourceName - */ - getResourceFieldsRelations(modelName: string) { - const fieldsRelations = resourceFieldsKeys[modelName]; - - if (!fieldsRelations) { - throw new Error('Fields relation not found in thte given resource model.'); - } - return fieldsRelations; - } - /** * Transform resource to model name. * @param {string} resourceName */ private resourceToModelName(resourceName: string): string { - return upperFirst(camelCase(resourceName)); + return upperFirst(camelCase(pluralize.singular(resourceName))); } /** - * Retrieve model from resource name in specific tenant. + * Retrieve model fields. * @param {number} tenantId - * @param {string} resourceName + * @param {IModel} Model */ - public getModel(tenantId: number, resourceName: string) { - const models = this.tenancy.models(tenantId); - const modelName = this.resourceToModelName(resourceName); + private getModelFields(tenantId: number, Model: IModel) { + const { __ } = this.tenancy.i18n(tenantId); + const fields = getModelFields(Model); - return models[modelName]; - } - - getModelFields(Model: IModel) { - const fields = Object.keys(Model.fields); - - return fields.sort((a, b) => { - if (a < b) { return -1; } - if (a > b) { return 1; } - return 0; - }); + return fields.map((field) => ({ + label: __(field.label, field.label), + key: field.key, + dataType: field.columnType, + })); } /** - * + * Retrieve resource fields from resource model name. * @param {string} resourceName */ - getResourceFields(Model: IModel) { - console.log(Model); + public getResourceFields(tenantId: number, modelName: string) { + const resourceModel = this.getResourceModel(tenantId, modelName); - if (Model.resourceable) { - return this.getModelFields(Model); - } - return []; + return this.getModelFields(tenantId, resourceModel); } /** - * - * @param {string} resourceName + * Retrieve resource model object. + * @param {number} tenantId - + * @param {string} inputModelName - */ - getResourceColumns(Model: IModel) { - if (Model.resourceable) { - return this.getModelFields(Model); + public getResourceModel(tenantId: number, inputModelName: string) { + const modelName = this.resourceToModelName(inputModelName); + const Models = this.tenancy.models(tenantId); + + if (!Models[modelName]) { + throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); } - return []; + if (!Models[modelName].resourceable) { + throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); + } + return Models[modelName]; } } \ No newline at end of file diff --git a/server/src/services/Views/ViewsService.ts b/server/src/services/Views/ViewsService.ts index cec769496..a55a17265 100644 --- a/server/src/services/Views/ViewsService.ts +++ b/server/src/services/Views/ViewsService.ts @@ -6,7 +6,11 @@ import { IViewDTO, IView, IViewEditDTO, + IModel, + IViewColumnDTO, + IViewRoleDTO, } from 'interfaces'; +import { getModelFieldsKeys } from 'lib/ViewRolesBuilder'; import TenancyService from 'services/Tenancy/TenancyService'; import ResourceService from "services/Resource/ResourceService"; import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder'; @@ -37,14 +41,14 @@ export default class ViewsService implements IViewsService { * @param {number} tenantId - * @param {string} resourceModel - */ - public async listResourceViews(tenantId: number, resourceModel: string): Promise { - this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel }); + public async listResourceViews(tenantId: number, resourceModelName: string): Promise { + this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModelName }); // Validate the resource model name is valid. - this.getResourceModelOrThrowError(tenantId, resourceModel); + const resourceModel = this.getResourceModelOrThrowError(tenantId, resourceModelName); const { viewRepository } = this.tenancy.repositories(tenantId); - return viewRepository.allByResource(resourceModel); + return viewRepository.allByResource(resourceModel.name); } /** @@ -53,7 +57,7 @@ export default class ViewsService implements IViewsService { * @param {IViewRoleDTO[]} viewRoles */ private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) { - const resourceFieldsKeys = this.resourceService.getResourceFields(ResourceModel); + const resourceFieldsKeys = getModelFieldsKeys(ResourceModel); const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey); const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); @@ -70,7 +74,7 @@ export default class ViewsService implements IViewsService { * @param {IViewColumnDTO[]} viewColumns */ private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) { - const resourceFieldsKeys = this.resourceService.getResourceColumns(ResourceModel); + const resourceFieldsKeys = getModelFieldsKeys(ResourceModel); const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey); const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); @@ -115,12 +119,7 @@ export default class ViewsService implements IViewsService { * @param {number} resourceModel */ private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel { - const ResourceModel = this.resourceService.getModel(tenantId, resourceModel); - - if (!ResourceModel || !ResourceModel.resourceable) { - throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); - } - return ResourceModel; + return this.resourceService.getResourceModel(tenantId, resourceModel); } /** @@ -137,6 +136,8 @@ export default class ViewsService implements IViewsService { notViewId?: number ): void { const { View } = this.tenancy.models(tenantId); + + this.logger.info('[views] trying to validate view name uniqiness.', { tenantId, resourceModel, viewName }); const foundViews = await View.query() .where('resource_model', resourceModel) .where('name', viewName) @@ -165,6 +166,8 @@ export default class ViewsService implements IViewsService { * --------- * @param {number} tenantId - Tenant id. * @param {IViewDTO} viewDTO - View DTO. + * + * @return {Promise} */ public async newView(tenantId: number, viewDTO: IViewDTO): Promise { const { viewRepository } = this.tenancy.repositories(tenantId); @@ -187,6 +190,7 @@ export default class ViewsService implements IViewsService { throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } // Save view details. + this.logger.info('[views] trying to insert to storage.', { tenantId, viewDTO }) const view = await viewRepository.insert({ predefined: false, name: viewDTO.name, @@ -216,7 +220,7 @@ export default class ViewsService implements IViewsService { * @param {IViewEditDTO} */ public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise { - const { View } = this.tenancy.models(tenantId); + const { viewRepository } = this.tenancy.repositories(tenantId); this.logger.info('[view] trying to edit custom view.', { tenantId, viewId }); // Retrieve view details or throw not found error. @@ -229,22 +233,23 @@ export default class ViewsService implements IViewsService { await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId); // Validate the given fields keys exist on the storage. - this.validateResourceRolesFieldsExistance(ResourceModel, view.roles); + this.validateResourceRolesFieldsExistance(ResourceModel, viewEditDTO.roles); // Validate the given columnable fields keys exists on the storage. - this.validateResourceColumnsExistance(ResourceModel, view.columns); + this.validateResourceColumnsExistance(ResourceModel, viewEditDTO.columns); // Validates the view conditional logic expression. if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) { throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } - // Save view details. - await View.query() - .where('id', view.id) - .patch({ - name: viewEditDTO.name, - roles_logic_expression: viewEditDTO.logicExpression, - }); + // Update view details. + await viewRepository.update(tenantId, viewId, { + predefined: false, + name: viewEditDTO.name, + rolesLogicExpression: viewEditDTO.logicExpression, + roles: viewEditDTO.roles, + columns: viewEditDTO.columns, + }) this.logger.info('[view] edited successfully.', { tenantId, viewId }); } From ebcd1d12f37b7b6933c9246517e6ecb2ed549016 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 17 Oct 2020 15:04:07 +0200 Subject: [PATCH 4/4] fix: listing custom views endpoint API in frontend. --- client/src/store/customViews/customViews.actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/store/customViews/customViews.actions.js b/client/src/store/customViews/customViews.actions.js index 9b781b837..e952d4c78 100644 --- a/client/src/store/customViews/customViews.actions.js +++ b/client/src/store/customViews/customViews.actions.js @@ -27,7 +27,7 @@ export const fetchView = ({ id }) => { export const fetchResourceViews = ({ resourceSlug }) => { return (dispatch) => new Promise((resolve, reject) => { - ApiService.get('views', { params: { resource_name: resourceSlug } }) + ApiService.get(`views/resource/${resourceSlug}`) .then((response) => { dispatch({ type: t.RESOURCE_VIEWS_SET,