From 426f9fcf558879900b14bebc95e2933083f4bb9f Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 25 Oct 2020 18:30:44 +0200 Subject: [PATCH] refactoring: payment receive and sale invoice actions. --- server/src/api/controllers/Purchases/Bills.ts | 11 +- .../api/controllers/Sales/PaymentReceives.ts | 409 +++++-------- .../api/controllers/Sales/SalesInvoices.ts | 294 +++------- server/src/api/controllers/Sales/index.ts | 4 +- server/src/interfaces/PaymentReceive.ts | 43 +- server/src/interfaces/SaleInvoice.ts | 2 + server/src/loaders/events.ts | 5 +- server/src/models/PaymentReceive.js | 13 + server/src/models/SaleInvoice.js | 2 - server/src/repositories/CustomerRepository.ts | 32 + server/src/repositories/VendorRepository.ts | 18 +- .../src/services/Items/ItemsEntriesService.ts | 6 +- server/src/services/Purchases/BillPayments.ts | 4 +- server/src/services/Purchases/Bills.ts | 7 +- server/src/services/Sales/PaymentsReceives.ts | 550 ++++++------------ server/src/services/Sales/SalesInvoices.ts | 349 ++++------- server/src/subscribers/bills.ts | 10 +- server/src/subscribers/paymentMades.ts | 2 +- server/src/subscribers/paymentReceives.ts | 88 +++ server/src/subscribers/saleInvoices.ts | 40 +- 20 files changed, 820 insertions(+), 1069 deletions(-) create mode 100644 server/src/subscribers/paymentReceives.ts diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 0b5950944..10b48df6a 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -152,7 +152,11 @@ export default class BillsController extends BaseController { try { const storedBill = await this.billsService.createBill(tenantId, billDTO, user); - return res.status(200).send({ id: storedBill.id }); + + return res.status(200).send({ + id: storedBill.id, + message: 'The bill has been created successfully.', + }); } catch (error) { next(error); } @@ -170,7 +174,10 @@ export default class BillsController extends BaseController { try { const editedBill = await this.billsService.editBill(tenantId, billId, billDTO); - return res.status(200).send({ id: billId }); + return res.status(200).send({ + id: billId, + message: 'The bill has been edited successfully.', + }); } catch (error) { next(error); } diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 0d2d1fac7..1a25a6c6a 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -1,13 +1,12 @@ -import { Router, Request, Response } from 'express'; -import { check, param, query, ValidationChain, matchedData } from 'express-validator'; -import { difference } from 'lodash'; +import { Router, Request, Response, NextFunction } from 'express'; +import { check, param, query, ValidationChain } from 'express-validator'; import { Inject, Service } from 'typedi'; -import { IPaymentReceive, IPaymentReceiveOTD } from 'interfaces'; +import { IPaymentReceiveDTO } from 'interfaces'; import BaseController from 'api/controllers/BaseController'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import PaymentReceiveService from 'services/Sales/PaymentsReceives'; -import SaleInvoiceService from 'services/Sales/SalesInvoices'; -import AccountsService from 'services/Accounts/AccountsService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { ServiceError } from 'exceptions'; /** * Payments receives controller. @@ -19,11 +18,8 @@ export default class PaymentReceivesController extends BaseController { paymentReceiveService: PaymentReceiveService; @Inject() - accountsService: AccountsService; - - @Inject() - saleInvoiceService: SaleInvoiceService; - + dynamicListService: DynamicListingService; + /** * Router constructor. */ @@ -34,45 +30,37 @@ export default class PaymentReceivesController extends BaseController { '/:id', this.editPaymentReceiveValidation, this.validationResult, - asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)), - asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)), - asyncMiddleware(this.validateCustomerExistance.bind(this)), - asyncMiddleware(this.validateDepositAccount.bind(this)), - asyncMiddleware(this.validateInvoicesIDs.bind(this)), - asyncMiddleware(this.validateEntriesIdsExistance.bind(this)), - asyncMiddleware(this.validateInvoicesPaymentsAmount.bind(this)), asyncMiddleware(this.editPaymentReceive.bind(this)), + this.handleServiceErrors, ); router.post( '/', this.newPaymentReceiveValidation, this.validationResult, - asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)), - asyncMiddleware(this.validateCustomerExistance.bind(this)), - asyncMiddleware(this.validateDepositAccount.bind(this)), - asyncMiddleware(this.validateInvoicesIDs.bind(this)), - asyncMiddleware(this.validateInvoicesPaymentsAmount.bind(this)), asyncMiddleware(this.newPaymentReceive.bind(this)), + this.handleServiceErrors, ); router.get( '/:id', this.paymentReceiveValidation, this.validationResult, - asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)), - asyncMiddleware(this.getPaymentReceive.bind(this)) + asyncMiddleware(this.getPaymentReceive.bind(this)), + this.handleServiceErrors, ); router.get( '/', this.validatePaymentReceiveList, this.validationResult, asyncMiddleware(this.getPaymentReceiveList.bind(this)), + this.handleServiceErrors, + this.dynamicListService.handlerErrorsToResponse, ); router.delete( '/:id', this.paymentReceiveValidation, this.validationResult, - asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)), asyncMiddleware(this.deletePaymentReceive.bind(this)), + this.handleServiceErrors, ); return router; } @@ -126,198 +114,6 @@ export default class PaymentReceivesController extends BaseController { return [...this.paymentReceiveSchema]; } - /** - * 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(); - } - - /** - * Records payment receive to the given customer with associated invoices. - */ - async newPaymentReceive(req: Request, res: Response) { - const { tenantId } = req; - const paymentReceive: IPaymentReceiveOTD = matchedData(req, { - locations: ['body'], - includeOptionals: true, - }); - - const storedPaymentReceive = await this.paymentReceiveService - .createPaymentReceive( - tenantId, - paymentReceive, - ); - return res.status(200).send({ id: storedPaymentReceive.id }); - } - /** * Edit payment receive validation. */ @@ -328,34 +124,48 @@ export default class PaymentReceivesController extends BaseController { ]; } + /** + * Records payment receive to the given customer with associated invoices. + */ + async newPaymentReceive(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req); + + try { + const storedPaymentReceive = await this.paymentReceiveService + .createPaymentReceive( + tenantId, + paymentReceive, + ); + return res.status(200).send({ + id: storedPaymentReceive.id, + message: 'The payment receive has been created successfully.', + }); + } catch (error) { + next(error); + } + } + /** * Edit the given payment receive. * @param {Request} req * @param {Response} res * @return {Response} */ - async editPaymentReceive(req: Request, res: Response) { + async editPaymentReceive(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: paymentReceiveId } = req.params; - const { PaymentReceive } = req.models; - const paymentReceive: IPaymentReceiveOTD = matchedData(req, { - locations: ['body'], - }); + const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req); - // Retrieve the payment receive before updating. - const oldPaymentReceive: IPaymentReceive = await PaymentReceive.query() - .where('id', paymentReceiveId) - .withGraphFetched('entries') - .first(); - - await this.paymentReceiveService.editPaymentReceive( - tenantId, - paymentReceiveId, - paymentReceive, - oldPaymentReceive, - ); - return res.status(200).send({ id: paymentReceiveId }); + try { + await this.paymentReceiveService.editPaymentReceive( + tenantId, paymentReceiveId, paymentReceive, + ); + return res.status(200).send({ id: paymentReceiveId }); + } catch (error) { + next(error); + } } /** @@ -363,22 +173,22 @@ export default class PaymentReceivesController extends BaseController { * @param {Request} req * @param {Response} res */ - async deletePaymentReceive(req: Request, res: Response) { + async deletePaymentReceive(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: paymentReceiveId } = req.params; - const { PaymentReceive } = req.models; - const storedPaymentReceive = await PaymentReceive.query() - .where('id', paymentReceiveId) - .withGraphFetched('entries') - .first(); - - await this.paymentReceiveService.deletePaymentReceive( - tenantId, - paymentReceiveId, - storedPaymentReceive - ); - return res.status(200).send({ id: paymentReceiveId }); + try { + await this.paymentReceiveService.deletePaymentReceive( + tenantId, + paymentReceiveId, + ); + return res.status(200).send({ + id: paymentReceiveId, + message: 'The payment receive has been edited successfully', + }); + } catch (error) { + next(error); + } } /** @@ -387,12 +197,18 @@ export default class PaymentReceivesController extends BaseController { * @param {Request} req - * @param {Response} res - */ - async getPaymentReceive(req: Request, res: Response) { + async getPaymentReceive(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; const { id: paymentReceiveId } = req.params; - const paymentReceive = await PaymentReceiveService.getPaymentReceive( - paymentReceiveId - ); - return res.status(200).send({ paymentReceive }); + + try { + const paymentReceive = await this.paymentReceiveService.getPaymentReceive( + tenantId, paymentReceiveId + ); + return res.status(200).send({ paymentReceive }); + } catch (error) { + next(error); + } } /** @@ -401,7 +217,88 @@ export default class PaymentReceivesController extends BaseController { * @param {Response} res * @return {Response} */ - async getPaymentReceiveList(req: Request, res: Response) { - + async getPaymentReceiveList(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 { + const { + paymentReceives, + pagination, + filterMeta, + } = await this.paymentReceiveService.listPaymentReceives(tenantId, filter); + + return res.status(200).send({ + payment_receives: paymentReceives, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); + } catch (error) { + next(error); + } + } + + /** + * Handles service errors. + * @param error + * @param req + * @param res + * @param next + */ + handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }], + }); + } + if (error.errorType === 'PAYMENT_RECEIVE_NO_EXISTS') { + return res.boom.badRequest(null, { + errors: [{ type: 'PAYMENT_RECEIVE_NO_EXISTS', code: 300 }], + }); + } + if (error.errorType === 'PAYMENT_RECEIVE_NOT_EXISTS') { + return res.boom.badRequest(null, { + errors: [{ type: 'PAYMENT_RECEIVE_NOT_EXISTS', code: 300 }], + }); + } + if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') { + return res.boom.badRequest(null, { + errors: [{ type: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', code: 300 }], + }); + } + if (error.errorType === 'INVALID_PAYMENT_AMOUNT_INVALID') { + return res.boom.badRequest(null, { + errors: [{ type: 'INVALID_PAYMENT_AMOUNT', code: 300 }], + }); + } + if (error.errorType === 'INVOICES_IDS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'INVOICES_IDS_NOT_FOUND', code: 300 }], + }); + } + if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 300 }], + }); + } + if (error.errorType === 'contact_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 300 }], + }); + } + console.log(error.errorType); + } + next(error); } } diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index c6141b69f..938179819 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -1,6 +1,5 @@ import { Router, Request, Response, NextFunction } from 'express'; -import { check, param, query, matchedData } from 'express-validator'; -import { difference } from 'lodash'; +import { check, param, query } from 'express-validator'; import { raw } from 'objection'; import { Service, Inject } from 'typedi'; import BaseController from '../BaseController'; @@ -8,6 +7,7 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleInvoiceService from 'services/Sales/SalesInvoices'; import ItemsService from 'services/Items/ItemsService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { ServiceError } from 'exceptions'; import { ISaleInvoiceOTD, ISalesInvoicesFilter } from 'interfaces'; @Service() @@ -31,11 +31,8 @@ export default class SaleInvoicesController extends BaseController{ '/', this.saleInvoiceValidationSchema, this.validationResult, - asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), - // asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), - asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)), - asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)), - asyncMiddleware(this.newSaleInvoice.bind(this)) + asyncMiddleware(this.newSaleInvoice.bind(this)), + this.handleServiceErrors, ); router.post( '/:id', @@ -44,39 +41,36 @@ export default class SaleInvoicesController extends BaseController{ ...this.specificSaleInvoiceValidation, ], this.validationResult, - asyncMiddleware(this.validateInvoiceExistance.bind(this)), - asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), - // asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), - asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)), - asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)), - asyncMiddleware(this.validateEntriesIdsExistance.bind(this)), - asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)), - asyncMiddleware(this.editSaleInvoice.bind(this)) + asyncMiddleware(this.editSaleInvoice.bind(this)), + this.handleServiceErrors, ); router.delete( '/:id', this.specificSaleInvoiceValidation, this.validationResult, - asyncMiddleware(this.validateInvoiceExistance.bind(this)), - asyncMiddleware(this.deleteSaleInvoice.bind(this)) + asyncMiddleware(this.deleteSaleInvoice.bind(this)), + this.handleServiceErrors, ); router.get( '/due_invoices', this.dueSalesInvoicesListValidationSchema, asyncMiddleware(this.getDueSalesInvoice.bind(this)), + this.handleServiceErrors, ); router.get( '/:id', this.specificSaleInvoiceValidation, this.validationResult, - asyncMiddleware(this.validateInvoiceExistance.bind(this)), - asyncMiddleware(this.getSaleInvoice.bind(this)) + asyncMiddleware(this.getSaleInvoice.bind(this)), + this.handleServiceErrors, ); router.get( '/', this.saleInvoiceListValidationSchema, this.validationResult, - asyncMiddleware(this.getSalesInvoices.bind(this)) + asyncMiddleware(this.getSalesInvoices.bind(this)), + this.handleServiceErrors, + this.dynamicListService.handlerErrorsToResponse, ) return router; } @@ -133,177 +127,8 @@ export default class SaleInvoicesController extends BaseController{ */ get dueSalesInvoicesListValidationSchema() { return [ - query('customer_id').optional().isNumeric().toInt(), - ] - } - - /** - * Validate whether sale invoice customer exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateInvoiceCustomerExistance(req: Request, res: Response, next: Function) { - const saleInvoice = { ...req.body }; - const { Customer } = req.models; - - const isCustomerIDExists = await Customer.query().findById(saleInvoice.customer_id); - - if (!isCustomerIDExists) { - return res.status(400).send({ - errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }], - }); - } - next(); - } - - /** - * Validate whether sale invoice items ids esits on the storage. - * @param {Request} req - - * @param {Response} res - - * @param {Function} next - - */ - async validateInvoiceItemsIdsExistance(req: Request, res: Response, next: Function) { - const { tenantId } = req; - const saleInvoice = { ...req.body }; - const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id); - - const isItemsIdsExists = await this.itemsService.isItemsIdsExists( - tenantId, entriesItemsIds, - ); - if (isItemsIdsExists.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }], - }); - } - next(); - } - - /** - * - * Validate whether sale invoice number unqiue on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateInvoiceNumberUnique(req: Request, res: Response, next: Function) { - const { tenantId } = req; - const saleInvoice = { ...req.body }; - - const isInvoiceNoExists = await this.saleInvoiceService.isSaleInvoiceNumberExists( - tenantId, - saleInvoice.invoice_no, - req.params.id - ); - if (isInvoiceNoExists) { - return res - .status(400) - .send({ - errors: [{ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }], - }); - } - next(); - } - - /** - * Validate whether sale invoice exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateInvoiceExistance(req: Request, res: Response, next: Function) { - const { id: saleInvoiceId } = req.params; - const { tenantId } = req; - - const isSaleInvoiceExists = await this.saleInvoiceService.isSaleInvoiceExists( - tenantId, saleInvoiceId, - ); - if (!isSaleInvoiceExists) { - return res - .status(404) - .send({ errors: [{ type: 'SALE.INVOICE.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 { tenantId } = req; - const saleInvoice = { ...req.body }; - const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id); - - const isItemsIdsExists = await this.itemsService.isItemsIdsExists( - tenantId, entriesItemsIds, - ); - if (isItemsIdsExists.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }], - }); - } - next(); - } - - /** - * Validate whether the sale estimate entries IDs exist on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEntriesIdsExistance(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 storedEntries = await ItemEntry.query() - .whereIn('reference_id', [saleInvoiceId]) - .whereIn('reference_type', ['SaleInvoice']); - - const storedEntriesIds = storedEntries.map((entry) => entry.id); - const notFoundEntriesIds = difference( - entriesIds, - storedEntriesIds, - ); - if (notFoundEntriesIds.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'SALE.INVOICE.ENTRIES.IDS.NOT.FOUND', code: 500 }], - }); - } - next(); - } - - /** - * Validate the entries items that not sellable. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateNonSellableEntriesItems(req: Request, res: Response, next: Function) { - const { Item } = req.models; - const saleInvoice = { ...req.body }; - const itemsIds = saleInvoice.entries.map(e => e.item_id); - - 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) { - return res.status(400).send({ - errors: [{ type: 'NOT.SELLABLE.ITEMS', code: 600 }], - }); - } - next(); + query('customer_id').optional().isNumeric().toInt(), + ]; } /** @@ -372,14 +197,18 @@ export default class SaleInvoicesController extends BaseController{ * @param {Request} req * @param {Response} res */ - async getSaleInvoice(req: Request, res: Response) { + async getSaleInvoice(req: Request, res: Response, next: NextFunction) { const { id: saleInvoiceId } = req.params; const { tenantId } = req; - const saleInvoice = await this.saleInvoiceService.getSaleInvoiceWithEntries( - tenantId, saleInvoiceId, - ); - return res.status(200).send({ sale_invoice: saleInvoice }); + try { + const saleInvoice = await this.saleInvoiceService.getSaleInvoiceWithEntries( + tenantId, saleInvoiceId, + ); + return res.status(200).send({ sale_invoice: saleInvoice }); + } catch (error) { + next(error); + } } /** @@ -422,12 +251,20 @@ export default class SaleInvoicesController extends BaseController{ * @param {Function} next */ public async getSalesInvoices(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req.params; - const salesInvoicesFilter: ISalesInvoicesFilter = req.query; + const { tenantId } = req; + const filter: ISalesInvoicesFilter = { + filterRoles: [], + sortOrder: 'asc', + columnSortBy: 'name', + ...this.matchedQueryData(req), + }; + if (filter.stringifiedFilterRoles) { + filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); + } try { const { salesInvoices, filterMeta, pagination } = await this.saleInvoiceService.salesInvoicesList( - tenantId, salesInvoicesFilter, + tenantId, filter, ); return res.status(200).send({ sales_invoices: salesInvoices, @@ -438,4 +275,63 @@ export default class SaleInvoicesController extends BaseController{ 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 === 'INVOICE_NUMBER_NOT_UNIQUE') { + return res.boom.badRequest(null, { + errors: [{ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }], + }); + } + if (error.errorType === 'SALE_INVOICE_NOT_FOUND') { + return res.status(404).send({ + errors: [{ type: 'SALE.INVOICE.NOT.FOUND', code: 200 }] + }); + } + if (error.errorType === 'ENTRIES_ITEMS_IDS_NOT_EXISTS') { + return res.boom.badRequest(null, { + errors: [{ type: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', code: 200 }], + }); + } + if (error.errorType === 'NOT_SELLABLE_ITEMS') { + return res.boom.badRequest(null, { + errors: [{ type: 'NOT_SELLABLE_ITEMS', code: 200 }], + }); + } + if (error.errorType === 'SALE_INVOICE_NO_NOT_UNIQUE') { + return res.boom.badRequest(null, { + errors: [{ type: 'SALE_INVOICE_NO_NOT_UNIQUE', code: 200 }], + }); + } + if (error.errorType === 'ITEMS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEMS_NOT_FOUND', code: 200 }], + }); + } + if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 200 }], + }); + } + if (error.errorType === 'NOT_SELL_ABLE_ITEMS') { + return res.boom.badRequest(null, { + errors: [{ type: 'NOT_SELL_ABLE_ITEMS', code: 200 }], + }); + } + if (error.errorType === 'contact_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 200 }], + }); + } + } + console.log(error.errorType); + next(error); + } } diff --git a/server/src/api/controllers/Sales/index.ts b/server/src/api/controllers/Sales/index.ts index fe930ef3a..419fde88b 100644 --- a/server/src/api/controllers/Sales/index.ts +++ b/server/src/api/controllers/Sales/index.ts @@ -13,10 +13,10 @@ export default class SalesController { router() { const router = Router(); - // router.use('/invoices', Container.get(SalesInvoices).router()); + router.use('/invoices', Container.get(SalesInvoices).router()); router.use('/estimates', Container.get(SalesEstimates).router()); router.use('/receipts', Container.get(SalesReceipts).router()); - // router.use('/payment_receives', Container.get(PaymentReceives).router()); + router.use('/payment_receives', Container.get(PaymentReceives).router()); return router; } diff --git a/server/src/interfaces/PaymentReceive.ts b/server/src/interfaces/PaymentReceive.ts index 99e6c6f9b..44c7615f4 100644 --- a/server/src/interfaces/PaymentReceive.ts +++ b/server/src/interfaces/PaymentReceive.ts @@ -1,4 +1,43 @@ +import { IDynamicListFilterDTO } from "./DynamicFilter"; -export interface IPaymentReceive { }; -export interface IPaymentReceiveOTD { }; \ No newline at end of file +export interface IPaymentReceive { + id?: number, + customerId: number, + paymentDate: Date, + amount: number, + referenceNo: string, + depositAccountId: number, + paymentReceiveNo: string, + description: string, + entries: IPaymentReceiveEntry[], + userId: number, +}; +export interface IPaymentReceiveDTO { + customerId: number, + paymentDate: Date, + amount: number, + referenceNo: string, + depositAccountId: number, + paymentReceiveNo: string, + description: string, + entries: IPaymentReceiveEntryDTO[], +}; + +export interface IPaymentReceiveEntry { + id?: number, + paymentReceiveId: number, + invoiceId: number, + paymentAmount: number, +}; + +export interface IPaymentReceiveEntryDTO { + id?: number, + paymentReceiveId: number, + invoiceId: number, + paymentAmount: number, +}; + +export interface IPaymentReceivesFilter extends IDynamicListFilterDTO { + stringifiedFilterRoles?: string, +} \ No newline at end of file diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index e3ef2060b..f0a9949ae 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -13,6 +13,8 @@ export interface ISaleInvoiceOTD { invoiceDate: Date, dueDate: Date, referenceNo: string, + invoiceNo: string, + customerId: number, invoiceMessage: string, termsConditions: string, entries: IItemEntryDTO[], diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index 1c3847b02..a0602f8ba 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -5,7 +5,8 @@ import 'subscribers/organization'; import 'subscribers/manualJournals'; import 'subscribers/expenses'; import 'subscribers/bills'; -// import 'subscribers/saleInvoices'; +import 'subscribers/saleInvoices'; import 'subscribers/customers'; import 'subscribers/vendors'; -import 'subscribers/paymentMades'; \ No newline at end of file +import 'subscribers/paymentMades'; +import 'subscribers/paymentReceives'; diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js index db1df4b0a..ddf65f54e 100644 --- a/server/src/models/PaymentReceive.js +++ b/server/src/models/PaymentReceive.js @@ -69,4 +69,17 @@ export default class PaymentReceive extends TenantModel { } }; } + + /** + * Model defined fields. + */ + static get fields() { + return { + created_at: { + label: 'Created at', + column: 'created_at', + columnType: 'date', + }, + }; + } } diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index 4070dbcf9..d1a4c89a7 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -123,6 +123,4 @@ export default class SaleInvoice extends TenantModel { .where('id', invoiceId) [changeMethod]('payment_amount', Math.abs(amount)); } - - } diff --git a/server/src/repositories/CustomerRepository.ts b/server/src/repositories/CustomerRepository.ts index 12c41cd93..67d3e51c6 100644 --- a/server/src/repositories/CustomerRepository.ts +++ b/server/src/repositories/CustomerRepository.ts @@ -68,4 +68,36 @@ export default class CustomerRepository extends TenantRepository { .whereIn('id', customersIds) .withGraphFetched('salesInvoices'); } + + 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)); + } + + async changeDiffBalance( + vendorId: number, + amount: number, + oldAmount: number, + oldVendorId?: number, + ) { + const diffAmount = amount - oldAmount; + const asyncOpers = []; + const _oldVendorId = oldVendorId || vendorId; + + if (vendorId != _oldVendorId) { + const oldCustomerOper = this.changeBalance(_oldVendorId, (oldAmount * -1)); + const customerOper = this.changeBalance(vendorId, amount); + + asyncOpers.push(customerOper); + asyncOpers.push(oldCustomerOper); + } else { + const balanceChangeOper = this.changeBalance(vendorId, diffAmount); + asyncOpers.push(balanceChangeOper); + } + await Promise.all(asyncOpers); + } } \ No newline at end of file diff --git a/server/src/repositories/VendorRepository.ts b/server/src/repositories/VendorRepository.ts index e7856ada2..36e683053 100644 --- a/server/src/repositories/VendorRepository.ts +++ b/server/src/repositories/VendorRepository.ts @@ -68,13 +68,27 @@ export default class VendorRepository extends TenantRepository { [changeMethod]('balance', Math.abs(amount)); } - - changeDiffBalance( + + async changeDiffBalance( vendorId: number, amount: number, oldAmount: number, oldVendorId?: number, ) { + const diffAmount = amount - oldAmount; + const asyncOpers = []; + const _oldVendorId = oldVendorId || vendorId; + if (vendorId != _oldVendorId) { + const oldCustomerOper = this.changeBalance(_oldVendorId, (oldAmount * -1)); + const customerOper = this.changeBalance(vendorId, amount); + + asyncOpers.push(customerOper); + asyncOpers.push(oldCustomerOper); + } else { + const balanceChangeOper = this.changeBalance(vendorId, diffAmount); + asyncOpers.push(balanceChangeOper); + } + await Promise.all(asyncOpers); } } diff --git a/server/src/services/Items/ItemsEntriesService.ts b/server/src/services/Items/ItemsEntriesService.ts index cc2cec1d2..6f2a3eee1 100644 --- a/server/src/services/Items/ItemsEntriesService.ts +++ b/server/src/services/Items/ItemsEntriesService.ts @@ -46,15 +46,15 @@ export default class ItemsEntriesService { * @param {number} billId - * @param {IItemEntry[]} billEntries - */ - public async validateEntriesIdsExistance(tenantId: number, billId: number, modelName: string, billEntries: IItemEntryDTO[]) { + public async validateEntriesIdsExistance(tenantId: number, referenceId: number, referenceType: string, billEntries: IItemEntryDTO[]) { const { ItemEntry } = this.tenancy.models(tenantId); const entriesIds = billEntries .filter((e: IItemEntry) => e.id) .map((e: IItemEntry) => e.id); const storedEntries = await ItemEntry.query() - .whereIn('reference_id', [billId]) - .whereIn('reference_type', [modelName]); + .whereIn('reference_id', [referenceId]) + .whereIn('reference_type', [referenceType]); const storedEntriesIds = storedEntries.map((entry) => entry.id); const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index 59ee8b162..0e7494b9a 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -316,7 +316,9 @@ export default class BillPaymentsService { ...omit(billPaymentObj, ['entries']), entries: billPaymentDTO.entries, }); - await this.eventDispatcher.dispatch(events.billPayments.onEdited); + await this.eventDispatcher.dispatch(events.billPayments.onEdited, { + tenantId, billPaymentId, billPayment, oldPaymentMade, + }); this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldPaymentMade }); return billPayment; diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 1afdeb915..4013891f4 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -184,7 +184,10 @@ export default class BillsService extends SalesInvoicesCost { await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.validateBillNumberExists(tenantId, billDTO.billNumber); + // Validate items IDs existance. await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries); + + // Validate non-purchasable items. await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); const bill = await Bill.query() @@ -232,7 +235,7 @@ export default class BillsService extends SalesInvoicesCost { this.logger.info('[bill] trying to edit bill.', { tenantId, billId }); const oldBill = await this.getBillOrThrowError(tenantId, billId); - const billObj = this.billDTOToModel(tenantId, billDTO, oldBill); + const billObj = await this.billDTOToModel(tenantId, billDTO, oldBill); await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId); @@ -242,7 +245,7 @@ export default class BillsService extends SalesInvoicesCost { await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); // Update the bill transaction. - const bill = await Bill.query().upsertGraph({ + const bill = await Bill.query().upsertGraphAndFetch({ id: billId, ...omit(billObj, ['entries', 'invLotNumber']), diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index c7f568be4..d25691afe 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -1,4 +1,4 @@ -import { omit, sumBy, chain } from 'lodash'; +import { omit, sumBy, chain, difference } from 'lodash'; import moment from 'moment'; import { Service, Inject } from 'typedi'; import { @@ -6,18 +6,35 @@ import { EventDispatcherInterface, } from 'decorators/eventDispatcher'; import events from 'subscribers/events'; -import { IPaymentReceiveOTD } from 'interfaces'; +import { + IAccount, + IFilterMeta, + IPaginationMeta, + IPaymentReceive, + IPaymentReceiveDTO, + IPaymentReceiveEntryDTO, + IPaymentReceivesFilter, + ISaleInvoice +} from 'interfaces'; import AccountsService from 'services/Accounts/AccountsService'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; import JournalPosterService from 'services/Sales/JournalPosterService'; -import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; -import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository'; -import CustomerRepository from 'repositories/CustomerRepository'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; +import { ServiceError } from 'exceptions'; +import CustomersService from 'services/Contacts/CustomersService'; +import ItemsEntriesService from 'services/Items/ItemsEntriesService'; +const ERRORS = { + PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', + PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS', + DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', + DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', + INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT', + INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND' +}; /** * Payment receive service. * @service @@ -27,6 +44,12 @@ export default class PaymentReceiveService { @Inject() accountsService: AccountsService; + @Inject() + customersService: CustomersService; + + @Inject() + itemsEntries: ItemsEntriesService; + @Inject() tenancy: TenancyService; @@ -42,112 +65,88 @@ export default class PaymentReceiveService { @EventDispatcher() eventDispatcher: EventDispatcherInterface; - - /** * Validates the payment receive number existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {number} tenantId - + * @param {string} paymentReceiveNo - */ - 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 }], + async validatePaymentReceiveNoExistance( + tenantId: number, + paymentReceiveNo: string, + notPaymentReceiveId?: number + ): Promise { + const { PaymentReceive } = this.tenancy.models(tenantId); + const paymentReceive = await PaymentReceive.query().findOne('payment_receive_no', paymentReceiveNo) + .onBuild((builder) => { + if (notPaymentReceiveId) { + builder.whereNot('id', notPaymentReceiveId); + } }); + + if (paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_EXISTS); } - next(); } /** * Validates the payment receive existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {number} tenantId - + * @param {number} paymentReceiveId - */ - 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 }], - }); + async getPaymentReceiveOrThrowError( + tenantId: number, + paymentReceiveId: number + ): Promise { + const { PaymentReceive } = this.tenancy.models(tenantId); + const paymentReceive = await PaymentReceive.query() + .withGraphFetched('entries') + .findById(paymentReceiveId); + + if (!paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); } - next(); + return paymentReceive; } /** * Validate the deposit account id existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {number} tenantId - + * @param {number} depositAccountId - */ - 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 }], - }); + async getDepositAccountOrThrowError(tenantId: number, depositAccountId: number): Promise { + const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + + const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset'); + const depositAccount = await accountRepository.findById(depositAccountId); + + const currentAssetTypesIds = currentAssetTypes.map(type => type.id); + + if (!depositAccount) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); } - 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 }], - }); + if (currentAssetTypesIds.indexOf(depositAccount.accountTypeId) === -1) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE); } - next(); + return depositAccount; } - + /** * Validates the invoices IDs existance. - * @param {Request} req - - * @param {Response} res - - * @param {Function} next - + * @param {number} tenantId - + * @param {} paymentReceiveEntries - */ - async validateInvoicesIDs(req: Request, res: Response, next: Function) { - const paymentReceive = { ...req.body }; - const { tenantId } = req; - const invoicesIds = paymentReceive.entries - .map((e) => e.invoice_id); + async validateInvoicesIDsExistance(tenantId: number, paymentReceiveEntries: any): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const invoicesIds = paymentReceiveEntries.map((e) => e.invoiceId); + const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds); + const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id); + + const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds); - 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 }], - }); + throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND); } - next(); } /** @@ -156,69 +155,30 @@ export default class PaymentReceiveService { * @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); + async validateInvoicesPaymentsAmount(tenantId: number, paymentReceiveEntries: IPaymentReceiveEntryDTO[]) { + const { SaleInvoice } = this.tenancy.models(tenantId); + const invoicesIds = paymentReceiveEntries.map((e: IPaymentReceiveEntryDTO) => e.invoiceId); - const storedInvoices = await SaleInvoice.query() - .whereIn('id', invoicesIds); + const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds); const storedInvoicesMap = new Map( - storedInvoices.map((invoice) => [invoice.id, invoice]) + storedInvoices.map((invoice: ISaleInvoice) => [invoice.id, invoice]) ); const hasWrongPaymentAmount: any[] = []; - req.body.entries.forEach((entry, index: number) => { - const entryInvoice = storedInvoicesMap.get(entry.invoice_id); + paymentReceiveEntries.forEach((entry: IPaymentReceiveEntryDTO, index: number) => { + const entryInvoice = storedInvoicesMap.get(entry.invoiceId); const { dueAmount } = entryInvoice; - if (dueAmount < entry.payment_amount) { + if (dueAmount < entry.paymentAmount) { hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); } }); if (hasWrongPaymentAmount.length > 0) { - return res.status(400).send({ - errors: [ - { - type: 'INVOICE.PAYMENT.AMOUNT', - code: 200, - indexes: hasWrongPaymentAmount, - }, - ], - }); + throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT); } - 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. @@ -226,61 +186,42 @@ export default class PaymentReceiveService { * @param {number} tenantId - Tenant id. * @param {IPaymentReceive} paymentReceive */ - public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) { - const { - PaymentReceive, - PaymentReceiveEntry, - SaleInvoice, - Customer, - } = this.tenancy.models(tenantId); + public async createPaymentReceive(tenantId: number, paymentReceiveDTO: IPaymentReceiveDTO) { + const { PaymentReceive } = this.tenancy.models(tenantId); + const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); - const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); + // Validate payment receive number uniquiness. + await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo); + + // Validate customer existance. + await this.customersService.getCustomerByIdOrThrowError(tenantId, paymentReceiveDTO.customerId); + + // Validate the deposit account existance and type. + await this.getDepositAccountOrThrowError(tenantId, paymentReceiveDTO.depositAccountId); + + // Validate payment receive invoices IDs existance. + await this.validateInvoicesIDsExistance(tenantId, paymentReceiveDTO.entries); + + // Validate invoice payment amount. + await this.validateInvoicesPaymentsAmount(tenantId, paymentReceiveDTO.entries); this.logger.info('[payment_receive] inserting to the storage.'); - const storedPaymentReceive = await PaymentReceive.query() - .insertGraph({ + const paymentReceive = await PaymentReceive.query() + .insertGraphAndFetch({ amount: paymentAmount, - ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), - entries: paymentReceive.entries.map((entry) => ({ ...entry })), + ...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']), + + entries: paymentReceiveDTO.entries.map((entry) => ({ + ...omit(entry, ['id']), + })), }); - const storeOpers: Array = []; - this.logger.info('[payment_receive] inserting associated entries to the storage.'); - paymentReceive.entries.forEach((entry: any) => { - const oper = PaymentReceiveEntry.query() - .insert({ - payment_receive_id: storedPaymentReceive.id, - ...entry, - }); - - this.logger.info('[payment_receive] increment the sale invoice payment amount.'); - // Increment the invoice payment amount. - const invoice = SaleInvoice.query() - .where('id', entry.invoice_id) - .increment('payment_amount', entry.payment_amount); - - storeOpers.push(oper); - storeOpers.push(invoice); + await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated, { + tenantId, paymentReceive, paymentReceiveId: paymentReceive.id, }); + this.logger.info('[payment_receive] updated successfully.', { tenantId, paymentReceive }); - this.logger.info('[payment_receive] decrementing customer balance.'); - const customerIncrementOper = Customer.decrementBalance( - paymentReceive.customer_id, - paymentAmount, - ); - // Records the sale invoice journal transactions. - const recordJournalTransactions = this.recordPaymentReceiveJournalEntries(tenantId,{ - id: storedPaymentReceive.id, - ...paymentReceive, - }); - await Promise.all([ - ...storeOpers, - customerIncrementOper, - recordJournalTransactions, - ]); - await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated); - - return storedPaymentReceive; + return paymentReceive; } /** @@ -297,84 +238,52 @@ export default class PaymentReceiveService { * @param {number} tenantId - * @param {Integer} paymentReceiveId - * @param {IPaymentReceive} paymentReceive - - * @param {IPaymentReceive} oldPaymentReceive - */ public async editPaymentReceive( tenantId: number, paymentReceiveId: number, - paymentReceive: any, - oldPaymentReceive: any + paymentReceiveDTO: any, ) { const { PaymentReceive } = this.tenancy.models(tenantId); + const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + + this.logger.info('[payment_receive] trying to edit payment receive.', { tenantId, paymentReceiveId, paymentReceiveDTO }); + + // Validate the payment receive existance. + const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId); + + // Validate payment receive number uniquiness. + await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo, paymentReceiveId); + + // Validate the deposit account existance and type. + this.getDepositAccountOrThrowError(tenantId, paymentReceiveDTO.depositAccountId); + + // Validate customer existance. + await this.customersService.getCustomerByIdOrThrowError(tenantId, paymentReceiveDTO.customerId); + + // Validate the entries ids existance on payment receive type. + await this.itemsEntries.validateEntriesIdsExistance( + tenantId, paymentReceiveId, 'PaymentReceive', paymentReceiveDTO.entries + ); + // Validate payment receive invoices IDs existance. + await this.validateInvoicesIDsExistance(tenantId, paymentReceiveDTO.entries); + + // Validate invoice payment amount. + await this.validateInvoicesPaymentsAmount(tenantId, paymentReceiveDTO.entries); - const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); // Update the payment receive transaction. - const updatePaymentReceive = await PaymentReceive.query() - .where('id', paymentReceiveId) - .update({ + const paymentReceive = await PaymentReceive.query() + .upsertGraphAndFetch({ + id: paymentReceiveId, amount: paymentAmount, - ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), + ...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']), + entries: paymentReceiveDTO.entries, }); - const opers = []; - const entriesIds = paymentReceive.entries.filter((i: any) => i.id); - const entriesShouldInsert = paymentReceive.entries.filter((i: any) => !i.id); - // Detarmines which entries ids should be deleted. - const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( - oldPaymentReceive.entries, - entriesIds - ); - if (entriesIdsShouldDelete.length > 0) { - // Deletes the given payment receive entries. - const deleteOper = PaymentReceiveEntryRepository.deleteBulk( - entriesIdsShouldDelete - ); - opers.push(deleteOper); - } - // Entries that should be updated to the storage. - if (entriesIds.length > 0) { - const updateOper = PaymentReceiveEntryRepository.updateBulk(entriesIds); - opers.push(updateOper); - } - // Entries should insert to the storage. - if (entriesShouldInsert.length > 0) { - const insertOper = PaymentReceiveEntryRepository.insertBulk( - entriesShouldInsert, - paymentReceiveId - ); - opers.push(insertOper); - } - // Re-write the journal transactions of the given payment receive. - const recordJournalTransactions = this.recordPaymentReceiveJournalEntries( - tenantId, - { - id: oldPaymentReceive.id, - ...paymentReceive, - }, - paymentReceiveId, - ); - // Increment/decrement the customer balance after calc the diff - // between old and new value. - const changeCustomerBalance = CustomerRepository.changeDiffBalance( - paymentReceive.customer_id, - oldPaymentReceive.customerId, - paymentAmount * -1, - oldPaymentReceive.amount * -1, - ); - // Change the difference between the old and new invoice payment amount. - const diffInvoicePaymentAmount = this.saveChangeInvoicePaymentAmount( - tenantId, - oldPaymentReceive.entries, - paymentReceive.entries - ); - // Await the async operations. - await Promise.all([ - ...opers, - recordJournalTransactions, - changeCustomerBalance, - diffInvoicePaymentAmount, - ]); - await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited); + await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited, { + tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive + }); + this.logger.info('[payment_receive] upserted successfully.', { tenantId, paymentReceiveId }); } /** @@ -392,43 +301,20 @@ export default class PaymentReceiveService { * @param {IPaymentReceive} paymentReceive - Payment receive object. */ async deletePaymentReceive(tenantId: number, paymentReceiveId: number, paymentReceive: any) { - const { PaymentReceive, PaymentReceiveEntry, Customer } = this.tenancy.models(tenantId); + const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models(tenantId); - // Deletes the payment receive transaction. - await PaymentReceive.query() - .where('id', paymentReceiveId) - .delete(); + const oldPaymentReceive = this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId); // Deletes the payment receive associated entries. - await PaymentReceiveEntry.query() - .where('payment_receive_id', paymentReceiveId) - .delete(); + await PaymentReceiveEntry.query().where('payment_receive_id', paymentReceiveId).delete(); - // Delete all associated journal transactions to payment receive transaction. - const deleteTransactionsOper = this.journalService.deleteJournalTransactions( - tenantId, - paymentReceiveId, - 'PaymentReceive' - ); - // Revert the customer balance. - const revertCustomerBalance = Customer.incrementBalance( - paymentReceive.customerId, - paymentReceive.amount - ); - // Revert the invoices payments amount. - const revertInvoicesPaymentAmount = this.revertInvoicePaymentAmount( - tenantId, - paymentReceive.entries.map((entry: any) => ({ - invoiceId: entry.invoiceId, - revertAmount: entry.paymentAmount, - })) - ); - await Promise.all([ - deleteTransactionsOper, - revertCustomerBalance, - revertInvoicesPaymentAmount, - ]); - await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted); + // Deletes the payment receive transaction. + await PaymentReceive.query().where('id', paymentReceiveId).delete(); + + await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted, { + tenantId, paymentReceiveId, oldPaymentReceive, + }); + this.logger.info('[payment_receive] deleted successfully.', { tenantId, paymentReceiveId }); } /** @@ -439,9 +325,12 @@ export default class PaymentReceiveService { public async getPaymentReceive(tenantId: number, paymentReceiveId: number) { const { PaymentReceive } = this.tenancy.models(tenantId); const paymentReceive = await PaymentReceive.query() - .where('id', paymentReceiveId) - .withGraphFetched('entries.invoice') - .first(); + .findById(paymentReceiveId) + .withGraphFetched('entries.invoice'); + + if (!paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); + } return paymentReceive; } @@ -450,7 +339,10 @@ export default class PaymentReceiveService { * @param {number} tenantId * @param {IPaymentReceivesFilter} paymentReceivesFilter */ - public async listPaymentReceives(tenantId: number, paymentReceivesFilter: IPaymentReceivesFilter) { + public async listPaymentReceives( + tenantId: number, + paymentReceivesFilter: IPaymentReceivesFilter, + ): Promise<{ paymentReceives: IPaymentReceive[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { const { PaymentReceive } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter); @@ -481,41 +373,6 @@ export default class PaymentReceiveService { .first(); } - /** - * Detarmines whether the payment receive exists on the storage. - * @param {number} tenantId - Tenant id. - * @param {Integer} paymentReceiveId - */ - async isPaymentReceiveExists(tenantId: number, paymentReceiveId: number) { - const { PaymentReceive } = this.tenancy.models(tenantId); - const paymentReceives = await PaymentReceive.query() - .where('id', paymentReceiveId); - return paymentReceives.length > 0; - } - - /** - * Detarmines the payment receive number existance. - * @async - * @param {number} tenantId - Tenant id. - * @param {Integer} paymentReceiveNumber - Payment receive number. - * @param {Integer} paymentReceiveId - Payment receive id. - */ - async isPaymentReceiveNoExists( - tenantId: number, - paymentReceiveNumber: string|number, - paymentReceiveId: number - ) { - const { PaymentReceive } = this.tenancy.models(tenantId); - const paymentReceives = await PaymentReceive.query() - .where('payment_receive_no', paymentReceiveNumber) - .onBuild((query) => { - if (paymentReceiveId) { - query.whereNot('id', paymentReceiveId); - } - }); - return paymentReceives.length > 0; - } - /** * Records payment receive journal transactions. * @@ -581,26 +438,6 @@ export default class PaymentReceiveService { ]); } - /** - * Revert the payment amount of the given invoices ids. - * @async - * @param {number} tenantId - Tenant id. - * @param {Array} revertInvoices - * @return {Promise} - */ - private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) { - const { SaleInvoice } = this.tenancy.models(tenantId); - const opers: Promise[] = []; - - revertInvoices.forEach((revertInvoice) => { - const { revertAmount, invoiceId } = revertInvoice; - const oper = SaleInvoice.query() - .where('id', invoiceId) - .decrement('payment_amount', revertAmount); - opers.push(oper); - }); - await Promise.all(opers); - } /** * Saves difference changing between old and new invoice payment amount. @@ -610,34 +447,35 @@ export default class PaymentReceiveService { * @param {Array} newPaymentReceiveEntries * @return */ - private async saveChangeInvoicePaymentAmount( + public async saveChangeInvoicePaymentAmount( tenantId: number, - paymentReceiveEntries: [], - newPaymentReceiveEntries: [], - ) { + newPaymentReceiveEntries: IPaymentReceiveEntryDTO[], + oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[], + ): Promise { const { SaleInvoice } = this.tenancy.models(tenantId); const opers: Promise[] = []; - const newEntriesTable = chain(newPaymentReceiveEntries) - .groupBy('invoice_id') - .mapValues((group) => (sumBy(group, 'payment_amount') || 0) * -1) - .value(); - const diffEntries = chain(paymentReceiveEntries) + const oldEntriesTable = chain(oldPaymentReceiveEntries) .groupBy('invoiceId') .mapValues((group) => (sumBy(group, 'paymentAmount') || 0) * -1) - .mapValues((value, key) => value - (newEntriesTable[key] || 0)) - .mapValues((value, key) => ({ invoice_id: key, payment_amount: value })) - .filter((entry) => entry.payment_amount != 0) + .value(); + + const diffEntries = chain(newPaymentReceiveEntries) + .groupBy('invoiceId') + .mapValues((group) => (sumBy(group, 'paymentAmount') || 0)) + .mapValues((value, key) => value - (oldEntriesTable[key] || 0)) + .mapValues((value, key) => ({ invoiceId: key, paymentAmount: value })) + .filter((entry) => entry.paymentAmount != 0) .values() .value(); diffEntries.forEach((diffEntry: any) => { const oper = SaleInvoice.changePaymentAmount( - diffEntry.invoice_id, - diffEntry.payment_amount + diffEntry.invoiceId, + diffEntry.paymentAmount ); opers.push(oper); }); - return Promise.all([ ...opers ]); + await Promise.all([ ...opers ]); } } diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 160cb20ed..e42ca0650 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -14,7 +14,6 @@ import { } 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'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -22,14 +21,17 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; +import ItemsEntriesService from 'services/Items/ItemsEntriesService'; +import CustomersService from 'services/Contacts/CustomersService'; const ERRORS = { + INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE', 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 @@ -44,7 +46,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { inventoryService: InventoryService; @Inject() - itemsEntriesService: HasItemsEntries; + itemsEntriesService: ItemsEntriesService; @Inject('logger') logger: any; @@ -58,78 +60,50 @@ export default class SaleInvoicesService extends SalesInvoicesCost { @Inject() itemsService: ItemsService; + @Inject() + customersService: CustomersService; + /** - * Retrieve sale invoice or throw not found error. - * @param {number} tenantId - * @param {number} saleInvoiceId + * + * Validate whether sale invoice number unqiue on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next */ - private async getSaleInvoiceOrThrowError(tenantId: number, saleInvoiceId: number): Promise { + async validateInvoiceNumberUnique(tenantId: number, invoiceNumber: string, notInvoiceId?: number) { const { SaleInvoice } = this.tenancy.models(tenantId); - const saleInvoice = await SaleInvoice.query().where('id', saleInvoiceId); + this.logger.info('[sale_invoice] validating sale invoice number existance.', { tenantId, invoiceNumber }); + const saleInvoice = await SaleInvoice.query() + .findOne('invoice_no', invoiceNumber) + .onBuild((builder) => { + if (notInvoiceId) { + builder.whereNot('id', notInvoiceId); + } + }); + + if (saleInvoice) { + this.logger.info('[sale_invoice] sale invoice number not unique.', { tenantId, invoiceNumber }); + throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE) + } + } + + /** + * Validate whether sale invoice exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + async getInvoiceOrThrowError(tenantId: number, saleInvoiceId: number) { + const { SaleInvoice } = this.tenancy.models(tenantId); + const saleInvoice = await SaleInvoice.query().findById(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. @@ -138,102 +112,100 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {ISaleInvoice} saleInvoiceDTO - * @return {ISaleInvoice} */ - public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) { - const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId); + public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD): Promise { + const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); - const invLotNumber = await this.inventoryService.nextLotNumber(tenantId); - - const saleInvoice: ISaleInvoice = { + const invLotNumber = 1; + + const saleInvoiceObj: ISaleInvoice = { ...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']), balance, paymentAmount: 0, - invLotNumber, + // invLotNumber, }; - await this.validateSaleInvoiceNoUniquiness(tenantId, saleInvoiceDTO.invoiceNo); - await this.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries); + // Validate customer existance. + await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId); + + // Validate sale invoice number uniquiness. + await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo); + + // Validate items ids existance. + await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries); + await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries); this.logger.info('[sale_invoice] inserting sale invoice to the storage.'); - const storedInvoice = await SaleInvoice.query() - .insert({ - ...omit(saleInvoice, ['entries']), - }); - const opers: Array = []; + const saleInvoice = await SaleInvoice.query() + .insertGraph({ + ...omit(saleInvoiceObj, ['entries']), - this.logger.info('[sale_invoice] inserting sale invoice entries to the storage.'); - saleInvoice.entries.forEach((entry: any) => { - const oper = ItemEntry.query() - .insertAndFetch({ + entries: saleInvoiceObj.entries.map((entry) => ({ reference_type: 'SaleInvoice', - reference_id: storedInvoice.id, ...omit(entry, ['amount', 'id']), - }).then((itemEntry) => { - entry.id = itemEntry.id; - }); - opers.push(oper); + })) + }); + + await this.eventDispatcher.dispatch(events.saleInvoice.onCreated, { + tenantId, saleInvoice, saleInvoiceId: saleInvoice.id, }); + this.logger.info('[sale_invoice] successfully inserted.', { tenantId, saleInvoice }); - this.logger.info('[sale_invoice] trying to increment the customer balance.'); - // Increment the customer balance after deliver the sale invoice. - const incrementOper = Customer.incrementBalance( - saleInvoice.customer_id, - balance, - ); - - // Await all async operations. - await Promise.all([ ...opers, incrementOper ]); - - // Records the inventory transactions for inventory items. - await this.recordInventoryTranscactions(tenantId, saleInvoice, storedInvoice.id); - - // Schedule sale invoice re-compute based on the item cost - // method and starting date. - await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id); - - await this.eventDispatcher.dispatch(events.saleInvoice.onCreated); - - return storedInvoice; + return saleInvoice; } /** * Edit the given sale invoice. * @async - * @param {number} tenantId - + * @param {number} tenantId - * @param {Number} saleInvoiceId - * @param {ISaleInvoice} saleInvoice - */ - public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) { - const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId); + public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any): Promise { + const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); - const oldSaleInvoice = await SaleInvoice.query() - .where('id', saleInvoiceId) - .first(); + const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId); - const saleInvoice = { + const saleInvoiceObj = { ...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']), balance, - invLotNumber: oldSaleInvoice.invLotNumber, + // invLotNumber: oldSaleInvoice.invLotNumber, }; - this.logger.info('[sale_invoice] trying to update sale invoice.'); - const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.query() - .where('id', saleInvoiceId) - .update({ - ...omit(saleInvoice, ['entries', 'invLotNumber']), - }); - // Fetches the sale invoice items entries. - const storedEntries = await ItemEntry.query() - .where('reference_id', saleInvoiceId) - .where('reference_type', 'SaleInvoice'); + // Validate customer existance. + await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId); + + // Validate sale invoice number uniquiness. + await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo, saleInvoiceId); + + // Validate items ids existance. + await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries); + + // Validate non-sellable entries items. + await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries); + + // Validate the items entries existance. + await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, saleInvoiceId, 'SaleInvoice', saleInvoiceDTO.entries); + + this.logger.info('[sale_invoice] trying to update sale invoice.'); + const saleInvoice: ISaleInvoice = await SaleInvoice.query() + .upsertGraph({ + id: saleInvoiceId, + ...omit(saleInvoiceObj, ['entries', 'invLotNumber']), + + entries: saleInvoiceObj.entries.map((entry) => ({ + reference_type: 'SaleInvoice', + ...omit(entry, ['amount']), + })) + }); - // Patch update the sale invoice items entries. - await this.itemsEntriesService.patchItemsEntries( - tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId, - ); // Triggers `onSaleInvoiceEdited` event. - await this.eventDispatcher.dispatch(events.saleInvoice.onEdited); + await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, { + saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId, + }); + + return saleInvoice; } /** @@ -242,16 +214,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @async * @param {Number} saleInvoiceId - The given sale invoice id. */ - public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) { - const { - SaleInvoice, - ItemEntry, - Customer, - InventoryTransaction, - AccountTransaction, - } = this.tenancy.models(tenantId); + public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number): Promise { + const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId); - const oldSaleInvoice = await this.getSaleInvoiceOrThrowError(tenantId, saleInvoiceId); + const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId); this.logger.info('[sale_invoice] delete sale invoice with entries.'); await SaleInvoice.query().where('id', saleInvoiceId).delete(); @@ -260,42 +226,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost { .where('reference_type', 'SaleInvoice') .delete(); - this.logger.info('[sale_invoice] revert the customer balance.'); - const revertCustomerBalanceOper = Customer.changeBalance( - oldSaleInvoice.customerId, - oldSaleInvoice.balance * -1, - ); - const invoiceTransactions = await AccountTransaction.query() - .whereIn('reference_type', ['SaleInvoice']) - .where('reference_id', saleInvoiceId) - .withGraphFetched('account.type'); - - const journal = new JournalPoster(tenantId); - - journal.loadEntries(invoiceTransactions); - journal.removeEntries(); - - const inventoryTransactions = await InventoryTransaction.query() - .where('transaction_type', 'SaleInvoice') - .where('transaction_id', saleInvoiceId); - - // Revert inventory transactions. - const revertInventoryTransactionsOper = this.revertInventoryTransactions( - tenantId, - inventoryTransactions, - ); - // Await all async operations. - await Promise.all([ - journal.deleteEntries(), - journal.saveBalance(), - revertCustomerBalanceOper, - revertInventoryTransactionsOper, - ]); - // 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); + await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted, { + tenantId, oldSaleInvoice, + }); } /** @@ -378,60 +311,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost { .first(); } - /** - * Detarmines the sale invoice number id exists on the storage. - * @param {Integer} saleInvoiceId - * @return {Boolean} - */ - async isSaleInvoiceExists(tenantId: number, saleInvoiceId: number) { - const { SaleInvoice } = this.tenancy.models(tenantId); - const foundSaleInvoice = await SaleInvoice.query() - .where('id', saleInvoiceId); - - return foundSaleInvoice.length !== 0; - } - - /** - * Detarmines the sale invoice number exists on the storage. - * @async - * @param {Number|String} saleInvoiceNumber - * @param {Number} saleInvoiceId - * @return {Boolean} - */ - async isSaleInvoiceNumberExists( - tenantId: number, - saleInvoiceNumber: string|number, - saleInvoiceId: number - ) { - const { SaleInvoice } = this.tenancy.models(tenantId); - const foundSaleInvoice = await SaleInvoice.query() - .onBuild((query: any) => { - query.where('invoice_no', saleInvoiceNumber); - if (saleInvoiceId) { - query.whereNot('id', saleInvoiceId); - } - return query; - }); - return (foundSaleInvoice.length !== 0); - } - - /** - * Detarmine the invoices IDs in bulk and returns the not found ones. - * @param {Array} invoicesIds - * @return {Array} - */ - async isInvoicesExist(tenantId: number, invoicesIds: Array) { - const { SaleInvoice } = this.tenancy.models(tenantId); - const storedInvoices = await SaleInvoice.query() - .onBuild((builder: any) => { - builder.whereIn('id', invoicesIds); - return builder; - }); - const storedInvoicesIds = storedInvoices.map((i) => i.id); - const notStoredInvoices = difference(invoicesIds, storedInvoicesIds); - return notStoredInvoices; - } - /** * Schedules compute sale invoice items cost based on each item * cost method. @@ -501,8 +380,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {Response} res * @param {NextFunction} next */ - public async salesInvoicesList(tenantId: number, salesInvoicesFilter: ISalesInvoicesFilter): - Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + public async salesInvoicesList( + tenantId: number, + salesInvoicesFilter: ISalesInvoicesFilter + ): Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { const { SaleInvoice } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter); @@ -515,6 +396,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost { salesInvoicesFilter.page - 1, salesInvoicesFilter.pageSize, ); - return { salesInvoices: results, pagination, filterMeta: dynamicFilter.getResponseMeta() }; + return { + salesInvoices: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; } } diff --git a/server/src/subscribers/bills.ts b/server/src/subscribers/bills.ts index 4ee297266..aa0c1f767 100644 --- a/server/src/subscribers/bills.ts +++ b/server/src/subscribers/bills.ts @@ -41,7 +41,7 @@ export default class BillSubscriber { 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); + await this.billsService.recordJournalTransactions(tenantId, bill, billId); } /** @@ -66,18 +66,20 @@ export default class BillSubscriber { await this.journalPosterService.revertJournalTransactions(tenantId, billId, 'Bill'); } - + /** + * Handles vendor balance difference change. + */ @On(events.bills.onEdited) - async handleCustomerBalanceDiffChange({ tenantId, billId, oldBill, bill }) { + async handleVendorBalanceDiffChange({ 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, + oldBill.vendorId, ); } } \ No newline at end of file diff --git a/server/src/subscribers/paymentMades.ts b/server/src/subscribers/paymentMades.ts index e4b98ec36..c0df884d7 100644 --- a/server/src/subscribers/paymentMades.ts +++ b/server/src/subscribers/paymentMades.ts @@ -20,7 +20,7 @@ export default class PaymentMadesSubscriber { * Handles bills payment amount increment once payment made created. */ @On(events.billPayments.onCreated) - async handleBillsIncrement({ tenantId, billPayment, billPaymentId }) { + async handleBillsIncrementPaymentAmount({ tenantId, billPayment, billPaymentId }) { const { Bill } = this.tenancy.models(tenantId); const storeOpers = []; diff --git a/server/src/subscribers/paymentReceives.ts b/server/src/subscribers/paymentReceives.ts new file mode 100644 index 000000000..049bf2696 --- /dev/null +++ b/server/src/subscribers/paymentReceives.ts @@ -0,0 +1,88 @@ +import { Container, Inject, Service } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; +import PaymentReceiveService from 'services/Sales/PaymentsReceives'; + +@EventSubscriber() +export default class PaymentReceivesSubscriber { + tenancy: TenancyService; + logger: any; + paymentReceivesService: PaymentReceiveService; + + constructor() { + this.tenancy = Container.get(TenancyService); + this.logger = Container.get('logger'); + this.paymentReceivesService = Container.get(PaymentReceiveService); + } + + /** + * Handle customer balance decrement once payment receive created. + */ + @On(events.paymentReceipts.onCreated) + async handleCustomerBalanceDecrement({ tenantId, paymentReceiveId, paymentReceive }) { + const { customerRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[payment_receive] trying to decrement customer balance.'); + await customerRepository.changeBalance(paymentReceive.customerId, paymentReceive.amount * -1); + } + + /** + * Handle sale invoice increment/decrement payment amount once created, edited or deleted. + */ + @On(events.paymentReceipts.onCreated) + @On(events.paymentReceipts.onEdited) + async handleInvoiceIncrementPaymentAmount({ tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive }) { + + this.logger.info('[payment_receive] trying to change sale invoice payment amount.', { tenantId }); + await this.paymentReceivesService.saveChangeInvoicePaymentAmount( + tenantId, + paymentReceive.entries, + oldPaymentReceive?.entries || null, + ); + } + + /** + * Handle sale invoice diff payment amount change on payment receive edited. + */ + @On(events.paymentReceipts.onEdited) + async handleInvoiceDecrementPaymentAmount({ tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive }) { + this.logger.info('[payment_receive] trying to decrement sale invoice payment amount.'); + + await this.paymentReceivesService.saveChangeInvoicePaymentAmount( + tenantId, + paymentReceive.entries.map((entry) => ({ + ...entry, + paymentAmount: entry.paymentAmount * -1, + })), + ); + } + + /** + * Handle customer balance increment once payment receive deleted. + */ + @On(events.paymentReceipts.onDeleted) + async handleCustomerBalanceIncrement({ tenantId, paymentReceiveId, oldPaymentReceive }) { + const { customerRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[payment_receive] trying to increment customer balance.'); + await customerRepository.changeBalance(oldPaymentReceive.customerId, oldPaymentReceive.amount); + } + + /** + * Handles customer balance diff balance change once payment receive edited. + */ + @On(events.paymentReceipts.onEdited) + async handleCustomerBalanceDiffChange({ tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive }) { + const { customerRepository } = this.tenancy.repositories(tenantId); + + console.log(paymentReceive, oldPaymentReceive, 'XX'); + + await customerRepository.changeDiffBalance( + paymentReceive.customerId, + paymentReceive.amount * -1, + oldPaymentReceive.amount * -1, + oldPaymentReceive.customerId, + ); + } +} \ No newline at end of file diff --git a/server/src/subscribers/saleInvoices.ts b/server/src/subscribers/saleInvoices.ts index bf5f1da4e..5c7ddb915 100644 --- a/server/src/subscribers/saleInvoices.ts +++ b/server/src/subscribers/saleInvoices.ts @@ -1,22 +1,56 @@ import { Container } from 'typedi'; import { On, EventSubscriber } from "event-dispatch"; import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; @EventSubscriber() export default class SaleInvoiceSubscriber { + logger: any; + tenancy: TenancyService; + constructor() { + this.logger = Container.get('logger'); + this.tenancy = Container.get(TenancyService); + } + + /** + * Handles customer balance increment once sale invoice created. + */ @On(events.saleInvoice.onCreated) - public onSaleInvoiceCreated(payload) { + public async handleCustomerBalanceIncrement({ tenantId, saleInvoice, saleInvoiceId }) { + const { customerRepository } = this.tenancy.repositories(tenantId); + this.logger.info('[sale_invoice] trying to increment customer balance.', { tenantId }); + await customerRepository.changeBalance(saleInvoice.customerId, saleInvoice.balance); } + /** + * Handles customer balance diff balnace change once sale invoice edited. + */ @On(events.saleInvoice.onEdited) - public onSaleInvoiceEdited(payload) { + public async onSaleInvoiceEdited({ tenantId, saleInvoice, oldSaleInvoice, saleInvoiceId }) { + const { customerRepository } = this.tenancy.repositories(tenantId); + this.logger.info('[sale_invoice] trying to change diff customer balance.', { tenantId }); + await customerRepository.changeDiffBalance( + saleInvoice.customerId, + saleInvoice.balance, + oldSaleInvoice.balance, + oldSaleInvoice.customerId, + ) } + /** + * Handles customer balance decrement once sale invoice deleted. + */ @On(events.saleInvoice.onDeleted) - public onSaleInvoiceDeleted(payload) { + public async handleCustomerBalanceDecrement({ tenantId, saleInvoiceId, oldSaleInvoice }) { + const { customerRepository } = this.tenancy.repositories(tenantId); + this.logger.info('[sale_invoice] trying to decrement customer balance.', { tenantId }); + await customerRepository.changeBalance( + oldSaleInvoice.customerId, + oldSaleInvoice.balance * -1, + ); } } \ No newline at end of file