From 26c6ca9e36f6b889bbb46bfbddb3ff46142545a4 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 10 Aug 2023 20:29:39 +0200 Subject: [PATCH] refactor: split the services to multiple service classes (#202) --- e2e/items.spec.ts | 7 + .../src/api/controllers/Purchases/Bills.ts | 61 +- .../controllers/Purchases/BillsPayments.ts | 75 +- .../api/controllers/Sales/PaymentReceives.ts | 121 +-- .../api/controllers/Sales/SalesEstimates.ts | 126 +-- .../api/controllers/Sales/SalesInvoices.ts | 124 +-- .../api/controllers/Sales/SalesReceipts.ts | 106 ++- .../server/src/api/controllers/Sales/index.ts | 4 +- .../server/src/jobs/WriteInvoicesJEntries.ts | 4 +- packages/server/src/loaders/eventEmitter.ts | 5 +- packages/server/src/models/Bill.ts | 2 +- packages/server/src/models/SaleInvoice.ts | 2 +- .../src/services/Branches/DeleteBranch.ts | 1 + .../Integrations/ValidateBranchExistance.ts | 15 +- .../CreditNotes/CreditNoteApplyToInvoices.ts | 20 +- .../DeleteCreditNoteApplyToInvoices.ts | 12 +- .../InviteUsers/SyncSystemSendInvite.ts | 4 +- .../ItemInvoicesTransactionsTransformer.ts | 9 +- .../src/services/Items/ItemsEntriesService.ts | 12 +- .../BillPayments/BillPaymentBillSync.ts | 48 + .../BillPayments/BillPaymentGLEntries.ts | 2 +- .../BillPaymentTransactionTransformer.ts | 2 +- .../BillPayments/BillPaymentValidators.ts | 278 ++++++ .../Purchases/BillPayments/BillPayments.ts | 713 --------------- .../BillPayments/BillPaymentsApplication.ts | 109 +++ .../BillPayments/BillPaymentsPages.ts | 5 +- .../CommandBillPaymentDTOTransformer.ts | 37 + .../BillPayments/CreateBillPayment.ts | 124 +++ .../BillPayments/DeleteBillPayment.ts | 71 ++ .../Purchases/BillPayments/EditBillPayment.ts | 146 +++ .../Purchases/BillPayments/GetBillPayment.ts | 51 ++ .../Purchases/BillPayments/GetBillPayments.ts | 74 ++ .../Purchases/BillPayments/GetPaymentBills.ts | 32 + .../server/src/services/Purchases/Bills.ts | 751 ---------------- .../Purchases/Bills/BillDTOTransformer.ts | 130 +++ .../Bills/BillGLEntriesSubscriber.ts | 9 +- .../Bills/BillInventoryTransactions.ts | 82 ++ .../BillPaymentsGLEntriesRewriteSubscriber.ts | 2 +- .../Purchases/Bills/BillsApplication.ts | 147 +++ .../Purchases/Bills/BillsValidators.ts | 154 ++++ .../services/Purchases/Bills/CreateBill.ts | 116 +++ .../services/Purchases/Bills/DeleteBill.ts | 80 ++ .../src/services/Purchases/Bills/EditBill.ts | 151 ++++ .../src/services/Purchases/Bills/GetBill.ts | 42 + .../GetBillPayments.ts} | 4 +- .../src/services/Purchases/Bills/GetBills.ts | 76 ++ .../services/Purchases/Bills/GetDueBills.ts | 32 + .../src/services/Purchases/Bills/OpenBill.ts | 46 + .../PurchaseInvoiceTransformer.ts | 0 .../Purchases/{ => Bills}/constants.ts | 0 .../LandedCost/AllocateLandedCost.ts | 7 +- .../Purchases/LandedCost/BaseLandedCost.ts | 7 +- ...andedCostSyncCostTransactionsSubscriber.ts | 2 +- .../LandedCost/LandedCostTransactions.ts | 10 +- .../LandedCost/TransctionLandedCost.ts | 6 +- .../ApplyVendorCreditToBills.ts | 27 +- .../Sales/Estimates/ApproveSaleEstimate.ts | 75 ++ .../Sales/Estimates/ConvetSaleEstimate.ts | 46 + .../Sales/Estimates/CreateSaleEstimate.ts | 102 +++ .../Sales/Estimates/DeleteSaleEstimate.ts | 74 ++ .../Sales/Estimates/DeliverSaleEstimate.ts | 71 ++ .../Sales/Estimates/EditSaleEstimate.ts | 123 +++ .../Sales/Estimates/GetSaleEstimate.ts | 43 + .../Sales/Estimates/GetSaleEstimates.ts | 77 ++ .../Sales/Estimates/RejectSaleEstimate.ts | 57 ++ .../Estimates/SaleEstimateDTOTransformer.ts | 104 +++ .../Sales/Estimates/SaleEstimateIncrement.ts | 31 + .../Sales/Estimates/SaleEstimateSmsNotify.ts | 12 +- .../Estimates/SaleEstimateTransformer.ts | 2 +- .../Sales/Estimates/SaleEstimateValidators.ts | 87 ++ .../Estimates/SaleEstimatesApplication.ts | 212 +++++ .../Sales/Estimates/SaleEstimatesPdf.ts | 8 +- .../Estimates/UnlinkConvertedSaleEstimate.ts | 32 + .../CommandSaleInvoiceDTOTransformer.ts | 103 +++ .../Invoices/CommandSaleInvoiceValidators.ts | 86 ++ .../Sales/Invoices/CreateSaleInvoice.ts | 147 +++ .../Sales/Invoices/DeleteSaleInvoice.ts | 154 ++++ .../Sales/Invoices/DeliverSaleInvoice.ts | 77 ++ .../Sales/Invoices/EditSaleInvoice.ts | 165 ++++ ...ervice.ts => GetInvoicePaymentsService.ts} | 2 +- .../services/Sales/Invoices/GetSaleInvoice.ts | 47 + .../Sales/Invoices/GetSaleInvoices.ts | 80 ++ .../Sales/Invoices/GetSaleInvoicesPayable.ts | 31 + .../Sales/Invoices/InvoiceGLEntries.ts | 20 +- .../Invoices/InvoiceInventoryTransactions.ts | 77 ++ .../InvoicePaymentTransactionTransformer.ts | 5 + .../Invoices/InvoicePaymentsGLRewrite.ts | 2 +- .../Invoices/SaleInvoiceCostGLEntries.ts | 8 +- .../Sales/Invoices/SaleInvoiceIncrement.ts | 31 + .../{ => Invoices}/SaleInvoiceNotifyBySms.ts | 22 +- .../Sales/{ => Invoices}/SaleInvoicePdf.ts | 2 +- .../{ => Invoices}/SaleInvoiceTransformer.ts | 0 .../SaleInvoiceWriteoffGLEntries.ts | 14 +- .../SaleInvoiceWriteoffGLStorage.ts | 0 .../SaleInvoiceWriteoffSubscriber.ts | 0 .../Sales/Invoices/SaleInvoicesApplication.ts | 282 ++++++ .../Sales/{ => Invoices}/SalesInvoicesCost.ts | 11 +- .../WriteoffSaleInvoice.ts} | 39 +- .../Sales/{ => Invoices}/constants.ts | 7 +- .../PaymentReceives/CreatePaymentReceive.ts | 136 +++ .../PaymentReceives/DeletePaymentReceive.ts | 79 ++ .../PaymentReceives/EditPaymentReceive.ts | 177 ++++ .../PaymentReceives/GetPaymentReceive.ts | 46 + .../GetPaymentReceiveInvoices.ts | 41 + .../PaymentReceives/GetPaymentReceives.ts | 77 ++ .../PaymentReceives/GetPaymentReeceivePdf.ts | 4 +- .../PaymentReceiveDTOTransformer.ts | 69 ++ .../PaymentReceiveIncrement.ts | 31 + .../PaymentReceiveInvoiceSync.ts | 48 + .../PaymentReceiveSmsNotify.ts | 18 +- .../PaymentReceiveTransformer.ts | 2 +- .../PaymentReceiveValidators.ts | 295 ++++++ .../PaymentReceivesApplication.ts | 193 ++++ .../Sales/PaymentReceives/PaymentsReceives.ts | 847 ------------------ .../Sales/PaymentReceives/constants.ts | 5 +- .../Sales/Receipts/CloseSaleReceipt.ts | 76 ++ .../Sales/Receipts/CreateSaleReceipt.ts | 106 +++ .../Sales/Receipts/DeleteSaleReceipt.ts | 67 ++ .../Sales/Receipts/EditSaleReceipt.ts | 119 +++ .../services/Sales/Receipts/GetSaleReceipt.ts | 42 + .../Sales/Receipts/GetSaleReceipts.ts | 80 ++ .../Sales/Receipts/SaleReceiptApplication.ts | 169 ++++ .../Receipts/SaleReceiptDTOTransformer.ts | 89 ++ .../{ => Receipts}/SaleReceiptGLEntries.ts | 0 .../Sales/Receipts/SaleReceiptIncrement.ts | 31 + .../SaleReceiptInventoryTransactions.ts | 72 ++ .../{ => Receipts}/SaleReceiptNotifyBySms.ts | 19 +- .../Sales/Receipts/SaleReceiptValidators.ts | 106 +++ .../Sales/Receipts/SaleReceiptsPdfService.ts | 2 +- .../SaleReceiptCostGLEntriesSubscriber.ts | 2 +- .../src/services/Sales/SaleNotifyBySms.ts | 1 - .../src/services/Sales/SalesEstimate.ts | 718 --------------- .../src/services/Sales/SalesInvoices.ts | 799 ----------------- .../src/services/Sales/SalesReceipts.ts | 629 ------------- .../Settings/SmsNotificationsSettings.ts | 9 +- .../Bills/WriteInventoryTransactions.ts | 18 +- .../src/subscribers/Inventory/Inventory.ts | 15 +- .../PaymentMades/PaymentSyncBillBalance.ts | 11 +- .../PaymentReceive/AutoSerialIncrement.ts | 8 +- .../PaymentReceiveSyncInvoices.ts | 14 +- .../SendSmsNotificationToCustomer.ts | 4 +- .../SaleEstimate/SmsNotifications.ts | 4 +- .../SaleInvoices/AutoIncrementSerial.ts | 4 +- .../SaleInvoices/ConvertFromEstimate.ts | 8 +- .../SendSmsNotificationToCustomer.ts | 4 +- .../WriteInventoryTransactions.ts | 18 +- .../SaleReceipt/AutoIncrementSerial.ts | 4 +- .../SendSmsNotificationToCustomer.ts | 4 +- .../SaleReceipt/WriteInventoryTransactions.ts | 16 +- .../SaleReceipt/WriteJournalEntries.ts | 8 +- 150 files changed, 7188 insertions(+), 5007 deletions(-) create mode 100644 e2e/items.spec.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/BillPaymentBillSync.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts delete mode 100644 packages/server/src/services/Purchases/BillPayments/BillPayments.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/BillPaymentsApplication.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/CreateBillPayment.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts create mode 100644 packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts delete mode 100644 packages/server/src/services/Purchases/Bills.ts create mode 100644 packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts create mode 100644 packages/server/src/services/Purchases/Bills/BillInventoryTransactions.ts create mode 100644 packages/server/src/services/Purchases/Bills/BillsApplication.ts create mode 100644 packages/server/src/services/Purchases/Bills/BillsValidators.ts create mode 100644 packages/server/src/services/Purchases/Bills/CreateBill.ts create mode 100644 packages/server/src/services/Purchases/Bills/DeleteBill.ts create mode 100644 packages/server/src/services/Purchases/Bills/EditBill.ts create mode 100644 packages/server/src/services/Purchases/Bills/GetBill.ts rename packages/server/src/services/Purchases/{BillPaymentsService.ts => Bills/GetBillPayments.ts} (86%) create mode 100644 packages/server/src/services/Purchases/Bills/GetBills.ts create mode 100644 packages/server/src/services/Purchases/Bills/GetDueBills.ts create mode 100644 packages/server/src/services/Purchases/Bills/OpenBill.ts rename packages/server/src/services/Purchases/{PurchaseInvoices => Bills}/PurchaseInvoiceTransformer.ts (100%) rename packages/server/src/services/Purchases/{ => Bills}/constants.ts (100%) create mode 100644 packages/server/src/services/Sales/Estimates/ApproveSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/ConvetSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/CreateSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/DeleteSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/DeliverSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/EditSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/GetSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts create mode 100644 packages/server/src/services/Sales/Estimates/RejectSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts create mode 100644 packages/server/src/services/Sales/Estimates/SaleEstimateIncrement.ts create mode 100644 packages/server/src/services/Sales/Estimates/SaleEstimateValidators.ts create mode 100644 packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts create mode 100644 packages/server/src/services/Sales/Estimates/UnlinkConvertedSaleEstimate.ts create mode 100644 packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts create mode 100644 packages/server/src/services/Sales/Invoices/CommandSaleInvoiceValidators.ts create mode 100644 packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts create mode 100644 packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts create mode 100644 packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts create mode 100644 packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts rename packages/server/src/services/Sales/Invoices/{InvoicePaymentsService.ts => GetInvoicePaymentsService.ts} (95%) create mode 100644 packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts create mode 100644 packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts create mode 100644 packages/server/src/services/Sales/Invoices/GetSaleInvoicesPayable.ts create mode 100644 packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts create mode 100644 packages/server/src/services/Sales/Invoices/SaleInvoiceIncrement.ts rename packages/server/src/services/Sales/{ => Invoices}/SaleInvoiceNotifyBySms.ts (93%) rename packages/server/src/services/Sales/{ => Invoices}/SaleInvoicePdf.ts (96%) rename packages/server/src/services/Sales/{ => Invoices}/SaleInvoiceTransformer.ts (100%) rename packages/server/src/services/Sales/{ => Invoices}/SaleInvoiceWriteoffGLEntries.ts (90%) rename packages/server/src/services/Sales/{ => Invoices}/SaleInvoiceWriteoffGLStorage.ts (100%) rename packages/server/src/services/Sales/{ => Invoices}/SaleInvoiceWriteoffSubscriber.ts (100%) create mode 100644 packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts rename packages/server/src/services/Sales/{ => Invoices}/SalesInvoicesCost.ts (92%) rename packages/server/src/services/Sales/{SaleInvoiceWriteoff.ts => Invoices/WriteoffSaleInvoice.ts} (85%) rename packages/server/src/services/Sales/{ => Invoices}/constants.ts (89%) create mode 100644 packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/DeletePaymentReceive.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/GetPaymentReceive.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/GetPaymentReceiveInvoices.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/GetPaymentReceives.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceiveIncrement.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceiveInvoiceSync.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceiveValidators.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts delete mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts create mode 100644 packages/server/src/services/Sales/Receipts/CloseSaleReceipt.ts create mode 100644 packages/server/src/services/Sales/Receipts/CreateSaleReceipt.ts create mode 100644 packages/server/src/services/Sales/Receipts/DeleteSaleReceipt.ts create mode 100644 packages/server/src/services/Sales/Receipts/EditSaleReceipt.ts create mode 100644 packages/server/src/services/Sales/Receipts/GetSaleReceipt.ts create mode 100644 packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts rename packages/server/src/services/Sales/{ => Receipts}/SaleReceiptGLEntries.ts (100%) create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptIncrement.ts create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptInventoryTransactions.ts rename packages/server/src/services/Sales/{ => Receipts}/SaleReceiptNotifyBySms.ts (93%) create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptValidators.ts delete mode 100644 packages/server/src/services/Sales/SalesEstimate.ts delete mode 100644 packages/server/src/services/Sales/SalesInvoices.ts delete mode 100644 packages/server/src/services/Sales/SalesReceipts.ts diff --git a/e2e/items.spec.ts b/e2e/items.spec.ts new file mode 100644 index 000000000..6dfc12247 --- /dev/null +++ b/e2e/items.spec.ts @@ -0,0 +1,7 @@ +import { test, expect, Page } from '@playwright/test'; + +test.describe('item', () => { + test('should validate all required fields.', () => {}); + test('should save the item successfully.', () => {}); + test('should item code be unqiue.', () => {}); +}); diff --git a/packages/server/src/api/controllers/Purchases/Bills.ts b/packages/server/src/api/controllers/Purchases/Bills.ts index 3c1ef0311..2bfe230e3 100644 --- a/packages/server/src/api/controllers/Purchases/Bills.ts +++ b/packages/server/src/api/controllers/Purchases/Bills.ts @@ -1,26 +1,27 @@ +import { Service, Inject } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query } from 'express-validator'; -import { Service, Inject } from 'typedi'; -import { AbilitySubject, BillAction, IBillDTO, IBillEditDTO } from '@/interfaces'; +import { + AbilitySubject, + BillAction, + IBillDTO, + IBillEditDTO, +} from '@/interfaces'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; -import BillsService from '@/services/Purchases/Bills'; import BaseController from '@/api/controllers/BaseController'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import { ServiceError } from '@/exceptions'; import CheckPolicies from '@/api/middleware/CheckPolicies'; -import BillPaymentsService from '@/services/Purchases/BillPaymentsService'; +import { BillsApplication } from '@/services/Purchases/Bills/BillsApplication'; @Service() export default class BillsController extends BaseController { @Inject() - private billsService: BillsService; + private billsApplication: BillsApplication; @Inject() private dynamicListService: DynamicListingService; - @Inject() - private billPayments: BillPaymentsService; - /** * Router constructor. */ @@ -97,7 +98,7 @@ export default class BillsController extends BaseController { /** * Common validation schema. */ - get billValidationSchema() { + private get billValidationSchema() { return [ check('bill_number').exists().trim().escape(), check('reference_no').optional().trim().escape(), @@ -142,7 +143,7 @@ export default class BillsController extends BaseController { /** * Common validation schema. */ - get billEditValidationSchema() { + private get billEditValidationSchema() { return [ check('bill_number').optional().trim().escape(), check('reference_no').optional().trim().escape(), @@ -184,14 +185,14 @@ export default class BillsController extends BaseController { /** * Bill validation schema. */ - get specificBillValidationSchema() { + private get specificBillValidationSchema() { return [param('id').exists().isNumeric().toInt()]; } /** * Bills list validation schema. */ - get billsListingValidationSchema() { + private get billsListingValidationSchema() { return [ query('view_slug').optional().isString().trim(), query('stringified_filter_roles').optional().isJSON(), @@ -203,7 +204,7 @@ export default class BillsController extends BaseController { ]; } - get dueBillsListingValidationSchema() { + private get dueBillsListingValidationSchema() { return [ query('vendor_id').optional().trim().escape(), query('payment_made_id').optional().trim().escape(), @@ -216,17 +217,16 @@ export default class BillsController extends BaseController { * @param {Response} res * @param {Function} next */ - async newBill(req: Request, res: Response, next: NextFunction) { + private async newBill(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; const billDTO: IBillDTO = this.matchedBodyData(req); try { - const storedBill = await this.billsService.createBill( + const storedBill = await this.billsApplication.createBill( tenantId, billDTO, user ); - return res.status(200).send({ id: storedBill.id, message: 'The bill has been created successfully.', @@ -241,13 +241,13 @@ export default class BillsController extends BaseController { * @param {Request} req * @param {Response} res */ - async editBill(req: Request, res: Response, next: NextFunction) { + private async editBill(req: Request, res: Response, next: NextFunction) { const { id: billId } = req.params; const { tenantId, user } = req; const billDTO: IBillEditDTO = this.matchedBodyData(req); try { - await this.billsService.editBill(tenantId, billId, billDTO, user); + await this.billsApplication.editBill(tenantId, billId, billDTO, user); return res.status(200).send({ id: billId, @@ -263,12 +263,12 @@ export default class BillsController extends BaseController { * @param {Request} req - * @param {Response} res - */ - async openBill(req: Request, res: Response, next: NextFunction) { + private async openBill(req: Request, res: Response, next: NextFunction) { const { id: billId } = req.params; const { tenantId } = req; try { - await this.billsService.openBill(tenantId, billId); + await this.billsApplication.openBill(tenantId, billId); return res.status(200).send({ id: billId, @@ -285,12 +285,12 @@ export default class BillsController extends BaseController { * @param {Response} res * @return {Response} */ - async getBill(req: Request, res: Response, next: NextFunction) { + private async getBill(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: billId } = req.params; try { - const bill = await this.billsService.getBill(tenantId, billId); + const bill = await this.billsApplication.getBill(tenantId, billId); return res.status(200).send(this.transfromToResponse({ bill })); } catch (error) { @@ -304,12 +304,12 @@ export default class BillsController extends BaseController { * @param {Response} res - * @return {Response} */ - async deleteBill(req: Request, res: Response, next: NextFunction) { + private async deleteBill(req: Request, res: Response, next: NextFunction) { const billId = req.params.id; const { tenantId } = req; try { - await this.billsService.deleteBill(tenantId, billId); + await this.billsApplication.deleteBill(tenantId, billId); return res.status(200).send({ id: billId, @@ -326,7 +326,7 @@ export default class BillsController extends BaseController { * @param {Response} res - * @return {Response} */ - public async billsList(req: Request, res: Response, next: NextFunction) { + private async billsList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const filter = { page: 1, @@ -338,7 +338,7 @@ export default class BillsController extends BaseController { try { const { bills, pagination, filterMeta } = - await this.billsService.getBills(tenantId, filter); + await this.billsApplication.getBills(tenantId, filter); return res.status(200).send({ bills: this.transfromToResponse(bills), @@ -356,12 +356,13 @@ export default class BillsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - public async getDueBills(req: Request, res: Response, next: NextFunction) { + private async getDueBills(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { vendorId } = this.matchedQueryData(req); try { - const bills = await this.billsService.getDueBills(tenantId, vendorId); + const bills = await this.billsApplication.getDueBills(tenantId, vendorId); + return res.status(200).send({ bills }); } catch (error) { next(error); @@ -374,7 +375,7 @@ export default class BillsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - public getBillPaymentsTransactions = async ( + private getBillPaymentsTransactions = async ( req: Request, res: Response, next: NextFunction @@ -383,7 +384,7 @@ export default class BillsController extends BaseController { const { id: billId } = req.params; try { - const billPayments = await this.billPayments.getBillPayments( + const billPayments = await this.billsApplication.getBillPayments( tenantId, billId ); diff --git a/packages/server/src/api/controllers/Purchases/BillsPayments.ts b/packages/server/src/api/controllers/Purchases/BillsPayments.ts index 5af4010d4..8959fdd8e 100644 --- a/packages/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/packages/server/src/api/controllers/Purchases/BillsPayments.ts @@ -4,7 +4,7 @@ import { check, param, query, ValidationChain } from 'express-validator'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import { ServiceError } from '@/exceptions'; import BaseController from '@/api/controllers/BaseController'; -import BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments'; +import { BillPaymentsApplication } from '@/services/Purchases/BillPayments/BillPaymentsApplication'; import BillPaymentsPages from '@/services/Purchases/BillPayments/BillPaymentsPages'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import CheckPolicies from '@/api/middleware/CheckPolicies'; @@ -17,18 +17,18 @@ import { AbilitySubject, IPaymentMadeAction } from '@/interfaces'; @Service() export default class BillsPayments extends BaseController { @Inject() - billPaymentService: BillPaymentsService; + private billPaymentsApplication: BillPaymentsApplication; @Inject() - dynamicListService: DynamicListingService; + private dynamicListService: DynamicListingService; @Inject() - billPaymentsPages: BillPaymentsPages; + private billPaymentsPages: BillPaymentsPages; /** * Router constructor. */ - router() { + public router() { const router = Router(); router.post( @@ -106,7 +106,7 @@ export default class BillsPayments extends BaseController { * Bill payments schema validation. * @return {ValidationChain[]} */ - get billPaymentSchemaValidation(): ValidationChain[] { + private get billPaymentSchemaValidation(): ValidationChain[] { return [ check('vendor_id').exists().isNumeric().toInt(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), @@ -129,7 +129,7 @@ export default class BillsPayments extends BaseController { * Specific bill payment schema validation. * @returns {ValidationChain[]} */ - get specificBillPaymentValidateSchema(): ValidationChain[] { + private get specificBillPaymentValidateSchema(): ValidationChain[] { return [param('id').exists().isNumeric().toInt()]; } @@ -137,7 +137,7 @@ export default class BillsPayments extends BaseController { * Bills payment list validation schema. * @returns {ValidationChain[]} */ - get listingValidationSchema(): ValidationChain[] { + private get listingValidationSchema(): ValidationChain[] { return [ query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), @@ -154,7 +154,7 @@ export default class BillsPayments extends BaseController { * @param {Request} req - * @param {Response} res - */ - async getBillPaymentNewPageEntries(req: Request, res: Response) { + private async getBillPaymentNewPageEntries(req: Request, res: Response) { const { tenantId } = req; const { vendorId } = this.matchedQueryData(req); @@ -174,7 +174,7 @@ export default class BillsPayments extends BaseController { * @param {Request} req * @param {Response} res */ - async getBillPaymentEditPage( + private async getBillPaymentEditPage( req: Request, res: Response, next: NextFunction @@ -205,16 +205,19 @@ export default class BillsPayments extends BaseController { * @param {Response} res * @param {Response} res */ - async createBillPayment(req: Request, res: Response, next: NextFunction) { + private async createBillPayment( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const billPaymentDTO = this.matchedBodyData(req); try { - const billPayment = await this.billPaymentService.createBillPayment( + const billPayment = await this.billPaymentsApplication.createBillPayment( tenantId, billPaymentDTO ); - return res.status(200).send({ id: billPayment.id, message: 'Payment made has been created successfully.', @@ -229,13 +232,17 @@ export default class BillsPayments extends BaseController { * @param {Request} req * @param {Response} res */ - async editBillPayment(req: Request, res: Response, next: NextFunction) { + private async editBillPayment( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const billPaymentDTO = this.matchedBodyData(req); const { id: billPaymentId } = req.params; try { - const paymentMade = await this.billPaymentService.editBillPayment( + const paymentMade = await this.billPaymentsApplication.editBillPayment( tenantId, billPaymentId, billPaymentDTO @@ -256,12 +263,19 @@ export default class BillsPayments extends BaseController { * @param {Response} res - * @return {Response} res - */ - async deleteBillPayment(req: Request, res: Response, next: NextFunction) { + private async deleteBillPayment( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const { id: billPaymentId } = req.params; try { - await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId); + await this.billPaymentsApplication.deleteBillPayment( + tenantId, + billPaymentId + ); return res.status(200).send({ id: billPaymentId, @@ -277,16 +291,19 @@ export default class BillsPayments extends BaseController { * @param {Request} req * @param {Response} res */ - async getBillPayment(req: Request, res: Response, next: NextFunction) { + private async getBillPayment( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const { id: billPaymentId } = req.params; try { - const billPayment = await this.billPaymentService.getBillPayment( + const billPayment = await this.billPaymentsApplication.getBillPayment( tenantId, billPaymentId ); - return res.status(200).send({ bill_payment: this.transfromToResponse(billPayment), }); @@ -301,12 +318,16 @@ export default class BillsPayments extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async getPaymentBills(req: Request, res: Response, next: NextFunction) { + private async getPaymentBills( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const { id: billPaymentId } = req.params; try { - const bills = await this.billPaymentService.getPaymentBills( + const bills = await this.billPaymentsApplication.getPaymentBills( tenantId, billPaymentId ); @@ -322,7 +343,11 @@ export default class BillsPayments extends BaseController { * @param {Response} res - * @return {Response} */ - async getBillsPayments(req: Request, res: Response, next: NextFunction) { + private async getBillsPayments( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const billPaymentsFilter = { page: 1, @@ -335,7 +360,7 @@ export default class BillsPayments extends BaseController { try { const { billPayments, pagination, filterMeta } = - await this.billPaymentService.listBillPayments( + await this.billPaymentsApplication.getBillPayments( tenantId, billPaymentsFilter ); @@ -357,7 +382,7 @@ export default class BillsPayments extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handleServiceError( + private handleServiceError( error: Error, req: Request, res: Response, diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 3af447e9c..4e1f69720 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -1,42 +1,29 @@ +import { Inject, Service } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, ValidationChain } from 'express-validator'; -import { Inject, Service } from 'typedi'; import { AbilitySubject, IPaymentReceiveDTO, PaymentReceiveAction, - SaleInvoiceAction, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; -import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives'; import PaymentReceivesPages from '@/services/Sales/PaymentReceives/PaymentReceivesPages'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import { ServiceError } from '@/exceptions'; -import PaymentReceiveNotifyBySms from '@/services/Sales/PaymentReceives/PaymentReceiveSmsNotify'; +import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication'; import CheckPolicies from '@/api/middleware/CheckPolicies'; -import GetPaymentReceivePdf from '@/services/Sales/PaymentReceives/GetPaymentReeceivePdf'; +import { ServiceError } from '@/exceptions'; -/** - * Payments receives controller. - * @service - */ @Service() export default class PaymentReceivesController extends BaseController { @Inject() - paymentReceiveService: PaymentReceiveService; + private paymentReceiveApplication: PaymentReceivesApplication; @Inject() - PaymentReceivesPages: PaymentReceivesPages; + private PaymentReceivesPages: PaymentReceivesPages; @Inject() - dynamicListService: DynamicListingService; - - @Inject() - paymentReceiveSmsNotify: PaymentReceiveNotifyBySms; - - @Inject() - paymentReceivePdf: GetPaymentReceivePdf; + private dynamicListService: DynamicListingService; /** * Router constructor. @@ -137,7 +124,7 @@ export default class PaymentReceivesController extends BaseController { * Payment receive schema. * @return {Array} */ - get paymentReceiveSchema(): ValidationChain[] { + private get paymentReceiveSchema(): ValidationChain[] { return [ check('customer_id').exists().isNumeric().toInt(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), @@ -162,7 +149,7 @@ export default class PaymentReceivesController extends BaseController { /** * Payment receive list validation schema. */ - get validatePaymentReceiveList(): ValidationChain[] { + private get validatePaymentReceiveList(): ValidationChain[] { return [ query('stringified_filter_roles').optional().isJSON(), @@ -181,7 +168,7 @@ export default class PaymentReceivesController extends BaseController { /** * Validate payment receive parameters. */ - get paymentReceiveValidation() { + private get paymentReceiveValidation() { return [param('id').exists().isNumeric().toInt()]; } @@ -189,14 +176,14 @@ export default class PaymentReceivesController extends BaseController { * New payment receive validation schema. * @return {Array} */ - get newPaymentReceiveValidation() { + private get newPaymentReceiveValidation() { return [...this.paymentReceiveSchema]; } /** * Edit payment receive validation. */ - get editPaymentReceiveValidation() { + private get editPaymentReceiveValidation() { return [ param('id').exists().isNumeric().toInt(), ...this.paymentReceiveSchema, @@ -209,13 +196,17 @@ export default class PaymentReceivesController extends BaseController { * @param {Response} res * @return {Response} */ - async newPaymentReceive(req: Request, res: Response, next: NextFunction) { + private async newPaymentReceive( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId, user } = req; const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req); try { const storedPaymentReceive = - await this.paymentReceiveService.createPaymentReceive( + await this.paymentReceiveApplication.createPaymentReceive( tenantId, paymentReceive, user @@ -235,14 +226,18 @@ export default class PaymentReceivesController extends BaseController { * @param {Response} res * @return {Response} */ - async editPaymentReceive(req: Request, res: Response, next: NextFunction) { + private async editPaymentReceive( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId, user } = req; const { id: paymentReceiveId } = req.params; const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req); try { - await this.paymentReceiveService.editPaymentReceive( + await this.paymentReceiveApplication.editPaymentReceive( tenantId, paymentReceiveId, paymentReceive, @@ -262,17 +257,20 @@ export default class PaymentReceivesController extends BaseController { * @param {Request} req * @param {Response} res */ - async deletePaymentReceive(req: Request, res: Response, next: NextFunction) { + private async deletePaymentReceive( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId, user } = req; const { id: paymentReceiveId } = req.params; try { - await this.paymentReceiveService.deletePaymentReceive( + await this.paymentReceiveApplication.deletePaymentReceive( tenantId, paymentReceiveId, user ); - return res.status(200).send({ id: paymentReceiveId, message: 'The payment receive has been deleted successfully', @@ -288,7 +286,7 @@ export default class PaymentReceivesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async getPaymentReceiveInvoices( + private async getPaymentReceiveInvoices( req: Request, res: Response, next: NextFunction @@ -298,7 +296,7 @@ export default class PaymentReceivesController extends BaseController { try { const saleInvoices = - await this.paymentReceiveService.getPaymentReceiveInvoices( + await this.paymentReceiveApplication.getPaymentReceiveInvoices( tenantId, paymentReceiveId ); @@ -315,7 +313,11 @@ export default class PaymentReceivesController extends BaseController { * @param {Response} res * @return {Response} */ - async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) { + private async getPaymentReceiveList( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const filter = { sortOrder: 'desc', @@ -327,7 +329,10 @@ export default class PaymentReceivesController extends BaseController { try { const { paymentReceives, pagination, filterMeta } = - await this.paymentReceiveService.listPaymentReceives(tenantId, filter); + await this.paymentReceiveApplication.getPaymentReceives( + tenantId, + filter + ); return res.status(200).send({ payment_receives: this.transfromToResponse(paymentReceives), @@ -344,7 +349,7 @@ export default class PaymentReceivesController extends BaseController { * @param {Request} req - Request. * @param {Response} res - Response. */ - async getPaymentReceiveNewPageEntries( + private async getPaymentReceiveNewPageEntries( req: Request, res: Response, next: NextFunction @@ -371,7 +376,7 @@ export default class PaymentReceivesController extends BaseController { * @param {Request} req - * @param {Response} res - */ - async getPaymentReceiveEditPage( + private async getPaymentReceiveEditPage( req: Request, res: Response, next: NextFunction @@ -402,15 +407,20 @@ export default class PaymentReceivesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async getPaymentReceive(req: Request, res: Response, next: NextFunction) { + private async getPaymentReceive( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const { id: paymentReceiveId } = req.params; try { - const paymentReceive = await this.paymentReceiveService.getPaymentReceive( - tenantId, - paymentReceiveId - ); + const paymentReceive = + await this.paymentReceiveApplication.getPaymentReceive( + tenantId, + paymentReceiveId + ); const ACCEPT_TYPE = { APPLICATION_PDF: 'application/pdf', @@ -423,10 +433,11 @@ export default class PaymentReceivesController extends BaseController { }); }, [ACCEPT_TYPE.APPLICATION_PDF]: async () => { - const pdfContent = await this.paymentReceivePdf.getPaymentReceivePdf( - tenantId, - paymentReceive - ); + const pdfContent = + await this.paymentReceiveApplication.getPaymentReceivePdf( + tenantId, + paymentReceive + ); res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdfContent.length, @@ -454,10 +465,11 @@ export default class PaymentReceivesController extends BaseController { const { id: paymentReceiveId } = req.params; try { - const paymentReceive = await this.paymentReceiveSmsNotify.notifyBySms( - tenantId, - paymentReceiveId - ); + const paymentReceive = + await this.paymentReceiveApplication.notifyPaymentBySms( + tenantId, + paymentReceiveId + ); return res.status(200).send({ id: paymentReceive.id, message: 'The payment notification has been sent successfully.', @@ -482,10 +494,11 @@ export default class PaymentReceivesController extends BaseController { const { id: paymentReceiveId } = req.params; try { - const smsDetails = await this.paymentReceiveSmsNotify.smsDetails( - tenantId, - paymentReceiveId - ); + const smsDetails = + await this.paymentReceiveApplication.getPaymentSmsDetails( + tenantId, + paymentReceiveId + ); return res.status(200).send({ data: smsDetails, }); diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index 613186c90..9f3cf3719 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -1,20 +1,17 @@ import { Router, Request, Response, NextFunction } from 'express'; -import { check, param, query, matchedData } from 'express-validator'; +import { check, param, query } from 'express-validator'; import { Inject, Service } from 'typedi'; import { AbilitySubject, ISaleEstimateDTO, SaleEstimateAction, - SaleInvoiceAction, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; -import SaleEstimateService from '@/services/Sales/SalesEstimate'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import { ServiceError } from '@/exceptions'; -import SaleEstimatesPdfService from '@/services/Sales/Estimates/SaleEstimatesPdf'; -import SaleEstimateNotifyBySms from '@/services/Sales/Estimates/SaleEstimateSmsNotify'; import CheckPolicies from '@/api/middleware/CheckPolicies'; +import { SaleEstimatesApplication } from '@/services/Sales/Estimates/SaleEstimatesApplication'; const ACCEPT_TYPE = { APPLICATION_PDF: 'application/pdf', @@ -23,21 +20,15 @@ const ACCEPT_TYPE = { @Service() export default class SalesEstimatesController extends BaseController { @Inject() - saleEstimateService: SaleEstimateService; + private saleEstimatesApplication: SaleEstimatesApplication; @Inject() - dynamicListService: DynamicListingService; - - @Inject() - saleEstimatesPdf: SaleEstimatesPdfService; - - @Inject() - saleEstimateNotifySms: SaleEstimateNotifyBySms; + private dynamicListService: DynamicListingService; /** * Router constructor. */ - router() { + public router() { const router = Router(); router.post( @@ -136,7 +127,7 @@ export default class SalesEstimatesController extends BaseController { /** * Estimate validation schema. */ - get estimateValidationSchema() { + private get estimateValidationSchema() { return [ check('customer_id').exists().isNumeric().toInt(), check('estimate_date').exists().isISO8601().toDate(), @@ -177,14 +168,14 @@ export default class SalesEstimatesController extends BaseController { /** * Specific sale estimate validation schema. */ - get validateSpecificEstimateSchema() { + private get validateSpecificEstimateSchema() { return [param('id').exists().isNumeric().toInt()]; } /** * Sales estimates list validation schema. */ - get validateEstimateListSchema() { + private get validateEstimateListSchema() { return [ query('view_slug').optional().isString().trim(), query('stringified_filter_roles').optional().isJSON(), @@ -202,15 +193,16 @@ export default class SalesEstimatesController extends BaseController { * @param {Response} res - * @return {Response} res - */ - async newEstimate(req: Request, res: Response, next: NextFunction) { + private async newEstimate(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); try { - const storedEstimate = await this.saleEstimateService.createEstimate( - tenantId, - estimateDTO - ); + const storedEstimate = + await this.saleEstimatesApplication.createSaleEstimate( + tenantId, + estimateDTO + ); return res.status(200).send({ id: storedEstimate.id, @@ -226,19 +218,18 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async editEstimate(req: Request, res: Response, next: NextFunction) { + private async editEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; const { tenantId } = req; const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); try { // Update estimate with associated estimate entries. - await this.saleEstimateService.editEstimate( + await this.saleEstimatesApplication.editSaleEstimate( tenantId, estimateId, estimateDTO ); - return res.status(200).send({ id: estimateId, message: 'The sale estimate has been created successfully.', @@ -253,13 +244,19 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async deleteEstimate(req: Request, res: Response, next: NextFunction) { + private async deleteEstimate( + req: Request, + res: Response, + next: NextFunction + ) { const { id: estimateId } = req.params; const { tenantId } = req; try { - await this.saleEstimateService.deleteEstimate(tenantId, estimateId); - + await this.saleEstimatesApplication.deleteSaleEstimate( + tenantId, + estimateId + ); return res.status(200).send({ id: estimateId, message: 'The sale estimate has been deleted successfully.', @@ -274,13 +271,19 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) { + private async deliverSaleEstimate( + req: Request, + res: Response, + next: NextFunction + ) { const { id: estimateId } = req.params; const { tenantId } = req; try { - await this.saleEstimateService.deliverSaleEstimate(tenantId, estimateId); - + await this.saleEstimatesApplication.deliverSaleEstimate( + tenantId, + estimateId + ); return res.status(200).send({ id: estimateId, message: 'The sale estimate has been delivered successfully.', @@ -296,12 +299,19 @@ export default class SalesEstimatesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async approveSaleEstimate(req: Request, res: Response, next: NextFunction) { + private async approveSaleEstimate( + req: Request, + res: Response, + next: NextFunction + ) { const { id: estimateId } = req.params; const { tenantId } = req; try { - await this.saleEstimateService.approveSaleEstimate(tenantId, estimateId); + await this.saleEstimatesApplication.approveSaleEstimate( + tenantId, + estimateId + ); return res.status(200).send({ id: estimateId, @@ -318,12 +328,19 @@ export default class SalesEstimatesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) { + private async rejectSaleEstimate( + req: Request, + res: Response, + next: NextFunction + ) { const { id: estimateId } = req.params; const { tenantId } = req; try { - await this.saleEstimateService.rejectSaleEstimate(tenantId, estimateId); + await this.saleEstimatesApplication.rejectSaleEstimate( + tenantId, + estimateId + ); return res.status(200).send({ id: estimateId, @@ -340,12 +357,12 @@ export default class SalesEstimatesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async getEstimate(req: Request, res: Response, next: NextFunction) { + private async getEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; const { tenantId } = req; try { - const estimate = await this.saleEstimateService.getEstimate( + const estimate = await this.saleEstimatesApplication.getSaleEstimate( tenantId, estimateId ); @@ -357,10 +374,11 @@ export default class SalesEstimatesController extends BaseController { }, // PDF content type. [ACCEPT_TYPE.APPLICATION_PDF]: async () => { - const pdfContent = await this.saleEstimatesPdf.saleEstimatePdf( - tenantId, - estimate - ); + const pdfContent = + await this.saleEstimatesApplication.getSaleEstimatePdf( + tenantId, + estimate + ); res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdfContent.length, @@ -378,7 +396,7 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async getEstimates(req: Request, res: Response, next: NextFunction) { + private async getEstimates(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const filter = { sortOrder: 'desc', @@ -390,7 +408,7 @@ export default class SalesEstimatesController extends BaseController { try { const { salesEstimates, pagination, filterMeta } = - await this.saleEstimateService.estimatesList(tenantId, filter); + await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter); res.format({ [ACCEPT_TYPE.APPLICATION_JSON]: () => { @@ -408,7 +426,7 @@ export default class SalesEstimatesController extends BaseController { } } - public saleEstimateNotifyBySms = async ( + private saleEstimateNotifyBySms = async ( req: Request, res: Response, next: NextFunction @@ -417,10 +435,11 @@ export default class SalesEstimatesController extends BaseController { const { id: estimateId } = req.params; try { - const saleEstimate = await this.saleEstimateNotifySms.notifyBySms( - tenantId, - estimateId - ); + const saleEstimate = + await this.saleEstimatesApplication.notifySaleEstimateBySms( + tenantId, + estimateId + ); return res.status(200).send({ id: saleEstimate.id, message: @@ -437,7 +456,7 @@ export default class SalesEstimatesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - public saleEstimateSmsDetails = async ( + private saleEstimateSmsDetails = async ( req: Request, res: Response, next: NextFunction @@ -446,10 +465,11 @@ export default class SalesEstimatesController extends BaseController { const { id: estimateId } = req.params; try { - const estimateSmsDetails = await this.saleEstimateNotifySms.smsDetails( - tenantId, - estimateId - ); + const estimateSmsDetails = + await this.saleEstimatesApplication.getSaleEstimateSmsDetails( + tenantId, + estimateId + ); return res.status(200).send({ data: estimateSmsDetails, }); diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 209f7e848..c9975b710 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -3,7 +3,6 @@ import { check, param, query } from 'express-validator'; import { Service, Inject } from 'typedi'; import BaseController from '../BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; -import SaleInvoiceService from '@/services/Sales/SalesInvoices'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import { ServiceError } from '@/exceptions'; import { @@ -12,11 +11,8 @@ import { SaleInvoiceAction, AbilitySubject, } from '@/interfaces'; -import SaleInvoicePdf from '@/services/Sales/SaleInvoicePdf'; -import SaleInvoiceWriteoff from '@/services/Sales/SaleInvoiceWriteoff'; -import SaleInvoiceNotifyBySms from '@/services/Sales/SaleInvoiceNotifyBySms'; import CheckPolicies from '@/api/middleware/CheckPolicies'; -import InvoicePaymentsService from '@/services/Sales/Invoices/InvoicePaymentsService'; +import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication'; const ACCEPT_TYPE = { APPLICATION_PDF: 'application/pdf', @@ -25,27 +21,15 @@ const ACCEPT_TYPE = { @Service() export default class SaleInvoicesController extends BaseController { @Inject() - saleInvoiceService: SaleInvoiceService; + private saleInvoiceApplication: SaleInvoiceApplication; @Inject() - dynamicListService: DynamicListingService; - - @Inject() - saleInvoicePdf: SaleInvoicePdf; - - @Inject() - saleInvoiceWriteoff: SaleInvoiceWriteoff; - - @Inject() - saleInvoiceSmsNotify: SaleInvoiceNotifyBySms; - - @Inject() - invoicePaymentsSerivce: InvoicePaymentsService; + private dynamicListService: DynamicListingService; /** * Router constructor. */ - router() { + public router() { const router = Router(); router.post( @@ -167,7 +151,7 @@ export default class SaleInvoicesController extends BaseController { /** * Sale invoice validation schema. */ - get saleInvoiceValidationSchema() { + private get saleInvoiceValidationSchema() { return [ check('customer_id').exists().isNumeric().toInt(), check('invoice_date').exists().isISO8601().toDate(), @@ -227,14 +211,14 @@ export default class SaleInvoicesController extends BaseController { /** * Specific sale invoice validation schema. */ - get specificSaleInvoiceValidation() { + private get specificSaleInvoiceValidation() { return [param('id').exists().isNumeric().toInt()]; } /** * Sales invoices list validation schema. */ - get saleInvoiceListValidationSchema() { + private get saleInvoiceListValidationSchema() { return [ query('view_slug').optional({ nullable: true }).isString().trim(), query('stringified_filter_roles').optional().isJSON(), @@ -249,7 +233,7 @@ export default class SaleInvoicesController extends BaseController { /** * Due sale invoice list validation schema. */ - get dueSalesInvoicesListValidationSchema() { + private get dueSalesInvoicesListValidationSchema() { return [query('customer_id').optional().isNumeric().toInt()]; } @@ -259,17 +243,22 @@ export default class SaleInvoicesController extends BaseController { * @param {Response} res * @param {Function} next */ - async newSaleInvoice(req: Request, res: Response, next: NextFunction) { + private async newSaleInvoice( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId, user } = req; const saleInvoiceDTO: ISaleInvoiceCreateDTO = this.matchedBodyData(req); try { // Creates a new sale invoice with associated entries. - const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice( - tenantId, - saleInvoiceDTO, - user - ); + const storedSaleInvoice = + await this.saleInvoiceApplication.createSaleInvoice( + tenantId, + saleInvoiceDTO, + user + ); return res.status(200).send({ id: storedSaleInvoice.id, message: 'The sale invoice has been created successfully.', @@ -285,14 +274,18 @@ export default class SaleInvoicesController extends BaseController { * @param {Response} res * @param {Function} next */ - async editSaleInvoice(req: Request, res: Response, next: NextFunction) { + private async editSaleInvoice( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId, user } = req; const { id: saleInvoiceId } = req.params; const saleInvoiceOTD: ISaleInvoiceDTO = this.matchedBodyData(req); try { // Update the given sale invoice details. - await this.saleInvoiceService.editSaleInvoice( + await this.saleInvoiceApplication.editSaleInvoice( tenantId, saleInvoiceId, saleInvoiceOTD, @@ -313,12 +306,16 @@ export default class SaleInvoicesController extends BaseController { * @param {Response} res - * @param {NextFunction} next - */ - async deliverSaleInvoice(req: Request, res: Response, next: NextFunction) { + private async deliverSaleInvoice( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId, user } = req; const { id: saleInvoiceId } = req.params; try { - await this.saleInvoiceService.deliverSaleInvoice( + await this.saleInvoiceApplication.deliverSaleInvoice( tenantId, saleInvoiceId, user @@ -338,13 +335,17 @@ export default class SaleInvoicesController extends BaseController { * @param {Response} res * @param {Function} next */ - async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) { + private async deleteSaleInvoice( + req: Request, + res: Response, + next: NextFunction + ) { const { id: saleInvoiceId } = req.params; const { tenantId, user } = req; try { // Deletes the sale invoice with associated entries and journal transaction. - await this.saleInvoiceService.deleteSaleInvoice( + await this.saleInvoiceApplication.deleteSaleInvoice( tenantId, saleInvoiceId, user @@ -364,12 +365,16 @@ export default class SaleInvoicesController extends BaseController { * @param {Request} req - Request object. * @param {Response} res - Response object. */ - async getSaleInvoice(req: Request, res: Response, next: NextFunction) { + private async getSaleInvoice( + req: Request, + res: Response, + next: NextFunction + ) { const { id: saleInvoiceId } = req.params; const { tenantId, user } = req; try { - const saleInvoice = await this.saleInvoiceService.getSaleInvoice( + const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice( tenantId, saleInvoiceId, user @@ -384,7 +389,7 @@ export default class SaleInvoicesController extends BaseController { }, // PDF content type. [ACCEPT_TYPE.APPLICATION_PDF]: async () => { - const pdfContent = await this.saleInvoicePdf.saleInvoicePdf( + const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf( tenantId, saleInvoice ); @@ -420,7 +425,7 @@ export default class SaleInvoicesController extends BaseController { }; try { const { salesInvoices, filterMeta, pagination } = - await this.saleInvoiceService.salesInvoicesList(tenantId, filter); + await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter); return res.status(200).send({ sales_invoices: this.transfromToResponse(salesInvoices), @@ -448,10 +453,11 @@ export default class SaleInvoicesController extends BaseController { const { customerId } = this.matchedQueryData(req); try { - const salesInvoices = await this.saleInvoiceService.getPayableInvoices( - tenantId, - customerId - ); + const salesInvoices = + await this.saleInvoiceApplication.getReceivableSaleInvoices( + tenantId, + customerId + ); return res.status(200).send({ sales_invoices: this.transfromToResponse(salesInvoices), }); @@ -477,7 +483,7 @@ export default class SaleInvoicesController extends BaseController { const writeoffDTO = this.matchedBodyData(req); try { - const saleInvoice = await this.saleInvoiceWriteoff.writeOff( + const saleInvoice = await this.saleInvoiceApplication.writeOff( tenantId, invoiceId, writeoffDTO @@ -485,7 +491,7 @@ export default class SaleInvoicesController extends BaseController { return res.status(200).send({ id: saleInvoice.id, - message: 'The given sale invoice has been writte-off successfully.', + message: 'The given sale invoice has been written-off successfully.', }); } catch (error) { next(error); @@ -507,7 +513,7 @@ export default class SaleInvoicesController extends BaseController { const { id: invoiceId } = req.params; try { - const saleInvoice = await this.saleInvoiceWriteoff.cancelWrittenoff( + const saleInvoice = await this.saleInvoiceApplication.cancelWrittenoff( tenantId, invoiceId ); @@ -538,11 +544,12 @@ export default class SaleInvoicesController extends BaseController { const invoiceNotifySmsDTO = this.matchedBodyData(req); try { - const saleInvoice = await this.saleInvoiceSmsNotify.notifyBySms( - tenantId, - invoiceId, - invoiceNotifySmsDTO.notificationKey - ); + const saleInvoice = + await this.saleInvoiceApplication.notifySaleInvoiceBySms( + tenantId, + invoiceId, + invoiceNotifySmsDTO.notificationKey + ); return res.status(200).send({ id: saleInvoice.id, message: @@ -569,11 +576,12 @@ export default class SaleInvoicesController extends BaseController { const smsDetailsDTO = this.matchedQueryData(req); try { - const invoiceSmsDetails = await this.saleInvoiceSmsNotify.smsDetails( - tenantId, - invoiceId, - smsDetailsDTO - ); + const invoiceSmsDetails = + await this.saleInvoiceApplication.getSaleInvoiceSmsDetails( + tenantId, + invoiceId, + smsDetailsDTO + ); return res.status(200).send({ data: invoiceSmsDetails, }); @@ -599,7 +607,7 @@ export default class SaleInvoicesController extends BaseController { try { const invoicePayments = - await this.invoicePaymentsSerivce.getInvoicePayments( + await this.saleInvoiceApplication.getInvoicePayments( tenantId, invoiceId ); diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index bed4c2d93..d364826b6 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -2,34 +2,26 @@ 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 SaleReceiptService from '@/services/Sales/SalesReceipts'; -import SaleReceiptsPdfService from '@/services/Sales/Receipts/SaleReceiptsPdfService'; import BaseController from '../BaseController'; import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt'; import { ServiceError } from '@/exceptions'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import SaleReceiptNotifyBySms from '@/services/Sales/SaleReceiptNotifyBySms'; import CheckPolicies from '@/api/middleware/CheckPolicies'; import { AbilitySubject, SaleReceiptAction } from '@/interfaces'; +import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication'; @Service() export default class SalesReceiptsController extends BaseController { @Inject() - saleReceiptService: SaleReceiptService; + private saleReceiptsApplication: SaleReceiptApplication; @Inject() - saleReceiptsPdf: SaleReceiptsPdfService; - - @Inject() - dynamicListService: DynamicListingService; - - @Inject() - saleReceiptSmsNotify: SaleReceiptNotifyBySms; + private dynamicListService: DynamicListingService; /** * Router constructor. */ - router() { + public router() { const router = Router(); router.post( @@ -105,7 +97,7 @@ export default class SalesReceiptsController extends BaseController { * Sales receipt validation schema. * @return {Array} */ - get salesReceiptsValidationSchema() { + private get salesReceiptsValidationSchema() { return [ check('customer_id').exists().isNumeric().toInt(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), @@ -146,14 +138,14 @@ export default class SalesReceiptsController extends BaseController { /** * Specific sale receipt validation schema. */ - get specificReceiptValidationSchema() { + private get specificReceiptValidationSchema() { return [param('id').exists().isNumeric().toInt()]; } /** * List sales receipts validation schema. */ - get listSalesReceiptsValidationSchema() { + private get listSalesReceiptsValidationSchema() { return [ query('view_slug').optional().isString().trim(), query('stringified_filter_roles').optional().isJSON(), @@ -170,16 +162,21 @@ export default class SalesReceiptsController extends BaseController { * @param {Request} req * @param {Response} res */ - async newSaleReceipt(req: Request, res: Response, next: NextFunction) { + private async newSaleReceipt( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req); try { // Store the given sale receipt details with associated entries. - const storedSaleReceipt = await this.saleReceiptService.createSaleReceipt( - tenantId, - saleReceiptDTO - ); + const storedSaleReceipt = + await this.saleReceiptsApplication.createSaleReceipt( + tenantId, + saleReceiptDTO + ); return res.status(200).send({ id: storedSaleReceipt.id, message: 'Sale receipt has been created successfully.', @@ -194,13 +191,20 @@ export default class SalesReceiptsController extends BaseController { * @param {Request} req * @param {Response} res */ - async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) { + private async deleteSaleReceipt( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const { id: saleReceiptId } = req.params; try { // Deletes the sale receipt. - await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId); + await this.saleReceiptsApplication.deleteSaleReceipt( + tenantId, + saleReceiptId + ); return res.status(200).send({ id: saleReceiptId, @@ -217,14 +221,18 @@ export default class SalesReceiptsController extends BaseController { * @param {Request} req - * @param {Response} res - */ - async editSaleReceipt(req: Request, res: Response, next: NextFunction) { + private async editSaleReceipt( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const { id: saleReceiptId } = req.params; const saleReceipt = this.matchedBodyData(req); try { // Update the given sale receipt details. - await this.saleReceiptService.editSaleReceipt( + await this.saleReceiptsApplication.editSaleReceipt( tenantId, saleReceiptId, saleReceipt @@ -244,13 +252,20 @@ export default class SalesReceiptsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - async closeSaleReceipt(req: Request, res: Response, next: NextFunction) { + private async closeSaleReceipt( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const { id: saleReceiptId } = req.params; try { // Update the given sale receipt details. - await this.saleReceiptService.closeSaleReceipt(tenantId, saleReceiptId); + await this.saleReceiptsApplication.closeSaleReceipt( + tenantId, + saleReceiptId + ); return res.status(200).send({ id: saleReceiptId, message: 'Sale receipt has been closed successfully.', @@ -265,7 +280,11 @@ export default class SalesReceiptsController extends BaseController { * @param {Request} req * @param {Response} res */ - async getSalesReceipts(req: Request, res: Response, next: NextFunction) { + private async getSalesReceipts( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const filter = { sortOrder: 'desc', @@ -274,10 +293,9 @@ export default class SalesReceiptsController extends BaseController { pageSize: 12, ...this.matchedQueryData(req), }; - try { const { data, pagination, filterMeta } = - await this.saleReceiptService.salesReceiptsList(tenantId, filter); + await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter); const response = this.transfromToResponse({ data, @@ -301,11 +319,10 @@ export default class SalesReceiptsController extends BaseController { const { tenantId } = req; try { - const saleReceipt = await this.saleReceiptService.getSaleReceipt( + const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt( tenantId, saleReceiptId ); - res.format({ 'application/json': () => { return res @@ -313,10 +330,11 @@ export default class SalesReceiptsController extends BaseController { .send(this.transfromToResponse({ saleReceipt })); }, 'application/pdf': async () => { - const pdfContent = await this.saleReceiptsPdf.saleReceiptPdf( - tenantId, - saleReceipt - ); + const pdfContent = + await this.saleReceiptsApplication.getSaleReceiptPdf( + tenantId, + saleReceipt + ); res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdfContent.length, @@ -344,10 +362,11 @@ export default class SalesReceiptsController extends BaseController { const { id: receiptId } = req.params; try { - const saleReceipt = await this.saleReceiptSmsNotify.notifyBySms( - tenantId, - receiptId - ); + const saleReceipt = + await this.saleReceiptsApplication.saleReceiptNotifyBySms( + tenantId, + receiptId + ); return res.status(200).send({ id: saleReceipt.id, message: @@ -373,10 +392,11 @@ export default class SalesReceiptsController extends BaseController { const { id: receiptId } = req.params; try { - const smsDetails = await this.saleReceiptSmsNotify.smsDetails( - tenantId, - receiptId - ); + const smsDetails = + await this.saleReceiptsApplication.getSaleReceiptSmsDetails( + tenantId, + receiptId + ); return res.status(200).send({ data: smsDetails, }); diff --git a/packages/server/src/api/controllers/Sales/index.ts b/packages/server/src/api/controllers/Sales/index.ts index 46306b54f..2e3c987f2 100644 --- a/packages/server/src/api/controllers/Sales/index.ts +++ b/packages/server/src/api/controllers/Sales/index.ts @@ -1,10 +1,10 @@ import { Router } from 'express'; import { Container, Service } from 'typedi'; +import SalesInvoices from './SalesInvoices' import SalesEstimates from './SalesEstimates'; import SalesReceipts from './SalesReceipts'; -import SalesInvoices from './SalesInvoices' -import PaymentReceives from './PaymentReceives'; import CreditNotes from './CreditNotes'; +import PaymentReceives from './PaymentReceives'; @Service() export default class SalesController { /** diff --git a/packages/server/src/jobs/WriteInvoicesJEntries.ts b/packages/server/src/jobs/WriteInvoicesJEntries.ts index c3982fc22..d7a57d63a 100644 --- a/packages/server/src/jobs/WriteInvoicesJEntries.ts +++ b/packages/server/src/jobs/WriteInvoicesJEntries.ts @@ -1,7 +1,7 @@ import { Container } from 'typedi'; import events from '@/subscribers/events'; -import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost'; export default class WriteInvoicesJournalEntries { eventPublisher: EventPublisher; @@ -26,7 +26,7 @@ export default class WriteInvoicesJournalEntries { */ public async handler(job, done: Function): Promise { const { startingDate, tenantId } = job.attrs.data; - const salesInvoicesCost = Container.get(SalesInvoicesCost); + const salesInvoicesCost = Container.get(SaleInvoicesCost); try { await salesInvoicesCost.writeCostLotsGLEntries(tenantId, startingDate); diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index 769085788..94ac8adcf 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -7,11 +7,11 @@ import PaymentSyncBillBalance from '@/subscribers/PaymentMades/PaymentSyncBillBa import SaleReceiptInventoryTransactionsSubscriber from '@/subscribers/SaleReceipt/WriteInventoryTransactions'; import SaleInvoiceWriteInventoryTransactions from '@/subscribers/SaleInvoices/WriteInventoryTransactions'; import SaleInvoiceWriteGLEntriesSubscriber from '@/subscribers/SaleInvoices/WriteJournalEntries'; +import SaleReceiptWriteGLEntriesSubscriber from '@/subscribers/SaleReceipt/WriteJournalEntries'; import PaymentReceiveSyncInvoices from '@/subscribers/PaymentReceive/PaymentReceiveSyncInvoices'; import CashflowTransactionSubscriber from '@/services/Cashflow/CashflowTransactionSubscriber'; import PaymentReceivesWriteGLEntriesSubscriber from '@/subscribers/PaymentReceive/WriteGLEntries'; import InventorySubscriber from '@/subscribers/Inventory/Inventory'; -import SaleReceiptWriteGLEntriesSubscriber from '@/subscribers/SaleReceipt/WriteJournalEntries'; import { CustomerWriteGLOpeningBalanceSubscriber } from '@/services/Contacts/Customers/Subscribers/CustomerGLEntriesSubscriber'; import { VendorsWriteGLOpeningSubscriber } from '@/services/Contacts/Vendors/Subscribers/VendorGLEntriesSubscriber'; import SaleEstimateAutoSerialSubscriber from '@/subscribers/SaleEstimate/AutoIncrementSerial'; @@ -35,7 +35,7 @@ import PurgeAuthorizedUserOnceRoleMutate from '@/services/Roles/PurgeAuthorizedU import SendSmsNotificationToCustomer from '@/subscribers/SaleInvoices/SendSmsNotificationToCustomer'; import SendSmsNotificationSaleReceipt from '@/subscribers/SaleReceipt/SendSmsNotificationToCustomer'; import SendSmsNotificationPaymentReceive from '@/subscribers/PaymentReceive/SendSmsNotificationToCustomer'; -import SaleInvoiceWriteoffSubscriber from '@/services/Sales/SaleInvoiceWriteoffSubscriber'; +import SaleInvoiceWriteoffSubscriber from '@/services/Sales/Invoices/SaleInvoiceWriteoffSubscriber'; import LandedCostSyncCostTransactionsSubscriber from '@/services/Purchases/LandedCost/LandedCostSyncCostTransactionsSubscriber'; import LandedCostInventoryTransactionsSubscriber from '@/services/Purchases/LandedCost/LandedCostInventoryTransactionsSubscriber'; import CreditNoteGLEntriesSubscriber from '@/services/CreditNotes/CreditNoteGLEntriesSubscriber'; @@ -66,7 +66,6 @@ import { ActivateWarehousesSubscriber } from '@/services/Warehouses/ActivateWare import { ManualJournalWriteGLSubscriber } from '@/services/ManualJournals/ManualJournalGLEntriesSubscriber'; import { BillGLEntriesSubscriber } from '@/services/Purchases/Bills/BillGLEntriesSubscriber'; import { PaymentWriteGLEntriesSubscriber } from '@/services/Purchases/BillPayments/BillPaymentGLEntriesSubscriber'; - import BranchesIntegrationsSubscribers from '@/services/Branches/EventsProvider'; import WarehousesIntegrationsSubscribers from '@/services/Warehouses/EventsProvider'; import { WarehouseTransferAutoIncrementSubscriber } from '@/services/Warehouses/WarehousesTransfers/WarehouseTransferAutoIncrementSubscriber'; diff --git a/packages/server/src/models/Bill.ts b/packages/server/src/models/Bill.ts index 18d375d1e..b651671c1 100644 --- a/packages/server/src/models/Bill.ts +++ b/packages/server/src/models/Bill.ts @@ -5,7 +5,7 @@ import TenantModel from 'models/TenantModel'; import BillSettings from './Bill.Settings'; import ModelSetting from './ModelSetting'; import CustomViewBaseModel from './CustomViewBaseModel'; -import { DEFAULT_VIEWS } from '@/services/Purchases/constants'; +import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants'; import ModelSearchable from './ModelSearchable'; export default class Bill extends mixin(TenantModel, [ diff --git a/packages/server/src/models/SaleInvoice.ts b/packages/server/src/models/SaleInvoice.ts index 9c7fdfbfe..38f21f285 100644 --- a/packages/server/src/models/SaleInvoice.ts +++ b/packages/server/src/models/SaleInvoice.ts @@ -5,7 +5,7 @@ import TenantModel from 'models/TenantModel'; import ModelSetting from './ModelSetting'; import SaleInvoiceMeta from './SaleInvoice.Settings'; import CustomViewBaseModel from './CustomViewBaseModel'; -import { DEFAULT_VIEWS } from '@/services/Sales/constants'; +import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/constants'; import ModelSearchable from './ModelSearchable'; export default class SaleInvoice extends mixin(TenantModel, [ diff --git a/packages/server/src/services/Branches/DeleteBranch.ts b/packages/server/src/services/Branches/DeleteBranch.ts index bb40da392..738cf297e 100644 --- a/packages/server/src/services/Branches/DeleteBranch.ts +++ b/packages/server/src/services/Branches/DeleteBranch.ts @@ -8,6 +8,7 @@ import { IBranchDeletedPayload, IBranchDeletePayload } from '@/interfaces'; import { CURDBranch } from './CRUDBranch'; import { BranchValidator } from './BranchValidate'; import { ERRORS } from './constants'; + @Service() export class DeleteBranch extends CURDBranch { @Inject() diff --git a/packages/server/src/services/Branches/Integrations/ValidateBranchExistance.ts b/packages/server/src/services/Branches/Integrations/ValidateBranchExistance.ts index 8c3d12eee..9b5d5b1f0 100644 --- a/packages/server/src/services/Branches/Integrations/ValidateBranchExistance.ts +++ b/packages/server/src/services/Branches/Integrations/ValidateBranchExistance.ts @@ -14,8 +14,8 @@ export class ValidateBranchExistance { /** * Validate transaction branch id when the feature is active. - * @param {number} tenantId - * @param {number} branchId + * @param {number} tenantId + * @param {number} branchId * @returns {Promise} */ public validateTransactionBranchWhenActive = async ( @@ -32,18 +32,16 @@ export class ValidateBranchExistance { /** * Validate transaction branch id existance. - * @param {number} tenantId - * @param {number} branchId + * @param {number} tenantId + * @param {number} branchId * @return {Promise} */ public validateTransactionBranch = async ( tenantId: number, branchId: number | null ) => { - // this.validateBranchIdExistance(branchId); - // await this.validateBranchExistance(tenantId, branchId); }; @@ -62,7 +60,10 @@ export class ValidateBranchExistance { * @param tenantId * @param branchId */ - public validateBranchExistance = async (tenantId: number, branchId: number) => { + public validateBranchExistance = async ( + tenantId: number, + branchId: number + ) => { const { Branch } = this.tenancy.models(tenantId); const branch = await Branch.query().findById(branchId); diff --git a/packages/server/src/services/CreditNotes/CreditNoteApplyToInvoices.ts b/packages/server/src/services/CreditNotes/CreditNoteApplyToInvoices.ts index bb951dcb2..5156dbd56 100644 --- a/packages/server/src/services/CreditNotes/CreditNoteApplyToInvoices.ts +++ b/packages/server/src/services/CreditNotes/CreditNoteApplyToInvoices.ts @@ -1,5 +1,5 @@ import { Service, Inject } from 'typedi'; -import Knex from 'knex'; +import { Knex } from 'knex'; import { sumBy } from 'lodash'; import { ICreditNote, @@ -8,27 +8,31 @@ import { ISaleInvoice, } from '@/interfaces'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives'; import UnitOfWork from '@/services/UnitOfWork'; -import events from '@/subscribers/events'; +import { PaymentReceiveValidators } from '../Sales/PaymentReceives/PaymentReceiveValidators'; import BaseCreditNotes from './CreditNotes'; import { IApplyCreditToInvoicesDTO, IApplyCreditToInvoicesCreatedPayload, } from '@/interfaces'; import { ServiceError } from '@/exceptions'; +import events from '@/subscribers/events'; import { ERRORS } from './constants'; +import HasTenancyService from '../Tenancy/TenancyService'; @Service() export default class CreditNoteApplyToInvoices extends BaseCreditNotes { - @Inject('PaymentReceives') - paymentReceive: PaymentReceiveService; + @Inject() + private tenancy: HasTenancyService; @Inject() - uow: UnitOfWork; + private paymentReceiveValidators: PaymentReceiveValidators; @Inject() - eventPublisher: EventPublisher; + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; /** * Apply credit note to the given invoices. @@ -50,7 +54,7 @@ export default class CreditNoteApplyToInvoices extends BaseCreditNotes { ); // Retrieve the applied invoices that associated to the credit note customer. const appliedInvoicesEntries = - await this.paymentReceive.validateInvoicesIDsExistance( + await this.paymentReceiveValidators.validateInvoicesIDsExistance( tenantId, creditNote.customerId, applyCreditToInvoicesDTO.entries diff --git a/packages/server/src/services/CreditNotes/DeleteCreditNoteApplyToInvoices.ts b/packages/server/src/services/CreditNotes/DeleteCreditNoteApplyToInvoices.ts index 33b21c873..281cc3d88 100644 --- a/packages/server/src/services/CreditNotes/DeleteCreditNoteApplyToInvoices.ts +++ b/packages/server/src/services/CreditNotes/DeleteCreditNoteApplyToInvoices.ts @@ -1,24 +1,24 @@ import { Service, Inject } from 'typedi'; -import Knex from 'knex'; +import { Knex } from 'knex'; import { IApplyCreditToInvoicesDeletedPayload } from '@/interfaces'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives'; import UnitOfWork from '@/services/UnitOfWork'; import events from '@/subscribers/events'; import BaseCreditNotes from './CreditNotes'; import { ServiceError } from '@/exceptions'; import { ERRORS } from './constants'; +import HasTenancyService from '../Tenancy/TenancyService'; @Service() export default class DeletreCreditNoteApplyToInvoices extends BaseCreditNotes { - @Inject('PaymentReceives') - paymentReceive: PaymentReceiveService; + @Inject() + private uow: UnitOfWork; @Inject() - uow: UnitOfWork; + private eventPublisher: EventPublisher; @Inject() - eventPublisher: EventPublisher; + private tenancy: HasTenancyService; /** * Apply credit note to the given invoices. diff --git a/packages/server/src/services/InviteUsers/SyncSystemSendInvite.ts b/packages/server/src/services/InviteUsers/SyncSystemSendInvite.ts index 58dbe141b..c201fa739 100644 --- a/packages/server/src/services/InviteUsers/SyncSystemSendInvite.ts +++ b/packages/server/src/services/InviteUsers/SyncSystemSendInvite.ts @@ -12,10 +12,10 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; @Service() export default class SyncSystemSendInvite { @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; @Inject() - eventPublisher: EventPublisher; + private eventPublisher: EventPublisher; /** * Attaches events with handlers. diff --git a/packages/server/src/services/Items/ItemInvoicesTransactionsTransformer.ts b/packages/server/src/services/Items/ItemInvoicesTransactionsTransformer.ts index 9cab83380..a244f47a4 100644 --- a/packages/server/src/services/Items/ItemInvoicesTransactionsTransformer.ts +++ b/packages/server/src/services/Items/ItemInvoicesTransactionsTransformer.ts @@ -27,7 +27,7 @@ export class ItemInvoicesTransactionsTransformer extends Transformer { } /** - * + * Formatted invoice date. * @param item * @returns */ @@ -36,16 +36,17 @@ export class ItemInvoicesTransactionsTransformer extends Transformer { }; /** - * + * Formatted item quantity. + * @returns {string} */ public formattedQuantity = (entry): string => { return entry.quantity; }; /** - * + * Formatted date. * @param entry - * @returns + * @returns {string} */ public formattedRate = (entry): string => { return formatNumber(entry.rate, { diff --git a/packages/server/src/services/Items/ItemsEntriesService.ts b/packages/server/src/services/Items/ItemsEntriesService.ts index 354e529da..b92b821ca 100644 --- a/packages/server/src/services/Items/ItemsEntriesService.ts +++ b/packages/server/src/services/Items/ItemsEntriesService.ts @@ -3,8 +3,8 @@ import { Inject, Service } from 'typedi'; import { IItemEntry, IItemEntryDTO, IItem } from '@/interfaces'; import { ServiceError } from '@/exceptions'; import TenancyService from '@/services/Tenancy/TenancyService'; +import { ItemEntry } from '@/models'; import { entriesAmountDiff } from 'utils'; -import { ItemEntry } from 'models'; const ERRORS = { ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', @@ -16,7 +16,7 @@ const ERRORS = { @Service() export default class ItemsEntriesService { @Inject() - tenancy: TenancyService; + private tenancy: TenancyService; /** * Retrieve the inventory items entries of the reference id and type. @@ -235,7 +235,7 @@ export default class ItemsEntriesService { /** * Sets the cost/sell accounts to the invoice entries. */ - setItemsEntriesDefaultAccounts(tenantId: number) { + public setItemsEntriesDefaultAccounts(tenantId: number) { return async (entries: IItemEntry[]) => { const { Item } = this.tenancy.models(tenantId); @@ -258,10 +258,10 @@ export default class ItemsEntriesService { /** * Retrieve the total items entries. - * @param entries - * @returns + * @param entries + * @returns */ - getTotalItemsEntries(entries: ItemEntry[]): number { + public getTotalItemsEntries(entries: ItemEntry[]): number { return sumBy(entries, (e) => ItemEntry.calcAmount(e)); } } diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentBillSync.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentBillSync.ts new file mode 100644 index 000000000..3447fd2a3 --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentBillSync.ts @@ -0,0 +1,48 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { IBillPaymentEntryDTO } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { entriesAmountDiff } from '@/utils'; + +@Service() +export class BillPaymentBillSync { + @Inject() + private tenancy: HasTenancyService; + + /** + * Saves bills payment amount changes different. + * @param {number} tenantId - + * @param {IBillPaymentEntryDTO[]} paymentMadeEntries - + * @param {IBillPaymentEntryDTO[]} oldPaymentMadeEntries - + */ + public async saveChangeBillsPaymentAmount( + tenantId: number, + paymentMadeEntries: IBillPaymentEntryDTO[], + oldPaymentMadeEntries?: IBillPaymentEntryDTO[], + trx?: Knex.Transaction + ): Promise { + const { Bill } = this.tenancy.models(tenantId); + const opers: Promise[] = []; + + const diffEntries = entriesAmountDiff( + paymentMadeEntries, + oldPaymentMadeEntries, + 'paymentAmount', + 'billId' + ); + diffEntries.forEach( + (diffEntry: { paymentAmount: number; billId: number }) => { + if (diffEntry.paymentAmount === 0) { + return; + } + const oper = Bill.changePaymentAmount( + diffEntry.billId, + diffEntry.paymentAmount, + trx + ); + opers.push(oper); + } + ); + await Promise.all(opers); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts index f2118929e..e2f51d309 100644 --- a/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts @@ -99,7 +99,7 @@ export class BillPaymentGLEntries { /** * Retrieves the payment common entry. - * @param {IBillPayment} billPayment + * @param {IBillPayment} billPayment * @returns {} */ private getPaymentCommonEntry = (billPayment: IBillPayment) => { diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentTransactionTransformer.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentTransactionTransformer.ts index 7f42bcbcb..598609e44 100644 --- a/packages/server/src/services/Purchases/BillPayments/BillPaymentTransactionTransformer.ts +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentTransactionTransformer.ts @@ -24,7 +24,7 @@ export class BillPaymentTransactionTransformer extends Transformer { /** * Retrieve formatted bill payment date. * @param entry - * @returns + * @returns {string} */ protected formattedPaymentDate = (entry): string => { return this.formatDate(entry.payment.paymentDate); diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts new file mode 100644 index 000000000..3aa2902ca --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts @@ -0,0 +1,278 @@ +import { Inject, Service } from 'typedi'; +import { sumBy, difference } from 'lodash'; +import { + IBill, + IBillPaymentDTO, + IBillPaymentEntryDTO, + IBillPayment, + IBillPaymentEntry, +} from '@/interfaces'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; +import { ACCOUNT_TYPE } from '@/data/AccountTypes'; +import { BillPayment } from '@/models'; +import { ERRORS } from './constants'; + +@Service() +export class BillPaymentValidators { + @Inject() + private tenancy: TenancyService; + + /** + * Validates the payment existance. + * @param {BillPayment | undefined | null} payment + * @throws {ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND)} + */ + public async validateBillPaymentExistance( + payment: BillPayment | undefined | null + ) { + if (!payment) { + throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); + } + } + + /** + * Validates the bill payment existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + public async getPaymentMadeOrThrowError( + tenantid: number, + paymentMadeId: number + ) { + const { BillPayment } = this.tenancy.models(tenantid); + const billPayment = await BillPayment.query() + .withGraphFetched('entries') + .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} + */ + public async getPaymentAccountOrThrowError( + tenantId: number, + paymentAccountId: number + ) { + const { accountRepository } = this.tenancy.repositories(tenantId); + + const paymentAccount = await accountRepository.findOneById( + paymentAccountId + ); + if (!paymentAccount) { + throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); + } + // Validate the payment account type. + if ( + !paymentAccount.isAccountType([ + ACCOUNT_TYPE.BANK, + ACCOUNT_TYPE.CASH, + ACCOUNT_TYPE.OTHER_CURRENT_ASSET, + ]) + ) { + 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} + */ + public async validatePaymentNumber( + tenantId: number, + paymentMadeNumber: string, + notPaymentMadeId?: number + ) { + 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 + */ + public async validateBillsExistance( + tenantId: number, + billPaymentEntries: { billId: number }[], + vendorId: number + ) { + const { Bill } = this.tenancy.models(tenantId); + const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId); + + const storedBills = await Bill.query() + .whereIn('id', entriesBillsIds) + .where('vendor_id', vendorId); + + 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 the not opened bills. + const notOpenedBills = storedBills.filter((bill) => !bill.openedAt); + + if (notOpenedBills.length > 0) { + throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, { + notOpenedBills, + }); + } + return storedBills; + } + + /** + * Validate wether the payment amount bigger than the payable amount. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @return {void} + */ + public async validateBillsDueAmount( + tenantId: number, + billPaymentEntries: IBillPaymentEntryDTO[], + oldPaymentEntries: IBillPaymentEntry[] = [] + ) { + 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) => { + const oldEntries = oldPaymentEntries.filter( + (entry) => entry.billId === bill.id + ); + const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; + + return [ + bill.id, + { ...bill, dueAmount: bill.dueAmount + oldPaymentAmount }, + ]; + }) + ); + 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} + */ + public 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); + } + } + + /** + * * Validate the payment vendor whether modified. + * @param {string} billPaymentNo + */ + public validateVendorNotModified( + billPaymentDTO: IBillPaymentDTO, + oldBillPayment: IBillPayment + ) { + if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) { + throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY); + } + } + + /** + * Validates the payment account currency code. The deposit account curreny + * should be equals the customer currency code or the base currency. + * @param {string} paymentAccountCurrency + * @param {string} customerCurrency + * @param {string} baseCurrency + * @throws {ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID)} + */ + public validateWithdrawalAccountCurrency = ( + paymentAccountCurrency: string, + customerCurrency: string, + baseCurrency: string + ) => { + if ( + paymentAccountCurrency !== customerCurrency && + paymentAccountCurrency !== baseCurrency + ) { + throw new ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID); + } + }; + + /** + * Validates the given vendor has no associated payments. + * @param {number} tenantId + * @param {number} vendorId + */ + public async validateVendorHasNoPayments(tenantId: number, vendorId: number) { + const { BillPayment } = this.tenancy.models(tenantId); + + const payments = await BillPayment.query().where('vendor_id', vendorId); + + if (payments.length > 0) { + throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS); + } + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/BillPayments.ts b/packages/server/src/services/Purchases/BillPayments/BillPayments.ts deleted file mode 100644 index 0c5ffde28..000000000 --- a/packages/server/src/services/Purchases/BillPayments/BillPayments.ts +++ /dev/null @@ -1,713 +0,0 @@ -import { Inject, Service } from 'typedi'; -import { sumBy, difference } from 'lodash'; -import * as R from 'ramda'; -import { Knex } from 'knex'; -import events from '@/subscribers/events'; -import { - IBill, - IBillPaymentDTO, - IBillPaymentEntryDTO, - IBillPayment, - IBillPaymentsFilter, - IPaginationMeta, - IFilterMeta, - IBillPaymentEntry, - IBillPaymentEventCreatedPayload, - IBillPaymentEventEditedPayload, - IBillPaymentEventDeletedPayload, - IBillPaymentCreatingPayload, - IBillPaymentEditingPayload, - IBillPaymentDeletingPayload, - IVendor, -} from '@/interfaces'; -import JournalPosterService from '@/services/Sales/JournalPosterService'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import { entriesAmountDiff, formatDateFields } from 'utils'; -import { ServiceError } from '@/exceptions'; -import { ACCOUNT_TYPE } from '@/data/AccountTypes'; -import { BillPaymentTransformer } from './BillPaymentTransformer'; -import { ERRORS } from './constants'; -import UnitOfWork from '@/services/UnitOfWork'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; -import { TenantMetadata } from '@/system/models'; -import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -/** - * Bill payments service. - * @service - */ -@Service('BillPayments') -export default class BillPaymentsService implements IBillPaymentsService { - @Inject() - tenancy: TenancyService; - - @Inject() - journalService: JournalPosterService; - - @Inject() - dynamicListService: DynamicListingService; - - @Inject() - eventPublisher: EventPublisher; - - @Inject() - private transformer: TransformerInjectable; - - @Inject() - uow: UnitOfWork; - - @Inject() - private branchDTOTransform: BranchTransactionDTOTransform; - - /** - * 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() - .withGraphFetched('entries') - .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 { accountRepository } = this.tenancy.repositories(tenantId); - - const paymentAccount = await accountRepository.findOneById( - paymentAccountId - ); - if (!paymentAccount) { - throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); - } - // Validate the payment account type. - if ( - !paymentAccount.isAccountType([ - ACCOUNT_TYPE.BANK, - ACCOUNT_TYPE.CASH, - ACCOUNT_TYPE.OTHER_CURRENT_ASSET, - ]) - ) { - 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?: number - ) { - 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 - */ - public async validateBillsExistance( - tenantId: number, - billPaymentEntries: { billId: number }[], - vendorId: number - ) { - const { Bill } = this.tenancy.models(tenantId); - const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId); - - const storedBills = await Bill.query() - .whereIn('id', entriesBillsIds) - .where('vendor_id', vendorId); - - 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 the not opened bills. - const notOpenedBills = storedBills.filter((bill) => !bill.openedAt); - - if (notOpenedBills.length > 0) { - throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, { - notOpenedBills, - }); - } - return storedBills; - } - - /** - * 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[], - oldPaymentEntries: IBillPaymentEntry[] = [] - ) { - 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) => { - const oldEntries = oldPaymentEntries.filter( - (entry) => entry.billId === bill.id - ); - const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; - - return [ - bill.id, - { ...bill, dueAmount: bill.dueAmount + oldPaymentAmount }, - ]; - }) - ); - 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); - } - } - - /** - * * Validate the payment vendor whether modified. - * @param {string} billPaymentNo - */ - private validateVendorNotModified( - billPaymentDTO: IBillPaymentDTO, - oldBillPayment: IBillPayment - ) { - if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) { - throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY); - } - } - - /** - * Validates the payment account currency code. The deposit account curreny - * should be equals the customer currency code or the base currency. - * @param {string} paymentAccountCurrency - * @param {string} customerCurrency - * @param {string} baseCurrency - * @throws {ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID)} - */ - public validateWithdrawalAccountCurrency = ( - paymentAccountCurrency: string, - customerCurrency: string, - baseCurrency: string - ) => { - if ( - paymentAccountCurrency !== customerCurrency && - paymentAccountCurrency !== baseCurrency - ) { - throw new ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID); - } - }; - - /** - * Transforms create/edit DTO to model. - * @param {number} tenantId - * @param {IBillPaymentDTO} billPaymentDTO - Bill payment. - * @param {IBillPayment} oldBillPayment - Old bill payment. - * @return {Promise} - */ - async transformDTOToModel( - tenantId: number, - billPaymentDTO: IBillPaymentDTO, - vendor: IVendor, - oldBillPayment?: IBillPayment - ): Promise { - const initialDTO = { - ...formatDateFields(billPaymentDTO, ['paymentDate']), - amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), - currencyCode: vendor.currencyCode, - exchangeRate: billPaymentDTO.exchangeRate || 1, - entries: billPaymentDTO.entries, - }; - return R.compose( - this.branchDTOTransform.transformDTO(tenantId) - )(initialDTO); - } - - /** - * Creates a new bill payment transcations and store it to the storage - * with associated bills entries and journal transactions. - * - * Precedures:- - * ------ - * - Records the bill payment transaction. - * - Records the bill payment associated entries. - * - 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: IBillPaymentDTO - ): Promise { - const { BillPayment, Contact } = this.tenancy.models(tenantId); - - const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); - - // Retrieves the payment vendor or throw not found error. - const vendor = await Contact.query() - .findById(billPaymentDTO.vendorId) - .modify('vendor') - .throwIfNotFound(); - - // Transform create DTO to model object. - const billPaymentObj = await this.transformDTOToModel( - tenantId, - billPaymentDTO, - vendor - ); - // Validate the payment account existance and type. - const paymentAccount = await this.getPaymentAccountOrThrowError( - tenantId, - billPaymentObj.paymentAccountId - ); - // Validate the payment number uniquiness. - if (billPaymentObj.paymentNumber) { - await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber); - } - // Validates the bills existance and associated to the given vendor. - await this.validateBillsExistance( - tenantId, - billPaymentObj.entries, - billPaymentDTO.vendorId - ); - // Validates the bills due payment amount. - await this.validateBillsDueAmount(tenantId, billPaymentObj.entries); - - // Validates the withdrawal account currency code. - this.validateWithdrawalAccountCurrency( - paymentAccount.currencyCode, - vendor.currencyCode, - tenantMeta.baseCurrency - ); - // Writes bill payment transacation with associated transactions - // under unit-of-work envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onBillPaymentCreating` event. - await this.eventPublisher.emitAsync(events.billPayment.onCreating, { - tenantId, - billPaymentDTO, - trx, - } as IBillPaymentCreatingPayload); - - // Writes the bill payment graph to the storage. - const billPayment = await BillPayment.query(trx).insertGraphAndFetch({ - ...billPaymentObj, - }); - - // Triggers `onBillPaymentCreated` event. - await this.eventPublisher.emitAsync(events.billPayment.onCreated, { - tenantId, - billPayment, - billPaymentId: billPayment.id, - trx, - } as IBillPaymentEventCreatedPayload); - - return billPayment; - }); - } - - /** - * Edits the details of the given bill payment. - * - * Preceducres: - * ------ - * - Update the bill payment transaction. - * - Insert the new bill payment entries that have no ids. - * - Update the bill paymeny entries that have ids. - * - Delete the bill payment entries that not presented. - * - Re-insert the journal transactions and update the diff accounts balance. - * - Update the diff vendor balance. - * - Update the diff bill payment amount. - * ------ - * @param {number} tenantId - Tenant id - * @param {Integer} billPaymentId - * @param {BillPaymentDTO} billPayment - * @param {IBillPayment} oldBillPayment - */ - public async editBillPayment( - tenantId: number, - billPaymentId: number, - billPaymentDTO - ): Promise { - const { BillPayment, Contact } = this.tenancy.models(tenantId); - - const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); - - // - const oldBillPayment = await this.getPaymentMadeOrThrowError( - tenantId, - billPaymentId - ); - - // - const vendor = await Contact.query() - .modify('vendor') - .findById(billPaymentDTO.vendorId) - .throwIfNotFound(); - - // Transform bill payment DTO to model object. - const billPaymentObj = await this.transformDTOToModel( - tenantId, - billPaymentDTO, - vendor, - oldBillPayment - ); - // Validate vendor not modified. - this.validateVendorNotModified(billPaymentDTO, oldBillPayment); - - // Validate the payment account existance and type. - const paymentAccount = await this.getPaymentAccountOrThrowError( - tenantId, - billPaymentObj.paymentAccountId - ); - // Validate the items entries IDs existance on the storage. - await this.validateEntriesIdsExistance( - tenantId, - billPaymentId, - billPaymentObj.entries - ); - // Validate the bills existance and associated to the given vendor. - await this.validateBillsExistance( - tenantId, - billPaymentObj.entries, - billPaymentDTO.vendorId - ); - // Validates the bills due payment amount. - await this.validateBillsDueAmount( - tenantId, - billPaymentObj.entries, - oldBillPayment.entries - ); - // Validate the payment number uniquiness. - if (billPaymentObj.paymentNumber) { - await this.validatePaymentNumber( - tenantId, - billPaymentObj.paymentNumber, - billPaymentId - ); - } - // Validates the withdrawal account currency code. - this.validateWithdrawalAccountCurrency( - paymentAccount.currencyCode, - vendor.currencyCode, - tenantMeta.baseCurrency - ); - // Edits the bill transactions with associated transactions - // under unit-of-work envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onBillPaymentEditing` event. - await this.eventPublisher.emitAsync(events.billPayment.onEditing, { - tenantId, - oldBillPayment, - billPaymentDTO, - trx, - } as IBillPaymentEditingPayload); - - // Deletes the bill payment transaction graph from the storage. - const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({ - id: billPaymentId, - ...billPaymentObj, - }); - // Triggers `onBillPaymentEdited` event. - await this.eventPublisher.emitAsync(events.billPayment.onEdited, { - tenantId, - billPaymentId, - billPayment, - oldBillPayment, - trx, - } as IBillPaymentEventEditedPayload); - - return billPayment; - }); - } - - /** - * Deletes the bill payment and associated transactions. - * @param {number} tenantId - Tenant id. - * @param {Integer} billPaymentId - The given bill payment id. - * @return {Promise} - */ - public async deleteBillPayment(tenantId: number, billPaymentId: number) { - const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId); - - // Retrieve the bill payment or throw not found service error. - const oldBillPayment = await this.getPaymentMadeOrThrowError( - tenantId, - billPaymentId - ); - // Deletes the bill transactions with associated transactions under - // unit-of-work envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onBillPaymentDeleting` payload. - await this.eventPublisher.emitAsync(events.billPayment.onDeleting, { - tenantId, - trx, - oldBillPayment, - } as IBillPaymentDeletingPayload); - - // Deletes the bill payment assocaited entries. - await BillPaymentEntry.query(trx) - .where('bill_payment_id', billPaymentId) - .delete(); - - // Deletes the bill payment transaction. - await BillPayment.query(trx).where('id', billPaymentId).delete(); - - // Triggers `onBillPaymentDeleted` event. - await this.eventPublisher.emitAsync(events.billPayment.onDeleted, { - tenantId, - billPaymentId, - oldBillPayment, - trx, - } as IBillPaymentEventDeletedPayload); - }); - } - - /** - * Retrieve payment made associated bills. - * @param {number} tenantId - - * @param {number} billPaymentId - - */ - public async getPaymentBills(tenantId: number, billPaymentId: number) { - const { Bill } = this.tenancy.models(tenantId); - - const billPayment = await this.getPaymentMadeOrThrowError( - tenantId, - billPaymentId - ); - const paymentBillsIds = billPayment.entries.map((entry) => entry.id); - - const bills = await Bill.query().whereIn('id', paymentBillsIds); - - return bills; - } - - /** - * Retrieve bill payment. - * @param {number} tenantId - * @param {number} billPyamentId - * @return {Promise} - */ - public async getBillPayment( - tenantId: number, - billPyamentId: number - ): Promise { - const { BillPayment } = this.tenancy.models(tenantId); - - const billPayment = await BillPayment.query() - .withGraphFetched('entries.bill') - .withGraphFetched('vendor') - .withGraphFetched('paymentAccount') - .withGraphFetched('transactions') - .withGraphFetched('branch') - .findById(billPyamentId); - - if (!billPayment) { - throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); - } - return this.transformer.transform( - tenantId, - billPayment, - new BillPaymentTransformer() - ); - } - - private parseListFilterDTO(filterDTO) { - return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); - } - - /** - * Retrieve bill payment paginted and filterable list. - * @param {number} tenantId - * @param {IBillPaymentsFilter} billPaymentsFilter - */ - public async listBillPayments( - tenantId: number, - filterDTO: IBillPaymentsFilter - ): Promise<{ - billPayments: IBillPayment; - pagination: IPaginationMeta; - filterMeta: IFilterMeta; - }> { - const { BillPayment } = this.tenancy.models(tenantId); - - // Parses filter DTO. - const filter = this.parseListFilterDTO(filterDTO); - - // Dynamic list service. - const dynamicList = await this.dynamicListService.dynamicList( - tenantId, - BillPayment, - filter - ); - - const { results, pagination } = await BillPayment.query() - .onBuild((builder) => { - builder.withGraphFetched('vendor'); - builder.withGraphFetched('paymentAccount'); - - dynamicList.buildQuery()(builder); - }) - .pagination(filter.page - 1, filter.pageSize); - - // Transformes the bill payments models to POJO. - const billPayments = await this.transformer.transform( - tenantId, - results, - new BillPaymentTransformer() - ); - return { - billPayments, - pagination, - filterMeta: dynamicList.getResponseMeta(), - }; - } - - /** - * Saves bills payment amount changes different. - * @param {number} tenantId - - * @param {IBillPaymentEntryDTO[]} paymentMadeEntries - - * @param {IBillPaymentEntryDTO[]} oldPaymentMadeEntries - - */ - public async saveChangeBillsPaymentAmount( - tenantId: number, - paymentMadeEntries: IBillPaymentEntryDTO[], - oldPaymentMadeEntries?: IBillPaymentEntryDTO[], - trx?: Knex.Transaction - ): Promise { - const { Bill } = this.tenancy.models(tenantId); - const opers: Promise[] = []; - - const diffEntries = entriesAmountDiff( - paymentMadeEntries, - oldPaymentMadeEntries, - 'paymentAmount', - 'billId' - ); - diffEntries.forEach( - (diffEntry: { paymentAmount: number; billId: number }) => { - if (diffEntry.paymentAmount === 0) { - return; - } - const oper = Bill.changePaymentAmount( - diffEntry.billId, - diffEntry.paymentAmount, - trx - ); - opers.push(oper); - } - ); - await Promise.all(opers); - } - - /** - * Validates the given vendor has no associated payments. - * @param {number} tenantId - * @param {number} vendorId - */ - public async validateVendorHasNoPayments(tenantId: number, vendorId: number) { - const { BillPayment } = this.tenancy.models(tenantId); - - const payments = await BillPayment.query().where('vendor_id', vendorId); - - if (payments.length > 0) { - throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS); - } - } -} diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentsApplication.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentsApplication.ts new file mode 100644 index 000000000..c7f8923b1 --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentsApplication.ts @@ -0,0 +1,109 @@ +import { Inject, Service } from 'typedi'; +import { IBillPaymentDTO, IBillPayment } from '@/interfaces'; +import { CreateBillPayment } from './CreateBillPayment'; +import { DeleteBillPayment } from './DeleteBillPayment'; +import { EditBillPayment } from './EditBillPayment'; +import { GetBillPayments } from './GetBillPayments'; +import { GetBillPayment } from './GetBillPayment'; +import { GetPaymentBills } from './GetPaymentBills'; + +/** + * Bill payments application. + * @service + */ +@Service() +export class BillPaymentsApplication { + @Inject() + private createBillPaymentService: CreateBillPayment; + + @Inject() + private deleteBillPaymentService: DeleteBillPayment; + + @Inject() + private editBillPaymentService: EditBillPayment; + + @Inject() + private getBillPaymentsService: GetBillPayments; + + @Inject() + private getBillPaymentService: GetBillPayment; + + @Inject() + private getPaymentBillsService: GetPaymentBills; + + /** + * Creates a bill payment with associated GL entries. + * @param {number} tenantId + * @param {IBillPaymentDTO} billPaymentDTO + * @returns {Promise} + */ + public createBillPayment( + tenantId: number, + billPaymentDTO: IBillPaymentDTO + ): Promise { + return this.createBillPaymentService.createBillPayment( + tenantId, + billPaymentDTO + ); + } + + /** + * Delets the given bill payment with associated GL entries. + * @param {number} tenantId + * @param {number} billPaymentId + */ + public deleteBillPayment(tenantId: number, billPaymentId: number) { + return this.deleteBillPaymentService.deleteBillPayment( + tenantId, + billPaymentId + ); + } + + /** + * Edits the given bill payment with associated GL entries. + * @param {number} tenantId + * @param {number} billPaymentId + * @param billPaymentDTO + * @returns {Promise} + */ + public editBillPayment( + tenantId: number, + billPaymentId: number, + billPaymentDTO + ): Promise { + return this.editBillPaymentService.editBillPayment( + tenantId, + billPaymentId, + billPaymentDTO + ); + } + + /** + * Retrieves bill payments list. + * @param {number} tenantId + * @param filterDTO + * @returns + */ + public getBillPayments(tenantId: number, filterDTO: IBillPaymentsFilter) { + return this.getBillPaymentsService.getBillPayments(tenantId, filterDTO); + } + + /** + * Retrieve specific bill payment. + * @param {number} tenantId + * @param {number} billPyamentId + * @returns + */ + public getBillPayment(tenantId: number, billPyamentId: number) { + return this.getBillPaymentService.getBillPayment(tenantId, billPyamentId); + } + + /** + * Retrieve payment made associated bills. + * @param {number} tenantId - + * @param {number} billPaymentId - + */ + public getPaymentBills(tenantId: number, billPaymentId: number) { + return this.getPaymentBillsService.getPaymentBills(tenantId, billPaymentId); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentsPages.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentsPages.ts index 7d5a0572c..7b4e0d8fe 100644 --- a/packages/server/src/services/Purchases/BillPayments/BillPaymentsPages.ts +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentsPages.ts @@ -5,13 +5,10 @@ import { IBill, IBillPayment, IBillReceivePageEntry } from '@/interfaces'; import { ERRORS } from './constants'; import { ServiceError } from '@/exceptions'; -/** - * Bill payments edit and create pages services. - */ @Service() export default class BillPaymentsPages { @Inject() - tenancy: TenancyService; + private tenancy: TenancyService; /** * Retrieve bill payment with associated metadata. diff --git a/packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts b/packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts new file mode 100644 index 000000000..cefad23ec --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts @@ -0,0 +1,37 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { sumBy } from 'lodash'; +import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces'; +import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; +import { formatDateFields } from '@/utils'; + +@Service() +export class CommandBillPaymentDTOTransformer { + @Inject() + private branchDTOTransform: BranchTransactionDTOTransform; + + /** + * Transforms create/edit DTO to model. + * @param {number} tenantId + * @param {IBillPaymentDTO} billPaymentDTO - Bill payment. + * @param {IBillPayment} oldBillPayment - Old bill payment. + * @return {Promise} + */ + public async transformDTOToModel( + tenantId: number, + billPaymentDTO: IBillPaymentDTO, + vendor: IVendor, + oldBillPayment?: IBillPayment + ): Promise { + const initialDTO = { + ...formatDateFields(billPaymentDTO, ['paymentDate']), + amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), + currencyCode: vendor.currencyCode, + exchangeRate: billPaymentDTO.exchangeRate || 1, + entries: billPaymentDTO.entries, + }; + return R.compose( + this.branchDTOTransform.transformDTO(tenantId) + )(initialDTO); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/CreateBillPayment.ts b/packages/server/src/services/Purchases/BillPayments/CreateBillPayment.ts new file mode 100644 index 000000000..f2456f217 --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/CreateBillPayment.ts @@ -0,0 +1,124 @@ +import { Knex } from 'knex'; +import events from '@/subscribers/events'; +import { + IBillPaymentDTO, + IBillPayment, + IBillPaymentEventCreatedPayload, + IBillPaymentCreatingPayload, +} from '@/interfaces'; +import { TenantMetadata } from '@/system/models'; +import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { BillPaymentValidators } from './BillPaymentValidators'; +import { CommandBillPaymentDTOTransformer } from './CommandBillPaymentDTOTransformer'; + +@Service() +export class CreateBillPayment { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private validators: BillPaymentValidators; + + @Inject() + private commandTransformerDTO: CommandBillPaymentDTOTransformer; + + /** + * Creates a new bill payment transcations and store it to the storage + * with associated bills entries and journal transactions. + * ------ + * Precedures:- + * ------ + * - Records the bill payment transaction. + * - Records the bill payment associated entries. + * - 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: IBillPaymentDTO + ): Promise { + const { BillPayment, Contact } = this.tenancy.models(tenantId); + + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + + // Retrieves the payment vendor or throw not found error. + const vendor = await Contact.query() + .findById(billPaymentDTO.vendorId) + .modify('vendor') + .throwIfNotFound(); + + // Transform create DTO to model object. + const billPaymentObj = await this.commandTransformerDTO.transformDTOToModel( + tenantId, + billPaymentDTO, + vendor + ); + // Validate the payment account existance and type. + const paymentAccount = await this.validators.getPaymentAccountOrThrowError( + tenantId, + billPaymentObj.paymentAccountId + ); + // Validate the payment number uniquiness. + if (billPaymentObj.paymentNumber) { + await this.validators.validatePaymentNumber( + tenantId, + billPaymentObj.paymentNumber + ); + } + // Validates the bills existance and associated to the given vendor. + await this.validators.validateBillsExistance( + tenantId, + billPaymentObj.entries, + billPaymentDTO.vendorId + ); + // Validates the bills due payment amount. + await this.validators.validateBillsDueAmount( + tenantId, + billPaymentObj.entries + ); + // Validates the withdrawal account currency code. + this.validators.validateWithdrawalAccountCurrency( + paymentAccount.currencyCode, + vendor.currencyCode, + tenantMeta.baseCurrency + ); + // Writes bill payment transacation with associated transactions + // under unit-of-work envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onBillPaymentCreating` event. + await this.eventPublisher.emitAsync(events.billPayment.onCreating, { + tenantId, + billPaymentDTO, + trx, + } as IBillPaymentCreatingPayload); + + // Writes the bill payment graph to the storage. + const billPayment = await BillPayment.query(trx).insertGraphAndFetch({ + ...billPaymentObj, + }); + + // Triggers `onBillPaymentCreated` event. + await this.eventPublisher.emitAsync(events.billPayment.onCreated, { + tenantId, + billPayment, + billPaymentId: billPayment.id, + trx, + } as IBillPaymentEventCreatedPayload); + + return billPayment; + }); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts b/packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts new file mode 100644 index 000000000..4ab1c9a25 --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts @@ -0,0 +1,71 @@ +import { Knex } from 'knex'; +import UnitOfWork from '@/services/UnitOfWork'; +import { BillPaymentValidators } from './BillPaymentValidators'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { + IBillPaymentDeletingPayload, + IBillPaymentEventDeletedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; + +@Service() +export class DeleteBillPayment { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private validators: BillPaymentValidators; + + /** + * Deletes the bill payment and associated transactions. + * @param {number} tenantId - Tenant id. + * @param {Integer} billPaymentId - The given bill payment id. + * @return {Promise} + */ + public async deleteBillPayment(tenantId: number, billPaymentId: number) { + const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId); + + // Retrieve the bill payment or throw not found service error. + const oldBillPayment = await BillPayment.query() + .withGraphFetched('entries') + .findById(billPaymentId); + + // Validates the bill payment existance. + this.validators.validateBillPaymentExistance(oldBillPayment); + + // Deletes the bill transactions with associated transactions under + // unit-of-work envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onBillPaymentDeleting` payload. + await this.eventPublisher.emitAsync(events.billPayment.onDeleting, { + tenantId, + trx, + oldBillPayment, + } as IBillPaymentDeletingPayload); + + // Deletes the bill payment assocaited entries. + await BillPaymentEntry.query(trx) + .where('bill_payment_id', billPaymentId) + .delete(); + + // Deletes the bill payment transaction. + await BillPayment.query(trx).where('id', billPaymentId).delete(); + + // Triggers `onBillPaymentDeleted` event. + await this.eventPublisher.emitAsync(events.billPayment.onDeleted, { + tenantId, + billPaymentId, + oldBillPayment, + trx, + } as IBillPaymentEventDeletedPayload); + }); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts b/packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts new file mode 100644 index 000000000..20c72d38b --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts @@ -0,0 +1,146 @@ +import { Inject, Service } from 'typedi'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { BillPaymentValidators } from './BillPaymentValidators'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { + IBillPayment, + IBillPaymentEditingPayload, + IBillPaymentEventEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { Knex } from 'knex'; +import UnitOfWork from '@/services/UnitOfWork'; +import { CommandBillPaymentDTOTransformer } from './CommandBillPaymentDTOTransformer'; +import { TenantMetadata } from '@/system/models'; + +@Service() +export class EditBillPayment { + @Inject() + private validators: BillPaymentValidators; + + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private transformer: CommandBillPaymentDTOTransformer; + + /** + * Edits the details of the given bill payment. + * + * Preceducres: + * ------ + * - Update the bill payment transaction. + * - Insert the new bill payment entries that have no ids. + * - Update the bill paymeny entries that have ids. + * - Delete the bill payment entries that not presented. + * - Re-insert the journal transactions and update the diff accounts balance. + * - Update the diff vendor balance. + * - Update the diff bill payment amount. + * ------ + * @param {number} tenantId - Tenant id + * @param {Integer} billPaymentId + * @param {BillPaymentDTO} billPayment + * @param {IBillPayment} oldBillPayment + */ + public async editBillPayment( + tenantId: number, + billPaymentId: number, + billPaymentDTO + ): Promise { + const { BillPayment, Contact } = this.tenancy.models(tenantId); + + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + + const oldBillPayment = await BillPayment.query().findById(billPaymentId); + + // Validates the bill payment existance. + this.validators.validateBillPaymentExistance(oldBillPayment); + + // + const vendor = await Contact.query() + .modify('vendor') + .findById(billPaymentDTO.vendorId) + .throwIfNotFound(); + + // Transform bill payment DTO to model object. + const billPaymentObj = await this.transformer.transformDTOToModel( + tenantId, + billPaymentDTO, + vendor, + oldBillPayment + ); + // Validate vendor not modified. + this.validators.validateVendorNotModified(billPaymentDTO, oldBillPayment); + + // Validate the payment account existance and type. + const paymentAccount = await this.validators.getPaymentAccountOrThrowError( + tenantId, + billPaymentObj.paymentAccountId + ); + // Validate the items entries IDs existance on the storage. + await this.validators.validateEntriesIdsExistance( + tenantId, + billPaymentId, + billPaymentObj.entries + ); + // Validate the bills existance and associated to the given vendor. + await this.validators.validateBillsExistance( + tenantId, + billPaymentObj.entries, + billPaymentDTO.vendorId + ); + // Validates the bills due payment amount. + await this.validators.validateBillsDueAmount( + tenantId, + billPaymentObj.entries, + oldBillPayment.entries + ); + // Validate the payment number uniquiness. + if (billPaymentObj.paymentNumber) { + await this.validators.validatePaymentNumber( + tenantId, + billPaymentObj.paymentNumber, + billPaymentId + ); + } + // Validates the withdrawal account currency code. + this.validators.validateWithdrawalAccountCurrency( + paymentAccount.currencyCode, + vendor.currencyCode, + tenantMeta.baseCurrency + ); + // Edits the bill transactions with associated transactions + // under unit-of-work envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onBillPaymentEditing` event. + await this.eventPublisher.emitAsync(events.billPayment.onEditing, { + tenantId, + oldBillPayment, + billPaymentDTO, + trx, + } as IBillPaymentEditingPayload); + + // Deletes the bill payment transaction graph from the storage. + const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({ + id: billPaymentId, + ...billPaymentObj, + }); + // Triggers `onBillPaymentEdited` event. + await this.eventPublisher.emitAsync(events.billPayment.onEdited, { + tenantId, + billPaymentId, + billPayment, + oldBillPayment, + trx, + } as IBillPaymentEventEditedPayload); + + return billPayment; + }); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts b/packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts new file mode 100644 index 000000000..5984e8932 --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts @@ -0,0 +1,51 @@ +import { IBillPayment } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { ERRORS } from './constants'; +import { ServiceError } from '@/exceptions'; +import { BillPaymentTransformer } from './BillPaymentTransformer'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import { BillsValidators } from '../Bills/BillsValidators'; +import { BillPaymentValidators } from './BillPaymentValidators'; + +@Service() +export class GetBillPayment { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + @Inject() + private validators: BillPaymentValidators; + + /** + * Retrieve bill payment. + * @param {number} tenantId + * @param {number} billPyamentId + * @return {Promise} + */ + public async getBillPayment( + tenantId: number, + billPyamentId: number + ): Promise { + const { BillPayment } = this.tenancy.models(tenantId); + + const billPayment = await BillPayment.query() + .withGraphFetched('entries.bill') + .withGraphFetched('vendor') + .withGraphFetched('paymentAccount') + .withGraphFetched('transactions') + .withGraphFetched('branch') + .findById(billPyamentId); + + // Validates the bill payment existance. + this.validators.validateBillPaymentExistance(billPayment); + + return this.transformer.transform( + tenantId, + billPayment, + new BillPaymentTransformer() + ); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts b/packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts new file mode 100644 index 000000000..2c9fda01f --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts @@ -0,0 +1,74 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { + IBillPayment, + IBillPaymentsFilter, + IPaginationMeta, + IFilterMeta, +} from '@/interfaces'; +import { BillPaymentTransformer } from './BillPaymentTransformer'; +import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; + +@Service() +export class GetBillPayments { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private dynamicListService: DynamicListingService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieve bill payment paginted and filterable list. + * @param {number} tenantId + * @param {IBillPaymentsFilter} billPaymentsFilter + */ + public async getBillPayments( + tenantId: number, + filterDTO: IBillPaymentsFilter + ): Promise<{ + billPayments: IBillPayment; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + const { BillPayment } = this.tenancy.models(tenantId); + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + tenantId, + BillPayment, + filter + ); + const { results, pagination } = await BillPayment.query() + .onBuild((builder) => { + builder.withGraphFetched('vendor'); + builder.withGraphFetched('paymentAccount'); + + dynamicList.buildQuery()(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Transformes the bill payments models to POJO. + const billPayments = await this.transformer.transform( + tenantId, + results, + new BillPaymentTransformer() + ); + return { + billPayments, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } + + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts b/packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts new file mode 100644 index 000000000..7b86c8f04 --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts @@ -0,0 +1,32 @@ +import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { BillPaymentValidators } from './BillPaymentValidators'; + +@Service() +export class GetPaymentBills { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private validators: BillPaymentValidators; + + /** + * Retrieve payment made associated bills. + * @param {number} tenantId - + * @param {number} billPaymentId - + */ + public async getPaymentBills(tenantId: number, billPaymentId: number) { + const { Bill, BillPayment } = this.tenancy.models(tenantId); + + const billPayment = await BillPayment.query().findById(billPaymentId); + + // Validates the bill payment existance. + this.validators.validateBillPaymentExistance(billPayment); + + const paymentBillsIds = billPayment.entries.map((entry) => entry.id); + + const bills = await Bill.query().whereIn('id', paymentBillsIds); + + return bills; + } +} diff --git a/packages/server/src/services/Purchases/Bills.ts b/packages/server/src/services/Purchases/Bills.ts deleted file mode 100644 index a98632c36..000000000 --- a/packages/server/src/services/Purchases/Bills.ts +++ /dev/null @@ -1,751 +0,0 @@ -import { omit, sumBy } from 'lodash'; -import moment from 'moment'; -import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; -import { Knex } from 'knex'; -import composeAsync from 'async/compose'; -import events from '@/subscribers/events'; -import InventoryService from '@/services/Inventory/Inventory'; -import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import { formatDateFields, transformToMap } from 'utils'; -import { - IBillDTO, - IBill, - ISystemUser, - IBillEditDTO, - IPaginationMeta, - IFilterMeta, - IBillsFilter, - IBillsService, - IItemEntry, - IItemEntryDTO, - IBillCreatedPayload, - IBillEditedPayload, - IBIllEventDeletedPayload, - IBillEventDeletingPayload, - IBillEditingPayload, - IBillCreatingPayload, - IVendor, -} from '@/interfaces'; -import { ServiceError } from '@/exceptions'; -import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; -import JournalPosterService from '@/services/Sales/JournalPosterService'; -import { ERRORS } from './constants'; -import EntriesService from '@/services/Entries'; -import { PurchaseInvoiceTransformer } from './PurchaseInvoices/PurchaseInvoiceTransformer'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import UnitOfWork from '@/services/UnitOfWork'; -import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; -import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; -import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -/** - * Vendor bills services. - * @service - */ -@Service('Bills') -export default class BillsService - extends SalesInvoicesCost - implements IBillsService -{ - @Inject() - inventoryService: InventoryService; - - @Inject() - tenancy: TenancyService; - - @Inject() - eventPublisher: EventPublisher; - - @Inject('logger') - logger: any; - - @Inject() - dynamicListService: DynamicListingService; - - @Inject() - itemsEntriesService: ItemsEntriesService; - - @Inject() - journalPosterService: JournalPosterService; - - @Inject() - entriesService: EntriesService; - - @Inject() - transformer: TransformerInjectable; - - @Inject() - uow: UnitOfWork; - - @Inject() - private branchDTOTransform: BranchTransactionDTOTransform; - - @Inject() - private warehouseDTOTransform: WarehouseTransactionDTOTransform; - - /** - * Validates the given bill existance. - * @async - * @param {number} tenantId - - * @param {number} billId - - */ - public async getBillOrThrowError(tenantId: number, billId: number) { - const { Bill } = this.tenancy.models(tenantId); - - const foundBill = await Bill.query() - .findById(billId) - .withGraphFetched('entries'); - - if (!foundBill) { - throw new ServiceError(ERRORS.BILL_NOT_FOUND); - } - return foundBill; - } - - /** - * Validates the bill number existance. - * @async - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - private async validateBillNumberExists( - tenantId: number, - billNumber: string, - notBillId?: number - ) { - const { Bill } = this.tenancy.models(tenantId); - const foundBills = await Bill.query() - .where('bill_number', billNumber) - .onBuild((builder) => { - if (notBillId) { - builder.whereNot('id', notBillId); - } - }); - - if (foundBills.length > 0) { - throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS); - } - } - - /** - * Validate the bill has no payment entries. - * @param {number} tenantId - * @param {number} billId - Bill id. - */ - private async validateBillHasNoEntries(tenantId, billId: number) { - const { BillPaymentEntry } = this.tenancy.models(tenantId); - - // Retireve the bill associate payment made entries. - const entries = await BillPaymentEntry.query().where('bill_id', billId); - - if (entries.length > 0) { - throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES); - } - return entries; - } - - /** - * Validate the bill number require. - * @param {string} billNo - - */ - private validateBillNoRequire(billNo: string) { - if (!billNo) { - throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED); - } - } - - /** - * Validate bill transaction has no associated allocated landed cost transactions. - * @param {number} tenantId - * @param {number} billId - */ - private async validateBillHasNoLandedCost(tenantId: number, billId: number) { - const { BillLandedCost } = this.tenancy.models(tenantId); - - const billLandedCosts = await BillLandedCost.query().where( - 'billId', - billId - ); - if (billLandedCosts.length > 0) { - throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS); - } - } - - /** - * Validate transaction entries that have landed cost type should not be - * inventory items. - * @param {number} tenantId - - * @param {IItemEntryDTO[]} newEntriesDTO - - */ - public async validateCostEntriesShouldBeInventoryItems( - tenantId: number, - newEntriesDTO: IItemEntryDTO[] - ) { - const { Item } = this.tenancy.models(tenantId); - - const entriesItemsIds = newEntriesDTO.map((e) => e.itemId); - const entriesItems = await Item.query().whereIn('id', entriesItemsIds); - - const entriesItemsById = transformToMap(entriesItems, 'id'); - - // Filter the landed cost entries that not associated with inventory item. - const nonInventoryHasCost = newEntriesDTO.filter((entry) => { - const item = entriesItemsById.get(entry.itemId); - - return entry.landedCost && item.type !== 'inventory'; - }); - if (nonInventoryHasCost.length > 0) { - throw new ServiceError( - ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS - ); - } - } - - /** - * Sets the default cost account to the bill entries. - */ - private setBillEntriesDefaultAccounts(tenantId: number) { - return async (entries: IItemEntry[]) => { - const { Item } = this.tenancy.models(tenantId); - - const entriesItemsIds = entries.map((e) => e.itemId); - const items = await Item.query().whereIn('id', entriesItemsIds); - - return entries.map((entry) => { - const item = items.find((i) => i.id === entry.itemId); - - return { - ...entry, - ...(item.type !== 'inventory' && { - costAccountId: entry.costAccountId || item.costAccountId, - }), - }; - }); - }; - } - - /** - * Retrieve the bill entries total. - * @param {IItemEntry[]} entries - * @returns {number} - */ - private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number { - const { ItemEntry } = this.tenancy.models(tenantId); - - return sumBy(entries, (e) => ItemEntry.calcAmount(e)); - } - - /** - * Retrieve the bill landed cost amount. - * @param {IBillDTO} billDTO - * @returns {number} - */ - private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number { - const costEntries = billDTO.entries.filter((entry) => entry.landedCost); - - return this.getBillEntriesTotal(tenantId, costEntries); - } - - /** - * Converts create bill DTO to model. - * @param {number} tenantId - * @param {IBillDTO} billDTO - * @param {IBill} oldBill - * @returns {IBill} - */ - private async billDTOToModel( - tenantId: number, - billDTO: IBillDTO, - vendor: IVendor, - authorizedUser: ISystemUser, - oldBill?: IBill - ) { - const { ItemEntry } = this.tenancy.models(tenantId); - - const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e)); - - // Retrieve the landed cost amount from landed cost entries. - const landedCostAmount = this.getBillLandedCostAmount(tenantId, billDTO); - - // Bill number from DTO or from auto-increment. - const billNumber = billDTO.billNumber || oldBill?.billNumber; - - const initialEntries = billDTO.entries.map((entry) => ({ - reference_type: 'Bill', - ...omit(entry, ['amount']), - })); - const entries = await composeAsync( - // Sets the default cost account to the bill entries. - this.setBillEntriesDefaultAccounts(tenantId) - )(initialEntries); - - const initialDTO = { - ...formatDateFields(omit(billDTO, ['open', 'entries']), [ - 'billDate', - 'dueDate', - ]), - amount, - landedCostAmount, - currencyCode: vendor.currencyCode, - exchangeRate: billDTO.exchangeRate || 1, - billNumber, - entries, - // Avoid rewrite the open date in edit mode when already opened. - ...(billDTO.open && - !oldBill?.openedAt && { - openedAt: moment().toMySqlDateTime(), - }), - userId: authorizedUser.id, - }; - return R.compose( - this.branchDTOTransform.transformDTO(tenantId), - this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); - } - - /** - * Creates a new bill and stored it to the storage. - * ---- - * Precedures. - * ---- - * - Insert bill transactions to the storage. - * - Insert bill entries to the storage. - * - Increment the given vendor id. - * - Record bill journal transactions on the given accounts. - * - Record bill items inventory transactions. - * ---- - * @param {number} tenantId - The given tenant id. - * @param {IBillDTO} billDTO - - * @return {Promise} - */ - public async createBill( - tenantId: number, - billDTO: IBillDTO, - authorizedUser: ISystemUser - ): Promise { - const { Bill, Contact } = this.tenancy.models(tenantId); - - // Retrieves the given bill vendor or throw not found error. - const vendor = await Contact.query() - .modify('vendor') - .findById(billDTO.vendorId) - .throwIfNotFound(); - - // Validate the bill number uniqiness on the storage. - 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 - ); - // Validates the cost entries should be with inventory items. - await this.validateCostEntriesShouldBeInventoryItems( - tenantId, - billDTO.entries - ); - // Transform the bill DTO to model object. - const billObj = await this.billDTOToModel( - tenantId, - billDTO, - vendor, - authorizedUser - ); - // Write new bill transaction with associated transactions under UOW env. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onBillCreating` event. - await this.eventPublisher.emitAsync(events.bill.onCreating, { - trx, - billDTO, - tenantId, - } as IBillCreatingPayload); - - // Inserts the bill graph object to the storage. - const bill = await Bill.query(trx).upsertGraph(billObj); - - // Triggers `onBillCreated` event. - await this.eventPublisher.emitAsync(events.bill.onCreated, { - tenantId, - bill, - billId: bill.id, - trx, - } as IBillCreatedPayload); - - return bill; - }); - } - - /** - * Edits details of the given bill id with associated entries. - * - * Precedures: - * ------- - * - Update the bill transaction on the storage. - * - Update the bill entries on the storage and insert the not have id and delete - * once that not presented. - * - Increment the diff amount on the given vendor id. - * - Re-write the inventory transactions. - * - Re-write the bill journal transactions. - * ------ - * @param {number} tenantId - The given tenant id. - * @param {Integer} billId - The given bill id. - * @param {IBillEditDTO} billDTO - The given new bill details. - * @return {Promise} - */ - public async editBill( - tenantId: number, - billId: number, - billDTO: IBillEditDTO, - authorizedUser: ISystemUser - ): Promise { - const { Bill, Contact } = this.tenancy.models(tenantId); - - const oldBill = await this.getBillOrThrowError(tenantId, billId); - - // Retrieve vendor details or throw not found service error. - const vendor = await Contact.query() - .findById(billDTO.vendorId) - .modify('vendor') - .throwIfNotFound(); - - // Validate bill number uniqiness on the storage. - if (billDTO.billNumber) { - await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId); - } - // Validate the entries ids existance. - await this.itemsEntriesService.validateEntriesIdsExistance( - tenantId, - billId, - 'Bill', - billDTO.entries - ); - // Validate the items ids existance on the storage. - await this.itemsEntriesService.validateItemsIdsExistance( - tenantId, - billDTO.entries - ); - // Accept the purchasable items only. - await this.itemsEntriesService.validateNonPurchasableEntriesItems( - tenantId, - billDTO.entries - ); - // Transforms the bill DTO to model object. - const billObj = await this.billDTOToModel( - tenantId, - billDTO, - vendor, - authorizedUser, - oldBill - ); - // Validate landed cost entries that have allocated cost could not be deleted. - await this.entriesService.validateLandedCostEntriesNotDeleted( - oldBill.entries, - billObj.entries - ); - // Validate new landed cost entries should be bigger than new entries. - await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( - oldBill.entries, - billObj.entries - ); - // Edits bill transactions and associated transactions under UOW envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onBillEditing` event. - await this.eventPublisher.emitAsync(events.bill.onEditing, { - trx, - tenantId, - oldBill, - billDTO, - } as IBillEditingPayload); - - // Update the bill transaction. - const bill = await Bill.query(trx).upsertGraph({ - id: billId, - ...billObj, - }); - // Triggers event `onBillEdited`. - await this.eventPublisher.emitAsync(events.bill.onEdited, { - tenantId, - billId, - oldBill, - bill, - trx, - } as IBillEditedPayload); - - return bill; - }); - } - - /** - * Deletes the bill with associated entries. - * @param {Integer} billId - * @return {void} - */ - public async deleteBill(tenantId: number, billId: number) { - const { ItemEntry, Bill } = this.tenancy.models(tenantId); - - // Retrieve the given bill or throw not found error. - const oldBill = await this.getBillOrThrowError(tenantId, billId); - - // Validate the givne bill has no associated landed cost transactions. - await this.validateBillHasNoLandedCost(tenantId, billId); - - // Validate the purchase bill has no assocaited payments transactions. - await this.validateBillHasNoEntries(tenantId, billId); - - // Validate the given bill has no associated reconciled with vendor credits. - await this.validateBillHasNoAppliedToCredit(tenantId, billId); - - // Deletes bill transaction with associated transactions under - // unit-of-work envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onBillDeleting` event. - await this.eventPublisher.emitAsync(events.bill.onDeleting, { - trx, - tenantId, - oldBill, - } as IBillEventDeletingPayload); - - // Delete all associated bill entries. - await ItemEntry.query(trx) - .where('reference_type', 'Bill') - .where('reference_id', billId) - .delete(); - - // Delete the bill transaction. - await Bill.query(trx).findById(billId).delete(); - - // Triggers `onBillDeleted` event. - await this.eventPublisher.emitAsync(events.bill.onDeleted, { - tenantId, - billId, - oldBill, - trx, - } as IBIllEventDeletedPayload); - }); - } - - validateBillHasNoAppliedToCredit = async ( - tenantId: number, - billId: number - ) => { - const { VendorCreditAppliedBill } = this.tenancy.models(tenantId); - - const appliedTransactions = await VendorCreditAppliedBill.query().where( - 'billId', - billId - ); - if (appliedTransactions.length > 0) { - throw new ServiceError(ERRORS.BILL_HAS_APPLIED_TO_VENDOR_CREDIT); - } - }; - - /** - * Parses bills list filter DTO. - * @param filterDTO - - */ - private parseListFilterDTO(filterDTO) { - return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); - } - - /** - * Retrieve bills data table list. - * @param {number} tenantId - - * @param {IBillsFilter} billsFilter - - */ - public async getBills( - tenantId: number, - filterDTO: IBillsFilter - ): Promise<{ - bills: IBill; - pagination: IPaginationMeta; - filterMeta: IFilterMeta; - }> { - const { Bill } = this.tenancy.models(tenantId); - - // Parses bills list filter DTO. - const filter = this.parseListFilterDTO(filterDTO); - - // Dynamic list service. - const dynamicFilter = await this.dynamicListService.dynamicList( - tenantId, - Bill, - filter - ); - const { results, pagination } = await Bill.query() - .onBuild((builder) => { - builder.withGraphFetched('vendor'); - dynamicFilter.buildQuery()(builder); - }) - .pagination(filter.page - 1, filter.pageSize); - - // Tranform the bills to POJO. - const bills = await this.transformer.transform( - tenantId, - results, - new PurchaseInvoiceTransformer() - ); - return { - bills, - pagination, - filterMeta: dynamicFilter.getResponseMeta(), - }; - } - - /** - * Retrieve all due bills or for specific given vendor id. - * @param {number} tenantId - - * @param {number} vendorId - - */ - public async getDueBills( - tenantId: number, - vendorId?: number - ): Promise { - const { Bill } = this.tenancy.models(tenantId); - - const dueBills = await Bill.query().onBuild((query) => { - query.orderBy('bill_date', 'DESC'); - query.modify('dueBills'); - - if (vendorId) { - query.where('vendor_id', vendorId); - } - }); - return dueBills; - } - - /** - * Retrieve the given bill details with associated items entries. - * @param {Integer} billId - Specific bill. - * @returns {Promise} - */ - public async getBill(tenantId: number, billId: number): Promise { - const { Bill } = this.tenancy.models(tenantId); - - const bill = await Bill.query() - .findById(billId) - .withGraphFetched('vendor') - .withGraphFetched('entries.item') - .withGraphFetched('branch'); - - if (!bill) { - throw new ServiceError(ERRORS.BILL_NOT_FOUND); - } - return this.transformer.transform( - tenantId, - bill, - new PurchaseInvoiceTransformer() - ); - } - - /** - * Mark the bill as open. - * @param {number} tenantId - * @param {number} billId - */ - public async openBill(tenantId: number, billId: number): Promise { - const { Bill } = this.tenancy.models(tenantId); - - // Retrieve the given bill or throw not found error. - const oldBill = await this.getBillOrThrowError(tenantId, billId); - - if (oldBill.isOpen) { - throw new ServiceError(ERRORS.BILL_ALREADY_OPEN); - } - // - return this.uow.withTransaction(tenantId, async (trx) => { - // Record the bill opened at on the storage. - await Bill.query(trx).findById(billId).patch({ - openedAt: moment().toMySqlDateTime(), - }); - }); - } - - /** - * Records the inventory transactions from the given bill input. - * @param {Bill} bill - Bill model object. - * @param {number} billId - Bill id. - * @return {Promise} - */ - public async recordInventoryTransactions( - tenantId: number, - billId: number, - override?: boolean, - trx?: Knex.Transaction - ): Promise { - const { Bill } = this.tenancy.models(tenantId); - - // Retireve bill with assocaited entries and allocated cost entries. - const bill = await Bill.query(trx) - .findById(billId) - .withGraphFetched('entries.allocatedCostEntries'); - - // Loads the inventory items entries of the given sale invoice. - const inventoryEntries = - await this.itemsEntriesService.filterInventoryEntries( - tenantId, - bill.entries - ); - const transaction = { - transactionId: bill.id, - transactionType: 'Bill', - exchangeRate: bill.exchangeRate, - - date: bill.billDate, - direction: 'IN', - entries: inventoryEntries, - createdAt: bill.createdAt, - - warehouseId: bill.warehouseId, - }; - await this.inventoryService.recordInventoryTransactionsFromItemsEntries( - tenantId, - transaction, - override, - trx - ); - } - - /** - * Reverts the inventory transactions of the given bill id. - * @param {number} tenantId - Tenant id. - * @param {number} billId - Bill id. - * @return {Promise} - */ - public async revertInventoryTransactions( - tenantId: number, - billId: number, - trx?: Knex.Transaction - ) { - // Deletes the inventory transactions by the given reference id and type. - await this.inventoryService.deleteInventoryTransactions( - tenantId, - billId, - 'Bill', - trx - ); - } - - /** - * Validate the given vendor has no associated bills transactions. - * @param {number} tenantId - * @param {number} vendorId - Vendor id. - */ - public async validateVendorHasNoBills(tenantId: number, vendorId: number) { - const { Bill } = this.tenancy.models(tenantId); - - const bills = await Bill.query().where('vendor_id', vendorId); - - if (bills.length > 0) { - throw new ServiceError(ERRORS.VENDOR_HAS_BILLS); - } - } -} diff --git a/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts b/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts new file mode 100644 index 000000000..046e0177e --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts @@ -0,0 +1,130 @@ +import { omit, sumBy } from 'lodash'; +import moment from 'moment'; +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import composeAsync from 'async/compose'; +import { formatDateFields } from 'utils'; +import { + IBillDTO, + IBill, + ISystemUser, + IVendor, + IItemEntry, +} from '@/interfaces'; +import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; +import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export class BillDTOTransformer { + @Inject() + private branchDTOTransform: BranchTransactionDTOTransform; + + @Inject() + private warehouseDTOTransform: WarehouseTransactionDTOTransform; + + @Inject() + private tenancy: HasTenancyService; + + /** + * Retrieve the bill entries total. + * @param {IItemEntry[]} entries + * @returns {number} + */ + private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number { + const { ItemEntry } = this.tenancy.models(tenantId); + + return sumBy(entries, (e) => ItemEntry.calcAmount(e)); + } + + /** + * Retrieve the bill landed cost amount. + * @param {IBillDTO} billDTO + * @returns {number} + */ + private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number { + const costEntries = billDTO.entries.filter((entry) => entry.landedCost); + + return this.getBillEntriesTotal(tenantId, costEntries); + } + + /** + * Converts create bill DTO to model. + * @param {number} tenantId + * @param {IBillDTO} billDTO + * @param {IBill} oldBill + * @returns {IBill} + */ + public async billDTOToModel( + tenantId: number, + billDTO: IBillDTO, + vendor: IVendor, + authorizedUser: ISystemUser, + oldBill?: IBill + ) { + const { ItemEntry } = this.tenancy.models(tenantId); + + const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e)); + + // Retrieve the landed cost amount from landed cost entries. + const landedCostAmount = this.getBillLandedCostAmount(tenantId, billDTO); + + // Bill number from DTO or from auto-increment. + const billNumber = billDTO.billNumber || oldBill?.billNumber; + + const initialEntries = billDTO.entries.map((entry) => ({ + reference_type: 'Bill', + ...omit(entry, ['amount']), + })); + const entries = await composeAsync( + // Sets the default cost account to the bill entries. + this.setBillEntriesDefaultAccounts(tenantId) + )(initialEntries); + + const initialDTO = { + ...formatDateFields(omit(billDTO, ['open', 'entries']), [ + 'billDate', + 'dueDate', + ]), + amount, + landedCostAmount, + currencyCode: vendor.currencyCode, + exchangeRate: billDTO.exchangeRate || 1, + billNumber, + entries, + // Avoid rewrite the open date in edit mode when already opened. + ...(billDTO.open && + !oldBill?.openedAt && { + openedAt: moment().toMySqlDateTime(), + }), + userId: authorizedUser.id, + }; + return R.compose( + this.branchDTOTransform.transformDTO(tenantId), + this.warehouseDTOTransform.transformDTO(tenantId) + )(initialDTO); + } + + /** + * Sets the default cost account to the bill entries. + */ + private setBillEntriesDefaultAccounts(tenantId: number) { + return async (entries: IItemEntry[]) => { + const { Item } = this.tenancy.models(tenantId); + + const entriesItemsIds = entries.map((e) => e.itemId); + const items = await Item.query().whereIn('id', entriesItemsIds); + + return entries.map((entry) => { + const item = items.find((i) => i.id === entry.itemId); + + return { + ...entry, + ...(item.type !== 'inventory' && { + costAccountId: entry.costAccountId || item.costAccountId, + }), + }; + }); + }; + } +} diff --git a/packages/server/src/services/Purchases/Bills/BillGLEntriesSubscriber.ts b/packages/server/src/services/Purchases/Bills/BillGLEntriesSubscriber.ts index 8658c0f81..c38e65cb7 100644 --- a/packages/server/src/services/Purchases/Bills/BillGLEntriesSubscriber.ts +++ b/packages/server/src/services/Purchases/Bills/BillGLEntriesSubscriber.ts @@ -1,7 +1,5 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import BillsService from '@/services/Purchases/Bills'; import { IBillCreatedPayload, IBillEditedPayload, @@ -12,15 +10,12 @@ import { BillGLEntries } from './BillGLEntries'; @Service() export class BillGLEntriesSubscriber { @Inject() - tenancy: TenancyService; - - @Inject() - billGLEntries: BillGLEntries; + private billGLEntries: BillGLEntries; /** * Attachs events with handles. */ - attach(bus) { + public attach(bus) { bus.subscribe( events.bill.onCreated, this.handlerWriteJournalEntriesOnCreate diff --git a/packages/server/src/services/Purchases/Bills/BillInventoryTransactions.ts b/packages/server/src/services/Purchases/Bills/BillInventoryTransactions.ts new file mode 100644 index 000000000..1486ce2c5 --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/BillInventoryTransactions.ts @@ -0,0 +1,82 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import InventoryService from '@/services/Inventory/Inventory'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export class BillInventoryTransactions { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private inventoryService: InventoryService; + + /** + * Records the inventory transactions from the given bill input. + * @param {Bill} bill - Bill model object. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async recordInventoryTransactions( + tenantId: number, + billId: number, + override?: boolean, + trx?: Knex.Transaction + ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + // Retireve bill with assocaited entries and allocated cost entries. + const bill = await Bill.query(trx) + .findById(billId) + .withGraphFetched('entries.allocatedCostEntries'); + + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + tenantId, + bill.entries + ); + const transaction = { + transactionId: bill.id, + transactionType: 'Bill', + exchangeRate: bill.exchangeRate, + + date: bill.billDate, + direction: 'IN', + entries: inventoryEntries, + createdAt: bill.createdAt, + + warehouseId: bill.warehouseId, + }; + await this.inventoryService.recordInventoryTransactionsFromItemsEntries( + tenantId, + transaction, + override, + trx + ); + } + + /** + * Reverts the inventory transactions of the given bill id. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async revertInventoryTransactions( + tenantId: number, + billId: number, + trx?: Knex.Transaction + ) { + // Deletes the inventory transactions by the given reference id and type. + await this.inventoryService.deleteInventoryTransactions( + tenantId, + billId, + 'Bill', + trx + ); + } +} diff --git a/packages/server/src/services/Purchases/Bills/BillPaymentsGLEntriesRewriteSubscriber.ts b/packages/server/src/services/Purchases/Bills/BillPaymentsGLEntriesRewriteSubscriber.ts index f670f7b38..b2571fef3 100644 --- a/packages/server/src/services/Purchases/Bills/BillPaymentsGLEntriesRewriteSubscriber.ts +++ b/packages/server/src/services/Purchases/Bills/BillPaymentsGLEntriesRewriteSubscriber.ts @@ -11,7 +11,7 @@ export class BillPaymentsGLEntriesRewriteSubscriber { /** * Attachs events with handles. */ - attach(bus) { + public attach(bus) { bus.subscribe( events.bill.onEdited, this.handlerRewritePaymentsGLOnBillEdited diff --git a/packages/server/src/services/Purchases/Bills/BillsApplication.ts b/packages/server/src/services/Purchases/Bills/BillsApplication.ts new file mode 100644 index 000000000..1ffdd5aa4 --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/BillsApplication.ts @@ -0,0 +1,147 @@ +import { Inject, Service } from 'typedi'; +import { CreateBill } from './CreateBill'; +import { EditBill } from './EditBill'; +import { GetBill } from './GetBill'; +import { GetBills } from './GetBills'; +import { DeleteBill } from './DeleteBill'; +import { + IBill, + IBillDTO, + IBillEditDTO, + IBillsFilter, + IFilterMeta, + IPaginationMeta, + ISystemUser, +} from '@/interfaces'; +import { GetDueBills } from './GetDueBills'; +import { OpenBill } from './OpenBill'; +import { GetBillPayments } from './GetBillPayments'; + +@Service() +export class BillsApplication { + @Inject() + private createBillService: CreateBill; + + @Inject() + private editBillService: EditBill; + + @Inject() + private getBillService: GetBill; + + @Inject() + private getBillsService: GetBills; + + @Inject() + private deleteBillService: DeleteBill; + + @Inject() + private getDueBillsService: GetDueBills; + + @Inject() + private openBillService: OpenBill; + + @Inject() + private getBillPaymentsService: GetBillPayments; + + /** + * Creates a new bill with associated GL entries. + * @param {number} tenantId + * @param {IBillDTO} billDTO + * @param {ISystemUser} authorizedUser + * @returns + */ + public createBill( + tenantId: number, + billDTO: IBillDTO, + authorizedUser: ISystemUser + ): Promise { + return this.createBillService.createBill(tenantId, billDTO, authorizedUser); + } + + /** + * Edits the given bill with associated GL entries. + * @param {number} tenantId + * @param {number} billId + * @param {IBillEditDTO} billDTO + * @param {ISystemUser} authorizedUser + * @returns + */ + public editBill( + tenantId: number, + billId: number, + billDTO: IBillEditDTO, + authorizedUser: ISystemUser + ): Promise { + return this.editBillService.editBill( + tenantId, + billId, + billDTO, + authorizedUser + ); + } + + /** + * Deletes the given bill with associated GL entries. + * @param {number} tenantId + * @param {number} billId + * @returns {Promise} + */ + public deleteBill(tenantId: number, billId: number) { + return this.deleteBillService.deleteBill(tenantId, billId); + } + + /** + * Retrieve bills data table list. + * @param {number} tenantId - + * @param {IBillsFilter} billsFilter - + */ + public getBills( + tenantId: number, + filterDTO: IBillsFilter + ): Promise<{ + bills: IBill; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + return this.getBillsService.getBills(tenantId, filterDTO); + } + + /** + * Retrieves the given bill details. + * @param {number} tenantId + * @param {number} billId + * @returns + */ + public getBill(tenantId: number, billId: number): Promise { + return this.getBillService.getBill(tenantId, billId); + } + + /** + * Open the given bill. + * @param {number} tenantId + * @param {number} billId + * @returns {Promise} + */ + public openBill(tenantId: number, billId: number): Promise { + return this.openBillService.openBill(tenantId, billId); + } + + /** + * Retrieves due bills of the given vendor. + * @param {number} tenantId + * @param {number} vendorId + * @returns + */ + public getDueBills(tenantId: number, vendorId?: number) { + return this.getDueBillsService.getDueBills(tenantId, vendorId); + } + + /** + * Retrieve the specific bill associated payment transactions. + * @param {number} tenantId + * @param {number} billId + */ + public getBillPayments = async (tenantId: number, billId: number) => { + return this.getBillPaymentsService.getBillPayments(tenantId, billId); + }; +} diff --git a/packages/server/src/services/Purchases/Bills/BillsValidators.ts b/packages/server/src/services/Purchases/Bills/BillsValidators.ts new file mode 100644 index 000000000..cba38dfbb --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/BillsValidators.ts @@ -0,0 +1,154 @@ +import { ServiceError } from '@/exceptions'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { ERRORS } from './constants'; +import { IItemEntryDTO } from '@/interfaces'; +import { transformToMap } from '@/utils'; +import { Bill } from '@/models'; + +@Service() +export class BillsValidators { + @Inject() + private tenancy: HasTenancyService; + + /** + * Validates the bill existance. + * @param {Bill | undefined | null} bill + */ + public validateBillExistance(bill: Bill | undefined | null) { + if (!bill) { + throw new ServiceError(ERRORS.BILL_NOT_FOUND); + } + } + + /** + * Validates the bill number existance. + */ + public async validateBillNumberExists( + tenantId: number, + billNumber: string, + notBillId?: number + ) { + const { Bill } = this.tenancy.models(tenantId); + const foundBills = await Bill.query() + .where('bill_number', billNumber) + .onBuild((builder) => { + if (notBillId) { + builder.whereNot('id', notBillId); + } + }); + + if (foundBills.length > 0) { + throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS); + } + } + + /** + * Validate the bill has no payment entries. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + */ + public async validateBillHasNoEntries(tenantId, billId: number) { + const { BillPaymentEntry } = this.tenancy.models(tenantId); + + // Retireve the bill associate payment made entries. + const entries = await BillPaymentEntry.query().where('bill_id', billId); + + if (entries.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES); + } + return entries; + } + + /** + * Validate the bill number require. + * @param {string} billNo - + */ + public validateBillNoRequire(billNo: string) { + if (!billNo) { + throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED); + } + } + + /** + * Validate bill transaction has no associated allocated landed cost transactions. + * @param {number} tenantId + * @param {number} billId + */ + public async validateBillHasNoLandedCost(tenantId: number, billId: number) { + const { BillLandedCost } = this.tenancy.models(tenantId); + + const billLandedCosts = await BillLandedCost.query().where( + 'billId', + billId + ); + if (billLandedCosts.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS); + } + } + + /** + * Validate transaction entries that have landed cost type should not be + * inventory items. + * @param {number} tenantId - + * @param {IItemEntryDTO[]} newEntriesDTO - + */ + public async validateCostEntriesShouldBeInventoryItems( + tenantId: number, + newEntriesDTO: IItemEntryDTO[] + ) { + const { Item } = this.tenancy.models(tenantId); + + const entriesItemsIds = newEntriesDTO.map((e) => e.itemId); + const entriesItems = await Item.query().whereIn('id', entriesItemsIds); + + const entriesItemsById = transformToMap(entriesItems, 'id'); + + // Filter the landed cost entries that not associated with inventory item. + const nonInventoryHasCost = newEntriesDTO.filter((entry) => { + const item = entriesItemsById.get(entry.itemId); + + return entry.landedCost && item.type !== 'inventory'; + }); + if (nonInventoryHasCost.length > 0) { + throw new ServiceError( + ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS + ); + } + } + + /** + * + * @param {number} tenantId + * @param {number} billId + */ + public validateBillHasNoAppliedToCredit = async ( + tenantId: number, + billId: number + ) => { + const { VendorCreditAppliedBill } = this.tenancy.models(tenantId); + + const appliedTransactions = await VendorCreditAppliedBill.query().where( + 'billId', + billId + ); + if (appliedTransactions.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_APPLIED_TO_VENDOR_CREDIT); + } + }; + + /** + * Validate the given vendor has no associated bills transactions. + * @param {number} tenantId + * @param {number} vendorId - Vendor id. + */ + public async validateVendorHasNoBills(tenantId: number, vendorId: number) { + const { Bill } = this.tenancy.models(tenantId); + + const bills = await Bill.query().where('vendor_id', vendorId); + + if (bills.length > 0) { + throw new ServiceError(ERRORS.VENDOR_HAS_BILLS); + } + } +} diff --git a/packages/server/src/services/Purchases/Bills/CreateBill.ts b/packages/server/src/services/Purchases/Bills/CreateBill.ts new file mode 100644 index 000000000..47dfadecc --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/CreateBill.ts @@ -0,0 +1,116 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import events from '@/subscribers/events'; +import { + IBillDTO, + IBill, + ISystemUser, + IBillCreatedPayload, + IBillCreatingPayload, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import UnitOfWork from '@/services/UnitOfWork'; +import { BillsValidators } from './BillsValidators'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import { BillDTOTransformer } from './BillDTOTransformer'; + +@Service() +export class CreateBill { + @Inject() + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private validators: BillsValidators; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private transformerDTO: BillDTOTransformer; + + /** + * Creates a new bill and stored it to the storage. + * ---- + * Precedures. + * ---- + * - Insert bill transactions to the storage. + * - Insert bill entries to the storage. + * - Increment the given vendor id. + * - Record bill journal transactions on the given accounts. + * - Record bill items inventory transactions. + * ---- + * @param {number} tenantId - The given tenant id. + * @param {IBillDTO} billDTO - + * @return {Promise} + */ + public async createBill( + tenantId: number, + billDTO: IBillDTO, + authorizedUser: ISystemUser + ): Promise { + const { Bill, Contact } = this.tenancy.models(tenantId); + + // Retrieves the given bill vendor or throw not found error. + const vendor = await Contact.query() + .modify('vendor') + .findById(billDTO.vendorId) + .throwIfNotFound(); + + // Validate the bill number uniqiness on the storage. + await this.validators.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 + ); + // Validates the cost entries should be with inventory items. + await this.validators.validateCostEntriesShouldBeInventoryItems( + tenantId, + billDTO.entries + ); + // Transform the bill DTO to model object. + const billObj = await this.transformerDTO.billDTOToModel( + tenantId, + billDTO, + vendor, + authorizedUser + ); + // Write new bill transaction with associated transactions under UOW env. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onBillCreating` event. + await this.eventPublisher.emitAsync(events.bill.onCreating, { + trx, + billDTO, + tenantId, + } as IBillCreatingPayload); + + // Inserts the bill graph object to the storage. + const bill = await Bill.query(trx).upsertGraph(billObj); + + // Triggers `onBillCreated` event. + await this.eventPublisher.emitAsync(events.bill.onCreated, { + tenantId, + bill, + billId: bill.id, + trx, + } as IBillCreatedPayload); + + return bill; + }); + } +} diff --git a/packages/server/src/services/Purchases/Bills/DeleteBill.ts b/packages/server/src/services/Purchases/Bills/DeleteBill.ts new file mode 100644 index 000000000..51f58ddfc --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/DeleteBill.ts @@ -0,0 +1,80 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import events from '@/subscribers/events'; +import { + IBIllEventDeletedPayload, + IBillEventDeletingPayload, +} from '@/interfaces'; +import { BillsValidators } from './BillsValidators'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export class DeleteBill { + @Inject() + private validators: BillsValidators; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private tenancy: HasTenancyService; + + /** + * Deletes the bill with associated entries. + * @param {number} billId + * @return {void} + */ + public async deleteBill(tenantId: number, billId: number) { + const { ItemEntry, Bill } = this.tenancy.models(tenantId); + + // Retrieve the given bill or throw not found error. + const oldBill = await Bill.query() + .findById(billId) + .withGraphFetched('entries'); + + // Validates the bill existance. + this.validators.validateBillExistance(oldBill); + + // Validate the givne bill has no associated landed cost transactions. + await this.validators.validateBillHasNoLandedCost(tenantId, billId); + + // Validate the purchase bill has no assocaited payments transactions. + await this.validators.validateBillHasNoEntries(tenantId, billId); + + // Validate the given bill has no associated reconciled with vendor credits. + await this.validators.validateBillHasNoAppliedToCredit(tenantId, billId); + + // Deletes bill transaction with associated transactions under + // unit-of-work envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onBillDeleting` event. + await this.eventPublisher.emitAsync(events.bill.onDeleting, { + trx, + tenantId, + oldBill, + } as IBillEventDeletingPayload); + + // Delete all associated bill entries. + await ItemEntry.query(trx) + .where('reference_type', 'Bill') + .where('reference_id', billId) + .delete(); + + // Delete the bill transaction. + await Bill.query(trx).findById(billId).delete(); + + // Triggers `onBillDeleted` event. + await this.eventPublisher.emitAsync(events.bill.onDeleted, { + tenantId, + billId, + oldBill, + trx, + } as IBIllEventDeletedPayload); + }); + } +} diff --git a/packages/server/src/services/Purchases/Bills/EditBill.ts b/packages/server/src/services/Purchases/Bills/EditBill.ts new file mode 100644 index 000000000..a7259c730 --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/EditBill.ts @@ -0,0 +1,151 @@ +import { + IBill, + IBillEditDTO, + IBillEditedPayload, + IBillEditingPayload, + ISystemUser, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { BillsValidators } from './BillsValidators'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { Knex } from 'knex'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; +import EntriesService from '@/services/Entries'; +import { BillDTOTransformer } from './BillDTOTransformer'; + +@Service() +export class EditBill { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private validators: BillsValidators; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private entriesService: EntriesService; + + @Inject() + private transformerDTO: BillDTOTransformer; + + /** + * Edits details of the given bill id with associated entries. + * + * Precedures: + * ------- + * - Update the bill transaction on the storage. + * - Update the bill entries on the storage and insert the not have id and delete + * once that not presented. + * - Increment the diff amount on the given vendor id. + * - Re-write the inventory transactions. + * - Re-write the bill journal transactions. + * ------ + * @param {number} tenantId - The given tenant id. + * @param {Integer} billId - The given bill id. + * @param {IBillEditDTO} billDTO - The given new bill details. + * @return {Promise} + */ + public async editBill( + tenantId: number, + billId: number, + billDTO: IBillEditDTO, + authorizedUser: ISystemUser + ): Promise { + const { Bill, Contact } = this.tenancy.models(tenantId); + + // Retrieve the given bill or throw not found error. + const oldBill = await Bill.query() + .findById(billId) + .withGraphFetched('entries'); + + // Validate bill existance. + this.validators.validateBillExistance(oldBill); + + // Retrieve vendor details or throw not found service error. + const vendor = await Contact.query() + .findById(billDTO.vendorId) + .modify('vendor') + .throwIfNotFound(); + + // Validate bill number uniqiness on the storage. + if (billDTO.billNumber) { + await this.validators.validateBillNumberExists( + tenantId, + billDTO.billNumber, + billId + ); + } + // Validate the entries ids existance. + await this.itemsEntriesService.validateEntriesIdsExistance( + tenantId, + billId, + 'Bill', + billDTO.entries + ); + // Validate the items ids existance on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + tenantId, + billDTO.entries + ); + // Accept the purchasable items only. + await this.itemsEntriesService.validateNonPurchasableEntriesItems( + tenantId, + billDTO.entries + ); + // Transforms the bill DTO to model object. + const billObj = await this.transformerDTO.billDTOToModel( + tenantId, + billDTO, + vendor, + authorizedUser, + oldBill + ); + // Validate landed cost entries that have allocated cost could not be deleted. + await this.entriesService.validateLandedCostEntriesNotDeleted( + oldBill.entries, + billObj.entries + ); + // Validate new landed cost entries should be bigger than new entries. + await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( + oldBill.entries, + billObj.entries + ); + // Edits bill transactions and associated transactions under UOW envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onBillEditing` event. + await this.eventPublisher.emitAsync(events.bill.onEditing, { + trx, + tenantId, + oldBill, + billDTO, + } as IBillEditingPayload); + + // Update the bill transaction. + const bill = await Bill.query(trx).upsertGraph({ + id: billId, + ...billObj, + }); + // Triggers event `onBillEdited`. + await this.eventPublisher.emitAsync(events.bill.onEdited, { + tenantId, + billId, + oldBill, + bill, + trx, + } as IBillEditedPayload); + + return bill; + }); + } +} diff --git a/packages/server/src/services/Purchases/Bills/GetBill.ts b/packages/server/src/services/Purchases/Bills/GetBill.ts new file mode 100644 index 000000000..fb653c25c --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/GetBill.ts @@ -0,0 +1,42 @@ +import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import { IBill } from '@/interfaces'; +import { BillsValidators } from './BillsValidators'; +import { PurchaseInvoiceTransformer } from './PurchaseInvoiceTransformer'; + +@Service() +export class GetBill { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + @Inject() + private validators: BillsValidators; + + /** + * Retrieve the given bill details with associated items entries. + * @param {Integer} billId - Specific bill. + * @returns {Promise} + */ + public async getBill(tenantId: number, billId: number): Promise { + const { Bill } = this.tenancy.models(tenantId); + + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('vendor') + .withGraphFetched('entries.item') + .withGraphFetched('branch'); + + // Validates the bill existance. + this.validators.validateBillExistance(bill); + + return this.transformer.transform( + tenantId, + bill, + new PurchaseInvoiceTransformer() + ); + } +} diff --git a/packages/server/src/services/Purchases/BillPaymentsService.ts b/packages/server/src/services/Purchases/Bills/GetBillPayments.ts similarity index 86% rename from packages/server/src/services/Purchases/BillPaymentsService.ts rename to packages/server/src/services/Purchases/Bills/GetBillPayments.ts index faa321181..7b99d73e8 100644 --- a/packages/server/src/services/Purchases/BillPaymentsService.ts +++ b/packages/server/src/services/Purchases/Bills/GetBillPayments.ts @@ -1,10 +1,10 @@ import { Service, Inject } from 'typedi'; import TenancyService from '@/services/Tenancy/TenancyService'; -import { BillPaymentTransactionTransformer } from './BillPayments/BillPaymentTransactionTransformer'; +import { BillPaymentTransactionTransformer } from '../BillPayments/BillPaymentTransactionTransformer'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; @Service() -export default class BillPaymentsService { +export class GetBillPayments { @Inject() private tenancy: TenancyService; diff --git a/packages/server/src/services/Purchases/Bills/GetBills.ts b/packages/server/src/services/Purchases/Bills/GetBills.ts new file mode 100644 index 000000000..1ea19797d --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/GetBills.ts @@ -0,0 +1,76 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { + IBill, + IBillsFilter, + IFilterMeta, + IPaginationMeta, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { PurchaseInvoiceTransformer } from './PurchaseInvoiceTransformer'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import DynamicListingService from '@/services/DynamicListing/DynamicListService'; + +@Service() +export class GetBills { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + @Inject() + private dynamicListService: DynamicListingService; + + /** + * Retrieve bills data table list. + * @param {number} tenantId - + * @param {IBillsFilter} billsFilter - + */ + public async getBills( + tenantId: number, + filterDTO: IBillsFilter + ): Promise<{ + bills: IBill; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + const { Bill } = this.tenancy.models(tenantId); + + // Parses bills list filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + tenantId, + Bill, + filter + ); + const { results, pagination } = await Bill.query() + .onBuild((builder) => { + builder.withGraphFetched('vendor'); + dynamicFilter.buildQuery()(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Tranform the bills to POJO. + const bills = await this.transformer.transform( + tenantId, + results, + new PurchaseInvoiceTransformer() + ); + return { + bills, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + + /** + * Parses bills list filter DTO. + * @param filterDTO - + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server/src/services/Purchases/Bills/GetDueBills.ts b/packages/server/src/services/Purchases/Bills/GetDueBills.ts new file mode 100644 index 000000000..87792ae49 --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/GetDueBills.ts @@ -0,0 +1,32 @@ +import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { IBill } from '@/interfaces'; + +@Service() +export class GetDueBills { + @Inject() + private tenancy: HasTenancyService; + + /** + * Retrieve all due bills or for specific given vendor id. + * @param {number} tenantId - + * @param {number} vendorId - + */ + public async getDueBills( + tenantId: number, + vendorId?: number + ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + const dueBills = await Bill.query().onBuild((query) => { + query.orderBy('bill_date', 'DESC'); + query.modify('dueBills'); + + if (vendorId) { + query.where('vendor_id', vendorId); + } + }); + return dueBills; + } +} diff --git a/packages/server/src/services/Purchases/Bills/OpenBill.ts b/packages/server/src/services/Purchases/Bills/OpenBill.ts new file mode 100644 index 000000000..a2c2039b3 --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/OpenBill.ts @@ -0,0 +1,46 @@ +import moment from 'moment'; +import { Inject, Service } from 'typedi'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './constants'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { BillsValidators } from './BillsValidators'; + +@Service() +export class OpenBill { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private validators: BillsValidators; + + /** + * Mark the bill as open. + * @param {number} tenantId + * @param {number} billId + */ + public async openBill(tenantId: number, billId: number): Promise { + const { Bill } = this.tenancy.models(tenantId); + + // Retrieve the given bill or throw not found error. + const oldBill = await Bill.query() + .findById(billId) + .withGraphFetched('entries'); + + // Validates the bill existance. + this.validators.validateBillExistance(oldBill); + + if (oldBill.isOpen) { + throw new ServiceError(ERRORS.BILL_ALREADY_OPEN); + } + return this.uow.withTransaction(tenantId, async (trx) => { + // Record the bill opened at on the storage. + await Bill.query(trx).findById(billId).patch({ + openedAt: moment().toMySqlDateTime(), + }); + }); + } +} diff --git a/packages/server/src/services/Purchases/PurchaseInvoices/PurchaseInvoiceTransformer.ts b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts similarity index 100% rename from packages/server/src/services/Purchases/PurchaseInvoices/PurchaseInvoiceTransformer.ts rename to packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts diff --git a/packages/server/src/services/Purchases/constants.ts b/packages/server/src/services/Purchases/Bills/constants.ts similarity index 100% rename from packages/server/src/services/Purchases/constants.ts rename to packages/server/src/services/Purchases/Bills/constants.ts diff --git a/packages/server/src/services/Purchases/LandedCost/AllocateLandedCost.ts b/packages/server/src/services/Purchases/LandedCost/AllocateLandedCost.ts index 5aaf8fde6..958fb3370 100644 --- a/packages/server/src/services/Purchases/LandedCost/AllocateLandedCost.ts +++ b/packages/server/src/services/Purchases/LandedCost/AllocateLandedCost.ts @@ -41,13 +41,16 @@ export default class AllocateLandedCost extends BaseLandedCostService { allocateCostDTO: ILandedCostDTO, billId: number ): Promise => { - const { BillLandedCost } = this.tenancy.models(tenantId); + const { BillLandedCost, Bill } = this.tenancy.models(tenantId); // Retrieve total cost of allocated items. const amount = this.getAllocateItemsCostTotal(allocateCostDTO); // Retrieve the purchase invoice or throw not found error. - const bill = await this.billsService.getBillOrThrowError(tenantId, billId); + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('entries') + .throwIfNotFound(); // Retrieve landed cost transaction or throw not found service error. const costTransaction = await this.getLandedCostOrThrowError( diff --git a/packages/server/src/services/Purchases/LandedCost/BaseLandedCost.ts b/packages/server/src/services/Purchases/LandedCost/BaseLandedCost.ts index 9bef3dd53..c133a7205 100644 --- a/packages/server/src/services/Purchases/LandedCost/BaseLandedCost.ts +++ b/packages/server/src/services/Purchases/LandedCost/BaseLandedCost.ts @@ -1,6 +1,5 @@ import { Inject, Service } from 'typedi'; import { difference, sumBy } from 'lodash'; -import BillsService from '../Bills'; import { ServiceError } from '@/exceptions'; import { IItemEntry, @@ -13,14 +12,10 @@ import { } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import TransactionLandedCost from './TransctionLandedCost'; -import { ERRORS } from './utils'; -import { CONFIG } from './utils'; +import { ERRORS, CONFIG } from './utils'; @Service() export default class BaseLandedCostService { - @Inject() - public billsService: BillsService; - @Inject() public tenancy: HasTenancyService; diff --git a/packages/server/src/services/Purchases/LandedCost/LandedCostSyncCostTransactionsSubscriber.ts b/packages/server/src/services/Purchases/LandedCost/LandedCostSyncCostTransactionsSubscriber.ts index 4fa1470b5..6b9526e43 100644 --- a/packages/server/src/services/Purchases/LandedCost/LandedCostSyncCostTransactionsSubscriber.ts +++ b/packages/server/src/services/Purchases/LandedCost/LandedCostSyncCostTransactionsSubscriber.ts @@ -9,7 +9,7 @@ import LandedCostSyncCostTransactions from './LandedCostSyncCostTransactions'; @Service() export default class LandedCostSyncCostTransactionsSubscriber { @Inject() - landedCostSyncCostTransaction: LandedCostSyncCostTransactions; + private landedCostSyncCostTransaction: LandedCostSyncCostTransactions; /** * Attaches events with handlers. diff --git a/packages/server/src/services/Purchases/LandedCost/LandedCostTransactions.ts b/packages/server/src/services/Purchases/LandedCost/LandedCostTransactions.ts index 041ffce32..5195ea368 100644 --- a/packages/server/src/services/Purchases/LandedCost/LandedCostTransactions.ts +++ b/packages/server/src/services/Purchases/LandedCost/LandedCostTransactions.ts @@ -9,20 +9,12 @@ import { ILandedCostTransactionEntryDOJO, } from '@/interfaces'; import TransactionLandedCost from './TransctionLandedCost'; -import BillsService from '../Bills'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; import { formatNumber } from 'utils'; @Service() export default class LandedCostTranasctions { @Inject() - transactionLandedCost: TransactionLandedCost; - - @Inject() - billsService: BillsService; - - @Inject() - tenancy: HasTenancyService; + private transactionLandedCost: TransactionLandedCost; /** * Retrieve the landed costs based on the given query. diff --git a/packages/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts b/packages/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts index dc9350175..9a8741887 100644 --- a/packages/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts +++ b/packages/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts @@ -16,13 +16,13 @@ import { ERRORS } from './utils'; @Service() export default class TransactionLandedCost { @Inject() - billLandedCost: BillLandedCost; + private billLandedCost: BillLandedCost; @Inject() - expenseLandedCost: ExpenseLandedCost; + private expenseLandedCost: ExpenseLandedCost; @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; /** * Retrieve the cost transaction code model. diff --git a/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditToBills.ts b/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditToBills.ts index 6e326bcdd..1fa7a5628 100644 --- a/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditToBills.ts +++ b/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditToBills.ts @@ -1,5 +1,5 @@ import { Service, Inject } from 'typedi'; -import Knex from 'knex'; +import { Knex } from 'knex'; import { sumBy } from 'lodash'; import { IVendorCredit, @@ -9,27 +9,27 @@ import { IBill, } from '@/interfaces'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives'; import UnitOfWork from '@/services/UnitOfWork'; import events from '@/subscribers/events'; import VendorCredit from '../BaseVendorCredit'; -import BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments'; import { ServiceError } from '@/exceptions'; +import { BillPaymentValidators } from '../../BillPayments/BillPaymentValidators'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; import { ERRORS } from '../constants'; @Service() export default class ApplyVendorCreditToBills extends VendorCredit { - @Inject('PaymentReceives') - paymentReceive: PaymentReceiveService; + @Inject() + private tenancy: HasTenancyService; @Inject() - uow: UnitOfWork; + private uow: UnitOfWork; @Inject() - eventPublisher: EventPublisher; + private eventPublisher: EventPublisher; @Inject() - billPayment: BillPaymentsService; + private billPaymentValidators: BillPaymentValidators; /** * Apply credit note to the given invoices. @@ -55,11 +55,12 @@ export default class ApplyVendorCreditToBills extends VendorCredit { vendorCredit ); // Validate bills entries existance. - const appliedBills = await this.billPayment.validateBillsExistance( - tenantId, - vendorCreditAppliedModel.entries, - vendorCredit.vendorId - ); + const appliedBills = + await this.billPaymentValidators.validateBillsExistance( + tenantId, + vendorCreditAppliedModel.entries, + vendorCredit.vendorId + ); // Validate bills has remaining amount to apply. this.validateBillsRemainingAmount( appliedBills, diff --git a/packages/server/src/services/Sales/Estimates/ApproveSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/ApproveSaleEstimate.ts new file mode 100644 index 000000000..e83009096 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/ApproveSaleEstimate.ts @@ -0,0 +1,75 @@ +import { Inject, Service } from 'typedi'; +import { ServiceError } from '@/exceptions'; +import { + ISaleEstimateApprovedEvent, + ISaleEstimateApprovingEvent, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { ERRORS } from './constants'; +import { Knex } from 'knex'; +import events from '@/subscribers/events'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import moment from 'moment'; + +@Service() +export class ApproveSaleEstimate { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; + + /** + * Mark the sale estimate as approved from the customer. + * @param {number} tenantId + * @param {number} saleEstimateId + */ + public async approveSaleEstimate( + tenantId: number, + saleEstimateId: number + ): Promise { + const { SaleEstimate } = this.tenancy.models(tenantId); + + // Retrieve details of the given sale estimate id. + const oldSaleEstimate = await SaleEstimate.query() + .findById(saleEstimateId) + .throwIfNotFound(); + + // Throws error in case the sale estimate still not delivered to customer. + if (!oldSaleEstimate.isDelivered) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED); + } + // Throws error in case the sale estimate already approved. + if (oldSaleEstimate.isApproved) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_APPROVED); + } + // Triggers `onSaleEstimateApproving` event. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleEstimateApproving` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onApproving, { + trx, + tenantId, + oldSaleEstimate, + } as ISaleEstimateApprovingEvent); + + // Update estimate as approved. + const saleEstimate = await SaleEstimate.query(trx) + .where('id', saleEstimateId) + .patch({ + approvedAt: moment().toMySqlDateTime(), + rejectedAt: null, + }); + // Triggers `onSaleEstimateApproved` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onApproved, { + trx, + tenantId, + oldSaleEstimate, + saleEstimate, + } as ISaleEstimateApprovedEvent); + }); + } +} diff --git a/packages/server/src/services/Sales/Estimates/ConvetSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/ConvetSaleEstimate.ts new file mode 100644 index 000000000..64a730f96 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/ConvetSaleEstimate.ts @@ -0,0 +1,46 @@ +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import events from '@/subscribers/events'; +import { Knex } from 'knex'; +import moment from 'moment'; +import { Inject, Service } from 'typedi'; + +@Service() +export class ConvertSaleEstimate { + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private tenancy: HasTenancyService; + + /** + * Converts estimate to invoice. + * @param {number} tenantId - + * @param {number} estimateId - + * @return {Promise} + */ + public async convertEstimateToInvoice( + tenantId: number, + estimateId: number, + invoiceId: number, + trx?: Knex.Transaction + ): Promise { + const { SaleEstimate } = this.tenancy.models(tenantId); + + // Retrieve details of the given sale estimate. + const saleEstimate = await SaleEstimate.query() + .findById(estimateId) + .throwIfNotFound(); + + // Marks the estimate as converted from the givne invoice. + await SaleEstimate.query(trx).where('id', estimateId).patch({ + convertedToInvoiceId: invoiceId, + convertedToInvoiceAt: moment().toMySqlDateTime(), + }); + // Triggers `onSaleEstimateConvertedToInvoice` event. + await this.eventPublisher.emitAsync( + events.saleEstimate.onConvertedToInvoice, + {} + ); + } +} diff --git a/packages/server/src/services/Sales/Estimates/CreateSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/CreateSaleEstimate.ts new file mode 100644 index 000000000..190962151 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/CreateSaleEstimate.ts @@ -0,0 +1,102 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import { + ISaleEstimate, + ISaleEstimateCreatedPayload, + ISaleEstimateCreatingPayload, + ISaleEstimateDTO, +} from '@/interfaces'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer'; +import events from '@/subscribers/events'; +import { SaleEstimateValidators } from './SaleEstimateValidators'; + +@Service() +export class CreateSaleEstimate { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private transformerDTO: SaleEstimateDTOTransformer; + + @Inject() + private validators: SaleEstimateValidators; + + /** + * Creates a new estimate with associated entries. + * @async + * @param {number} tenantId - The tenant id. + * @param {EstimateDTO} estimate + * @return {Promise} + */ + public async createEstimate( + tenantId: number, + estimateDTO: ISaleEstimateDTO + ): Promise { + const { SaleEstimate, Contact } = this.tenancy.models(tenantId); + + // Retrieve the given customer or throw not found service error. + const customer = await Contact.query() + .modify('customer') + .findById(estimateDTO.customerId) + .throwIfNotFound(); + + // Transform DTO object ot model object. + const estimateObj = await this.transformerDTO.transformDTOToModel( + tenantId, + estimateDTO, + customer + ); + // Validate estimate number uniquiness on the storage. + await this.validators.validateEstimateNumberExistance( + tenantId, + estimateObj.estimateNumber + ); + // Validate items IDs existance on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + tenantId, + estimateDTO.entries + ); + // Validate non-sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + tenantId, + estimateDTO.entries + ); + // Creates a sale estimate transaction with associated transactions as UOW. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleEstimateCreating` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, { + estimateDTO, + tenantId, + trx, + } as ISaleEstimateCreatingPayload); + + // Upsert the sale estimate graph to the storage. + const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({ + ...estimateObj, + }); + // Triggers `onSaleEstimateCreated` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, { + tenantId, + saleEstimate, + saleEstimateId: saleEstimate.id, + saleEstimateDTO: estimateDTO, + trx, + } as ISaleEstimateCreatedPayload); + + return saleEstimate; + }); + } +} diff --git a/packages/server/src/services/Sales/Estimates/DeleteSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/DeleteSaleEstimate.ts new file mode 100644 index 000000000..4975b58f3 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/DeleteSaleEstimate.ts @@ -0,0 +1,74 @@ +import { Inject, Service } from 'typedi'; +import { ServiceError } from '@/exceptions'; +import { + ISaleEstimateDeletedPayload, + ISaleEstimateDeletingPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { ERRORS } from './constants'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import UnitOfWork from '@/services/UnitOfWork'; +import { Knex } from 'knex'; + +@Service() +export class DeleteSaleEstimate { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + /** + * Deletes the given estimate id with associated entries. + * @async + * @param {number} tenantId - The tenant id. + * @param {IEstimate} estimateId + * @return {void} + */ + public async deleteEstimate( + tenantId: number, + estimateId: number + ): Promise { + const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); + + // Retrieve sale estimate or throw not found service error. + const oldSaleEstimate = await SaleEstimate.query() + .findById(estimateId) + .throwIfNotFound(); + + // Throw error if the sale estimate converted to sale invoice. + if (oldSaleEstimate.convertedToInvoiceId) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE); + } + // Deletes the estimate with associated transactions under UOW enivrement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleEstimatedDeleting` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onDeleting, { + trx, + tenantId, + oldSaleEstimate, + } as ISaleEstimateDeletingPayload); + + // Delete sale estimate entries. + await ItemEntry.query(trx) + .where('reference_id', estimateId) + .where('reference_type', 'SaleEstimate') + .delete(); + + // Delete sale estimate transaction. + await SaleEstimate.query(trx).where('id', estimateId).delete(); + + // Triggers `onSaleEstimatedDeleted` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onDeleted, { + tenantId, + saleEstimateId: estimateId, + oldSaleEstimate, + trx, + } as ISaleEstimateDeletedPayload); + }); + } +} diff --git a/packages/server/src/services/Sales/Estimates/DeliverSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/DeliverSaleEstimate.ts new file mode 100644 index 000000000..926715a5c --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/DeliverSaleEstimate.ts @@ -0,0 +1,71 @@ +import { ServiceError } from '@/exceptions'; +import { + ISaleEstimateEventDeliveredPayload, + ISaleEstimateEventDeliveringPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ERRORS } from './constants'; +import moment from 'moment'; + +@Service() +export class DeliverSaleEstimate { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + /** + * Mark the sale estimate as delivered. + * @param {number} tenantId - Tenant id. + * @param {number} saleEstimateId - Sale estimate id. + */ + public async deliverSaleEstimate( + tenantId: number, + saleEstimateId: number + ): Promise { + const { SaleEstimate } = this.tenancy.models(tenantId); + + // Retrieve details of the given sale estimate id. + const oldSaleEstimate = await SaleEstimate.query() + .findById(saleEstimateId) + .throwIfNotFound(); + + // Throws error in case the sale estimate already published. + if (oldSaleEstimate.isDelivered) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED); + } + // Updates the sale estimate transaction with assocaited transactions + // under UOW envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleEstimateDelivering` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onDelivering, { + oldSaleEstimate, + trx, + tenantId, + } as ISaleEstimateEventDeliveringPayload); + + // Record the delivered at on the storage. + const saleEstimate = await SaleEstimate.query(trx).patchAndFetchById( + saleEstimateId, + { + deliveredAt: moment().toMySqlDateTime(), + } + ); + // Triggers `onSaleEstimateDelivered` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onDelivered, { + tenantId, + saleEstimate, + trx, + } as ISaleEstimateEventDeliveredPayload); + }); + } +} diff --git a/packages/server/src/services/Sales/Estimates/EditSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/EditSaleEstimate.ts new file mode 100644 index 000000000..5b2323a23 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/EditSaleEstimate.ts @@ -0,0 +1,123 @@ +import { Inject, Service } from 'typedi'; +import { + ISaleEstimate, + ISaleEstimateDTO, + ISaleEstimateEditedPayload, + ISaleEstimateEditingPayload, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SaleEstimateValidators } from './SaleEstimateValidators'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import events from '@/subscribers/events'; + +@Service() +export class EditSaleEstimate { + @Inject() + private validators: SaleEstimateValidators; + + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private transformerDTO: SaleEstimateDTOTransformer; + + /** + * Edit details of the given estimate with associated entries. + * @async + * @param {number} tenantId - The tenant id. + * @param {Integer} estimateId + * @param {EstimateDTO} estimate + * @return {Promise} + */ + public async editEstimate( + tenantId: number, + estimateId: number, + estimateDTO: ISaleEstimateDTO + ): Promise { + const { SaleEstimate, Contact } = this.tenancy.models(tenantId); + + // Retrieve details of the given sale estimate id. + const oldSaleEstimate = await SaleEstimate.query().findById(estimateId); + + // Validates the given estimate existance. + this.validators.validateEstimateExistance(oldSaleEstimate); + + // Retrieve the given customer or throw not found service error. + const customer = await Contact.query() + .modify('customer') + .findById(estimateDTO.customerId) + .throwIfNotFound(); + + // Transform DTO object ot model object. + const estimateObj = await this.transformerDTO.transformDTOToModel( + tenantId, + estimateDTO, + oldSaleEstimate, + customer + ); + // Validate estimate number uniquiness on the storage. + if (estimateDTO.estimateNumber) { + await this.validators.validateEstimateNumberExistance( + tenantId, + estimateDTO.estimateNumber, + estimateId + ); + } + // Validate sale estimate entries existance. + await this.itemsEntriesService.validateEntriesIdsExistance( + tenantId, + estimateId, + 'SaleEstimate', + estimateDTO.entries + ); + // Validate items IDs existance on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + tenantId, + estimateDTO.entries + ); + // Validate non-sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + tenantId, + estimateDTO.entries + ); + // Edits estimate transaction with associated transactions + // under unit-of-work envirement. + return this.uow.withTransaction(tenantId, async (trx) => { + // Trigger `onSaleEstimateEditing` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onEditing, { + tenantId, + oldSaleEstimate, + estimateDTO, + trx, + } as ISaleEstimateEditingPayload); + + // Upsert the estimate graph to the storage. + const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({ + id: estimateId, + ...estimateObj, + }); + // Trigger `onSaleEstimateEdited` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onEdited, { + tenantId, + estimateId, + saleEstimate, + oldSaleEstimate, + trx, + } as ISaleEstimateEditedPayload); + + return saleEstimate; + }); + } +} diff --git a/packages/server/src/services/Sales/Estimates/GetSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/GetSaleEstimate.ts new file mode 100644 index 000000000..f4d0d1419 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/GetSaleEstimate.ts @@ -0,0 +1,43 @@ +import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import { SaleEstimateTransfromer } from './SaleEstimateTransformer'; +import { SaleEstimateValidators } from './SaleEstimateValidators'; + +@Service() +export class GetSaleEstimate { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + @Inject() + private validators: SaleEstimateValidators; + + /** + * Retrieve the estimate details with associated entries. + * @async + * @param {number} tenantId - The tenant id. + * @param {Integer} estimateId + */ + public async getEstimate(tenantId: number, estimateId: number) { + const { SaleEstimate } = this.tenancy.models(tenantId); + + const estimate = await SaleEstimate.query() + .findById(estimateId) + .withGraphFetched('entries.item') + .withGraphFetched('customer') + .withGraphFetched('branch'); + + // Validates the estimate existance. + this.validators.validateEstimateExistance(estimate); + + // Transformes sale estimate model to POJO. + return this.transformer.transform( + tenantId, + estimate, + new SaleEstimateTransfromer() + ); + } +} diff --git a/packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts b/packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts new file mode 100644 index 000000000..e0b53adb3 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts @@ -0,0 +1,77 @@ +import * as R from 'ramda'; +import { Inject, Service } from 'typedi'; +import { + IFilterMeta, + IPaginationMeta, + ISaleEstimate, + ISalesEstimatesFilter, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer'; +import { SaleEstimateTransfromer } from './SaleEstimateTransformer'; + +@Service() +export class GetSaleEstimates { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private dynamicListService: DynamicListingService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieves estimates filterable and paginated list. + * @param {number} tenantId - + * @param {IEstimatesFilter} estimatesFilter - + */ + public async getEstimates( + tenantId: number, + filterDTO: ISalesEstimatesFilter + ): Promise<{ + salesEstimates: ISaleEstimate[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + const { SaleEstimate } = this.tenancy.models(tenantId); + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + tenantId, + SaleEstimate, + filter + ); + const { results, pagination } = await SaleEstimate.query() + .onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries'); + dynamicFilter.buildQuery()(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + const transformedEstimates = await this.transformer.transform( + tenantId, + results, + new SaleEstimateTransfromer() + ); + return { + salesEstimates: transformedEstimates, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + + /** + * Parses the sale receipts list filter DTO. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server/src/services/Sales/Estimates/RejectSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/RejectSaleEstimate.ts new file mode 100644 index 000000000..2ded056ef --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/RejectSaleEstimate.ts @@ -0,0 +1,57 @@ +import { Service, Inject } from 'typedi'; +import moment from 'moment'; +import { Knex } from 'knex'; +import events from '@/subscribers/events'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './constants'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; + +@Service() +export class RejectSaleEstimate { + @Inject() + private tenancy: TenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + /** + * Mark the sale estimate as rejected from the customer. + * @param {number} tenantId + * @param {number} saleEstimateId + */ + public async rejectSaleEstimate( + tenantId: number, + saleEstimateId: number + ): Promise { + const { SaleEstimate } = this.tenancy.models(tenantId); + + // Retrieve details of the given sale estimate id. + const saleEstimate = await SaleEstimate.query() + .findById(saleEstimateId) + .throwIfNotFound(); + + // Throws error in case the sale estimate still not delivered to customer. + if (!saleEstimate.isDelivered) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED); + } + // Throws error in case the sale estimate already rejected. + if (saleEstimate.isRejected) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_REJECTED); + } + // + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Mark the sale estimate as reject on the storage. + await SaleEstimate.query(trx).where('id', saleEstimateId).patch({ + rejectedAt: moment().toMySqlDateTime(), + approvedAt: null, + }); + // Triggers `onSaleEstimateRejected` event. + await this.eventPublisher.emitAsync(events.saleEstimate.onRejected, {}); + }); + } +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts b/packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts new file mode 100644 index 000000000..967edcb63 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts @@ -0,0 +1,104 @@ +import * as R from 'ramda'; +import { Inject, Service } from 'typedi'; +import { omit, sumBy } from 'lodash'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces'; +import { SaleEstimateValidators } from './SaleEstimateValidators'; +import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; +import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +import { formatDateFields } from '@/utils'; +import moment from 'moment'; +import { SaleEstimateIncrement } from './SaleEstimateIncrement'; + +@Service() +export class SaleEstimateDTOTransformer { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private validators: SaleEstimateValidators; + + @Inject() + private branchDTOTransform: BranchTransactionDTOTransform; + + @Inject() + private warehouseDTOTransform: WarehouseTransactionDTOTransform; + + @Inject() + private estimateIncrement: SaleEstimateIncrement; + + /** + * Transform create DTO object ot model object. + * @param {number} tenantId + * @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO. + * @return {ISaleEstimate} + */ + async transformDTOToModel( + tenantId: number, + estimateDTO: ISaleEstimateDTO, + paymentCustomer: ICustomer, + oldSaleEstimate?: ISaleEstimate + ): Promise { + const { ItemEntry, Contact } = this.tenancy.models(tenantId); + + const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e)); + + // Retreive the next invoice number. + const autoNextNumber = + this.estimateIncrement.getNextEstimateNumber(tenantId); + + // Retreive the next estimate number. + const estimateNumber = + estimateDTO.estimateNumber || + oldSaleEstimate?.estimateNumber || + autoNextNumber; + + // Validate the sale estimate number require. + this.validators.validateEstimateNoRequire(estimateNumber); + + const initialDTO = { + amount, + ...formatDateFields(omit(estimateDTO, ['delivered', 'entries']), [ + 'estimateDate', + 'expirationDate', + ]), + currencyCode: paymentCustomer.currencyCode, + exchangeRate: estimateDTO.exchangeRate || 1, + ...(estimateNumber ? { estimateNumber } : {}), + entries: estimateDTO.entries.map((entry) => ({ + reference_type: 'SaleEstimate', + ...entry, + })), + // Avoid rewrite the deliver date in edit mode when already published. + ...(estimateDTO.delivered && + !oldSaleEstimate?.deliveredAt && { + deliveredAt: moment().toMySqlDateTime(), + }), + }; + return R.compose( + this.branchDTOTransform.transformDTO(tenantId), + this.warehouseDTOTransform.transformDTO(tenantId) + )(initialDTO); + } + + /** + * Retrieve estimate number to object model. + * @param {number} tenantId + * @param {ISaleEstimateDTO} saleEstimateDTO + * @param {ISaleEstimate} oldSaleEstimate + */ + public transformEstimateNumberToModel( + tenantId: number, + saleEstimateDTO: ISaleEstimateDTO, + oldSaleEstimate?: ISaleEstimate + ): string { + // Retreive the next invoice number. + const autoNextNumber = + this.estimateIncrement.getNextEstimateNumber(tenantId); + + if (saleEstimateDTO.estimateNumber) { + return saleEstimateDTO.estimateNumber; + } + return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber; + } +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimateIncrement.ts b/packages/server/src/services/Sales/Estimates/SaleEstimateIncrement.ts new file mode 100644 index 000000000..d3928b06e --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SaleEstimateIncrement.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import AutoIncrementOrdersService from '../AutoIncrementOrdersService'; + +@Service() +export class SaleEstimateIncrement { + @Inject() + private autoIncrementOrdersService: AutoIncrementOrdersService; + + /** + * Retrieve the next unique estimate number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + public getNextEstimateNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'sales_estimates' + ); + } + + /** + * Increment the estimate next number. + * @param {number} tenantId - + */ + public incrementNextEstimateNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'sales_estimates' + ); + } +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimateSmsNotify.ts b/packages/server/src/services/Sales/Estimates/SaleEstimateSmsNotify.ts index 037bb0331..3757b15d7 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimateSmsNotify.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimateSmsNotify.ts @@ -4,7 +4,6 @@ import events from '@/subscribers/events'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import SaleNotifyBySms from '../SaleNotifyBySms'; import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; -import SMSClient from '@/services/SMSClient'; import { ICustomer, IPaymentReceiveSmsDetails, @@ -21,18 +20,18 @@ const ERRORS = { }; @Service() -export default class SaleEstimateNotifyBySms { +export class SaleEstimateNotifyBySms { @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; @Inject() - saleSmsNotification: SaleNotifyBySms; + private saleSmsNotification: SaleNotifyBySms; @Inject() - eventPublisher: EventPublisher; + private eventPublisher: EventPublisher; @Inject() - smsNotificationsSettings: SmsNotificationsSettingsService; + private smsNotificationsSettings: SmsNotificationsSettingsService; /** * @@ -187,6 +186,7 @@ export default class SaleEstimateNotifyBySms { .findById(saleEstimateId) .withGraphFetched('customer'); + // Validates the estimate existance. this.validateEstimateExistance(saleEstimate); // Retrieve the current tenant metadata. diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts b/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts index ad69bf867..770a13c3c 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts @@ -3,7 +3,7 @@ import { ISaleEstimate } from '@/interfaces'; import { Transformer } from '@/lib/Transformer/Transformer'; import { formatNumber } from 'utils'; -export default class SaleEstimateTransfromer extends Transformer { +export class SaleEstimateTransfromer extends Transformer { /** * Include these attributes to sale invoice object. * @returns {Array} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimateValidators.ts b/packages/server/src/services/Sales/Estimates/SaleEstimateValidators.ts new file mode 100644 index 000000000..447df7795 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SaleEstimateValidators.ts @@ -0,0 +1,87 @@ +import { Inject, Service } from 'typedi'; +import { ServiceError } from '@/exceptions'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ISaleEstimate } from '@/interfaces'; +import { ERRORS } from './constants'; +import { SaleEstimate } from '@/models'; + +@Service() +export class SaleEstimateValidators { + @Inject() + private tenancy: HasTenancyService; + + /** + * Validates the given estimate existance. + * @param {SaleEstimate | undefined | null} estimate - + */ + public validateEstimateExistance(estimate: SaleEstimate | undefined | null) { + if (!estimate) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND); + } + } + + /** + * Validate the estimate number unique on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + public async validateEstimateNumberExistance( + tenantId: number, + estimateNumber: string, + notEstimateId?: number + ) { + const { SaleEstimate } = this.tenancy.models(tenantId); + + const foundSaleEstimate = await SaleEstimate.query() + .findOne('estimate_number', estimateNumber) + .onBuild((builder) => { + if (notEstimateId) { + builder.whereNot('id', notEstimateId); + } + }); + if (foundSaleEstimate) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE); + } + } + + /** + * Validates the given sale estimate not already converted to invoice. + * @param {ISaleEstimate} saleEstimate - + */ + public validateEstimateNotConverted(saleEstimate: ISaleEstimate) { + if (saleEstimate.isConvertedToInvoice) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE); + } + } + + /** + * Validate the sale estimate number require. + * @param {ISaleEstimate} saleInvoiceObj + */ + public validateEstimateNoRequire(estimateNumber: string) { + if (!estimateNumber) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED); + } + } + + /** + * Validate the given customer has no sales estimates. + * @param {number} tenantId + * @param {number} customerId - Customer id. + */ + public async validateCustomerHasNoEstimates( + tenantId: number, + customerId: number + ) { + const { SaleEstimate } = this.tenancy.models(tenantId); + + const estimates = await SaleEstimate.query().where( + 'customer_id', + customerId + ); + if (estimates.length > 0) { + throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_ESTIMATES); + } + } +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts new file mode 100644 index 000000000..3f63b27de --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -0,0 +1,212 @@ +import { Inject, Service } from 'typedi'; +import { CreateSaleEstimate } from './CreateSaleEstimate'; +import { + IFilterMeta, + IPaginationMeta, + IPaymentReceiveSmsDetails, + ISaleEstimate, + ISaleEstimateDTO, + ISalesEstimatesFilter, +} from '@/interfaces'; +import { EditSaleEstimate } from './EditSaleEstimate'; +import { DeleteSaleEstimate } from './DeleteSaleEstimate'; +import { GetSaleEstimate } from './GetSaleEstimate'; +import { GetSaleEstimates } from './GetSaleEstimates'; +import { DeliverSaleEstimate } from './DeliverSaleEstimate'; +import { ApproveSaleEstimate } from './ApproveSaleEstimate'; +import { RejectSaleEstimate } from './RejectSaleEstimate'; +import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify'; +import { SaleEstimatesPdf } from './SaleEstimatesPdf'; + +@Service() +export class SaleEstimatesApplication { + @Inject() + private createSaleEstimateService: CreateSaleEstimate; + + @Inject() + private editSaleEstimateService: EditSaleEstimate; + + @Inject() + private deleteSaleEstimateService: DeleteSaleEstimate; + + @Inject() + private getSaleEstimateService: GetSaleEstimate; + + @Inject() + private getSaleEstimatesService: GetSaleEstimates; + + @Inject() + private deliverSaleEstimateService: DeliverSaleEstimate; + + @Inject() + private approveSaleEstimateService: ApproveSaleEstimate; + + @Inject() + private rejectSaleEstimateService: RejectSaleEstimate; + + @Inject() + private saleEstimateNotifyBySmsService: SaleEstimateNotifyBySms; + + @Inject() + private saleEstimatesPdfService: SaleEstimatesPdf; + + /** + * Create a sale estimate. + * @param {number} tenantId - The tenant id. + * @param {EstimateDTO} estimate + * @return {Promise} + */ + public createSaleEstimate( + tenantId: number, + estimateDTO: ISaleEstimateDTO + ): Promise { + return this.createSaleEstimateService.createEstimate(tenantId, estimateDTO); + } + + /** + * Edit the given sale estimate. + * @param {number} tenantId - The tenant id. + * @param {Integer} estimateId + * @param {EstimateDTO} estimate + * @return {Promise} + */ + public editSaleEstimate( + tenantId: number, + estimateId: number, + estimateDTO: ISaleEstimateDTO + ): Promise { + return this.editSaleEstimateService.editEstimate( + tenantId, + estimateId, + estimateDTO + ); + } + + /** + * Deletes the given sale estimate. + * @param {number} tenantId - + * @param {number} estimateId - + * @return {Promise} + */ + public deleteSaleEstimate( + tenantId: number, + estimateId: number + ): Promise { + return this.deleteSaleEstimateService.deleteEstimate(tenantId, estimateId); + } + + /** + * Retrieves the given sale estimate. + * @param {number} tenantId + * @param {number} estimateId + */ + public getSaleEstimate(tenantId: number, estimateId: number) { + return this.getSaleEstimateService.getEstimate(tenantId, estimateId); + } + + /** + * Retrieves the sale estimate. + * @param {number} tenantId + * @param {ISalesEstimatesFilter} filterDTO + * @returns + */ + public getSaleEstimates( + tenantId: number, + filterDTO: ISalesEstimatesFilter + ): Promise<{ + salesEstimates: ISaleEstimate[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + return this.getSaleEstimatesService.getEstimates(tenantId, filterDTO); + } + + /** + * Deliver the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public deliverSaleEstimate(tenantId: number, saleEstimateId: number) { + return this.deliverSaleEstimateService.deliverSaleEstimate( + tenantId, + saleEstimateId + ); + } + + /** + * Approve the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public approveSaleEstimate( + tenantId: number, + saleEstimateId: number + ): Promise { + return this.approveSaleEstimateService.approveSaleEstimate( + tenantId, + saleEstimateId + ); + } + + /** + * Mark the sale estimate as rejected from the customer. + * @param {number} tenantId + * @param {number} saleEstimateId + */ + public async rejectSaleEstimate( + tenantId: number, + saleEstimateId: number + ): Promise { + return this.rejectSaleEstimateService.rejectSaleEstimate( + tenantId, + saleEstimateId + ); + } + + /** + * Notify the customer of the given sale estimate by SMS. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public notifySaleEstimateBySms = async ( + tenantId: number, + saleEstimateId: number + ): Promise => { + return this.saleEstimateNotifyBySmsService.notifyBySms( + tenantId, + saleEstimateId + ); + }; + + /** + * Retrieve the SMS details of the given payment receive transaction. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public getSaleEstimateSmsDetails = ( + tenantId: number, + saleEstimateId: number + ): Promise => { + return this.saleEstimateNotifyBySmsService.smsDetails( + tenantId, + saleEstimateId + ); + }; + + /** + * + * @param {number} tenantId + * @param {} saleEstimate + * @returns + */ + public getSaleEstimatePdf(tenantId: number, saleEstimate) { + return this.saleEstimatesPdfService.getSaleEstimatePdf( + tenantId, + saleEstimate + ); + } +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts index f5959cd0a..0bd932f99 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts @@ -5,18 +5,18 @@ import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Tenant } from '@/system/models'; @Service() -export default class SaleEstimatesPdf { +export class SaleEstimatesPdf { @Inject() - pdfService: PdfService; + private pdfService: PdfService; @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; /** * Retrieve sale invoice pdf content. * @param {} saleInvoice - */ - async saleEstimatePdf(tenantId: number, saleEstimate) { + async getSaleEstimatePdf(tenantId: number, saleEstimate) { const i18n = this.tenancy.i18n(tenantId); const organization = await Tenant.query() diff --git a/packages/server/src/services/Sales/Estimates/UnlinkConvertedSaleEstimate.ts b/packages/server/src/services/Sales/Estimates/UnlinkConvertedSaleEstimate.ts new file mode 100644 index 000000000..0f2b6c28f --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/UnlinkConvertedSaleEstimate.ts @@ -0,0 +1,32 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export class UnlinkConvertedSaleEstimate { + @Inject() + private tenancy: HasTenancyService; + + /** + * Unlink the converted sale estimates from the given sale invoice. + * @param {number} tenantId - + * @param {number} invoiceId - + * @return {Promise} + */ + public async unlinkConvertedEstimateFromInvoice( + tenantId: number, + invoiceId: number, + trx?: Knex.Transaction + ): Promise { + const { SaleEstimate } = this.tenancy.models(tenantId); + + await SaleEstimate.query(trx) + .where({ + convertedToInvoiceId: invoiceId, + }) + .patch({ + convertedToInvoiceId: null, + convertedToInvoiceAt: null, + }); + } +} diff --git a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts new file mode 100644 index 000000000..c4743350e --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts @@ -0,0 +1,103 @@ +import { Service, Inject } from 'typedi'; +import { omit, sumBy } from 'lodash'; +import * as R from 'ramda'; +import moment from 'moment'; +import composeAsync from 'async/compose'; +import { + ISaleInvoice, + ISaleInvoiceCreateDTO, + ISaleInvoiceEditDTO, + ICustomer, + ITenantUser, +} from '@/interfaces'; +import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; +import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; +import { SaleInvoiceIncrement } from './SaleInvoiceIncrement'; +import { formatDateFields } from 'utils'; + +@Service() +export class CommandSaleInvoiceDTOTransformer { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private branchDTOTransform: BranchTransactionDTOTransform; + + @Inject() + private warehouseDTOTransform: WarehouseTransactionDTOTransform; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private validators: CommandSaleInvoiceValidators; + + @Inject() + private invoiceIncrement: SaleInvoiceIncrement; + + /** + * Transformes the create DTO to invoice object model. + * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. + * @param {ISaleInvoice} oldSaleInvoice - Old sale invoice. + * @return {ISaleInvoice} + */ + public async transformDTOToModel( + tenantId: number, + customer: ICustomer, + saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, + authorizedUser: ITenantUser, + oldSaleInvoice?: ISaleInvoice + ): Promise { + const { ItemEntry } = this.tenancy.models(tenantId); + + const balance = sumBy(saleInvoiceDTO.entries, (e) => + ItemEntry.calcAmount(e) + ); + // Retreive the next invoice number. + const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber(tenantId); + + // Invoice number. + const invoiceNo = + saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber; + + // Validate the invoice is required. + this.validators.validateInvoiceNoRequire(invoiceNo); + + const initialEntries = saleInvoiceDTO.entries.map((entry) => ({ + referenceType: 'SaleInvoice', + ...entry, + })); + const entries = await composeAsync( + // Sets default cost and sell account to invoice items entries. + this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId) + )(initialEntries); + + const initialDTO = { + ...formatDateFields( + omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']), + ['invoiceDate', 'dueDate'] + ), + // Avoid rewrite the deliver date in edit mode when already published. + balance, + currencyCode: customer.currencyCode, + exchangeRate: saleInvoiceDTO.exchangeRate || 1, + ...(saleInvoiceDTO.delivered && + !oldSaleInvoice?.deliveredAt && { + deliveredAt: moment().toMySqlDateTime(), + }), + // Avoid override payment amount in edit mode. + ...(!oldSaleInvoice && { paymentAmount: 0 }), + ...(invoiceNo ? { invoiceNo } : {}), + entries, + userId: authorizedUser.id, + } as ISaleInvoice; + + return R.compose( + this.branchDTOTransform.transformDTO(tenantId), + this.warehouseDTOTransform.transformDTO(tenantId) + )(initialDTO); + } +} diff --git a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceValidators.ts b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceValidators.ts new file mode 100644 index 000000000..983abcc6f --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceValidators.ts @@ -0,0 +1,86 @@ +import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; +import { SaleInvoice } from '@/models'; +import { ERRORS } from './constants'; + +@Service() +export class CommandSaleInvoiceValidators { + @Inject() + private tenancy: HasTenancyService; + + /** + * Validates the given invoice is existance. + * @param {SaleInvoice | undefined} invoice + */ + public validateInvoiceExistance(invoice: SaleInvoice | undefined) { + if (!invoice) { + throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); + } + } + + /** + * Validate whether sale invoice number unqiue on the storage. + */ + public async validateInvoiceNumberUnique( + tenantId: number, + invoiceNumber: string, + notInvoiceId?: number + ) { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findOne('invoice_no', invoiceNumber) + .onBuild((builder) => { + if (notInvoiceId) { + builder.whereNot('id', notInvoiceId); + } + }); + + if (saleInvoice) { + throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE); + } + } + + /** + * Validate the invoice amount is bigger than payment amount before edit the invoice. + * @param {number} saleInvoiceAmount + * @param {number} paymentAmount + */ + public validateInvoiceAmountBiggerPaymentAmount( + saleInvoiceAmount: number, + paymentAmount: number + ) { + if (saleInvoiceAmount < paymentAmount) { + throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT); + } + } + + /** + * Validate the invoice number require. + * @param {ISaleInvoice} saleInvoiceObj + */ + public validateInvoiceNoRequire(invoiceNo: string) { + if (!invoiceNo) { + throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED); + } + } + + /** + * Validate the given customer has no sales invoices. + * @param {number} tenantId + * @param {number} customerId - Customer id. + */ + public async validateCustomerHasNoInvoices( + tenantId: number, + customerId: number + ) { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const invoices = await SaleInvoice.query().where('customer_id', customerId); + + if (invoices.length > 0) { + throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES); + } + } +} diff --git a/packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts new file mode 100644 index 000000000..2fb6c0176 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts @@ -0,0 +1,147 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { + ICustomer, + ISaleInvoice, + ISaleInvoiceCreateDTO, + ISaleInvoiceCreatedPayload, + ISaleInvoiceCreatingPaylaod, + ITenantUser, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; +import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer'; +import { SaleEstimateValidators } from '../Estimates/SaleEstimateValidators'; + +@Service() +export class CreateSaleInvoice { + @Inject() + private tenancy: TenancyService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private validators: CommandSaleInvoiceValidators; + + @Inject() + private transformerDTO: CommandSaleInvoiceDTOTransformer; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private commandEstimateValidators: SaleEstimateValidators; + + /** + * Creates a new sale invoices and store it to the storage + * with associated to entries and journal transactions. + * @async + * @param {number} tenantId - Tenant id. + * @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO. + * @return {Promise} + */ + public createSaleInvoice = async ( + tenantId: number, + saleInvoiceDTO: ISaleInvoiceCreateDTO, + authorizedUser: ITenantUser + ): Promise => { + const { SaleInvoice, SaleEstimate, Contact } = + this.tenancy.models(tenantId); + + // Validate customer existance. + const customer = await Contact.query() + .modify('customer') + .findById(saleInvoiceDTO.customerId) + .throwIfNotFound(); + + // Validate the from estimate id exists on the storage. + if (saleInvoiceDTO.fromEstimateId) { + const fromEstimate = await SaleEstimate.query() + .findById(saleInvoiceDTO.fromEstimateId) + .throwIfNotFound(); + + // Validate the sale estimate is not already converted to invoice. + this.commandEstimateValidators.validateEstimateNotConverted(fromEstimate); + } + // Validate items ids existance. + await this.itemsEntriesService.validateItemsIdsExistance( + tenantId, + saleInvoiceDTO.entries + ); + // Validate items should be sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + tenantId, + saleInvoiceDTO.entries + ); + // Transform DTO object to model object. + const saleInvoiceObj = await this.transformCreateDTOToModel( + tenantId, + customer, + saleInvoiceDTO, + authorizedUser + ); + // Validate sale invoice number uniquiness. + if (saleInvoiceObj.invoiceNo) { + await this.validators.validateInvoiceNumberUnique( + tenantId, + saleInvoiceObj.invoiceNo + ); + } + // Creates a new sale invoice and associated transactions under unit of work env. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceCreating` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, { + saleInvoiceDTO, + tenantId, + trx, + } as ISaleInvoiceCreatingPaylaod); + + // Create sale invoice graph to the storage. + const saleInvoice = await SaleInvoice.query(trx).upsertGraph( + saleInvoiceObj + ); + const eventPayload: ISaleInvoiceCreatedPayload = { + tenantId, + saleInvoice, + saleInvoiceDTO, + saleInvoiceId: saleInvoice.id, + authorizedUser, + trx, + }; + // Triggers the event `onSaleInvoiceCreated`. + await this.eventPublisher.emitAsync( + events.saleInvoice.onCreated, + eventPayload + ); + return saleInvoice; + }); + }; + + /** + * Transformes create DTO to model. + * @param {number} tenantId - + * @param {ICustomer} customer - + * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - + */ + private transformCreateDTOToModel = async ( + tenantId: number, + customer: ICustomer, + saleInvoiceDTO: ISaleInvoiceCreateDTO, + authorizedUser: ITenantUser + ) => { + return this.transformerDTO.transformDTOToModel( + tenantId, + customer, + saleInvoiceDTO, + authorizedUser + ); + }; +} diff --git a/packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts new file mode 100644 index 000000000..e012e8e26 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts @@ -0,0 +1,154 @@ +import { Service, Inject } from 'typedi'; +import { Knex } from 'knex'; +import { + ISystemUser, + ISaleInvoiceDeletePayload, + ISaleInvoiceDeletedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './constants'; +import { UnlinkConvertedSaleEstimate } from '../Estimates/UnlinkConvertedSaleEstimate'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export class DeleteSaleInvoice { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + /** + * Validate the sale invoice has no payment entries. + * @param {number} tenantId + * @param {number} saleInvoiceId + */ + private async validateInvoiceHasNoPaymentEntries( + tenantId: number, + saleInvoiceId: number + ) { + const { PaymentReceiveEntry } = this.tenancy.models(tenantId); + + // Retrieve the sale invoice associated payment receive entries. + const entries = await PaymentReceiveEntry.query().where( + 'invoice_id', + saleInvoiceId + ); + if (entries.length > 0) { + throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES); + } + return entries; + } + + /** + * Validate the sale invoice has no applied to credit note transaction. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public validateInvoiceHasNoAppliedToCredit = async ( + tenantId: number, + invoiceId: number + ): Promise => { + const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId); + + const appliedTransactions = await CreditNoteAppliedInvoice.query().where( + 'invoiceId', + invoiceId + ); + if (appliedTransactions.length > 0) { + throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES); + } + }; + + /** + * Validate whether sale invoice exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + private async getInvoiceOrThrowError( + tenantId: number, + saleInvoiceId: number + ) { + const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); + + const saleInvoice = await saleInvoiceRepository.findOneById( + saleInvoiceId, + 'entries' + ); + if (!saleInvoice) { + throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); + } + return saleInvoice; + } + + /** + * Deletes the given sale invoice with associated entries + * and journal transactions. + * @param {number} tenantId - Tenant id. + * @param {Number} saleInvoiceId - The given sale invoice id. + * @param {ISystemUser} authorizedUser - + */ + public async deleteSaleInvoice( + tenantId: number, + saleInvoiceId: number, + authorizedUser: ISystemUser + ): Promise { + const { ItemEntry, SaleInvoice } = this.tenancy.models(tenantId); + + // Retrieve the given sale invoice with associated entries + // or throw not found error. + const oldSaleInvoice = await this.getInvoiceOrThrowError( + tenantId, + saleInvoiceId + ); + // Validate the sale invoice has no associated payment entries. + await this.validateInvoiceHasNoPaymentEntries(tenantId, saleInvoiceId); + + // Validate the sale invoice has applied to credit note transaction. + await this.validateInvoiceHasNoAppliedToCredit(tenantId, saleInvoiceId); + + // Deletes sale invoice transaction and associate transactions with UOW env. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceDelete` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, { + tenantId, + saleInvoice: oldSaleInvoice, + saleInvoiceId, + trx, + } as ISaleInvoiceDeletePayload); + + // Unlink the converted sale estimates from the given sale invoice. + await this.unlockEstimateFromInvoice.unlinkConvertedEstimateFromInvoice( + tenantId, + saleInvoiceId, + trx + ); + await ItemEntry.query(trx) + .where('reference_id', saleInvoiceId) + .where('reference_type', 'SaleInvoice') + .delete(); + + await SaleInvoice.query(trx).findById(saleInvoiceId).delete(); + + // Triggers `onSaleInvoiceDeleted` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, { + tenantId, + oldSaleInvoice, + saleInvoiceId, + authorizedUser, + trx, + } as ISaleInvoiceDeletedPayload); + }); + } +} diff --git a/packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts new file mode 100644 index 000000000..77511af84 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts @@ -0,0 +1,77 @@ +import { Knex } from 'knex'; +import moment from 'moment'; +import { ServiceError } from '@/exceptions'; +import { + ISaleInvoiceDeliveringPayload, + ISaleInvoiceEventDeliveredPayload, + ISystemUser, +} from '@/interfaces'; +import { ERRORS } from './constants'; +import { Inject, Service } from 'typedi'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import UnitOfWork from '@/services/UnitOfWork'; +import events from '@/subscribers/events'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; + +@Service() +export class DeliverSaleInvoice { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private validators: CommandSaleInvoiceValidators; + + /** + * Deliver the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} saleInvoiceId - Sale invoice id. + * @return {Promise} + */ + public async deliverSaleInvoice( + tenantId: number, + saleInvoiceId: number, + authorizedUser: ISystemUser + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + // Retrieve details of the given sale invoice id. + const oldSaleInvoice = await SaleInvoice.query().findById(saleInvoiceId); + + // Validates the given invoice existance. + this.validators.validateInvoiceExistance(oldSaleInvoice); + + // Throws error in case the sale invoice already published. + if (oldSaleInvoice.isDelivered) { + throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED); + } + // Update sale invoice transaction with assocaite transactions + // under unit-of-work envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceDelivering` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onDelivering, { + tenantId, + oldSaleInvoice, + trx, + } as ISaleInvoiceDeliveringPayload); + + // Record the delivered at on the storage. + const saleInvoice = await SaleInvoice.query(trx) + .where({ id: saleInvoiceId }) + .update({ deliveredAt: moment().toMySqlDateTime() }); + + // Triggers `onSaleInvoiceDelivered` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, { + tenantId, + saleInvoiceId, + saleInvoice, + } as ISaleInvoiceEventDeliveredPayload); + }); + } +} diff --git a/packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts new file mode 100644 index 000000000..5b0638be0 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts @@ -0,0 +1,165 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import UnitOfWork from '@/services/UnitOfWork'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { + ICustomer, + ISaleInvoice, + ISaleInvoiceEditDTO, + ISaleInvoiceEditedPayload, + ISaleInvoiceEditingPayload, + ISystemUser, + ITenantUser, +} from '@/interfaces'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; +import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer'; +import events from '@/subscribers/events'; + +@Service() +export class EditSaleInvoice { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private validators: CommandSaleInvoiceValidators; + + @Inject() + private transformerDTO: CommandSaleInvoiceDTOTransformer; + + @Inject() + private uow: UnitOfWork; + + /** + * Edit the given sale invoice. + * @async + * @param {number} tenantId - Tenant id. + * @param {Number} saleInvoiceId - Sale invoice id. + * @param {ISaleInvoice} saleInvoice - Sale invoice DTO object. + * @return {Promise} + */ + public async editSaleInvoice( + tenantId: number, + saleInvoiceId: number, + saleInvoiceDTO: ISaleInvoiceEditDTO, + authorizedUser: ISystemUser + ): Promise { + const { SaleInvoice, Contact } = this.tenancy.models(tenantId); + + // Retrieve the sale invoice or throw not found service error. + const oldSaleInvoice = await SaleInvoice.query() + .findById(saleInvoiceId) + .withGraphJoined('entries'); + + // Validates the given invoice existance. + this.validators.validateInvoiceExistance(oldSaleInvoice); + + // Validate customer existance. + const customer = await Contact.query() + .findById(saleInvoiceDTO.customerId) + .modify('customer') + .throwIfNotFound(); + + // 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 + ); + // Transform DTO object to model object. + const saleInvoiceObj = await this.tranformEditDTOToModel( + tenantId, + customer, + saleInvoiceDTO, + oldSaleInvoice, + authorizedUser + ); + // Validate sale invoice number uniquiness. + if (saleInvoiceObj.invoiceNo) { + await this.validators.validateInvoiceNumberUnique( + tenantId, + saleInvoiceObj.invoiceNo, + saleInvoiceId + ); + } + // Validate the invoice amount is not smaller than the invoice payment amount. + this.validators.validateInvoiceAmountBiggerPaymentAmount( + saleInvoiceObj.balance, + oldSaleInvoice.paymentAmount + ); + // Edit sale invoice transaction in UOW envirment. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceEditing` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, { + trx, + oldSaleInvoice, + tenantId, + saleInvoiceDTO, + } as ISaleInvoiceEditingPayload); + + // Upsert the the invoice graph to the storage. + const saleInvoice: ISaleInvoice = + await SaleInvoice.query().upsertGraphAndFetch({ + id: saleInvoiceId, + ...saleInvoiceObj, + }); + // Edit event payload. + const editEventPayload: ISaleInvoiceEditedPayload = { + tenantId, + saleInvoiceId, + saleInvoice, + saleInvoiceDTO, + oldSaleInvoice, + authorizedUser, + trx, + }; + // Triggers `onSaleInvoiceEdited` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onEdited, + editEventPayload + ); + return saleInvoice; + }); + } + + /** + * Transformes edit DTO to model. + * @param {number} tennatId - + * @param {ICustomer} customer - + * @param {ISaleInvoiceEditDTO} saleInvoiceDTO - + * @param {ISaleInvoice} oldSaleInvoice + */ + private tranformEditDTOToModel = async ( + tenantId: number, + customer: ICustomer, + saleInvoiceDTO: ISaleInvoiceEditDTO, + oldSaleInvoice: ISaleInvoice, + authorizedUser: ITenantUser + ) => { + return this.transformerDTO.transformDTOToModel( + tenantId, + customer, + saleInvoiceDTO, + authorizedUser, + oldSaleInvoice + ); + }; +} diff --git a/packages/server/src/services/Sales/Invoices/InvoicePaymentsService.ts b/packages/server/src/services/Sales/Invoices/GetInvoicePaymentsService.ts similarity index 95% rename from packages/server/src/services/Sales/Invoices/InvoicePaymentsService.ts rename to packages/server/src/services/Sales/Invoices/GetInvoicePaymentsService.ts index 7d4dd1115..2bdb874f1 100644 --- a/packages/server/src/services/Sales/Invoices/InvoicePaymentsService.ts +++ b/packages/server/src/services/Sales/Invoices/GetInvoicePaymentsService.ts @@ -4,7 +4,7 @@ import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransactio import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; @Service() -export default class InvoicePaymentsService { +export class GetInvoicePaymentsService { @Inject() private tenancy: HasTenancyService; diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts new file mode 100644 index 000000000..7fb5a4407 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts @@ -0,0 +1,47 @@ +import { Inject, Service } from 'typedi'; +import { ISaleInvoice, ISystemUser } from '@/interfaces'; +import { SaleInvoiceTransformer } from './SaleInvoiceTransformer'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; + +@Service() +export class GetSaleInvoice { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + @Inject() + private validators: CommandSaleInvoiceValidators; + + /** + * Retrieve sale invoice with associated entries. + * @param {Number} saleInvoiceId - + * @param {ISystemUser} authorizedUser - + * @return {Promise} + */ + public async getSaleInvoice( + tenantId: number, + saleInvoiceId: number, + authorizedUser: ISystemUser + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findById(saleInvoiceId) + .withGraphFetched('entries.item') + .withGraphFetched('customer') + .withGraphFetched('branch'); + + // Validates the given sale invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); + + return this.transformer.transform( + tenantId, + saleInvoice, + new SaleInvoiceTransformer() + ); + } +} diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts new file mode 100644 index 000000000..b1d9b93db --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts @@ -0,0 +1,80 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { + IFilterMeta, + IPaginationMeta, + ISaleInvoice, + ISalesInvoicesFilter, +} from '@/interfaces'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +import { SaleInvoiceTransformer } from './SaleInvoiceTransformer'; + +@Service() +export class GetSaleInvoices { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private dynamicListService: DynamicListingService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieve sales invoices filterable and paginated list. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async getSaleInvoices( + tenantId: number, + filterDTO: ISalesInvoicesFilter + ): Promise<{ + salesInvoices: ISaleInvoice[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + const { SaleInvoice } = this.tenancy.models(tenantId); + + // Parses stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + tenantId, + SaleInvoice, + filter + ); + const { results, pagination } = await SaleInvoice.query() + .onBuild((builder) => { + builder.withGraphFetched('entries'); + builder.withGraphFetched('customer'); + dynamicFilter.buildQuery()(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Retrieves the transformed sale invoices. + const salesInvoices = await this.transformer.transform( + tenantId, + results, + new SaleInvoiceTransformer() + ); + + return { + salesInvoices, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + + /** + * Parses the sale invoice list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoicesPayable.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoicesPayable.ts new file mode 100644 index 000000000..c81174376 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoicesPayable.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import { ISaleInvoice } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export class GetSaleInvoicesPayable { + @Inject() + private tenancy: HasTenancyService; + + /** + * Retrieve due sales invoices. + * @param {number} tenantId + * @param {number} customerId + */ + public async getPayableInvoices( + tenantId: number, + customerId?: number + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const salesInvoices = await SaleInvoice.query().onBuild((query) => { + query.modify('dueInvoices'); + query.modify('delivered'); + + if (customerId) { + query.where('customer_id', customerId); + } + }); + return salesInvoices; + } +} diff --git a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts index 791991b91..c47291fe0 100644 --- a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts +++ b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts @@ -88,8 +88,8 @@ export class SaleInvoiceGLEntries { /** * Retrieves the given invoice ledger. - * @param {ISaleInvoice} saleInvoice - * @param {number} ARAccountId + * @param {ISaleInvoice} saleInvoice + * @param {number} ARAccountId * @returns {ILedger} */ public getInvoiceGLedger = ( @@ -103,7 +103,7 @@ export class SaleInvoiceGLEntries { /** * Retrieves the invoice GL common entry. - * @param {ISaleInvoice} saleInvoice + * @param {ISaleInvoice} saleInvoice * @returns {Partial} */ private getInvoiceGLCommonEntry = ( @@ -131,8 +131,8 @@ export class SaleInvoiceGLEntries { /** * Retrieve receivable entry of the given invoice. - * @param {ISaleInvoice} saleInvoice - * @param {number} ARAccountId + * @param {ISaleInvoice} saleInvoice + * @param {number} ARAccountId * @returns {ILedgerEntry} */ private getInvoiceReceivableEntry = ( @@ -153,9 +153,9 @@ export class SaleInvoiceGLEntries { /** * Retrieve item income entry of the given invoice. - * @param {ISaleInvoice} saleInvoice - - * @param {IItemEntry} entry - - * @param {number} index - + * @param {ISaleInvoice} saleInvoice - + * @param {IItemEntry} entry - + * @param {number} index - * @returns {ILedgerEntry} */ private getInvoiceItemEntry = R.curry( @@ -183,8 +183,8 @@ export class SaleInvoiceGLEntries { /** * Retrieves the invoice GL entries. - * @param {ISaleInvoice} saleInvoice - * @param {number} ARAccountId + * @param {ISaleInvoice} saleInvoice + * @param {number} ARAccountId * @returns {ILedgerEntry[]} */ public getInvoiceGLEntries = ( diff --git a/packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts b/packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts new file mode 100644 index 000000000..16cbb1b0f --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts @@ -0,0 +1,77 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { ISaleInvoice } from '@/interfaces'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import InventoryService from '@/services/Inventory/Inventory'; + +@Service() +export class InvoiceInventoryTransactions { + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private inventoryService: InventoryService; + + /** + * Records the inventory transactions of the given sale invoice in case + * the invoice has inventory entries only. + * + * @param {number} tenantId - Tenant id. + * @param {SaleInvoice} saleInvoice - Sale invoice DTO. + * @param {number} saleInvoiceId - Sale invoice id. + * @param {boolean} override - Allow to override old transactions. + * @return {Promise} + */ + public async recordInventoryTranscactions( + tenantId: number, + saleInvoice: ISaleInvoice, + override?: boolean, + trx?: Knex.Transaction + ): Promise { + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + tenantId, + saleInvoice.entries + ); + const transaction = { + transactionId: saleInvoice.id, + transactionType: 'SaleInvoice', + transactionNumber: saleInvoice.invoiceNo, + + exchangeRate: saleInvoice.exchangeRate, + warehouseId: saleInvoice.warehouseId, + + date: saleInvoice.invoiceDate, + direction: 'OUT', + entries: inventoryEntries, + createdAt: saleInvoice.createdAt, + }; + await this.inventoryService.recordInventoryTransactionsFromItemsEntries( + tenantId, + transaction, + override, + trx + ); + } + /** + * Reverting the inventory transactions once the invoice deleted. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async revertInventoryTransactions( + tenantId: number, + saleInvoiceId: number, + trx?: Knex.Transaction + ): Promise { + // Delete the inventory transaction of the given sale invoice. + const { oldInventoryTransactions } = + await this.inventoryService.deleteInventoryTransactions( + tenantId, + saleInvoiceId, + 'SaleInvoice', + trx + ); + } +} diff --git a/packages/server/src/services/Sales/Invoices/InvoicePaymentTransactionTransformer.ts b/packages/server/src/services/Sales/Invoices/InvoicePaymentTransactionTransformer.ts index 602fc491c..23d548a0f 100644 --- a/packages/server/src/services/Sales/Invoices/InvoicePaymentTransactionTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/InvoicePaymentTransactionTransformer.ts @@ -21,6 +21,11 @@ export class InvoicePaymentTransactionTransformer extends Transformer { }); }; + /** + * Formatted payment date. + * @param entry + * @returns {string} + */ protected formattedPaymentDate = (entry): string => { return this.formatDate(entry.payment.paymentDate); }; diff --git a/packages/server/src/services/Sales/Invoices/InvoicePaymentsGLRewrite.ts b/packages/server/src/services/Sales/Invoices/InvoicePaymentsGLRewrite.ts index 82c61361b..3e3e9ffda 100644 --- a/packages/server/src/services/Sales/Invoices/InvoicePaymentsGLRewrite.ts +++ b/packages/server/src/services/Sales/Invoices/InvoicePaymentsGLRewrite.ts @@ -1,7 +1,7 @@ import { Knex } from 'knex'; import async from 'async'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; import { PaymentReceiveGLEntries } from '../PaymentReceives/PaymentReceiveGLEntries'; @Service() diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceCostGLEntries.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceCostGLEntries.ts index ad5951e4d..ff480fe28 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoiceCostGLEntries.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceCostGLEntries.ts @@ -64,7 +64,7 @@ export class SaleInvoiceCostGLEntries { /** * - * @param {IInventoryLotCost} inventoryCostLot + * @param {IInventoryLotCost} inventoryCostLot * @returns {} */ private getInvoiceCostGLCommonEntry = ( @@ -91,7 +91,7 @@ export class SaleInvoiceCostGLEntries { /** * Retrieves the inventory cost GL entry. - * @param {IInventoryLotCost} inventoryLotCost + * @param {IInventoryLotCost} inventoryLotCost * @returns {ILedgerEntry[]} */ private getInventoryCostGLEntry = R.curry( @@ -127,10 +127,10 @@ export class SaleInvoiceCostGLEntries { /** * Writes journal entries for given sale invoice. - * ------- + * ----- * - Cost of goods sold -> Debit -> YYYY * - Inventory assets -> Credit -> YYYY - * -------- + *----- * @param {ISaleInvoice} saleInvoice * @param {JournalPoster} journal */ diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceIncrement.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceIncrement.ts new file mode 100644 index 000000000..887ee31a2 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceIncrement.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import AutoIncrementOrdersService from '../AutoIncrementOrdersService'; + +@Service() +export class SaleInvoiceIncrement { + @Inject() + private autoIncrementOrdersService: AutoIncrementOrdersService; + + /** + * Retrieves the next unique invoice number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + public getNextInvoiceNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'sales_invoices' + ); + } + + /** + * Increment the invoice next number. + * @param {number} tenantId - + */ + public incrementNextInvoiceNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'sales_invoices' + ); + } +} diff --git a/packages/server/src/services/Sales/SaleInvoiceNotifyBySms.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceNotifyBySms.ts similarity index 93% rename from packages/server/src/services/Sales/SaleInvoiceNotifyBySms.ts rename to packages/server/src/services/Sales/Invoices/SaleInvoiceNotifyBySms.ts index f0303b981..1eff51f6e 100644 --- a/packages/server/src/services/Sales/SaleInvoiceNotifyBySms.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceNotifyBySms.ts @@ -2,8 +2,6 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import events from '@/subscribers/events'; -import SaleInvoicesService from './SalesInvoices'; -import SMSClient from '@/services/SMSClient'; import { ISaleInvoice, ISaleInvoiceSmsDetailsDTO, @@ -15,27 +13,28 @@ import { import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; import { formatSmsMessage, formatNumber } from 'utils'; import { TenantMetadata } from '@/system/models'; -import SaleNotifyBySms from './SaleNotifyBySms'; +import SaleNotifyBySms from '../SaleNotifyBySms'; import { ServiceError } from '@/exceptions'; -import { ERRORS } from './constants'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { ERRORS } from './constants'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; @Service() -export default class SaleInvoiceNotifyBySms { +export class SaleInvoiceNotifyBySms { @Inject() - invoiceService: SaleInvoicesService; + private tenancy: HasTenancyService; @Inject() - tenancy: HasTenancyService; + private eventPublisher: EventPublisher; @Inject() - eventPublisher: EventPublisher; + private smsNotificationsSettings: SmsNotificationsSettingsService; @Inject() - smsNotificationsSettings: SmsNotificationsSettingsService; + private saleSmsNotification: SaleNotifyBySms; @Inject() - saleSmsNotification: SaleNotifyBySms; + private validators: CommandSaleInvoiceValidators; /** * Notify customer via sms about sale invoice. @@ -54,6 +53,9 @@ export default class SaleInvoiceNotifyBySms { .findById(saleInvoiceId) .withGraphFetched('customer'); + // Validates the givne invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); + // Validate the customer phone number existance and number validation. this.saleSmsNotification.validateCustomerPhoneNumber( saleInvoice.customer.personalPhone diff --git a/packages/server/src/services/Sales/SaleInvoicePdf.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts similarity index 96% rename from packages/server/src/services/Sales/SaleInvoicePdf.ts rename to packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts index fdbd89100..edd98139f 100644 --- a/packages/server/src/services/Sales/SaleInvoicePdf.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts @@ -5,7 +5,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Tenant } from '@/system/models'; @Service() -export default class SaleInvoicePdf { +export class SaleInvoicePdf { @Inject() pdfService: PdfService; diff --git a/packages/server/src/services/Sales/SaleInvoiceTransformer.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts similarity index 100% rename from packages/server/src/services/Sales/SaleInvoiceTransformer.ts rename to packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts diff --git a/packages/server/src/services/Sales/SaleInvoiceWriteoffGLEntries.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceWriteoffGLEntries.ts similarity index 90% rename from packages/server/src/services/Sales/SaleInvoiceWriteoffGLEntries.ts rename to packages/server/src/services/Sales/Invoices/SaleInvoiceWriteoffGLEntries.ts index 456432cde..75225d95f 100644 --- a/packages/server/src/services/Sales/SaleInvoiceWriteoffGLEntries.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceWriteoffGLEntries.ts @@ -26,8 +26,8 @@ export class SaleInvoiceWriteoffGLEntries { /** * Retrieves the invoice write-off receiveable GL entry. - * @param {number} ARAccountId - * @param {ISaleInvoice} saleInvoice + * @param {number} ARAccountId + * @param {ISaleInvoice} saleInvoice * @returns {ILedgerEntry} */ private getInvoiceWriteoffGLReceivableEntry = ( @@ -50,7 +50,7 @@ export class SaleInvoiceWriteoffGLEntries { /** * Retrieves the invoice write-off expense GL entry. - * @param {ISaleInvoice} saleInvoice + * @param {ISaleInvoice} saleInvoice * @returns {ILedgerEntry} */ private getInvoiceWriteoffGLExpenseEntry = ( @@ -71,8 +71,8 @@ export class SaleInvoiceWriteoffGLEntries { /** * Retrieves the invoice write-off GL entries. - * @param {number} ARAccountId - * @param {ISaleInvoice} saleInvoice + * @param {number} ARAccountId + * @param {ISaleInvoice} saleInvoice * @returns {ILedgerEntry[]} */ public getInvoiceWriteoffGLEntries = ( @@ -89,8 +89,8 @@ export class SaleInvoiceWriteoffGLEntries { /** * Retrieves the invoice write-off ledger. - * @param {number} ARAccountId - * @param {ISaleInvoice} saleInvoice + * @param {number} ARAccountId + * @param {ISaleInvoice} saleInvoice * @returns {Ledger} */ public getInvoiceWriteoffLedger = ( diff --git a/packages/server/src/services/Sales/SaleInvoiceWriteoffGLStorage.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceWriteoffGLStorage.ts similarity index 100% rename from packages/server/src/services/Sales/SaleInvoiceWriteoffGLStorage.ts rename to packages/server/src/services/Sales/Invoices/SaleInvoiceWriteoffGLStorage.ts diff --git a/packages/server/src/services/Sales/SaleInvoiceWriteoffSubscriber.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceWriteoffSubscriber.ts similarity index 100% rename from packages/server/src/services/Sales/SaleInvoiceWriteoffSubscriber.ts rename to packages/server/src/services/Sales/Invoices/SaleInvoiceWriteoffSubscriber.ts diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts new file mode 100644 index 000000000..8a37386f9 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -0,0 +1,282 @@ +import { + IFilterMeta, + IPaginationMeta, + ISaleInvoice, + ISaleInvoiceCreateDTO, + ISaleInvoiceEditDTO, + ISaleInvoiceSmsDetails, + ISaleInvoiceSmsDetailsDTO, + ISaleInvoiceWriteoffDTO, + ISalesInvoicesFilter, + ISystemUser, + ITenantUser, + InvoiceNotificationType, +} from '@/interfaces'; +import { Inject, Service } from 'typedi'; +import { CreateSaleInvoice } from './CreateSaleInvoice'; +import { DeleteSaleInvoice } from './DeleteSaleInvoice'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { EditSaleInvoice } from './EditSaleInvoice'; +import { GetSaleInvoices } from './GetSaleInvoices'; +import { DeliverSaleInvoice } from './DeliverSaleInvoice'; +import { GetSaleInvoicesPayable } from './GetSaleInvoicesPayable'; +import { WriteoffSaleInvoice } from './WriteoffSaleInvoice'; +import { SaleInvoicePdf } from './SaleInvoicePdf'; +import { GetInvoicePaymentsService } from './GetInvoicePaymentsService'; +import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms'; + +@Service() +export class SaleInvoiceApplication { + @Inject() + private createSaleInvoiceService: CreateSaleInvoice; + + @Inject() + private deleteSaleInvoiceService: DeleteSaleInvoice; + + @Inject() + private getSaleInvoiceService: GetSaleInvoice; + + @Inject() + private getSaleInvoicesService: GetSaleInvoices; + + @Inject() + private editSaleInvoiceService: EditSaleInvoice; + + @Inject() + private deliverSaleInvoiceService: DeliverSaleInvoice; + + @Inject() + private getReceivableSaleInvoicesService: GetSaleInvoicesPayable; + + @Inject() + private writeoffInvoiceService: WriteoffSaleInvoice; + + @Inject() + private getInvoicePaymentsService: GetInvoicePaymentsService; + + @Inject() + private pdfSaleInvoiceService: SaleInvoicePdf; + + @Inject() + private invoiceSms: SaleInvoiceNotifyBySms; + + /** + * Creates a new sale invoice with associated GL entries. + * @param {number} tenantId + * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO + * @param {ITenantUser} authorizedUser + * @returns {Promise} + */ + public createSaleInvoice( + tenantId: number, + saleInvoiceDTO: ISaleInvoiceCreateDTO, + authorizedUser: ITenantUser + ): Promise { + return this.createSaleInvoiceService.createSaleInvoice( + tenantId, + saleInvoiceDTO, + authorizedUser + ); + } + + /** + * Edits the given sale invoice with associated GL entries. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {ISaleInvoiceEditDTO} saleInvoiceDTO + * @param {ISystemUser} authorizedUser + * @returns {Promise} + */ + public editSaleInvoice( + tenantId: number, + saleInvoiceId: number, + saleInvoiceDTO: ISaleInvoiceEditDTO, + authorizedUser: ISystemUser + ) { + return this.editSaleInvoiceService.editSaleInvoice( + tenantId, + saleInvoiceId, + saleInvoiceDTO, + authorizedUser + ); + } + + /** + * Deletes the given sale invoice with given associated GL entries. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {ISystemUser} authorizedUser + * @returns {Promise} + */ + public deleteSaleInvoice( + tenantId: number, + saleInvoiceId: number, + authorizedUser: ISystemUser + ): Promise { + return this.deleteSaleInvoiceService.deleteSaleInvoice( + tenantId, + saleInvoiceId, + authorizedUser + ); + } + + /** + * Retrieves the given sale invoice details. + * @param {number} tenantId + * @param {ISalesInvoicesFilter} filterDTO + * @returns + */ + public getSaleInvoices( + tenantId: number, + filterDTO: ISalesInvoicesFilter + ): Promise<{ + salesInvoices: ISaleInvoice[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + return this.getSaleInvoicesService.getSaleInvoices(tenantId, filterDTO); + } + + /** + * Retrieves sale invoice details. + * @param {number} tenantId - + * @param {number} saleInvoiceId - + * @param {ISystemUser} authorizedUser - + * @return {Promise} + */ + public getSaleInvoice( + tenantId: number, + saleInvoiceId: number, + authorizedUser: ISystemUser + ) { + return this.getSaleInvoiceService.getSaleInvoice( + tenantId, + saleInvoiceId, + authorizedUser + ); + } + + /** + * Mark the given sale invoice as delivered. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {ISystemUser} authorizedUser + * @returns {} + */ + public deliverSaleInvoice( + tenantId: number, + saleInvoiceId: number, + authorizedUser: ISystemUser + ) { + return this.deliverSaleInvoiceService.deliverSaleInvoice( + tenantId, + saleInvoiceId, + authorizedUser + ); + } + + /** + * Retrieves the receivable sale invoices of the given customer. + * @param {number} tenantId + * @param {number} customerId + * @returns + */ + public getReceivableSaleInvoices(tenantId: number, customerId?: number) { + return this.getReceivableSaleInvoicesService.getPayableInvoices( + tenantId, + customerId + ); + } + + /** + * Writes-off the sale invoice on bad debt expense account. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {ISaleInvoiceWriteoffDTO} writeoffDTO + * @return {Promise} + */ + public writeOff = async ( + tenantId: number, + saleInvoiceId: number, + writeoffDTO: ISaleInvoiceWriteoffDTO + ): Promise => { + return this.writeoffInvoiceService.writeOff( + tenantId, + saleInvoiceId, + writeoffDTO + ); + }; + + /** + * Cancels the written-off sale invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @returns {Promise} + */ + public cancelWrittenoff = ( + tenantId: number, + saleInvoiceId: number + ): Promise => { + return this.writeoffInvoiceService.cancelWrittenoff( + tenantId, + saleInvoiceId + ); + }; + + /** + * Retrieve the invoice assocaited payments transactions. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Invoice id. + */ + public getInvoicePayments = async (tenantId: number, invoiceId: number) => { + return this.getInvoicePaymentsService.getInvoicePayments( + tenantId, + invoiceId + ); + }; + + /** + * + * @param {number} tenantId ] + * @param saleInvoice + * @returns + */ + public saleInvoicePdf(tenantId: number, saleInvoice) { + return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice); + } + + /** + * + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {InvoiceNotificationType} invoiceNotificationType + */ + public notifySaleInvoiceBySms = async ( + tenantId: number, + saleInvoiceId: number, + invoiceNotificationType: InvoiceNotificationType + ) => { + return this.invoiceSms.notifyBySms( + tenantId, + saleInvoiceId, + invoiceNotificationType + ); + }; + + /** + * Retrieves the SMS details of the given invoice. + * @param {number} tenantId - Tenant id. + * @param {number} saleInvoiceId - Sale invoice id. + */ + public getSaleInvoiceSmsDetails = async ( + tenantId: number, + saleInvoiceId: number, + invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO + ): Promise => { + return this.invoiceSms.smsDetails( + tenantId, + saleInvoiceId, + invoiceSmsDetailsDTO + ); + }; +} diff --git a/packages/server/src/services/Sales/SalesInvoicesCost.ts b/packages/server/src/services/Sales/Invoices/SalesInvoicesCost.ts similarity index 92% rename from packages/server/src/services/Sales/SalesInvoicesCost.ts rename to packages/server/src/services/Sales/Invoices/SalesInvoicesCost.ts index 8dcfe9977..1faa37350 100644 --- a/packages/server/src/services/Sales/SalesInvoicesCost.ts +++ b/packages/server/src/services/Sales/Invoices/SalesInvoicesCost.ts @@ -3,27 +3,22 @@ import { chain } from 'lodash'; import moment from 'moment'; import { Knex } from 'knex'; import InventoryService from '@/services/Inventory/Inventory'; -import TenancyService from '@/services/Tenancy/TenancyService'; import { IInventoryCostLotsGLEntriesWriteEvent, IInventoryTransaction, } from '@/interfaces'; import UnitOfWork from '@/services/UnitOfWork'; -import { SaleInvoiceCostGLEntries } from './Invoices/SaleInvoiceCostGLEntries'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; @Service() -export default class SaleInvoicesCost { +export class SaleInvoicesCost { @Inject() private inventoryService: InventoryService; @Inject() private uow: UnitOfWork; - @Inject() - private costGLEntries: SaleInvoiceCostGLEntries; - @Inject() private eventPublisher: EventPublisher; @@ -122,8 +117,8 @@ export default class SaleInvoicesCost { /** * Writes cost GL entries from the inventory cost lots. - * @param {number} tenantId - - * @param {Date} startingDate - + * @param {number} tenantId - + * @param {Date} startingDate - * @returns {Promise} */ public writeCostLotsGLEntries = (tenantId: number, startingDate: Date) => { diff --git a/packages/server/src/services/Sales/SaleInvoiceWriteoff.ts b/packages/server/src/services/Sales/Invoices/WriteoffSaleInvoice.ts similarity index 85% rename from packages/server/src/services/Sales/SaleInvoiceWriteoff.ts rename to packages/server/src/services/Sales/Invoices/WriteoffSaleInvoice.ts index 35792b75f..7069e7635 100644 --- a/packages/server/src/services/Sales/SaleInvoiceWriteoff.ts +++ b/packages/server/src/services/Sales/Invoices/WriteoffSaleInvoice.ts @@ -10,29 +10,24 @@ import { import HasTenancyService from '@/services/Tenancy/TenancyService'; import events from '@/subscribers/events'; import { ServiceError } from '@/exceptions'; - -import JournalPosterService from './JournalPosterService'; import UnitOfWork from '@/services/UnitOfWork'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; - -const ERRORS = { - SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF', - SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF', -}; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; +import { ERRORS } from './constants'; @Service() -export default class SaleInvoiceWriteoff { +export class WriteoffSaleInvoice { @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; @Inject() - eventPublisher: EventPublisher; + private eventPublisher: EventPublisher; @Inject() - journalService: JournalPosterService; + private uow: UnitOfWork; @Inject() - uow: UnitOfWork; + private validators: CommandSaleInvoiceValidators; /** * Writes-off the sale invoice on bad debt expense account. @@ -48,16 +43,15 @@ export default class SaleInvoiceWriteoff { ): Promise => { const { SaleInvoice } = this.tenancy.models(tenantId); - // Validate the sale invoice existance. - // Retrieve the sale invoice or throw not found service error. - const saleInvoice = await SaleInvoice.query() - .findById(saleInvoiceId) - .throwIfNotFound(); + const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId); + + // Validates the given invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); // Validate the sale invoice whether already written-off. this.validateSaleInvoiceAlreadyWrittenoff(saleInvoice); - // Saves the invoice write-off transaction with associated transactions + // Saves the invoice write-off transaction with associated transactions // under unit-of-work envirmenet. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { const eventPayload = { @@ -105,15 +99,16 @@ export default class SaleInvoiceWriteoff { // Validate the sale invoice existance. // Retrieve the sale invoice or throw not found service error. - const saleInvoice = await SaleInvoice.query() - .findById(saleInvoiceId) - .throwIfNotFound(); + const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId); + + // Validate the sale invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); // Validate the sale invoice whether already written-off. this.validateSaleInvoiceNotWrittenoff(saleInvoice); // Cancels the invoice written-off and removes the associated transactions. - return this.uow.withTransaction(tenantId, async (trx) => { + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onSaleInvoiceWrittenoffCancel` event. await this.eventPublisher.emitAsync( events.saleInvoice.onWrittenoffCancel, diff --git a/packages/server/src/services/Sales/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts similarity index 89% rename from packages/server/src/services/Sales/constants.ts rename to packages/server/src/services/Sales/Invoices/constants.ts index b4e5ff272..018dec027 100644 --- a/packages/server/src/services/Sales/constants.ts +++ b/packages/server/src/services/Sales/Invoices/constants.ts @@ -11,8 +11,11 @@ export const ERRORS = { 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES', SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED', CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES', - SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES: 'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES', - PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID' + SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES: + 'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES', + PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', + SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF', + SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF', }; export const DEFAULT_VIEW_COLUMNS = []; diff --git a/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts b/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts new file mode 100644 index 000000000..462fb6495 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts @@ -0,0 +1,136 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { + ICustomer, + IPaymentReceiveCreateDTO, + IPaymentReceiveCreatedPayload, + IPaymentReceiveCreatingPayload, + ISystemUser, +} from '@/interfaces'; +import { PaymentReceiveValidators } from './PaymentReceiveValidators'; +import events from '@/subscribers/events'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { PaymentReceiveDTOTransformer } from './PaymentReceiveDTOTransformer'; +import { TenantMetadata } from '@/system/models'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; + +@Service() +export class CreatePaymentReceive { + @Inject() + private validators: PaymentReceiveValidators; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private transformer: PaymentReceiveDTOTransformer; + + /** + * Creates a new payment receive and store it to the storage + * with associated invoices payment and journal transactions. + * @async + * @param {number} tenantId - Tenant id. + * @param {IPaymentReceive} paymentReceive + */ + public async createPaymentReceive( + tenantId: number, + paymentReceiveDTO: IPaymentReceiveCreateDTO, + authorizedUser: ISystemUser + ) { + const { PaymentReceive, Contact } = this.tenancy.models(tenantId); + + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + + // Validate customer existance. + const paymentCustomer = await Contact.query() + .modify('customer') + .findById(paymentReceiveDTO.customerId) + .throwIfNotFound(); + + // Transformes the payment receive DTO to model. + const paymentReceiveObj = await this.transformCreateDTOToModel( + tenantId, + paymentCustomer, + paymentReceiveDTO + ); + // Validate payment receive number uniquiness. + await this.validators.validatePaymentReceiveNoExistance( + tenantId, + paymentReceiveObj.paymentReceiveNo + ); + // Validate the deposit account existance and type. + const depositAccount = await this.validators.getDepositAccountOrThrowError( + tenantId, + paymentReceiveDTO.depositAccountId + ); + // Validate payment receive invoices IDs existance. + await this.validators.validateInvoicesIDsExistance( + tenantId, + paymentReceiveDTO.customerId, + paymentReceiveDTO.entries + ); + // Validate invoice payment amount. + await this.validators.validateInvoicesPaymentsAmount( + tenantId, + paymentReceiveDTO.entries + ); + // Validates the payment account currency code. + this.validators.validatePaymentAccountCurrency( + depositAccount.currencyCode, + paymentCustomer.currencyCode, + tenantMeta.baseCurrency + ); + // Creates a payment receive transaction under UOW envirment. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onPaymentReceiveCreating` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, { + trx, + paymentReceiveDTO, + tenantId, + } as IPaymentReceiveCreatingPayload); + + // Inserts the payment receive transaction. + const paymentReceive = await PaymentReceive.query( + trx + ).insertGraphAndFetch({ + ...paymentReceiveObj, + }); + // Triggers `onPaymentReceiveCreated` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, { + tenantId, + paymentReceive, + paymentReceiveId: paymentReceive.id, + authorizedUser, + trx, + } as IPaymentReceiveCreatedPayload); + + return paymentReceive; + }); + } + + /** + * Transform the create payment receive DTO. + * @param {number} tenantId + * @param {ICustomer} customer + * @param {IPaymentReceiveCreateDTO} paymentReceiveDTO + * @returns + */ + private transformCreateDTOToModel = async ( + tenantId: number, + customer: ICustomer, + paymentReceiveDTO: IPaymentReceiveCreateDTO + ) => { + return this.transformer.transformPaymentReceiveDTOToModel( + tenantId, + customer, + paymentReceiveDTO + ); + }; +} diff --git a/packages/server/src/services/Sales/PaymentReceives/DeletePaymentReceive.ts b/packages/server/src/services/Sales/PaymentReceives/DeletePaymentReceive.ts new file mode 100644 index 000000000..3a7d211a7 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/DeletePaymentReceive.ts @@ -0,0 +1,79 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { + IPaymentReceiveDeletedPayload, + IPaymentReceiveDeletingPayload, + ISystemUser, +} from '@/interfaces'; +import UnitOfWork from '@/services/UnitOfWork'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import events from '@/subscribers/events'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; + +@Service() +export class DeletePaymentReceive { + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + /** + * Deletes the given payment receive with associated entries + * and journal transactions. + * ----- + * - Deletes the payment receive transaction. + * - Deletes the payment receive associated entries. + * - Deletes the payment receive associated journal transactions. + * - Revert the customer balance. + * - Revert the payment amount of the associated invoices. + * @async + * @param {number} tenantId - Tenant id. + * @param {Integer} paymentReceiveId - Payment receive id. + * @param {IPaymentReceive} paymentReceive - Payment receive object. + */ + public async deletePaymentReceive( + tenantId: number, + paymentReceiveId: number, + authorizedUser: ISystemUser + ) { + const { PaymentReceive, PaymentReceiveEntry } = + this.tenancy.models(tenantId); + + // Retreive payment receive or throw not found service error. + const oldPaymentReceive = await PaymentReceive.query() + .withGraphFetched('entries') + .findById(paymentReceiveId) + .throwIfNotFound(); + + // Delete payment receive transaction and associate transactions under UOW env. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onPaymentReceiveDeleting` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onDeleting, { + tenantId, + oldPaymentReceive, + trx, + } as IPaymentReceiveDeletingPayload); + + // Deletes the payment receive associated entries. + await PaymentReceiveEntry.query(trx) + .where('payment_receive_id', paymentReceiveId) + .delete(); + + // Deletes the payment receive transaction. + await PaymentReceive.query(trx).findById(paymentReceiveId).delete(); + + // Triggers `onPaymentReceiveDeleted` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onDeleted, { + tenantId, + paymentReceiveId, + oldPaymentReceive, + authorizedUser, + trx, + } as IPaymentReceiveDeletedPayload); + }); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts b/packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts new file mode 100644 index 000000000..b99413748 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts @@ -0,0 +1,177 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { + ICustomer, + IPaymentReceive, + IPaymentReceiveEditDTO, + IPaymentReceiveEditedPayload, + IPaymentReceiveEditingPayload, + ISystemUser, +} from '@/interfaces'; +import { PaymentReceiveDTOTransformer } from './PaymentReceiveDTOTransformer'; +import { PaymentReceiveValidators } from './PaymentReceiveValidators'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; +import UnitOfWork from '@/services/UnitOfWork'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TenantMetadata } from '@/system/models'; + +@Service() +export class EditPaymentReceive { + @Inject() + private transformer: PaymentReceiveDTOTransformer; + + @Inject() + private validators: PaymentReceiveValidators; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private tenancy: HasTenancyService; + + /** + * Edit details the given payment receive with associated entries. + * ------ + * - Update the payment receive transactions. + * - Insert the new payment receive entries. + * - Update the given payment receive entries. + * - Delete the not presented payment receive entries. + * - Re-insert the journal transactions and update the different accounts balance. + * - Update the different customer balances. + * - Update the different invoice payment amount. + * @async + * @param {number} tenantId - + * @param {Integer} paymentReceiveId - + * @param {IPaymentReceive} paymentReceive - + */ + public async editPaymentReceive( + tenantId: number, + paymentReceiveId: number, + paymentReceiveDTO: IPaymentReceiveEditDTO, + authorizedUser: ISystemUser + ) { + const { PaymentReceive, Contact } = this.tenancy.models(tenantId); + + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + + // Validate the payment receive existance. + const oldPaymentReceive = await PaymentReceive.query() + .withGraphFetched('entries') + .findById(paymentReceiveId); + + // Validates the payment existance. + this.validators.validatePaymentExistance(oldPaymentReceive); + + // Validate customer existance. + const customer = await Contact.query() + .modify('customer') + .findById(paymentReceiveDTO.customerId) + .throwIfNotFound(); + + // Transformes the payment receive DTO to model. + const paymentReceiveObj = await this.transformEditDTOToModel( + tenantId, + customer, + paymentReceiveDTO, + oldPaymentReceive + ); + // Validate customer whether modified. + this.validators.validateCustomerNotModified( + paymentReceiveDTO, + oldPaymentReceive + ); + // Validate payment receive number uniquiness. + if (paymentReceiveDTO.paymentReceiveNo) { + await this.validators.validatePaymentReceiveNoExistance( + tenantId, + paymentReceiveDTO.paymentReceiveNo, + paymentReceiveId + ); + } + // Validate the deposit account existance and type. + const depositAccount = await this.validators.getDepositAccountOrThrowError( + tenantId, + paymentReceiveDTO.depositAccountId + ); + // Validate the entries ids existance on payment receive type. + await this.validators.validateEntriesIdsExistance( + tenantId, + paymentReceiveId, + paymentReceiveDTO.entries + ); + // Validate payment receive invoices IDs existance and associated + // to the given customer id. + await this.validators.validateInvoicesIDsExistance( + tenantId, + oldPaymentReceive.customerId, + paymentReceiveDTO.entries + ); + // Validate invoice payment amount. + await this.validators.validateInvoicesPaymentsAmount( + tenantId, + paymentReceiveDTO.entries, + oldPaymentReceive.entries + ); + // Validates the payment account currency code. + this.validators.validatePaymentAccountCurrency( + depositAccount.currencyCode, + customer.currencyCode, + tenantMeta.baseCurrency + ); + // Creates payment receive transaction under UOW envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onPaymentReceiveEditing` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onEditing, { + trx, + tenantId, + oldPaymentReceive, + paymentReceiveDTO, + } as IPaymentReceiveEditingPayload); + + // Update the payment receive transaction. + const paymentReceive = await PaymentReceive.query( + trx + ).upsertGraphAndFetch({ + id: paymentReceiveId, + ...paymentReceiveObj, + }); + // Triggers `onPaymentReceiveEdited` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, { + tenantId, + paymentReceiveId, + paymentReceive, + oldPaymentReceive, + authorizedUser, + trx, + } as IPaymentReceiveEditedPayload); + + return paymentReceive; + }); + } + + /** + * Transform the edit payment receive DTO. + * @param {number} tenantId + * @param {ICustomer} customer + * @param {IPaymentReceiveEditDTO} paymentReceiveDTO + * @param {IPaymentReceive} oldPaymentReceive + * @returns + */ + private transformEditDTOToModel = async ( + tenantId: number, + customer: ICustomer, + paymentReceiveDTO: IPaymentReceiveEditDTO, + oldPaymentReceive: IPaymentReceive + ) => { + return this.transformer.transformPaymentReceiveDTOToModel( + tenantId, + customer, + paymentReceiveDTO, + oldPaymentReceive + ); + }; +} diff --git a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceive.ts b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceive.ts new file mode 100644 index 000000000..c89697588 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceive.ts @@ -0,0 +1,46 @@ +import { ServiceError } from '@/exceptions'; +import { IPaymentReceive } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { ERRORS } from './constants'; +import { PaymentReceiveTransfromer } from './PaymentReceiveTransformer'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; + +@Service() +export class GetPaymentReceive { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieve payment receive details. + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment receive id. + * @return {Promise} + */ + public async getPaymentReceive( + tenantId: number, + paymentReceiveId: number + ): Promise { + const { PaymentReceive } = this.tenancy.models(tenantId); + + const paymentReceive = await PaymentReceive.query() + .withGraphFetched('customer') + .withGraphFetched('depositAccount') + .withGraphFetched('entries.invoice') + .withGraphFetched('transactions') + .withGraphFetched('branch') + .findById(paymentReceiveId); + + if (!paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); + } + return this.transformer.transform( + tenantId, + paymentReceive, + new PaymentReceiveTransfromer() + ); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceiveInvoices.ts b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceiveInvoices.ts new file mode 100644 index 000000000..48629770c --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceiveInvoices.ts @@ -0,0 +1,41 @@ +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { PaymentReceiveValidators } from './PaymentReceiveValidators'; + +@Service() +export class GetPaymentReceiveInvoices { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private validators: PaymentReceiveValidators; + + /** + * Retrieve sale invoices that assocaited to the given payment receive. + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment receive id. + * @return {Promise} + */ + public async getPaymentReceiveInvoices( + tenantId: number, + paymentReceiveId: number + ) { + const { SaleInvoice, PaymentReceive } = this.tenancy.models(tenantId); + + const paymentReceive = await PaymentReceive.query() + .findById(paymentReceiveId) + .withGraphFetched('entries'); + + // Validates the payment receive existance. + this.validators.validatePaymentExistance(paymentReceive); + + const paymentReceiveInvoicesIds = paymentReceive.entries.map( + (entry) => entry.invoiceId + ); + const saleInvoices = await SaleInvoice.query().whereIn( + 'id', + paymentReceiveInvoicesIds + ); + return saleInvoices; + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceives.ts b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceives.ts new file mode 100644 index 000000000..3c076b442 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReceives.ts @@ -0,0 +1,77 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { + IFilterMeta, + IPaginationMeta, + IPaymentReceive, + IPaymentReceivesFilter, +} from '@/interfaces'; +import { PaymentReceiveTransfromer } from './PaymentReceiveTransformer'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import DynamicListingService from '@/services/DynamicListing/DynamicListService'; + +@Service() +export class GetPaymentReceives { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private dynamicListService: DynamicListingService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieve payment receives paginated and filterable list. + * @param {number} tenantId + * @param {IPaymentReceivesFilter} paymentReceivesFilter + */ + public async getPaymentReceives( + tenantId: number, + filterDTO: IPaymentReceivesFilter + ): Promise<{ + paymentReceives: IPaymentReceive[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + const { PaymentReceive } = this.tenancy.models(tenantId); + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + tenantId, + PaymentReceive, + filter + ); + const { results, pagination } = await PaymentReceive.query() + .onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('depositAccount'); + dynamicList.buildQuery()(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Transformer the payment receives models to POJO. + const transformedPayments = await this.transformer.transform( + tenantId, + results, + new PaymentReceiveTransfromer() + ); + return { + paymentReceives: transformedPayments, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } + + /** + * Parses payments receive list filter DTO. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts index 76abd1e5d..24949425d 100644 --- a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts +++ b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts @@ -7,10 +7,10 @@ import { Tenant } from '@/system/models'; @Service() export default class GetPaymentReceivePdf { @Inject() - pdfService: PdfService; + private pdfService: PdfService; @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; /** * Retrieve sale invoice pdf content. diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts new file mode 100644 index 000000000..a1eab9814 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts @@ -0,0 +1,69 @@ +import * as R from 'ramda'; +import { Inject, Service } from 'typedi'; +import { omit, sumBy } from 'lodash'; +import { + ICustomer, + IPaymentReceive, + IPaymentReceiveCreateDTO, + IPaymentReceiveEditDTO, +} from '@/interfaces'; +import { PaymentReceiveValidators } from './PaymentReceiveValidators'; +import { PaymentReceiveIncrement } from './PaymentReceiveIncrement'; +import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; +import { formatDateFields } from '@/utils'; + +@Service() +export class PaymentReceiveDTOTransformer { + @Inject() + private validators: PaymentReceiveValidators; + + @Inject() + private increments: PaymentReceiveIncrement; + + @Inject() + private branchDTOTransform: BranchTransactionDTOTransform; + + /** + * Transformes the create payment receive DTO to model object. + * @param {number} tenantId + * @param {IPaymentReceiveCreateDTO|IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO. + * @param {IPaymentReceive} oldPaymentReceive - + * @return {IPaymentReceive} + */ + public async transformPaymentReceiveDTOToModel( + tenantId: number, + customer: ICustomer, + paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, + oldPaymentReceive?: IPaymentReceive + ): Promise { + const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + + // Retreive the next invoice number. + const autoNextNumber = + this.increments.getNextPaymentReceiveNumber(tenantId); + + // Retrieve the next payment receive number. + const paymentReceiveNo = + paymentReceiveDTO.paymentReceiveNo || + oldPaymentReceive?.paymentReceiveNo || + autoNextNumber; + + this.validators.validatePaymentNoRequire(paymentReceiveNo); + + const initialDTO = { + ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [ + 'paymentDate', + ]), + amount: paymentAmount, + currencyCode: customer.currencyCode, + ...(paymentReceiveNo ? { paymentReceiveNo } : {}), + exchangeRate: paymentReceiveDTO.exchangeRate || 1, + entries: paymentReceiveDTO.entries.map((entry) => ({ + ...entry, + })), + }; + return R.compose( + this.branchDTOTransform.transformDTO(tenantId) + )(initialDTO); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveIncrement.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveIncrement.ts new file mode 100644 index 000000000..804fb1fbc --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveIncrement.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import AutoIncrementOrdersService from '../AutoIncrementOrdersService'; + +@Service() +export class PaymentReceiveIncrement { + @Inject() + private autoIncrementOrdersService: AutoIncrementOrdersService; + + /** + * Retrieve the next unique payment receive number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + public getNextPaymentReceiveNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'payment_receives' + ); + } + + /** + * Increment the payment receive next number. + * @param {number} tenantId + */ + public incrementNextPaymentReceiveNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'payment_receives' + ); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveInvoiceSync.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveInvoiceSync.ts new file mode 100644 index 000000000..0f7831237 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveInvoiceSync.ts @@ -0,0 +1,48 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { IPaymentReceiveEntryDTO } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { entriesAmountDiff } from '@/utils'; + +@Service() +export class PaymentReceiveInvoiceSync { + @Inject() + private tenancy: HasTenancyService; + + /** + * Saves difference changing between old and new invoice payment amount. + * @async + * @param {number} tenantId - Tenant id. + * @param {Array} paymentReceiveEntries + * @param {Array} newPaymentReceiveEntries + * @return {Promise} + */ + public async saveChangeInvoicePaymentAmount( + tenantId: number, + newPaymentReceiveEntries: IPaymentReceiveEntryDTO[], + oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[], + trx?: Knex.Transaction + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + const opers: Promise[] = []; + + const diffEntries = entriesAmountDiff( + newPaymentReceiveEntries, + oldPaymentReceiveEntries, + 'paymentAmount', + 'invoiceId' + ); + diffEntries.forEach((diffEntry: any) => { + if (diffEntry.paymentAmount === 0) { + return; + } + const oper = SaleInvoice.changePaymentAmount( + diffEntry.invoiceId, + diffEntry.paymentAmount, + trx + ); + opers.push(oper); + }); + await Promise.all([...opers]); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveSmsNotify.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveSmsNotify.ts index fc4eba813..67810aa85 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveSmsNotify.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveSmsNotify.ts @@ -1,36 +1,35 @@ import { Service, Inject } from 'typedi'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import events from '@/subscribers/events'; -import SMSClient from '@/services/SMSClient'; import { IPaymentReceiveSmsDetails, SMS_NOTIFICATION_KEY, IPaymentReceive, IPaymentReceiveEntry, } from '@/interfaces'; -import PaymentReceiveService from './PaymentsReceives'; import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; import { formatNumber, formatSmsMessage } from 'utils'; import { TenantMetadata } from '@/system/models'; import SaleNotifyBySms from '../SaleNotifyBySms'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { PaymentReceiveValidators } from './PaymentReceiveValidators'; @Service() -export default class PaymentReceiveNotifyBySms { +export class PaymentReceiveNotifyBySms { @Inject() - paymentReceiveService: PaymentReceiveService; + private tenancy: HasTenancyService; @Inject() - tenancy: HasTenancyService; + private eventPublisher: EventPublisher; @Inject() - eventPublisher: EventPublisher; + private smsNotificationsSettings: SmsNotificationsSettingsService; @Inject() - smsNotificationsSettings: SmsNotificationsSettingsService; + private saleSmsNotification: SaleNotifyBySms; @Inject() - saleSmsNotification: SaleNotifyBySms; + private validators: PaymentReceiveValidators; /** * Notify customer via sms about payment receive details. @@ -46,6 +45,9 @@ export default class PaymentReceiveNotifyBySms { .withGraphFetched('customer') .withGraphFetched('entries.invoice'); + // Validates the payment existance. + this.validators.validatePaymentExistance(paymentReceive); + // Validate the customer phone number. this.saleSmsNotification.validateCustomerPhoneNumber( paymentReceive.customer.personalPhone diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts index f12ca5bcc..0cb45a973 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts @@ -1,7 +1,7 @@ import { IPaymentReceive, IPaymentReceiveEntry } from '@/interfaces'; import { Transformer } from '@/lib/Transformer/Transformer'; import { formatNumber } from 'utils'; -import { SaleInvoiceTransformer } from '../SaleInvoiceTransformer'; +import { SaleInvoiceTransformer } from '../Invoices/SaleInvoiceTransformer'; export class PaymentReceiveTransfromer extends Transformer { /** diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveValidators.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveValidators.ts new file mode 100644 index 000000000..35899e057 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveValidators.ts @@ -0,0 +1,295 @@ +import { Inject, Service } from 'typedi'; +import { difference, sumBy } from 'lodash'; +import { + IAccount, + IPaymentReceive, + IPaymentReceiveEditDTO, + IPaymentReceiveEntry, + IPaymentReceiveEntryDTO, + ISaleInvoice, +} from '@/interfaces'; +import { ServiceError } from '@/exceptions'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ERRORS } from './constants'; +import { ACCOUNT_TYPE } from '@/data/AccountTypes'; +import { PaymentReceive } from '@/models'; + +@Service() +export class PaymentReceiveValidators { + @Inject() + private tenancy: HasTenancyService; + + /** + * Validates the payment existance. + * @param {PaymentReceive | null | undefined} payment + */ + public validatePaymentExistance(payment: PaymentReceive | null | undefined) { + if (!payment) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); + } + } + + /** + * Validates the payment receive number existance. + * @param {number} tenantId - + * @param {string} paymentReceiveNo - + */ + public 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); + } + } + + /** + * Validates the invoices IDs existance. + * @param {number} tenantId - + * @param {number} customerId - + * @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries - + */ + public async validateInvoicesIDsExistance( + tenantId: number, + customerId: number, + paymentReceiveEntries: { invoiceId: number }[] + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const invoicesIds = paymentReceiveEntries.map( + (e: { invoiceId: number }) => e.invoiceId + ); + const storedInvoices = await SaleInvoice.query() + .whereIn('id', invoicesIds) + .where('customer_id', customerId); + + const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id); + const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds); + + if (notFoundInvoicesIDs.length > 0) { + throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND); + } + // Filters the not delivered invoices. + const notDeliveredInvoices = storedInvoices.filter( + (invoice) => !invoice.isDelivered + ); + if (notDeliveredInvoices.length > 0) { + throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, { + notDeliveredInvoices, + }); + } + return storedInvoices; + } + + /** + * Validates entries invoice payment amount. + * @param {Request} req - + * @param {Response} res - + * @param {Function} next - + */ + public async validateInvoicesPaymentsAmount( + tenantId: number, + paymentReceiveEntries: IPaymentReceiveEntryDTO[], + oldPaymentEntries: IPaymentReceiveEntry[] = [] + ) { + const { SaleInvoice } = this.tenancy.models(tenantId); + const invoicesIds = paymentReceiveEntries.map( + (e: IPaymentReceiveEntryDTO) => e.invoiceId + ); + + const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds); + + const storedInvoicesMap = new Map( + storedInvoices.map((invoice: ISaleInvoice) => { + const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId); + const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; + + return [ + invoice.id, + { ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount }, + ]; + }) + ); + const hasWrongPaymentAmount: any[] = []; + + paymentReceiveEntries.forEach( + (entry: IPaymentReceiveEntryDTO, index: number) => { + const entryInvoice = storedInvoicesMap.get(entry.invoiceId); + const { dueAmount } = entryInvoice; + + if (dueAmount < entry.paymentAmount) { + hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); + } + } + ); + if (hasWrongPaymentAmount.length > 0) { + throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT); + } + } + + /** + * Validate the payment receive number require. + * @param {IPaymentReceive} paymentReceiveObj + */ + public validatePaymentReceiveNoRequire(paymentReceiveObj: IPaymentReceive) { + if (!paymentReceiveObj.paymentReceiveNo) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED); + } + } + + /** + * Validate the payment receive entries IDs existance. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries + */ + public async validateEntriesIdsExistance( + tenantId: number, + paymentReceiveId: number, + paymentReceiveEntries: IPaymentReceiveEntryDTO[] + ) { + const { PaymentReceiveEntry } = this.tenancy.models(tenantId); + + const entriesIds = paymentReceiveEntries + .filter((entry) => entry.id) + .map((entry) => entry.id); + + const storedEntries = await PaymentReceiveEntry.query().where( + 'payment_receive_id', + paymentReceiveId + ); + const storedEntriesIds = storedEntries.map((entry: any) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS); + } + } + + /** + * Validates the payment receive number require. + * @param {string} paymentReceiveNo + */ + public validatePaymentNoRequire(paymentReceiveNo: string) { + if (!paymentReceiveNo) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED); + } + } + + /** + * Validate the payment customer whether modified. + * @param {IPaymentReceiveEditDTO} paymentReceiveDTO + * @param {IPaymentReceive} oldPaymentReceive + */ + public validateCustomerNotModified( + paymentReceiveDTO: IPaymentReceiveEditDTO, + oldPaymentReceive: IPaymentReceive + ) { + if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) { + throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE); + } + } + + /** + * Validates the payment account currency code. The deposit account curreny + * should be equals the customer currency code or the base currency. + * @param {string} paymentAccountCurrency + * @param {string} customerCurrency + * @param {string} baseCurrency + * @throws {ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID)} + */ + public validatePaymentAccountCurrency = ( + paymentAccountCurrency: string, + customerCurrency: string, + baseCurrency: string + ) => { + if ( + paymentAccountCurrency !== customerCurrency && + paymentAccountCurrency !== baseCurrency + ) { + throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID); + } + }; + + /** + * Validates the payment receive existance. + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment receive id. + */ + 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); + } + return paymentReceive; + } + + /** + * Validate the deposit account id existance. + * @param {number} tenantId - Tenant id. + * @param {number} depositAccountId - Deposit account id. + * @return {Promise} + */ + async getDepositAccountOrThrowError( + tenantId: number, + depositAccountId: number + ): Promise { + const { accountRepository } = this.tenancy.repositories(tenantId); + + const depositAccount = await accountRepository.findOneById( + depositAccountId + ); + if (!depositAccount) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); + } + // Detarmines whether the account is cash, bank or other current asset. + if ( + !depositAccount.isAccountType([ + ACCOUNT_TYPE.CASH, + ACCOUNT_TYPE.BANK, + ACCOUNT_TYPE.OTHER_CURRENT_ASSET, + ]) + ) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE); + } + return depositAccount; + } + + /** + * Validate the given customer has no payments receives. + * @param {number} tenantId + * @param {number} customerId - Customer id. + */ + public async validateCustomerHasNoPayments( + tenantId: number, + customerId: number + ) { + const { PaymentReceive } = this.tenancy.models(tenantId); + + const paymentReceives = await PaymentReceive.query().where( + 'customer_id', + customerId + ); + if (paymentReceives.length > 0) { + throw new ServiceError(ERRORS.CUSTOMER_HAS_PAYMENT_RECEIVES); + } + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts new file mode 100644 index 000000000..3b5057ed9 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts @@ -0,0 +1,193 @@ +import { + IFilterMeta, + IPaginationMeta, + IPaymentReceive, + IPaymentReceiveCreateDTO, + IPaymentReceiveEditDTO, + IPaymentReceiveSmsDetails, + IPaymentReceivesFilter, + ISystemUser, +} from '@/interfaces'; +import { Inject, Service } from 'typedi'; +import { CreatePaymentReceive } from './CreatePaymentReceive'; +import { EditPaymentReceive } from './EditPaymentReceive'; +import { DeletePaymentReceive } from './DeletePaymentReceive'; +import { GetPaymentReceives } from './GetPaymentReceives'; +import { GetPaymentReceive } from './GetPaymentReceive'; +import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices'; +import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify'; +import GetPaymentReceivePdf from './GetPaymentReeceivePdf'; +import { PaymentReceive } from '@/models'; + +@Service() +export class PaymentReceivesApplication { + @Inject() + private createPaymentReceiveService: CreatePaymentReceive; + + @Inject() + private editPaymentReceiveService: EditPaymentReceive; + + @Inject() + private deletePaymentReceiveService: DeletePaymentReceive; + + @Inject() + private getPaymentReceivesService: GetPaymentReceives; + + @Inject() + private getPaymentReceiveService: GetPaymentReceive; + + @Inject() + private getPaymentReceiveInvoicesService: GetPaymentReceiveInvoices; + + @Inject() + private paymentSmsNotify: PaymentReceiveNotifyBySms; + + @Inject() + private getPaymentReceivePdfService: GetPaymentReceivePdf; + + /** + * Creates a new payment receive. + * @param {number} tenantId + * @param {IPaymentReceiveCreateDTO} paymentReceiveDTO + * @param {ISystemUser} authorizedUser + * @returns + */ + public createPaymentReceive( + tenantId: number, + paymentReceiveDTO: IPaymentReceiveCreateDTO, + authorizedUser: ISystemUser + ) { + return this.createPaymentReceiveService.createPaymentReceive( + tenantId, + paymentReceiveDTO, + authorizedUser + ); + } + + /** + * Edit details the given payment receive with associated entries. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {IPaymentReceiveEditDTO} paymentReceiveDTO + * @param {ISystemUser} authorizedUser + * @returns + */ + public editPaymentReceive( + tenantId: number, + paymentReceiveId: number, + paymentReceiveDTO: IPaymentReceiveEditDTO, + authorizedUser: ISystemUser + ) { + return this.editPaymentReceiveService.editPaymentReceive( + tenantId, + paymentReceiveId, + paymentReceiveDTO, + authorizedUser + ); + } + + /** + * deletes the given payment receive. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {ISystemUser} authorizedUser + * @returns + */ + public deletePaymentReceive( + tenantId: number, + paymentReceiveId: number, + authorizedUser: ISystemUser + ) { + return this.deletePaymentReceiveService.deletePaymentReceive( + tenantId, + paymentReceiveId, + authorizedUser + ); + } + + /** + * Retrieve payment receives paginated and filterable. + * @param {number} tenantId + * @param {IPaymentReceivesFilter} filterDTO + * @returns + */ + public async getPaymentReceives( + tenantId: number, + filterDTO: IPaymentReceivesFilter + ): Promise<{ + paymentReceives: IPaymentReceive[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + return this.getPaymentReceivesService.getPaymentReceives( + tenantId, + filterDTO + ); + } + + /** + * + * @param {number} tenantId + * @param {number} paymentReceiveId + * @returns {Promise} + */ + public async getPaymentReceive( + tenantId: number, + paymentReceiveId: number + ): Promise { + return this.getPaymentReceiveService.getPaymentReceive( + tenantId, + paymentReceiveId + ); + } + + /** + * Retrieves associated sale invoices of the given payment receive. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @returns + */ + public getPaymentReceiveInvoices(tenantId: number, paymentReceiveId: number) { + return this.getPaymentReceiveInvoicesService.getPaymentReceiveInvoices( + tenantId, + paymentReceiveId + ); + } + + /** + * Notify customer via sms about payment receive details. + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveid - Payment receive id. + */ + public notifyPaymentBySms(tenantId: number, paymentReceiveid: number) { + return this.paymentSmsNotify.notifyBySms(tenantId, paymentReceiveid); + } + + /** + * Retrieve the SMS details of the given invoice. + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveid - Payment receive id. + */ + public getPaymentSmsDetails = async ( + tenantId: number, + paymentReceiveId: number + ): Promise => { + return this.paymentSmsNotify.smsDetails(tenantId, paymentReceiveId); + }; + + /** + * Retrieve PDF content of the given payment receive. + * @param {number} tenantId + * @param {PaymentReceive} paymentReceive + * @returns + */ + public getPaymentReceivePdf = ( + tenantId: number, + paymentReceive: PaymentReceive + ) => { + return this.getPaymentReceivePdfService.getPaymentReceivePdf( + tenantId, + paymentReceive + ); + }; +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts deleted file mode 100644 index ff6eb7f23..000000000 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts +++ /dev/null @@ -1,847 +0,0 @@ -import { omit, sumBy, difference } from 'lodash'; -import { Service, Inject } from 'typedi'; -import * as R from 'ramda'; -import { Knex } from 'knex'; -import events from '@/subscribers/events'; -import { - IAccount, - IFilterMeta, - IPaginationMeta, - IPaymentReceive, - IPaymentReceiveCreateDTO, - IPaymentReceiveEditDTO, - IPaymentReceiveEntry, - IPaymentReceiveEntryDTO, - IPaymentReceivesFilter, - IPaymentsReceiveService, - IPaymentReceiveCreatedPayload, - ISaleInvoice, - ISystemUser, - IPaymentReceiveEditedPayload, - IPaymentReceiveDeletedPayload, - IPaymentReceiveCreatingPayload, - IPaymentReceiveDeletingPayload, - IPaymentReceiveEditingPayload, - ICustomer, -} from '@/interfaces'; -import JournalPosterService from '@/services/Sales/JournalPosterService'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import { formatDateFields, entriesAmountDiff } from 'utils'; -import { ServiceError } from '@/exceptions'; -import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; -import { ACCOUNT_TYPE } from '@/data/AccountTypes'; -import AutoIncrementOrdersService from '../AutoIncrementOrdersService'; -import { ERRORS } from './constants'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import { PaymentReceiveTransfromer } from './PaymentReceiveTransformer'; -import UnitOfWork from '@/services/UnitOfWork'; -import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; -import { TenantMetadata } from '@/system/models'; -import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -/** - * Payment receive service. - * @service - */ -@Service('PaymentReceives') -export default class PaymentReceiveService implements IPaymentsReceiveService { - @Inject() - itemsEntries: ItemsEntriesService; - - @Inject() - tenancy: TenancyService; - - @Inject() - journalService: JournalPosterService; - - @Inject() - dynamicListService: DynamicListingService; - - @Inject() - autoIncrementOrdersService: AutoIncrementOrdersService; - - @Inject('logger') - logger: any; - - @Inject() - uow: UnitOfWork; - - @Inject() - eventPublisher: EventPublisher; - - @Inject() - branchDTOTransform: BranchTransactionDTOTransform; - - @Inject() - transformer: TransformerInjectable; - - /** - * Validates the payment receive number existance. - * @param {number} tenantId - - * @param {string} paymentReceiveNo - - */ - 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); - } - } - - /** - * Validates the payment receive existance. - * @param {number} tenantId - Tenant id. - * @param {number} paymentReceiveId - Payment receive id. - */ - 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); - } - return paymentReceive; - } - - /** - * Validate the deposit account id existance. - * @param {number} tenantId - Tenant id. - * @param {number} depositAccountId - Deposit account id. - * @return {Promise} - */ - async getDepositAccountOrThrowError( - tenantId: number, - depositAccountId: number - ): Promise { - const { accountRepository } = this.tenancy.repositories(tenantId); - - const depositAccount = await accountRepository.findOneById( - depositAccountId - ); - if (!depositAccount) { - throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); - } - // Detarmines whether the account is cash, bank or other current asset. - if ( - !depositAccount.isAccountType([ - ACCOUNT_TYPE.CASH, - ACCOUNT_TYPE.BANK, - ACCOUNT_TYPE.OTHER_CURRENT_ASSET, - ]) - ) { - throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE); - } - return depositAccount; - } - - /** - * Validates the invoices IDs existance. - * @param {number} tenantId - - * @param {number} customerId - - * @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries - - */ - async validateInvoicesIDsExistance( - tenantId: number, - customerId: number, - paymentReceiveEntries: { invoiceId: number }[] - ): Promise { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const invoicesIds = paymentReceiveEntries.map( - (e: { invoiceId: number }) => e.invoiceId - ); - const storedInvoices = await SaleInvoice.query() - .whereIn('id', invoicesIds) - .where('customer_id', customerId); - - const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id); - const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds); - - if (notFoundInvoicesIDs.length > 0) { - throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND); - } - // Filters the not delivered invoices. - const notDeliveredInvoices = storedInvoices.filter( - (invoice) => !invoice.isDelivered - ); - if (notDeliveredInvoices.length > 0) { - throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, { - notDeliveredInvoices, - }); - } - return storedInvoices; - } - - /** - * Validates entries invoice payment amount. - * @param {Request} req - - * @param {Response} res - - * @param {Function} next - - */ - async validateInvoicesPaymentsAmount( - tenantId: number, - paymentReceiveEntries: IPaymentReceiveEntryDTO[], - oldPaymentEntries: IPaymentReceiveEntry[] = [] - ) { - const { SaleInvoice } = this.tenancy.models(tenantId); - const invoicesIds = paymentReceiveEntries.map( - (e: IPaymentReceiveEntryDTO) => e.invoiceId - ); - - const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds); - - const storedInvoicesMap = new Map( - storedInvoices.map((invoice: ISaleInvoice) => { - const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId); - const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; - - return [ - invoice.id, - { ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount }, - ]; - }) - ); - const hasWrongPaymentAmount: any[] = []; - - paymentReceiveEntries.forEach( - (entry: IPaymentReceiveEntryDTO, index: number) => { - const entryInvoice = storedInvoicesMap.get(entry.invoiceId); - const { dueAmount } = entryInvoice; - - if (dueAmount < entry.paymentAmount) { - hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); - } - } - ); - if (hasWrongPaymentAmount.length > 0) { - throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT); - } - } - - /** - * Retrieve the next unique payment receive number. - * @param {number} tenantId - Tenant id. - * @return {string} - */ - getNextPaymentReceiveNumber(tenantId: number): string { - return this.autoIncrementOrdersService.getNextTransactionNumber( - tenantId, - 'payment_receives' - ); - } - - /** - * Increment the payment receive next number. - * @param {number} tenantId - */ - incrementNextPaymentReceiveNumber(tenantId: number) { - return this.autoIncrementOrdersService.incrementSettingsNextNumber( - tenantId, - 'payment_receives' - ); - } - - /** - * Validate the payment receive number require. - * @param {IPaymentReceive} paymentReceiveObj - */ - validatePaymentReceiveNoRequire(paymentReceiveObj: IPaymentReceive) { - if (!paymentReceiveObj.paymentReceiveNo) { - throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED); - } - } - - /** - * Validate the payment receive entries IDs existance. - * @param {number} tenantId - * @param {number} paymentReceiveId - * @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries - */ - private async validateEntriesIdsExistance( - tenantId: number, - paymentReceiveId: number, - paymentReceiveEntries: IPaymentReceiveEntryDTO[] - ) { - const { PaymentReceiveEntry } = this.tenancy.models(tenantId); - - const entriesIds = paymentReceiveEntries - .filter((entry) => entry.id) - .map((entry) => entry.id); - - const storedEntries = await PaymentReceiveEntry.query().where( - 'payment_receive_id', - paymentReceiveId - ); - const storedEntriesIds = storedEntries.map((entry: any) => entry.id); - const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); - - if (notFoundEntriesIds.length > 0) { - throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS); - } - } - - /** - * Validates the payment receive number require. - * @param {string} paymentReceiveNo - */ - validatePaymentNoRequire(paymentReceiveNo: string) { - if (!paymentReceiveNo) { - throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED); - } - } - - /** - * Validate the payment customer whether modified. - * @param {IPaymentReceiveEditDTO} paymentReceiveDTO - * @param {IPaymentReceive} oldPaymentReceive - */ - validateCustomerNotModified( - paymentReceiveDTO: IPaymentReceiveEditDTO, - oldPaymentReceive: IPaymentReceive - ) { - if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) { - throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE); - } - } - - /** - * Validates the payment account currency code. The deposit account curreny - * should be equals the customer currency code or the base currency. - * @param {string} paymentAccountCurrency - * @param {string} customerCurrency - * @param {string} baseCurrency - * @throws {ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID)} - */ - public validatePaymentAccountCurrency = ( - paymentAccountCurrency: string, - customerCurrency: string, - baseCurrency: string - ) => { - if ( - paymentAccountCurrency !== customerCurrency && - paymentAccountCurrency !== baseCurrency - ) { - throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID); - } - }; - - /** - * Transformes the create payment receive DTO to model object. - * @param {number} tenantId - * @param {IPaymentReceiveCreateDTO|IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO. - * @param {IPaymentReceive} oldPaymentReceive - - * @return {IPaymentReceive} - */ - async transformPaymentReceiveDTOToModel( - tenantId: number, - customer: ICustomer, - paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, - oldPaymentReceive?: IPaymentReceive - ): Promise { - const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); - - // Retreive the next invoice number. - const autoNextNumber = this.getNextPaymentReceiveNumber(tenantId); - - // Retrieve the next payment receive number. - const paymentReceiveNo = - paymentReceiveDTO.paymentReceiveNo || - oldPaymentReceive?.paymentReceiveNo || - autoNextNumber; - - this.validatePaymentNoRequire(paymentReceiveNo); - - const initialDTO = { - ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [ - 'paymentDate', - ]), - amount: paymentAmount, - currencyCode: customer.currencyCode, - ...(paymentReceiveNo ? { paymentReceiveNo } : {}), - exchangeRate: paymentReceiveDTO.exchangeRate || 1, - entries: paymentReceiveDTO.entries.map((entry) => ({ - ...entry, - })), - }; - return R.compose( - this.branchDTOTransform.transformDTO(tenantId) - )(initialDTO); - } - - /** - * Transform the create payment receive DTO. - * @param {number} tenantId - * @param {ICustomer} customer - * @param {IPaymentReceiveCreateDTO} paymentReceiveDTO - * @returns - */ - private transformCreateDTOToModel = async ( - tenantId: number, - customer: ICustomer, - paymentReceiveDTO: IPaymentReceiveCreateDTO - ) => { - return this.transformPaymentReceiveDTOToModel( - tenantId, - customer, - paymentReceiveDTO - ); - }; - - /** - * Transform the edit payment receive DTO. - * @param {number} tenantId - * @param {ICustomer} customer - * @param {IPaymentReceiveEditDTO} paymentReceiveDTO - * @param {IPaymentReceive} oldPaymentReceive - * @returns - */ - private transformEditDTOToModel = async ( - tenantId: number, - customer: ICustomer, - paymentReceiveDTO: IPaymentReceiveEditDTO, - oldPaymentReceive: IPaymentReceive - ) => { - return this.transformPaymentReceiveDTOToModel( - tenantId, - customer, - paymentReceiveDTO, - oldPaymentReceive - ); - }; - /** - * Creates a new payment receive and store it to the storage - * with associated invoices payment and journal transactions. - * @async - * @param {number} tenantId - Tenant id. - * @param {IPaymentReceive} paymentReceive - */ - public async createPaymentReceive( - tenantId: number, - paymentReceiveDTO: IPaymentReceiveCreateDTO, - authorizedUser: ISystemUser - ) { - const { PaymentReceive, Contact } = this.tenancy.models(tenantId); - - const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); - - // Validate customer existance. - const paymentCustomer = await Contact.query() - .modify('customer') - .findById(paymentReceiveDTO.customerId) - .throwIfNotFound(); - - // Transformes the payment receive DTO to model. - const paymentReceiveObj = await this.transformCreateDTOToModel( - tenantId, - paymentCustomer, - paymentReceiveDTO - ); - // Validate payment receive number uniquiness. - await this.validatePaymentReceiveNoExistance( - tenantId, - paymentReceiveObj.paymentReceiveNo - ); - // Validate the deposit account existance and type. - const depositAccount = await this.getDepositAccountOrThrowError( - tenantId, - paymentReceiveDTO.depositAccountId - ); - // Validate payment receive invoices IDs existance. - await this.validateInvoicesIDsExistance( - tenantId, - paymentReceiveDTO.customerId, - paymentReceiveDTO.entries - ); - // Validate invoice payment amount. - await this.validateInvoicesPaymentsAmount( - tenantId, - paymentReceiveDTO.entries - ); - // Validates the payment account currency code. - this.validatePaymentAccountCurrency( - depositAccount.currencyCode, - paymentCustomer.currencyCode, - tenantMeta.baseCurrency - ); - // Creates a payment receive transaction under UOW envirment. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onPaymentReceiveCreating` event. - await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, { - trx, - paymentReceiveDTO, - tenantId, - } as IPaymentReceiveCreatingPayload); - - // Inserts the payment receive transaction. - const paymentReceive = await PaymentReceive.query( - trx - ).insertGraphAndFetch({ - ...paymentReceiveObj, - }); - // Triggers `onPaymentReceiveCreated` event. - await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, { - tenantId, - paymentReceive, - paymentReceiveId: paymentReceive.id, - authorizedUser, - trx, - } as IPaymentReceiveCreatedPayload); - - return paymentReceive; - }); - } - - /** - * Edit details the given payment receive with associated entries. - * ------ - * - Update the payment receive transactions. - * - Insert the new payment receive entries. - * - Update the given payment receive entries. - * - Delete the not presented payment receive entries. - * - Re-insert the journal transactions and update the different accounts balance. - * - Update the different customer balances. - * - Update the different invoice payment amount. - * @async - * @param {number} tenantId - - * @param {Integer} paymentReceiveId - - * @param {IPaymentReceive} paymentReceive - - */ - public async editPaymentReceive( - tenantId: number, - paymentReceiveId: number, - paymentReceiveDTO: IPaymentReceiveEditDTO, - authorizedUser: ISystemUser - ) { - const { PaymentReceive, Contact } = this.tenancy.models(tenantId); - - const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); - - // Validate the payment receive existance. - const oldPaymentReceive = await this.getPaymentReceiveOrThrowError( - tenantId, - paymentReceiveId - ); - // Validate customer existance. - const customer = await Contact.query() - .modify('customer') - .findById(paymentReceiveDTO.customerId) - .throwIfNotFound(); - - // Transformes the payment receive DTO to model. - const paymentReceiveObj = await this.transformEditDTOToModel( - tenantId, - customer, - paymentReceiveDTO, - oldPaymentReceive - ); - // Validate customer whether modified. - this.validateCustomerNotModified(paymentReceiveDTO, oldPaymentReceive); - - // Validate payment receive number uniquiness. - if (paymentReceiveDTO.paymentReceiveNo) { - await this.validatePaymentReceiveNoExistance( - tenantId, - paymentReceiveDTO.paymentReceiveNo, - paymentReceiveId - ); - } - // Validate the deposit account existance and type. - const depositAccount = await this.getDepositAccountOrThrowError( - tenantId, - paymentReceiveDTO.depositAccountId - ); - // Validate the entries ids existance on payment receive type. - await this.validateEntriesIdsExistance( - tenantId, - paymentReceiveId, - paymentReceiveDTO.entries - ); - // Validate payment receive invoices IDs existance and associated - // to the given customer id. - await this.validateInvoicesIDsExistance( - tenantId, - oldPaymentReceive.customerId, - paymentReceiveDTO.entries - ); - // Validate invoice payment amount. - await this.validateInvoicesPaymentsAmount( - tenantId, - paymentReceiveDTO.entries, - oldPaymentReceive.entries - ); - // Validates the payment account currency code. - this.validatePaymentAccountCurrency( - depositAccount.currencyCode, - customer.currencyCode, - tenantMeta.baseCurrency - ); - // Creates payment receive transaction under UOW envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onPaymentReceiveEditing` event. - await this.eventPublisher.emitAsync(events.paymentReceive.onEditing, { - trx, - tenantId, - oldPaymentReceive, - paymentReceiveDTO, - } as IPaymentReceiveEditingPayload); - - // Update the payment receive transaction. - const paymentReceive = await PaymentReceive.query( - trx - ).upsertGraphAndFetch({ - id: paymentReceiveId, - ...paymentReceiveObj, - }); - // Triggers `onPaymentReceiveEdited` event. - await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, { - tenantId, - paymentReceiveId, - paymentReceive, - oldPaymentReceive, - authorizedUser, - trx, - } as IPaymentReceiveEditedPayload); - - return paymentReceive; - }); - } - - /** - * Deletes the given payment receive with associated entries - * and journal transactions. - * ----- - * - Deletes the payment receive transaction. - * - Deletes the payment receive associated entries. - * - Deletes the payment receive associated journal transactions. - * - Revert the customer balance. - * - Revert the payment amount of the associated invoices. - * @async - * @param {number} tenantId - Tenant id. - * @param {Integer} paymentReceiveId - Payment receive id. - * @param {IPaymentReceive} paymentReceive - Payment receive object. - */ - public async deletePaymentReceive( - tenantId: number, - paymentReceiveId: number, - authorizedUser: ISystemUser - ) { - const { PaymentReceive, PaymentReceiveEntry } = - this.tenancy.models(tenantId); - - // Retreive payment receive or throw not found service error. - const oldPaymentReceive = await this.getPaymentReceiveOrThrowError( - tenantId, - paymentReceiveId - ); - // Delete payment receive transaction and associate transactions under UOW env. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onPaymentReceiveDeleting` event. - await this.eventPublisher.emitAsync(events.paymentReceive.onDeleting, { - tenantId, - oldPaymentReceive, - trx, - } as IPaymentReceiveDeletingPayload); - - // Deletes the payment receive associated entries. - await PaymentReceiveEntry.query(trx) - .where('payment_receive_id', paymentReceiveId) - .delete(); - - // Deletes the payment receive transaction. - await PaymentReceive.query(trx).findById(paymentReceiveId).delete(); - - // Triggers `onPaymentReceiveDeleted` event. - await this.eventPublisher.emitAsync(events.paymentReceive.onDeleted, { - tenantId, - paymentReceiveId, - oldPaymentReceive, - authorizedUser, - trx, - } as IPaymentReceiveDeletedPayload); - }); - } - - /** - * Retrieve payment receive details. - * @param {number} tenantId - Tenant id. - * @param {number} paymentReceiveId - Payment receive id. - * @return {Promise} - */ - public async getPaymentReceive( - tenantId: number, - paymentReceiveId: number - ): Promise { - const { PaymentReceive } = this.tenancy.models(tenantId); - - const paymentReceive = await PaymentReceive.query() - .withGraphFetched('customer') - .withGraphFetched('depositAccount') - .withGraphFetched('entries.invoice') - .withGraphFetched('transactions') - .withGraphFetched('branch') - .findById(paymentReceiveId); - - if (!paymentReceive) { - throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); - } - return this.transformer.transform( - tenantId, - paymentReceive, - new PaymentReceiveTransfromer() - ); - } - - /** - * Retrieve sale invoices that assocaited to the given payment receive. - * @param {number} tenantId - Tenant id. - * @param {number} paymentReceiveId - Payment receive id. - * @return {Promise} - */ - public async getPaymentReceiveInvoices( - tenantId: number, - paymentReceiveId: number - ) { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const paymentReceive = await this.getPaymentReceiveOrThrowError( - tenantId, - paymentReceiveId - ); - const paymentReceiveInvoicesIds = paymentReceive.entries.map( - (entry) => entry.invoiceId - ); - const saleInvoices = await SaleInvoice.query().whereIn( - 'id', - paymentReceiveInvoicesIds - ); - - return saleInvoices; - } - - /** - * Parses payments receive list filter DTO. - * @param filterDTO - */ - private parseListFilterDTO(filterDTO) { - return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); - } - - /** - * Retrieve payment receives paginated and filterable list. - * @param {number} tenantId - * @param {IPaymentReceivesFilter} paymentReceivesFilter - */ - public async listPaymentReceives( - tenantId: number, - filterDTO: IPaymentReceivesFilter - ): Promise<{ - paymentReceives: IPaymentReceive[]; - pagination: IPaginationMeta; - filterMeta: IFilterMeta; - }> { - const { PaymentReceive } = this.tenancy.models(tenantId); - - // Parses filter DTO. - const filter = this.parseListFilterDTO(filterDTO); - - // Dynamic list service. - const dynamicList = await this.dynamicListService.dynamicList( - tenantId, - PaymentReceive, - filter - ); - const { results, pagination } = await PaymentReceive.query() - .onBuild((builder) => { - builder.withGraphFetched('customer'); - builder.withGraphFetched('depositAccount'); - dynamicList.buildQuery()(builder); - }) - .pagination(filter.page - 1, filter.pageSize); - - // Transformer the payment receives models to POJO. - const transformedPayments = await this.transformer.transform( - tenantId, - results, - new PaymentReceiveTransfromer() - ); - return { - paymentReceives: transformedPayments, - pagination, - filterMeta: dynamicList.getResponseMeta(), - }; - } - - /** - * Saves difference changing between old and new invoice payment amount. - * @async - * @param {number} tenantId - Tenant id. - * @param {Array} paymentReceiveEntries - * @param {Array} newPaymentReceiveEntries - * @return {Promise} - */ - public async saveChangeInvoicePaymentAmount( - tenantId: number, - newPaymentReceiveEntries: IPaymentReceiveEntryDTO[], - oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[], - trx?: Knex.Transaction - ): Promise { - const { SaleInvoice } = this.tenancy.models(tenantId); - const opers: Promise[] = []; - - const diffEntries = entriesAmountDiff( - newPaymentReceiveEntries, - oldPaymentReceiveEntries, - 'paymentAmount', - 'invoiceId' - ); - diffEntries.forEach((diffEntry: any) => { - if (diffEntry.paymentAmount === 0) { - return; - } - const oper = SaleInvoice.changePaymentAmount( - diffEntry.invoiceId, - diffEntry.paymentAmount, - trx - ); - opers.push(oper); - }); - await Promise.all([...opers]); - } - - /** - * Validate the given customer has no payments receives. - * @param {number} tenantId - * @param {number} customerId - Customer id. - */ - public async validateCustomerHasNoPayments( - tenantId: number, - customerId: number - ) { - const { PaymentReceive } = this.tenancy.models(tenantId); - - const paymentReceives = await PaymentReceive.query().where( - 'customer_id', - customerId - ); - if (paymentReceives.length > 0) { - throw new ServiceError(ERRORS.CUSTOMER_HAS_PAYMENT_RECEIVES); - } - } -} diff --git a/packages/server/src/services/Sales/PaymentReceives/constants.ts b/packages/server/src/services/Sales/PaymentReceives/constants.ts index baa4b2f48..ccd8d75ee 100644 --- a/packages/server/src/services/Sales/PaymentReceives/constants.ts +++ b/packages/server/src/services/Sales/PaymentReceives/constants.ts @@ -11,8 +11,7 @@ export const ERRORS = { PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED', PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES', - PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID' + PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', }; - -export const DEFAULT_VIEWS = []; \ No newline at end of file +export const DEFAULT_VIEWS = []; diff --git a/packages/server/src/services/Sales/Receipts/CloseSaleReceipt.ts b/packages/server/src/services/Sales/Receipts/CloseSaleReceipt.ts new file mode 100644 index 000000000..0566d4733 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/CloseSaleReceipt.ts @@ -0,0 +1,76 @@ +import { Service, Inject } from 'typedi'; +import moment from 'moment'; +import { Knex } from 'knex'; +import events from '@/subscribers/events'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './constants'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { + ISaleReceiptEventClosedPayload, + ISaleReceiptEventClosingPayload, +} from '@/interfaces'; +import { SaleReceiptValidators } from './SaleReceiptValidators'; + +@Service() +export class CloseSaleReceipt { + @Inject() + private tenancy: TenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private validators: SaleReceiptValidators; + + /** + * Mark the given sale receipt as closed. + * @param {number} tenantId + * @param {number} saleReceiptId + * @return {Promise} + */ + public async closeSaleReceipt( + tenantId: number, + saleReceiptId: number + ): Promise { + const { SaleReceipt } = this.tenancy.models(tenantId); + + // Retrieve sale receipt or throw not found service error. + const oldSaleReceipt = await SaleReceipt.query() + .findById(saleReceiptId) + .withGraphFetched('entries') + .throwIfNotFound(); + + // Throw service error if the sale receipt already closed. + this.validators.validateReceiptNotClosed(oldSaleReceipt); + + // Updates the sale recept transaction under unit-of-work envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptClosing` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onClosing, { + tenantId, + oldSaleReceipt, + trx, + } as ISaleReceiptEventClosingPayload); + + // Mark the sale receipt as closed on the storage. + const saleReceipt = await SaleReceipt.query(trx) + .findById(saleReceiptId) + .patch({ + closedAt: moment().toMySqlDateTime(), + }); + + // Triggers `onSaleReceiptClosed` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onClosed, { + saleReceiptId, + saleReceipt, + tenantId, + trx, + } as ISaleReceiptEventClosedPayload); + }); + } +} diff --git a/packages/server/src/services/Sales/Receipts/CreateSaleReceipt.ts b/packages/server/src/services/Sales/Receipts/CreateSaleReceipt.ts new file mode 100644 index 000000000..0fcafff8a --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/CreateSaleReceipt.ts @@ -0,0 +1,106 @@ +import { Service, Inject } from 'typedi'; +import { Knex } from 'knex'; +import { + ISaleReceipt, + ISaleReceiptCreatedPayload, + ISaleReceiptCreatingPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer'; +import { SaleReceiptValidators } from './SaleReceiptValidators'; + +@Service() +export class CreateSaleReceipt { + @Inject() + private tenancy: TenancyService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private transformer: SaleReceiptDTOTransformer; + + @Inject() + private validators: SaleReceiptValidators; + + /** + * Creates a new sale receipt with associated entries. + * @async + * @param {ISaleReceipt} saleReceipt + * @return {Object} + */ + public async createSaleReceipt( + tenantId: number, + saleReceiptDTO: any + ): Promise { + const { SaleReceipt, Contact } = this.tenancy.models(tenantId); + + // Retireves the payment customer model. + const paymentCustomer = await Contact.query() + .modify('customer') + .findById(saleReceiptDTO.customerId) + .throwIfNotFound(); + + // Transform sale receipt DTO to model. + const saleReceiptObj = await this.transformer.transformDTOToModel( + tenantId, + saleReceiptDTO, + paymentCustomer + ); + // Validate receipt deposit account existance and type. + await this.validators.validateReceiptDepositAccountExistance( + tenantId, + saleReceiptDTO.depositAccountId + ); + // Validate items IDs existance on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + tenantId, + saleReceiptDTO.entries + ); + // Validate the sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + tenantId, + saleReceiptDTO.entries + ); + // Validate sale receipt number uniuqiness. + if (saleReceiptDTO.receiptNumber) { + await this.validators.validateReceiptNumberUnique( + tenantId, + saleReceiptDTO.receiptNumber + ); + } + // Creates a sale receipt transaction and associated transactions under UOW env. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptCreating` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onCreating, { + saleReceiptDTO, + tenantId, + trx, + } as ISaleReceiptCreatingPayload); + + // Inserts the sale receipt graph to the storage. + const saleReceipt = await SaleReceipt.query().upsertGraph({ + ...saleReceiptObj, + }); + // Triggers `onSaleReceiptCreated` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onCreated, { + tenantId, + saleReceipt, + saleReceiptId: saleReceipt.id, + trx, + } as ISaleReceiptCreatedPayload); + + return saleReceipt; + }); + } +} diff --git a/packages/server/src/services/Sales/Receipts/DeleteSaleReceipt.ts b/packages/server/src/services/Sales/Receipts/DeleteSaleReceipt.ts new file mode 100644 index 000000000..b972c8279 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/DeleteSaleReceipt.ts @@ -0,0 +1,67 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { + ISaleReceiptDeletingPayload, + ISaleReceiptEventDeletedPayload, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import events from '@/subscribers/events'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { SaleReceiptValidators } from './SaleReceiptValidators'; + +@Service() +export class DeleteSaleReceipt { + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private validators: SaleReceiptValidators; + + /** + * Deletes the sale receipt with associated entries. + * @param {Integer} saleReceiptId + * @return {void} + */ + public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { + const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); + + const oldSaleReceipt = await SaleReceipt.query() + .findById(saleReceiptId) + .withGraphFetched('entries'); + + // Validates the sale receipt existance. + this.validators.validateReceiptExistance(oldSaleReceipt); + + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptsDeleting` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onDeleting, { + trx, + oldSaleReceipt, + tenantId, + } as ISaleReceiptDeletingPayload); + + await ItemEntry.query(trx) + .where('reference_id', saleReceiptId) + .where('reference_type', 'SaleReceipt') + .delete(); + + // Delete the sale receipt transaction. + await SaleReceipt.query(trx).where('id', saleReceiptId).delete(); + + // Triggers `onSaleReceiptsDeleted` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onDeleted, { + tenantId, + saleReceiptId, + oldSaleReceipt, + trx, + } as ISaleReceiptEventDeletedPayload); + }); + } +} diff --git a/packages/server/src/services/Sales/Receipts/EditSaleReceipt.ts b/packages/server/src/services/Sales/Receipts/EditSaleReceipt.ts new file mode 100644 index 000000000..fbdb397e4 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/EditSaleReceipt.ts @@ -0,0 +1,119 @@ +import { Service, Inject } from 'typedi'; +import { Knex } from 'knex'; +import events from '@/subscribers/events'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { + ISaleReceiptEditedPayload, + ISaleReceiptEditingPayload, +} from '@/interfaces'; +import { SaleReceiptValidators } from './SaleReceiptValidators'; +import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer'; + +@Service() +export class EditSaleReceipt { + @Inject() + private tenancy: TenancyService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private validators: SaleReceiptValidators; + + @Inject() + private DTOTransformer: SaleReceiptDTOTransformer; + + /** + * Edit details sale receipt with associated entries. + * @param {Integer} saleReceiptId + * @param {ISaleReceipt} saleReceipt + * @return {void} + */ + public async editSaleReceipt( + tenantId: number, + saleReceiptId: number, + saleReceiptDTO: any + ) { + const { SaleReceipt, Contact } = this.tenancy.models(tenantId); + + // Retrieve sale receipt or throw not found service error. + const oldSaleReceipt = await SaleReceipt.query() + .findById(saleReceiptId) + .withGraphFetched('entries'); + + // Validates the sale receipt existance. + this.validators.validateReceiptExistance(oldSaleReceipt); + + // Retrieves the payment customer model. + const paymentCustomer = await Contact.query() + .findById(saleReceiptDTO.customerId) + .modify('customer') + .throwIfNotFound(); + + // Transform sale receipt DTO to model. + const saleReceiptObj = await this.DTOTransformer.transformDTOToModel( + tenantId, + saleReceiptDTO, + paymentCustomer, + oldSaleReceipt + ); + // Validate receipt deposit account existance and type. + await this.validators.validateReceiptDepositAccountExistance( + tenantId, + saleReceiptDTO.depositAccountId + ); + // Validate items IDs existance on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + tenantId, + saleReceiptDTO.entries + ); + // Validate the sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + tenantId, + saleReceiptDTO.entries + ); + // Validate sale receipt number uniuqiness. + if (saleReceiptDTO.receiptNumber) { + await this.validators.validateReceiptNumberUnique( + tenantId, + saleReceiptDTO.receiptNumber, + saleReceiptId + ); + } + // Edits the sale receipt tranasctions with associated transactions under UOW env. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptsEditing` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onEditing, { + tenantId, + oldSaleReceipt, + saleReceiptDTO, + trx, + } as ISaleReceiptEditingPayload); + + // Upsert the receipt graph to the storage. + const saleReceipt = await SaleReceipt.query(trx).upsertGraphAndFetch({ + id: saleReceiptId, + ...saleReceiptObj, + }); + // Triggers `onSaleReceiptEdited` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, { + tenantId, + oldSaleReceipt, + saleReceipt, + saleReceiptId, + trx, + } as ISaleReceiptEditedPayload); + + return saleReceipt; + }); + } +} diff --git a/packages/server/src/services/Sales/Receipts/GetSaleReceipt.ts b/packages/server/src/services/Sales/Receipts/GetSaleReceipt.ts new file mode 100644 index 000000000..b2da2660c --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/GetSaleReceipt.ts @@ -0,0 +1,42 @@ +import { Inject, Service } from 'typedi'; +import { SaleReceiptTransformer } from './SaleReceiptTransformer'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import { SaleReceiptValidators } from './SaleReceiptValidators'; + +@Service() +export class GetSaleReceipt { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + @Inject() + private validators: SaleReceiptValidators; + + /** + * Retrieve sale receipt with associated entries. + * @param {Integer} saleReceiptId + * @return {ISaleReceipt} + */ + public async getSaleReceipt(tenantId: number, saleReceiptId: number) { + const { SaleReceipt } = this.tenancy.models(tenantId); + + const saleReceipt = await SaleReceipt.query() + .findById(saleReceiptId) + .withGraphFetched('entries.item') + .withGraphFetched('customer') + .withGraphFetched('depositAccount') + .withGraphFetched('branch'); + + // Valdiates the sale receipt existance. + this.validators.validateReceiptExistance(saleReceipt); + + return this.transformer.transform( + tenantId, + saleReceipt, + new SaleReceiptTransformer() + ); + } +} diff --git a/packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts b/packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts new file mode 100644 index 000000000..24150e349 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts @@ -0,0 +1,80 @@ +import * as R from 'ramda'; +import { + IFilterMeta, + IPaginationMeta, + ISaleReceipt, + ISalesReceiptsFilter, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { SaleReceiptTransformer } from './SaleReceiptTransformer'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import DynamicListingService from '@/services/DynamicListing/DynamicListService'; + +@Service() +export class GetSaleReceipts { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + @Inject() + private dynamicListService: DynamicListingService; + + /** + * Retrieve sales receipts paginated and filterable list. + * @param {number} tenantId + * @param {ISaleReceiptFilter} salesReceiptsFilter + */ + public async getSaleReceipts( + tenantId: number, + filterDTO: ISalesReceiptsFilter + ): Promise<{ + data: ISaleReceipt[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + const { SaleReceipt } = this.tenancy.models(tenantId); + + // Parses the stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + tenantId, + SaleReceipt, + filter + ); + const { results, pagination } = await SaleReceipt.query() + .onBuild((builder) => { + builder.withGraphFetched('depositAccount'); + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries'); + + dynamicFilter.buildQuery()(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Transformes the estimates models to POJO. + const salesEstimates = await this.transformer.transform( + tenantId, + results, + new SaleReceiptTransformer() + ); + return { + data: salesEstimates, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + + /** + * Parses the sale invoice list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts new file mode 100644 index 000000000..8dfdd4c75 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -0,0 +1,169 @@ +import { Inject, Service } from 'typedi'; +import { CreateSaleReceipt } from './CreateSaleReceipt'; +import { + IFilterMeta, + IPaginationMeta, + ISaleReceipt, + ISalesReceiptsFilter, +} from '@/interfaces'; +import { EditSaleReceipt } from './EditSaleReceipt'; +import { GetSaleReceipt } from './GetSaleReceipt'; +import { DeleteSaleReceipt } from './DeleteSaleReceipt'; +import { GetSaleReceipts } from './GetSaleReceipts'; +import { CloseSaleReceipt } from './CloseSaleReceipt'; +import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; +import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms'; + +@Service() +export class SaleReceiptApplication { + @Inject() + private createSaleReceiptService: CreateSaleReceipt; + + @Inject() + private editSaleReceiptService: EditSaleReceipt; + + @Inject() + private getSaleReceiptService: GetSaleReceipt; + + @Inject() + private deleteSaleReceiptService: DeleteSaleReceipt; + + @Inject() + private getSaleReceiptsService: GetSaleReceipts; + + @Inject() + private closeSaleReceiptService: CloseSaleReceipt; + + @Inject() + private getSaleReceiptPdfService: SaleReceiptsPdf; + + @Inject() + private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms; + + /** + * Creates a new sale receipt with associated entries. + * @param {number} tenantId + * @param {} saleReceiptDTO + * @returns {Promise} + */ + public async createSaleReceipt( + tenantId: number, + saleReceiptDTO: any + ): Promise { + return this.createSaleReceiptService.createSaleReceipt( + tenantId, + saleReceiptDTO + ); + } + + /** + * Edit details sale receipt with associated entries. + * @param {number} tenantId + * @param {number} saleReceiptId + * @param {} saleReceiptDTO + * @returns + */ + public async editSaleReceipt( + tenantId: number, + saleReceiptId: number, + saleReceiptDTO: any + ) { + return this.editSaleReceiptService.editSaleReceipt( + tenantId, + saleReceiptId, + saleReceiptDTO + ); + } + + /** + * Retrieve sale receipt with associated entries. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + public async getSaleReceipt(tenantId: number, saleReceiptId: number) { + return this.getSaleReceiptService.getSaleReceipt(tenantId, saleReceiptId); + } + + /** + * Deletes the sale receipt with associated entries. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { + return this.deleteSaleReceiptService.deleteSaleReceipt( + tenantId, + saleReceiptId + ); + } + + /** + * Retrieve sales receipts paginated and filterable list. + * @param {number} tenantId + * @param {ISalesReceiptsFilter} filterDTO + * @returns + */ + public async getSaleReceipts( + tenantId: number, + filterDTO: ISalesReceiptsFilter + ): Promise<{ + data: ISaleReceipt[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + return this.getSaleReceiptsService.getSaleReceipts(tenantId, filterDTO); + } + + /** + * Closes the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns {Promise} + */ + public async closeSaleReceipt(tenantId: number, saleReceiptId: number) { + return this.closeSaleReceiptService.closeSaleReceipt( + tenantId, + saleReceiptId + ); + } + + /** + * Retrieves the given sale receipt pdf. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + public getSaleReceiptPdf(tenantId: number, saleReceiptId: number) { + return this.getSaleReceiptPdfService.saleReceiptPdf( + tenantId, + saleReceiptId + ); + } + + /** + * Notify receipt customer by SMS of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + public saleReceiptNotifyBySms(tenantId: number, saleReceiptId: number) { + return this.saleReceiptNotifyBySmsService.notifyBySms( + tenantId, + saleReceiptId + ); + } + + /** + * Retrieves sms details of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + public getSaleReceiptSmsDetails(tenantId: number, saleReceiptId: number) { + return this.saleReceiptNotifyBySmsService.smsDetails( + tenantId, + saleReceiptId + ); + } +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts new file mode 100644 index 000000000..3fd078862 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts @@ -0,0 +1,89 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { sumBy, omit } from 'lodash'; +import composeAsync from 'async/compose'; +import moment from 'moment'; +import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +import { SaleReceiptValidators } from './SaleReceiptValidators'; +import { ICustomer, ISaleReceipt, ISaleReceiptDTO } from '@/interfaces'; +import { formatDateFields } from '@/utils'; +import { SaleReceiptIncrement } from './SaleReceiptIncrement'; +import { ItemEntry } from '@/models'; + +@Service() +export class SaleReceiptDTOTransformer { + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private branchDTOTransform: BranchTransactionDTOTransform; + + @Inject() + private warehouseDTOTransform: WarehouseTransactionDTOTransform; + + @Inject() + private validators: SaleReceiptValidators; + + @Inject() + private receiptIncrement: SaleReceiptIncrement; + + /** + * Transform create DTO object to model object. + * @param {ISaleReceiptDTO} saleReceiptDTO - + * @param {ISaleReceipt} oldSaleReceipt - + * @returns {ISaleReceipt} + */ + async transformDTOToModel( + tenantId: number, + saleReceiptDTO: ISaleReceiptDTO, + paymentCustomer: ICustomer, + oldSaleReceipt?: ISaleReceipt + ): Promise { + const amount = sumBy(saleReceiptDTO.entries, (e) => + ItemEntry.calcAmount(e) + ); + // Retreive the next invoice number. + const autoNextNumber = this.receiptIncrement.getNextReceiptNumber(tenantId); + + // Retreive the receipt number. + const receiptNumber = + saleReceiptDTO.receiptNumber || + oldSaleReceipt?.receiptNumber || + autoNextNumber; + + // Validate receipt number require. + this.validators.validateReceiptNoRequire(receiptNumber); + + const initialEntries = saleReceiptDTO.entries.map((entry) => ({ + reference_type: 'SaleReceipt', + ...entry, + })); + + const entries = await composeAsync( + // Sets default cost and sell account to receipt items entries. + this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId) + )(initialEntries); + + const initialDTO = { + amount, + ...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [ + 'receiptDate', + ]), + currencyCode: paymentCustomer.currencyCode, + exchangeRate: saleReceiptDTO.exchangeRate || 1, + receiptNumber, + // Avoid rewrite the deliver date in edit mode when already published. + ...(saleReceiptDTO.closed && + !oldSaleReceipt?.closedAt && { + closedAt: moment().toMySqlDateTime(), + }), + entries, + }; + return R.compose( + this.branchDTOTransform.transformDTO(tenantId), + this.warehouseDTOTransform.transformDTO(tenantId) + )(initialDTO); + } +} diff --git a/packages/server/src/services/Sales/SaleReceiptGLEntries.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts similarity index 100% rename from packages/server/src/services/Sales/SaleReceiptGLEntries.ts rename to packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptIncrement.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptIncrement.ts new file mode 100644 index 000000000..eae263044 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptIncrement.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import AutoIncrementOrdersService from '../AutoIncrementOrdersService'; + +@Service() +export class SaleReceiptIncrement { + @Inject() + private autoIncrementOrdersService: AutoIncrementOrdersService; + + /** + * Retrieve the next unique receipt number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + public getNextReceiptNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'sales_receipts' + ); + } + + /** + * Increment the receipt next number. + * @param {number} tenantId - + */ + public incrementNextReceiptNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'sales_receipts' + ); + } +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptInventoryTransactions.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptInventoryTransactions.ts new file mode 100644 index 000000000..fccf91418 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptInventoryTransactions.ts @@ -0,0 +1,72 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import { ISaleReceipt } from '@/interfaces'; +import InventoryService from '@/services/Inventory/Inventory'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; + +@Service() +export class SaleReceiptInventoryTransactions { + @Inject() + private inventoryService: InventoryService; + + @Inject() + private itemsEntriesService: ItemsEntriesService; + + /** + * Records the inventory transactions from the given bill input. + * @param {Bill} bill - Bill model object. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async recordInventoryTransactions( + tenantId: number, + saleReceipt: ISaleReceipt, + override?: boolean, + trx?: Knex.Transaction + ): Promise { + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + tenantId, + saleReceipt.entries + ); + const transaction = { + transactionId: saleReceipt.id, + transactionType: 'SaleReceipt', + transactionNumber: saleReceipt.receiptNumber, + exchangeRate: saleReceipt.exchangeRate, + + date: saleReceipt.receiptDate, + direction: 'OUT', + entries: inventoryEntries, + createdAt: saleReceipt.createdAt, + + warehouseId: saleReceipt.warehouseId, + }; + return this.inventoryService.recordInventoryTransactionsFromItemsEntries( + tenantId, + transaction, + override, + trx + ); + } + + /** + * Reverts the inventory transactions of the given bill id. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async revertInventoryTransactions( + tenantId: number, + receiptId: number, + trx?: Knex.Transaction + ) { + return this.inventoryService.deleteInventoryTransactions( + tenantId, + receiptId, + 'SaleReceipt', + trx + ); + } +} diff --git a/packages/server/src/services/Sales/SaleReceiptNotifyBySms.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptNotifyBySms.ts similarity index 93% rename from packages/server/src/services/Sales/SaleReceiptNotifyBySms.ts rename to packages/server/src/services/Sales/Receipts/SaleReceiptNotifyBySms.ts index cac535935..b53c4a472 100644 --- a/packages/server/src/services/Sales/SaleReceiptNotifyBySms.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptNotifyBySms.ts @@ -1,38 +1,33 @@ import { Service, Inject } from 'typedi'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import events from '@/subscribers/events'; -import SMSClient from '@/services/SMSClient'; import { ISaleReceiptSmsDetails, ISaleReceipt, SMS_NOTIFICATION_KEY, ICustomer, } from '@/interfaces'; -import SalesReceiptService from './SalesReceipts'; import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; import { formatNumber, formatSmsMessage } from 'utils'; import { TenantMetadata } from '@/system/models'; -import SaleNotifyBySms from './SaleNotifyBySms'; import { ServiceError } from '@/exceptions'; -import { ERRORS } from './Receipts/constants'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import SaleNotifyBySms from '../SaleNotifyBySms'; +import { ERRORS } from './constants'; @Service() -export default class SaleReceiptNotifyBySms { +export class SaleReceiptNotifyBySms { @Inject() - receiptsService: SalesReceiptService; + private tenancy: HasTenancyService; @Inject() - tenancy: HasTenancyService; + private eventPublisher: EventPublisher; @Inject() - eventPublisher: EventPublisher; + private smsNotificationsSettings: SmsNotificationsSettingsService; @Inject() - smsNotificationsSettings: SmsNotificationsSettingsService; - - @Inject() - saleSmsNotification: SaleNotifyBySms; + private saleSmsNotification: SaleNotifyBySms; /** * Notify customer via sms about sale receipt. diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptValidators.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptValidators.ts new file mode 100644 index 000000000..910d78383 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptValidators.ts @@ -0,0 +1,106 @@ +import { Inject, Service } from 'typedi'; +import { ServiceError } from '@/exceptions'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ACCOUNT_PARENT_TYPE } from '@/data/AccountTypes'; +import { ERRORS } from './constants'; +import { SaleEstimate, SaleReceipt } from '@/models'; + +@Service() +export class SaleReceiptValidators { + @Inject() + private tenancy: HasTenancyService; + + /** + * Validates the sale receipt existance. + * @param {SaleEstimate | undefined | null} estimate + */ + public validateReceiptExistance(receipt: SaleReceipt | undefined | null) { + if (!receipt) { + throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND); + } + } + + /** + * Validates the receipt not closed. + * @param {SaleReceipt} receipt + */ + public validateReceiptNotClosed(receipt: SaleReceipt) { + if (receipt.isClosed) { + throw new ServiceError(ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED); + } + } + + /** + * Validate whether sale receipt deposit account exists on the storage. + * @param {number} tenantId - Tenant id. + * @param {number} accountId - Account id. + */ + public async validateReceiptDepositAccountExistance( + tenantId: number, + accountId: number + ) { + const { accountRepository } = this.tenancy.repositories(tenantId); + const depositAccount = await accountRepository.findOneById(accountId); + + if (!depositAccount) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); + } + if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET); + } + } + + /** + * Validate sale receipt number uniquiness on the storage. + * @param {number} tenantId - + * @param {string} receiptNumber - + * @param {number} notReceiptId - + */ + public async validateReceiptNumberUnique( + tenantId: number, + receiptNumber: string, + notReceiptId?: number + ) { + const { SaleReceipt } = this.tenancy.models(tenantId); + + const saleReceipt = await SaleReceipt.query() + .findOne('receipt_number', receiptNumber) + .onBuild((builder) => { + if (notReceiptId) { + builder.whereNot('id', notReceiptId); + } + }); + + if (saleReceipt) { + throw new ServiceError(ERRORS.SALE_RECEIPT_NUMBER_NOT_UNIQUE); + } + } + + /** + * Validate the sale receipt number require. + * @param {ISaleReceipt} saleReceipt + */ + public validateReceiptNoRequire(receiptNumber: string) { + if (!receiptNumber) { + throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED); + } + } + + /** + * Validate the given customer has no sales receipts. + * @param {number} tenantId + * @param {number} customerId - Customer id. + */ + public async validateCustomerHasNoReceipts( + tenantId: number, + customerId: number + ) { + const { SaleReceipt } = this.tenancy.models(tenantId); + + const receipts = await SaleReceipt.query().where('customer_id', customerId); + + if (receipts.length > 0) { + throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES); + } + } +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts index 92db42f38..55863b4d8 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -5,7 +5,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Tenant } from '@/system/models'; @Service() -export default class SaleReceiptsPdf { +export class SaleReceiptsPdf { @Inject() pdfService: PdfService; diff --git a/packages/server/src/services/Sales/Receipts/subscribers/SaleReceiptCostGLEntriesSubscriber.ts b/packages/server/src/services/Sales/Receipts/subscribers/SaleReceiptCostGLEntriesSubscriber.ts index faaa9a124..5e6311005 100644 --- a/packages/server/src/services/Sales/Receipts/subscribers/SaleReceiptCostGLEntriesSubscriber.ts +++ b/packages/server/src/services/Sales/Receipts/subscribers/SaleReceiptCostGLEntriesSubscriber.ts @@ -22,7 +22,7 @@ export class SaleReceiptCostGLEntriesSubscriber { * Writes the receipts cost GL entries once the inventory cost lots be written. * @param {IInventoryCostLotsGLEntriesWriteEvent} */ - writeJournalEntriesOnceWriteoffCreate = async ({ + private writeJournalEntriesOnceWriteoffCreate = async ({ trx, startingDate, tenantId, diff --git a/packages/server/src/services/Sales/SaleNotifyBySms.ts b/packages/server/src/services/Sales/SaleNotifyBySms.ts index 8755d714c..6bece44a7 100644 --- a/packages/server/src/services/Sales/SaleNotifyBySms.ts +++ b/packages/server/src/services/Sales/SaleNotifyBySms.ts @@ -17,7 +17,6 @@ export default class SaleNotifyBySms { if (!personalPhone) { throw new ServiceError(ERRORS.CUSTOMER_HAS_NO_PHONE_NUMBER); } - this.validateCustomerPhoneNumberLocally(personalPhone); }; diff --git a/packages/server/src/services/Sales/SalesEstimate.ts b/packages/server/src/services/Sales/SalesEstimate.ts deleted file mode 100644 index f367c0f4a..000000000 --- a/packages/server/src/services/Sales/SalesEstimate.ts +++ /dev/null @@ -1,718 +0,0 @@ -import { omit, sumBy } from 'lodash'; -import { Service, Inject } from 'typedi'; -import * as R from 'ramda'; -import { Knex } from 'knex'; -import { - IEstimatesFilter, - IFilterMeta, - IPaginationMeta, - ISaleEstimate, - ISaleEstimateApprovedEvent, - ISaleEstimateCreatedPayload, - ISaleEstimateCreatingPayload, - ISaleEstimateDeletedPayload, - ISaleEstimateDeletingPayload, - ISaleEstimateDTO, - ISaleEstimateEditedPayload, - ISaleEstimateEditingPayload, - ISaleEstimateEventDeliveredPayload, - ISaleEstimateEventDeliveringPayload, - ISaleEstimateApprovingEvent, - ISalesEstimatesService, - ICustomer, -} from '@/interfaces'; -import { formatDateFields } from 'utils'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; -import events from '@/subscribers/events'; -import { ServiceError } from '@/exceptions'; -import moment from 'moment'; -import AutoIncrementOrdersService from './AutoIncrementOrdersService'; -import SaleEstimateTransformer from './Estimates/SaleEstimateTransformer'; -import { ERRORS } from './Estimates/constants'; -import UnitOfWork from '@/services/UnitOfWork'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; -import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; -import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -/** - * Sale estimate service. - * @Service - */ -@Service('SalesEstimates') -export default class SaleEstimateService implements ISalesEstimatesService { - @Inject() - tenancy: TenancyService; - - @Inject() - itemsEntriesService: ItemsEntriesService; - - @Inject('logger') - logger: any; - - @Inject() - dynamicListService: DynamicListingService; - - @Inject() - eventPublisher: EventPublisher; - - @Inject() - autoIncrementOrdersService: AutoIncrementOrdersService; - - @Inject() - uow: UnitOfWork; - - @Inject() - branchDTOTransform: BranchTransactionDTOTransform; - - @Inject() - warehouseDTOTransform: WarehouseTransactionDTOTransform; - - @Inject() - transformer: TransformerInjectable; - - /** - * Retrieve sale estimate or throw service error. - * @param {number} tenantId - * @return {ISaleEstimate} - */ - async getSaleEstimateOrThrowError(tenantId: number, saleEstimateId: number) { - const { SaleEstimate } = this.tenancy.models(tenantId); - const foundSaleEstimate = await SaleEstimate.query().findById( - saleEstimateId - ); - - if (!foundSaleEstimate) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND); - } - return foundSaleEstimate; - } - - /** - * Validate the estimate number unique on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEstimateNumberExistance( - tenantId: number, - estimateNumber: string, - notEstimateId?: number - ) { - const { SaleEstimate } = this.tenancy.models(tenantId); - - const foundSaleEstimate = await SaleEstimate.query() - .findOne('estimate_number', estimateNumber) - .onBuild((builder) => { - if (notEstimateId) { - builder.whereNot('id', notEstimateId); - } - }); - if (foundSaleEstimate) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE); - } - } - - /** - * Validates the given sale estimate not already converted to invoice. - * @param {ISaleEstimate} saleEstimate - - */ - validateEstimateNotConverted(saleEstimate: ISaleEstimate) { - if (saleEstimate.isConvertedToInvoice) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE); - } - } - - /** - * Retrieve the next unique estimate number. - * @param {number} tenantId - Tenant id. - * @return {string} - */ - getNextEstimateNumber(tenantId: number): string { - return this.autoIncrementOrdersService.getNextTransactionNumber( - tenantId, - 'sales_estimates' - ); - } - - /** - * Increment the estimate next number. - * @param {number} tenantId - - */ - incrementNextEstimateNumber(tenantId: number) { - return this.autoIncrementOrdersService.incrementSettingsNextNumber( - tenantId, - 'sales_estimates' - ); - } - - /** - * Retrieve estimate number to object model. - * @param {number} tenantId - * @param {ISaleEstimateDTO} saleEstimateDTO - * @param {ISaleEstimate} oldSaleEstimate - */ - transformEstimateNumberToModel( - tenantId: number, - saleEstimateDTO: ISaleEstimateDTO, - oldSaleEstimate?: ISaleEstimate - ): string { - // Retreive the next invoice number. - const autoNextNumber = this.getNextEstimateNumber(tenantId); - - if (saleEstimateDTO.estimateNumber) { - return saleEstimateDTO.estimateNumber; - } - return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber; - } - - /** - * Transform create DTO object ot model object. - * @param {number} tenantId - * @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO. - * @return {ISaleEstimate} - */ - async transformDTOToModel( - tenantId: number, - estimateDTO: ISaleEstimateDTO, - paymentCustomer: ICustomer, - oldSaleEstimate?: ISaleEstimate - ): Promise { - const { ItemEntry, Contact } = this.tenancy.models(tenantId); - - const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e)); - - // Retreive the next invoice number. - const autoNextNumber = this.getNextEstimateNumber(tenantId); - - // Retreive the next estimate number. - const estimateNumber = - estimateDTO.estimateNumber || - oldSaleEstimate?.estimateNumber || - autoNextNumber; - - // Validate the sale estimate number require. - this.validateEstimateNoRequire(estimateNumber); - - const initialDTO = { - amount, - ...formatDateFields(omit(estimateDTO, ['delivered', 'entries']), [ - 'estimateDate', - 'expirationDate', - ]), - currencyCode: paymentCustomer.currencyCode, - exchangeRate: estimateDTO.exchangeRate || 1, - ...(estimateNumber ? { estimateNumber } : {}), - entries: estimateDTO.entries.map((entry) => ({ - reference_type: 'SaleEstimate', - ...entry, - })), - // Avoid rewrite the deliver date in edit mode when already published. - ...(estimateDTO.delivered && - !oldSaleEstimate?.deliveredAt && { - deliveredAt: moment().toMySqlDateTime(), - }), - }; - return R.compose( - this.branchDTOTransform.transformDTO(tenantId), - this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); - } - - /** - * Validate the sale estimate number require. - * @param {ISaleEstimate} saleInvoiceObj - */ - validateEstimateNoRequire(estimateNumber: string) { - if (!estimateNumber) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED); - } - } - - /** - * Creates a new estimate with associated entries. - * @async - * @param {number} tenantId - The tenant id. - * @param {EstimateDTO} estimate - * @return {Promise} - */ - public async createEstimate( - tenantId: number, - estimateDTO: ISaleEstimateDTO - ): Promise { - const { SaleEstimate, Contact } = this.tenancy.models(tenantId); - - // Retrieve the given customer or throw not found service error. - const customer = await Contact.query() - .modify('customer') - .findById(estimateDTO.customerId) - .throwIfNotFound(); - - // Transform DTO object ot model object. - const estimateObj = await this.transformDTOToModel( - tenantId, - estimateDTO, - customer - ); - // Validate estimate number uniquiness on the storage. - await this.validateEstimateNumberExistance( - tenantId, - estimateObj.estimateNumber - ); - // Validate items IDs existance on the storage. - await this.itemsEntriesService.validateItemsIdsExistance( - tenantId, - estimateDTO.entries - ); - // Validate non-sellable items. - await this.itemsEntriesService.validateNonSellableEntriesItems( - tenantId, - estimateDTO.entries - ); - // Creates a sale estimate transaction with associated transactions as UOW. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleEstimateCreating` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, { - estimateDTO, - tenantId, - trx, - } as ISaleEstimateCreatingPayload); - - // Upsert the sale estimate graph to the storage. - const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({ - ...estimateObj, - }); - // Triggers `onSaleEstimateCreated` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, { - tenantId, - saleEstimate, - saleEstimateId: saleEstimate.id, - saleEstimateDTO: estimateDTO, - trx, - } as ISaleEstimateCreatedPayload); - - return saleEstimate; - }); - } - - /** - * Edit details of the given estimate with associated entries. - * @async - * @param {number} tenantId - The tenant id. - * @param {Integer} estimateId - * @param {EstimateDTO} estimate - * @return {void} - */ - public async editEstimate( - tenantId: number, - estimateId: number, - estimateDTO: ISaleEstimateDTO - ): Promise { - const { SaleEstimate, Contact } = this.tenancy.models(tenantId); - - const oldSaleEstimate = await this.getSaleEstimateOrThrowError( - tenantId, - estimateId - ); - // Retrieve the given customer or throw not found service error. - const customer = await Contact.query() - .modify('customer') - .findById(estimateDTO.customerId) - .throwIfNotFound(); - - // Transform DTO object ot model object. - const estimateObj = await this.transformDTOToModel( - tenantId, - estimateDTO, - oldSaleEstimate, - customer - ); - // Validate estimate number uniquiness on the storage. - if (estimateDTO.estimateNumber) { - await this.validateEstimateNumberExistance( - tenantId, - estimateDTO.estimateNumber, - estimateId - ); - } - // Validate sale estimate entries existance. - await this.itemsEntriesService.validateEntriesIdsExistance( - tenantId, - estimateId, - 'SaleEstimate', - estimateDTO.entries - ); - // Validate items IDs existance on the storage. - await this.itemsEntriesService.validateItemsIdsExistance( - tenantId, - estimateDTO.entries - ); - // Validate non-sellable items. - await this.itemsEntriesService.validateNonSellableEntriesItems( - tenantId, - estimateDTO.entries - ); - // Edits estimate transaction with associated transactions - // under unit-of-work envirement. - return this.uow.withTransaction(tenantId, async (trx) => { - // Trigger `onSaleEstimateEditing` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onEditing, { - tenantId, - oldSaleEstimate, - estimateDTO, - trx, - } as ISaleEstimateEditingPayload); - - // Upsert the estimate graph to the storage. - const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({ - id: estimateId, - ...estimateObj, - }); - // Trigger `onSaleEstimateEdited` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onEdited, { - tenantId, - estimateId, - saleEstimate, - oldSaleEstimate, - trx, - } as ISaleEstimateEditedPayload); - - return saleEstimate; - }); - } - - /** - * Deletes the given estimate id with associated entries. - * @async - * @param {number} tenantId - The tenant id. - * @param {IEstimate} estimateId - * @return {void} - */ - public async deleteEstimate( - tenantId: number, - estimateId: number - ): Promise { - const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); - - // Retrieve sale estimate or throw not found service error. - const oldSaleEstimate = await this.getSaleEstimateOrThrowError( - tenantId, - estimateId - ); - // Throw error if the sale estimate converted to sale invoice. - if (oldSaleEstimate.convertedToInvoiceId) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE); - } - // Deletes the estimate with associated transactions under UOW enivrement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleEstimatedDeleting` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onDeleting, { - trx, - tenantId, - oldSaleEstimate, - } as ISaleEstimateDeletingPayload); - - // Delete sale estimate entries. - await ItemEntry.query(trx) - .where('reference_id', estimateId) - .where('reference_type', 'SaleEstimate') - .delete(); - - // Delete sale estimate transaction. - await SaleEstimate.query(trx).where('id', estimateId).delete(); - - // Triggers `onSaleEstimatedDeleted` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onDeleted, { - tenantId, - saleEstimateId: estimateId, - oldSaleEstimate, - trx, - } as ISaleEstimateDeletedPayload); - }); - } - - /** - * Retrieve the estimate details with associated entries. - * @async - * @param {number} tenantId - The tenant id. - * @param {Integer} estimateId - */ - public async getEstimate(tenantId: number, estimateId: number) { - const { SaleEstimate } = this.tenancy.models(tenantId); - const estimate = await SaleEstimate.query() - .findById(estimateId) - .withGraphFetched('entries.item') - .withGraphFetched('customer') - .withGraphFetched('branch'); - - if (!estimate) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND); - } - // Transformes sale estimate model to POJO. - return this.transformer.transform( - tenantId, - estimate, - new SaleEstimateTransformer() - ); - } - - /** - * Parses estimates list filter DTO. - * @param filterDTO - */ - private parseListFilterDTO(filterDTO) { - return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); - } - - /** - * Retrieves estimates filterable and paginated list. - * @param {number} tenantId - - * @param {IEstimatesFilter} estimatesFilter - - */ - public async estimatesList( - tenantId: number, - filterDTO: IEstimatesFilter - ): Promise<{ - salesEstimates: ISaleEstimate[]; - pagination: IPaginationMeta; - filterMeta: IFilterMeta; - }> { - const { SaleEstimate } = this.tenancy.models(tenantId); - - // Parses filter DTO. - const filter = this.parseListFilterDTO(filterDTO); - - // Dynamic list service. - const dynamicFilter = await this.dynamicListService.dynamicList( - tenantId, - SaleEstimate, - filter - ); - const { results, pagination } = await SaleEstimate.query() - .onBuild((builder) => { - builder.withGraphFetched('customer'); - builder.withGraphFetched('entries'); - dynamicFilter.buildQuery()(builder); - }) - .pagination(filter.page - 1, filter.pageSize); - - const transformedEstimates = await this.transformer.transform( - tenantId, - results, - new SaleEstimateTransformer() - ); - return { - salesEstimates: transformedEstimates, - pagination, - filterMeta: dynamicFilter.getResponseMeta(), - }; - } - - /** - * Converts estimate to invoice. - * @param {number} tenantId - - * @param {number} estimateId - - * @return {Promise} - */ - async convertEstimateToInvoice( - tenantId: number, - estimateId: number, - invoiceId: number, - trx?: Knex.Transaction - ): Promise { - const { SaleEstimate } = this.tenancy.models(tenantId); - - // Retrieve details of the given sale estimate. - const saleEstimate = await this.getSaleEstimateOrThrowError( - tenantId, - estimateId - ); - // Marks the estimate as converted from the givne invoice. - await SaleEstimate.query(trx).where('id', estimateId).patch({ - convertedToInvoiceId: invoiceId, - convertedToInvoiceAt: moment().toMySqlDateTime(), - }); - // Triggers `onSaleEstimateConvertedToInvoice` event. - await this.eventPublisher.emitAsync( - events.saleEstimate.onConvertedToInvoice, - {} - ); - } - - /** - * Unlink the converted sale estimates from the given sale invoice. - * @param {number} tenantId - - * @param {number} invoiceId - - * @return {Promise} - */ - async unlinkConvertedEstimateFromInvoice( - tenantId: number, - invoiceId: number, - trx?: Knex.Transaction - ): Promise { - const { SaleEstimate } = this.tenancy.models(tenantId); - - await SaleEstimate.query(trx) - .where({ - convertedToInvoiceId: invoiceId, - }) - .patch({ - convertedToInvoiceId: null, - convertedToInvoiceAt: null, - }); - } - - /** - * Mark the sale estimate as delivered. - * @param {number} tenantId - Tenant id. - * @param {number} saleEstimateId - Sale estimate id. - */ - public async deliverSaleEstimate( - tenantId: number, - saleEstimateId: number - ): Promise { - const { SaleEstimate } = this.tenancy.models(tenantId); - - // Retrieve details of the given sale estimate id. - const oldSaleEstimate = await this.getSaleEstimateOrThrowError( - tenantId, - saleEstimateId - ); - // Throws error in case the sale estimate already published. - if (oldSaleEstimate.isDelivered) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED); - } - // Updates the sale estimate transaction with assocaited transactions - // under UOW envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleEstimateDelivering` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onDelivering, { - oldSaleEstimate, - trx, - tenantId, - } as ISaleEstimateEventDeliveringPayload); - - // Record the delivered at on the storage. - const saleEstimate = await SaleEstimate.query(trx).patchAndFetchById( - saleEstimateId, - { - deliveredAt: moment().toMySqlDateTime(), - } - ); - // Triggers `onSaleEstimateDelivered` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onDelivered, { - tenantId, - saleEstimate, - trx, - } as ISaleEstimateEventDeliveredPayload); - }); - } - - /** - * Mark the sale estimate as approved from the customer. - * @param {number} tenantId - * @param {number} saleEstimateId - */ - public async approveSaleEstimate( - tenantId: number, - saleEstimateId: number - ): Promise { - const { SaleEstimate } = this.tenancy.models(tenantId); - - // Retrieve details of the given sale estimate id. - const oldSaleEstimate = await this.getSaleEstimateOrThrowError( - tenantId, - saleEstimateId - ); - // Throws error in case the sale estimate still not delivered to customer. - if (!oldSaleEstimate.isDelivered) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED); - } - // Throws error in case the sale estimate already approved. - if (oldSaleEstimate.isApproved) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_APPROVED); - } - // Triggers `onSaleEstimateApproving` event. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleEstimateApproving` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onApproving, { - trx, - tenantId, - oldSaleEstimate, - } as ISaleEstimateApprovingEvent); - - // Update estimate as approved. - const saleEstimate = await SaleEstimate.query(trx) - .where('id', saleEstimateId) - .patch({ - approvedAt: moment().toMySqlDateTime(), - rejectedAt: null, - }); - // Triggers `onSaleEstimateApproved` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onApproved, { - trx, - tenantId, - oldSaleEstimate, - saleEstimate, - } as ISaleEstimateApprovedEvent); - }); - } - - /** - * Mark the sale estimate as rejected from the customer. - * @param {number} tenantId - * @param {number} saleEstimateId - */ - public async rejectSaleEstimate( - tenantId: number, - saleEstimateId: number - ): Promise { - const { SaleEstimate } = this.tenancy.models(tenantId); - - // Retrieve details of the given sale estimate id. - const saleEstimate = await this.getSaleEstimateOrThrowError( - tenantId, - saleEstimateId - ); - // Throws error in case the sale estimate still not delivered to customer. - if (!saleEstimate.isDelivered) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED); - } - // Throws error in case the sale estimate already rejected. - if (saleEstimate.isRejected) { - throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_REJECTED); - } - // - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Mark the sale estimate as reject on the storage. - await SaleEstimate.query(trx).where('id', saleEstimateId).patch({ - rejectedAt: moment().toMySqlDateTime(), - approvedAt: null, - }); - // Triggers `onSaleEstimateRejected` event. - await this.eventPublisher.emitAsync(events.saleEstimate.onRejected, {}); - }); - } - - /** - * Validate the given customer has no sales estimates. - * @param {number} tenantId - * @param {number} customerId - Customer id. - */ - public async validateCustomerHasNoEstimates( - tenantId: number, - customerId: number - ) { - const { SaleEstimate } = this.tenancy.models(tenantId); - - const estimates = await SaleEstimate.query().where( - 'customer_id', - customerId - ); - if (estimates.length > 0) { - throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_ESTIMATES); - } - } -} diff --git a/packages/server/src/services/Sales/SalesInvoices.ts b/packages/server/src/services/Sales/SalesInvoices.ts deleted file mode 100644 index 6c7c42632..000000000 --- a/packages/server/src/services/Sales/SalesInvoices.ts +++ /dev/null @@ -1,799 +0,0 @@ -import { Service, Inject } from 'typedi'; -import { omit, sumBy } from 'lodash'; -import * as R from 'ramda'; -import moment from 'moment'; -import { Knex } from 'knex'; -import composeAsync from 'async/compose'; -import { - ISaleInvoice, - ISaleInvoiceCreateDTO, - ISaleInvoiceEditDTO, - ISalesInvoicesFilter, - IPaginationMeta, - IFilterMeta, - ISystemUser, - ISalesInvoicesService, - ISaleInvoiceCreatedPayload, - ISaleInvoiceDeletePayload, - ISaleInvoiceDeletedPayload, - ISaleInvoiceEventDeliveredPayload, - ISaleInvoiceEditedPayload, - ISaleInvoiceCreatingPaylaod, - ISaleInvoiceEditingPayload, - ISaleInvoiceDeliveringPayload, - ICustomer, - ITenantUser, -} from '@/interfaces'; -import events from '@/subscribers/events'; -import InventoryService from '@/services/Inventory/Inventory'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import { formatDateFields } from 'utils'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import { ServiceError } from '@/exceptions'; -import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; -import SaleEstimateService from '@/services/Sales/SalesEstimate'; -import AutoIncrementOrdersService from './AutoIncrementOrdersService'; -import { ERRORS } from './constants'; -import { SaleInvoiceTransformer } from './SaleInvoiceTransformer'; -import UnitOfWork from '@/services/UnitOfWork'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; -import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; -import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -/** - * Sales invoices service - * @service - */ -@Service('SalesInvoices') -export default class SaleInvoicesService implements ISalesInvoicesService { - @Inject() - tenancy: TenancyService; - - @Inject() - inventoryService: InventoryService; - - @Inject() - itemsEntriesService: ItemsEntriesService; - - @Inject('logger') - logger: any; - - @Inject() - dynamicListService: DynamicListingService; - - @Inject() - private saleEstimatesService: SaleEstimateService; - - @Inject() - private autoIncrementOrdersService: AutoIncrementOrdersService; - - @Inject() - private eventPublisher: EventPublisher; - - @Inject() - private uow: UnitOfWork; - - @Inject() - private branchDTOTransform: BranchTransactionDTOTransform; - - @Inject() - private warehouseDTOTransform: WarehouseTransactionDTOTransform; - - @Inject() - private transformer: TransformerInjectable; - - /** - * Validate whether sale invoice number unqiue on the storage. - */ - async validateInvoiceNumberUnique( - tenantId: number, - invoiceNumber: string, - notInvoiceId?: number - ) { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const saleInvoice = await SaleInvoice.query() - .findOne('invoice_no', invoiceNumber) - .onBuild((builder) => { - if (notInvoiceId) { - builder.whereNot('id', notInvoiceId); - } - }); - - if (saleInvoice) { - throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE); - } - } - - /** - * Validate the sale invoice has no payment entries. - * @param {number} tenantId - * @param {number} saleInvoiceId - */ - async validateInvoiceHasNoPaymentEntries( - tenantId: number, - saleInvoiceId: number - ) { - const { PaymentReceiveEntry } = this.tenancy.models(tenantId); - - // Retrieve the sale invoice associated payment receive entries. - const entries = await PaymentReceiveEntry.query().where( - 'invoice_id', - saleInvoiceId - ); - if (entries.length > 0) { - throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES); - } - return entries; - } - - /** - * Validate the invoice amount is bigger than payment amount before edit the invoice. - * @param {number} saleInvoiceAmount - * @param {number} paymentAmount - */ - validateInvoiceAmountBiggerPaymentAmount( - saleInvoiceAmount: number, - paymentAmount: number - ) { - if (saleInvoiceAmount < paymentAmount) { - throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT); - } - } - - /** - * Validate whether sale invoice exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async getInvoiceOrThrowError(tenantId: number, saleInvoiceId: number) { - const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); - - const saleInvoice = await saleInvoiceRepository.findOneById( - saleInvoiceId, - 'entries' - ); - if (!saleInvoice) { - throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); - } - return saleInvoice; - } - - /** - * Retrieve the next unique invoice number. - * @param {number} tenantId - Tenant id. - * @return {string} - */ - getNextInvoiceNumber(tenantId: number): string { - return this.autoIncrementOrdersService.getNextTransactionNumber( - tenantId, - 'sales_invoices' - ); - } - - /** - * Increment the invoice next number. - * @param {number} tenantId - - */ - incrementNextInvoiceNumber(tenantId: number) { - return this.autoIncrementOrdersService.incrementSettingsNextNumber( - tenantId, - 'sales_invoices' - ); - } - - /** - * Transformes edit DTO to model. - * @param {number} tennatId - - * @param {ICustomer} customer - - * @param {ISaleInvoiceEditDTO} saleInvoiceDTO - - * @param {ISaleInvoice} oldSaleInvoice - */ - private tranformEditDTOToModel = async ( - tenantId: number, - customer: ICustomer, - saleInvoiceDTO: ISaleInvoiceEditDTO, - oldSaleInvoice: ISaleInvoice, - authorizedUser: ITenantUser - ) => { - return this.transformDTOToModel( - tenantId, - customer, - saleInvoiceDTO, - authorizedUser, - oldSaleInvoice - ); - }; - - /** - * Transformes create DTO to model. - * @param {number} tenantId - - * @param {ICustomer} customer - - * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - - */ - private transformCreateDTOToModel = async ( - tenantId: number, - customer: ICustomer, - saleInvoiceDTO: ISaleInvoiceCreateDTO, - authorizedUser: ITenantUser - ) => { - return this.transformDTOToModel( - tenantId, - customer, - saleInvoiceDTO, - authorizedUser - ); - }; - - /** - * Transformes the create DTO to invoice object model. - * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. - * @param {ISaleInvoice} oldSaleInvoice - Old sale invoice. - * @return {ISaleInvoice} - */ - private async transformDTOToModel( - tenantId: number, - customer: ICustomer, - saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, - authorizedUser: ITenantUser, - oldSaleInvoice?: ISaleInvoice - ): Promise { - const { ItemEntry } = this.tenancy.models(tenantId); - - const balance = sumBy(saleInvoiceDTO.entries, (e) => - ItemEntry.calcAmount(e) - ); - // Retreive the next invoice number. - const autoNextNumber = this.getNextInvoiceNumber(tenantId); - - // Invoice number. - const invoiceNo = - saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber; - - // Validate the invoice is required. - this.validateInvoiceNoRequire(invoiceNo); - - const initialEntries = saleInvoiceDTO.entries.map((entry) => ({ - referenceType: 'SaleInvoice', - ...entry, - })); - const entries = await composeAsync( - // Sets default cost and sell account to invoice items entries. - this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId) - )(initialEntries); - - const initialDTO = { - ...formatDateFields( - omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']), - ['invoiceDate', 'dueDate'] - ), - // Avoid rewrite the deliver date in edit mode when already published. - balance, - currencyCode: customer.currencyCode, - exchangeRate: saleInvoiceDTO.exchangeRate || 1, - ...(saleInvoiceDTO.delivered && - !oldSaleInvoice?.deliveredAt && { - deliveredAt: moment().toMySqlDateTime(), - }), - // Avoid override payment amount in edit mode. - ...(!oldSaleInvoice && { paymentAmount: 0 }), - ...(invoiceNo ? { invoiceNo } : {}), - entries, - userId: authorizedUser.id, - } as ISaleInvoice; - - return R.compose( - this.branchDTOTransform.transformDTO(tenantId), - this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); - } - - /** - * Validate the invoice number require. - * @param {ISaleInvoice} saleInvoiceObj - */ - validateInvoiceNoRequire(invoiceNo: string) { - if (!invoiceNo) { - throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED); - } - } - - /** - * Creates a new sale invoices and store it to the storage - * with associated to entries and journal transactions. - * @async - * @param {number} tenantId - Tenant id. - * @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO. - * @return {Promise} - */ - public createSaleInvoice = async ( - tenantId: number, - saleInvoiceDTO: ISaleInvoiceCreateDTO, - authorizedUser: ITenantUser - ): Promise => { - const { SaleInvoice, Contact } = this.tenancy.models(tenantId); - - // Validate customer existance. - const customer = await Contact.query() - .modify('customer') - .findById(saleInvoiceDTO.customerId) - .throwIfNotFound(); - - // Validate the from estimate id exists on the storage. - if (saleInvoiceDTO.fromEstimateId) { - const fromEstimate = - await this.saleEstimatesService.getSaleEstimateOrThrowError( - tenantId, - saleInvoiceDTO.fromEstimateId - ); - // Validate the sale estimate is not already converted to invoice. - this.saleEstimatesService.validateEstimateNotConverted(fromEstimate); - } - // Validate items ids existance. - await this.itemsEntriesService.validateItemsIdsExistance( - tenantId, - saleInvoiceDTO.entries - ); - // Validate items should be sellable items. - await this.itemsEntriesService.validateNonSellableEntriesItems( - tenantId, - saleInvoiceDTO.entries - ); - // Transform DTO object to model object. - const saleInvoiceObj = await this.transformCreateDTOToModel( - tenantId, - customer, - saleInvoiceDTO, - authorizedUser - ); - // Validate sale invoice number uniquiness. - if (saleInvoiceObj.invoiceNo) { - await this.validateInvoiceNumberUnique( - tenantId, - saleInvoiceObj.invoiceNo - ); - } - // Creates a new sale invoice and associated transactions under unit of work env. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleInvoiceCreating` event. - await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, { - saleInvoiceDTO, - tenantId, - trx, - } as ISaleInvoiceCreatingPaylaod); - - // Create sale invoice graph to the storage. - const saleInvoice = await SaleInvoice.query(trx).upsertGraph( - saleInvoiceObj - ); - const eventPayload: ISaleInvoiceCreatedPayload = { - tenantId, - saleInvoice, - saleInvoiceDTO, - saleInvoiceId: saleInvoice.id, - authorizedUser, - trx, - }; - // Triggers the event `onSaleInvoiceCreated`. - await this.eventPublisher.emitAsync( - events.saleInvoice.onCreated, - eventPayload - ); - return saleInvoice; - }); - }; - - /** - * Edit the given sale invoice. - * @async - * @param {number} tenantId - Tenant id. - * @param {Number} saleInvoiceId - Sale invoice id. - * @param {ISaleInvoice} saleInvoice - Sale invoice DTO object. - * @return {Promise} - */ - public async editSaleInvoice( - tenantId: number, - saleInvoiceId: number, - saleInvoiceDTO: ISaleInvoiceEditDTO, - authorizedUser: ISystemUser - ): Promise { - const { SaleInvoice, Contact } = this.tenancy.models(tenantId); - - // Retrieve the sale invoice or throw not found service error. - const oldSaleInvoice = await this.getInvoiceOrThrowError( - tenantId, - saleInvoiceId - ); - // Validate customer existance. - const customer = await Contact.query() - .findById(saleInvoiceDTO.customerId) - .modify('customer') - .throwIfNotFound(); - - // 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 - ); - // Transform DTO object to model object. - const saleInvoiceObj = await this.tranformEditDTOToModel( - tenantId, - customer, - saleInvoiceDTO, - oldSaleInvoice, - authorizedUser - ); - // Validate sale invoice number uniquiness. - if (saleInvoiceObj.invoiceNo) { - await this.validateInvoiceNumberUnique( - tenantId, - saleInvoiceObj.invoiceNo, - saleInvoiceId - ); - } - // Validate the invoice amount is not smaller than the invoice payment amount. - this.validateInvoiceAmountBiggerPaymentAmount( - saleInvoiceObj.balance, - oldSaleInvoice.paymentAmount - ); - // Edit sale invoice transaction in UOW envirment. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleInvoiceEditing` event. - await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, { - trx, - oldSaleInvoice, - tenantId, - saleInvoiceDTO, - } as ISaleInvoiceEditingPayload); - - // Upsert the the invoice graph to the storage. - const saleInvoice: ISaleInvoice = - await SaleInvoice.query().upsertGraphAndFetch({ - id: saleInvoiceId, - ...saleInvoiceObj, - }); - // Edit event payload. - const editEventPayload: ISaleInvoiceEditedPayload = { - tenantId, - saleInvoiceId, - saleInvoice, - saleInvoiceDTO, - oldSaleInvoice, - authorizedUser, - trx, - }; - // Triggers `onSaleInvoiceEdited` event. - await this.eventPublisher.emitAsync( - events.saleInvoice.onEdited, - editEventPayload - ); - return saleInvoice; - }); - } - - /** - * Deliver the given sale invoice. - * @param {number} tenantId - Tenant id. - * @param {number} saleInvoiceId - Sale invoice id. - * @return {Promise} - */ - public async deliverSaleInvoice( - tenantId: number, - saleInvoiceId: number, - authorizedUser: ISystemUser - ): Promise { - const { SaleInvoice } = this.tenancy.models(tenantId); - - // Retrieve details of the given sale invoice id. - const oldSaleInvoice = await this.getInvoiceOrThrowError( - tenantId, - saleInvoiceId - ); - // Throws error in case the sale invoice already published. - if (oldSaleInvoice.isDelivered) { - throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED); - } - // Update sale invoice transaction with assocaite transactions - // under unit-of-work envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleInvoiceDelivering` event. - await this.eventPublisher.emitAsync(events.saleInvoice.onDelivering, { - tenantId, - oldSaleInvoice, - trx, - } as ISaleInvoiceDeliveringPayload); - - // Record the delivered at on the storage. - const saleInvoice = await SaleInvoice.query(trx) - .where({ id: saleInvoiceId }) - .update({ deliveredAt: moment().toMySqlDateTime() }); - - // Triggers `onSaleInvoiceDelivered` event. - await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, { - tenantId, - saleInvoiceId, - saleInvoice, - } as ISaleInvoiceEventDeliveredPayload); - }); - } - - /** - * Deletes the given sale invoice with associated entries - * and journal transactions. - * @param {number} tenantId - Tenant id. - * @param {Number} saleInvoiceId - The given sale invoice id. - * @param {ISystemUser} authorizedUser - - */ - public async deleteSaleInvoice( - tenantId: number, - saleInvoiceId: number, - authorizedUser: ISystemUser - ): Promise { - const { ItemEntry, SaleInvoice } = this.tenancy.models(tenantId); - - // Retrieve the given sale invoice with associated entries - // or throw not found error. - const oldSaleInvoice = await this.getInvoiceOrThrowError( - tenantId, - saleInvoiceId - ); - // Validate the sale invoice has no associated payment entries. - await this.validateInvoiceHasNoPaymentEntries(tenantId, saleInvoiceId); - - // Validate the sale invoice has applied to credit note transaction. - await this.validateInvoiceHasNoAppliedToCredit(tenantId, saleInvoiceId); - - // Deletes sale invoice transaction and associate transactions with UOW env. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleInvoiceDelete` event. - await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, { - tenantId, - saleInvoice: oldSaleInvoice, - saleInvoiceId, - trx, - } as ISaleInvoiceDeletePayload); - - // Unlink the converted sale estimates from the given sale invoice. - await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice( - tenantId, - saleInvoiceId, - trx - ); - await ItemEntry.query(trx) - .where('reference_id', saleInvoiceId) - .where('reference_type', 'SaleInvoice') - .delete(); - - await SaleInvoice.query(trx).findById(saleInvoiceId).delete(); - - // Triggers `onSaleInvoiceDeleted` event. - await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, { - tenantId, - oldSaleInvoice, - saleInvoiceId, - authorizedUser, - trx, - } as ISaleInvoiceDeletedPayload); - }); - } - - /** - * Records the inventory transactions of the given sale invoice in case - * the invoice has inventory entries only. - * - * @param {number} tenantId - Tenant id. - * @param {SaleInvoice} saleInvoice - Sale invoice DTO. - * @param {number} saleInvoiceId - Sale invoice id. - * @param {boolean} override - Allow to override old transactions. - * @return {Promise} - */ - public async recordInventoryTranscactions( - tenantId: number, - saleInvoice: ISaleInvoice, - override?: boolean, - trx?: Knex.Transaction - ): Promise { - // Loads the inventory items entries of the given sale invoice. - const inventoryEntries = - await this.itemsEntriesService.filterInventoryEntries( - tenantId, - saleInvoice.entries - ); - const transaction = { - transactionId: saleInvoice.id, - transactionType: 'SaleInvoice', - transactionNumber: saleInvoice.invoiceNo, - - exchangeRate: saleInvoice.exchangeRate, - warehouseId: saleInvoice.warehouseId, - - date: saleInvoice.invoiceDate, - direction: 'OUT', - entries: inventoryEntries, - createdAt: saleInvoice.createdAt, - }; - await this.inventoryService.recordInventoryTransactionsFromItemsEntries( - tenantId, - transaction, - override, - trx - ); - } - /** - * Reverting the inventory transactions once the invoice deleted. - * @param {number} tenantId - Tenant id. - * @param {number} billId - Bill id. - * @return {Promise} - */ - public async revertInventoryTransactions( - tenantId: number, - saleInvoiceId: number, - trx?: Knex.Transaction - ): Promise { - // Delete the inventory transaction of the given sale invoice. - const { oldInventoryTransactions } = - await this.inventoryService.deleteInventoryTransactions( - tenantId, - saleInvoiceId, - 'SaleInvoice', - trx - ); - } - - /** - * Retrieve sale invoice with associated entries. - * @param {Number} saleInvoiceId - - * @param {ISystemUser} authorizedUser - - * @return {Promise} - */ - public async getSaleInvoice( - tenantId: number, - saleInvoiceId: number, - authorizedUser: ISystemUser - ): Promise { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const saleInvoice = await SaleInvoice.query() - .findById(saleInvoiceId) - .withGraphFetched('entries.item') - .withGraphFetched('customer') - .withGraphFetched('branch'); - - return this.transformer.transform( - tenantId, - saleInvoice, - new SaleInvoiceTransformer() - ); - } - - /** - * Parses the sale invoice list filter DTO. - * @param filterDTO - * @returns - */ - private parseListFilterDTO(filterDTO) { - return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); - } - - /** - * Retrieve sales invoices filterable and paginated list. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - public async salesInvoicesList( - tenantId: number, - filterDTO: ISalesInvoicesFilter - ): Promise<{ - salesInvoices: ISaleInvoice[]; - pagination: IPaginationMeta; - filterMeta: IFilterMeta; - }> { - const { SaleInvoice } = this.tenancy.models(tenantId); - - // Parses stringified filter roles. - const filter = this.parseListFilterDTO(filterDTO); - - // Dynamic list service. - const dynamicFilter = await this.dynamicListService.dynamicList( - tenantId, - SaleInvoice, - filter - ); - const { results, pagination } = await SaleInvoice.query() - .onBuild((builder) => { - builder.withGraphFetched('entries'); - builder.withGraphFetched('customer'); - dynamicFilter.buildQuery()(builder); - }) - .pagination(filter.page - 1, filter.pageSize); - - // Retrieves the transformed sale invoices. - const salesInvoices = await this.transformer.transform( - tenantId, - results, - new SaleInvoiceTransformer() - ); - - return { - salesInvoices, - pagination, - filterMeta: dynamicFilter.getResponseMeta(), - }; - } - - /** - * Retrieve due sales invoices. - * @param {number} tenantId - * @param {number} customerId - */ - public async getPayableInvoices( - tenantId: number, - customerId?: number - ): Promise { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const salesInvoices = await SaleInvoice.query().onBuild((query) => { - query.modify('dueInvoices'); - query.modify('delivered'); - - if (customerId) { - query.where('customer_id', customerId); - } - }); - return salesInvoices; - } - - /** - * Validate the given customer has no sales invoices. - * @param {number} tenantId - * @param {number} customerId - Customer id. - */ - public async validateCustomerHasNoInvoices( - tenantId: number, - customerId: number - ) { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const invoices = await SaleInvoice.query().where('customer_id', customerId); - - if (invoices.length > 0) { - throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES); - } - } - - /** - * Validate the sale invoice has no applied to credit note transaction. - * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} - */ - public validateInvoiceHasNoAppliedToCredit = async ( - tenantId: number, - invoiceId: number - ): Promise => { - const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId); - - const appliedTransactions = await CreditNoteAppliedInvoice.query().where( - 'invoiceId', - invoiceId - ); - if (appliedTransactions.length > 0) { - throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES); - } - }; -} diff --git a/packages/server/src/services/Sales/SalesReceipts.ts b/packages/server/src/services/Sales/SalesReceipts.ts deleted file mode 100644 index a2df05fd7..000000000 --- a/packages/server/src/services/Sales/SalesReceipts.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { omit, sumBy } from 'lodash'; -import { Service, Inject } from 'typedi'; -import moment from 'moment'; -import * as R from 'ramda'; -import { Knex } from 'knex'; -import composeAsync from 'async/compose'; -import events from '@/subscribers/events'; -import { - IFilterMeta, - IPaginationMeta, - ISaleReceipt, - ISaleReceiptDTO, - ISalesReceiptsService, - ISaleReceiptCreatedPayload, - ISaleReceiptEditedPayload, - ISaleReceiptEventClosedPayload, - ISaleReceiptEventDeletedPayload, - ISaleReceiptCreatingPayload, - ISaleReceiptDeletingPayload, - ISaleReceiptEditingPayload, - ISaleReceiptEventClosingPayload, - ICustomer, -} from '@/interfaces'; -import JournalPosterService from '@/services/Sales/JournalPosterService'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import { formatDateFields } from 'utils'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import { ServiceError } from '@/exceptions'; -import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; -import { ItemEntry } from 'models'; -import InventoryService from '@/services/Inventory/Inventory'; -import { ACCOUNT_PARENT_TYPE } from '@/data/AccountTypes'; -import AutoIncrementOrdersService from './AutoIncrementOrdersService'; -import { ERRORS } from './Receipts/constants'; -import { SaleReceiptTransformer } from './Receipts/SaleReceiptTransformer'; -import UnitOfWork from '@/services/UnitOfWork'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; -import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; -import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -@Service('SalesReceipts') -export default class SalesReceiptService implements ISalesReceiptsService { - @Inject() - tenancy: TenancyService; - - @Inject() - dynamicListService: DynamicListingService; - - @Inject() - journalService: JournalPosterService; - - @Inject() - itemsEntriesService: ItemsEntriesService; - - @Inject() - inventoryService: InventoryService; - - @Inject() - eventPublisher: EventPublisher; - - @Inject('logger') - logger: any; - - @Inject() - autoIncrementOrdersService: AutoIncrementOrdersService; - - @Inject() - uow: UnitOfWork; - - @Inject() - branchDTOTransform: BranchTransactionDTOTransform; - - @Inject() - warehouseDTOTransform: WarehouseTransactionDTOTransform; - - @Inject() - transformer: TransformerInjectable; - - /** - * Validate whether sale receipt exists on the storage. - * @param {number} tenantId - - * @param {number} saleReceiptId - - */ - async getSaleReceiptOrThrowError(tenantId: number, saleReceiptId: number) { - const { SaleReceipt } = this.tenancy.models(tenantId); - - const foundSaleReceipt = await SaleReceipt.query() - .findById(saleReceiptId) - .withGraphFetched('entries'); - - if (!foundSaleReceipt) { - throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND); - } - return foundSaleReceipt; - } - - /** - * Validate whether sale receipt deposit account exists on the storage. - * @param {number} tenantId - Tenant id. - * @param {number} accountId - Account id. - */ - async validateReceiptDepositAccountExistance( - tenantId: number, - accountId: number - ) { - const { accountRepository } = this.tenancy.repositories(tenantId); - const depositAccount = await accountRepository.findOneById(accountId); - - if (!depositAccount) { - throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); - } - if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { - throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET); - } - } - - /** - * Validate sale receipt number uniquiness on the storage. - * @param {number} tenantId - - * @param {string} receiptNumber - - * @param {number} notReceiptId - - */ - async validateReceiptNumberUnique( - tenantId: number, - receiptNumber: string, - notReceiptId?: number - ) { - const { SaleReceipt } = this.tenancy.models(tenantId); - - const saleReceipt = await SaleReceipt.query() - .findOne('receipt_number', receiptNumber) - .onBuild((builder) => { - if (notReceiptId) { - builder.whereNot('id', notReceiptId); - } - }); - - if (saleReceipt) { - throw new ServiceError(ERRORS.SALE_RECEIPT_NUMBER_NOT_UNIQUE); - } - } - - /** - * Validate the sale receipt number require. - * @param {ISaleReceipt} saleReceipt - */ - validateReceiptNoRequire(receiptNumber: string) { - if (!receiptNumber) { - throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED); - } - } - - /** - * Retrieve the next unique receipt number. - * @param {number} tenantId - Tenant id. - * @return {string} - */ - getNextReceiptNumber(tenantId: number): string { - return this.autoIncrementOrdersService.getNextTransactionNumber( - tenantId, - 'sales_receipts' - ); - } - - /** - * Increment the receipt next number. - * @param {number} tenantId - - */ - incrementNextReceiptNumber(tenantId: number) { - return this.autoIncrementOrdersService.incrementSettingsNextNumber( - tenantId, - 'sales_receipts' - ); - } - - /** - * Transform create DTO object to model object. - * @param {ISaleReceiptDTO} saleReceiptDTO - - * @param {ISaleReceipt} oldSaleReceipt - - * @returns {ISaleReceipt} - */ - async transformDTOToModel( - tenantId: number, - saleReceiptDTO: ISaleReceiptDTO, - paymentCustomer: ICustomer, - oldSaleReceipt?: ISaleReceipt - ): Promise { - const amount = sumBy(saleReceiptDTO.entries, (e) => - ItemEntry.calcAmount(e) - ); - // Retreive the next invoice number. - const autoNextNumber = this.getNextReceiptNumber(tenantId); - - // Retreive the receipt number. - const receiptNumber = - saleReceiptDTO.receiptNumber || - oldSaleReceipt?.receiptNumber || - autoNextNumber; - - // Validate receipt number require. - this.validateReceiptNoRequire(receiptNumber); - - const initialEntries = saleReceiptDTO.entries.map((entry) => ({ - reference_type: 'SaleReceipt', - ...entry, - })); - - const entries = await composeAsync( - // Sets default cost and sell account to receipt items entries. - this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId) - )(initialEntries); - - const initialDTO = { - amount, - ...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [ - 'receiptDate', - ]), - currencyCode: paymentCustomer.currencyCode, - exchangeRate: saleReceiptDTO.exchangeRate || 1, - receiptNumber, - // Avoid rewrite the deliver date in edit mode when already published. - ...(saleReceiptDTO.closed && - !oldSaleReceipt?.closedAt && { - closedAt: moment().toMySqlDateTime(), - }), - entries, - }; - return R.compose( - this.branchDTOTransform.transformDTO(tenantId), - this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); - } - - /** - * Creates a new sale receipt with associated entries. - * @async - * @param {ISaleReceipt} saleReceipt - * @return {Object} - */ - public async createSaleReceipt( - tenantId: number, - saleReceiptDTO: any - ): Promise { - const { SaleReceipt, Contact } = this.tenancy.models(tenantId); - - // Retireves the payment customer model. - const paymentCustomer = await Contact.query() - .modify('customer') - .findById(saleReceiptDTO.customerId) - .throwIfNotFound(); - - // Transform sale receipt DTO to model. - const saleReceiptObj = await this.transformDTOToModel( - tenantId, - saleReceiptDTO, - paymentCustomer - ); - // Validate receipt deposit account existance and type. - await this.validateReceiptDepositAccountExistance( - tenantId, - saleReceiptDTO.depositAccountId - ); - // Validate items IDs existance on the storage. - await this.itemsEntriesService.validateItemsIdsExistance( - tenantId, - saleReceiptDTO.entries - ); - // Validate the sellable items. - await this.itemsEntriesService.validateNonSellableEntriesItems( - tenantId, - saleReceiptDTO.entries - ); - // Validate sale receipt number uniuqiness. - if (saleReceiptDTO.receiptNumber) { - await this.validateReceiptNumberUnique( - tenantId, - saleReceiptDTO.receiptNumber - ); - } - // Creates a sale receipt transaction and associated transactions under UOW env. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleReceiptCreating` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onCreating, { - saleReceiptDTO, - tenantId, - trx, - } as ISaleReceiptCreatingPayload); - - // Inserts the sale receipt graph to the storage. - const saleReceipt = await SaleReceipt.query().upsertGraph({ - ...saleReceiptObj, - }); - // Triggers `onSaleReceiptCreated` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onCreated, { - tenantId, - saleReceipt, - saleReceiptId: saleReceipt.id, - trx, - } as ISaleReceiptCreatedPayload); - - return saleReceipt; - }); - } - - /** - * Edit details sale receipt with associated entries. - * @param {Integer} saleReceiptId - * @param {ISaleReceipt} saleReceipt - * @return {void} - */ - public async editSaleReceipt( - tenantId: number, - saleReceiptId: number, - saleReceiptDTO: any - ) { - const { SaleReceipt, Contact } = this.tenancy.models(tenantId); - - // Retrieve sale receipt or throw not found service error. - const oldSaleReceipt = await this.getSaleReceiptOrThrowError( - tenantId, - saleReceiptId - ); - // Retrieves the payment customer model. - const paymentCustomer = await Contact.query() - .findById(saleReceiptId) - .modify('customer') - .throwIfNotFound(); - - // Transform sale receipt DTO to model. - const saleReceiptObj = await this.transformDTOToModel( - tenantId, - saleReceiptDTO, - paymentCustomer, - oldSaleReceipt - ); - // Validate receipt deposit account existance and type. - await this.validateReceiptDepositAccountExistance( - tenantId, - saleReceiptDTO.depositAccountId - ); - // Validate items IDs existance on the storage. - await this.itemsEntriesService.validateItemsIdsExistance( - tenantId, - saleReceiptDTO.entries - ); - // Validate the sellable items. - await this.itemsEntriesService.validateNonSellableEntriesItems( - tenantId, - saleReceiptDTO.entries - ); - // Validate sale receipt number uniuqiness. - if (saleReceiptDTO.receiptNumber) { - await this.validateReceiptNumberUnique( - tenantId, - saleReceiptDTO.receiptNumber, - saleReceiptId - ); - } - // Edits the sale receipt tranasctions with associated transactions under UOW env. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleReceiptsEditing` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onEditing, { - tenantId, - oldSaleReceipt, - saleReceiptDTO, - trx, - } as ISaleReceiptEditingPayload); - - // Upsert the receipt graph to the storage. - const saleReceipt = await SaleReceipt.query(trx).upsertGraphAndFetch({ - id: saleReceiptId, - ...saleReceiptObj, - }); - // Triggers `onSaleReceiptEdited` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, { - tenantId, - oldSaleReceipt, - saleReceipt, - saleReceiptId, - trx, - } as ISaleReceiptEditedPayload); - - return saleReceipt; - }); - } - - /** - * Deletes the sale receipt with associated entries. - * @param {Integer} saleReceiptId - * @return {void} - */ - public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { - const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); - - const oldSaleReceipt = await this.getSaleReceiptOrThrowError( - tenantId, - saleReceiptId - ); - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleReceiptsDeleting` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onDeleting, { - trx, - oldSaleReceipt, - tenantId, - } as ISaleReceiptDeletingPayload); - - // - await ItemEntry.query(trx) - .where('reference_id', saleReceiptId) - .where('reference_type', 'SaleReceipt') - .delete(); - - // Delete the sale receipt transaction. - await SaleReceipt.query(trx).where('id', saleReceiptId).delete(); - - // Triggers `onSaleReceiptsDeleted` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onDeleted, { - tenantId, - saleReceiptId, - oldSaleReceipt, - trx, - } as ISaleReceiptEventDeletedPayload); - }); - } - - /** - * Retrieve sale receipt with associated entries. - * @param {Integer} saleReceiptId - * @return {ISaleReceipt} - */ - async getSaleReceipt(tenantId: number, saleReceiptId: number) { - const { SaleReceipt } = this.tenancy.models(tenantId); - - const saleReceipt = await SaleReceipt.query() - .findById(saleReceiptId) - .withGraphFetched('entries.item') - .withGraphFetched('customer') - .withGraphFetched('depositAccount') - .withGraphFetched('branch'); - - if (!saleReceipt) { - throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND); - } - return this.transformer.transform( - tenantId, - saleReceipt, - new SaleReceiptTransformer() - ); - } - - /** - * Parses the sale receipts list filter DTO. - * @param filterDTO - */ - private parseListFilterDTO(filterDTO) { - return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); - } - - /** - * Retrieve sales receipts paginated and filterable list. - * @param {number} tenantId - * @param {ISaleReceiptFilter} salesReceiptsFilter - */ - public async salesReceiptsList( - tenantId: number, - filterDTO: ISaleReceiptFilter - ): Promise<{ - data: ISaleReceipt[]; - pagination: IPaginationMeta; - filterMeta: IFilterMeta; - }> { - const { SaleReceipt } = this.tenancy.models(tenantId); - - // Parses the stringified filter roles. - const filter = this.parseListFilterDTO(filterDTO); - - // Dynamic list service. - const dynamicFilter = await this.dynamicListService.dynamicList( - tenantId, - SaleReceipt, - filter - ); - const { results, pagination } = await SaleReceipt.query() - .onBuild((builder) => { - builder.withGraphFetched('depositAccount'); - builder.withGraphFetched('customer'); - builder.withGraphFetched('entries'); - - dynamicFilter.buildQuery()(builder); - }) - .pagination(filter.page - 1, filter.pageSize); - - // Transformes the estimates models to POJO. - const salesEstimates = await this.transformer.transform( - tenantId, - results, - new SaleReceiptTransformer() - ); - return { - data: salesEstimates, - pagination, - filterMeta: dynamicFilter.getResponseMeta(), - }; - } - - /** - * Mark the given sale receipt as closed. - * @param {number} tenantId - * @param {number} saleReceiptId - * @return {Promise} - */ - async closeSaleReceipt( - tenantId: number, - saleReceiptId: number - ): Promise { - const { SaleReceipt } = this.tenancy.models(tenantId); - - // Retrieve sale receipt or throw not found service error. - const oldSaleReceipt = await this.getSaleReceiptOrThrowError( - tenantId, - saleReceiptId - ); - - // Throw service error if the sale receipt already closed. - if (oldSaleReceipt.isClosed) { - throw new ServiceError(ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED); - } - // Updates the sale recept transaction under unit-of-work envirement. - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onSaleReceiptClosing` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onClosing, { - tenantId, - oldSaleReceipt, - trx, - } as ISaleReceiptEventClosingPayload); - - // Mark the sale receipt as closed on the storage. - const saleReceipt = await SaleReceipt.query(trx) - .findById(saleReceiptId) - .patch({ - closedAt: moment().toMySqlDateTime(), - }); - - // Triggers `onSaleReceiptClosed` event. - await this.eventPublisher.emitAsync(events.saleReceipt.onClosed, { - saleReceiptId, - saleReceipt, - tenantId, - trx, - } as ISaleReceiptEventClosedPayload); - }); - } - /** - * Records the inventory transactions from the given bill input. - * @param {Bill} bill - Bill model object. - * @param {number} billId - Bill id. - * @return {Promise} - */ - public async recordInventoryTransactions( - tenantId: number, - saleReceipt: ISaleReceipt, - override?: boolean, - trx?: Knex.Transaction - ): Promise { - // Loads the inventory items entries of the given sale invoice. - const inventoryEntries = - await this.itemsEntriesService.filterInventoryEntries( - tenantId, - saleReceipt.entries - ); - const transaction = { - transactionId: saleReceipt.id, - transactionType: 'SaleReceipt', - transactionNumber: saleReceipt.receiptNumber, - exchangeRate: saleReceipt.exchangeRate, - - date: saleReceipt.receiptDate, - direction: 'OUT', - entries: inventoryEntries, - createdAt: saleReceipt.createdAt, - - warehouseId: saleReceipt.warehouseId, - }; - return this.inventoryService.recordInventoryTransactionsFromItemsEntries( - tenantId, - transaction, - override, - trx - ); - } - - /** - * Reverts the inventory transactions of the given bill id. - * @param {number} tenantId - Tenant id. - * @param {number} billId - Bill id. - * @return {Promise} - */ - public async revertInventoryTransactions( - tenantId: number, - receiptId: number, - trx?: Knex.Transaction - ) { - return this.inventoryService.deleteInventoryTransactions( - tenantId, - receiptId, - 'SaleReceipt', - trx - ); - } - - /** - * Validate the given customer has no sales receipts. - * @param {number} tenantId - * @param {number} customerId - Customer id. - */ - public async validateCustomerHasNoReceipts( - tenantId: number, - customerId: number - ) { - const { SaleReceipt } = this.tenancy.models(tenantId); - - const receipts = await SaleReceipt.query().where('customer_id', customerId); - - if (receipts.length > 0) { - throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES); - } - } -} diff --git a/packages/server/src/services/Settings/SmsNotificationsSettings.ts b/packages/server/src/services/Settings/SmsNotificationsSettings.ts index 7fd02541c..3061aa2ba 100644 --- a/packages/server/src/services/Settings/SmsNotificationsSettings.ts +++ b/packages/server/src/services/Settings/SmsNotificationsSettings.ts @@ -9,7 +9,6 @@ import { import TenancyService from '@/services/Tenancy/TenancyService'; import SMSNotificationsConfig from 'config/smsNotifications'; import { ServiceError } from '@/exceptions'; -import I18nService from '@/services/I18n/I18nService'; const ERRORS = { SMS_NOTIFICATION_KEY_NOT_FOUND: 'SMS_NOTIFICATION_KEY_NOT_FOUND', @@ -19,13 +18,7 @@ const ERRORS = { @Service() export default class SmsNotificationsSettingsService { @Inject() - tenancy: TenancyService; - - @Inject('logger') - logger: any; - - @Inject() - i18nService: I18nService; + private tenancy: TenancyService; /** * Retrieve sms notification meta from the given notification key. diff --git a/packages/server/src/subscribers/Bills/WriteInventoryTransactions.ts b/packages/server/src/subscribers/Bills/WriteInventoryTransactions.ts index 300d38e0f..ee04c98a1 100644 --- a/packages/server/src/subscribers/Bills/WriteInventoryTransactions.ts +++ b/packages/server/src/subscribers/Bills/WriteInventoryTransactions.ts @@ -1,20 +1,16 @@ import { Service, Inject } from 'typedi'; import events from '@/subscribers/events'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import BillsService from '@/services/Purchases/Bills'; import { IBillCreatedPayload, IBillEditedPayload, IBIllEventDeletedPayload, } from '@/interfaces'; +import { BillInventoryTransactions } from '@/services/Purchases/Bills/BillInventoryTransactions'; @Service() export default class BillWriteInventoryTransactionsSubscriber { @Inject() - tenancy: TenancyService; - - @Inject() - billsService: BillsService; + private billsInventory: BillInventoryTransactions; /** * Attaches events with handles. @@ -42,7 +38,7 @@ export default class BillWriteInventoryTransactionsSubscriber { billId, trx, }: IBillCreatedPayload) => { - await this.billsService.recordInventoryTransactions( + await this.billsInventory.recordInventoryTransactions( tenantId, billId, false, @@ -58,7 +54,7 @@ export default class BillWriteInventoryTransactionsSubscriber { billId, trx, }: IBillEditedPayload) => { - await this.billsService.recordInventoryTransactions( + await this.billsInventory.recordInventoryTransactions( tenantId, billId, true, @@ -74,6 +70,10 @@ export default class BillWriteInventoryTransactionsSubscriber { billId, trx, }: IBIllEventDeletedPayload) => { - await this.billsService.revertInventoryTransactions(tenantId, billId, trx); + await this.billsInventory.revertInventoryTransactions( + tenantId, + billId, + trx + ); }; } diff --git a/packages/server/src/subscribers/Inventory/Inventory.ts b/packages/server/src/subscribers/Inventory/Inventory.ts index 117cffd8f..80c917f1f 100644 --- a/packages/server/src/subscribers/Inventory/Inventory.ts +++ b/packages/server/src/subscribers/Inventory/Inventory.ts @@ -1,9 +1,6 @@ import { Inject, Service } from 'typedi'; import { map, head } from 'lodash'; - import events from '@/subscribers/events'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import SaleInvoicesCost from '@/services/Sales/SalesInvoicesCost'; import InventoryItemsQuantitySync from '@/services/Inventory/InventoryItemsQuantitySync'; import InventoryService from '@/services/Inventory/Inventory'; import { @@ -12,23 +9,21 @@ import { IInventoryTransactionsDeletedPayload, } from '@/interfaces'; import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks'; +import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost'; @Service() export default class InventorySubscriber { @Inject() - saleInvoicesCost: SaleInvoicesCost; + private saleInvoicesCost: SaleInvoicesCost; @Inject() - tenancy: TenancyService; + private itemsQuantitySync: InventoryItemsQuantitySync; @Inject() - itemsQuantitySync: InventoryItemsQuantitySync; - - @Inject() - inventoryService: InventoryService; + private inventoryService: InventoryService; @Inject('agenda') - agenda: any; + private agenda: any; /** * Attaches events with handlers. diff --git a/packages/server/src/subscribers/PaymentMades/PaymentSyncBillBalance.ts b/packages/server/src/subscribers/PaymentMades/PaymentSyncBillBalance.ts index 2b65f6a65..5a6ce8a72 100644 --- a/packages/server/src/subscribers/PaymentMades/PaymentSyncBillBalance.ts +++ b/packages/server/src/subscribers/PaymentMades/PaymentSyncBillBalance.ts @@ -1,7 +1,6 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; -import BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments'; -import TenancyService from '@/services/Tenancy/TenancyService'; +import { BillPaymentBillSync } from '@/services/Purchases/BillPayments/BillPaymentBillSync'; import { IBillPaymentEventCreatedPayload, IBillPaymentEventDeletedPayload, @@ -11,16 +10,13 @@ import { @Service() export default class PaymentSyncBillBalance { @Inject() - tenancy: TenancyService; - - @Inject() - billPaymentsService: BillPaymentsService; + private billPaymentsService: BillPaymentBillSync; /** * * @param bus */ - attach(bus) { + public attach(bus) { bus.subscribe( events.billPayment.onCreated, this.handleBillsIncrementPaymentAmount @@ -34,6 +30,7 @@ export default class PaymentSyncBillBalance { this.handleBillDecrementPaymentAmount ); } + /** * Handle bill payment amount increment/decrement once bill payment created or edited. */ diff --git a/packages/server/src/subscribers/PaymentReceive/AutoSerialIncrement.ts b/packages/server/src/subscribers/PaymentReceive/AutoSerialIncrement.ts index 0109b6c80..f8cdceba2 100644 --- a/packages/server/src/subscribers/PaymentReceive/AutoSerialIncrement.ts +++ b/packages/server/src/subscribers/PaymentReceive/AutoSerialIncrement.ts @@ -1,13 +1,13 @@ import { Service, Inject } from 'typedi'; import events from '@/subscribers/events'; import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher'; -import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives'; +import { PaymentReceiveIncrement } from '@/services/Sales/PaymentReceives/PaymentReceiveIncrement'; import { IPaymentReceiveCreatedPayload } from '@/interfaces'; @Service() export default class PaymentReceiveAutoSerialSubscriber extends EventSubscriber { @Inject() - paymentReceivesService: PaymentReceiveService; + private paymentIncrement: PaymentReceiveIncrement; /** * Attaches the events with handles. @@ -29,8 +29,6 @@ export default class PaymentReceiveAutoSerialSubscriber extends EventSubscriber paymentReceiveId, trx, }: IPaymentReceiveCreatedPayload) => { - await this.paymentReceivesService.incrementNextPaymentReceiveNumber( - tenantId - ); + await this.paymentIncrement.incrementNextPaymentReceiveNumber(tenantId); }; } diff --git a/packages/server/src/subscribers/PaymentReceive/PaymentReceiveSyncInvoices.ts b/packages/server/src/subscribers/PaymentReceive/PaymentReceiveSyncInvoices.ts index 5dcd14528..26ac16e14 100644 --- a/packages/server/src/subscribers/PaymentReceive/PaymentReceiveSyncInvoices.ts +++ b/packages/server/src/subscribers/PaymentReceive/PaymentReceiveSyncInvoices.ts @@ -1,6 +1,6 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; -import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives'; +import { PaymentReceiveInvoiceSync } from '@/services/Sales/PaymentReceives/PaymentReceiveInvoiceSync'; import { IPaymentReceiveCreatedPayload, IPaymentReceiveDeletedPayload, @@ -8,15 +8,15 @@ import { } from '@/interfaces'; @Service() -export default class PaymentReceiveSyncInvoices { +export default class PaymentReceiveSyncInvoicesSubscriber { @Inject() - paymentReceivesService: PaymentReceiveService; + private paymentSyncInvoice: PaymentReceiveInvoiceSync; /** * Attaches the events to handles. * @param bus */ - attach(bus) { + public attach(bus) { bus.subscribe( events.paymentReceive.onCreated, this.handleInvoiceIncrementPaymentOnceCreated @@ -41,7 +41,7 @@ export default class PaymentReceiveSyncInvoices { paymentReceive, trx, }: IPaymentReceiveCreatedPayload) => { - await this.paymentReceivesService.saveChangeInvoicePaymentAmount( + await this.paymentSyncInvoice.saveChangeInvoicePaymentAmount( tenantId, paymentReceive.entries, null, @@ -59,7 +59,7 @@ export default class PaymentReceiveSyncInvoices { oldPaymentReceive, trx, }: IPaymentReceiveEditedPayload) => { - await this.paymentReceivesService.saveChangeInvoicePaymentAmount( + await this.paymentSyncInvoice.saveChangeInvoicePaymentAmount( tenantId, paymentReceive.entries, oldPaymentReceive?.entries || null, @@ -76,7 +76,7 @@ export default class PaymentReceiveSyncInvoices { oldPaymentReceive, trx, }: IPaymentReceiveDeletedPayload) => { - await this.paymentReceivesService.saveChangeInvoicePaymentAmount( + await this.paymentSyncInvoice.saveChangeInvoicePaymentAmount( tenantId, oldPaymentReceive.entries.map((entry) => ({ ...entry, diff --git a/packages/server/src/subscribers/PaymentReceive/SendSmsNotificationToCustomer.ts b/packages/server/src/subscribers/PaymentReceive/SendSmsNotificationToCustomer.ts index 4520c01f2..988260856 100644 --- a/packages/server/src/subscribers/PaymentReceive/SendSmsNotificationToCustomer.ts +++ b/packages/server/src/subscribers/PaymentReceive/SendSmsNotificationToCustomer.ts @@ -1,13 +1,13 @@ import { Service, Inject } from 'typedi'; import events from '@/subscribers/events'; -import PaymentReceiveNotifyBySms from '@/services/Sales/PaymentReceives/PaymentReceiveSmsNotify'; +import { PaymentReceiveNotifyBySms } from '@/services/Sales/PaymentReceives/PaymentReceiveSmsNotify'; import { IPaymentReceiveCreatedPayload } from '@/interfaces'; import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks'; @Service() export default class SendSmsNotificationPaymentReceive { @Inject() - paymentReceiveSmsNotify: PaymentReceiveNotifyBySms; + private paymentReceiveSmsNotify: PaymentReceiveNotifyBySms; /** * Attach events. diff --git a/packages/server/src/subscribers/SaleEstimate/SmsNotifications.ts b/packages/server/src/subscribers/SaleEstimate/SmsNotifications.ts index a7475c7bc..1da8f501d 100644 --- a/packages/server/src/subscribers/SaleEstimate/SmsNotifications.ts +++ b/packages/server/src/subscribers/SaleEstimate/SmsNotifications.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; -import SaleEstimateNotifyBySms from '@/services/Sales/Estimates/SaleEstimateSmsNotify'; +import { SaleEstimateNotifyBySms } from '@/services/Sales/Estimates/SaleEstimateSmsNotify'; import { ISaleEstimateCreatedPayload } from '@/interfaces'; import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks'; @Service() export default class SaleEstimateSmsNotificationSubscriber { @Inject() - saleEstimateNotifyBySms: SaleEstimateNotifyBySms; + private saleEstimateNotifyBySms: SaleEstimateNotifyBySms; /** * Attaches events to handles.events.saleEstimate.onCreated diff --git a/packages/server/src/subscribers/SaleInvoices/AutoIncrementSerial.ts b/packages/server/src/subscribers/SaleInvoices/AutoIncrementSerial.ts index fd458738b..1c8764f6a 100644 --- a/packages/server/src/subscribers/SaleInvoices/AutoIncrementSerial.ts +++ b/packages/server/src/subscribers/SaleInvoices/AutoIncrementSerial.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; -import SaleInvoicesService from '@/services/Sales/SalesInvoices'; +import { SaleInvoiceIncrement } from '@/services/Sales/Invoices/SaleInvoiceIncrement'; import { ISaleInvoiceCreatedPayload } from '@/interfaces'; @Service() export default class SaleInvoiceAutoIncrementSubscriber extends EventSubscriber { @Inject() - saleInvoicesService: SaleInvoicesService; + private saleInvoicesService: SaleInvoiceIncrement; /** * Constructor method. diff --git a/packages/server/src/subscribers/SaleInvoices/ConvertFromEstimate.ts b/packages/server/src/subscribers/SaleInvoices/ConvertFromEstimate.ts index 913bcf3ab..e8cba71a2 100644 --- a/packages/server/src/subscribers/SaleInvoices/ConvertFromEstimate.ts +++ b/packages/server/src/subscribers/SaleInvoices/ConvertFromEstimate.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher'; -import events from '@/subscribers/events'; -import SaleEstimateService from '@/services/Sales/SalesEstimate'; +import { ConvertSaleEstimate } from '@/services/Sales/Estimates/ConvetSaleEstimate'; import { ISaleInvoiceCreatedPayload } from '@/interfaces'; +import events from '@/subscribers/events'; @Service() export default class SaleInvoiceConvertFromEstimateSubscriber extends EventSubscriber { @Inject() - saleEstimatesService: SaleEstimateService; + private convertEstimateToInvoiceService: ConvertSaleEstimate; /** * Constructor method. @@ -30,7 +30,7 @@ export default class SaleInvoiceConvertFromEstimateSubscriber extends EventSubsc trx, }: ISaleInvoiceCreatedPayload) => { if (saleInvoiceDTO.fromEstimateId) { - await this.saleEstimatesService.convertEstimateToInvoice( + await this.convertEstimateToInvoiceService.convertEstimateToInvoice( tenantId, saleInvoiceDTO.fromEstimateId, saleInvoiceId, diff --git a/packages/server/src/subscribers/SaleInvoices/SendSmsNotificationToCustomer.ts b/packages/server/src/subscribers/SaleInvoices/SendSmsNotificationToCustomer.ts index 35d4e3e16..94907a99d 100644 --- a/packages/server/src/subscribers/SaleInvoices/SendSmsNotificationToCustomer.ts +++ b/packages/server/src/subscribers/SaleInvoices/SendSmsNotificationToCustomer.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; -import SaleInvoiceNotifyBySms from '@/services/Sales/SaleInvoiceNotifyBySms'; +import { SaleInvoiceNotifyBySms } from '@/services/Sales/Invoices/SaleInvoiceNotifyBySms'; import { ISaleInvoiceCreatedPayload } from '@/interfaces'; import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks'; @Service() export default class SendSmsNotificationToCustomer { @Inject() - saleInvoiceNotifyBySms: SaleInvoiceNotifyBySms; + private saleInvoiceNotifyBySms: SaleInvoiceNotifyBySms; /** * Attaches events with handlers. diff --git a/packages/server/src/subscribers/SaleInvoices/WriteInventoryTransactions.ts b/packages/server/src/subscribers/SaleInvoices/WriteInventoryTransactions.ts index 7d64a146f..b4f620b20 100644 --- a/packages/server/src/subscribers/SaleInvoices/WriteInventoryTransactions.ts +++ b/packages/server/src/subscribers/SaleInvoices/WriteInventoryTransactions.ts @@ -1,20 +1,16 @@ import { Service, Inject } from 'typedi'; import events from '@/subscribers/events'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import SaleInvoicesService from '@/services/Sales/SalesInvoices'; import { ISaleInvoiceCreatedPayload, ISaleInvoiceDeletedPayload, ISaleInvoiceEditedPayload, } from '@/interfaces'; +import { InvoiceInventoryTransactions } from '@/services/Sales/Invoices/InvoiceInventoryTransactions'; @Service() export default class WriteInventoryTransactions { @Inject() - tenancy: TenancyService; - - @Inject() - saleInvoicesService: SaleInvoicesService; + private saleInvoiceInventory: InvoiceInventoryTransactions; /** * Attaches events with handles @@ -43,7 +39,7 @@ export default class WriteInventoryTransactions { saleInvoice, trx, }: ISaleInvoiceCreatedPayload) => { - await this.saleInvoicesService.recordInventoryTranscactions( + await this.saleInvoiceInventory.recordInventoryTranscactions( tenantId, saleInvoice, false, @@ -53,14 +49,14 @@ export default class WriteInventoryTransactions { /** * Rewriting the inventory transactions once the sale invoice be edited. - * @param {ISaleInvoiceEditPayload} payload - + * @param {ISaleInvoiceEditPayload} payload - */ private handleRewritingInventoryTransactions = async ({ tenantId, saleInvoice, trx, }: ISaleInvoiceEditedPayload) => { - await this.saleInvoicesService.recordInventoryTranscactions( + await this.saleInvoiceInventory.recordInventoryTranscactions( tenantId, saleInvoice, true, @@ -70,7 +66,7 @@ export default class WriteInventoryTransactions { /** * Handles deleting the inventory transactions once the invoice deleted. - * @param {ISaleInvoiceDeletedPayload} payload - + * @param {ISaleInvoiceDeletedPayload} payload - */ private handleDeletingInventoryTransactions = async ({ tenantId, @@ -78,7 +74,7 @@ export default class WriteInventoryTransactions { oldSaleInvoice, trx, }: ISaleInvoiceDeletedPayload) => { - await this.saleInvoicesService.revertInventoryTransactions( + await this.saleInvoiceInventory.revertInventoryTransactions( tenantId, saleInvoiceId, trx diff --git a/packages/server/src/subscribers/SaleReceipt/AutoIncrementSerial.ts b/packages/server/src/subscribers/SaleReceipt/AutoIncrementSerial.ts index d2b712a8a..934690fe9 100644 --- a/packages/server/src/subscribers/SaleReceipt/AutoIncrementSerial.ts +++ b/packages/server/src/subscribers/SaleReceipt/AutoIncrementSerial.ts @@ -1,12 +1,12 @@ import { Service, Inject } from 'typedi'; import events from '@/subscribers/events'; -import SalesReceiptService from '@/services/Sales/SalesReceipts'; +import { SaleReceiptIncrement } from '@/services/Sales/Receipts/SaleReceiptIncrement'; import { ISaleReceiptCreatedPayload } from '@/interfaces'; @Service() export default class SaleReceiptAutoSerialSubscriber { @Inject() - saleReceiptsService: SalesReceiptService; + private saleReceiptsService: SaleReceiptIncrement; /** * diff --git a/packages/server/src/subscribers/SaleReceipt/SendSmsNotificationToCustomer.ts b/packages/server/src/subscribers/SaleReceipt/SendSmsNotificationToCustomer.ts index 536adb6a1..41aa53c4d 100644 --- a/packages/server/src/subscribers/SaleReceipt/SendSmsNotificationToCustomer.ts +++ b/packages/server/src/subscribers/SaleReceipt/SendSmsNotificationToCustomer.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; -import SaleReceiptNotifyBySms from '@/services/Sales/SaleReceiptNotifyBySms'; +import { SaleReceiptNotifyBySms } from '@/services/Sales/Receipts/SaleReceiptNotifyBySms'; import { ISaleReceiptCreatedPayload } from '@/interfaces'; import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks'; @Service() export default class SendSmsNotificationSaleReceipt { @Inject() - saleReceiptNotifyBySms: SaleReceiptNotifyBySms; + private saleReceiptNotifyBySms: SaleReceiptNotifyBySms; /** * Attaches events with handlers. diff --git a/packages/server/src/subscribers/SaleReceipt/WriteInventoryTransactions.ts b/packages/server/src/subscribers/SaleReceipt/WriteInventoryTransactions.ts index 1d224372c..57da23747 100644 --- a/packages/server/src/subscribers/SaleReceipt/WriteInventoryTransactions.ts +++ b/packages/server/src/subscribers/SaleReceipt/WriteInventoryTransactions.ts @@ -1,26 +1,22 @@ import { Inject } from 'typedi'; import { EventSubscriber } from 'event-dispatch'; import events from '@/subscribers/events'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import SalesReceiptService from '@/services/Sales/SalesReceipts'; import { ISaleReceiptCreatedPayload, ISaleReceiptEditedPayload, ISaleReceiptEventDeletedPayload, } from '@/interfaces'; +import { SaleReceiptInventoryTransactions } from '@/services/Sales/Receipts/SaleReceiptInventoryTransactions'; @EventSubscriber() export default class SaleReceiptInventoryTransactionsSubscriber { @Inject() - tenancy: TenancyService; - - @Inject() - saleReceiptsService: SalesReceiptService; + private saleReceiptInventory: SaleReceiptInventoryTransactions; /** * Subscribe events to handles. */ - attach(bus) { + public attach(bus) { bus.subscribe( events.saleReceipt.onCreated, this.handleWritingInventoryTransactions @@ -44,7 +40,7 @@ export default class SaleReceiptInventoryTransactionsSubscriber { saleReceipt, trx, }: ISaleReceiptCreatedPayload) => { - await this.saleReceiptsService.recordInventoryTransactions( + await this.saleReceiptInventory.recordInventoryTransactions( tenantId, saleReceipt, false, @@ -61,7 +57,7 @@ export default class SaleReceiptInventoryTransactionsSubscriber { saleReceipt, trx, }: ISaleReceiptEditedPayload) => { - await this.saleReceiptsService.recordInventoryTransactions( + await this.saleReceiptInventory.recordInventoryTransactions( tenantId, saleReceipt, true, @@ -78,7 +74,7 @@ export default class SaleReceiptInventoryTransactionsSubscriber { saleReceiptId, trx, }: ISaleReceiptEventDeletedPayload) => { - await this.saleReceiptsService.revertInventoryTransactions( + await this.saleReceiptInventory.revertInventoryTransactions( tenantId, saleReceiptId, trx diff --git a/packages/server/src/subscribers/SaleReceipt/WriteJournalEntries.ts b/packages/server/src/subscribers/SaleReceipt/WriteJournalEntries.ts index d505a3841..8a14bfcad 100644 --- a/packages/server/src/subscribers/SaleReceipt/WriteJournalEntries.ts +++ b/packages/server/src/subscribers/SaleReceipt/WriteJournalEntries.ts @@ -1,21 +1,17 @@ import { Service, Inject } from 'typedi'; import events from '@/subscribers/events'; import TenancyService from '@/services/Tenancy/TenancyService'; -import SalesReceiptService from '@/services/Sales/SalesReceipts'; import { ISaleReceiptCreatedPayload, ISaleReceiptEditedPayload, ISaleReceiptEventDeletedPayload, } from '@/interfaces'; -import { SaleReceiptGLEntries } from '@/services/Sales/SaleReceiptGLEntries'; +import { SaleReceiptGLEntries } from '@/services/Sales/Receipts/SaleReceiptGLEntries'; @Service() export default class SaleReceiptWriteGLEntriesSubscriber { @Inject() - tenancy: TenancyService; - - @Inject() - saleReceiptGLEntries: SaleReceiptGLEntries; + private saleReceiptGLEntries: SaleReceiptGLEntries; /** * Attaches events with handlers.