diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index 8f512fa09..f1cbef385 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -1,11 +1,12 @@ import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, matchedData } from 'express-validator'; import { Inject, Service } from 'typedi'; -import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces'; +import { ISaleEstimateDTO } from 'interfaces'; import BaseController from 'api/controllers/BaseController' import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleEstimateService from 'services/Sales/SalesEstimate'; -import ItemsService from 'services/Items/ItemsService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { ServiceError } from "exceptions"; @Service() export default class SalesEstimatesController extends BaseController { @@ -13,7 +14,7 @@ export default class SalesEstimatesController extends BaseController { saleEstimateService: SaleEstimateService; @Inject() - itemsService: ItemsService; + dynamicListService: DynamicListingService; /** * Router constructor. @@ -25,10 +26,8 @@ export default class SalesEstimatesController extends BaseController { '/', this.estimateValidationSchema, this.validationResult, - // asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), - // asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), - // asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), - asyncMiddleware(this.newEstimate.bind(this)) + asyncMiddleware(this.newEstimate.bind(this)), + this.handleServiceErrors, ); router.post( '/:id', [ @@ -36,33 +35,31 @@ export default class SalesEstimatesController extends BaseController { ...this.estimateValidationSchema, ], this.validationResult, - // asyncMiddleware(this.validateEstimateIdExistance.bind(this)), - // asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), - // asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), - // asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), - // asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)), - asyncMiddleware(this.editEstimate.bind(this)) + asyncMiddleware(this.editEstimate.bind(this)), + this.handleServiceErrors, ); router.delete( '/:id', [ this.validateSpecificEstimateSchema, ], this.validationResult, - // asyncMiddleware(this.validateEstimateIdExistance.bind(this)), - asyncMiddleware(this.deleteEstimate.bind(this)) + asyncMiddleware(this.deleteEstimate.bind(this)), + this.handleServiceErrors, ); router.get( '/:id', this.validateSpecificEstimateSchema, this.validationResult, - // asyncMiddleware(this.validateEstimateIdExistance.bind(this)), - asyncMiddleware(this.getEstimate.bind(this)) + asyncMiddleware(this.getEstimate.bind(this)), + this.handleServiceErrors, ); router.get( '/', this.validateEstimateListSchema, this.validationResult, - asyncMiddleware(this.getEstimates.bind(this)) + asyncMiddleware(this.getEstimates.bind(this)), + this.handleServiceErrors, + this.dynamicListService.handlerErrorsToResponse, ); return router; } @@ -120,16 +117,17 @@ export default class SalesEstimatesController extends BaseController { * @param {Response} res - * @return {Response} res - */ - async newEstimate(req: Request, res: Response) { + async newEstimate(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const estimateOTD: ISaleEstimateOTD = matchedData(req, { - locations: ['body'], - includeOptionals: true, - }); - const storedEstimate = await this.saleEstimateService - .createEstimate(tenantId, estimateOTD); + const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); - return res.status(200).send({ id: storedEstimate.id }); + try { + const storedEstimate = await this.saleEstimateService.createEstimate(tenantId, estimateDTO); + + return res.status(200).send({ id: storedEstimate.id }); + } catch (error) { + next(error); + } } /** @@ -137,18 +135,19 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async editEstimate(req: Request, res: Response) { + async editEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; const { tenantId } = req; + const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); - const estimateOTD: ISaleEstimateOTD = matchedData(req, { - locations: ['body'], - includeOptionals: true, - }); - // Update estimate with associated estimate entries. - await this.saleEstimateService.editEstimate(tenantId, estimateId, estimateOTD); + try { + // Update estimate with associated estimate entries. + await this.saleEstimateService.editEstimate(tenantId, estimateId, estimateDTO); - return res.status(200).send({ id: estimateId }); + return res.status(200).send({ id: estimateId }); + } catch (error) { + next(error); + } } /** @@ -156,26 +155,32 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async deleteEstimate(req: Request, res: Response) { + async deleteEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; const { tenantId } = req; - await this.saleEstimateService.deleteEstimate(tenantId, estimateId); + try { + await this.saleEstimateService.deleteEstimate(tenantId, estimateId); - return res.status(200).send({ id: estimateId }); + return res.status(200).send({ id: estimateId }); + } catch (error) { + next(error); + } } /** * Retrieve the given estimate with associated entries. */ - async getEstimate(req: Request, res: Response) { + async getEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; const { tenantId } = req; - const estimate = await this.saleEstimateService - .getEstimateWithEntries(tenantId, estimateId); - - return res.status(200).send({ estimate }); + try { + const estimate = await this.saleEstimateService.getEstimate(tenantId, estimateId); + return res.status(200).send({ estimate }); + } catch (error) { + next(error); + } } /** @@ -185,12 +190,25 @@ export default class SalesEstimatesController extends BaseController { */ async getEstimates(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const estimatesFilter: ISalesEstimatesFilter = this.matchedQueryData(req); + const filter = { + filterRoles: [], + sortOrder: 'asc', + columnSortBy: 'created_at', + page: 1, + pageSize: 12, + ...this.matchedQueryData(req), + }; + if (filter.stringifiedFilterRoles) { + filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); + } try { - const { salesEstimates, pagination, filterMeta } = await this.saleEstimateService - .estimatesList(tenantId, estimatesFilter); - + const { + salesEstimates, + pagination, + filterMeta + } = await this.saleEstimateService.estimatesList(tenantId, filter); + return res.status(200).send({ sales_estimates: this.transfromToResponse(salesEstimates), pagination, @@ -200,4 +218,52 @@ export default class SalesEstimatesController extends BaseController { next(error); } } + + /** + * Handles service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'ITEMS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }], + }); + } + if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }], + }); + } + if (error.errorType === 'ITEMS_IDS_NOT_EXISTS') { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 200 }], + }); + } + if (error.errorType === 'NOT_PURCHASE_ABLE_ITEMS') { + return res.boom.badRequest(null, { + errors: [{ type: 'NOT_PURCHASABLE_ITEMS', code: 200 }], + }); + } + if (error.errorType === 'SALE_ESTIMATE_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'SALE_ESTIMATE_NOT_FOUND', code: 200 }], + }); + } + if (error.errorType === 'CUSTOMER_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 200 }], + }); + } + if (error.errorType === 'SALE_ESTIMATE_NUMBER_EXISTANCE') { + return res.boom.badRequest(null, { + errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }], + }); + } + } + next(error); + } }; diff --git a/server/src/api/controllers/Sales/index.ts b/server/src/api/controllers/Sales/index.ts index af92ee890..a447d0420 100644 --- a/server/src/api/controllers/Sales/index.ts +++ b/server/src/api/controllers/Sales/index.ts @@ -13,10 +13,10 @@ export default class SalesController { router() { const router = express.Router(); - router.use('/invoices', Container.get(SalesInvoices).router()); + // router.use('/invoices', Container.get(SalesInvoices).router()); router.use('/estimates', Container.get(SalesEstimates).router()); - router.use('/receipts', Container.get(SalesReceipts).router()); - router.use('/payment_receives', Container.get(PaymentReceives).router()); + // router.use('/receipts', Container.get(SalesReceipts).router()); + // router.use('/payment_receives', Container.get(PaymentReceives).router()); return router; } diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 69bebb022..ed8286ca1 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -29,7 +29,7 @@ import Settings from 'api/controllers/Settings'; import Currencies from 'api/controllers/Currencies'; import Customers from 'api/controllers/Contacts/Customers'; import Vendors from 'api/controllers/Contacts/Vendors'; -// import Sales from 'api/controllers/Sales' +import Sales from 'api/controllers/Sales' import Purchases from 'api/controllers/Purchases'; import Resources from './controllers/Resources'; import ExchangeRates from 'api/controllers/ExchangeRates'; @@ -94,7 +94,7 @@ export default () => { dashboard.use('/financial_statements', FinancialStatements.router()); dashboard.use('/customers', Container.get(Customers).router()); dashboard.use('/vendors', Container.get(Vendors).router()); - // dashboard.use('/sales', Container.get(Sales).router()); + dashboard.use('/sales', Container.get(Sales).router()); dashboard.use('/purchases', Container.get(Purchases).router()); dashboard.use('/resources', Container.get(Resources).router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); diff --git a/server/src/interfaces/SaleEstimate.ts b/server/src/interfaces/SaleEstimate.ts index 0f623bb93..edef799c0 100644 --- a/server/src/interfaces/SaleEstimate.ts +++ b/server/src/interfaces/SaleEstimate.ts @@ -1,5 +1,5 @@ import { IItemEntry } from "./ItemEntry"; - +import { IDynamicListFilterDTO } from 'interfaces/DynamicFilter'; export interface ISaleEstimate { id?: number, @@ -22,4 +22,8 @@ export interface ISaleEstimateDTO { entries: IItemEntry[], note: string, termsConditions: string, -}; \ No newline at end of file +}; + +export interface ISalesEstimatesFilter extends IDynamicListFilterDTO { + stringifiedFilterRoles?: string, +} \ No newline at end of file diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index 375682193..838da1694 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -32,7 +32,7 @@ export default class SaleEstimate extends TenantModel { to: 'contacts.id', }, filter(query) { - query.where('contact_type', 'Customer'); + query.where('contact_service', 'customer'); } }, @@ -49,4 +49,17 @@ export default class SaleEstimate extends TenantModel { }, }; } + + /** + * Model defined fields. + */ + static get fields() { + return { + created_at: { + label: 'Created at', + column: 'created_at', + columnType: 'date', + }, + }; + } } diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index b98356a60..8aee2c55f 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -192,7 +192,7 @@ export default class CustomersService { * @param {number} tenantId * @param {number} customerId */ - private getCustomerByIdOrThrowError(tenantId: number, customerId: number) { + public getCustomerByIdOrThrowError(tenantId: number, customerId: number) { return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer'); } diff --git a/server/src/services/Items/ItemsEntriesService.ts b/server/src/services/Items/ItemsEntriesService.ts new file mode 100644 index 000000000..7516a6c27 --- /dev/null +++ b/server/src/services/Items/ItemsEntriesService.ts @@ -0,0 +1,104 @@ +import { difference } from 'lodash'; +import { Inject, Service } from 'typedi'; +import { + IItemEntry, + IItemEntryDTO, + IItem, +} from 'interfaces'; +import { ServiceError } from 'exceptions'; +import TenancyService from 'services/Tenancy/TenancyService'; + +const ERRORS = { + ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', + ENTRIES_IDS_NOT_FOUND: 'ENTRIES_IDS_NOT_FOUND', + NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS', +}; + +@Service() +export default class ItemsEntriesService { + @Inject() + tenancy: TenancyService; + + /** + * Validates the entries items ids. + * @async + * @param {number} tenantId - + * @param {IItemEntryDTO} itemEntries - + */ + public async validateItemsIdsExistance(tenantId: number, itemEntries: IItemEntryDTO[]) { + const { Item } = this.tenancy.models(tenantId); + const itemsIds = itemEntries.map((e) => e.itemId); + + const foundItems = await Item.query().whereIn('id', itemsIds); + + const foundItemsIds = foundItems.map((item: IItem) => item.id); + const notFoundItemsIds = difference(itemsIds, foundItemsIds); + + if (notFoundItemsIds.length > 0) { + throw new ServiceError(ERRORS.ITEMS_NOT_FOUND); + } + } + + /** + * Validates the entries ids existance on the storage. + * @param {number} tenantId - + * @param {number} billId - + * @param {IItemEntry[]} billEntries - + */ + public async validateEntriesIdsExistance(tenantId: number, billId: number, modelName: string, billEntries: IItemEntryDTO[]) { + const { ItemEntry } = this.tenancy.models(tenantId); + const entriesIds = billEntries + .filter((e: IItemEntry) => e.id) + .map((e: IItemEntry) => e.id); + + const storedEntries = await ItemEntry.query() + .whereIn('reference_id', [billId]) + .whereIn('reference_type', [modelName]); + + const storedEntriesIds = storedEntries.map((entry) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_FOUND) + } + } + + + /** + * Validate the entries items that not purchase-able. + */ + public async validateNonPurchasableEntriesItems(tenantId: number, itemEntries: IItemEntryDTO[]) { + const { Item } = this.tenancy.models(tenantId); + const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId); + + const purchasbleItems = await Item.query() + .where('purchasable', true) + .whereIn('id', itemsIds); + + const purchasbleItemsIds = purchasbleItems.map((item: IItem) => item.id); + const notPurchasableItems = difference(itemsIds, purchasbleItemsIds); + + if (notPurchasableItems.length > 0) { + throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS); + } + } + + /** + * Validate the entries items that not sell-able. + */ + public async validateNonSellableEntriesItems(tenantId: number, itemEntries: IItemEntryDTO[]) { + const { Item } = this.tenancy.models(tenantId); + const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId); + + const sellableItems = await Item.query() + .where('sellable', true) + .whereIn('id', itemsIds); + + const sellableItemsIds = sellableItems.map((item: IItem) => item.id); + const nonSellableItems = difference(itemsIds, sellableItemsIds); + + if (nonSellableItems.length > 0) { + throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS); + } + } +} \ No newline at end of file diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index eb3be857c..1afdeb915 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -19,8 +19,6 @@ import { IBill, IItem, ISystemUser, - IItemEntry, - IItemEntryDTO, IBillEditDTO, IPaginationMeta, IFilterMeta, @@ -28,6 +26,7 @@ import { } from 'interfaces'; import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; +import ItemsEntriesService from 'services/Items/ItemsEntriesService'; const ERRORS = { BILL_NOT_FOUND: 'BILL_NOT_FOUND', @@ -66,6 +65,9 @@ export default class BillsService extends SalesInvoicesCost { @Inject() dynamicListService: DynamicListingService; + @Inject() + itemsEntriesService: ItemsEntriesService; + /** * Validates whether the vendor is exist. * @async @@ -105,27 +107,6 @@ export default class BillsService extends SalesInvoicesCost { return foundBill; } - /** - * Validates the entries items ids. - * @async - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - private async validateItemsIdsExistance(tenantId: number, billEntries: IItemEntryDTO[]) { - const { Item } = this.tenancy.models(tenantId); - const itemsIds = billEntries.map((e) => e.itemId); - - const foundItems = await Item.query().whereIn('id', itemsIds); - - const foundItemsIds = foundItems.map((item: IItem) => item.id); - const notFoundItemsIds = difference(itemsIds, foundItemsIds); - - if (notFoundItemsIds.length > 0) { - throw new ServiceError(ERRORS.BILL_ITEMS_NOT_FOUND); - } - } - /** * Validates the bill number existance. * @async @@ -146,52 +127,6 @@ export default class BillsService extends SalesInvoicesCost { } } - /** - * Validates the entries ids existance on the storage. - * @param {number} tenantId - - * @param {number} billId - - * @param {IItemEntry[]} billEntries - - */ - private async validateEntriesIdsExistance(tenantId: number, billId: number, billEntries: IItemEntryDTO[]) { - const { ItemEntry } = this.tenancy.models(tenantId); - const entriesIds = billEntries - .filter((e: IItemEntry) => e.id) - .map((e: IItemEntry) => e.id); - - const storedEntries = await ItemEntry.query() - .whereIn('reference_id', [billId]) - .whereIn('reference_type', ['Bill']); - - const storedEntriesIds = storedEntries.map((entry) => entry.id); - const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); - - if (notFoundEntriesIds.length > 0) { - throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND) - } - } - - /** - * Validate the entries items that not purchase-able. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - private async validateNonPurchasableEntriesItems(tenantId: number, billEntries: IItemEntryDTO[]) { - const { Item } = this.tenancy.models(tenantId); - const itemsIds = billEntries.map((e: IItemEntryDTO) => e.itemId); - - const purchasbleItems = await Item.query() - .where('purchasable', true) - .whereIn('id', itemsIds); - - const purchasbleItemsIds = purchasbleItems.map((item: IItem) => item.id); - const notPurchasableItems = difference(itemsIds, purchasbleItemsIds); - - if (notPurchasableItems.length > 0) { - throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS); - } - } - /** * Converts bill DTO to model. * @param {number} tenantId @@ -249,8 +184,8 @@ export default class BillsService extends SalesInvoicesCost { await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.validateBillNumberExists(tenantId, billDTO.billNumber); - await this.validateItemsIdsExistance(tenantId, billDTO.entries); - await this.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); + await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries); + await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); const bill = await Bill.query() .insertGraph({ @@ -302,9 +237,9 @@ export default class BillsService extends SalesInvoicesCost { await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId); - await this.validateEntriesIdsExistance(tenantId, billId, billDTO.entries); - await this.validateItemsIdsExistance(tenantId, billDTO.entries); - await this.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); + await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, billId, 'Bill', billDTO.entries); + await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries); + await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); // Update the bill transaction. const bill = await Bill.query().upsertGraph({ diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index 44526fb46..b2c4f03bd 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -1,16 +1,25 @@ -import { omit, difference, sumBy, mixin } from 'lodash'; +import { omit, sumBy } from 'lodash'; import { Service, Inject } from 'typedi'; import { IEstimatesFilter, IFilterMeta, IPaginationMeta, ISaleEstimate, ISaleEstimateDTO } from 'interfaces'; import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; -import HasItemsEntries from 'services/Sales/HasItemsEntries'; import { formatDateFields } from 'utils'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import events from 'subscribers/events'; +import { ServiceError } from 'exceptions'; +import CustomersService from 'services/Contacts/CustomersService'; + +const ERRORS = { + SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND', + CUSTOMER_NOT_FOUND: 'CUSTOMER_NOT_FOUND', + SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE', + ITEMS_IDS_NOT_EXISTS: 'ITEMS_IDS_NOT_EXISTS', +} /** * Sale estimate service. * @Service @@ -21,7 +30,10 @@ export default class SaleEstimateService { tenancy: TenancyService; @Inject() - itemsEntriesService: HasItemsEntries; + itemsEntriesService: ItemsEntriesService; + + @Inject() + customersService: CustomersService; @Inject('logger') logger: any; @@ -31,120 +43,41 @@ export default class SaleEstimateService { @EventDispatcher() eventDispatcher: EventDispatcherInterface; - /** - * Validate whether the estimate customer exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * Retrieve sale estimate or throw service error. + * @param {number} tenantId + * @return {ISaleEstimate} */ - async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) { - const estimate = { ...req.body }; - const { Customer } = req.models - - const foundCustomer = await Customer.query().findById(estimate.customer_id); - - if (!foundCustomer) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }], - }); + 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); } - next(); + return foundSaleEstimate; } - + /** * Validate the estimate number unique on the storage. * @param {Request} req * @param {Response} res * @param {Function} next */ - async validateEstimateNumberExistance(req: Request, res: Response, next: Function) { - const estimate = { ...req.body }; - const { tenantId } = req; + async validateEstimateNumberExistance(tenantId: number, estimateNumber: string, notEstimateId?: number) { + const { SaleEstimate } = this.tenancy.models(tenantId); - const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique( - tenantId, - estimate.estimate_number, - req.params.id, - ); - if (isEstNumberUnqiue) { - return res.boom.badRequest(null, { - errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }], + 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); } - next(); - } - - /** - * Validate the estimate entries items ids existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) { - const tenantId = req.tenantId; - const estimate = { ...req.body }; - const estimateItemsIds = estimate.entries.map(e => e.item_id); - - // Validate items ids in estimate entries exists. - const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds); - - if (notFoundItemsIds.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }], - }); - } - next(); - } - - /** - * Validate whether the sale estimate id exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateEstimateIdExistance(req: Request, res: Response, next: Function) { - const { id: estimateId } = req.params; - const { tenantId } = req; - - const storedEstimate = await this.saleEstimateService - .getEstimate(tenantId, estimateId); - - if (!storedEstimate) { - return res.status(404).send({ - errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }], - }); - } - next(); - } - - /** - * Validate sale invoice entries ids existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) { - const { ItemEntry } = req.models; - - const { id: saleInvoiceId } = req.params; - const saleInvoice = { ...req.body }; - const entriesIds = saleInvoice.entries - .filter(e => e.id) - .map((e) => e.id); - - const foundEntries = await ItemEntry.query() - .whereIn('id', entriesIds) - .where('reference_type', 'SaleInvoice') - .where('reference_id', saleInvoiceId); - - if (foundEntries.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }], - }); - } - next(); } /** @@ -154,22 +87,27 @@ export default class SaleEstimateService { * @param {EstimateDTO} estimate * @return {Promise} */ - async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise { + public async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise { const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); + this.logger.info('[sale_estimate] inserting sale estimate to the storage.'); + const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e)); - const estimate = { + const estimateObj = { amount, ...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']), }; + await this.validateEstimateNumberExistance(tenantId, estimateDTO.estimateNumber); + await this.customersService.getCustomer(tenantId, estimateDTO.customerId); - this.logger.info('[sale_estimate] inserting sale estimate to the storage.'); - const storedEstimate = await SaleEstimate.query() - .insert({ - ...omit(estimate, ['entries']), - entries: estimate.entries.map((entry) => ({ + await this.itemsEntriesService.validateItemsIdsExistance(tenantId, estimateDTO.entries); + await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, estimateDTO.entries); + + const saleEstimate = await SaleEstimate.query() + .upsertGraph({ + ...omit(estimateObj, ['entries']), + entries: estimateObj.entries.map((entry) => ({ reference_type: 'SaleEstimate', - reference_id: storedEstimate.id, ...omit(entry, ['total', 'amount', 'id']), })) }); @@ -177,7 +115,7 @@ export default class SaleEstimateService { this.logger.info('[sale_estimate] insert sale estimated success.'); await this.eventDispatcher.dispatch(events.saleEstimates.onCreated); - return storedEstimate; + return saleEstimate; } /** @@ -188,31 +126,36 @@ export default class SaleEstimateService { * @param {EstimateDTO} estimate * @return {void} */ - async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise { + public async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise { const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); - const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e)); + const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId); - const estimate = { + const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e)); + const estimateObj = { amount, ...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']), }; - this.logger.info('[sale_estimate] editing sale estimate on the storage.'); - const updatedEstimate = await SaleEstimate.query() - .update({ - ...omit(estimate, ['entries']), - }); - const storedEstimateEntries = await ItemEntry.query() - .where('reference_id', estimateId) - .where('reference_type', 'SaleEstimate'); + await this.validateEstimateNumberExistance(tenantId, estimateDTO.estimateNumber, estimateId); + await this.customersService.getCustomer(tenantId, estimateDTO.customerId); - await this.itemsEntriesService.patchItemsEntries( - tenantId, - estimate.entries, - storedEstimateEntries, - 'SaleEstimate', - estimateId, - ); - await this.eventDispatcher.dispatch(events.saleEstimates.onEdited); + await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, estimateId, 'SaleEstiamte', estimateDTO.entries); + await this.itemsEntriesService.validateItemsIdsExistance(tenantId, estimateDTO.entries); + await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, estimateDTO.entries); + + this.logger.info('[sale_estimate] editing sale estimate on the storage.'); + const saleEstimate = await SaleEstimate.query() + .upsertGraph({ + id: estimateId, + ...omit(estimateObj, ['entries']), + entries: estimateObj.entries.map((entry) => ({ + ...omit(entry, ['total', 'amount']), + })), + }); + + await this.eventDispatcher.dispatch(events.saleEstimates.onEdited, { + tenantId, estimateId, saleEstimate, oldSaleEstimate, + }); + return saleEstimate; } /** @@ -222,9 +165,11 @@ export default class SaleEstimateService { * @param {IEstimate} estimateId * @return {void} */ - async deleteEstimate(tenantId: number, estimateId: number) { + public async deleteEstimate(tenantId: number, estimateId: number): Promise { const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); + const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId); + this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.'); await ItemEntry.query() .where('reference_id', estimateId) @@ -234,63 +179,9 @@ export default class SaleEstimateService { await SaleEstimate.query().where('id', estimateId).delete(); this.logger.info('[sale_estimate] deleted successfully.', { tenantId, estimateId }); - await this.eventDispatcher.dispatch(events.saleEstimates.onDeleted); - } - - /** - * Validates the given estimate ID exists. - * @async - * @param {number} tenantId - The tenant id. - * @param {Numeric} estimateId - * @return {Boolean} - */ - async isEstimateExists(tenantId: number, estimateId: number) { - const { SaleEstimate } = this.tenancy.models(tenantId); - const foundEstimate = await SaleEstimate.query().where('id', estimateId); - - return foundEstimate.length !== 0; - } - - /** - * Validates the given estimate entries IDs. - * @async - * @param {number} tenantId - The tenant id. - * @param {Numeric} estimateId - the sale estimate id. - * @param {IEstimate} estimate - */ - async isEstimateEntriesIDsExists(tenantId: number, estimateId: number, estimate: any) { - const { ItemEntry } = this.tenancy.models(tenantId); - const estimateEntriesIds = estimate.entries - .filter((e: any) => e.id) - .map((e: any) => e.id); - - const estimateEntries = await ItemEntry.query() - .whereIn('id', estimateEntriesIds) - .where('reference_id', estimateId) - .where('reference_type', 'SaleEstimate'); - - const storedEstimateEntriesIds = estimateEntries.map((e: any) => e.id); - const notFoundEntriesIDs = difference( - estimateEntriesIds, - storedEstimateEntriesIds - ); - return notFoundEntriesIDs; - } - - /** - * Retrieve the estimate details of the given estimate id. - * @async - * @param {number} tenantId - The tenant id. - * @param {Integer} estimateId - * @return {IEstimate} - */ - async getEstimate(tenantId: number, estimateId: number) { - const { SaleEstimate } = this.tenancy.models(tenantId); - const estimate = await SaleEstimate.query() - .where('id', estimateId) - .first(); - - return estimate; + await this.eventDispatcher.dispatch(events.saleEstimates.onDeleted, { + tenantId, saleEstimateId: estimateId, oldSaleEstimate, + }); } /** @@ -299,42 +190,23 @@ export default class SaleEstimateService { * @param {number} tenantId - The tenant id. * @param {Integer} estimateId */ - async getEstimateWithEntries(tenantId: number, estimateId: number) { + public async getEstimate(tenantId: number, estimateId: number) { const { SaleEstimate } = this.tenancy.models(tenantId); const estimate = await SaleEstimate.query() - .where('id', estimateId) + .findById(estimateId) .withGraphFetched('entries') - .withGraphFetched('customer') - .first(); - + .withGraphFetched('customer'); + + if (!estimate) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND); + } return estimate; } - /** - * Detarmines the estimate number uniqness. - * @async - * @param {number} tenantId - The tenant id. - * @param {String} estimateNumber - * @param {Integer} excludeEstimateId - * @return {Boolean} - */ - async isEstimateNumberUnique(tenantId: number, estimateNumber: string, excludeEstimateId: number) { - const { SaleEstimate } = this.tenancy.models(tenantId); - const foundEstimates = await SaleEstimate.query() - .onBuild((query: any) => { - query.where('estimate_number', estimateNumber); - if (excludeEstimateId) { - query.whereNot('id', excludeEstimateId); - } - return query; - }); - return foundEstimates.length > 0; - } - /** * Retrieves estimates filterable and paginated list. - * @param {number} tenantId - * @param {IEstimatesFilter} estimatesFilter + * @param {number} tenantId - + * @param {IEstimatesFilter} estimatesFilter - */ public async estimatesList( tenantId: number,