refactoring: bills service.

refactoring: bills payments made service.
This commit is contained in:
Ahmed Bouhuolia
2020-10-15 15:10:41 +02:00
parent 8713c77289
commit 899ea7a52d
39 changed files with 2192 additions and 1193 deletions

View File

@@ -24,7 +24,8 @@ export default class CustomersController extends ContactsController {
...this.customerDTOSchema, ...this.customerDTOSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.newCustomer.bind(this)) asyncMiddleware(this.newCustomer.bind(this)),
this.handlerServiceErrors
); );
router.post('/:id', [ router.post('/:id', [
...this.contactDTOSchema, ...this.contactDTOSchema,
@@ -32,19 +33,22 @@ export default class CustomersController extends ContactsController {
...this.customerDTOSchema, ...this.customerDTOSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editCustomer.bind(this)) asyncMiddleware(this.editCustomer.bind(this)),
this.handlerServiceErrors,
); );
router.delete('/:id', [ router.delete('/:id', [
...this.specificContactSchema, ...this.specificContactSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteCustomer.bind(this)) asyncMiddleware(this.deleteCustomer.bind(this)),
this.handlerServiceErrors,
); );
router.delete('/', [ router.delete('/', [
...this.bulkContactsSchema, ...this.bulkContactsSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteBulkCustomers.bind(this)) asyncMiddleware(this.deleteBulkCustomers.bind(this)),
this.handlerServiceErrors,
); );
router.get('/', [ router.get('/', [
@@ -56,7 +60,8 @@ export default class CustomersController extends ContactsController {
...this.specificContactSchema, ...this.specificContactSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.getCustomer.bind(this)) asyncMiddleware(this.getCustomer.bind(this)),
this.handlerServiceErrors
); );
return router; return router;
} }
@@ -104,13 +109,6 @@ export default class CustomersController extends ContactsController {
await this.customersService.editCustomer(tenantId, contactId, contactDTO); await this.customersService.editCustomer(tenantId, contactId, contactDTO);
return res.status(200).send({ id: contactId }); return res.status(200).send({ id: contactId });
} catch (error) { } 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); next(error);
} }
} }
@@ -129,18 +127,6 @@ export default class CustomersController extends ContactsController {
await this.customersService.deleteCustomer(tenantId, contactId) await this.customersService.deleteCustomer(tenantId, contactId)
return res.status(200).send({ id: contactId }); return res.status(200).send({ id: contactId });
} catch (error) { } 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); next(error);
} }
} }
@@ -159,13 +145,6 @@ export default class CustomersController extends ContactsController {
const contact = await this.customersService.getCustomer(tenantId, contactId) const contact = await this.customersService.getCustomer(tenantId, contactId)
return res.status(200).send({ contact }); return res.status(200).send({ contact });
} catch (error) { } 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); next(error);
} }
} }
@@ -184,18 +163,6 @@ export default class CustomersController extends ContactsController {
await this.customersService.deleteBulkCustomers(tenantId, contactsIds) await this.customersService.deleteBulkCustomers(tenantId, contactsIds)
return res.status(200).send({ ids: contactsIds }); return res.status(200).send({ ids: contactsIds });
} catch (error) { } 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); next(error);
} }
} }
@@ -210,4 +177,31 @@ export default class CustomersController extends ContactsController {
next(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.vendorDTOSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.newVendor.bind(this)) asyncMiddleware(this.newVendor.bind(this)),
this.handlerServiceErrors,
); );
router.post('/:id', [ router.post('/:id', [
...this.contactDTOSchema, ...this.contactDTOSchema,
@@ -32,25 +33,29 @@ export default class VendorsController extends ContactsController {
...this.vendorDTOSchema, ...this.vendorDTOSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editVendor.bind(this)) asyncMiddleware(this.editVendor.bind(this)),
this.handlerServiceErrors,
); );
router.delete('/:id', [ router.delete('/:id', [
...this.specificContactSchema, ...this.specificContactSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteVendor.bind(this)) asyncMiddleware(this.deleteVendor.bind(this)),
this.handlerServiceErrors,
); );
router.delete('/', [ router.delete('/', [
...this.bulkContactsSchema, ...this.bulkContactsSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteBulkVendors.bind(this)) asyncMiddleware(this.deleteBulkVendors.bind(this)),
this.handlerServiceErrors,
); );
router.get('/:id', [ router.get('/:id', [
...this.specificContactSchema, ...this.specificContactSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.getVendor.bind(this)) asyncMiddleware(this.getVendor.bind(this)),
this.handlerServiceErrors,
); );
router.get('/', [ router.get('/', [
...this.vendorsListSchema, ...this.vendorsListSchema,
@@ -99,8 +104,8 @@ export default class VendorsController extends ContactsController {
const { tenantId } = req; const { tenantId } = req;
try { try {
const contact = await this.vendorsService.newVendor(tenantId, contactDTO); const vendor = await this.vendorsService.newVendor(tenantId, contactDTO);
return res.status(200).send({ id: contact.id }); return res.status(200).send({ id: vendor.id });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -121,13 +126,6 @@ export default class VendorsController extends ContactsController {
await this.vendorsService.editVendor(tenantId, contactId, contactDTO); await this.vendorsService.editVendor(tenantId, contactId, contactDTO);
return res.status(200).send({ id: contactId }); return res.status(200).send({ id: contactId });
} catch (error) { } 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); next(error);
} }
} }
@@ -146,18 +144,6 @@ export default class VendorsController extends ContactsController {
await this.vendorsService.deleteVendor(tenantId, contactId) await this.vendorsService.deleteVendor(tenantId, contactId)
return res.status(200).send({ id: contactId }); return res.status(200).send({ id: contactId });
} catch (error) { } 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); next(error);
} }
} }
@@ -176,13 +162,6 @@ export default class VendorsController extends ContactsController {
const vendor = await this.vendorsService.getVendor(tenantId, vendorId) const vendor = await this.vendorsService.getVendor(tenantId, vendorId)
return res.status(200).send({ vendor }); return res.status(200).send({ vendor });
} catch (error) { } 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); next(error);
} }
} }
@@ -201,18 +180,6 @@ export default class VendorsController extends ContactsController {
await this.vendorsService.deleteBulkVendors(tenantId, contactsIds) await this.vendorsService.deleteBulkVendors(tenantId, contactsIds)
return res.status(200).send({ ids: contactsIds }); return res.status(200).send({ ids: contactsIds });
} catch (error) { } 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); next(error);
} }
} }
@@ -236,4 +203,31 @@ export default class VendorsController extends ContactsController {
next(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 { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator'; import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { difference } from 'lodash'; import { IBillDTO, IBillEditDTO } from 'interfaces';
import { BillOTD } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BillsService from 'services/Purchases/Bills'; import BillsService from 'services/Purchases/Bills';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
@Service() @Service()
export default class BillsController extends BaseController { export default class BillsController extends BaseController {
@@ -27,45 +27,49 @@ export default class BillsController extends BaseController {
const router = Router(); const router = Router();
router.post( router.post(
'/', '/', [
[...this.billValidationSchema], ...this.billValidationSchema
],
this.validationResult, this.validationResult,
asyncMiddleware(this.validateVendorExistance.bind(this)), asyncMiddleware(this.newBill.bind(this)),
asyncMiddleware(this.validateItemsIds.bind(this)), this.handleServiceError,
asyncMiddleware(this.validateBillNumberExists.bind(this)),
asyncMiddleware(this.validateNonPurchasableEntriesItems.bind(this)),
asyncMiddleware(this.newBill.bind(this))
); );
router.post( router.post(
'/:id', '/:id', [
[...this.billValidationSchema, ...this.specificBillValidationSchema], ...this.billValidationSchema,
...this.specificBillValidationSchema,
],
this.validationResult, 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)) asyncMiddleware(this.editBill.bind(this))
); );
router.get( router.get(
'/:id', '/:id', [
[...this.specificBillValidationSchema], ...this.specificBillValidationSchema
],
this.validationResult, this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)), asyncMiddleware(this.getBill.bind(this)),
asyncMiddleware(this.getBill.bind(this))
);
router.get(
'/',
[...this.billsListingValidationSchema],
this.validationResult,
asyncMiddleware(this.listingBills.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( router.delete(
'/:id', '/:id',
[...this.specificBillValidationSchema], [...this.specificBillValidationSchema],
this.validationResult, this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)), asyncMiddleware(this.deleteBill.bind(this)),
asyncMiddleware(this.deleteBill.bind(this)) this.handleServiceError,
); );
return router; 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. * Bill validation schema.
*/ */
@@ -112,162 +138,23 @@ export default class BillsController extends BaseController {
query('sort_order').optional().isIn(['desc', 'asc']), 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. * Creates a new bill and records journal transactions.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async newBill(req: Request, res: Response, next: Function) { async newBill(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId, user } = req;
const { ItemEntry } = req.models; const billDTO: IBillDTO = this.matchedBodyData(req);
const billOTD: BillOTD = matchedData(req, { try {
locations: ['body'], const storedBill = await this.billsService.createBill(tenantId, billDTO, user);
includeOptionals: true return res.status(200).send({ id: storedBill.id });
}); } catch (error) {
const storedBill = await this.billsService.createBill(tenantId, billOTD); next(error);
}
return res.status(200).send({ id: storedBill.id });
} }
/** /**
@@ -275,18 +162,17 @@ export default class BillsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async editBill(req: Request, res: Response) { async editBill(req: Request, res: Response, next: NextFunction) {
const { id: billId } = req.params; const { id: billId } = req.params;
const { ItemEntry } = req.models;
const { tenantId } = req; const { tenantId } = req;
const billDTO: IBillEditDTO = this.matchedBodyData(req);
const billOTD: BillOTD = matchedData(req, { try {
locations: ['body'], const editedBill = await this.billsService.editBill(tenantId, billId, billDTO);
includeOptionals: true return res.status(200).send({ id: billId });
}); } catch (error) {
const editedBill = await this.billsService.editBill(tenantId, billId, billOTD); next(error);
}
return res.status(200).send({ id: billId });
} }
/** /**
@@ -295,13 +181,17 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async getBill(req: Request, res: Response) { async getBill(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: billId } = req.params; 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 - * @param {Response} res -
* @return {Response} * @return {Response}
*/ */
async deleteBill(req: Request, res: Response) { async deleteBill(req: Request, res: Response, next: NextFunction) {
const billId = req.params.id; const billId = req.params.id;
const { tenantId } = req; const { tenantId } = req;
await this.billsService.deleteBill(tenantId, billId); try {
await this.billsService.deleteBill(tenantId, billId);
return res.status(200).send({ id: 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 - * @param {Response} res -
* @return {Response} * @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 { Service, Inject } from 'typedi';
import { check, param, query, ValidationChain, matchedData } from 'express-validator'; import { check, param, query, ValidationChain } from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import validateMiddleware from 'api/middleware/validateMiddleware'; import { ServiceError } from 'exceptions';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import BillPaymentsService from 'services/Purchases/BillPayments'; import BillPaymentsService from 'services/Purchases/BillPayments';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
@@ -31,43 +30,34 @@ export default class BillsPayments extends BaseController {
...this.billPaymentSchemaValidation, ...this.billPaymentSchemaValidation,
], ],
this.validationResult, 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)), asyncMiddleware(this.createBillPayment.bind(this)),
this.handleServiceError,
); );
router.post('/:id', [ router.post('/:id', [
...this.billPaymentSchemaValidation, ...this.billPaymentSchemaValidation,
...this.specificBillPaymentValidateSchema, ...this.specificBillPaymentValidateSchema,
], ],
this.validationResult, 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)), asyncMiddleware(this.editBillPayment.bind(this)),
this.handleServiceError,
) )
router.delete('/:id', router.delete('/:id', [
this.specificBillPaymentValidateSchema, ...this.specificBillPaymentValidateSchema,
],
this.validationResult, this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
asyncMiddleware(this.deleteBillPayment.bind(this)), asyncMiddleware(this.deleteBillPayment.bind(this)),
this.handleServiceError,
); );
router.get('/:id', // router.get('/:id',
this.specificBillPaymentValidateSchema, // this.specificBillPaymentValidateSchema,
this.validationResult, // this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)), // asyncMiddleware(this.getBillPayment.bind(this)),
asyncMiddleware(this.getBillPayment.bind(this)), // );
); // router.get('/',
router.get('/', // this.listingValidationSchema,
this.listingValidationSchema, // this.validationResult,
this.validationResult, // asyncMiddleware(this.getBillsPayments.bind(this))
asyncMiddleware(this.getBillsPayments.bind(this)) // );
);
return router; 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. * Creates a bill payment.
* @async * @async
@@ -299,17 +109,21 @@ export default class BillsPayments extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Response} res * @param {Response} res
*/ */
async createBillPayment(req: Request, res: Response) { async createBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const billPaymentDTO = this.matchedBodyData(req);
const billPayment = matchedData(req, { try {
locations: ['body'], const billPayment = await this.billPaymentService.createBillPayment(tenantId, billPaymentDTO);
includeOptionals: true,
});
const storedPayment = await this.billPaymentService
.createBillPayment(tenantId, billPayment);
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 {Request} req
* @param {Response} res * @param {Response} res
*/ */
async editBillPayment(req: Request, res: Response) { async editBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const billPaymentDTO = this.matchedBodyData(req);
const billPayment = matchedData(req, {
locations: ['body'],
includeOptionals: true,
});
const { id: billPaymentId } = req.params; const { id: billPaymentId } = req.params;
const { BillPayment } = req.models;
const oldBillPayment = await BillPayment.query() try {
.where('id', billPaymentId) const paymentMade = await this.billPaymentService.editBillPayment(
.withGraphFetched('entries') tenantId,
.first(); billPaymentId,
billPaymentDTO
await this.billPaymentService.editBillPayment( );
tenantId, return res.status(200).send({
billPaymentId, id: paymentMade.id,
billPayment, message: 'Payment made has been edited successfully.',
oldBillPayment, });
); } catch (error) {
return res.status(200).send({ id: 1 }); next(error);
}
} }
/** /**
@@ -348,15 +158,20 @@ export default class BillsPayments extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} res - * @return {Response} res -
*/ */
async deleteBillPayment(req: Request, res: Response) { async deleteBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: billPaymentId } = req.params; 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 {Request} req
* @param {Response} res * @param {Response} res
*/ */
async getBillPayment(req: Request, res: Response) { async getBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: billPaymentId } = req.params; const { id: billPaymentId } = req.params;
@@ -380,7 +195,7 @@ export default class BillsPayments extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} * @return {Response}
*/ */
async getBillsPayments(req: Request, res: Response) { async getBillsPayments(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req.params; const { tenantId } = req.params;
const billPaymentsFilter = this.matchedQueryData(req); const billPaymentsFilter = this.matchedQueryData(req);
@@ -397,4 +212,68 @@ export default class BillsPayments extends BaseController {
next(error); 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 { Container, Service } from 'typedi';
import Bills from 'api/controllers/Purchases/Bills' import Bills from 'api/controllers/Purchases/Bills'
import BillPayments from 'api/controllers/Purchases/BillsPayments'; import BillPayments from 'api/controllers/Purchases/BillsPayments';
@@ -7,7 +7,7 @@ import BillPayments from 'api/controllers/Purchases/BillsPayments';
export default class PurchasesController { export default class PurchasesController {
router() { router() {
const router = express.Router(); const router = Router();
router.use('/bills', Container.get(Bills).router()); router.use('/bills', Container.get(Bills).router());
router.use('/bill_payments', Container.get(BillPayments).router()); router.use('/bill_payments', Container.get(BillPayments).router());

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 { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces'; import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces';
@@ -25,9 +25,9 @@ export default class SalesEstimatesController extends BaseController {
'/', '/',
this.estimateValidationSchema, this.estimateValidationSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), // asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), // asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), // asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
asyncMiddleware(this.newEstimate.bind(this)) asyncMiddleware(this.newEstimate.bind(this))
); );
router.post( router.post(
@@ -36,11 +36,11 @@ export default class SalesEstimatesController extends BaseController {
...this.estimateValidationSchema, ...this.estimateValidationSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)), // asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), // asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), // asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), // asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)), // asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
asyncMiddleware(this.editEstimate.bind(this)) asyncMiddleware(this.editEstimate.bind(this))
); );
router.delete( router.delete(
@@ -48,14 +48,14 @@ export default class SalesEstimatesController extends BaseController {
this.validateSpecificEstimateSchema, this.validateSpecificEstimateSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)), // asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.deleteEstimate.bind(this)) asyncMiddleware(this.deleteEstimate.bind(this))
); );
router.get( router.get(
'/:id', '/:id',
this.validateSpecificEstimateSchema, this.validateSpecificEstimateSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)), // asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.getEstimate.bind(this)) asyncMiddleware(this.getEstimate.bind(this))
); );
router.get( 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. * Handle create a new estimate with associated entries.
* @param {Request} req - * @param {Request} req -

View File

@@ -32,7 +32,7 @@ export default class SaleInvoicesController extends BaseController{
this.saleInvoiceValidationSchema, this.saleInvoiceValidationSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), // asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)), asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)), asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)),
asyncMiddleware(this.newSaleInvoice.bind(this)) asyncMiddleware(this.newSaleInvoice.bind(this))
@@ -46,7 +46,7 @@ export default class SaleInvoicesController extends BaseController{
this.validationResult, this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), // asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)), asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)), asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)), asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
@@ -312,18 +312,19 @@ export default class SaleInvoicesController extends BaseController{
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async newSaleInvoice(req: Request, res: Response) { async newSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, { const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req);
locations: ['body'],
includeOptionals: true
});
// Creates a new sale invoice with associated entries. try {
const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice( // Creates a new sale invoice with associated entries.
tenantId, saleInvoiceOTD, const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice(
); tenantId, saleInvoiceOTD,
return res.status(200).send({ id: storedSaleInvoice.id }); );
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 {Response} res
* @param {Function} next * @param {Function} next
*/ */
async editSaleInvoice(req: Request, res: Response) { async editSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req);
const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, { try {
locations: ['body'], // Update the given sale invoice details.
includeOptionals: true await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD);
}); return res.status(200).send({ id: saleInvoiceId });
// Update the given sale invoice details. } catch (error) {
await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD); next(error);
}
return res.status(200).send({ id: saleInvoiceId });
} }
/** /**
@@ -352,14 +353,18 @@ export default class SaleInvoicesController extends BaseController{
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async deleteSaleInvoice(req: Request, res: Response) { async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
const { tenantId } = req; const { tenantId } = req;
// Deletes the sale invoice with associated entries and journal transaction. try {
await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId); // Deletes the sale invoice with associated entries and journal transaction.
await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId);
return res.status(200).send({ id: 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 { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator'; import { check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import SaleReceiptService from 'services/Sales/SalesReceipts'; import SaleReceiptService from 'services/Sales/SalesReceipts';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { ISaleReceiptDTO } from 'interfaces/SaleReceipt';
@Service() @Service()
export default class SalesReceiptsController extends BaseController{ export default class SalesReceiptsController extends BaseController{
@@ -232,20 +233,21 @@ export default class SalesReceiptsController extends BaseController{
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async newSaleReceipt(req: Request, res: Response) { async newSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req);
const saleReceipt = matchedData(req, { try {
locations: ['body'], // Store the given sale receipt details with associated entries.
includeOptionals: true, const storedSaleReceipt = await this.saleReceiptService
}); .createSaleReceipt(
// Store the given sale receipt details with associated entries. tenantId,
const storedSaleReceipt = await this.saleReceiptService saleReceiptDTO,
.createSaleReceipt( );
tenantId, return res.status(200).send({ id: storedSaleReceipt.id });
saleReceipt, } catch (error) {
); next(error);
return res.status(200).send({ id: storedSaleReceipt.id }); }
} }
/** /**
@@ -253,14 +255,18 @@ export default class SalesReceiptsController extends BaseController{
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async deleteSaleReceipt(req: Request, res: Response) { async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: saleReceiptId } = req.params; const { id: saleReceiptId } = req.params;
// Deletes the sale receipt. try {
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId); // Deletes the sale receipt.
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
return res.status(200).send({ id: 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 {Request} req
* @param {Response} res * @param {Response} res
*/ */
async editSaleReceipt(req: Request, res: Response) { async editSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: saleReceiptId } = req.params; const { id: saleReceiptId } = req.params;
const saleReceipt = { ...req.body }; 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

