This commit is contained in:
elforjani3
2020-10-17 15:25:57 +02:00
59 changed files with 2538 additions and 1301 deletions

View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
function ErrorBoundary({
error,
errorInfo,
children
}) {
if (errorInfo) {
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{error && error.toString()}
<br />
{errorInfo.componentStack}
</details>
</div>
);
}
return children;
}
ErrorBoundary.defaultProps = {
children: null,
};
ErrorBoundary.propTypes = {
children: PropTypes.node,
};
export default ErrorBoundary;

View File

@@ -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,

View File

@@ -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",

View File

@@ -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 }],
});
}
}
}
}

View File

@@ -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 }],
});
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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 }],
});
}
}
}
};

View File

@@ -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 -

View File

@@ -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);
}
}
/**

View File

@@ -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);
}
}
/**

View File

@@ -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))
);

View File

@@ -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());

View File

@@ -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();

View File

@@ -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();
});
};

View File

@@ -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([

View File

@@ -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);

View File

@@ -29,6 +29,7 @@ export interface IAccountsFilter extends IDynamicListFilterDTO {
export interface IAccountType {
id: number,
key: string,
label: string,
normal: string,
rootType: string,
childType: string,

View File

@@ -1,3 +1,44 @@
export interface IBillOTD {};
export interface IBill {};
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[],
};

View File

@@ -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 {};
export interface IBillPaymentEntryDTO {
billId: number,
paymentAmount: number,
};
export interface IBillPaymentDTO {
vendorId: number,
paymentAccountId: number,
paymentNumber: string,
paymentDate: Date,
description: string,
reference: string,
entries: IBillPaymentEntryDTO[],
};

View File

@@ -11,4 +11,8 @@ export interface IItemEntry {
discount: number,
quantity: number,
rate: number,
}
export interface IItemEntryDTO {
}

View File

@@ -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<void>;

View File

@@ -1,4 +1,25 @@
import { IItemEntry } from "./ItemEntry";
export interface ISaleEstimate {};
export interface ISaleEstimateOTD {};
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,
};

View File

@@ -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{

View File

@@ -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';
export * from './Media';
export * from './SaleEstimate';

View File

@@ -268,4 +268,31 @@ export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRo
return filterRoles.filter((filterRole: IFilterRole) => {
return !validateFieldKeyExistance(model, filterRole.fieldKey);
});
}
}
/**
* 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,
};
})
}

View File

@@ -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';
import 'subscribers/expenses';
import 'subscribers/bills';
// import 'subscribers/saleInvoices';
import 'subscribers/customers';
import 'subscribers/vendors';
import 'subscribers/paymentMades';

View File

@@ -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"
}

View File

@@ -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'

View File

@@ -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',
};
}
}

View File

@@ -121,4 +121,13 @@ export default class Contact extends TenantModel {
}
return Promise.all(asyncOpers);
}
static get fields() {
return {
created_at: {
column: 'created_at',
}
};
}
}

View File

@@ -70,13 +70,24 @@ export default class AccountTypeRepository extends TenantRepository {
* @param {string} rootType
* @return {IAccountType[]}
*/
getByRootType(rootType: string): IAccountType[] {
getByRootType(rootType: string): Promise<IAccountType[]> {
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<IAccountType[]> {
const { AccountType } = this.models;
return this.cache.get(`accountType.childType.${childType}`, () => {
return AccountType.query().where('child_type', childType);
});
}
/**
* Flush repository cache.
*/

View File

@@ -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<void>}
*/
async deleteById(contactId: number): Promise<void> {
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();
}

View File

@@ -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,
) {
}
}

View File

@@ -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<IView> {
const { View } = this.models;
const insertedView = await View.query().insertGraph({ ...view });
this.flushCache();
return insertedView;
}
async update(viewId: number, view: IView): Promise<IView> {
const { View } = this.models;
const updatedView = await View.query().upsertGraph({
id: viewId,
...view
});
this.flushCache();
return updatedView;
}
/**
* Flushes repository cache.

View File

@@ -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 {

View File

@@ -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<IAccountType>}
*/
getAccountsTypes(tenantId: number): Promise<IAccountType> {
async getAccountsTypes(tenantId: number): Promise<IAccountType> {
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']),
}));
}
}

View File

@@ -26,7 +26,7 @@ export default class ContactsService {
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
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 });

View File

@@ -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<void>}
* @return {Promise<ICustomer>}
*/
public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
public async newCustomer(
tenantId: number,
customerDTO: ICustomerNewDTO
): Promise<ICustomer> {
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<ICustomer>}
*/
public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
public async editCustomer(
tenantId: number,
customerId: number,
customerDTO: ICustomerEditDTO,
): Promise<ICustomer> {
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<void>}
*/
public async deleteCustomer(tenantId: number, customerId: number) {
public async deleteCustomer(tenantId: number, customerId: number): Promise<void> {
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) {
private async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) {
const { customerRepository } = this.tenancy.repositories(tenantId);
const customersWithInvoices = await customerRepository.customersWithSalesInvoices(

View File

@@ -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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
async deleteBulkVendors(tenantId: number, vendorsIds: number[]) {
public async deleteBulkVendors(
tenantId: number,
vendorsIds: number[]
): Promise<void> {
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(),
};
}
}

View File

@@ -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<void> {
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(),
]);

View File

@@ -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 });
}
}

View File

@@ -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;

View File

@@ -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<IAccountType>}
*/
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<IBillPayment>}
*/
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<IBillPayment> {
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<any>[] = [];
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<IBillPayment> {
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<void>}
*/
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);
}
}

View File

@@ -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<IBill>}
*/
async createBill(tenantId: number, billDTO: IBillOTD) {
const { Vendor, Bill, ItemEntry } = this.tenancy.models(tenantId);
public async createBill(
tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
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<IBill>}
*/
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<IBill> {
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');
}
/**

View File

@@ -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];
}
}

View File

@@ -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()
]);
}
}

View File

@@ -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<any> = [];
@@ -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);
}
/**

View File

@@ -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<ISaleEstimate>}
*/
async createEstimate(tenantId: number, estimateDTO: any) {
async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
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<void> {
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);
}
/**

View File

@@ -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<ISaleInvoice> {
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);
}
/**

View File

@@ -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<any> = [];
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);
}
/**

View File

@@ -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<IView[]> {
this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel });
public async listResourceViews(tenantId: number, resourceModelName: string): Promise<IView[]> {
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<IView>}
*/
public async newView(tenantId: number, viewDTO: IViewDTO): Promise<IView> {
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<void> {
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 });
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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',
}
}

View File

@@ -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,
);
}
}

View File

@@ -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) {
}
}

View File

@@ -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,
);
}
}