mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
refactoring: bills service.
refactoring: bills payments made service.
This commit is contained in:
@@ -24,7 +24,8 @@ export default class CustomersController extends ContactsController {
|
||||
...this.customerDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCustomer.bind(this))
|
||||
asyncMiddleware(this.newCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post('/:id', [
|
||||
...this.contactDTOSchema,
|
||||
@@ -32,19 +33,22 @@ export default class CustomersController extends ContactsController {
|
||||
...this.customerDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editCustomer.bind(this))
|
||||
asyncMiddleware(this.editCustomer.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.delete('/:id', [
|
||||
...this.specificContactSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteCustomer.bind(this))
|
||||
asyncMiddleware(this.deleteCustomer.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.delete('/', [
|
||||
...this.bulkContactsSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteBulkCustomers.bind(this))
|
||||
asyncMiddleware(this.deleteBulkCustomers.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.get('/', [
|
||||
|
||||
@@ -56,7 +60,8 @@ export default class CustomersController extends ContactsController {
|
||||
...this.specificContactSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getCustomer.bind(this))
|
||||
asyncMiddleware(this.getCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -104,13 +109,6 @@ export default class CustomersController extends ContactsController {
|
||||
await this.customersService.editCustomer(tenantId, contactId, contactDTO);
|
||||
return res.status(200).send({ id: contactId });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -129,18 +127,6 @@ export default class CustomersController extends ContactsController {
|
||||
await this.customersService.deleteCustomer(tenantId, contactId)
|
||||
return res.status(200).send({ id: contactId });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'customer_has_invoices') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER.HAS.SALES_INVOICES', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -159,13 +145,6 @@ export default class CustomersController extends ContactsController {
|
||||
const contact = await this.customersService.getCustomer(tenantId, contactId)
|
||||
return res.status(200).send({ contact });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -184,18 +163,6 @@ export default class CustomersController extends ContactsController {
|
||||
await this.customersService.deleteBulkCustomers(tenantId, contactsIds)
|
||||
return res.status(200).send({ ids: contactsIds });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'some_customers_have_invoices') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SOME.CUSTOMERS.HAVE.SALES_INVOICES', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -210,4 +177,31 @@ export default class CustomersController extends ContactsController {
|
||||
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 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ export default class VendorsController extends ContactsController {
|
||||
...this.vendorDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newVendor.bind(this))
|
||||
asyncMiddleware(this.newVendor.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.post('/:id', [
|
||||
...this.contactDTOSchema,
|
||||
@@ -32,25 +33,29 @@ export default class VendorsController extends ContactsController {
|
||||
...this.vendorDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editVendor.bind(this))
|
||||
asyncMiddleware(this.editVendor.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.delete('/:id', [
|
||||
...this.specificContactSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteVendor.bind(this))
|
||||
asyncMiddleware(this.deleteVendor.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.delete('/', [
|
||||
...this.bulkContactsSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteBulkVendors.bind(this))
|
||||
asyncMiddleware(this.deleteBulkVendors.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.get('/:id', [
|
||||
...this.specificContactSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getVendor.bind(this))
|
||||
asyncMiddleware(this.getVendor.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.get('/', [
|
||||
...this.vendorsListSchema,
|
||||
@@ -99,8 +104,8 @@ export default class VendorsController extends ContactsController {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const contact = await this.vendorsService.newVendor(tenantId, contactDTO);
|
||||
return res.status(200).send({ id: contact.id });
|
||||
const vendor = await this.vendorsService.newVendor(tenantId, contactDTO);
|
||||
return res.status(200).send({ id: vendor.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -121,13 +126,6 @@ export default class VendorsController extends ContactsController {
|
||||
await this.vendorsService.editVendor(tenantId, contactId, contactDTO);
|
||||
return res.status(200).send({ id: contactId });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -146,18 +144,6 @@ export default class VendorsController extends ContactsController {
|
||||
await this.vendorsService.deleteVendor(tenantId, contactId)
|
||||
return res.status(200).send({ id: contactId });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'vendor_has_bills') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDOR.HAS.BILLS', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -176,13 +162,6 @@ export default class VendorsController extends ContactsController {
|
||||
const vendor = await this.vendorsService.getVendor(tenantId, vendorId)
|
||||
return res.status(200).send({ vendor });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -201,18 +180,6 @@ export default class VendorsController extends ContactsController {
|
||||
await this.vendorsService.deleteBulkVendors(tenantId, contactsIds)
|
||||
return res.status(200).send({ ids: contactsIds });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDORS.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'some_vendors_have_bills') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SOME.VENDORS.HAVE.BILLS', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -236,4 +203,31 @@ export default class VendorsController extends ContactsController {
|
||||
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 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { check, param, query, matchedData } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { difference } from 'lodash';
|
||||
import { BillOTD } from 'interfaces';
|
||||
import { IBillDTO, IBillEditDTO } from 'interfaces';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import BillsService from 'services/Purchases/Bills';
|
||||
import BaseController from 'api/controllers/BaseController';
|
||||
import ItemsService from 'services/Items/ItemsService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ServiceError } from 'exceptions';
|
||||
|
||||
@Service()
|
||||
export default class BillsController extends BaseController {
|
||||
@@ -27,45 +27,49 @@ export default class BillsController extends BaseController {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
[...this.billValidationSchema],
|
||||
'/', [
|
||||
...this.billValidationSchema
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateVendorExistance.bind(this)),
|
||||
asyncMiddleware(this.validateItemsIds.bind(this)),
|
||||
asyncMiddleware(this.validateBillNumberExists.bind(this)),
|
||||
asyncMiddleware(this.validateNonPurchasableEntriesItems.bind(this)),
|
||||
asyncMiddleware(this.newBill.bind(this))
|
||||
asyncMiddleware(this.newBill.bind(this)),
|
||||
this.handleServiceError,
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.billValidationSchema, ...this.specificBillValidationSchema],
|
||||
'/:id', [
|
||||
...this.billValidationSchema,
|
||||
...this.specificBillValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateBillExistance.bind(this)),
|
||||
asyncMiddleware(this.validateVendorExistance.bind(this)),
|
||||
asyncMiddleware(this.validateItemsIds.bind(this)),
|
||||
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
|
||||
asyncMiddleware(this.validateNonPurchasableEntriesItems.bind(this)),
|
||||
asyncMiddleware(this.editBill.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[...this.specificBillValidationSchema],
|
||||
'/:id', [
|
||||
...this.specificBillValidationSchema
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateBillExistance.bind(this)),
|
||||
asyncMiddleware(this.getBill.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
[...this.billsListingValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.listingBills.bind(this))
|
||||
asyncMiddleware(this.getBill.bind(this)),
|
||||
);
|
||||
|
||||
// router.get(
|
||||
// '/:id',
|
||||
// [...this.specificBillValidationSchema],
|
||||
// this.validationResult,
|
||||
// asyncMiddleware(this.getBill.bind(this)),
|
||||
// this.handleServiceError,
|
||||
// );
|
||||
// router.get(
|
||||
// '/',
|
||||
// [...this.billsListingValidationSchema],
|
||||
// this.validationResult,
|
||||
// asyncMiddleware(this.listingBills.bind(this)),
|
||||
// this.handleServiceError,
|
||||
// );
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.specificBillValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateBillExistance.bind(this)),
|
||||
asyncMiddleware(this.deleteBill.bind(this))
|
||||
asyncMiddleware(this.deleteBill.bind(this)),
|
||||
this.handleServiceError,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -92,6 +96,28 @@ export default class BillsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
get billEditValidationSchema() {
|
||||
return [
|
||||
// check('bill_number').exists().trim().escape(),
|
||||
check('bill_date').exists().isISO8601(),
|
||||
check('due_date').optional().isISO8601(),
|
||||
// check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.id').optional().isNumeric().toInt(),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount').optional().isNumeric().toFloat(),
|
||||
check('entries.*.description').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill validation schema.
|
||||
*/
|
||||
@@ -112,162 +138,23 @@ export default class BillsController extends BaseController {
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether the vendor is exist.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateVendorExistance(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const { Vendor } = req.models;
|
||||
|
||||
const isVendorExists = await Vendor.query().findById(req.body.vendor_id);
|
||||
|
||||
if (!isVendorExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDOR.ID.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given bill existance.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateBillExistance(req: Request, res: Response, next: Function) {
|
||||
const billId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
const isBillExists = await this.billsService.isBillExists(tenantId, billId);
|
||||
|
||||
if (!isBillExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries items ids.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateItemsIds(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const itemsIds = req.body.entries.map((e) => e.item_id);
|
||||
|
||||
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, itemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the bill number existance.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateBillNumberExists(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const isBillNoExists = await this.billsService.isBillNoExists(
|
||||
tenantId, req.body.bill_number,
|
||||
);
|
||||
if (isBillNoExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.NUMBER.EXISTS', code: 500 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { id: billId } = req.params;
|
||||
const bill = { ...req.body };
|
||||
const { ItemEntry } = req.models;
|
||||
|
||||
const entriesIds = bill.entries.filter((e) => e.id).map((e) => e.id);
|
||||
|
||||
const storedEntries = await ItemEntry.query()
|
||||
.whereIn('reference_id', [billId])
|
||||
.whereIn('reference_type', ['Bill']);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.ENTRIES.IDS.NOT.FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the entries items that not purchase-able.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateNonPurchasableEntriesItems(req: Request, res: Response, next: Function) {
|
||||
const { Item } = req.models;
|
||||
const bill = { ...req.body };
|
||||
const itemsIds = bill.entries.map(e => e.item_id);
|
||||
|
||||
const purchasbleItems = await Item.query()
|
||||
.where('purchasable', true)
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
const purchasbleItemsIds = purchasbleItems.map((item) => item.id);
|
||||
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
|
||||
|
||||
if (notPurchasableItems.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'NOT.PURCHASE.ABLE.ITEMS', code: 600 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new bill and records journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async newBill(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const { ItemEntry } = req.models;
|
||||
async newBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const billDTO: IBillDTO = this.matchedBodyData(req);
|
||||
|
||||
const billOTD: BillOTD = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true
|
||||
});
|
||||
const storedBill = await this.billsService.createBill(tenantId, billOTD);
|
||||
|
||||
return res.status(200).send({ id: storedBill.id });
|
||||
try {
|
||||
const storedBill = await this.billsService.createBill(tenantId, billDTO, user);
|
||||
return res.status(200).send({ id: storedBill.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,18 +162,17 @@ export default class BillsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editBill(req: Request, res: Response) {
|
||||
async editBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: billId } = req.params;
|
||||
const { ItemEntry } = req.models;
|
||||
const { tenantId } = req;
|
||||
const billDTO: IBillEditDTO = this.matchedBodyData(req);
|
||||
|
||||
const billOTD: BillOTD = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true
|
||||
});
|
||||
const editedBill = await this.billsService.editBill(tenantId, billId, billOTD);
|
||||
|
||||
return res.status(200).send({ id: billId });
|
||||
try {
|
||||
const editedBill = await this.billsService.editBill(tenantId, billId, billDTO);
|
||||
return res.status(200).send({ id: billId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,13 +181,17 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getBill(req: Request, res: Response) {
|
||||
async getBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: billId } = req.params;
|
||||
|
||||
const bill = await this.billsService.getBillWithMetadata(tenantId, billId);
|
||||
try {
|
||||
const bill = await this.billsService.getBillWithMetadata(tenantId, billId);
|
||||
|
||||
return res.status(200).send({ bill });
|
||||
return res.status(200).send({ bill });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,13 +200,19 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteBill(req: Request, res: Response) {
|
||||
async deleteBill(req: Request, res: Response, next: NextFunction) {
|
||||
const billId = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
await this.billsService.deleteBill(tenantId, billId);
|
||||
|
||||
return res.status(200).send({ id: billId });
|
||||
try {
|
||||
await this.billsService.deleteBill(tenantId, billId);
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
message: 'The given bill deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,7 +221,50 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async listingBills(req: Request, res: Response) {
|
||||
async billsList(req: Request, res: Response) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'BILL_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_NUMBER_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.NUMBER.EXISTS', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_VENDOR_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_VENDOR_NOT_FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ITEMS_NOT_PURCHASABLE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_ITEMS_NOT_PURCHASABLE', code: 700 }]
|
||||
})
|
||||
}
|
||||
if (error.errorType === 'NOT_PURCHASE_ABLE_ITEMS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'NOT_PURCHASE_ABLE_ITEMS', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ITEMS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check, param, query, ValidationChain, matchedData } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import validateMiddleware from 'api/middleware/validateMiddleware';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import BaseController from 'api/controllers/BaseController';
|
||||
import BillPaymentsService from 'services/Purchases/BillPayments';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
@@ -31,43 +30,34 @@ export default class BillsPayments extends BaseController {
|
||||
...this.billPaymentSchemaValidation,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
|
||||
asyncMiddleware(this.validatePaymentAccount.bind(this)),
|
||||
asyncMiddleware(this.validatePaymentNumber.bind(this)),
|
||||
asyncMiddleware(this.validateEntriesBillsExistance.bind(this)),
|
||||
asyncMiddleware(this.validateVendorsDueAmount.bind(this)),
|
||||
asyncMiddleware(this.createBillPayment.bind(this)),
|
||||
this.handleServiceError,
|
||||
);
|
||||
router.post('/:id', [
|
||||
...this.billPaymentSchemaValidation,
|
||||
...this.specificBillPaymentValidateSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
|
||||
asyncMiddleware(this.validatePaymentAccount.bind(this)),
|
||||
asyncMiddleware(this.validatePaymentNumber.bind(this)),
|
||||
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
|
||||
asyncMiddleware(this.validateEntriesBillsExistance.bind(this)),
|
||||
asyncMiddleware(this.validateVendorsDueAmount.bind(this)),
|
||||
asyncMiddleware(this.editBillPayment.bind(this)),
|
||||
this.handleServiceError,
|
||||
)
|
||||
router.delete('/:id',
|
||||
this.specificBillPaymentValidateSchema,
|
||||
router.delete('/:id', [
|
||||
...this.specificBillPaymentValidateSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
|
||||
asyncMiddleware(this.deleteBillPayment.bind(this)),
|
||||
this.handleServiceError,
|
||||
);
|
||||
router.get('/:id',
|
||||
this.specificBillPaymentValidateSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
|
||||
asyncMiddleware(this.getBillPayment.bind(this)),
|
||||
);
|
||||
router.get('/',
|
||||
this.listingValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getBillsPayments.bind(this))
|
||||
);
|
||||
// router.get('/:id',
|
||||
// this.specificBillPaymentValidateSchema,
|
||||
// this.validationResult,
|
||||
// asyncMiddleware(this.getBillPayment.bind(this)),
|
||||
// );
|
||||
// router.get('/',
|
||||
// this.listingValidationSchema,
|
||||
// this.validationResult,
|
||||
// asyncMiddleware(this.getBillsPayments.bind(this))
|
||||
// );
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -112,186 +102,6 @@ export default class BillsPayments extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the bill payment vendor exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateBillPaymentVendorExistance(req: Request, res: Response, next: any ) {
|
||||
const billPayment = req.body;
|
||||
const { Vendor } = req.models;
|
||||
const isVendorExists = await Vendor.query().findById(billPayment.vendor_id);
|
||||
|
||||
if (!isVendorExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the bill payment existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateBillPaymentExistance(req: Request, res: Response, next: any ) {
|
||||
const { id: billPaymentId } = req.params;
|
||||
const { BillPayment } = req.models;
|
||||
const foundBillPayment = await BillPayment.query().findById(billPaymentId);
|
||||
|
||||
if (!foundBillPayment) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validatePaymentAccount(req: Request, res: Response, next: any) {
|
||||
const { tenantId } = req;
|
||||
const billPayment = { ...req.body };
|
||||
|
||||
const isAccountExists = await this.accountsService.isAccountExists(
|
||||
tenantId,
|
||||
billPayment.payment_account_id
|
||||
);
|
||||
|
||||
if (!isAccountExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment number uniqness.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} res
|
||||
*/
|
||||
async validatePaymentNumber(req: Request, res: Response, next: any) {
|
||||
const billPayment = { ...req.body };
|
||||
const { id: billPaymentId } = req.params;
|
||||
const { BillPayment } = req.models;
|
||||
|
||||
const foundBillPayment = await BillPayment.query()
|
||||
.onBuild((builder: any) => {
|
||||
builder.where('payment_number', billPayment.payment_number)
|
||||
if (billPaymentId) {
|
||||
builder.whereNot('id', billPaymentId);
|
||||
}
|
||||
})
|
||||
.first();
|
||||
|
||||
if (foundBillPayment) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the entries bills ids exist on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async validateEntriesBillsExistance(req: Request, res: Response, next: any) {
|
||||
const { Bill } = req.models;
|
||||
const billPayment = { ...req.body };
|
||||
const entriesBillsIds = billPayment.entries.map((e: any) => e.bill_id);
|
||||
|
||||
// Retrieve not found bills that associated to the given vendor id.
|
||||
const notFoundBillsIds = await Bill.getNotFoundBills(
|
||||
entriesBillsIds,
|
||||
billPayment.vendor_id,
|
||||
);
|
||||
if (notFoundBillsIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate wether the payment amount bigger than the payable amount.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @return {void}
|
||||
*/
|
||||
async validateVendorsDueAmount(req: Request, res: Response, next: Function) {
|
||||
const { Bill } = req.models;
|
||||
const billsIds = req.body.entries.map((entry: any) => entry.bill_id);
|
||||
const storedBills = await Bill.query()
|
||||
.whereIn('id', billsIds);
|
||||
|
||||
const storedBillsMap = new Map(
|
||||
storedBills.map((bill: any) => [bill.id, bill]),
|
||||
);
|
||||
interface invalidPaymentAmountError{
|
||||
index: number,
|
||||
due_amount: number
|
||||
};
|
||||
const hasWrongPaymentAmount: invalidPaymentAmountError[] = [];
|
||||
const { entries } = req.body;
|
||||
|
||||
entries.forEach((entry: any, index: number) => {
|
||||
const entryBill = storedBillsMap.get(entry.bill_id);
|
||||
const { dueAmount } = entryBill;
|
||||
|
||||
if (dueAmount < entry.payment_amount) {
|
||||
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
|
||||
}
|
||||
});
|
||||
if (hasWrongPaymentAmount.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVALID.BILL.PAYMENT.AMOUNT', code: 400, indexes: hasWrongPaymentAmount }]
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the payment receive entries IDs existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { BillPaymentEntry } = req.models;
|
||||
|
||||
const billPayment = { id: req.params.id, ...req.body };
|
||||
const entriesIds = billPayment.entries
|
||||
.filter((entry: any) => entry.id)
|
||||
.map((entry: any) => entry.id);
|
||||
|
||||
const storedEntries = await BillPaymentEntry.query()
|
||||
.where('bill_payment_id', billPayment.id);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bill payment.
|
||||
* @async
|
||||
@@ -299,17 +109,21 @@ export default class BillsPayments extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {Response} res
|
||||
*/
|
||||
async createBillPayment(req: Request, res: Response) {
|
||||
async createBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const billPaymentDTO = this.matchedBodyData(req);
|
||||
|
||||
const billPayment = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
});
|
||||
const storedPayment = await this.billPaymentService
|
||||
.createBillPayment(tenantId, billPayment);
|
||||
try {
|
||||
const billPayment = await this.billPaymentService.createBillPayment(tenantId, billPaymentDTO);
|
||||
|
||||
return res.status(200).send({ id: storedPayment.id });
|
||||
return res.status(200).send({
|
||||
id: billPayment.id,
|
||||
message: 'Payment made has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,28 +131,24 @@ export default class BillsPayments extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editBillPayment(req: Request, res: Response) {
|
||||
async editBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const billPayment = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
});
|
||||
const billPaymentDTO = this.matchedBodyData(req);
|
||||
const { id: billPaymentId } = req.params;
|
||||
const { BillPayment } = req.models;
|
||||
|
||||
const oldBillPayment = await BillPayment.query()
|
||||
.where('id', billPaymentId)
|
||||
.withGraphFetched('entries')
|
||||
.first();
|
||||
|
||||
await this.billPaymentService.editBillPayment(
|
||||
tenantId,
|
||||
billPaymentId,
|
||||
billPayment,
|
||||
oldBillPayment,
|
||||
);
|
||||
return res.status(200).send({ id: 1 });
|
||||
try {
|
||||
const paymentMade = await this.billPaymentService.editBillPayment(
|
||||
tenantId,
|
||||
billPaymentId,
|
||||
billPaymentDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: paymentMade.id,
|
||||
message: 'Payment made has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,15 +158,20 @@ export default class BillsPayments extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @return {Response} res -
|
||||
*/
|
||||
async deleteBillPayment(req: Request, res: Response) {
|
||||
async deleteBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const { id: billPaymentId } = req.params;
|
||||
const billPayment = req.body;
|
||||
|
||||
await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId);
|
||||
try {
|
||||
await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId);
|
||||
|
||||
return res.status(200).send({ id: billPaymentId });
|
||||
return res.status(200).send({
|
||||
id: billPaymentId,
|
||||
message: 'Payment made has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,7 +179,7 @@ export default class BillsPayments extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getBillPayment(req: Request, res: Response) {
|
||||
async getBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: billPaymentId } = req.params;
|
||||
|
||||
@@ -380,7 +195,7 @@ export default class BillsPayments extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async getBillsPayments(req: Request, res: Response) {
|
||||
async getBillsPayments(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req.params;
|
||||
const billPaymentsFilter = this.matchedQueryData(req);
|
||||
|
||||
@@ -397,4 +212,68 @@ export default class BillsPayments extends BaseController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'PAYMENT_MADE_NOT_FOUND') {
|
||||
return res.status(404).send({
|
||||
message: 'Payment made not found.',
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT_ACCOUNT.NOT.CURRENT_ASSET.TYPE', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_PAYMENT_NUMBER_NOT_UNQIUE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === '') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_PAYMENT_ENTRIES_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVALID_BILL_PAYMENT_AMOUNT') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVALID_BILL_PAYMENT_AMOUNT', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILLS_NOT_FOUND', code: 100 }],
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import express from 'express';
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import Bills from 'api/controllers/Purchases/Bills'
|
||||
import BillPayments from 'api/controllers/Purchases/BillsPayments';
|
||||
@@ -7,7 +7,7 @@ import BillPayments from 'api/controllers/Purchases/BillsPayments';
|
||||
export default class PurchasesController {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
const router = Router();
|
||||
|
||||
router.use('/bills', Container.get(Bills).router());
|
||||
router.use('/bill_payments', Container.get(BillPayments).router());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, matchedData } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces';
|
||||
@@ -25,9 +25,9 @@ export default class SalesEstimatesController extends BaseController {
|
||||
'/',
|
||||
this.estimateValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
|
||||
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
|
||||
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
|
||||
asyncMiddleware(this.newEstimate.bind(this))
|
||||
);
|
||||
router.post(
|
||||
@@ -36,11 +36,11 @@ export default class SalesEstimatesController extends BaseController {
|
||||
...this.estimateValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
|
||||
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
|
||||
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
|
||||
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
|
||||
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
|
||||
// asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
|
||||
asyncMiddleware(this.editEstimate.bind(this))
|
||||
);
|
||||
router.delete(
|
||||
@@ -48,14 +48,14 @@ export default class SalesEstimatesController extends BaseController {
|
||||
this.validateSpecificEstimateSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
|
||||
asyncMiddleware(this.deleteEstimate.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
this.validateSpecificEstimateSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
|
||||
// asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
|
||||
asyncMiddleware(this.getEstimate.bind(this))
|
||||
);
|
||||
router.get(
|
||||
@@ -114,120 +114,6 @@ export default class SalesEstimatesController extends BaseController {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the estimate customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const estimate = { ...req.body };
|
||||
const { Customer } = req.models
|
||||
|
||||
const foundCustomer = await Customer.query().findById(estimate.customer_id);
|
||||
|
||||
if (!foundCustomer) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the estimate number unique on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateNumberExistance(req: Request, res: Response, next: Function) {
|
||||
const estimate = { ...req.body };
|
||||
const { tenantId } = req;
|
||||
|
||||
const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique(
|
||||
tenantId,
|
||||
estimate.estimate_number,
|
||||
req.params.id,
|
||||
);
|
||||
if (isEstNumberUnqiue) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the estimate entries items ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const estimate = { ...req.body };
|
||||
const estimateItemsIds = estimate.entries.map(e => e.item_id);
|
||||
|
||||
// Validate items ids in estimate entries exists.
|
||||
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the sale estimate id exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateIdExistance(req: Request, res: Response, next: Function) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
const storedEstimate = await this.saleEstimateService
|
||||
.getEstimate(tenantId, estimateId);
|
||||
|
||||
if (!storedEstimate) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sale invoice entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { ItemEntry } = req.models;
|
||||
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoice = { ...req.body };
|
||||
const entriesIds = saleInvoice.entries
|
||||
.filter(e => e.id)
|
||||
.map((e) => e.id);
|
||||
|
||||
const foundEntries = await ItemEntry.query()
|
||||
.whereIn('id', entriesIds)
|
||||
.where('reference_type', 'SaleInvoice')
|
||||
.where('reference_id', saleInvoiceId);
|
||||
|
||||
if (foundEntries.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle create a new estimate with associated entries.
|
||||
* @param {Request} req -
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class SaleInvoicesController extends BaseController{
|
||||
this.saleInvoiceValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
|
||||
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
|
||||
// asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
|
||||
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
|
||||
asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)),
|
||||
asyncMiddleware(this.newSaleInvoice.bind(this))
|
||||
@@ -46,7 +46,7 @@ export default class SaleInvoicesController extends BaseController{
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
|
||||
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
|
||||
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
|
||||
// asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
|
||||
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
|
||||
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
|
||||
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
|
||||
@@ -312,18 +312,19 @@ export default class SaleInvoicesController extends BaseController{
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async newSaleInvoice(req: Request, res: Response) {
|
||||
async newSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true
|
||||
});
|
||||
const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req);
|
||||
|
||||
// Creates a new sale invoice with associated entries.
|
||||
const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice(
|
||||
tenantId, saleInvoiceOTD,
|
||||
);
|
||||
return res.status(200).send({ id: storedSaleInvoice.id });
|
||||
try {
|
||||
// Creates a new sale invoice with associated entries.
|
||||
const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice(
|
||||
tenantId, saleInvoiceOTD,
|
||||
);
|
||||
return res.status(200).send({ id: storedSaleInvoice.id });
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,18 +333,18 @@ export default class SaleInvoicesController extends BaseController{
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async editSaleInvoice(req: Request, res: Response) {
|
||||
async editSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req);
|
||||
|
||||
const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true
|
||||
});
|
||||
// Update the given sale invoice details.
|
||||
await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD);
|
||||
|
||||
return res.status(200).send({ id: saleInvoiceId });
|
||||
try {
|
||||
// Update the given sale invoice details.
|
||||
await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD);
|
||||
return res.status(200).send({ id: saleInvoiceId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,14 +353,18 @@ export default class SaleInvoicesController extends BaseController{
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async deleteSaleInvoice(req: Request, res: Response) {
|
||||
async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
// Deletes the sale invoice with associated entries and journal transaction.
|
||||
await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId);
|
||||
|
||||
return res.status(200).send({ id: saleInvoiceId });
|
||||
try {
|
||||
// Deletes the sale invoice with associated entries and journal transaction.
|
||||
await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId);
|
||||
|
||||
return res.status(200).send({ id: saleInvoiceId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { check, param, query, matchedData } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import ItemsService from 'services/Items/ItemsService';
|
||||
import SaleReceiptService from 'services/Sales/SalesReceipts';
|
||||
import BaseController from '../BaseController';
|
||||
import { ISaleReceiptDTO } from 'interfaces/SaleReceipt';
|
||||
|
||||
@Service()
|
||||
export default class SalesReceiptsController extends BaseController{
|
||||
@@ -232,20 +233,21 @@ export default class SalesReceiptsController extends BaseController{
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newSaleReceipt(req: Request, res: Response) {
|
||||
async newSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req);
|
||||
|
||||
const saleReceipt = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
});
|
||||
// Store the given sale receipt details with associated entries.
|
||||
const storedSaleReceipt = await this.saleReceiptService
|
||||
.createSaleReceipt(
|
||||
tenantId,
|
||||
saleReceipt,
|
||||
);
|
||||
return res.status(200).send({ id: storedSaleReceipt.id });
|
||||
try {
|
||||
// Store the given sale receipt details with associated entries.
|
||||
const storedSaleReceipt = await this.saleReceiptService
|
||||
.createSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptDTO,
|
||||
);
|
||||
return res.status(200).send({ id: storedSaleReceipt.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,14 +255,18 @@ export default class SalesReceiptsController extends BaseController{
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteSaleReceipt(req: Request, res: Response) {
|
||||
async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
// Deletes the sale receipt.
|
||||
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
|
||||
|
||||
return res.status(200).send({ id: saleReceiptId });
|
||||
try {
|
||||
// Deletes the sale receipt.
|
||||
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
|
||||
|
||||
return res.status(200).send({ id: saleReceiptId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,25 +275,22 @@ export default class SalesReceiptsController extends BaseController{
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editSaleReceipt(req: Request, res: Response) {
|
||||
async editSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const saleReceipt = { ...req.body };
|
||||
const errorReasons = [];
|
||||
|
||||
// Handle all errors with reasons messages.
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
// Update the given sale receipt details.
|
||||
await this.saleReceiptService.editSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
saleReceipt,
|
||||
);
|
||||
|
||||
return res.status(200).send();
|
||||
try {
|
||||
// Update the given sale receipt details.
|
||||
await this.saleReceiptService.editSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
saleReceipt,
|
||||
);
|
||||
return res.status(200).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@ import Settings from 'api/controllers/Settings';
|
||||
import Currencies from 'api/controllers/Currencies';
|
||||
import Customers from 'api/controllers/Contacts/Customers';
|
||||
import Vendors from 'api/controllers/Contacts/Vendors';
|
||||
import Sales from 'api/controllers/Sales'
|
||||
// import Sales from 'api/controllers/Sales'
|
||||
import Purchases from 'api/controllers/Purchases';
|
||||
import Resources from './controllers/Resources';
|
||||
import ExchangeRates from 'api/controllers/ExchangeRates';
|
||||
@@ -95,7 +95,7 @@ export default () => {
|
||||
dashboard.use('/customers', Container.get(Customers).router());
|
||||
dashboard.use('/vendors', Container.get(Vendors).router());
|
||||
// dashboard.use('/sales', Container.get(Sales).router());
|
||||
// dashboard.use('/purchases', Container.get(Purchases).router());
|
||||
dashboard.use('/purchases', Container.get(Purchases).router());
|
||||
dashboard.use('/resources', Container.get(Resources).router());
|
||||
dashboard.use('/exchange_rates', Container.get(ExchangeRates).router());
|
||||
dashboard.use('/media', Container.get(Media).router());
|
||||
|
||||
@@ -9,7 +9,7 @@ exports.up = function(knex) {
|
||||
table.integer('reference_id').index();
|
||||
table.integer('account_id').unsigned().index().references('id').inTable('accounts');
|
||||
table.string('contact_type').nullable().index();
|
||||
table.integer('contact_id').unsigned().nullable().index().references('id').inTable('contacts');
|
||||
table.integer('contact_id').unsigned().nullable().index();
|
||||
table.string('note');
|
||||
table.boolean('draft').defaultTo(false);
|
||||
table.integer('user_id').unsigned().index();
|
||||
|
||||
@@ -14,6 +14,7 @@ exports.up = function(knex) {
|
||||
table.decimal('payment_amount', 13, 3).defaultTo(0);
|
||||
|
||||
table.string('inv_lot_number').index();
|
||||
table.integer('user_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Container from 'typedi';
|
||||
import TenancyService from 'services/Tenancy/TenancyService'
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
exports.up = (knex) => {
|
||||
const tenancyService = Container.get(TenancyService);
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
export interface IBillOTD {};
|
||||
export interface IBill {};
|
||||
|
||||
import { IItemEntry, IItemEntryDTO } from "./ItemEntry";
|
||||
|
||||
export interface IBillDTO {
|
||||
vendorId: number,
|
||||
billNumber: string,
|
||||
billDate: Date,
|
||||
dueDate: Date,
|
||||
referenceNo: string,
|
||||
status: string,
|
||||
note: string,
|
||||
amount: number,
|
||||
paymentAmount: number,
|
||||
entries: IItemEntryDTO[],
|
||||
};
|
||||
|
||||
export interface IBillEditDTO {
|
||||
billDate: Date,
|
||||
dueDate: Date,
|
||||
referenceNo: string,
|
||||
status: string,
|
||||
note: string,
|
||||
amount: number,
|
||||
paymentAmount: number,
|
||||
entries: IItemEntryDTO[],
|
||||
};
|
||||
|
||||
export interface IBill {
|
||||
id?: number,
|
||||
|
||||
vendorId: number,
|
||||
billNumber: string,
|
||||
billDate: Date,
|
||||
dueDate: Date,
|
||||
referenceNo: string,
|
||||
status: string,
|
||||
note: string,
|
||||
amount: number,
|
||||
paymentAmount: number,
|
||||
|
||||
invLotNumber: string,
|
||||
|
||||
entries: IItemEntry[],
|
||||
};
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
|
||||
|
||||
export interface IBillPaymentEntry {
|
||||
id?: number,
|
||||
billPaymentId: number,
|
||||
billId: number,
|
||||
paymentAmount: number,
|
||||
};
|
||||
|
||||
export interface IBillPayment {
|
||||
id?: number,
|
||||
vendorId: number,
|
||||
amount: number,
|
||||
reference: string,
|
||||
billNo: string,
|
||||
paymentAccountId: number,
|
||||
paymentNumber: string,
|
||||
paymentDate: Date,
|
||||
userId: number,
|
||||
entries: IBillPaymentEntry[],
|
||||
}
|
||||
|
||||
export interface IBillPaymentOTD {};
|
||||
export interface IBillPaymentEntryDTO {
|
||||
billId: number,
|
||||
paymentAmount: number,
|
||||
};
|
||||
|
||||
export interface IBillPaymentDTO {
|
||||
vendorId: number,
|
||||
paymentAccountId: number,
|
||||
paymentNumber: string,
|
||||
paymentDate: Date,
|
||||
description: string,
|
||||
reference: string,
|
||||
entries: IBillPaymentEntryDTO[],
|
||||
};
|
||||
@@ -11,4 +11,8 @@ export interface IItemEntry {
|
||||
discount: number,
|
||||
quantity: number,
|
||||
rate: number,
|
||||
}
|
||||
|
||||
export interface IItemEntryDTO {
|
||||
|
||||
}
|
||||
@@ -1,4 +1,25 @@
|
||||
import { IItemEntry } from "./ItemEntry";
|
||||
|
||||
|
||||
export interface ISaleEstimate {};
|
||||
export interface ISaleEstimateOTD {};
|
||||
export interface ISaleEstimate {
|
||||
id?: number,
|
||||
amount: number,
|
||||
customerId: number,
|
||||
estimateDate: Date,
|
||||
reference: string,
|
||||
note: string,
|
||||
termsConditions: string,
|
||||
userId: number,
|
||||
entries: IItemEntry[],
|
||||
|
||||
createdAt?: Date,
|
||||
};
|
||||
export interface ISaleEstimateDTO {
|
||||
customerId: number,
|
||||
estimateDate?: Date,
|
||||
reference: string,
|
||||
estimateNumber: string,
|
||||
entries: IItemEntry[],
|
||||
note: string,
|
||||
termsConditions: string,
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IItemEntry, IItemEntryDTO } from "./ItemEntry";
|
||||
|
||||
export interface ISaleInvoice {
|
||||
id: number,
|
||||
@@ -5,7 +6,7 @@ export interface ISaleInvoice {
|
||||
paymentAmount: number,
|
||||
invoiceDate: Date,
|
||||
dueDate: Date,
|
||||
entries: any[],
|
||||
entries: IItemEntry[],
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceOTD {
|
||||
@@ -14,7 +15,7 @@ export interface ISaleInvoiceOTD {
|
||||
referenceNo: string,
|
||||
invoiceMessage: string,
|
||||
termsConditions: string,
|
||||
entries: any[],
|
||||
entries: IItemEntryDTO[],
|
||||
}
|
||||
|
||||
export interface ISalesInvoicesFilter{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export * from './Model';
|
||||
export * from './InventoryTransaction';
|
||||
export * from './BillPayment';
|
||||
export * from './Bill';
|
||||
export * from './InventoryCostMethod';
|
||||
export * from './ItemEntry';
|
||||
export * from './Item';
|
||||
@@ -25,4 +26,5 @@ export * from './View';
|
||||
export * from './ManualJournal';
|
||||
export * from './Currency';
|
||||
export * from './ExchangeRate';
|
||||
export * from './Media';
|
||||
export * from './Media';
|
||||
export * from './SaleEstimate';
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Container } from 'typedi';
|
||||
|
||||
// Here we import all events.
|
||||
import 'subscribers/authentication';
|
||||
import 'subscribers/organization';
|
||||
import 'subscribers/manualJournals';
|
||||
import 'subscribers/expenses';
|
||||
import 'subscribers/expenses';
|
||||
import 'subscribers/bills';
|
||||
// import 'subscribers/saleInvoices';
|
||||
import 'subscribers/customers';
|
||||
import 'subscribers/vendors';
|
||||
import 'subscribers/paymentMades';
|
||||
@@ -70,13 +70,24 @@ export default class AccountTypeRepository extends TenantRepository {
|
||||
* @param {string} rootType
|
||||
* @return {IAccountType[]}
|
||||
*/
|
||||
getByRootType(rootType: string): IAccountType[] {
|
||||
getByRootType(rootType: string): Promise<IAccountType[]> {
|
||||
const { AccountType } = this.models;
|
||||
return this.cache.get(`accountType.rootType.${rootType}`, () => {
|
||||
return AccountType.query().where('root_type', rootType);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts types of the given child type.
|
||||
* @param {string} childType
|
||||
*/
|
||||
getByChildType(childType: string): Promise<IAccountType[]> {
|
||||
const { AccountType } = this.models;
|
||||
return this.cache.get(`accountType.childType.${childType}`, () => {
|
||||
return AccountType.query().where('child_type', childType);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush repository cache.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import TenantRepository from 'repositories/TenantRepository';
|
||||
import { IContact } from 'interfaces';
|
||||
import Contact from 'models/Contact';
|
||||
|
||||
export default class ContactRepository extends TenantRepository {
|
||||
cache: any;
|
||||
@@ -45,9 +44,11 @@ export default class ContactRepository extends TenantRepository {
|
||||
* Inserts a new contact model.
|
||||
* @param contact
|
||||
*/
|
||||
async insert(contact) {
|
||||
await Contact.query().insert({ ...contact })
|
||||
async insert(contactInput: IContact) {
|
||||
const { Contact } = this.models;
|
||||
const contact = await Contact.query().insert({ ...contactInput })
|
||||
this.flushCache();
|
||||
return contact;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ export default class ContactRepository extends TenantRepository {
|
||||
* @param {IContact} contact - Contact input.
|
||||
*/
|
||||
async update(contactId: number, contact: IContact) {
|
||||
const { Contact } = this.models;
|
||||
await Contact.query().findById(contactId).patch({ ...contact });
|
||||
this.flushCache();
|
||||
}
|
||||
@@ -66,6 +68,7 @@ export default class ContactRepository extends TenantRepository {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteById(contactId: number): Promise<void> {
|
||||
const { Contact } = this.models;
|
||||
await Contact.query().where('id', contactId).delete();
|
||||
this.flushCache();
|
||||
}
|
||||
@@ -75,6 +78,7 @@ export default class ContactRepository extends TenantRepository {
|
||||
* @param {number[]} contactsIds
|
||||
*/
|
||||
async bulkDelete(contactsIds: number[]) {
|
||||
const { Contact } = this.models;
|
||||
await Contact.query().whereIn('id', contactsIds);
|
||||
this.flushCache();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,15 @@ export default class VendorRepository extends TenantRepository {
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendor details of the given id.
|
||||
* @param {number} vendorId - Vendor id.
|
||||
*/
|
||||
findById(vendorId: number) {
|
||||
const { Contact } = this.models;
|
||||
return Contact.query().findById(vendorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the bill that associated to the given vendor id.
|
||||
* @param {number} vendorId - Vendor id.
|
||||
@@ -49,4 +58,23 @@ export default class VendorRepository extends TenantRepository {
|
||||
.whereIn('id', vendorIds)
|
||||
.withGraphFetched('bills');
|
||||
}
|
||||
|
||||
changeBalance(vendorId: number, amount: number) {
|
||||
const { Contact } = this.models;
|
||||
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
|
||||
|
||||
return Contact.query()
|
||||
.where('id', vendorId)
|
||||
[changeMethod]('balance', Math.abs(amount));
|
||||
}
|
||||
|
||||
|
||||
changeDiffBalance(
|
||||
vendorId: number,
|
||||
amount: number,
|
||||
oldAmount: number,
|
||||
oldVendorId?: number,
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class ContactsService {
|
||||
* @param {TContactService} contactService
|
||||
* @return {Promise<IContact>}
|
||||
*/
|
||||
private async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) {
|
||||
public async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId });
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit, difference } from 'lodash';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import JournalPoster from "services/Accounting/JournalPoster";
|
||||
import JournalCommands from "services/Accounting/JournalCommands";
|
||||
import ContactsService from 'services/Contacts/ContactsService';
|
||||
@@ -13,6 +17,7 @@ import {
|
||||
import { ServiceError } from 'exceptions';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import events from 'subscribers/events';
|
||||
|
||||
@Service()
|
||||
export default class CustomersService {
|
||||
@@ -25,6 +30,12 @@ export default class CustomersService {
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Converts customer to contact DTO.
|
||||
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
|
||||
@@ -43,31 +54,44 @@ export default class CustomersService {
|
||||
* Creates a new customer.
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerNewDTO} customerDTO
|
||||
* @return {Promise<void>}
|
||||
* @return {Promise<ICustomer>}
|
||||
*/
|
||||
public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
|
||||
public async newCustomer(
|
||||
tenantId: number,
|
||||
customerDTO: ICustomerNewDTO
|
||||
): Promise<ICustomer> {
|
||||
this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO });
|
||||
|
||||
const contactDTO = this.customerToContactDTO(customerDTO)
|
||||
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
|
||||
|
||||
// Writes the customer opening balance journal entries.
|
||||
if (customer.openingBalance) {
|
||||
await this.writeCustomerOpeningBalanceJournal(
|
||||
tenantId,
|
||||
customer.id,
|
||||
customer.openingBalance,
|
||||
);
|
||||
}
|
||||
this.logger.info('[customer] created successfully.', { tenantId, customerDTO });
|
||||
await this.eventDispatcher.dispatch(events.customers.onCreated);
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits details of the given customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @param {ICustomerEditDTO} customerDTO
|
||||
* @return {Promise<ICustomer>}
|
||||
*/
|
||||
public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
|
||||
public async editCustomer(
|
||||
tenantId: number,
|
||||
customerId: number,
|
||||
customerDTO: ICustomerEditDTO,
|
||||
): Promise<ICustomer> {
|
||||
const contactDTO = this.customerToContactDTO(customerDTO);
|
||||
return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
|
||||
|
||||
this.logger.info('[customer] trying to edit customer.', { tenantId, customerId, customerDTO });
|
||||
const customer = this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
|
||||
|
||||
this.eventDispatcher.dispatch(events.customers.onEdited);
|
||||
this.logger.info('[customer] edited successfully.', { tenantId, customerId });
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,16 +100,31 @@ export default class CustomersService {
|
||||
* @param {number} customerId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteCustomer(tenantId: number, customerId: number) {
|
||||
public async deleteCustomer(tenantId: number, customerId: number): Promise<void> {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
this.logger.info('[customer] trying to delete customer.', { tenantId, customerId });
|
||||
|
||||
await this.getCustomerByIdOrThrowError(tenantId, customerId);
|
||||
await this.customerHasNoInvoicesOrThrowError(tenantId, customerId);
|
||||
|
||||
await Contact.query().findById(customerId).delete();
|
||||
|
||||
await this.eventDispatcher.dispatch(events.customers.onDeleted);
|
||||
this.logger.info('[customer] deleted successfully.', { tenantId, customerId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts customer opening balance journal entries.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} customerId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async revertOpeningBalanceEntries(tenantId: number, customerId: number|number[]) {
|
||||
const id = Array.isArray(customerId) ? customerId : [customerId];
|
||||
|
||||
this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId });
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId, [customerId], 'customer',
|
||||
tenantId, id, 'customer',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,7 +168,7 @@ export default class CustomersService {
|
||||
* @param {number} openingBalance
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async writeCustomerOpeningBalanceJournal(
|
||||
public async writeCustomerOpeningBalanceJournal(
|
||||
tenantId: number,
|
||||
customerId: number,
|
||||
openingBalance: number,
|
||||
@@ -150,7 +189,7 @@ export default class CustomersService {
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
*/
|
||||
getCustomerByIdOrThrowError(tenantId: number, customerId: number) {
|
||||
private getCustomerByIdOrThrowError(tenantId: number, customerId: number) {
|
||||
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer');
|
||||
}
|
||||
|
||||
@@ -159,7 +198,7 @@ export default class CustomersService {
|
||||
* @param {numebr} tenantId
|
||||
* @param {number[]} customersIds
|
||||
*/
|
||||
getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) {
|
||||
private getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) {
|
||||
return this.contactService.getContactsOrThrowErrorNotFound(tenantId, customersIds, 'customer');
|
||||
}
|
||||
|
||||
@@ -169,19 +208,14 @@ export default class CustomersService {
|
||||
* @param {number[]} customersIds
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteBulkCustomers(tenantId: number, customersIds: number[]) {
|
||||
public async deleteBulkCustomers(tenantId: number, customersIds: number[]) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds);
|
||||
await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds);
|
||||
|
||||
await Contact.query().whereIn('id', customersIds).delete();
|
||||
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId,
|
||||
customersIds,
|
||||
'Customer'
|
||||
);
|
||||
await this.eventDispatcher.dispatch(events.customers.onBulkDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,8 +223,10 @@ export default class CustomersService {
|
||||
* or throw service error.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @throws {ServiceError}
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) {
|
||||
private async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) {
|
||||
const { customerRepository } = this.tenancy.repositories(tenantId);
|
||||
const salesInvoice = await customerRepository.getSalesInvoices(customerId);
|
||||
|
||||
@@ -204,8 +240,9 @@ export default class CustomersService {
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} customersIds
|
||||
* @throws {ServiceError}
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) {
|
||||
private async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) {
|
||||
const { customerRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const customersWithInvoices = await customerRepository.customersWithSalesInvoices(
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { difference, rest } from 'lodash';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import JournalPoster from "services/Accounting/JournalPoster";
|
||||
import JournalCommands from "services/Accounting/JournalCommands";
|
||||
import ContactsService from 'services/Contacts/ContactsService';
|
||||
@@ -12,6 +16,7 @@ import {
|
||||
import { ServiceError } from 'exceptions';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import events from 'subscribers/events';
|
||||
|
||||
@Service()
|
||||
export default class VendorsService {
|
||||
@@ -24,12 +29,18 @@ export default class VendorsService {
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Converts vendor to contact DTO.
|
||||
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
|
||||
* @returns {IContactDTO}
|
||||
*/
|
||||
vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) {
|
||||
private vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) {
|
||||
return {
|
||||
...vendorDTO,
|
||||
active: (typeof vendorDTO.active === 'undefined') ?
|
||||
@@ -43,19 +54,15 @@ export default class VendorsService {
|
||||
* @param {IVendorNewDTO} vendorDTO
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) {
|
||||
const contactDTO = this.vendorToContactDTO(vendorDTO);
|
||||
public async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) {
|
||||
this.logger.info('[vendor] trying create a new vendor.', { tenantId, vendorDTO });
|
||||
|
||||
const contactDTO = this.vendorToContactDTO(vendorDTO);
|
||||
const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor');
|
||||
|
||||
// Writes the vendor opening balance journal entries.
|
||||
if (vendor.openingBalance) {
|
||||
await this.writeVendorOpeningBalanceJournal(
|
||||
tenantId,
|
||||
vendor.id,
|
||||
vendor.openingBalance,
|
||||
);
|
||||
}
|
||||
await this.eventDispatcher.dispatch(events.vendors.onCreated, {
|
||||
tenantId, vendorId: vendor.id, vendor,
|
||||
});
|
||||
return vendor;
|
||||
}
|
||||
|
||||
@@ -64,9 +71,13 @@ export default class VendorsService {
|
||||
* @param {number} tenantId
|
||||
* @param {IVendorEditDTO} vendorDTO
|
||||
*/
|
||||
async editVendor(tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO) {
|
||||
public async editVendor(tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO) {
|
||||
const contactDTO = this.vendorToContactDTO(vendorDTO);
|
||||
return this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor');
|
||||
const vendor = await this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor');
|
||||
|
||||
await this.eventDispatcher.dispatch(events.vendors.onEdited);
|
||||
|
||||
return vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +85,7 @@ export default class VendorsService {
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
*/
|
||||
getVendorByIdOrThrowError(tenantId: number, customerId: number) {
|
||||
private getVendorByIdOrThrowError(tenantId: number, customerId: number) {
|
||||
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor');
|
||||
}
|
||||
|
||||
@@ -84,17 +95,17 @@ export default class VendorsService {
|
||||
* @param {number} vendorId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteVendor(tenantId: number, vendorId: number) {
|
||||
public async deleteVendor(tenantId: number, vendorId: number) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
|
||||
await this.getVendorByIdOrThrowError(tenantId, vendorId);
|
||||
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId);
|
||||
|
||||
this.logger.info('[vendor] trying to delete vendor.', { tenantId, vendorId });
|
||||
await Contact.query().findById(vendorId).delete();
|
||||
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId, [vendorId], 'vendor',
|
||||
);
|
||||
await this.eventDispatcher.dispatch(events.vendors.onDeleted, { tenantId, vendorId });
|
||||
this.logger.info('[vendor] deleted successfully.', { tenantId, vendorId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,7 +113,7 @@ export default class VendorsService {
|
||||
* @param {number} tenantId
|
||||
* @param {number} vendorId
|
||||
*/
|
||||
async getVendor(tenantId: number, vendorId: number) {
|
||||
public async getVendor(tenantId: number, vendorId: number) {
|
||||
return this.contactService.getContact(tenantId, vendorId, 'vendor');
|
||||
}
|
||||
|
||||
@@ -113,7 +124,7 @@ export default class VendorsService {
|
||||
* @param {number} openingBalance
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async writeVendorOpeningBalanceJournal(
|
||||
public async writeVendorOpeningBalanceJournal(
|
||||
tenantId: number,
|
||||
vendorId: number,
|
||||
openingBalance: number,
|
||||
@@ -121,20 +132,36 @@ export default class VendorsService {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
this.logger.info('[vendor] writing opening balance journal entries.', { tenantId, vendorId });
|
||||
await journalCommands.vendorOpeningBalance(vendorId, openingBalance)
|
||||
|
||||
|
||||
await Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.saveEntries(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts vendor opening balance journal entries.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} vendorId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async revertOpeningBalanceEntries(tenantId: number, vendorId: number|number[]) {
|
||||
const id = Array.isArray(vendorId) ? vendorId : [vendorId];
|
||||
|
||||
this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId });
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId, id, 'vendor',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given vendors or throw error if one of them not found.
|
||||
* @param {numebr} tenantId
|
||||
* @param {number[]} vendorsIds
|
||||
*/
|
||||
getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) {
|
||||
private getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) {
|
||||
return this.contactService.getContactsOrThrowErrorNotFound(tenantId, vendorsIds, 'vendor');
|
||||
}
|
||||
|
||||
@@ -144,17 +171,19 @@ export default class VendorsService {
|
||||
* @param {number[]} vendorsIds
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteBulkVendors(tenantId: number, vendorsIds: number[]) {
|
||||
public async deleteBulkVendors(
|
||||
tenantId: number,
|
||||
vendorsIds: number[]
|
||||
): Promise<void> {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
|
||||
await this.getVendorsOrThrowErrorNotFound(tenantId, vendorsIds);
|
||||
await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds);
|
||||
|
||||
await Contact.query().whereIn('id', vendorsIds).delete();
|
||||
await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, { tenantId, vendorsIds });
|
||||
|
||||
await this.contactService.revertJEntriesContactsOpeningBalance(
|
||||
tenantId, vendorsIds, 'vendor',
|
||||
);
|
||||
this.logger.info('[vendor] bulk deleted successfully.', { tenantId, vendorsIds });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,7 +191,7 @@ export default class VendorsService {
|
||||
* @param {number} tenantId
|
||||
* @param {number} vendorId
|
||||
*/
|
||||
async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) {
|
||||
private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) {
|
||||
const { vendorRepository } = this.tenancy.repositories(tenantId);
|
||||
const bills = await vendorRepository.getBills(vendorId);
|
||||
|
||||
@@ -177,7 +206,7 @@ export default class VendorsService {
|
||||
* @param {number[]} customersIds
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) {
|
||||
private async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) {
|
||||
const { vendorRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const vendorsWithBills = await vendorRepository.vendorsWithBills(vendorsIds);
|
||||
@@ -197,7 +226,7 @@ export default class VendorsService {
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @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 dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Vendor, vendorsFilter);
|
||||
|
||||
@@ -143,16 +143,21 @@ export default class ExpensesService implements IExpensesService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts expense journal entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
public async revertJournalEntries(
|
||||
tenantId: number,
|
||||
expenseId: number|number[],
|
||||
) {
|
||||
): Promise<void> {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
await journalCommands.revertJournalEntries(expenseId, 'Expense');
|
||||
|
||||
return Promise.all([
|
||||
await Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.deleteEntries(),
|
||||
]);
|
||||
|
||||
@@ -1,15 +1,41 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import { entries, omit, sumBy, difference } from 'lodash';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import moment from 'moment';
|
||||
import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
|
||||
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
|
||||
import events from 'subscribers/events';
|
||||
import {
|
||||
IBill,
|
||||
IBillPaymentDTO,
|
||||
IBillPaymentEntryDTO,
|
||||
IBillPayment,
|
||||
IBillPaymentsFilter,
|
||||
IPaginationMeta,
|
||||
IFilterMeta,
|
||||
IBillPaymentEntry,
|
||||
} from 'interfaces';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import JournalCommands from 'services/Accounting/JournalCommands';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { ServiceError } from 'exceptions';
|
||||
|
||||
const ERRORS = {
|
||||
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
|
||||
PAYMENT_MADE_NOT_FOUND: 'PAYMENT_MADE_NOT_FOUND',
|
||||
BILL_PAYMENT_NUMBER_NOT_UNQIUE: 'BILL_PAYMENT_NUMBER_NOT_UNQIUE',
|
||||
PAYMENT_ACCOUNT_NOT_FOUND: 'PAYMENT_ACCOUNT_NOT_FOUND',
|
||||
PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE',
|
||||
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
|
||||
BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND',
|
||||
INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT',
|
||||
};
|
||||
|
||||
/**
|
||||
* Bill payments service.
|
||||
@@ -29,6 +55,177 @@ export default class BillPaymentsService {
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Validate whether the bill payment vendor exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async getVendorOrThrowError(tenantId: number, vendorId: number) {
|
||||
const { vendorRepository } = this.tenancy.repositories(tenantId);
|
||||
const vendor = await vendorRepository.findById(vendorId);
|
||||
|
||||
if (!vendor) {
|
||||
throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND)
|
||||
}
|
||||
return vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the bill payment existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async getPaymentMadeOrThrowError(tenantid: number, paymentMadeId: number) {
|
||||
const { BillPayment } = this.tenancy.models(tenantid);
|
||||
const billPayment = await BillPayment.query().findById(paymentMadeId);
|
||||
|
||||
if (!billPayment) {
|
||||
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
|
||||
}
|
||||
return billPayment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment account.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} paymentAccountId
|
||||
* @return {Promise<IAccountType>}
|
||||
*/
|
||||
private async getPaymentAccountOrThrowError(tenantId: number, paymentAccountId: number) {
|
||||
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset');
|
||||
const paymentAccount = await accountRepository.findById(paymentAccountId);
|
||||
|
||||
const currentAssetTypesIds = currentAssetTypes.map(type => type.id);
|
||||
|
||||
if (!paymentAccount) {
|
||||
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
if (currentAssetTypesIds.indexOf(paymentAccount.accountTypeId) === -1) {
|
||||
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE);
|
||||
}
|
||||
return paymentAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment number uniqness.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} paymentMadeNumber -
|
||||
* @return {Promise<IBillPayment>}
|
||||
*/
|
||||
private async validatePaymentNumber(tenantId: number, paymentMadeNumber: string, notPaymentMadeId?: string) {
|
||||
const { BillPayment } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundBillPayment = await BillPayment.query()
|
||||
.onBuild((builder: any) => {
|
||||
builder.findOne('payment_number', paymentMadeNumber);
|
||||
|
||||
if (notPaymentMadeId) {
|
||||
builder.whereNot('id', notPaymentMadeId);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundBillPayment) {
|
||||
throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE)
|
||||
}
|
||||
return foundBillPayment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the entries bills ids exist on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async validateBillsExistance(tenantId: number, billPaymentEntries: IBillPaymentEntry[], notVendorId?: number) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId);
|
||||
|
||||
const storedBills = await Bill.query().onBuild((builder) => {
|
||||
builder.whereIn('id', entriesBillsIds);
|
||||
|
||||
if (notVendorId) {
|
||||
builder.where('vendor_id', notVendorId);
|
||||
}
|
||||
});
|
||||
const storedBillsIds = storedBills.map((t: IBill) => t.id);
|
||||
const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds);
|
||||
|
||||
if (notFoundBillsIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate wether the payment amount bigger than the payable amount.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @return {void}
|
||||
*/
|
||||
private async validateBillsDueAmount(tenantId: number, billPaymentEntries: IBillPaymentEntryDTO[]) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
const billsIds = billPaymentEntries.map((entry: IBillPaymentEntryDTO) => entry.billId);
|
||||
|
||||
const storedBills = await Bill.query().whereIn('id', billsIds);
|
||||
const storedBillsMap = new Map(
|
||||
storedBills.map((bill: any) => [bill.id, bill]),
|
||||
);
|
||||
interface invalidPaymentAmountError{
|
||||
index: number,
|
||||
due_amount: number
|
||||
};
|
||||
const hasWrongPaymentAmount: invalidPaymentAmountError[] = [];
|
||||
|
||||
billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => {
|
||||
const entryBill = storedBillsMap.get(entry.billId);
|
||||
const { dueAmount } = entryBill;
|
||||
|
||||
if (dueAmount < entry.paymentAmount) {
|
||||
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
|
||||
}
|
||||
});
|
||||
if (hasWrongPaymentAmount.length > 0) {
|
||||
throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the payment receive entries IDs existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private async validateEntriesIdsExistance(
|
||||
tenantId: number,
|
||||
billPaymentId: number,
|
||||
billPaymentEntries: IBillPaymentEntry[]
|
||||
) {
|
||||
const { BillPaymentEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const entriesIds = billPaymentEntries
|
||||
.filter((entry: any) => entry.id)
|
||||
.map((entry: any) => entry.id);
|
||||
|
||||
const storedEntries = await BillPaymentEntry.query().where('bill_payment_id', billPaymentId);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bill payment transcations and store it to the storage
|
||||
* with associated bills entries and journal transactions.
|
||||
@@ -40,53 +237,39 @@ export default class BillPaymentsService {
|
||||
* - Increment the payment amount of the given vendor bills.
|
||||
* - Decrement the vendor balance.
|
||||
* - Records payment journal entries.
|
||||
* ------
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {BillPaymentDTO} billPayment - Bill payment object.
|
||||
*/
|
||||
public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) {
|
||||
const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
|
||||
public async createBillPayment(
|
||||
tenantId: number,
|
||||
billPaymentDTO: IBillPaymentDTO
|
||||
): Promise<IBillPayment> {
|
||||
this.logger.info('[paymentDate] trying to save payment made.', { tenantId, billPaymentDTO });
|
||||
const { BillPayment } = this.tenancy.models(tenantId);
|
||||
|
||||
const billPayment = {
|
||||
amount: sumBy(billPaymentDTO.entries, 'payment_amount'),
|
||||
...formatDateFields(billPaymentDTO, ['payment_date']),
|
||||
}
|
||||
const storedBillPayment = await BillPayment.query()
|
||||
.insert({
|
||||
...omit(billPayment, ['entries']),
|
||||
const billPaymentObj = {
|
||||
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
|
||||
...formatDateFields(billPaymentDTO, ['paymentDate']),
|
||||
};
|
||||
await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
|
||||
await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId);
|
||||
await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber);
|
||||
await this.validateBillsExistance(tenantId, billPaymentObj.entries);
|
||||
await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
|
||||
|
||||
const billPayment = await BillPayment.query()
|
||||
.insertGraph({
|
||||
...omit(billPaymentObj, ['entries']),
|
||||
entries: billPaymentDTO.entries,
|
||||
});
|
||||
const storeOpers: Promise<any>[] = [];
|
||||
|
||||
billPayment.entries.forEach((entry) => {
|
||||
const oper = BillPaymentEntry.query()
|
||||
.insert({
|
||||
bill_payment_id: storedBillPayment.id,
|
||||
...entry,
|
||||
});
|
||||
// Increment the bill payment amount.
|
||||
const billOper = Bill.changePaymentAmount(
|
||||
entry.bill_id,
|
||||
entry.payment_amount,
|
||||
);
|
||||
storeOpers.push(billOper);
|
||||
storeOpers.push(oper);
|
||||
await this.eventDispatcher.dispatch(events.billPayments.onCreated, {
|
||||
tenantId, billPayment, billPaymentId: billPayment.id,
|
||||
});
|
||||
// Decrement the vendor balance after bills payments.
|
||||
const vendorDecrementOper = Vendor.changeBalance(
|
||||
billPayment.vendor_id,
|
||||
billPayment.amount * -1,
|
||||
);
|
||||
// Records the journal transactions after bills payment
|
||||
// and change diff acoount balance.
|
||||
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries(tenantId, {
|
||||
id: storedBillPayment.id,
|
||||
...billPayment,
|
||||
});
|
||||
await Promise.all([
|
||||
...storeOpers,
|
||||
recordJournalTransaction,
|
||||
vendorDecrementOper,
|
||||
]);
|
||||
return storedBillPayment;
|
||||
this.logger.info('[payment_made] inserted successfully.', { tenantId, billPaymentId: billPayment.id, });
|
||||
|
||||
return billPayment;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,63 +293,31 @@ export default class BillPaymentsService {
|
||||
tenantId: number,
|
||||
billPaymentId: number,
|
||||
billPaymentDTO,
|
||||
oldBillPayment,
|
||||
) {
|
||||
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
|
||||
const billPayment = {
|
||||
amount: sumBy(billPaymentDTO.entries, 'payment_amount'),
|
||||
...formatDateFields(billPaymentDTO, ['payment_date']),
|
||||
};
|
||||
const updateBillPayment = await BillPayment.query()
|
||||
.where('id', billPaymentId)
|
||||
.update({
|
||||
...omit(billPayment, ['entries']),
|
||||
});
|
||||
const opers = [];
|
||||
const entriesHasIds = billPayment.entries.filter((i) => i.id);
|
||||
const entriesHasNoIds = billPayment.entries.filter((e) => !e.id);
|
||||
): Promise<IBillPayment> {
|
||||
const { BillPayment } = this.tenancy.models(tenantId);
|
||||
|
||||
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted(
|
||||
oldBillPayment.entries,
|
||||
entriesHasIds
|
||||
);
|
||||
if (entriesIdsShouldDelete.length > 0) {
|
||||
const deleteOper = BillPaymentEntry.query()
|
||||
.bulkDelete(entriesIdsShouldDelete);
|
||||
opers.push(deleteOper);
|
||||
}
|
||||
// Entries that should be update to the storage.
|
||||
if (entriesHasIds.length > 0) {
|
||||
const updateOper = BillPaymentEntry.query()
|
||||
.bulkUpdate(entriesHasIds, { where: 'id' });
|
||||
opers.push(updateOper);
|
||||
}
|
||||
// Entries that should be inserted to the storage.
|
||||
if (entriesHasNoIds.length > 0) {
|
||||
const insertOper = BillPaymentEntry.query()
|
||||
.bulkInsert(
|
||||
entriesHasNoIds.map((e) => ({ ...e, bill_payment_id: billPaymentId }))
|
||||
);
|
||||
opers.push(insertOper);
|
||||
}
|
||||
// Records the journal transactions after bills payment and change
|
||||
// different acoount balance.
|
||||
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries(tenantId, {
|
||||
id: storedBillPayment.id,
|
||||
...billPayment,
|
||||
});
|
||||
// Change the different vendor balance between the new and old one.
|
||||
const changeDiffBalance = Vendor.changeDiffBalance(
|
||||
billPayment.vendor_id,
|
||||
oldBillPayment.vendorId,
|
||||
billPayment.amount * -1,
|
||||
oldBillPayment.amount * -1,
|
||||
);
|
||||
await Promise.all([
|
||||
...opers,
|
||||
recordJournalTransaction,
|
||||
changeDiffBalance,
|
||||
]);
|
||||
const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId);
|
||||
|
||||
const billPaymentObj = {
|
||||
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
|
||||
...formatDateFields(billPaymentDTO, ['paymentDate']),
|
||||
};
|
||||
|
||||
await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
|
||||
await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId);
|
||||
await this.validateEntriesIdsExistance(tenantId, billPaymentId, billPaymentObj.entries);
|
||||
await this.validateBillsExistance(tenantId, billPaymentObj.entries);
|
||||
await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
|
||||
|
||||
const billPayment = await BillPayment.query()
|
||||
.upsertGraph({
|
||||
id: billPaymentId,
|
||||
...omit(billPaymentObj, ['entries']),
|
||||
});
|
||||
await this.eventDispatcher.dispatch(events.billPayments.onEdited);
|
||||
this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldPaymentMade });
|
||||
|
||||
return billPayment;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,29 +327,16 @@ export default class BillPaymentsService {
|
||||
* @return {Promise}
|
||||
*/
|
||||
public async deleteBillPayment(tenantId: number, billPaymentId: number) {
|
||||
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
|
||||
const billPayment = await BillPayment.query().where('id', billPaymentId).first();
|
||||
const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[bill_payment] trying to delete.', { tenantId, billPaymentId });
|
||||
const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId);
|
||||
|
||||
await BillPayment.query()
|
||||
.where('id', billPaymentId)
|
||||
.delete();
|
||||
await BillPaymentEntry.query().where('bill_payment_id', billPaymentId).delete();
|
||||
await BillPayment.query().where('id', billPaymentId).delete();
|
||||
|
||||
await BillPaymentEntry.query()
|
||||
.where('bill_payment_id', billPaymentId)
|
||||
.delete();
|
||||
|
||||
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
|
||||
billPaymentId,
|
||||
'BillPayment',
|
||||
);
|
||||
const revertVendorBalanceOper = Vendor.changeBalance(
|
||||
billPayment.vendorId,
|
||||
billPayment.amount,
|
||||
);
|
||||
return Promise.all([
|
||||
deleteTransactionsOper,
|
||||
revertVendorBalanceOper,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.billPayments.onDeleted, { tenantId, billPaymentId, oldPaymentMade });
|
||||
this.logger.info('[bill_payment] deleted successfully.', { tenantId, billPaymentId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,15 +345,13 @@ export default class BillPaymentsService {
|
||||
* @param {BillPayment} billPayment
|
||||
* @param {Integer} billPaymentId
|
||||
*/
|
||||
private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
|
||||
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
|
||||
public async recordJournalEntries(tenantId: number, billPayment: IBillPayment) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const paymentAmount = sumBy(billPayment.entries, 'payment_amount');
|
||||
const formattedDate = moment(billPayment.payment_date).format('YYYY-MM-DD');
|
||||
const payableAccount = await this.accountsService.getAccountByType(
|
||||
tenantId,
|
||||
'accounts_payable'
|
||||
);
|
||||
const paymentAmount = sumBy(billPayment.entries, 'paymentAmount');
|
||||
const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD');
|
||||
const payableAccount = await accountRepository.getBySlug('accounts-payable');
|
||||
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const commonJournal = {
|
||||
@@ -238,13 +374,13 @@ export default class BillPaymentsService {
|
||||
...commonJournal,
|
||||
debit: paymentAmount,
|
||||
contactType: 'Vendor',
|
||||
contactId: billPayment.vendor_id,
|
||||
contactId: billPayment.vendorId,
|
||||
account: payableAccount.id,
|
||||
});
|
||||
const creditPaymentAccount = new JournalEntry({
|
||||
...commonJournal,
|
||||
credit: paymentAmount,
|
||||
account: billPayment.payment_account_id,
|
||||
account: billPayment.paymentAccountId,
|
||||
});
|
||||
journal.debit(debitReceivable);
|
||||
journal.credit(creditPaymentAccount);
|
||||
@@ -256,6 +392,24 @@ export default class BillPaymentsService {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts bill payment journal entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} billPaymentId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async revertJournalEntries(tenantId: number, billPaymentId: number) {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
await journalCommands.revertJournalEntries(billPaymentId, 'BillPayment');
|
||||
|
||||
return Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.deleteEntries(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bill payment paginted and filterable list.
|
||||
* @param {number} tenantId
|
||||
@@ -301,18 +455,4 @@ export default class BillPaymentsService {
|
||||
|
||||
return billPayment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the bill payment exists on the storage.
|
||||
* @param {Integer} billPaymentId
|
||||
* @return {boolean}
|
||||
*/
|
||||
async isBillPaymentExists(tenantId: number, billPaymentId: number) {
|
||||
const { BillPayment } = this.tenancy.models(tenantId);
|
||||
const billPayment = await BillPayment.query()
|
||||
.where('id', billPaymentId)
|
||||
.first();
|
||||
|
||||
return (billPayment.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
import { omit, sumBy, pick } from 'lodash';
|
||||
import { omit, sumBy, pick, difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import{ IBillOTD, IBill, IItem } from 'interfaces';
|
||||
import {
|
||||
IBillDTO,
|
||||
IBill,
|
||||
IItem,
|
||||
ISystemUser,
|
||||
IItemEntry,
|
||||
IItemEntryDTO,
|
||||
IBillEditDTO,
|
||||
} from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import ItemsService from 'services/Items/ItemsService';
|
||||
|
||||
const ERRORS = {
|
||||
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
|
||||
BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND',
|
||||
BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE',
|
||||
BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS',
|
||||
BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND',
|
||||
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
|
||||
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
|
||||
};
|
||||
|
||||
/**
|
||||
* Vendor bills services.
|
||||
@@ -24,9 +47,138 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
@Inject()
|
||||
accountsService: AccountsService;
|
||||
|
||||
@Inject()
|
||||
itemsService: ItemsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Validates whether the vendor is exist.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async getVendorOrThrowError(tenantId: number, vendorId: number) {
|
||||
const { vendorRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[bill] trying to get vendor.', { tenantId, vendorId });
|
||||
const foundVendor = await vendorRepository.findById(vendorId);
|
||||
|
||||
if (!foundVendor) {
|
||||
this.logger.info('[bill] the given vendor not found.', { tenantId, vendorId });
|
||||
throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND);
|
||||
}
|
||||
return foundVendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given bill existance.
|
||||
* @async
|
||||
* @param {number} tenantId -
|
||||
* @param {number} billId -
|
||||
*/
|
||||
private async getBillOrThrowError(tenantId: number, billId: number) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[bill] trying to get bill.', { tenantId, billId });
|
||||
const foundBill = await Bill.query().findById(billId).withGraphFetched('entries');
|
||||
|
||||
if (!foundBill) {
|
||||
this.logger.info('[bill] the given bill not found.', { tenantId, billId });
|
||||
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
|
||||
}
|
||||
return foundBill;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries items ids.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async validateItemsIdsExistance(tenantId: number, billEntries: IItemEntryDTO[]) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const itemsIds = billEntries.map((e) => e.itemId);
|
||||
|
||||
const foundItems = await Item.query().whereIn('id', itemsIds);
|
||||
|
||||
const foundItemsIds = foundItems.map((item: IItem) => item.id);
|
||||
const notFoundItemsIds = difference(itemsIds, foundItemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.BILL_ITEMS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the bill number existance.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async validateBillNumberExists(tenantId: number, billNumber: string) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
const foundBills = await Bill.query().where('bill_number', billNumber);
|
||||
|
||||
if (foundBills.length > 0) {
|
||||
throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEntriesIdsExistance(tenantId: number, billId: number, billEntries: any) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
const entriesIds = billEntries.filter((e) => e.id).map((e) => e.id);
|
||||
|
||||
const storedEntries = await ItemEntry.query()
|
||||
.whereIn('reference_id', [billId])
|
||||
.whereIn('reference_type', ['Bill']);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the entries items that not purchase-able.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async validateNonPurchasableEntriesItems(tenantId: number, billEntries: any) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const itemsIds = billEntries.map((e: IItemEntry) => e.itemId);
|
||||
|
||||
const purchasbleItems = await Item.query()
|
||||
.where('purchasable', true)
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
const purchasbleItemsIds = purchasbleItems.map((item: IItem) => item.id);
|
||||
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
|
||||
|
||||
if (notPurchasableItems.length > 0) {
|
||||
throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts bill DTO to model.
|
||||
* @param {number} tenantId
|
||||
@@ -35,13 +187,13 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
*
|
||||
* @returns {IBill}
|
||||
*/
|
||||
async billDTOToModel(tenantId: number, billDTO: IBillOTD, oldBill?: IBill) {
|
||||
private async billDTOToModel(tenantId: number, billDTO: IBillDTO, oldBill?: IBill) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
let invLotNumber = oldBill?.invLotNumber;
|
||||
|
||||
if (!invLotNumber) {
|
||||
invLotNumber = await this.inventoryService.nextLotNumber(tenantId);
|
||||
}
|
||||
// if (!invLotNumber) {
|
||||
// invLotNumber = await this.inventoryService.nextLotNumber(tenantId);
|
||||
// }
|
||||
const entries = billDTO.entries.map((entry) => ({
|
||||
...entry,
|
||||
amount: ItemEntry.calcAmount(entry),
|
||||
@@ -58,7 +210,7 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
|
||||
/**
|
||||
* Creates a new bill and stored it to the storage.
|
||||
*
|
||||
* ----
|
||||
* Precedures.
|
||||
* ----
|
||||
* - Insert bill transactions to the storage.
|
||||
@@ -67,55 +219,43 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
* - Record bill journal transactions on the given accounts.
|
||||
* - Record bill items inventory transactions.
|
||||
* ----
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @param {IBillOTD} billDTO -
|
||||
* @return {void}
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @param {IBillDTO} billDTO -
|
||||
* @return {Promise<IBill>}
|
||||
*/
|
||||
async createBill(tenantId: number, billDTO: IBillOTD) {
|
||||
const { Vendor, Bill, ItemEntry } = this.tenancy.models(tenantId);
|
||||
public async createBill(
|
||||
tenantId: number,
|
||||
billDTO: IBillDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<IBill> {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
const bill = await this.billDTOToModel(tenantId, billDTO);
|
||||
const saveEntriesOpers = [];
|
||||
this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO });
|
||||
const billObj = await this.billDTOToModel(tenantId, billDTO);
|
||||
|
||||
const storedBill = await Bill.query()
|
||||
.insert({
|
||||
...omit(bill, ['entries']),
|
||||
});
|
||||
bill.entries.forEach((entry) => {
|
||||
const oper = ItemEntry.query()
|
||||
.insertAndFetch({
|
||||
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
|
||||
await this.validateBillNumberExists(tenantId, billDTO.billNumber);
|
||||
|
||||
await this.validateItemsIdsExistance(tenantId, billDTO.entries);
|
||||
await this.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
|
||||
|
||||
const bill = await Bill.query()
|
||||
.insertGraph({
|
||||
...omit(billObj, ['entries']),
|
||||
userId: authorizedUser.id,
|
||||
entries: billDTO.entries.map((entry) => ({
|
||||
reference_type: 'Bill',
|
||||
reference_id: storedBill.id,
|
||||
...omit(entry, ['amount']),
|
||||
}).then((itemEntry) => {
|
||||
entry.id = itemEntry.id;
|
||||
});
|
||||
saveEntriesOpers.push(oper);
|
||||
...omit(entry, ['amount', 'id']),
|
||||
})),
|
||||
});
|
||||
|
||||
// Triggers `onBillCreated` event.
|
||||
await this.eventDispatcher.dispatch(events.bills.onCreated, {
|
||||
tenantId, bill, billId: bill.id,
|
||||
});
|
||||
// Await save all bill entries operations.
|
||||
await Promise.all([...saveEntriesOpers]);
|
||||
this.logger.info('[bill] bill inserted successfully.', { tenantId, billId: bill.id });
|
||||
|
||||
// Increments vendor balance.
|
||||
const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount);
|
||||
|
||||
// Rewrite the inventory transactions for inventory items.
|
||||
const writeInvTransactionsOper = this.recordInventoryTransactions(
|
||||
tenantId, bill, storedBill.id
|
||||
);
|
||||
// Writes the journal entries for the given bill transaction.
|
||||
const writeJEntriesOper = this.recordJournalTransactions(tenantId, {
|
||||
id: storedBill.id, ...bill,
|
||||
});
|
||||
await Promise.all([
|
||||
incrementOper,
|
||||
writeInvTransactionsOper,
|
||||
writeJEntriesOper,
|
||||
]);
|
||||
// Schedule bill re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeBillItemsCost(tenantId, bill);
|
||||
|
||||
return storedBill;
|
||||
return bill;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,55 +272,34 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
*
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @param {Integer} billId - The given bill id.
|
||||
* @param {billDTO} billDTO - The given new bill details.
|
||||
* @param {IBillEditDTO} billDTO - The given new bill details.
|
||||
* @return {Promise<IBill>}
|
||||
*/
|
||||
async editBill(tenantId: number, billId: number, billDTO: billDTO) {
|
||||
const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldBill = await Bill.query().findById(billId);
|
||||
const bill = this.billDTOToModel(tenantId, billDTO, oldBill);
|
||||
public async editBill(
|
||||
tenantId: number,
|
||||
billId: number,
|
||||
billDTO: IBillEditDTO,
|
||||
): Promise<IBill> {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
|
||||
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
||||
const billObj = this.billDTOToModel(tenantId, billDTO, oldBill);
|
||||
|
||||
// Update the bill transaction.
|
||||
const updatedBill = await Bill.query()
|
||||
.where('id', billId)
|
||||
.update({
|
||||
...omit(bill, ['entries', 'invLotNumber'])
|
||||
const bill = await Bill.query()
|
||||
.upsertGraph({
|
||||
id: billId,
|
||||
...omit(billObj, ['entries', 'invLotNumber']),
|
||||
entries: billDTO.entries.map((entry) => ({
|
||||
reference_type: 'Bill',
|
||||
...omit(entry, ['amount']),
|
||||
}))
|
||||
});
|
||||
// Old stored entries.
|
||||
const storedEntries = await ItemEntry.query()
|
||||
.where('reference_id', billId)
|
||||
.where('reference_type', 'Bill');
|
||||
// Triggers event `onBillEdited`.
|
||||
await this.eventDispatcher.dispatch(events.bills.onEdited, { tenantId, billId, oldBill, bill });
|
||||
|
||||
// Patch the bill entries.
|
||||
const patchEntriesOper = HasItemsEntries.patchItemsEntries(
|
||||
bill.entries, storedEntries, 'Bill', billId,
|
||||
);
|
||||
// Changes the diff vendor balance between old and new amount.
|
||||
const changeVendorBalanceOper = Vendor.changeDiffBalance(
|
||||
bill.vendor_id,
|
||||
oldBill.vendorId,
|
||||
bill.amount,
|
||||
oldBill.amount,
|
||||
);
|
||||
// Re-write the inventory transactions for inventory items.
|
||||
const writeInvTransactionsOper = this.recordInventoryTransactions(
|
||||
tenantId, bill, billId, true
|
||||
);
|
||||
// Writes the journal entries for the given bill transaction.
|
||||
const writeJEntriesOper = this.recordJournalTransactions(tenantId, {
|
||||
id: billId,
|
||||
...bill,
|
||||
}, billId);
|
||||
|
||||
await Promise.all([
|
||||
patchEntriesOper,
|
||||
changeVendorBalanceOper,
|
||||
writeInvTransactionsOper,
|
||||
writeJEntriesOper,
|
||||
]);
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeBillItemsCost(tenantId, bill);
|
||||
return bill;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,13 +307,10 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
* @param {Integer} billId
|
||||
* @return {void}
|
||||
*/
|
||||
async deleteBill(tenantId: number, billId: number) {
|
||||
const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId);
|
||||
public async deleteBill(tenantId: number, billId: number) {
|
||||
const { Bill, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const bill = await Bill.query()
|
||||
.where('id', billId)
|
||||
.withGraphFetched('entries')
|
||||
.first();
|
||||
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
||||
|
||||
// Delete all associated bill entries.
|
||||
const deleteBillEntriesOper = ItemEntry.query()
|
||||
@@ -205,28 +321,10 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
// Delete the bill transaction.
|
||||
const deleteBillOper = Bill.query().where('id', billId).delete();
|
||||
|
||||
// Delete associated bill journal transactions.
|
||||
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
|
||||
billId,
|
||||
'Bill'
|
||||
);
|
||||
// Delete bill associated inventory transactions.
|
||||
const deleteInventoryTransOper = this.inventoryService.deleteInventoryTransactions(
|
||||
tenantId, billId, 'Bill'
|
||||
);
|
||||
// Revert vendor balance.
|
||||
const revertVendorBalance = Vendor.changeBalance(bill.vendorId, bill.amount * -1);
|
||||
|
||||
await Promise.all([
|
||||
deleteBillOper,
|
||||
deleteBillEntriesOper,
|
||||
deleteTransactionsOper,
|
||||
deleteInventoryTransOper,
|
||||
revertVendorBalance,
|
||||
]);
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeBillItemsCost(tenantId, bill);
|
||||
await Promise.all([deleteBillEntriesOper, deleteBillOper]);
|
||||
|
||||
// Triggers `onBillDeleted` event.
|
||||
await this.eventDispatcher.dispatch(events.bills.onDeleted, { tenantId, billId, oldBill });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,20 +360,18 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
* @param {IBill} bill
|
||||
* @param {Integer} billId
|
||||
*/
|
||||
async recordJournalTransactions(tenantId: number, bill: any, billId?: number) {
|
||||
const { AccountTransaction, Item } = this.tenancy.models(tenantId);
|
||||
async recordJournalTransactions(tenantId: number, bill: IBill, billId?: number) {
|
||||
const { AccountTransaction, Item, ItemEntry } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const entriesItemsIds = bill.entries.map((entry) => entry.item_id);
|
||||
const payableTotal = sumBy(bill.entries, 'amount');
|
||||
const formattedDate = moment(bill.bill_date).format('YYYY-MM-DD');
|
||||
const entriesItemsIds = bill.entries.map((entry) => entry.itemId);
|
||||
const formattedDate = moment(bill.billDate).format('YYYY-MM-DD');
|
||||
|
||||
const storedItems = await Item.query()
|
||||
.whereIn('id', entriesItemsIds);
|
||||
const storedItems = await Item.query().whereIn('id', entriesItemsIds);
|
||||
|
||||
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
|
||||
const payableAccount = await this.accountsService.getAccountByType(
|
||||
tenantId, 'accounts_payable'
|
||||
);
|
||||
const payableAccount = await accountRepository.getBySlug('accounts-payable');
|
||||
|
||||
const journal = new JournalPoster(tenantId);
|
||||
|
||||
const commonJournalMeta = {
|
||||
@@ -284,7 +380,7 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
referenceId: bill.id,
|
||||
referenceType: 'Bill',
|
||||
date: formattedDate,
|
||||
accural: true,
|
||||
userId: bill.userId,
|
||||
};
|
||||
if (billId) {
|
||||
const transactions = await AccountTransaction.query()
|
||||
@@ -297,23 +393,26 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
}
|
||||
const payableEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
credit: payableTotal,
|
||||
credit: bill.amount,
|
||||
account: payableAccount.id,
|
||||
contactId: bill.vendor_id,
|
||||
contactId: bill.vendorId,
|
||||
contactType: 'Vendor',
|
||||
index: 1,
|
||||
});
|
||||
journal.credit(payableEntry);
|
||||
|
||||
bill.entries.forEach((entry) => {
|
||||
const item: IItem = storedItemsMap.get(entry.item_id);
|
||||
bill.entries.forEach((entry, index) => {
|
||||
const item: IItem = storedItemsMap.get(entry.itemId);
|
||||
const amount = ItemEntry.calcAmount(entry);
|
||||
|
||||
const debitEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
debit: entry.amount,
|
||||
debit: amount,
|
||||
account:
|
||||
['inventory'].indexOf(item.type) !== -1
|
||||
? item.inventoryAccountId
|
||||
: item.costAccountId,
|
||||
index: index + 2,
|
||||
});
|
||||
journal.debit(debitEntry);
|
||||
});
|
||||
@@ -324,44 +423,6 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the bill exists on the storage.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @param {Integer} billId - The given bill id.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
async isBillExists(tenantId: number, billId: number) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundBills = await Bill.query().where('id', billId);
|
||||
return foundBills.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the given bills exist on the storage in bulk.
|
||||
* @param {Array} billsIds
|
||||
* @return {Boolean}
|
||||
*/
|
||||
async isBillsExist(tenantId: number, billsIds: number[]) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
const bills = await Bill.query().whereIn('id', billsIds);
|
||||
return bills.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the given bill id exists on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {Integer} billNumber
|
||||
* @return {boolean}
|
||||
*/
|
||||
async isBillNoExists(tenantId: number, billNumber : string) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundBills = await Bill.query()
|
||||
.where('bill_number', billNumber);
|
||||
return foundBills.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given bill details with associated items entries.
|
||||
@@ -370,8 +431,7 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
*/
|
||||
getBill(tenantId: number, billId: number) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
return Bill.query().where('id', billId).first();
|
||||
return Bill.query().findById(billId).withGraphFetched('entries');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import JournalCommands from 'services/Accounting/JournalCommands';
|
||||
|
||||
@Service()
|
||||
export default class JournalPosterService {
|
||||
@@ -19,20 +20,14 @@ export default class JournalPosterService {
|
||||
referenceId: number,
|
||||
referenceType: string
|
||||
) {
|
||||
const { Account, AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommand = new JournalCommands(journal);
|
||||
|
||||
const transactions = await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('reference_type', [referenceType])
|
||||
.where('reference_id', referenceId)
|
||||
.withGraphFetched('account.type');
|
||||
await journalCommand.revertJournalEntries(referenceId, referenceType);
|
||||
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
journal.loadEntries(transactions);
|
||||
journal.removeEntries();
|
||||
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveBalance()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { omit, sumBy, chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import { IPaymentReceiveOTD } from 'interfaces';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
@@ -34,6 +39,186 @@ export default class PaymentReceiveService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Validates the payment receive number existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validatePaymentReceiveNoExistance(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const isPaymentNoExists = await this.paymentReceiveService.isPaymentReceiveNoExists(
|
||||
tenantId,
|
||||
req.body.payment_receive_no,
|
||||
req.params.id,
|
||||
);
|
||||
if (isPaymentNoExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.RECEIVE.NUMBER.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment receive existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validatePaymentReceiveExistance(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const isPaymentNoExists = await this.paymentReceiveService
|
||||
.isPaymentReceiveExists(
|
||||
tenantId,
|
||||
req.params.id
|
||||
);
|
||||
if (!isPaymentNoExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.RECEIVE.NOT.EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the deposit account id existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateDepositAccount(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const isDepositAccExists = await this.accountsService.isAccountExists(
|
||||
tenantId,
|
||||
req.body.deposit_account_id
|
||||
);
|
||||
if (!isDepositAccExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the `customer_id` existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const { Customer } = req.models;
|
||||
|
||||
const isCustomerExists = await Customer.query().findById(req.body.customer_id);
|
||||
|
||||
if (!isCustomerExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the invoices IDs existance.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {Function} next -
|
||||
*/
|
||||
async validateInvoicesIDs(req: Request, res: Response, next: Function) {
|
||||
const paymentReceive = { ...req.body };
|
||||
const { tenantId } = req;
|
||||
const invoicesIds = paymentReceive.entries
|
||||
.map((e) => e.invoice_id);
|
||||
|
||||
const notFoundInvoicesIDs = await this.saleInvoiceService.isInvoicesExist(
|
||||
tenantId,
|
||||
invoicesIds,
|
||||
paymentReceive.customer_id,
|
||||
);
|
||||
if (notFoundInvoicesIDs.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVOICES.IDS.NOT.FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates entries invoice payment amount.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {Function} next -
|
||||
*/
|
||||
async validateInvoicesPaymentsAmount(req: Request, res: Response, next: Function) {
|
||||
const { SaleInvoice } = req.models;
|
||||
const invoicesIds = req.body.entries.map((e) => e.invoice_id);
|
||||
|
||||
const storedInvoices = await SaleInvoice.query()
|
||||
.whereIn('id', invoicesIds);
|
||||
|
||||
const storedInvoicesMap = new Map(
|
||||
storedInvoices.map((invoice) => [invoice.id, invoice])
|
||||
);
|
||||
const hasWrongPaymentAmount: any[] = [];
|
||||
|
||||
req.body.entries.forEach((entry, index: number) => {
|
||||
const entryInvoice = storedInvoicesMap.get(entry.invoice_id);
|
||||
const { dueAmount } = entryInvoice;
|
||||
|
||||
if (dueAmount < entry.payment_amount) {
|
||||
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
|
||||
}
|
||||
});
|
||||
if (hasWrongPaymentAmount.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVOICE.PAYMENT.AMOUNT',
|
||||
code: 200,
|
||||
indexes: hasWrongPaymentAmount,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the payment receive entries IDs existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const paymentReceive = { id: req.params.id, ...req.body };
|
||||
const entriesIds = paymentReceive.entries
|
||||
.filter(entry => entry.id)
|
||||
.map(entry => entry.id);
|
||||
|
||||
const { PaymentReceiveEntry } = req.models;
|
||||
|
||||
const storedEntries = await PaymentReceiveEntry.query()
|
||||
.where('payment_receive_id', paymentReceive.id);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new payment receive and store it to the storage
|
||||
* with associated invoices payment and journal transactions.
|
||||
@@ -53,9 +238,10 @@ export default class PaymentReceiveService {
|
||||
|
||||
this.logger.info('[payment_receive] inserting to the storage.');
|
||||
const storedPaymentReceive = await PaymentReceive.query()
|
||||
.insert({
|
||||
.insertGraph({
|
||||
amount: paymentAmount,
|
||||
...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']),
|
||||
entries: paymentReceive.entries.map((entry) => ({ ...entry })),
|
||||
});
|
||||
const storeOpers: Array<any> = [];
|
||||
|
||||
@@ -92,6 +278,8 @@ export default class PaymentReceiveService {
|
||||
customerIncrementOper,
|
||||
recordJournalTransactions,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated);
|
||||
|
||||
return storedPaymentReceive;
|
||||
}
|
||||
|
||||
@@ -186,6 +374,7 @@ export default class PaymentReceiveService {
|
||||
changeCustomerBalance,
|
||||
diffInvoicePaymentAmount,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,6 +428,7 @@ export default class PaymentReceiveService {
|
||||
revertCustomerBalance,
|
||||
revertInvoicesPaymentAmount,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { omit, difference, sumBy, mixin } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces';
|
||||
import { IEstimatesFilter, IFilterMeta, IPaginationMeta, ISaleEstimate, ISaleEstimateDTO } from 'interfaces';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import { formatDateFields } from 'utils';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import events from 'subscribers/events';
|
||||
|
||||
/**
|
||||
* Sale estimate service.
|
||||
@@ -24,14 +29,132 @@ export default class SaleEstimateService {
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
|
||||
/**
|
||||
* Validate whether the estimate customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const estimate = { ...req.body };
|
||||
const { Customer } = req.models
|
||||
|
||||
const foundCustomer = await Customer.query().findById(estimate.customer_id);
|
||||
|
||||
if (!foundCustomer) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the estimate number unique on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateNumberExistance(req: Request, res: Response, next: Function) {
|
||||
const estimate = { ...req.body };
|
||||
const { tenantId } = req;
|
||||
|
||||
const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique(
|
||||
tenantId,
|
||||
estimate.estimate_number,
|
||||
req.params.id,
|
||||
);
|
||||
if (isEstNumberUnqiue) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the estimate entries items ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const estimate = { ...req.body };
|
||||
const estimateItemsIds = estimate.entries.map(e => e.item_id);
|
||||
|
||||
// Validate items ids in estimate entries exists.
|
||||
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the sale estimate id exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateIdExistance(req: Request, res: Response, next: Function) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
const storedEstimate = await this.saleEstimateService
|
||||
.getEstimate(tenantId, estimateId);
|
||||
|
||||
if (!storedEstimate) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sale invoice entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { ItemEntry } = req.models;
|
||||
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoice = { ...req.body };
|
||||
const entriesIds = saleInvoice.entries
|
||||
.filter(e => e.id)
|
||||
.map((e) => e.id);
|
||||
|
||||
const foundEntries = await ItemEntry.query()
|
||||
.whereIn('id', entriesIds)
|
||||
.where('reference_type', 'SaleInvoice')
|
||||
.where('reference_id', saleInvoiceId);
|
||||
|
||||
if (foundEntries.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new estimate with associated entries.
|
||||
* @async
|
||||
* @param {number} tenantId - The tenant id.
|
||||
* @param {EstimateDTO} estimate
|
||||
* @return {void}
|
||||
* @return {Promise<ISaleEstimate>}
|
||||
*/
|
||||
async createEstimate(tenantId: number, estimateDTO: any) {
|
||||
async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
|
||||
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
@@ -44,22 +167,15 @@ export default class SaleEstimateService {
|
||||
const storedEstimate = await SaleEstimate.query()
|
||||
.insert({
|
||||
...omit(estimate, ['entries']),
|
||||
});
|
||||
const storeEstimateEntriesOpers: any[] = [];
|
||||
|
||||
this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.');
|
||||
estimate.entries.forEach((entry: any) => {
|
||||
const oper = ItemEntry.query()
|
||||
.insert({
|
||||
entries: estimate.entries.map((entry) => ({
|
||||
reference_type: 'SaleEstimate',
|
||||
reference_id: storedEstimate.id,
|
||||
...omit(entry, ['total', 'amount', 'id']),
|
||||
});
|
||||
storeEstimateEntriesOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...storeEstimateEntriesOpers]);
|
||||
}))
|
||||
});
|
||||
|
||||
this.logger.info('[sale_estimate] insert sale estimated success.');
|
||||
await this.eventDispatcher.dispatch(events.saleEstimates.onCreated);
|
||||
|
||||
return storedEstimate;
|
||||
}
|
||||
@@ -72,7 +188,7 @@ export default class SaleEstimateService {
|
||||
* @param {EstimateDTO} estimate
|
||||
* @return {void}
|
||||
*/
|
||||
async editEstimate(tenantId: number, estimateId: number, estimateDTO: any) {
|
||||
async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise<void> {
|
||||
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
|
||||
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
|
||||
@@ -89,16 +205,14 @@ export default class SaleEstimateService {
|
||||
.where('reference_id', estimateId)
|
||||
.where('reference_type', 'SaleEstimate');
|
||||
|
||||
const patchItemsEntries = this.itemsEntriesService.patchItemsEntries(
|
||||
await this.itemsEntriesService.patchItemsEntries(
|
||||
tenantId,
|
||||
estimate.entries,
|
||||
storedEstimateEntries,
|
||||
'SaleEstimate',
|
||||
estimateId,
|
||||
);
|
||||
return Promise.all([
|
||||
patchItemsEntries,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.saleEstimates.onEdited);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +232,9 @@ export default class SaleEstimateService {
|
||||
.delete();
|
||||
|
||||
await SaleEstimate.query().where('id', estimateId).delete();
|
||||
this.logger.info('[sale_estimate] deleted successfully.', { tenantId, estimateId });
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleEstimates.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,15 @@ import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceOTD,
|
||||
IItemEntry,
|
||||
ISalesInvoicesFilter,
|
||||
IPaginationMeta,
|
||||
IFilterMeta
|
||||
} from 'interfaces';
|
||||
import events from 'subscribers/events';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
@@ -12,6 +20,16 @@ import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import ItemsService from 'services/Items/ItemsService';
|
||||
|
||||
|
||||
const ERRORS = {
|
||||
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
|
||||
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
|
||||
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
|
||||
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
|
||||
}
|
||||
|
||||
/**
|
||||
* Sales invoices service
|
||||
@@ -37,6 +55,81 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject()
|
||||
itemsService: ItemsService;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
*/
|
||||
private async getSaleInvoiceOrThrowError(tenantId: number, saleInvoiceId: number): Promise<ISaleInvoice> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const saleInvoice = await SaleInvoice.query().where('id', saleInvoiceId);
|
||||
|
||||
if (!saleInvoice) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
|
||||
}
|
||||
return saleInvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale invoice number unqiue on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceNo
|
||||
* @param {number} notSaleInvoiceId
|
||||
*/
|
||||
private async validateSaleInvoiceNoUniquiness(tenantId: number, saleInvoiceNo: string, notSaleInvoiceId: number) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundSaleInvoice = await SaleInvoice.query()
|
||||
.onBuild((query: any) => {
|
||||
query.where('invoice_no', saleInvoiceNo);
|
||||
|
||||
if (notSaleInvoiceId) {
|
||||
query.whereNot('id', notSaleInvoiceId);
|
||||
}
|
||||
return query;
|
||||
});
|
||||
|
||||
if (foundSaleInvoice.length > 0) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NO_NOT_UNIQUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates sale invoice items that not sellable.
|
||||
*/
|
||||
private async validateNonSellableEntriesItems(tenantId: number, saleInvoiceEntries: any) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const itemsIds = saleInvoiceEntries.map(e => e.itemId);
|
||||
|
||||
const sellableItems = await Item.query().where('sellable', true).whereIn('id', itemsIds);
|
||||
|
||||
const sellableItemsIds = sellableItems.map((item) => item.id);
|
||||
const notSellableItems = difference(itemsIds, sellableItemsIds);
|
||||
|
||||
if (notSellableItems.length > 0) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {} saleInvoiceEntries
|
||||
*/
|
||||
validateEntriesIdsExistance(tenantId: number, saleInvoiceEntries: any) {
|
||||
const entriesItemsIds = saleInvoiceEntries.map((e) => e.item_id);
|
||||
|
||||
const isItemsIdsExists = await this.itemsService.isItemsIdsExists(
|
||||
tenantId, entriesItemsIds,
|
||||
);
|
||||
if (isItemsIdsExists.length > 0) {
|
||||
throw new ServiceError(ERRORS.ENTRIES_ITEMS_IDS_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sale invoices and store it to the storage
|
||||
* with associated to entries and journal transactions.
|
||||
@@ -58,6 +151,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
invLotNumber,
|
||||
};
|
||||
|
||||
await this.validateSaleInvoiceNoUniquiness(tenantId, saleInvoiceDTO.invoiceNo);
|
||||
await this.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
|
||||
|
||||
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
|
||||
const storedInvoice = await SaleInvoice.query()
|
||||
.insert({
|
||||
@@ -95,6 +191,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
// method and starting date.
|
||||
await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id);
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated);
|
||||
|
||||
return storedInvoice;
|
||||
}
|
||||
|
||||
@@ -131,30 +229,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
.where('reference_type', 'SaleInvoice');
|
||||
|
||||
// Patch update the sale invoice items entries.
|
||||
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries(
|
||||
await this.itemsEntriesService.patchItemsEntries(
|
||||
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
|
||||
);
|
||||
|
||||
this.logger.info('[sale_invoice] change customer different balance.');
|
||||
// Changes the diff customer balance between old and new amount.
|
||||
const changeCustomerBalanceOper = Customer.changeDiffBalance(
|
||||
saleInvoice.customer_id,
|
||||
oldSaleInvoice.customerId,
|
||||
balance,
|
||||
oldSaleInvoice.balance,
|
||||
);
|
||||
// Records the inventory transactions for inventory items.
|
||||
const recordInventoryTransOper = this.recordInventoryTranscactions(
|
||||
tenantId, saleInvoice, saleInvoiceId, true,
|
||||
);
|
||||
await Promise.all([
|
||||
patchItemsEntriesOper,
|
||||
changeCustomerBalanceOper,
|
||||
recordInventoryTransOper,
|
||||
]);
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeInvoiceItemsCost(tenantId, saleInvoiceId, true);
|
||||
// Triggers `onSaleInvoiceEdited` event.
|
||||
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,14 +247,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
SaleInvoice,
|
||||
ItemEntry,
|
||||
Customer,
|
||||
Account,
|
||||
InventoryTransaction,
|
||||
AccountTransaction,
|
||||
} = this.tenancy.models(tenantId);
|
||||
|
||||
const oldSaleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries');
|
||||
const oldSaleInvoice = await this.getSaleInvoiceOrThrowError(tenantId, saleInvoiceId);
|
||||
|
||||
this.logger.info('[sale_invoice] delete sale invoice with entries.');
|
||||
await SaleInvoice.query().where('id', saleInvoiceId).delete();
|
||||
@@ -218,6 +294,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeItemsCost(tenantId, oldSaleInvoice)
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { omit, difference, sumBy } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import HasItemEntries from 'services/Sales/HasItemsEntries';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
@@ -21,6 +26,125 @@ export default class SalesReceiptService {
|
||||
@Inject()
|
||||
itemsEntriesService: HasItemEntries;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getSaleReceiptOrThrowError(tenantId: number, saleReceiptId: number) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
const isSaleReceiptExists = await this.saleReceiptService
|
||||
.isSaleReceiptExists(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
);
|
||||
if (!isSaleReceiptExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const saleReceipt = { ...req.body };
|
||||
const { Customer } = req.models;
|
||||
|
||||
const foundCustomer = await Customer.query().findById(saleReceipt.customer_id);
|
||||
|
||||
if (!foundCustomer) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt deposit account exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptDepositAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const isDepositAccountExists = await this.accountsService.isAccountExists(
|
||||
tenantId,
|
||||
saleReceipt.deposit_account_id
|
||||
);
|
||||
if (!isDepositAccountExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether receipt items ids exist on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptItemsIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id);
|
||||
|
||||
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(
|
||||
tenantId,
|
||||
estimateItemsIds
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }] });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate receipt entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptEntriesIds(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
// Validate the entries IDs that not stored or associated to the sale receipt.
|
||||
const notExistsEntriesIds = await this.saleReceiptService
|
||||
.isSaleReceiptEntriesIDsExists(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
saleReceipt,
|
||||
);
|
||||
if (notExistsEntriesIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{
|
||||
type: 'ENTRIES.IDS.NOT.FOUND',
|
||||
code: 500,
|
||||
}]
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @async
|
||||
@@ -38,20 +162,14 @@ export default class SalesReceiptService {
|
||||
const storedSaleReceipt = await SaleReceipt.query()
|
||||
.insert({
|
||||
...omit(saleReceipt, ['entries']),
|
||||
});
|
||||
const storeSaleReceiptEntriesOpers: Array<any> = [];
|
||||
|
||||
saleReceipt.entries.forEach((entry: any) => {
|
||||
const oper = ItemEntry.query()
|
||||
.insert({
|
||||
entries: saleReceipt.entries.map((entry) => ({
|
||||
reference_type: 'SaleReceipt',
|
||||
reference_id: storedSaleReceipt.id,
|
||||
...omit(entry, ['id', 'amount']),
|
||||
});
|
||||
storeSaleReceiptEntriesOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...storeSaleReceiptEntriesOpers]);
|
||||
return storedSaleReceipt;
|
||||
}))
|
||||
});
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +203,9 @@ export default class SalesReceiptService {
|
||||
'SaleReceipt',
|
||||
saleReceiptId,
|
||||
);
|
||||
return Promise.all([patchItemsEntries]);
|
||||
await Promise.all([patchItemsEntries]);
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,11 +230,13 @@ export default class SalesReceiptService {
|
||||
saleReceiptId,
|
||||
'SaleReceipt'
|
||||
);
|
||||
return Promise.all([
|
||||
await Promise.all([
|
||||
deleteItemsEntriesOper,
|
||||
deleteSaleReceiptOper,
|
||||
deleteTransactionsOper,
|
||||
]);
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
83
server/src/subscribers/bills.ts
Normal file
83
server/src/subscribers/bills.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
46
server/src/subscribers/customers.ts
Normal file
46
server/src/subscribers/customers.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Authentication service.
|
||||
@@ -72,5 +71,90 @@ export default {
|
||||
|
||||
onBulkDeleted: 'onExpenseBulkDeleted',
|
||||
onBulkPublished: 'onBulkPublished',
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sales invoices service.
|
||||
*/
|
||||
saleInvoice: {
|
||||
onCreated: 'onSaleInvoiceCreated',
|
||||
onEdited: 'onSaleInvoiceEdited',
|
||||
onDeleted: 'onSaleInvoiceDeleted',
|
||||
onBulkDelete: 'onSaleInvoiceBulkDeleted',
|
||||
onPublished: 'onSaleInvoicePublished',
|
||||
},
|
||||
|
||||
/**
|
||||
* Sales estimates service.
|
||||
*/
|
||||
saleEstimates: {
|
||||
onCreated: 'onSaleEstimateCreated',
|
||||
onEdited: 'onSaleEstimateEdited',
|
||||
onDeleted: 'onSaleEstimatedDeleted',
|
||||
onBulkDelete: 'onSaleEstimatedBulkDeleted',
|
||||
onPublished: 'onSaleEstimatedPublished',
|
||||
},
|
||||
|
||||
/**
|
||||
* Sales receipts service.
|
||||
*/
|
||||
saleReceipts: {
|
||||
onCreated: 'onSaleReceiptsCreated',
|
||||
onEdited: 'onSaleReceiptsEdited',
|
||||
onDeleted: 'onSaleReceiptsDeleted',
|
||||
onBulkDeleted: 'onSaleReceiptsBulkDeleted',
|
||||
onPublished: 'onSaleReceiptPublished',
|
||||
},
|
||||
|
||||
/**
|
||||
* Payment receipts service.
|
||||
*/
|
||||
paymentReceipts: {
|
||||
onCreated: 'onPaymentReceiveCreated',
|
||||
onEdited: 'onPaymentReceiveEdited',
|
||||
onDeleted: 'onPaymentReceiveDeleted',
|
||||
onPublished: 'onPaymentReceiptPublished',
|
||||
},
|
||||
|
||||
/**
|
||||
* Bills service.
|
||||
*/
|
||||
bills: {
|
||||
onCreated: 'onBillCreated',
|
||||
onEdited: 'onBillEdited',
|
||||
onDeleted: 'onBillDeleted',
|
||||
onBulkDeleted: 'onBillBulkDeleted',
|
||||
onPublished: 'onBillPublished',
|
||||
},
|
||||
|
||||
/**
|
||||
* Bill payments service.
|
||||
*/
|
||||
billPayments: {
|
||||
onCreated: 'onBillPaymentCreated',
|
||||
onEdited: 'onBillPaymentEdited',
|
||||
onDeleted: 'onBillPaymentDeleted',
|
||||
onBulkDeleted: 'onBillPaymentsBulkDeleted',
|
||||
onPublished: 'onBillPaymentPublished',
|
||||
},
|
||||
|
||||
/**
|
||||
* Customers services.
|
||||
*/
|
||||
customers: {
|
||||
onCreated: 'onCustomerCreated',
|
||||
onEdited: 'onCustomerEdited',
|
||||
onDeleted: 'onCustomerDeleted',
|
||||
onBulkDeleted: 'onBulkDeleted',
|
||||
},
|
||||
|
||||
/**
|
||||
* Vendors services.
|
||||
*/
|
||||
vendors: {
|
||||
onCreated: 'onVendorCreated',
|
||||
onEdited: 'onVendorEdited',
|
||||
onDeleted: 'onVendorDeleted',
|
||||
onBulkDeleted: 'onVendorBulkDeleted',
|
||||
},
|
||||
}
|
||||
|
||||
108
server/src/subscribers/paymentMades.ts
Normal file
108
server/src/subscribers/paymentMades.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
server/src/subscribers/saleInvoices.ts
Normal file
22
server/src/subscribers/saleInvoices.ts
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
||||
46
server/src/subscribers/vendors.ts
Normal file
46
server/src/subscribers/vendors.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user