@@ -29,7 +29,7 @@ import Settings from 'api/controllers/Settings';
import Currencies from 'api/controllers/Currencies'; import Currencies from 'api/controllers/Currencies';
import Customers from 'api/controllers/Contacts/Customers'; import Customers from 'api/controllers/Contacts/Customers';
import Vendors from 'api/controllers/Contacts/Vendors'; 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 Purchases from 'api/controllers/Purchases';
import Resources from './controllers/Resources'; import Resources from './controllers/Resources';
import ExchangeRates from 'api/controllers/ExchangeRates'; import ExchangeRates from 'api/controllers/ExchangeRates';
@@ -95,7 +95,7 @@ export default () => {
dashboard.use('/customers', Container.get(Customers).router()); dashboard.use('/customers', Container.get(Customers).router());
dashboard.use('/vendors', Container.get(Vendors).router()); dashboard.use('/vendors', Container.get(Vendors).router());
// dashboard.use('/sales', Container.get(Sales).router()); // dashboard.use('/sales', Container.get(Sales).router());
// dashboard.use('/purchases', Container.get(Purchases).router()); dashboard.use('/purchases', Container.get(Purchases).router());
dashboard.use('/resources', Container.get(Resources).router()); dashboard.use('/resources', Container.get(Resources).router());
dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router());
dashboard.use('/media', Container.get(Media).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('reference_id').index();
table.integer('account_id').unsigned().index().references('id').inTable('accounts'); table.integer('account_id').unsigned().index().references('id').inTable('accounts');
table.string('contact_type').nullable().index(); 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.string('note');
table.boolean('draft').defaultTo(false); table.boolean('draft').defaultTo(false);
table.integer('user_id').unsigned().index(); 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.decimal('payment_amount', 13, 3).defaultTo(0);
table.string('inv_lot_number').index(); table.string('inv_lot_number').index();
table.integer('user_id').unsigned();
table.timestamps(); table.timestamps();
}); });
}; };

