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