View File

@@ -1,5 +1,5 @@
import Container from 'typedi'; import Container from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService' import TenancyService from 'services/Tenancy/TenancyService';
exports.up = (knex) => { exports.up = (knex) => {
const tenancyService = Container.get(TenancyService); const tenancyService = Container.get(TenancyService);

View File

@@ -1,3 +1,44 @@
export interface IBillOTD {}; import { IItemEntry, IItemEntryDTO } from "./ItemEntry";
export interface IBill {};
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 { export interface IBillPaymentEntry {
id?: number,
billPaymentId: number,
billId: number, billId: number,
paymentAmount: number, paymentAmount: number,
}; };
export interface IBillPayment { export interface IBillPayment {
id?: number,
vendorId: number,
amount: number, amount: number,
reference: string, reference: string,
billNo: string, paymentAccountId: number,
paymentNumber: string,
paymentDate: Date,
userId: number,
entries: IBillPaymentEntry[], 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, discount: number,
quantity: number, quantity: number,
rate: number, rate: number,
}
export interface IItemEntryDTO {
} }

View File

@@ -1,4 +1,25 @@
import { IItemEntry } from "./ItemEntry";
export interface ISaleEstimate {}; export interface ISaleEstimate {
export interface ISaleEstimateOTD {}; 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 { export interface ISaleInvoice {
id: number, id: number,
@@ -5,7 +6,7 @@ export interface ISaleInvoice {
paymentAmount: number, paymentAmount: number,
invoiceDate: Date, invoiceDate: Date,
dueDate: Date, dueDate: Date,
entries: any[], entries: IItemEntry[],
} }
export interface ISaleInvoiceOTD { export interface ISaleInvoiceOTD {
@@ -14,7 +15,7 @@ export interface ISaleInvoiceOTD {
referenceNo: string, referenceNo: string,
invoiceMessage: string, invoiceMessage: string,
termsConditions: string, termsConditions: string,
entries: any[], entries: IItemEntryDTO[],
} }
export interface ISalesInvoicesFilter{ export interface ISalesInvoicesFilter{

View File

@@ -2,6 +2,7 @@
export * from './Model'; export * from './Model';
export * from './InventoryTransaction'; export * from './InventoryTransaction';
export * from './BillPayment'; export * from './BillPayment';
export * from './Bill';
export * from './InventoryCostMethod'; export * from './InventoryCostMethod';
export * from './ItemEntry'; export * from './ItemEntry';
export * from './Item'; export * from './Item';
@@ -25,4 +26,5 @@ export * from './View';
export * from './ManualJournal'; export * from './ManualJournal';
export * from './Currency'; export * from './Currency';
export * from './ExchangeRate'; export * from './ExchangeRate';
export * from './Media'; export * from './Media';
export * from './SaleEstimate';

View File

@@ -1,7 +1,11 @@
import { Container } from 'typedi';
// Here we import all events. // Here we import all events.
import 'subscribers/authentication'; import 'subscribers/authentication';
import 'subscribers/organization'; import 'subscribers/organization';
import 'subscribers/manualJournals'; 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

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

View File

@@ -1,6 +1,5 @@
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
import { IContact } from 'interfaces'; import { IContact } from 'interfaces';
import Contact from 'models/Contact';
export default class ContactRepository extends TenantRepository { export default class ContactRepository extends TenantRepository {
cache: any; cache: any;
@@ -45,9 +44,11 @@ export default class ContactRepository extends TenantRepository {
* Inserts a new contact model. * Inserts a new contact model.
* @param contact * @param contact
*/ */
async insert(contact) { async insert(contactInput: IContact) {
await Contact.query().insert({ ...contact }) const { Contact } = this.models;
const contact = await Contact.query().insert({ ...contactInput })
this.flushCache(); this.flushCache();
return contact;
} }
/** /**
@@ -56,6 +57,7 @@ export default class ContactRepository extends TenantRepository {
* @param {IContact} contact - Contact input. * @param {IContact} contact - Contact input.
*/ */
async update(contactId: number, contact: IContact) { async update(contactId: number, contact: IContact) {
const { Contact } = this.models;
await Contact.query().findById(contactId).patch({ ...contact }); await Contact.query().findById(contactId).patch({ ...contact });
this.flushCache(); this.flushCache();
} }
@@ -66,6 +68,7 @@ export default class ContactRepository extends TenantRepository {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async deleteById(contactId: number): Promise<void> { async deleteById(contactId: number): Promise<void> {
const { Contact } = this.models;
await Contact.query().where('id', contactId).delete(); await Contact.query().where('id', contactId).delete();
this.flushCache(); this.flushCache();
} }
@@ -75,6 +78,7 @@ export default class ContactRepository extends TenantRepository {
* @param {number[]} contactsIds * @param {number[]} contactsIds
*/ */
async bulkDelete(contactsIds: number[]) { async bulkDelete(contactsIds: number[]) {
const { Contact } = this.models;
await Contact.query().whereIn('id', contactsIds); await Contact.query().whereIn('id', contactsIds);
this.flushCache(); this.flushCache();
} }

View File

@@ -17,6 +17,15 @@ export default class VendorRepository extends TenantRepository {
this.cache = this.tenancy.cache(tenantId); 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. * Retrieve the bill that associated to the given vendor id.
* @param {number} vendorId - Vendor id. * @param {number} vendorId - Vendor id.
@@ -49,4 +58,23 @@ export default class VendorRepository extends TenantRepository {
.whereIn('id', vendorIds) .whereIn('id', vendorIds)
.withGraphFetched('bills'); .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

@@ -26,7 +26,7 @@ export default class ContactsService {
* @param {TContactService} contactService * @param {TContactService} contactService
* @return {Promise<IContact>} * @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); const { Contact } = this.tenancy.models(tenantId);
this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId }); this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId });

View File

@@ -1,5 +1,9 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { omit, difference } from 'lodash'; import { omit, difference } from 'lodash';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import JournalPoster from "services/Accounting/JournalPoster"; import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands"; import JournalCommands from "services/Accounting/JournalCommands";
import ContactsService from 'services/Contacts/ContactsService'; import ContactsService from 'services/Contacts/ContactsService';
@@ -13,6 +17,7 @@ import {
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
@Service() @Service()
export default class CustomersService { export default class CustomersService {
@@ -25,6 +30,12 @@ export default class CustomersService {
@Inject() @Inject()
dynamicListService: DynamicListingService; dynamicListService: DynamicListingService;
@Inject('logger')
logger: any;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/** /**
* Converts customer to contact DTO. * Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
@@ -43,31 +54,44 @@ export default class CustomersService {
* Creates a new customer. * Creates a new customer.
* @param {number} tenantId * @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO * @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 contactDTO = this.customerToContactDTO(customerDTO)
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer'); const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
// Writes the customer opening balance journal entries. this.logger.info('[customer] created successfully.', { tenantId, customerDTO });
if (customer.openingBalance) { await this.eventDispatcher.dispatch(events.customers.onCreated);
await this.writeCustomerOpeningBalanceJournal(
tenantId,
customer.id,
customer.openingBalance,
);
}
return customer; return customer;
} }
/** /**
* Edits details of the given customer. * Edits details of the given customer.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId
* @param {ICustomerEditDTO} customerDTO * @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); 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 * @param {number} customerId
* @return {Promise<void>} * @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); const { Contact } = this.tenancy.models(tenantId);
this.logger.info('[customer] trying to delete customer.', { tenantId, customerId });
await this.getCustomerByIdOrThrowError(tenantId, customerId); await this.getCustomerByIdOrThrowError(tenantId, customerId);
await this.customerHasNoInvoicesOrThrowError(tenantId, customerId); await this.customerHasNoInvoicesOrThrowError(tenantId, customerId);
await Contact.query().findById(customerId).delete(); 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( await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, [customerId], 'customer', tenantId, id, 'customer',
); );
} }
@@ -129,7 +168,7 @@ export default class CustomersService {
* @param {number} openingBalance * @param {number} openingBalance
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async writeCustomerOpeningBalanceJournal( public async writeCustomerOpeningBalanceJournal(
tenantId: number, tenantId: number,
customerId: number, customerId: number,
openingBalance: number, openingBalance: number,
@@ -150,7 +189,7 @@ export default class CustomersService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
*/ */
getCustomerByIdOrThrowError(tenantId: number, customerId: number) { private getCustomerByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer'); return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer');
} }
@@ -159,7 +198,7 @@ export default class CustomersService {
* @param {numebr} tenantId * @param {numebr} tenantId
* @param {number[]} customersIds * @param {number[]} customersIds
*/ */
getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) { private getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) {
return this.contactService.getContactsOrThrowErrorNotFound(tenantId, customersIds, 'customer'); return this.contactService.getContactsOrThrowErrorNotFound(tenantId, customersIds, 'customer');
} }
@@ -169,19 +208,14 @@ export default class CustomersService {
* @param {number[]} customersIds * @param {number[]} customersIds
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async deleteBulkCustomers(tenantId: number, customersIds: number[]) { public async deleteBulkCustomers(tenantId: number, customersIds: number[]) {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds); await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds);
await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds); await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds);
await Contact.query().whereIn('id', customersIds).delete(); await Contact.query().whereIn('id', customersIds).delete();
await this.eventDispatcher.dispatch(events.customers.onBulkDeleted);
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId,
customersIds,
'Customer'
);
} }
/** /**
@@ -189,8 +223,10 @@ export default class CustomersService {
* or throw service error. * or throw service error.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @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 { customerRepository } = this.tenancy.repositories(tenantId);
const salesInvoice = await customerRepository.getSalesInvoices(customerId); const salesInvoice = await customerRepository.getSalesInvoices(customerId);
@@ -204,8 +240,9 @@ export default class CustomersService {
* @param {number} tenantId * @param {number} tenantId
* @param {number[]} customersIds * @param {number[]} customersIds
* @throws {ServiceError} * @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 { customerRepository } = this.tenancy.repositories(tenantId);
const customersWithInvoices = await customerRepository.customersWithSalesInvoices( const customersWithInvoices = await customerRepository.customersWithSalesInvoices(

View File

@@ -1,5 +1,9 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { difference, rest } from 'lodash'; import { difference, rest } from 'lodash';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import JournalPoster from "services/Accounting/JournalPoster"; import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands"; import JournalCommands from "services/Accounting/JournalCommands";
import ContactsService from 'services/Contacts/ContactsService'; import ContactsService from 'services/Contacts/ContactsService';
@@ -12,6 +16,7 @@ import {
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import events from 'subscribers/events';
@Service() @Service()
export default class VendorsService { export default class VendorsService {
@@ -24,12 +29,18 @@ export default class VendorsService {
@Inject() @Inject()
dynamicListService: DynamicListingService; dynamicListService: DynamicListingService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject('logger')
logger: any;
/** /**
* Converts vendor to contact DTO. * Converts vendor to contact DTO.
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO * @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
* @returns {IContactDTO} * @returns {IContactDTO}
*/ */
vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) { private vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) {
return { return {
...vendorDTO, ...vendorDTO,
active: (typeof vendorDTO.active === 'undefined') ? active: (typeof vendorDTO.active === 'undefined') ?
@@ -43,19 +54,15 @@ export default class VendorsService {
* @param {IVendorNewDTO} vendorDTO * @param {IVendorNewDTO} vendorDTO
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) { public async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) {
const contactDTO = this.vendorToContactDTO(vendorDTO); 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'); const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor');
// Writes the vendor opening balance journal entries. await this.eventDispatcher.dispatch(events.vendors.onCreated, {
if (vendor.openingBalance) { tenantId, vendorId: vendor.id, vendor,
await this.writeVendorOpeningBalanceJournal( });
tenantId,
vendor.id,
vendor.openingBalance,
);
}
return vendor; return vendor;
} }
@@ -64,9 +71,13 @@ export default class VendorsService {
* @param {number} tenantId * @param {number} tenantId
* @param {IVendorEditDTO} vendorDTO * @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); 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 +85,7 @@ export default class VendorsService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
*/ */
getVendorByIdOrThrowError(tenantId: number, customerId: number) { private getVendorByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor'); return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor');
} }
@@ -84,17 +95,17 @@ export default class VendorsService {
* @param {number} vendorId * @param {number} vendorId
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async deleteVendor(tenantId: number, vendorId: number) { public async deleteVendor(tenantId: number, vendorId: number) {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
await this.getVendorByIdOrThrowError(tenantId, vendorId); await this.getVendorByIdOrThrowError(tenantId, vendorId);
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId); await this.vendorHasNoBillsOrThrowError(tenantId, vendorId);
this.logger.info('[vendor] trying to delete vendor.', { tenantId, vendorId });
await Contact.query().findById(vendorId).delete(); await Contact.query().findById(vendorId).delete();
await this.contactService.revertJEntriesContactsOpeningBalance( await this.eventDispatcher.dispatch(events.vendors.onDeleted, { tenantId, vendorId });
tenantId, [vendorId], 'vendor', this.logger.info('[vendor] deleted successfully.', { tenantId, vendorId });
);
} }
/** /**
@@ -102,7 +113,7 @@ export default class VendorsService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} vendorId * @param {number} vendorId
*/ */
async getVendor(tenantId: number, vendorId: number) { public async getVendor(tenantId: number, vendorId: number) {
return this.contactService.getContact(tenantId, vendorId, 'vendor'); return this.contactService.getContact(tenantId, vendorId, 'vendor');
} }
@@ -113,7 +124,7 @@ export default class VendorsService {
* @param {number} openingBalance * @param {number} openingBalance
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async writeVendorOpeningBalanceJournal( public async writeVendorOpeningBalanceJournal(
tenantId: number, tenantId: number,
vendorId: number, vendorId: number,
openingBalance: number, openingBalance: number,
@@ -121,20 +132,36 @@ export default class VendorsService {
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
this.logger.info('[vendor] writing opening balance journal entries.', { tenantId, vendorId });
await journalCommands.vendorOpeningBalance(vendorId, openingBalance) await journalCommands.vendorOpeningBalance(vendorId, openingBalance)
await Promise.all([ await Promise.all([
journal.saveBalance(), journal.saveBalance(),
journal.saveEntries(), 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. * Retrieve the given vendors or throw error if one of them not found.
* @param {numebr} tenantId * @param {numebr} tenantId
* @param {number[]} vendorsIds * @param {number[]} vendorsIds
*/ */
getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) { private getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) {
return this.contactService.getContactsOrThrowErrorNotFound(tenantId, vendorsIds, 'vendor'); return this.contactService.getContactsOrThrowErrorNotFound(tenantId, vendorsIds, 'vendor');
} }
@@ -144,17 +171,19 @@ export default class VendorsService {
* @param {number[]} vendorsIds * @param {number[]} vendorsIds
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async deleteBulkVendors(tenantId: number, vendorsIds: number[]) { public async deleteBulkVendors(
tenantId: number,
vendorsIds: number[]
): Promise<void> {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
await this.getVendorsOrThrowErrorNotFound(tenantId, vendorsIds); await this.getVendorsOrThrowErrorNotFound(tenantId, vendorsIds);
await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds); await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds);
await Contact.query().whereIn('id', vendorsIds).delete(); await Contact.query().whereIn('id', vendorsIds).delete();
await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, { tenantId, vendorsIds });
await this.contactService.revertJEntriesContactsOpeningBalance( this.logger.info('[vendor] bulk deleted successfully.', { tenantId, vendorsIds });
tenantId, vendorsIds, 'vendor',
);
} }
/** /**
@@ -162,7 +191,7 @@ export default class VendorsService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} vendorId * @param {number} vendorId
*/ */
async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) { private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) {
const { vendorRepository } = this.tenancy.repositories(tenantId); const { vendorRepository } = this.tenancy.repositories(tenantId);
const bills = await vendorRepository.getBills(vendorId); const bills = await vendorRepository.getBills(vendorId);
@@ -177,7 +206,7 @@ export default class VendorsService {
* @param {number[]} customersIds * @param {number[]} customersIds
* @throws {ServiceError} * @throws {ServiceError}
*/ */
async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) { private async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) {
const { vendorRepository } = this.tenancy.repositories(tenantId); const { vendorRepository } = this.tenancy.repositories(tenantId);
const vendorsWithBills = await vendorRepository.vendorsWithBills(vendorsIds); const vendorsWithBills = await vendorRepository.vendorsWithBills(vendorsIds);
@@ -197,7 +226,7 @@ export default class VendorsService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IVendorsFilter} vendorsFilter - Vendors filter. * @param {IVendorsFilter} vendorsFilter - Vendors filter.
*/ */
async getVendorsList(tenantId: number, vendorsFilter: IVendorsFilter) { public async getVendorsList(tenantId: number, vendorsFilter: IVendorsFilter) {
const { Vendor } = this.tenancy.models(tenantId); const { Vendor } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Vendor, vendorsFilter); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Vendor, vendorsFilter);

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( public async revertJournalEntries(
tenantId: number, tenantId: number,
expenseId: number|number[], expenseId: number|number[],
) { ): Promise<void> {
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
await journalCommands.revertJournalEntries(expenseId, 'Expense'); await journalCommands.revertJournalEntries(expenseId, 'Expense');
return Promise.all([ await Promise.all([
journal.saveBalance(), journal.saveBalance(),
journal.deleteEntries(), journal.deleteEntries(),
]); ]);

View File

@@ -1,15 +1,41 @@
import { Inject, Service } from 'typedi'; 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 moment from 'moment';
import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces'; import events from 'subscribers/events';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; import {
IBill,
IBillPaymentDTO,
IBillPaymentEntryDTO,
IBillPayment,
IBillPaymentsFilter,
IPaginationMeta,
IFilterMeta,
IBillPaymentEntry,
} from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry'; import JournalEntry from 'services/Accounting/JournalEntry';
import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService'; import JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils'; 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. * Bill payments service.
@@ -29,6 +55,177 @@ export default class BillPaymentsService {
@Inject() @Inject()
dynamicListService: DynamicListingService; 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 * Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions. * with associated bills entries and journal transactions.
@@ -40,53 +237,39 @@ export default class BillPaymentsService {
* - Increment the payment amount of the given vendor bills. * - Increment the payment amount of the given vendor bills.
* - Decrement the vendor balance. * - Decrement the vendor balance.
* - Records payment journal entries. * - Records payment journal entries.
* ------
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {BillPaymentDTO} billPayment - Bill payment object. * @param {BillPaymentDTO} billPayment - Bill payment object.
*/ */
public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) { public async createBillPayment(
const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); 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 = { const billPaymentObj = {
amount: sumBy(billPaymentDTO.entries, 'payment_amount'), amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
...formatDateFields(billPaymentDTO, ['payment_date']), ...formatDateFields(billPaymentDTO, ['paymentDate']),
} };
const storedBillPayment = await BillPayment.query() await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
.insert({ await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId);
...omit(billPayment, ['entries']), 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) => { await this.eventDispatcher.dispatch(events.billPayments.onCreated, {
const oper = BillPaymentEntry.query() tenantId, billPayment, billPaymentId: billPayment.id,
.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);
}); });
// Decrement the vendor balance after bills payments. this.logger.info('[payment_made] inserted successfully.', { tenantId, billPaymentId: billPayment.id, });
const vendorDecrementOper = Vendor.changeBalance(
billPayment.vendor_id, return billPayment;
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;
} }
/** /**
@@ -110,63 +293,31 @@ export default class BillPaymentsService {
tenantId: number, tenantId: number,
billPaymentId: number, billPaymentId: number,
billPaymentDTO, billPaymentDTO,
oldBillPayment, ): Promise<IBillPayment> {
) { const { BillPayment } = this.tenancy.models(tenantId);
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);
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId);
oldBillPayment.entries,
entriesHasIds const billPaymentObj = {
); amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
if (entriesIdsShouldDelete.length > 0) { ...formatDateFields(billPaymentDTO, ['paymentDate']),
const deleteOper = BillPaymentEntry.query() };
.bulkDelete(entriesIdsShouldDelete);
opers.push(deleteOper); await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
} await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId);
// Entries that should be update to the storage. await this.validateEntriesIdsExistance(tenantId, billPaymentId, billPaymentObj.entries);
if (entriesHasIds.length > 0) { await this.validateBillsExistance(tenantId, billPaymentObj.entries);
const updateOper = BillPaymentEntry.query() await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
.bulkUpdate(entriesHasIds, { where: 'id' });
opers.push(updateOper); const billPayment = await BillPayment.query()
} .upsertGraph({
// Entries that should be inserted to the storage. id: billPaymentId,
if (entriesHasNoIds.length > 0) { ...omit(billPaymentObj, ['entries']),
const insertOper = BillPaymentEntry.query() });
.bulkInsert( await this.eventDispatcher.dispatch(events.billPayments.onEdited);
entriesHasNoIds.map((e) => ({ ...e, bill_payment_id: billPaymentId })) this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldPaymentMade });
);
opers.push(insertOper); return billPayment;
}
// 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,
]);
} }
/** /**
@@ -176,29 +327,16 @@ export default class BillPaymentsService {
* @return {Promise} * @return {Promise}
*/ */
public async deleteBillPayment(tenantId: number, billPaymentId: number) { public async deleteBillPayment(tenantId: number, billPaymentId: number) {
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query().where('id', billPaymentId).first();
this.logger.info('[bill_payment] trying to delete.', { tenantId, billPaymentId });
const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId);
await BillPayment.query() await BillPaymentEntry.query().where('bill_payment_id', billPaymentId).delete();
.where('id', billPaymentId) await BillPayment.query().where('id', billPaymentId).delete();
.delete();
await BillPaymentEntry.query() await this.eventDispatcher.dispatch(events.billPayments.onDeleted, { tenantId, billPaymentId, oldPaymentMade });
.where('bill_payment_id', billPaymentId) this.logger.info('[bill_payment] deleted successfully.', { tenantId, billPaymentId });
.delete();
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
billPaymentId,
'BillPayment',
);
const revertVendorBalanceOper = Vendor.changeBalance(
billPayment.vendorId,
billPayment.amount,
);
return Promise.all([
deleteTransactionsOper,
revertVendorBalanceOper,
]);
} }
/** /**
@@ -207,15 +345,13 @@ export default class BillPaymentsService {
* @param {BillPayment} billPayment * @param {BillPayment} billPayment
* @param {Integer} billPaymentId * @param {Integer} billPaymentId
*/ */
private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) { public async recordJournalEntries(tenantId: number, billPayment: IBillPayment) {
const { AccountTransaction, Account } = this.tenancy.models(tenantId); const { AccountTransaction } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAmount = sumBy(billPayment.entries, 'payment_amount'); const paymentAmount = sumBy(billPayment.entries, 'paymentAmount');
const formattedDate = moment(billPayment.payment_date).format('YYYY-MM-DD'); const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD');
const payableAccount = await this.accountsService.getAccountByType( const payableAccount = await accountRepository.getBySlug('accounts-payable');
tenantId,
'accounts_payable'
);
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const commonJournal = { const commonJournal = {
@@ -238,13 +374,13 @@ export default class BillPaymentsService {
...commonJournal, ...commonJournal,
debit: paymentAmount, debit: paymentAmount,
contactType: 'Vendor', contactType: 'Vendor',
contactId: billPayment.vendor_id, contactId: billPayment.vendorId,
account: payableAccount.id, account: payableAccount.id,
}); });
const creditPaymentAccount = new JournalEntry({ const creditPaymentAccount = new JournalEntry({
...commonJournal, ...commonJournal,
credit: paymentAmount, credit: paymentAmount,
account: billPayment.payment_account_id, account: billPayment.paymentAccountId,
}); });
journal.debit(debitReceivable); journal.debit(debitReceivable);
journal.credit(creditPaymentAccount); 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. * Retrieve bill payment paginted and filterable list.
* @param {number} tenantId * @param {number} tenantId
@@ -301,18 +455,4 @@ export default class BillPaymentsService {
return billPayment; 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 moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry'; import JournalEntry from 'services/Accounting/JournalEntry';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPosterService from 'services/Sales/JournalPosterService';
import InventoryService from 'services/Inventory/Inventory'; import InventoryService from 'services/Inventory/Inventory';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils'; 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. * Vendor bills services.
@@ -24,9 +47,138 @@ export default class BillsService extends SalesInvoicesCost {
@Inject() @Inject()
accountsService: AccountsService; accountsService: AccountsService;
@Inject()
itemsService: ItemsService;
@Inject() @Inject()
tenancy: TenancyService; 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. * Converts bill DTO to model.
* @param {number} tenantId * @param {number} tenantId
@@ -35,13 +187,13 @@ export default class BillsService extends SalesInvoicesCost {
* *
* @returns {IBill} * @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); const { ItemEntry } = this.tenancy.models(tenantId);
let invLotNumber = oldBill?.invLotNumber; let invLotNumber = oldBill?.invLotNumber;
if (!invLotNumber) { // if (!invLotNumber) {
invLotNumber = await this.inventoryService.nextLotNumber(tenantId); // invLotNumber = await this.inventoryService.nextLotNumber(tenantId);
} // }
const entries = billDTO.entries.map((entry) => ({ const entries = billDTO.entries.map((entry) => ({
...entry, ...entry,
amount: ItemEntry.calcAmount(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. * Creates a new bill and stored it to the storage.
* * ----
* Precedures. * Precedures.
* ---- * ----
* - Insert bill transactions to the storage. * - 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 journal transactions on the given accounts.
* - Record bill items inventory transactions. * - Record bill items inventory transactions.
* ---- * ----
* @param {number} tenantId - The given tenant id. * @param {number} tenantId - The given tenant id.
* @param {IBillOTD} billDTO - * @param {IBillDTO} billDTO -
* @return {void} * @return {Promise<IBill>}
*/ */
async createBill(tenantId: number, billDTO: IBillOTD) { public async createBill(
const { Vendor, Bill, ItemEntry } = this.tenancy.models(tenantId); tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
const bill = await this.billDTOToModel(tenantId, billDTO); this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO });
const saveEntriesOpers = []; const billObj = await this.billDTOToModel(tenantId, billDTO);
const storedBill = await Bill.query() await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
.insert({ await this.validateBillNumberExists(tenantId, billDTO.billNumber);
...omit(bill, ['entries']),
}); await this.validateItemsIdsExistance(tenantId, billDTO.entries);
bill.entries.forEach((entry) => { await this.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
const oper = ItemEntry.query()
.insertAndFetch({ const bill = await Bill.query()
.insertGraph({
...omit(billObj, ['entries']),
userId: authorizedUser.id,
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill', reference_type: 'Bill',
reference_id: storedBill.id, ...omit(entry, ['amount', 'id']),
...omit(entry, ['amount']), })),
}).then((itemEntry) => { });
entry.id = itemEntry.id;
}); // Triggers `onBillCreated` event.
saveEntriesOpers.push(oper); await this.eventDispatcher.dispatch(events.bills.onCreated, {
tenantId, bill, billId: bill.id,
}); });
// Await save all bill entries operations. this.logger.info('[bill] bill inserted successfully.', { tenantId, billId: bill.id });
await Promise.all([...saveEntriesOpers]);
// Increments vendor balance. return bill;
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;
} }
/** /**
@@ -132,55 +272,34 @@ export default class BillsService extends SalesInvoicesCost {
* *
* @param {number} tenantId - The given tenant id. * @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill 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) { public async editBill(
const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId); tenantId: number,
billId: number,
const oldBill = await Bill.query().findById(billId); billDTO: IBillEditDTO,
const bill = this.billDTOToModel(tenantId, billDTO, oldBill); ): 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. // Update the bill transaction.
const updatedBill = await Bill.query() const bill = await Bill.query()
.where('id', billId) .upsertGraph({
.update({ id: billId,
...omit(bill, ['entries', 'invLotNumber']) ...omit(billObj, ['entries', 'invLotNumber']),
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}))
}); });
// Old stored entries. // Triggers event `onBillEdited`.
const storedEntries = await ItemEntry.query() await this.eventDispatcher.dispatch(events.bills.onEdited, { tenantId, billId, oldBill, bill });
.where('reference_id', billId)
.where('reference_type', 'Bill');
// Patch the bill entries. return bill;
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);
} }
/** /**
@@ -188,13 +307,10 @@ export default class BillsService extends SalesInvoicesCost {
* @param {Integer} billId * @param {Integer} billId
* @return {void} * @return {void}
*/ */
async deleteBill(tenantId: number, billId: number) { public async deleteBill(tenantId: number, billId: number) {
const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId); const { Bill, ItemEntry } = this.tenancy.models(tenantId);
const bill = await Bill.query() const oldBill = await this.getBillOrThrowError(tenantId, billId);
.where('id', billId)
.withGraphFetched('entries')
.first();
// Delete all associated bill entries. // Delete all associated bill entries.
const deleteBillEntriesOper = ItemEntry.query() const deleteBillEntriesOper = ItemEntry.query()
@@ -205,28 +321,10 @@ export default class BillsService extends SalesInvoicesCost {
// Delete the bill transaction. // Delete the bill transaction.
const deleteBillOper = Bill.query().where('id', billId).delete(); const deleteBillOper = Bill.query().where('id', billId).delete();
// Delete associated bill journal transactions. await Promise.all([deleteBillEntriesOper, deleteBillOper]);
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
billId, // Triggers `onBillDeleted` event.
'Bill' await this.eventDispatcher.dispatch(events.bills.onDeleted, { tenantId, billId, oldBill });
);
// 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);
} }
/** /**
@@ -262,20 +360,18 @@ export default class BillsService extends SalesInvoicesCost {
* @param {IBill} bill * @param {IBill} bill
* @param {Integer} billId * @param {Integer} billId
*/ */
async recordJournalTransactions(tenantId: number, bill: any, billId?: number) { async recordJournalTransactions(tenantId: number, bill: IBill, billId?: number) {
const { AccountTransaction, Item } = this.tenancy.models(tenantId); const { AccountTransaction, Item, ItemEntry } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const entriesItemsIds = bill.entries.map((entry) => entry.item_id); const entriesItemsIds = bill.entries.map((entry) => entry.itemId);
const payableTotal = sumBy(bill.entries, 'amount'); const formattedDate = moment(bill.billDate).format('YYYY-MM-DD');
const formattedDate = moment(bill.bill_date).format('YYYY-MM-DD');
const storedItems = await Item.query() const storedItems = await Item.query().whereIn('id', entriesItemsIds);
.whereIn('id', entriesItemsIds);
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item])); const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
const payableAccount = await this.accountsService.getAccountByType( const payableAccount = await accountRepository.getBySlug('accounts-payable');
tenantId, 'accounts_payable'
);
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const commonJournalMeta = { const commonJournalMeta = {
@@ -284,7 +380,7 @@ export default class BillsService extends SalesInvoicesCost {
referenceId: bill.id, referenceId: bill.id,
referenceType: 'Bill', referenceType: 'Bill',
date: formattedDate, date: formattedDate,
accural: true, userId: bill.userId,
}; };
if (billId) { if (billId) {
const transactions = await AccountTransaction.query() const transactions = await AccountTransaction.query()
@@ -297,23 +393,26 @@ export default class BillsService extends SalesInvoicesCost {
} }
const payableEntry = new JournalEntry({ const payableEntry = new JournalEntry({
...commonJournalMeta, ...commonJournalMeta,
credit: payableTotal, credit: bill.amount,
account: payableAccount.id, account: payableAccount.id,
contactId: bill.vendor_id, contactId: bill.vendorId,
contactType: 'Vendor', contactType: 'Vendor',
index: 1,
}); });
journal.credit(payableEntry); journal.credit(payableEntry);
bill.entries.forEach((entry) => { bill.entries.forEach((entry, index) => {
const item: IItem = storedItemsMap.get(entry.item_id); const item: IItem = storedItemsMap.get(entry.itemId);
const amount = ItemEntry.calcAmount(entry);
const debitEntry = new JournalEntry({ const debitEntry = new JournalEntry({
...commonJournalMeta, ...commonJournalMeta,
debit: entry.amount, debit: amount,
account: account:
['inventory'].indexOf(item.type) !== -1 ['inventory'].indexOf(item.type) !== -1
? item.inventoryAccountId ? item.inventoryAccountId
: item.costAccountId, : item.costAccountId,
index: index + 2,
}); });
journal.debit(debitEntry); 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. * Retrieve the given bill details with associated items entries.
@@ -370,8 +431,7 @@ export default class BillsService extends SalesInvoicesCost {
*/ */
getBill(tenantId: number, billId: number) { getBill(tenantId: number, billId: number) {
const { Bill } = this.tenancy.models(tenantId); const { Bill } = this.tenancy.models(tenantId);
return Bill.query().findById(billId).withGraphFetched('entries');
return Bill.query().where('id', billId).first();
} }
/** /**

View File

@@ -1,6 +1,7 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import JournalCommands from 'services/Accounting/JournalCommands';
@Service() @Service()
export default class JournalPosterService { export default class JournalPosterService {
@@ -19,20 +20,14 @@ export default class JournalPosterService {
referenceId: number, referenceId: number,
referenceType: string referenceType: string
) { ) {
const { Account, AccountTransaction } = this.tenancy.models(tenantId); const journal = new JournalPoster(tenantId);
const journalCommand = new JournalCommands(journal);
const transactions = await AccountTransaction.tenant() await journalCommand.revertJournalEntries(referenceId, referenceType);
.query()
.whereIn('reference_type', [referenceType])
.where('reference_id', referenceId)
.withGraphFetched('account.type');
const accountsDepGraph = await Account.tenant().depGraph().query(); await Promise.all([
const journal = new JournalPoster(accountsDepGraph); journal.deleteEntries(),
journal.saveBalance()
journal.loadEntries(transactions); ]);
journal.removeEntries();
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
} }
} }

View File

@@ -1,6 +1,11 @@
import { omit, sumBy, chain } from 'lodash'; import { omit, sumBy, chain } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import { IPaymentReceiveOTD } from 'interfaces'; import { IPaymentReceiveOTD } from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
@@ -34,6 +39,186 @@ export default class PaymentReceiveService {
@Inject('logger') @Inject('logger')
logger: any; 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 * Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions. * with associated invoices payment and journal transactions.
@@ -53,9 +238,10 @@ export default class PaymentReceiveService {
this.logger.info('[payment_receive] inserting to the storage.'); this.logger.info('[payment_receive] inserting to the storage.');
const storedPaymentReceive = await PaymentReceive.query() const storedPaymentReceive = await PaymentReceive.query()
.insert({ .insertGraph({
amount: paymentAmount, amount: paymentAmount,
...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']),
entries: paymentReceive.entries.map((entry) => ({ ...entry })),
}); });
const storeOpers: Array<any> = []; const storeOpers: Array<any> = [];
@@ -92,6 +278,8 @@ export default class PaymentReceiveService {
customerIncrementOper, customerIncrementOper,
recordJournalTransactions, recordJournalTransactions,
]); ]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated);
return storedPaymentReceive; return storedPaymentReceive;
} }
@@ -186,6 +374,7 @@ export default class PaymentReceiveService {
changeCustomerBalance, changeCustomerBalance,
diffInvoicePaymentAmount, diffInvoicePaymentAmount,
]); ]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited);
} }
/** /**
@@ -239,6 +428,7 @@ export default class PaymentReceiveService {
revertCustomerBalance, revertCustomerBalance,
revertInvoicesPaymentAmount, revertInvoicesPaymentAmount,
]); ]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted);
} }
/** /**

View File

@@ -1,10 +1,15 @@
import { omit, difference, sumBy, mixin } from 'lodash'; import { omit, difference, sumBy, mixin } from 'lodash';
import { Service, Inject } from 'typedi'; 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 HasItemsEntries from 'services/Sales/HasItemsEntries';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
/** /**
* Sale estimate service. * Sale estimate service.
@@ -24,14 +29,132 @@ export default class SaleEstimateService {
@Inject() @Inject()
dynamicListService: DynamicListingService; 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. * Creates a new estimate with associated entries.
* @async * @async
* @param {number} tenantId - The tenant id. * @param {number} tenantId - The tenant id.
* @param {EstimateDTO} estimate * @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 { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e)); const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
@@ -44,22 +167,15 @@ export default class SaleEstimateService {
const storedEstimate = await SaleEstimate.query() const storedEstimate = await SaleEstimate.query()
.insert({ .insert({
...omit(estimate, ['entries']), ...omit(estimate, ['entries']),
}); entries: estimate.entries.map((entry) => ({
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({
reference_type: 'SaleEstimate', reference_type: 'SaleEstimate',
reference_id: storedEstimate.id, reference_id: storedEstimate.id,
...omit(entry, ['total', 'amount', 'id']), ...omit(entry, ['total', 'amount', 'id']),
}); }))
storeEstimateEntriesOpers.push(oper); });
});
await Promise.all([...storeEstimateEntriesOpers]);
this.logger.info('[sale_estimate] insert sale estimated success.'); this.logger.info('[sale_estimate] insert sale estimated success.');
await this.eventDispatcher.dispatch(events.saleEstimates.onCreated);
return storedEstimate; return storedEstimate;
} }
@@ -72,7 +188,7 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate * @param {EstimateDTO} estimate
* @return {void} * @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 { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e)); const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
@@ -89,16 +205,14 @@ export default class SaleEstimateService {
.where('reference_id', estimateId) .where('reference_id', estimateId)
.where('reference_type', 'SaleEstimate'); .where('reference_type', 'SaleEstimate');
const patchItemsEntries = this.itemsEntriesService.patchItemsEntries( await this.itemsEntriesService.patchItemsEntries(
tenantId, tenantId,
estimate.entries, estimate.entries,
storedEstimateEntries, storedEstimateEntries,
'SaleEstimate', 'SaleEstimate',
estimateId, estimateId,
); );
return Promise.all([ await this.eventDispatcher.dispatch(events.saleEstimates.onEdited);
patchItemsEntries,
]);
} }
/** /**
@@ -118,6 +232,9 @@ export default class SaleEstimateService {
.delete(); .delete();
await SaleEstimate.query().where('id', estimateId).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, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
} from 'decorators/eventDispatcher'; } 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 JournalPoster from 'services/Accounting/JournalPoster';
import HasItemsEntries from 'services/Sales/HasItemsEntries'; import HasItemsEntries from 'services/Sales/HasItemsEntries';
import InventoryService from 'services/Inventory/Inventory'; import InventoryService from 'services/Inventory/Inventory';
@@ -12,6 +20,16 @@ import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils'; 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 * Sales invoices service
@@ -37,6 +55,81 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@EventDispatcher() @EventDispatcher()
eventDispatcher: EventDispatcherInterface; 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 * Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions. * with associated to entries and journal transactions.
@@ -58,6 +151,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
invLotNumber, invLotNumber,
}; };
await this.validateSaleInvoiceNoUniquiness(tenantId, saleInvoiceDTO.invoiceNo);
await this.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
this.logger.info('[sale_invoice] inserting sale invoice to the storage.'); this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
const storedInvoice = await SaleInvoice.query() const storedInvoice = await SaleInvoice.query()
.insert({ .insert({
@@ -95,6 +191,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
// method and starting date. // method and starting date.
await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id); await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id);
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated);
return storedInvoice; return storedInvoice;
} }
@@ -131,30 +229,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
.where('reference_type', 'SaleInvoice'); .where('reference_type', 'SaleInvoice');
// Patch update the sale invoice items entries. // Patch update the sale invoice items entries.
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries( await this.itemsEntriesService.patchItemsEntries(
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId, tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
); );
// Triggers `onSaleInvoiceEdited` event.
this.logger.info('[sale_invoice] change customer different balance.'); await this.eventDispatcher.dispatch(events.saleInvoice.onEdited);
// 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);
} }
/** /**
@@ -168,14 +247,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
SaleInvoice, SaleInvoice,
ItemEntry, ItemEntry,
Customer, Customer,
Account,
InventoryTransaction, InventoryTransaction,
AccountTransaction, AccountTransaction,
} = this.tenancy.models(tenantId); } = this.tenancy.models(tenantId);
const oldSaleInvoice = await SaleInvoice.query() const oldSaleInvoice = await this.getSaleInvoiceOrThrowError(tenantId, saleInvoiceId);
.findById(saleInvoiceId)
.withGraphFetched('entries');
this.logger.info('[sale_invoice] delete sale invoice with entries.'); this.logger.info('[sale_invoice] delete sale invoice with entries.');
await SaleInvoice.query().where('id', saleInvoiceId).delete(); 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 // Schedule sale invoice re-compute based on the item cost
// method and starting date. // method and starting date.
await this.scheduleComputeItemsCost(tenantId, oldSaleInvoice) 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 { omit, difference, sumBy } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import JournalPosterService from 'services/Sales/JournalPosterService'; import JournalPosterService from 'services/Sales/JournalPosterService';
import HasItemEntries from 'services/Sales/HasItemsEntries'; import HasItemEntries from 'services/Sales/HasItemsEntries';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
@@ -21,6 +26,125 @@ export default class SalesReceiptService {
@Inject() @Inject()
itemsEntriesService: HasItemEntries; 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. * Creates a new sale receipt with associated entries.
* @async * @async
@@ -38,20 +162,14 @@ export default class SalesReceiptService {
const storedSaleReceipt = await SaleReceipt.query() const storedSaleReceipt = await SaleReceipt.query()
.insert({ .insert({
...omit(saleReceipt, ['entries']), ...omit(saleReceipt, ['entries']),
}); entries: saleReceipt.entries.map((entry) => ({
const storeSaleReceiptEntriesOpers: Array<any> = [];
saleReceipt.entries.forEach((entry: any) => {
const oper = ItemEntry.query()
.insert({
reference_type: 'SaleReceipt', reference_type: 'SaleReceipt',
reference_id: storedSaleReceipt.id, reference_id: storedSaleReceipt.id,
...omit(entry, ['id', 'amount']), ...omit(entry, ['id', 'amount']),
}); }))
storeSaleReceiptEntriesOpers.push(oper); });
});
await Promise.all([...storeSaleReceiptEntriesOpers]); await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
return storedSaleReceipt;
} }
/** /**
@@ -85,7 +203,9 @@ export default class SalesReceiptService {
'SaleReceipt', 'SaleReceipt',
saleReceiptId, 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, saleReceiptId,
'SaleReceipt' 'SaleReceipt'
); );
return Promise.all([ await Promise.all([
deleteItemsEntriesOper, deleteItemsEntriesOper,
deleteSaleReceiptOper, deleteSaleReceiptOper,
deleteTransactionsOper, deleteTransactionsOper,
]); ]);
await this.eventDispatcher.dispatch(events.saleReceipts.onDeleted);
} }
/** /**

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 { export default {
/** /**
* Authentication service. * Authentication service.
@@ -72,5 +71,90 @@ export default {
onBulkDeleted: 'onExpenseBulkDeleted', onBulkDeleted: 'onExpenseBulkDeleted',
onBulkPublished: 'onBulkPublished', 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',
},
} }

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