refactoring: bills service.

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

View File

@@ -24,7 +24,8 @@ export default class CustomersController extends ContactsController {
...this.customerDTOSchema,
],
this.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 }],
});
}
}
}
}

View File

@@ -24,7 +24,8 @@ export default class VendorsController extends ContactsController {
...this.vendorDTOSchema,
],
this.validationResult,
asyncMiddleware(this.newVendor.bind(this))
asyncMiddleware(this.newVendor.bind(this)),
this.handlerServiceErrors,
);
router.post('/:id', [
...this.contactDTOSchema,
@@ -32,25 +33,29 @@ export default class VendorsController extends ContactsController {
...this.vendorDTOSchema,
],
this.validationResult,
asyncMiddleware(this.editVendor.bind(this))
asyncMiddleware(this.editVendor.bind(this)),
this.handlerServiceErrors,
);
router.delete('/:id', [
...this.specificContactSchema,
],
this.validationResult,
asyncMiddleware(this.deleteVendor.bind(this))
asyncMiddleware(this.deleteVendor.bind(this)),
this.handlerServiceErrors,
);
router.delete('/', [
...this.bulkContactsSchema,
],
this.validationResult,
asyncMiddleware(this.deleteBulkVendors.bind(this))
asyncMiddleware(this.deleteBulkVendors.bind(this)),
this.handlerServiceErrors,
);
router.get('/:id', [
...this.specificContactSchema,
],
this.validationResult,
asyncMiddleware(this.getVendor.bind(this))
asyncMiddleware(this.getVendor.bind(this)),
this.handlerServiceErrors,
);
router.get('/', [
...this.vendorsListSchema,
@@ -99,8 +104,8 @@ export default class VendorsController extends ContactsController {
const { tenantId } = req;
try {
const contact = await this.vendorsService.newVendor(tenantId, contactDTO);
return res.status(200).send({ id: contact.id });
const vendor = await this.vendorsService.newVendor(tenantId, contactDTO);
return res.status(200).send({ id: vendor.id });
} catch (error) {
next(error);
}
@@ -121,13 +126,6 @@ export default class VendorsController extends ContactsController {
await this.vendorsService.editVendor(tenantId, contactId, contactDTO);
return res.status(200).send({ id: contactId });
} catch (error) {
if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') {
return res.status(400).send({
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
});
}
}
next(error);
}
}
@@ -146,18 +144,6 @@ export default class VendorsController extends ContactsController {
await this.vendorsService.deleteVendor(tenantId, contactId)
return res.status(200).send({ id: contactId });
} catch (error) {
if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') {
return res.status(400).send({
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'vendor_has_bills') {
return res.status(400).send({
errors: [{ type: 'VENDOR.HAS.BILLS', code: 200 }],
});
}
}
next(error);
}
}
@@ -176,13 +162,6 @@ export default class VendorsController extends ContactsController {
const vendor = await this.vendorsService.getVendor(tenantId, vendorId)
return res.status(200).send({ vendor });
} catch (error) {
if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') {
return res.status(400).send({
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
});
}
}
next(error);
}
}
@@ -201,18 +180,6 @@ export default class VendorsController extends ContactsController {
await this.vendorsService.deleteBulkVendors(tenantId, contactsIds)
return res.status(200).send({ ids: contactsIds });
} catch (error) {
if (error instanceof ServiceError) {
if (error.errorType === 'contacts_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'VENDORS.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'some_vendors_have_bills') {
return res.boom.badRequest(null, {
errors: [{ type: 'SOME.VENDORS.HAVE.BILLS', code: 200 }],
});
}
}
next(error);
}
}
@@ -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 }],
});
}
}
}
}

View File

@@ -1,13 +1,13 @@
import { Router, Request, Response } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import { difference } from 'lodash';
import { BillOTD } from 'interfaces';
import { IBillDTO, IBillEditDTO } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BillsService from 'services/Purchases/Bills';
import BaseController from 'api/controllers/BaseController';
import ItemsService from 'services/Items/ItemsService';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
@Service()
export default class BillsController extends BaseController {
@@ -27,45 +27,49 @@ export default class BillsController extends BaseController {
const router = Router();
router.post(
'/',
[...this.billValidationSchema],
'/', [
...this.billValidationSchema
],
this.validationResult,
asyncMiddleware(this.validateVendorExistance.bind(this)),
asyncMiddleware(this.validateItemsIds.bind(this)),
asyncMiddleware(this.validateBillNumberExists.bind(this)),
asyncMiddleware(this.validateNonPurchasableEntriesItems.bind(this)),
asyncMiddleware(this.newBill.bind(this))
asyncMiddleware(this.newBill.bind(this)),
this.handleServiceError,
);
router.post(
'/:id',
[...this.billValidationSchema, ...this.specificBillValidationSchema],
'/:id', [
...this.billValidationSchema,
...this.specificBillValidationSchema,
],
this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.validateVendorExistance.bind(this)),
asyncMiddleware(this.validateItemsIds.bind(this)),
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
asyncMiddleware(this.validateNonPurchasableEntriesItems.bind(this)),
asyncMiddleware(this.editBill.bind(this))
);
router.get(
'/:id',
[...this.specificBillValidationSchema],
'/:id', [
...this.specificBillValidationSchema
],
this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.getBill.bind(this))
);
router.get(
'/',
[...this.billsListingValidationSchema],
this.validationResult,
asyncMiddleware(this.listingBills.bind(this))
asyncMiddleware(this.getBill.bind(this)),
);
// router.get(
// '/:id',
// [...this.specificBillValidationSchema],
// this.validationResult,
// asyncMiddleware(this.getBill.bind(this)),
// this.handleServiceError,
// );
// router.get(
// '/',
// [...this.billsListingValidationSchema],
// this.validationResult,
// asyncMiddleware(this.listingBills.bind(this)),
// this.handleServiceError,
// );
router.delete(
'/:id',
[...this.specificBillValidationSchema],
this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.deleteBill.bind(this))
asyncMiddleware(this.deleteBill.bind(this)),
this.handleServiceError,
);
return router;
}
@@ -92,6 +96,28 @@ export default class BillsController extends BaseController {
];
}
/**
* Common validation schema.
*/
get billEditValidationSchema() {
return [
// check('bill_number').exists().trim().escape(),
check('bill_date').exists().isISO8601(),
check('due_date').optional().isISO8601(),
// check('vendor_id').exists().isNumeric().toInt(),
check('note').optional().trim().escape(),
check('entries').isArray({ min: 1 }),
check('entries.*.id').optional().isNumeric().toInt(),
check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.quantity').exists().isNumeric().toFloat(),
check('entries.*.discount').optional().isNumeric().toFloat(),
check('entries.*.description').optional().trim().escape(),
];
}
/**
* Bill validation schema.
*/
@@ -112,162 +138,23 @@ export default class BillsController extends BaseController {
query('sort_order').optional().isIn(['desc', 'asc']),
];
}
/**
* Validates whether the vendor is exist.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateVendorExistance(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { Vendor } = req.models;
const isVendorExists = await Vendor.query().findById(req.body.vendor_id);
if (!isVendorExists) {
return res.status(400).send({
errors: [{ type: 'VENDOR.ID.NOT.FOUND', code: 300 }],
});
}
next();
}
/**
* Validates the given bill existance.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateBillExistance(req: Request, res: Response, next: Function) {
const billId: number = req.params.id;
const { tenantId } = req;
const isBillExists = await this.billsService.isBillExists(tenantId, billId);
if (!isBillExists) {
return res.status(400).send({
errors: [{ type: 'BILL.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validates the entries items ids.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateItemsIds(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const itemsIds = req.body.entries.map((e) => e.item_id);
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, itemsIds);
if (notFoundItemsIds.length > 0) {
return res.status(400).send({
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
});
}
next();
}
/**
* Validates the bill number existance.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateBillNumberExists(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const isBillNoExists = await this.billsService.isBillNoExists(
tenantId, req.body.bill_number,
);
if (isBillNoExists) {
return res.status(400).send({
errors: [{ type: 'BILL.NUMBER.EXISTS', code: 500 }],
});
}
next();
}
/**
* Validates the entries ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
const { id: billId } = req.params;
const bill = { ...req.body };
const { ItemEntry } = req.models;
const entriesIds = bill.entries.filter((e) => e.id).map((e) => e.id);
const storedEntries = await ItemEntry.query()
.whereIn('reference_id', [billId])
.whereIn('reference_type', ['Bill']);
const storedEntriesIds = storedEntries.map((entry) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
return res.status(400).send({
errors: [{ type: 'BILL.ENTRIES.IDS.NOT.FOUND', code: 600 }],
});
}
next();
}
/**
* Validate the entries items that not purchase-able.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateNonPurchasableEntriesItems(req: Request, res: Response, next: Function) {
const { Item } = req.models;
const bill = { ...req.body };
const itemsIds = bill.entries.map(e => e.item_id);
const purchasbleItems = await Item.query()
.where('purchasable', true)
.whereIn('id', itemsIds);
const purchasbleItemsIds = purchasbleItems.map((item) => item.id);
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
if (notPurchasableItems.length > 0) {
return res.status(400).send({
errors: [{ type: 'NOT.PURCHASE.ABLE.ITEMS', code: 600 }],
});
}
next();
}
/**
* Creates a new bill and records journal transactions.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async newBill(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { ItemEntry } = req.models;
async newBill(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const billDTO: IBillDTO = this.matchedBodyData(req);
const billOTD: BillOTD = matchedData(req, {
locations: ['body'],
includeOptionals: true
});
const storedBill = await this.billsService.createBill(tenantId, billOTD);
return res.status(200).send({ id: storedBill.id });
try {
const storedBill = await this.billsService.createBill(tenantId, billDTO, user);
return res.status(200).send({ id: storedBill.id });
} catch (error) {
next(error);
}
}
/**
@@ -275,18 +162,17 @@ export default class BillsController extends BaseController {
* @param {Request} req
* @param {Response} res
*/
async editBill(req: Request, res: Response) {
async editBill(req: Request, res: Response, next: NextFunction) {
const { id: billId } = req.params;
const { ItemEntry } = req.models;
const { tenantId } = req;
const billDTO: IBillEditDTO = this.matchedBodyData(req);
const billOTD: BillOTD = matchedData(req, {
locations: ['body'],
includeOptionals: true
});
const editedBill = await this.billsService.editBill(tenantId, billId, billOTD);
return res.status(200).send({ id: billId });
try {
const editedBill = await this.billsService.editBill(tenantId, billId, billDTO);
return res.status(200).send({ id: billId });
} catch (error) {
next(error);
}
}
/**
@@ -295,13 +181,17 @@ export default class BillsController extends BaseController {
* @param {Response} res
* @return {Response}
*/
async getBill(req: Request, res: Response) {
async getBill(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: billId } = req.params;
const bill = await this.billsService.getBillWithMetadata(tenantId, billId);
try {
const bill = await this.billsService.getBillWithMetadata(tenantId, billId);
return res.status(200).send({ bill });
return res.status(200).send({ bill });
} catch (error) {
next(error);
}
}
/**
@@ -310,13 +200,19 @@ export default class BillsController extends BaseController {
* @param {Response} res -
* @return {Response}
*/
async deleteBill(req: Request, res: Response) {
async deleteBill(req: Request, res: Response, next: NextFunction) {
const billId = req.params.id;
const { tenantId } = req;
await this.billsService.deleteBill(tenantId, billId);
return res.status(200).send({ id: billId });
try {
await this.billsService.deleteBill(tenantId, billId);
return res.status(200).send({
id: billId,
message: 'The given bill deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
@@ -325,7 +221,50 @@ export default class BillsController extends BaseController {
* @param {Response} res -
* @return {Response}
*/
async listingBills(req: Request, res: Response) {
async billsList(req: Request, res: Response) {
}
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'BILL_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'BILL_NOT_FOUND', code: 100 }],
});
}
if (error.errorType === 'BILL_NUMBER_EXISTS') {
return res.status(400).send({
errors: [{ type: 'BILL.NUMBER.EXISTS', code: 500 }],
});
}
if (error.errorType === 'BILL_VENDOR_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'BILL_VENDOR_NOT_FOUND', code: 600 }],
});
}
if (error.errorType === 'BILL_ITEMS_NOT_PURCHASABLE') {
return res.status(400).send({
errors: [{ type: 'BILL_ITEMS_NOT_PURCHASABLE', code: 700 }]
})
}
if (error.errorType === 'NOT_PURCHASE_ABLE_ITEMS') {
return res.status(400).send({
errors: [{ type: 'NOT_PURCHASE_ABLE_ITEMS', code: 800 }],
});
}
if (error.errorType === 'BILL_ITEMS_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
});
}
}
next(error);
}
}

View File

@@ -1,10 +1,9 @@
import { Router, Request, Response } from 'express';
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { check, param, query, ValidationChain, matchedData } from 'express-validator';
import { difference } from 'lodash';
import { check, param, query, ValidationChain } from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import validateMiddleware from 'api/middleware/validateMiddleware';
import { ServiceError } from 'exceptions';
import BaseController from 'api/controllers/BaseController';
import BillPaymentsService from 'services/Purchases/BillPayments';
import AccountsService from 'services/Accounts/AccountsService';
@@ -31,43 +30,34 @@ export default class BillsPayments extends BaseController {
...this.billPaymentSchemaValidation,
],
this.validationResult,
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
asyncMiddleware(this.validatePaymentAccount.bind(this)),
asyncMiddleware(this.validatePaymentNumber.bind(this)),
asyncMiddleware(this.validateEntriesBillsExistance.bind(this)),
asyncMiddleware(this.validateVendorsDueAmount.bind(this)),
asyncMiddleware(this.createBillPayment.bind(this)),
this.handleServiceError,
);
router.post('/:id', [
...this.billPaymentSchemaValidation,
...this.specificBillPaymentValidateSchema,
],
this.validationResult,
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
asyncMiddleware(this.validatePaymentAccount.bind(this)),
asyncMiddleware(this.validatePaymentNumber.bind(this)),
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
asyncMiddleware(this.validateEntriesBillsExistance.bind(this)),
asyncMiddleware(this.validateVendorsDueAmount.bind(this)),
asyncMiddleware(this.editBillPayment.bind(this)),
this.handleServiceError,
)
router.delete('/:id',
this.specificBillPaymentValidateSchema,
router.delete('/:id', [
...this.specificBillPaymentValidateSchema,
],
this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
asyncMiddleware(this.deleteBillPayment.bind(this)),
this.handleServiceError,
);
router.get('/:id',
this.specificBillPaymentValidateSchema,
this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
asyncMiddleware(this.getBillPayment.bind(this)),
);
router.get('/',
this.listingValidationSchema,
this.validationResult,
asyncMiddleware(this.getBillsPayments.bind(this))
);
// router.get('/:id',
// this.specificBillPaymentValidateSchema,
// this.validationResult,
// asyncMiddleware(this.getBillPayment.bind(this)),
// );
// router.get('/',
// this.listingValidationSchema,
// this.validationResult,
// asyncMiddleware(this.getBillsPayments.bind(this))
// );
return router;
}
@@ -112,186 +102,6 @@ export default class BillsPayments extends BaseController {
];
}
/**
* Validate whether the bill payment vendor exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateBillPaymentVendorExistance(req: Request, res: Response, next: any ) {
const billPayment = req.body;
const { Vendor } = req.models;
const isVendorExists = await Vendor.query().findById(billPayment.vendor_id);
if (!isVendorExists) {
return res.status(400).send({
errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }],
});
}
next();
}
/**
* Validates the bill payment existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateBillPaymentExistance(req: Request, res: Response, next: any ) {
const { id: billPaymentId } = req.params;
const { BillPayment } = req.models;
const foundBillPayment = await BillPayment.query().findById(billPaymentId);
if (!foundBillPayment) {
return res.status(404).send({
errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }],
});
}
next();
}
/**
* Validates the payment account.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validatePaymentAccount(req: Request, res: Response, next: any) {
const { tenantId } = req;
const billPayment = { ...req.body };
const isAccountExists = await this.accountsService.isAccountExists(
tenantId,
billPayment.payment_account_id
);
if (!isAccountExists) {
return res.status(400).send({
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validates the payment number uniqness.
* @param {Request} req
* @param {Response} res
* @param {Function} res
*/
async validatePaymentNumber(req: Request, res: Response, next: any) {
const billPayment = { ...req.body };
const { id: billPaymentId } = req.params;
const { BillPayment } = req.models;
const foundBillPayment = await BillPayment.query()
.onBuild((builder: any) => {
builder.where('payment_number', billPayment.payment_number)
if (billPaymentId) {
builder.whereNot('id', billPaymentId);
}
})
.first();
if (foundBillPayment) {
return res.status(400).send({
errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }],
});
}
next();
}
/**
* Validate whether the entries bills ids exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async validateEntriesBillsExistance(req: Request, res: Response, next: any) {
const { Bill } = req.models;
const billPayment = { ...req.body };
const entriesBillsIds = billPayment.entries.map((e: any) => e.bill_id);
// Retrieve not found bills that associated to the given vendor id.
const notFoundBillsIds = await Bill.getNotFoundBills(
entriesBillsIds,
billPayment.vendor_id,
);
if (notFoundBillsIds.length > 0) {
return res.status(400).send({
errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 600 }],
});
}
next();
}
/**
* Validate wether the payment amount bigger than the payable amount.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @return {void}
*/
async validateVendorsDueAmount(req: Request, res: Response, next: Function) {
const { Bill } = req.models;
const billsIds = req.body.entries.map((entry: any) => entry.bill_id);
const storedBills = await Bill.query()
.whereIn('id', billsIds);
const storedBillsMap = new Map(
storedBills.map((bill: any) => [bill.id, bill]),
);
interface invalidPaymentAmountError{
index: number,
due_amount: number
};
const hasWrongPaymentAmount: invalidPaymentAmountError[] = [];
const { entries } = req.body;
entries.forEach((entry: any, index: number) => {
const entryBill = storedBillsMap.get(entry.bill_id);
const { dueAmount } = entryBill;
if (dueAmount < entry.payment_amount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
});
if (hasWrongPaymentAmount.length > 0) {
return res.status(400).send({
errors: [{ type: 'INVALID.BILL.PAYMENT.AMOUNT', code: 400, indexes: hasWrongPaymentAmount }]
});
}
next();
}
/**
* Validate the payment receive entries IDs existance.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
const { BillPaymentEntry } = req.models;
const billPayment = { id: req.params.id, ...req.body };
const entriesIds = billPayment.entries
.filter((entry: any) => entry.id)
.map((entry: any) => entry.id);
const storedEntries = await BillPaymentEntry.query()
.where('bill_payment_id', billPayment.id);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
return res.status(400).send({
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
});
}
next();
}
/**
* Creates a bill payment.
* @async
@@ -299,17 +109,21 @@ export default class BillsPayments extends BaseController {
* @param {Response} res
* @param {Response} res
*/
async createBillPayment(req: Request, res: Response) {
async createBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const billPaymentDTO = this.matchedBodyData(req);
const billPayment = matchedData(req, {
locations: ['body'],
includeOptionals: true,
});
const storedPayment = await this.billPaymentService
.createBillPayment(tenantId, billPayment);
try {
const billPayment = await this.billPaymentService.createBillPayment(tenantId, billPaymentDTO);
return res.status(200).send({ id: storedPayment.id });
return res.status(200).send({
id: billPayment.id,
message: 'Payment made has been created successfully.',
});
} catch (error) {
console.log(error);
next(error);
}
}
/**
@@ -317,28 +131,24 @@ export default class BillsPayments extends BaseController {
* @param {Request} req
* @param {Response} res
*/
async editBillPayment(req: Request, res: Response) {
async editBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const billPayment = matchedData(req, {
locations: ['body'],
includeOptionals: true,
});
const billPaymentDTO = this.matchedBodyData(req);
const { id: billPaymentId } = req.params;
const { BillPayment } = req.models;
const oldBillPayment = await BillPayment.query()
.where('id', billPaymentId)
.withGraphFetched('entries')
.first();
await this.billPaymentService.editBillPayment(
tenantId,
billPaymentId,
billPayment,
oldBillPayment,
);
return res.status(200).send({ id: 1 });
try {
const paymentMade = await this.billPaymentService.editBillPayment(
tenantId,
billPaymentId,
billPaymentDTO
);
return res.status(200).send({
id: paymentMade.id,
message: 'Payment made has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
@@ -348,15 +158,20 @@ export default class BillsPayments extends BaseController {
* @param {Response} res -
* @return {Response} res -
*/
async deleteBillPayment(req: Request, res: Response) {
async deleteBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: billPaymentId } = req.params;
const billPayment = req.body;
await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId);
try {
await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId);
return res.status(200).send({ id: billPaymentId });
return res.status(200).send({
id: billPaymentId,
message: 'Payment made has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
@@ -364,7 +179,7 @@ export default class BillsPayments extends BaseController {
* @param {Request} req
* @param {Response} res
*/
async getBillPayment(req: Request, res: Response) {
async getBillPayment(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: billPaymentId } = req.params;
@@ -380,7 +195,7 @@ export default class BillsPayments extends BaseController {
* @param {Response} res -
* @return {Response}
*/
async getBillsPayments(req: Request, res: Response) {
async getBillsPayments(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req.params;
const billPaymentsFilter = this.matchedQueryData(req);
@@ -397,4 +212,68 @@ export default class BillsPayments extends BaseController {
next(error);
}
}
/**
* Handle service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'PAYMENT_MADE_NOT_FOUND') {
return res.status(404).send({
message: 'Payment made not found.',
});
}
if (error.errorType === 'VENDOR_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }],
});
}
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') {
return res.status(400).send({
errors: [{ type: 'PAYMENT_ACCOUNT.NOT.CURRENT_ASSET.TYPE', code: 100 }],
});
}
if (error.errorType === 'BILL_PAYMENT_NUMBER_NOT_UNQIUE') {
return res.status(400).send({
errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }],
});
}
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === '') {
return res.status(400).send({
errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 600 }],
});
}
if (error.errorType === 'BILL_PAYMENT_ENTRIES_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
});
}
if (error.errorType === 'INVALID_BILL_PAYMENT_AMOUNT') {
return res.status(400).send({
errors: [{ type: 'INVALID_BILL_PAYMENT_AMOUNT', code: 100 }],
});
}
if (error.errorType === 'BILL_ENTRIES_IDS_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'BILLS_NOT_FOUND', code: 100 }],
})
}
}
console.log(error);
next(error);
}
}

View File

@@ -1,4 +1,4 @@
import express from 'express';
import { Router } from 'express';
import { Container, Service } from 'typedi';
import Bills from 'api/controllers/Purchases/Bills'
import BillPayments from 'api/controllers/Purchases/BillsPayments';
@@ -7,7 +7,7 @@ import BillPayments from 'api/controllers/Purchases/BillsPayments';
export default class PurchasesController {
router() {
const router = express.Router();
const router = Router();
router.use('/bills', Container.get(Bills).router());
router.use('/bill_payments', Container.get(BillPayments).router());

View File

@@ -1,4 +1,4 @@
import { Router, Request, Response } from 'express';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi';
import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces';
@@ -25,9 +25,9 @@ export default class SalesEstimatesController extends BaseController {
'/',
this.estimateValidationSchema,
this.validationResult,
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
// asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
// asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
// asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
asyncMiddleware(this.newEstimate.bind(this))
);
router.post(
@@ -36,11 +36,11 @@ export default class SalesEstimatesController extends BaseController {
...this.estimateValidationSchema,
],
this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
// asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
// asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
// asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
// asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
// asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
asyncMiddleware(this.editEstimate.bind(this))
);
router.delete(
@@ -48,14 +48,14 @@ export default class SalesEstimatesController extends BaseController {
this.validateSpecificEstimateSchema,
],
this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
// asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.deleteEstimate.bind(this))
);
router.get(
'/:id',
this.validateSpecificEstimateSchema,
this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
// asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.getEstimate.bind(this))
);
router.get(
@@ -114,120 +114,6 @@ export default class SalesEstimatesController extends BaseController {
]
}
/**
* Validate whether the estimate customer exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) {
const estimate = { ...req.body };
const { Customer } = req.models
const foundCustomer = await Customer.query().findById(estimate.customer_id);
if (!foundCustomer) {
return res.status(404).send({
errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validate the estimate number unique on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateNumberExistance(req: Request, res: Response, next: Function) {
const estimate = { ...req.body };
const { tenantId } = req;
const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique(
tenantId,
estimate.estimate_number,
req.params.id,
);
if (isEstNumberUnqiue) {
return res.boom.badRequest(null, {
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
});
}
next();
}
/**
* Validate the estimate entries items ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) {
const tenantId = req.tenantId;
const estimate = { ...req.body };
const estimateItemsIds = estimate.entries.map(e => e.item_id);
// Validate items ids in estimate entries exists.
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds);
if (notFoundItemsIds.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
});
}
next();
}
/**
* Validate whether the sale estimate id exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateIdExistance(req: Request, res: Response, next: Function) {
const { id: estimateId } = req.params;
const { tenantId } = req;
const storedEstimate = await this.saleEstimateService
.getEstimate(tenantId, estimateId);
if (!storedEstimate) {
return res.status(404).send({
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validate sale invoice entries ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) {
const { ItemEntry } = req.models;
const { id: saleInvoiceId } = req.params;
const saleInvoice = { ...req.body };
const entriesIds = saleInvoice.entries
.filter(e => e.id)
.map((e) => e.id);
const foundEntries = await ItemEntry.query()
.whereIn('id', entriesIds)
.where('reference_type', 'SaleInvoice')
.where('reference_id', saleInvoiceId);
if (foundEntries.length > 0) {
return res.status(400).send({
errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }],
});
}
next();
}
/**
* Handle create a new estimate with associated entries.
* @param {Request} req -

View File

@@ -32,7 +32,7 @@ export default class SaleInvoicesController extends BaseController{
this.saleInvoiceValidationSchema,
this.validationResult,
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
// asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)),
asyncMiddleware(this.newSaleInvoice.bind(this))
@@ -46,7 +46,7 @@ export default class SaleInvoicesController extends BaseController{
this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
// asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
@@ -312,18 +312,19 @@ export default class SaleInvoicesController extends BaseController{
* @param {Response} res
* @param {Function} next
*/
async newSaleInvoice(req: Request, res: Response) {
async newSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, {
locations: ['body'],
includeOptionals: true
});
const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req);
// Creates a new sale invoice with associated entries.
const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice(
tenantId, saleInvoiceOTD,
);
return res.status(200).send({ id: storedSaleInvoice.id });
try {
// Creates a new sale invoice with associated entries.
const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice(
tenantId, saleInvoiceOTD,
);
return res.status(200).send({ id: storedSaleInvoice.id });
} catch (error) {
next(error)
}
}
/**
@@ -332,18 +333,18 @@ export default class SaleInvoicesController extends BaseController{
* @param {Response} res
* @param {Function} next
*/
async editSaleInvoice(req: Request, res: Response) {
async editSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: saleInvoiceId } = req.params;
const saleInvoiceOTD: ISaleInvoiceOTD = this.matchedBodyData(req);
const saleInvoiceOTD: ISaleInvoiceOTD = matchedData(req, {
locations: ['body'],
includeOptionals: true
});
// Update the given sale invoice details.
await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD);
return res.status(200).send({ id: saleInvoiceId });
try {
// Update the given sale invoice details.
await this.saleInvoiceService.editSaleInvoice(tenantId, saleInvoiceId, saleInvoiceOTD);
return res.status(200).send({ id: saleInvoiceId });
} catch (error) {
next(error);
}
}
/**
@@ -352,14 +353,18 @@ export default class SaleInvoicesController extends BaseController{
* @param {Response} res
* @param {Function} next
*/
async deleteSaleInvoice(req: Request, res: Response) {
async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { id: saleInvoiceId } = req.params;
const { tenantId } = req;
// Deletes the sale invoice with associated entries and journal transaction.
await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId);
return res.status(200).send({ id: saleInvoiceId });
try {
// Deletes the sale invoice with associated entries and journal transaction.
await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId);
return res.status(200).send({ id: saleInvoiceId });
} catch (error) {
next(error);
}
}
/**

View File

@@ -1,11 +1,12 @@
import { Router, Request, Response } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService';
import SaleReceiptService from 'services/Sales/SalesReceipts';
import BaseController from '../BaseController';
import { ISaleReceiptDTO } from 'interfaces/SaleReceipt';
@Service()
export default class SalesReceiptsController extends BaseController{
@@ -232,20 +233,21 @@ export default class SalesReceiptsController extends BaseController{
* @param {Request} req
* @param {Response} res
*/
async newSaleReceipt(req: Request, res: Response) {
async newSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req);
const saleReceipt = matchedData(req, {
locations: ['body'],
includeOptionals: true,
});
// Store the given sale receipt details with associated entries.
const storedSaleReceipt = await this.saleReceiptService
.createSaleReceipt(
tenantId,
saleReceipt,
);
return res.status(200).send({ id: storedSaleReceipt.id });
try {
// Store the given sale receipt details with associated entries.
const storedSaleReceipt = await this.saleReceiptService
.createSaleReceipt(
tenantId,
saleReceiptDTO,
);
return res.status(200).send({ id: storedSaleReceipt.id });
} catch (error) {
next(error);
}
}
/**
@@ -253,14 +255,18 @@ export default class SalesReceiptsController extends BaseController{
* @param {Request} req
* @param {Response} res
*/
async deleteSaleReceipt(req: Request, res: Response) {
async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: saleReceiptId } = req.params;
// Deletes the sale receipt.
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
return res.status(200).send({ id: saleReceiptId });
try {
// Deletes the sale receipt.
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
return res.status(200).send({ id: saleReceiptId });
} catch (error) {
next(error);
}
}
/**
@@ -269,25 +275,22 @@ export default class SalesReceiptsController extends BaseController{
* @param {Request} req
* @param {Response} res
*/
async editSaleReceipt(req: Request, res: Response) {
async editSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: saleReceiptId } = req.params;
const saleReceipt = { ...req.body };
const errorReasons = [];
// Handle all errors with reasons messages.
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
// Update the given sale receipt details.
await this.saleReceiptService.editSaleReceipt(
tenantId,
saleReceiptId,
saleReceipt,
);
return res.status(200).send();
try {
// Update the given sale receipt details.
await this.saleReceiptService.editSaleReceipt(
tenantId,
saleReceiptId,
saleReceipt,
);
return res.status(200).send();
} catch (error) {
next(error);
}
}
/**

View File

@@ -29,7 +29,7 @@ import Settings from 'api/controllers/Settings';
import Currencies from 'api/controllers/Currencies';
import Customers from 'api/controllers/Contacts/Customers';
import Vendors from 'api/controllers/Contacts/Vendors';
import Sales from 'api/controllers/Sales'
// import Sales from 'api/controllers/Sales'
import Purchases from 'api/controllers/Purchases';
import Resources from './controllers/Resources';
import ExchangeRates from 'api/controllers/ExchangeRates';
@@ -95,7 +95,7 @@ export default () => {
dashboard.use('/customers', Container.get(Customers).router());
dashboard.use('/vendors', Container.get(Vendors).router());
// dashboard.use('/sales', Container.get(Sales).router());
// dashboard.use('/purchases', Container.get(Purchases).router());
dashboard.use('/purchases', Container.get(Purchases).router());
dashboard.use('/resources', Container.get(Resources).router());
dashboard.use('/exchange_rates', Container.get(ExchangeRates).router());
dashboard.use('/media', Container.get(Media).router());

View File

@@ -9,7 +9,7 @@ exports.up = function(knex) {
table.integer('reference_id').index();
table.integer('account_id').unsigned().index().references('id').inTable('accounts');
table.string('contact_type').nullable().index();
table.integer('contact_id').unsigned().nullable().index().references('id').inTable('contacts');
table.integer('contact_id').unsigned().nullable().index();
table.string('note');
table.boolean('draft').defaultTo(false);
table.integer('user_id').unsigned().index();

View File

@@ -14,6 +14,7 @@ exports.up = function(knex) {
table.decimal('payment_amount', 13, 3).defaultTo(0);
table.string('inv_lot_number').index();
table.integer('user_id').unsigned();
table.timestamps();
});
};

View File

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

View File

@@ -1,3 +1,44 @@
export interface IBillOTD {};
export interface IBill {};
import { IItemEntry, IItemEntryDTO } from "./ItemEntry";
export interface IBillDTO {
vendorId: number,
billNumber: string,
billDate: Date,
dueDate: Date,
referenceNo: string,
status: string,
note: string,
amount: number,
paymentAmount: number,
entries: IItemEntryDTO[],
};
export interface IBillEditDTO {
billDate: Date,
dueDate: Date,
referenceNo: string,
status: string,
note: string,
amount: number,
paymentAmount: number,
entries: IItemEntryDTO[],
};
export interface IBill {
id?: number,
vendorId: number,
billNumber: string,
billDate: Date,
dueDate: Date,
referenceNo: string,
status: string,
note: string,
amount: number,
paymentAmount: number,
invLotNumber: string,
entries: IItemEntry[],
};

View File

@@ -1,15 +1,35 @@
export interface IBillPaymentEntry {
id?: number,
billPaymentId: number,
billId: number,
paymentAmount: number,
};
export interface IBillPayment {
id?: number,
vendorId: number,
amount: number,
reference: string,
billNo: string,
paymentAccountId: number,
paymentNumber: string,
paymentDate: Date,
userId: number,
entries: IBillPaymentEntry[],
}
export interface IBillPaymentOTD {};
export interface IBillPaymentEntryDTO {
billId: number,
paymentAmount: number,
};
export interface IBillPaymentDTO {
vendorId: number,
paymentAccountId: number,
paymentNumber: string,
paymentDate: Date,
description: string,
reference: string,
entries: IBillPaymentEntryDTO[],
};

View File

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

View File

@@ -1,4 +1,25 @@
import { IItemEntry } from "./ItemEntry";
export interface ISaleEstimate {};
export interface ISaleEstimateOTD {};
export interface ISaleEstimate {
id?: number,
amount: number,
customerId: number,
estimateDate: Date,
reference: string,
note: string,
termsConditions: string,
userId: number,
entries: IItemEntry[],
createdAt?: Date,
};
export interface ISaleEstimateDTO {
customerId: number,
estimateDate?: Date,
reference: string,
estimateNumber: string,
entries: IItemEntry[],
note: string,
termsConditions: string,
};

View File

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

View File

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

View File

@@ -1,7 +1,11 @@
import { Container } from 'typedi';
// Here we import all events.
import 'subscribers/authentication';
import 'subscribers/organization';
import 'subscribers/manualJournals';
import 'subscribers/expenses';
import 'subscribers/expenses';
import 'subscribers/bills';
// import 'subscribers/saleInvoices';
import 'subscribers/customers';
import 'subscribers/vendors';
import 'subscribers/paymentMades';

View File

@@ -70,13 +70,24 @@ export default class AccountTypeRepository extends TenantRepository {
* @param {string} rootType
* @return {IAccountType[]}
*/
getByRootType(rootType: string): IAccountType[] {
getByRootType(rootType: string): Promise<IAccountType[]> {
const { AccountType } = this.models;
return this.cache.get(`accountType.rootType.${rootType}`, () => {
return AccountType.query().where('root_type', rootType);
});
}
/**
* Retrieve accounts types of the given child type.
* @param {string} childType
*/
getByChildType(childType: string): Promise<IAccountType[]> {
const { AccountType } = this.models;
return this.cache.get(`accountType.childType.${childType}`, () => {
return AccountType.query().where('child_type', childType);
});
}
/**
* Flush repository cache.
*/

View File

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

View File

@@ -17,6 +17,15 @@ export default class VendorRepository extends TenantRepository {
this.cache = this.tenancy.cache(tenantId);
}
/**
* Retrieve vendor details of the given id.
* @param {number} vendorId - Vendor id.
*/
findById(vendorId: number) {
const { Contact } = this.models;
return Contact.query().findById(vendorId);
}
/**
* Retrieve the bill that associated to the given vendor id.
* @param {number} vendorId - Vendor id.
@@ -49,4 +58,23 @@ export default class VendorRepository extends TenantRepository {
.whereIn('id', vendorIds)
.withGraphFetched('bills');
}
changeBalance(vendorId: number, amount: number) {
const { Contact } = this.models;
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
return Contact.query()
.where('id', vendorId)
[changeMethod]('balance', Math.abs(amount));
}
changeDiffBalance(
vendorId: number,
amount: number,
oldAmount: number,
oldVendorId?: number,
) {
}
}

View File

@@ -26,7 +26,7 @@ export default class ContactsService {
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
private async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) {
public async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) {
const { Contact } = this.tenancy.models(tenantId);
this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId });

View File

@@ -1,5 +1,9 @@
import { Inject, Service } from 'typedi';
import { omit, difference } from 'lodash';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import ContactsService from 'services/Contacts/ContactsService';
@@ -13,6 +17,7 @@ import {
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
@Service()
export default class CustomersService {
@@ -25,6 +30,12 @@ export default class CustomersService {
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger')
logger: any;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
@@ -43,31 +54,44 @@ export default class CustomersService {
* Creates a new customer.
* @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO
* @return {Promise<void>}
* @return {Promise<ICustomer>}
*/
public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
public async newCustomer(
tenantId: number,
customerDTO: ICustomerNewDTO
): Promise<ICustomer> {
this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO });
const contactDTO = this.customerToContactDTO(customerDTO)
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
// Writes the customer opening balance journal entries.
if (customer.openingBalance) {
await this.writeCustomerOpeningBalanceJournal(
tenantId,
customer.id,
customer.openingBalance,
);
}
this.logger.info('[customer] created successfully.', { tenantId, customerDTO });
await this.eventDispatcher.dispatch(events.customers.onCreated);
return customer;
}
/**
* Edits details of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {ICustomerEditDTO} customerDTO
* @return {Promise<ICustomer>}
*/
public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
public async editCustomer(
tenantId: number,
customerId: number,
customerDTO: ICustomerEditDTO,
): Promise<ICustomer> {
const contactDTO = this.customerToContactDTO(customerDTO);
return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
this.logger.info('[customer] trying to edit customer.', { tenantId, customerId, customerDTO });
const customer = this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
this.eventDispatcher.dispatch(events.customers.onEdited);
this.logger.info('[customer] edited successfully.', { tenantId, customerId });
return customer;
}
/**
@@ -76,16 +100,31 @@ export default class CustomersService {
* @param {number} customerId
* @return {Promise<void>}
*/
public async deleteCustomer(tenantId: number, customerId: number) {
public async deleteCustomer(tenantId: number, customerId: number): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
this.logger.info('[customer] trying to delete customer.', { tenantId, customerId });
await this.getCustomerByIdOrThrowError(tenantId, customerId);
await this.customerHasNoInvoicesOrThrowError(tenantId, customerId);
await Contact.query().findById(customerId).delete();
await this.eventDispatcher.dispatch(events.customers.onDeleted);
this.logger.info('[customer] deleted successfully.', { tenantId, customerId });
}
/**
* Reverts customer opening balance journal entries.
* @param {number} tenantId -
* @param {number} customerId -
* @return {Promise<void>}
*/
public async revertOpeningBalanceEntries(tenantId: number, customerId: number|number[]) {
const id = Array.isArray(customerId) ? customerId : [customerId];
this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId });
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, [customerId], 'customer',
tenantId, id, 'customer',
);
}
@@ -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(

View File

@@ -1,5 +1,9 @@
import { Inject, Service } from 'typedi';
import { difference, rest } from 'lodash';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import ContactsService from 'services/Contacts/ContactsService';
@@ -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);

View File

@@ -143,16 +143,21 @@ export default class ExpensesService implements IExpensesService {
}
}
/**
* Reverts expense journal entries.
* @param {number} tenantId
* @param {number} expenseId
*/
public async revertJournalEntries(
tenantId: number,
expenseId: number|number[],
) {
): Promise<void> {
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
await journalCommands.revertJournalEntries(expenseId, 'Expense');
return Promise.all([
await Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);

View File

@@ -1,15 +1,41 @@
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import { entries, omit, sumBy, difference } from 'lodash';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import moment from 'moment';
import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import events from 'subscribers/events';
import {
IBill,
IBillPaymentDTO,
IBillPaymentEntryDTO,
IBillPayment,
IBillPaymentsFilter,
IPaginationMeta,
IFilterMeta,
IBillPaymentEntry,
} from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
import { ServiceError } from 'exceptions';
const ERRORS = {
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
PAYMENT_MADE_NOT_FOUND: 'PAYMENT_MADE_NOT_FOUND',
BILL_PAYMENT_NUMBER_NOT_UNQIUE: 'BILL_PAYMENT_NUMBER_NOT_UNQIUE',
PAYMENT_ACCOUNT_NOT_FOUND: 'PAYMENT_ACCOUNT_NOT_FOUND',
PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND',
INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT',
};
/**
* Bill payments service.
@@ -29,6 +55,177 @@ export default class BillPaymentsService {
@Inject()
dynamicListService: DynamicListingService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject('logger')
logger: any;
/**
* Validate whether the bill payment vendor exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async getVendorOrThrowError(tenantId: number, vendorId: number) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
const vendor = await vendorRepository.findById(vendorId);
if (!vendor) {
throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND)
}
return vendor;
}
/**
* Validates the bill payment existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async getPaymentMadeOrThrowError(tenantid: number, paymentMadeId: number) {
const { BillPayment } = this.tenancy.models(tenantid);
const billPayment = await BillPayment.query().findById(paymentMadeId);
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return billPayment;
}
/**
* Validates the payment account.
* @param {number} tenantId -
* @param {number} paymentAccountId
* @return {Promise<IAccountType>}
*/
private async getPaymentAccountOrThrowError(tenantId: number, paymentAccountId: number) {
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset');
const paymentAccount = await accountRepository.findById(paymentAccountId);
const currentAssetTypesIds = currentAssetTypes.map(type => type.id);
if (!paymentAccount) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
}
if (currentAssetTypesIds.indexOf(paymentAccount.accountTypeId) === -1) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE);
}
return paymentAccount;
}
/**
* Validates the payment number uniqness.
* @param {number} tenantId -
* @param {string} paymentMadeNumber -
* @return {Promise<IBillPayment>}
*/
private async validatePaymentNumber(tenantId: number, paymentMadeNumber: string, notPaymentMadeId?: string) {
const { BillPayment } = this.tenancy.models(tenantId);
const foundBillPayment = await BillPayment.query()
.onBuild((builder: any) => {
builder.findOne('payment_number', paymentMadeNumber);
if (notPaymentMadeId) {
builder.whereNot('id', notPaymentMadeId);
}
});
if (foundBillPayment) {
throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE)
}
return foundBillPayment;
}
/**
* Validate whether the entries bills ids exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async validateBillsExistance(tenantId: number, billPaymentEntries: IBillPaymentEntry[], notVendorId?: number) {
const { Bill } = this.tenancy.models(tenantId);
const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId);
const storedBills = await Bill.query().onBuild((builder) => {
builder.whereIn('id', entriesBillsIds);
if (notVendorId) {
builder.where('vendor_id', notVendorId);
}
});
const storedBillsIds = storedBills.map((t: IBill) => t.id);
const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds);
if (notFoundBillsIds.length > 0) {
throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND)
}
}
/**
* Validate wether the payment amount bigger than the payable amount.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @return {void}
*/
private async validateBillsDueAmount(tenantId: number, billPaymentEntries: IBillPaymentEntryDTO[]) {
const { Bill } = this.tenancy.models(tenantId);
const billsIds = billPaymentEntries.map((entry: IBillPaymentEntryDTO) => entry.billId);
const storedBills = await Bill.query().whereIn('id', billsIds);
const storedBillsMap = new Map(
storedBills.map((bill: any) => [bill.id, bill]),
);
interface invalidPaymentAmountError{
index: number,
due_amount: number
};
const hasWrongPaymentAmount: invalidPaymentAmountError[] = [];
billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => {
const entryBill = storedBillsMap.get(entry.billId);
const { dueAmount } = entryBill;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
});
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT);
}
}
/**
* Validate the payment receive entries IDs existance.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
private async validateEntriesIdsExistance(
tenantId: number,
billPaymentId: number,
billPaymentEntries: IBillPaymentEntry[]
) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
const entriesIds = billPaymentEntries
.filter((entry: any) => entry.id)
.map((entry: any) => entry.id);
const storedEntries = await BillPaymentEntry.query().where('bill_payment_id', billPaymentId);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND);
}
}
/**
* Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions.
@@ -40,53 +237,39 @@ export default class BillPaymentsService {
* - Increment the payment amount of the given vendor bills.
* - Decrement the vendor balance.
* - Records payment journal entries.
* ------
* @param {number} tenantId - Tenant id.
* @param {BillPaymentDTO} billPayment - Bill payment object.
*/
public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) {
const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
public async createBillPayment(
tenantId: number,
billPaymentDTO: IBillPaymentDTO
): Promise<IBillPayment> {
this.logger.info('[paymentDate] trying to save payment made.', { tenantId, billPaymentDTO });
const { BillPayment } = this.tenancy.models(tenantId);
const billPayment = {
amount: sumBy(billPaymentDTO.entries, 'payment_amount'),
...formatDateFields(billPaymentDTO, ['payment_date']),
}
const storedBillPayment = await BillPayment.query()
.insert({
...omit(billPayment, ['entries']),
const billPaymentObj = {
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
...formatDateFields(billPaymentDTO, ['paymentDate']),
};
await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId);
await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber);
await this.validateBillsExistance(tenantId, billPaymentObj.entries);
await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
const billPayment = await BillPayment.query()
.insertGraph({
...omit(billPaymentObj, ['entries']),
entries: billPaymentDTO.entries,
});
const storeOpers: Promise<any>[] = [];
billPayment.entries.forEach((entry) => {
const oper = BillPaymentEntry.query()
.insert({
bill_payment_id: storedBillPayment.id,
...entry,
});
// Increment the bill payment amount.
const billOper = Bill.changePaymentAmount(
entry.bill_id,
entry.payment_amount,
);
storeOpers.push(billOper);
storeOpers.push(oper);
await this.eventDispatcher.dispatch(events.billPayments.onCreated, {
tenantId, billPayment, billPaymentId: billPayment.id,
});
// Decrement the vendor balance after bills payments.
const vendorDecrementOper = Vendor.changeBalance(
billPayment.vendor_id,
billPayment.amount * -1,
);
// Records the journal transactions after bills payment
// and change diff acoount balance.
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries(tenantId, {
id: storedBillPayment.id,
...billPayment,
});
await Promise.all([
...storeOpers,
recordJournalTransaction,
vendorDecrementOper,
]);
return storedBillPayment;
this.logger.info('[payment_made] inserted successfully.', { tenantId, billPaymentId: billPayment.id, });
return billPayment;
}
/**
@@ -110,63 +293,31 @@ export default class BillPaymentsService {
tenantId: number,
billPaymentId: number,
billPaymentDTO,
oldBillPayment,
) {
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
const billPayment = {
amount: sumBy(billPaymentDTO.entries, 'payment_amount'),
...formatDateFields(billPaymentDTO, ['payment_date']),
};
const updateBillPayment = await BillPayment.query()
.where('id', billPaymentId)
.update({
...omit(billPayment, ['entries']),
});
const opers = [];
const entriesHasIds = billPayment.entries.filter((i) => i.id);
const entriesHasNoIds = billPayment.entries.filter((e) => !e.id);
): Promise<IBillPayment> {
const { BillPayment } = this.tenancy.models(tenantId);
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted(
oldBillPayment.entries,
entriesHasIds
);
if (entriesIdsShouldDelete.length > 0) {
const deleteOper = BillPaymentEntry.query()
.bulkDelete(entriesIdsShouldDelete);
opers.push(deleteOper);
}
// Entries that should be update to the storage.
if (entriesHasIds.length > 0) {
const updateOper = BillPaymentEntry.query()
.bulkUpdate(entriesHasIds, { where: 'id' });
opers.push(updateOper);
}
// Entries that should be inserted to the storage.
if (entriesHasNoIds.length > 0) {
const insertOper = BillPaymentEntry.query()
.bulkInsert(
entriesHasNoIds.map((e) => ({ ...e, bill_payment_id: billPaymentId }))
);
opers.push(insertOper);
}
// Records the journal transactions after bills payment and change
// different acoount balance.
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries(tenantId, {
id: storedBillPayment.id,
...billPayment,
});
// Change the different vendor balance between the new and old one.
const changeDiffBalance = Vendor.changeDiffBalance(
billPayment.vendor_id,
oldBillPayment.vendorId,
billPayment.amount * -1,
oldBillPayment.amount * -1,
);
await Promise.all([
...opers,
recordJournalTransaction,
changeDiffBalance,
]);
const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId);
const billPaymentObj = {
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
...formatDateFields(billPaymentDTO, ['paymentDate']),
};
await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId);
await this.validateEntriesIdsExistance(tenantId, billPaymentId, billPaymentObj.entries);
await this.validateBillsExistance(tenantId, billPaymentObj.entries);
await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
const billPayment = await BillPayment.query()
.upsertGraph({
id: billPaymentId,
...omit(billPaymentObj, ['entries']),
});
await this.eventDispatcher.dispatch(events.billPayments.onEdited);
this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldPaymentMade });
return billPayment;
}
/**
@@ -176,29 +327,16 @@ export default class BillPaymentsService {
* @return {Promise}
*/
public async deleteBillPayment(tenantId: number, billPaymentId: number) {
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query().where('id', billPaymentId).first();
const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId);
this.logger.info('[bill_payment] trying to delete.', { tenantId, billPaymentId });
const oldPaymentMade = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId);
await BillPayment.query()
.where('id', billPaymentId)
.delete();
await BillPaymentEntry.query().where('bill_payment_id', billPaymentId).delete();
await BillPayment.query().where('id', billPaymentId).delete();
await BillPaymentEntry.query()
.where('bill_payment_id', billPaymentId)
.delete();
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
billPaymentId,
'BillPayment',
);
const revertVendorBalanceOper = Vendor.changeBalance(
billPayment.vendorId,
billPayment.amount,
);
return Promise.all([
deleteTransactionsOper,
revertVendorBalanceOper,
]);
await this.eventDispatcher.dispatch(events.billPayments.onDeleted, { tenantId, billPaymentId, oldPaymentMade });
this.logger.info('[bill_payment] deleted successfully.', { tenantId, billPaymentId });
}
/**
@@ -207,15 +345,13 @@ export default class BillPaymentsService {
* @param {BillPayment} billPayment
* @param {Integer} billPaymentId
*/
private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
public async recordJournalEntries(tenantId: number, billPayment: IBillPayment) {
const { AccountTransaction } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAmount = sumBy(billPayment.entries, 'payment_amount');
const formattedDate = moment(billPayment.payment_date).format('YYYY-MM-DD');
const payableAccount = await this.accountsService.getAccountByType(
tenantId,
'accounts_payable'
);
const paymentAmount = sumBy(billPayment.entries, 'paymentAmount');
const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD');
const payableAccount = await accountRepository.getBySlug('accounts-payable');
const journal = new JournalPoster(tenantId);
const commonJournal = {
@@ -238,13 +374,13 @@ export default class BillPaymentsService {
...commonJournal,
debit: paymentAmount,
contactType: 'Vendor',
contactId: billPayment.vendor_id,
contactId: billPayment.vendorId,
account: payableAccount.id,
});
const creditPaymentAccount = new JournalEntry({
...commonJournal,
credit: paymentAmount,
account: billPayment.payment_account_id,
account: billPayment.paymentAccountId,
});
journal.debit(debitReceivable);
journal.credit(creditPaymentAccount);
@@ -256,6 +392,24 @@ export default class BillPaymentsService {
]);
}
/**
* Reverts bill payment journal entries.
* @param {number} tenantId
* @param {number} billPaymentId
* @return {Promise<void>}
*/
public async revertJournalEntries(tenantId: number, billPaymentId: number) {
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
await journalCommands.revertJournalEntries(billPaymentId, 'BillPayment');
return Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);
}
/**
* Retrieve bill payment paginted and filterable list.
* @param {number} tenantId
@@ -301,18 +455,4 @@ export default class BillPaymentsService {
return billPayment;
}
/**
* Detarmines whether the bill payment exists on the storage.
* @param {Integer} billPaymentId
* @return {boolean}
*/
async isBillPaymentExists(tenantId: number, billPaymentId: number) {
const { BillPayment } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query()
.where('id', billPaymentId)
.first();
return (billPayment.length > 0);
}
}

View File

@@ -1,16 +1,39 @@
import { omit, sumBy, pick } from 'lodash';
import { omit, sumBy, pick, difference } from 'lodash';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPosterService from 'services/Sales/JournalPosterService';
import InventoryService from 'services/Inventory/Inventory';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
import{ IBillOTD, IBill, IItem } from 'interfaces';
import {
IBillDTO,
IBill,
IItem,
ISystemUser,
IItemEntry,
IItemEntryDTO,
IBillEditDTO,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService';
const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND',
BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE',
BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS',
BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
};
/**
* Vendor bills services.
@@ -24,9 +47,138 @@ export default class BillsService extends SalesInvoicesCost {
@Inject()
accountsService: AccountsService;
@Inject()
itemsService: ItemsService;
@Inject()
tenancy: TenancyService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject('logger')
logger: any;
/**
* Validates whether the vendor is exist.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async getVendorOrThrowError(tenantId: number, vendorId: number) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[bill] trying to get vendor.', { tenantId, vendorId });
const foundVendor = await vendorRepository.findById(vendorId);
if (!foundVendor) {
this.logger.info('[bill] the given vendor not found.', { tenantId, vendorId });
throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND);
}
return foundVendor;
}
/**
* Validates the given bill existance.
* @async
* @param {number} tenantId -
* @param {number} billId -
*/
private async getBillOrThrowError(tenantId: number, billId: number) {
const { Bill } = this.tenancy.models(tenantId);
this.logger.info('[bill] trying to get bill.', { tenantId, billId });
const foundBill = await Bill.query().findById(billId).withGraphFetched('entries');
if (!foundBill) {
this.logger.info('[bill] the given bill not found.', { tenantId, billId });
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
return foundBill;
}
/**
* Validates the entries items ids.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async validateItemsIdsExistance(tenantId: number, billEntries: IItemEntryDTO[]) {
const { Item } = this.tenancy.models(tenantId);
const itemsIds = billEntries.map((e) => e.itemId);
const foundItems = await Item.query().whereIn('id', itemsIds);
const foundItemsIds = foundItems.map((item: IItem) => item.id);
const notFoundItemsIds = difference(itemsIds, foundItemsIds);
if (notFoundItemsIds.length > 0) {
throw new ServiceError(ERRORS.BILL_ITEMS_NOT_FOUND);
}
}
/**
* Validates the bill number existance.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async validateBillNumberExists(tenantId: number, billNumber: string) {
const { Bill } = this.tenancy.models(tenantId);
const foundBills = await Bill.query().where('bill_number', billNumber);
if (foundBills.length > 0) {
throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS);
}
}
/**
* Validates the entries ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEntriesIdsExistance(tenantId: number, billId: number, billEntries: any) {
const { ItemEntry } = this.tenancy.models(tenantId);
const entriesIds = billEntries.filter((e) => e.id).map((e) => e.id);
const storedEntries = await ItemEntry.query()
.whereIn('reference_id', [billId])
.whereIn('reference_type', ['Bill']);
const storedEntriesIds = storedEntries.map((entry) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND)
}
}
/**
* Validate the entries items that not purchase-able.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async validateNonPurchasableEntriesItems(tenantId: number, billEntries: any) {
const { Item } = this.tenancy.models(tenantId);
const itemsIds = billEntries.map((e: IItemEntry) => e.itemId);
const purchasbleItems = await Item.query()
.where('purchasable', true)
.whereIn('id', itemsIds);
const purchasbleItemsIds = purchasbleItems.map((item: IItem) => item.id);
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
if (notPurchasableItems.length > 0) {
throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS);
}
}
/**
* Converts bill DTO to model.
* @param {number} tenantId
@@ -35,13 +187,13 @@ export default class BillsService extends SalesInvoicesCost {
*
* @returns {IBill}
*/
async billDTOToModel(tenantId: number, billDTO: IBillOTD, oldBill?: IBill) {
private async billDTOToModel(tenantId: number, billDTO: IBillDTO, oldBill?: IBill) {
const { ItemEntry } = this.tenancy.models(tenantId);
let invLotNumber = oldBill?.invLotNumber;
if (!invLotNumber) {
invLotNumber = await this.inventoryService.nextLotNumber(tenantId);
}
// if (!invLotNumber) {
// invLotNumber = await this.inventoryService.nextLotNumber(tenantId);
// }
const entries = billDTO.entries.map((entry) => ({
...entry,
amount: ItemEntry.calcAmount(entry),
@@ -58,7 +210,7 @@ export default class BillsService extends SalesInvoicesCost {
/**
* Creates a new bill and stored it to the storage.
*
* ----
* Precedures.
* ----
* - Insert bill transactions to the storage.
@@ -67,55 +219,43 @@ export default class BillsService extends SalesInvoicesCost {
* - Record bill journal transactions on the given accounts.
* - Record bill items inventory transactions.
* ----
* @param {number} tenantId - The given tenant id.
* @param {IBillOTD} billDTO -
* @return {void}
* @param {number} tenantId - The given tenant id.
* @param {IBillDTO} billDTO -
* @return {Promise<IBill>}
*/
async createBill(tenantId: number, billDTO: IBillOTD) {
const { Vendor, Bill, ItemEntry } = this.tenancy.models(tenantId);
public async createBill(
tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
const bill = await this.billDTOToModel(tenantId, billDTO);
const saveEntriesOpers = [];
this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO });
const billObj = await this.billDTOToModel(tenantId, billDTO);
const storedBill = await Bill.query()
.insert({
...omit(bill, ['entries']),
});
bill.entries.forEach((entry) => {
const oper = ItemEntry.query()
.insertAndFetch({
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
await this.validateBillNumberExists(tenantId, billDTO.billNumber);
await this.validateItemsIdsExistance(tenantId, billDTO.entries);
await this.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
const bill = await Bill.query()
.insertGraph({
...omit(billObj, ['entries']),
userId: authorizedUser.id,
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill',
reference_id: storedBill.id,
...omit(entry, ['amount']),
}).then((itemEntry) => {
entry.id = itemEntry.id;
});
saveEntriesOpers.push(oper);
...omit(entry, ['amount', 'id']),
})),
});
// Triggers `onBillCreated` event.
await this.eventDispatcher.dispatch(events.bills.onCreated, {
tenantId, bill, billId: bill.id,
});
// Await save all bill entries operations.
await Promise.all([...saveEntriesOpers]);
this.logger.info('[bill] bill inserted successfully.', { tenantId, billId: bill.id });
// Increments vendor balance.
const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount);
// Rewrite the inventory transactions for inventory items.
const writeInvTransactionsOper = this.recordInventoryTransactions(
tenantId, bill, storedBill.id
);
// Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions(tenantId, {
id: storedBill.id, ...bill,
});
await Promise.all([
incrementOper,
writeInvTransactionsOper,
writeJEntriesOper,
]);
// Schedule bill re-compute based on the item cost
// method and starting date.
await this.scheduleComputeBillItemsCost(tenantId, bill);
return storedBill;
return bill;
}
/**
@@ -132,55 +272,34 @@ export default class BillsService extends SalesInvoicesCost {
*
* @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill id.
* @param {billDTO} billDTO - The given new bill details.
* @param {IBillEditDTO} billDTO - The given new bill details.
* @return {Promise<IBill>}
*/
async editBill(tenantId: number, billId: number, billDTO: billDTO) {
const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId);
const oldBill = await Bill.query().findById(billId);
const bill = this.billDTOToModel(tenantId, billDTO, oldBill);
public async editBill(
tenantId: number,
billId: number,
billDTO: IBillEditDTO,
): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
const oldBill = await this.getBillOrThrowError(tenantId, billId);
const billObj = this.billDTOToModel(tenantId, billDTO, oldBill);
// Update the bill transaction.
const updatedBill = await Bill.query()
.where('id', billId)
.update({
...omit(bill, ['entries', 'invLotNumber'])
const bill = await Bill.query()
.upsertGraph({
id: billId,
...omit(billObj, ['entries', 'invLotNumber']),
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}))
});
// Old stored entries.
const storedEntries = await ItemEntry.query()
.where('reference_id', billId)
.where('reference_type', 'Bill');
// Triggers event `onBillEdited`.
await this.eventDispatcher.dispatch(events.bills.onEdited, { tenantId, billId, oldBill, bill });
// Patch the bill entries.
const patchEntriesOper = HasItemsEntries.patchItemsEntries(
bill.entries, storedEntries, 'Bill', billId,
);
// Changes the diff vendor balance between old and new amount.
const changeVendorBalanceOper = Vendor.changeDiffBalance(
bill.vendor_id,
oldBill.vendorId,
bill.amount,
oldBill.amount,
);
// Re-write the inventory transactions for inventory items.
const writeInvTransactionsOper = this.recordInventoryTransactions(
tenantId, bill, billId, true
);
// Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions(tenantId, {
id: billId,
...bill,
}, billId);
await Promise.all([
patchEntriesOper,
changeVendorBalanceOper,
writeInvTransactionsOper,
writeJEntriesOper,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeBillItemsCost(tenantId, bill);
return bill;
}
/**
@@ -188,13 +307,10 @@ export default class BillsService extends SalesInvoicesCost {
* @param {Integer} billId
* @return {void}
*/
async deleteBill(tenantId: number, billId: number) {
const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId);
public async deleteBill(tenantId: number, billId: number) {
const { Bill, ItemEntry } = this.tenancy.models(tenantId);
const bill = await Bill.query()
.where('id', billId)
.withGraphFetched('entries')
.first();
const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Delete all associated bill entries.
const deleteBillEntriesOper = ItemEntry.query()
@@ -205,28 +321,10 @@ export default class BillsService extends SalesInvoicesCost {
// Delete the bill transaction.
const deleteBillOper = Bill.query().where('id', billId).delete();
// Delete associated bill journal transactions.
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
billId,
'Bill'
);
// Delete bill associated inventory transactions.
const deleteInventoryTransOper = this.inventoryService.deleteInventoryTransactions(
tenantId, billId, 'Bill'
);
// Revert vendor balance.
const revertVendorBalance = Vendor.changeBalance(bill.vendorId, bill.amount * -1);
await Promise.all([
deleteBillOper,
deleteBillEntriesOper,
deleteTransactionsOper,
deleteInventoryTransOper,
revertVendorBalance,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeBillItemsCost(tenantId, bill);
await Promise.all([deleteBillEntriesOper, deleteBillOper]);
// Triggers `onBillDeleted` event.
await this.eventDispatcher.dispatch(events.bills.onDeleted, { tenantId, billId, oldBill });
}
/**
@@ -262,20 +360,18 @@ export default class BillsService extends SalesInvoicesCost {
* @param {IBill} bill
* @param {Integer} billId
*/
async recordJournalTransactions(tenantId: number, bill: any, billId?: number) {
const { AccountTransaction, Item } = this.tenancy.models(tenantId);
async recordJournalTransactions(tenantId: number, bill: IBill, billId?: number) {
const { AccountTransaction, Item, ItemEntry } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const entriesItemsIds = bill.entries.map((entry) => entry.item_id);
const payableTotal = sumBy(bill.entries, 'amount');
const formattedDate = moment(bill.bill_date).format('YYYY-MM-DD');
const entriesItemsIds = bill.entries.map((entry) => entry.itemId);
const formattedDate = moment(bill.billDate).format('YYYY-MM-DD');
const storedItems = await Item.query()
.whereIn('id', entriesItemsIds);
const storedItems = await Item.query().whereIn('id', entriesItemsIds);
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
const payableAccount = await this.accountsService.getAccountByType(
tenantId, 'accounts_payable'
);
const payableAccount = await accountRepository.getBySlug('accounts-payable');
const journal = new JournalPoster(tenantId);
const commonJournalMeta = {
@@ -284,7 +380,7 @@ export default class BillsService extends SalesInvoicesCost {
referenceId: bill.id,
referenceType: 'Bill',
date: formattedDate,
accural: true,
userId: bill.userId,
};
if (billId) {
const transactions = await AccountTransaction.query()
@@ -297,23 +393,26 @@ export default class BillsService extends SalesInvoicesCost {
}
const payableEntry = new JournalEntry({
...commonJournalMeta,
credit: payableTotal,
credit: bill.amount,
account: payableAccount.id,
contactId: bill.vendor_id,
contactId: bill.vendorId,
contactType: 'Vendor',
index: 1,
});
journal.credit(payableEntry);
bill.entries.forEach((entry) => {
const item: IItem = storedItemsMap.get(entry.item_id);
bill.entries.forEach((entry, index) => {
const item: IItem = storedItemsMap.get(entry.itemId);
const amount = ItemEntry.calcAmount(entry);
const debitEntry = new JournalEntry({
...commonJournalMeta,
debit: entry.amount,
debit: amount,
account:
['inventory'].indexOf(item.type) !== -1
? item.inventoryAccountId
: item.costAccountId,
index: index + 2,
});
journal.debit(debitEntry);
});
@@ -324,44 +423,6 @@ export default class BillsService extends SalesInvoicesCost {
]);
}
/**
* Detarmines whether the bill exists on the storage.
* @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill id.
* @return {Boolean}
*/
async isBillExists(tenantId: number, billId: number) {
const { Bill } = this.tenancy.models(tenantId);
const foundBills = await Bill.query().where('id', billId);
return foundBills.length > 0;
}
/**
* Detarmines whether the given bills exist on the storage in bulk.
* @param {Array} billsIds
* @return {Boolean}
*/
async isBillsExist(tenantId: number, billsIds: number[]) {
const { Bill } = this.tenancy.models(tenantId);
const bills = await Bill.query().whereIn('id', billsIds);
return bills.length > 0;
}
/**
* Detarmines whether the given bill id exists on the storage.
* @param {number} tenantId
* @param {Integer} billNumber
* @return {boolean}
*/
async isBillNoExists(tenantId: number, billNumber : string) {
const { Bill } = this.tenancy.models(tenantId);
const foundBills = await Bill.query()
.where('bill_number', billNumber);
return foundBills.length > 0;
}
/**
* Retrieve the given bill details with associated items entries.
@@ -370,8 +431,7 @@ export default class BillsService extends SalesInvoicesCost {
*/
getBill(tenantId: number, billId: number) {
const { Bill } = this.tenancy.models(tenantId);
return Bill.query().where('id', billId).first();
return Bill.query().findById(billId).withGraphFetched('entries');
}
/**

View File

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

View File

@@ -1,6 +1,11 @@
import { omit, sumBy, chain } from 'lodash';
import moment from 'moment';
import { Service, Inject } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import { IPaymentReceiveOTD } from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster';
@@ -34,6 +39,186 @@ export default class PaymentReceiveService {
@Inject('logger')
logger: any;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Validates the payment receive number existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validatePaymentReceiveNoExistance(req: Request, res: Response, next: Function) {
const tenantId = req.tenantId;
const isPaymentNoExists = await this.paymentReceiveService.isPaymentReceiveNoExists(
tenantId,
req.body.payment_receive_no,
req.params.id,
);
if (isPaymentNoExists) {
return res.status(400).send({
errors: [{ type: 'PAYMENT.RECEIVE.NUMBER.EXISTS', code: 400 }],
});
}
next();
}
/**
* Validates the payment receive existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validatePaymentReceiveExistance(req: Request, res: Response, next: Function) {
const tenantId = req.tenantId;
const isPaymentNoExists = await this.paymentReceiveService
.isPaymentReceiveExists(
tenantId,
req.params.id
);
if (!isPaymentNoExists) {
return res.status(400).send({
errors: [{ type: 'PAYMENT.RECEIVE.NOT.EXISTS', code: 600 }],
});
}
next();
}
/**
* Validate the deposit account id existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateDepositAccount(req: Request, res: Response, next: Function) {
const tenantId = req.tenantId;
const isDepositAccExists = await this.accountsService.isAccountExists(
tenantId,
req.body.deposit_account_id
);
if (!isDepositAccExists) {
return res.status(400).send({
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
});
}
next();
}
/**
* Validates the `customer_id` existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateCustomerExistance(req: Request, res: Response, next: Function) {
const { Customer } = req.models;
const isCustomerExists = await Customer.query().findById(req.body.customer_id);
if (!isCustomerExists) {
return res.status(400).send({
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
});
}
next();
}
/**
* Validates the invoices IDs existance.
* @param {Request} req -
* @param {Response} res -
* @param {Function} next -
*/
async validateInvoicesIDs(req: Request, res: Response, next: Function) {
const paymentReceive = { ...req.body };
const { tenantId } = req;
const invoicesIds = paymentReceive.entries
.map((e) => e.invoice_id);
const notFoundInvoicesIDs = await this.saleInvoiceService.isInvoicesExist(
tenantId,
invoicesIds,
paymentReceive.customer_id,
);
if (notFoundInvoicesIDs.length > 0) {
return res.status(400).send({
errors: [{ type: 'INVOICES.IDS.NOT.FOUND', code: 500 }],
});
}
next();
}
/**
* Validates entries invoice payment amount.
* @param {Request} req -
* @param {Response} res -
* @param {Function} next -
*/
async validateInvoicesPaymentsAmount(req: Request, res: Response, next: Function) {
const { SaleInvoice } = req.models;
const invoicesIds = req.body.entries.map((e) => e.invoice_id);
const storedInvoices = await SaleInvoice.query()
.whereIn('id', invoicesIds);
const storedInvoicesMap = new Map(
storedInvoices.map((invoice) => [invoice.id, invoice])
);
const hasWrongPaymentAmount: any[] = [];
req.body.entries.forEach((entry, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoice_id);
const { dueAmount } = entryInvoice;
if (dueAmount < entry.payment_amount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
});
if (hasWrongPaymentAmount.length > 0) {
return res.status(400).send({
errors: [
{
type: 'INVOICE.PAYMENT.AMOUNT',
code: 200,
indexes: hasWrongPaymentAmount,
},
],
});
}
next();
}
/**
* Validate the payment receive entries IDs existance.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
const paymentReceive = { id: req.params.id, ...req.body };
const entriesIds = paymentReceive.entries
.filter(entry => entry.id)
.map(entry => entry.id);
const { PaymentReceiveEntry } = req.models;
const storedEntries = await PaymentReceiveEntry.query()
.where('payment_receive_id', paymentReceive.id);
const storedEntriesIds = storedEntries.map((entry) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
return res.status(400).send({
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
});
}
next();
}
/**
* Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions.
@@ -53,9 +238,10 @@ export default class PaymentReceiveService {
this.logger.info('[payment_receive] inserting to the storage.');
const storedPaymentReceive = await PaymentReceive.query()
.insert({
.insertGraph({
amount: paymentAmount,
...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']),
entries: paymentReceive.entries.map((entry) => ({ ...entry })),
});
const storeOpers: Array<any> = [];
@@ -92,6 +278,8 @@ export default class PaymentReceiveService {
customerIncrementOper,
recordJournalTransactions,
]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated);
return storedPaymentReceive;
}
@@ -186,6 +374,7 @@ export default class PaymentReceiveService {
changeCustomerBalance,
diffInvoicePaymentAmount,
]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited);
}
/**
@@ -239,6 +428,7 @@ export default class PaymentReceiveService {
revertCustomerBalance,
revertInvoicesPaymentAmount,
]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted);
}
/**

View File

@@ -1,10 +1,15 @@
import { omit, difference, sumBy, mixin } from 'lodash';
import { Service, Inject } from 'typedi';
import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces';
import { IEstimatesFilter, IFilterMeta, IPaginationMeta, ISaleEstimate, ISaleEstimateDTO } from 'interfaces';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import { formatDateFields } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
/**
* Sale estimate service.
@@ -24,14 +29,132 @@ export default class SaleEstimateService {
@Inject()
dynamicListService: DynamicListingService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Validate whether the estimate customer exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) {
const estimate = { ...req.body };
const { Customer } = req.models
const foundCustomer = await Customer.query().findById(estimate.customer_id);
if (!foundCustomer) {
return res.status(404).send({
errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validate the estimate number unique on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateNumberExistance(req: Request, res: Response, next: Function) {
const estimate = { ...req.body };
const { tenantId } = req;
const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique(
tenantId,
estimate.estimate_number,
req.params.id,
);
if (isEstNumberUnqiue) {
return res.boom.badRequest(null, {
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
});
}
next();
}
/**
* Validate the estimate entries items ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) {
const tenantId = req.tenantId;
const estimate = { ...req.body };
const estimateItemsIds = estimate.entries.map(e => e.item_id);
// Validate items ids in estimate entries exists.
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds);
if (notFoundItemsIds.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
});
}
next();
}
/**
* Validate whether the sale estimate id exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEstimateIdExistance(req: Request, res: Response, next: Function) {
const { id: estimateId } = req.params;
const { tenantId } = req;
const storedEstimate = await this.saleEstimateService
.getEstimate(tenantId, estimateId);
if (!storedEstimate) {
return res.status(404).send({
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validate sale invoice entries ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) {
const { ItemEntry } = req.models;
const { id: saleInvoiceId } = req.params;
const saleInvoice = { ...req.body };
const entriesIds = saleInvoice.entries
.filter(e => e.id)
.map((e) => e.id);
const foundEntries = await ItemEntry.query()
.whereIn('id', entriesIds)
.where('reference_type', 'SaleInvoice')
.where('reference_id', saleInvoiceId);
if (foundEntries.length > 0) {
return res.status(400).send({
errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }],
});
}
next();
}
/**
* Creates a new estimate with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {EstimateDTO} estimate
* @return {void}
* @return {Promise<ISaleEstimate>}
*/
async createEstimate(tenantId: number, estimateDTO: any) {
async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
@@ -44,22 +167,15 @@ export default class SaleEstimateService {
const storedEstimate = await SaleEstimate.query()
.insert({
...omit(estimate, ['entries']),
});
const storeEstimateEntriesOpers: any[] = [];
this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.');
estimate.entries.forEach((entry: any) => {
const oper = ItemEntry.query()
.insert({
entries: estimate.entries.map((entry) => ({
reference_type: 'SaleEstimate',
reference_id: storedEstimate.id,
...omit(entry, ['total', 'amount', 'id']),
});
storeEstimateEntriesOpers.push(oper);
});
await Promise.all([...storeEstimateEntriesOpers]);
}))
});
this.logger.info('[sale_estimate] insert sale estimated success.');
await this.eventDispatcher.dispatch(events.saleEstimates.onCreated);
return storedEstimate;
}
@@ -72,7 +188,7 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate
* @return {void}
*/
async editEstimate(tenantId: number, estimateId: number, estimateDTO: any) {
async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise<void> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
@@ -89,16 +205,14 @@ export default class SaleEstimateService {
.where('reference_id', estimateId)
.where('reference_type', 'SaleEstimate');
const patchItemsEntries = this.itemsEntriesService.patchItemsEntries(
await this.itemsEntriesService.patchItemsEntries(
tenantId,
estimate.entries,
storedEstimateEntries,
'SaleEstimate',
estimateId,
);
return Promise.all([
patchItemsEntries,
]);
await this.eventDispatcher.dispatch(events.saleEstimates.onEdited);
}
/**
@@ -118,6 +232,9 @@ export default class SaleEstimateService {
.delete();
await SaleEstimate.query().where('id', estimateId).delete();
this.logger.info('[sale_estimate] deleted successfully.', { tenantId, estimateId });
await this.eventDispatcher.dispatch(events.saleEstimates.onDeleted);
}
/**

View File

@@ -4,7 +4,15 @@ import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
import {
ISaleInvoice,
ISaleInvoiceOTD,
IItemEntry,
ISalesInvoicesFilter,
IPaginationMeta,
IFilterMeta
} from 'interfaces';
import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import InventoryService from 'services/Inventory/Inventory';
@@ -12,6 +20,16 @@ import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService';
const ERRORS = {
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
}
/**
* Sales invoices service
@@ -37,6 +55,81 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject()
itemsService: ItemsService;
/**
* Retrieve sale invoice or throw not found error.
* @param {number} tenantId
* @param {number} saleInvoiceId
*/
private async getSaleInvoiceOrThrowError(tenantId: number, saleInvoiceId: number): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query().where('id', saleInvoiceId);
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
return saleInvoice;
}
/**
* Validate whether sale invoice number unqiue on the storage.
* @param {number} tenantId
* @param {number} saleInvoiceNo
* @param {number} notSaleInvoiceId
*/
private async validateSaleInvoiceNoUniquiness(tenantId: number, saleInvoiceNo: string, notSaleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const foundSaleInvoice = await SaleInvoice.query()
.onBuild((query: any) => {
query.where('invoice_no', saleInvoiceNo);
if (notSaleInvoiceId) {
query.whereNot('id', notSaleInvoiceId);
}
return query;
});
if (foundSaleInvoice.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_NO_NOT_UNIQUE);
}
}
/**
* Validates sale invoice items that not sellable.
*/
private async validateNonSellableEntriesItems(tenantId: number, saleInvoiceEntries: any) {
const { Item } = this.tenancy.models(tenantId);
const itemsIds = saleInvoiceEntries.map(e => e.itemId);
const sellableItems = await Item.query().where('sellable', true).whereIn('id', itemsIds);
const sellableItemsIds = sellableItems.map((item) => item.id);
const notSellableItems = difference(itemsIds, sellableItemsIds);
if (notSellableItems.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
}
/**
*
* @param {number} tenantId
* @param {} saleInvoiceEntries
*/
validateEntriesIdsExistance(tenantId: number, saleInvoiceEntries: any) {
const entriesItemsIds = saleInvoiceEntries.map((e) => e.item_id);
const isItemsIdsExists = await this.itemsService.isItemsIdsExists(
tenantId, entriesItemsIds,
);
if (isItemsIdsExists.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_ITEMS_IDS_NOT_EXISTS);
}
}
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
@@ -58,6 +151,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
invLotNumber,
};
await this.validateSaleInvoiceNoUniquiness(tenantId, saleInvoiceDTO.invoiceNo);
await this.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
const storedInvoice = await SaleInvoice.query()
.insert({
@@ -95,6 +191,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
// method and starting date.
await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id);
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated);
return storedInvoice;
}
@@ -131,30 +229,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
.where('reference_type', 'SaleInvoice');
// Patch update the sale invoice items entries.
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries(
await this.itemsEntriesService.patchItemsEntries(
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
);
this.logger.info('[sale_invoice] change customer different balance.');
// Changes the diff customer balance between old and new amount.
const changeCustomerBalanceOper = Customer.changeDiffBalance(
saleInvoice.customer_id,
oldSaleInvoice.customerId,
balance,
oldSaleInvoice.balance,
);
// Records the inventory transactions for inventory items.
const recordInventoryTransOper = this.recordInventoryTranscactions(
tenantId, saleInvoice, saleInvoiceId, true,
);
await Promise.all([
patchItemsEntriesOper,
changeCustomerBalanceOper,
recordInventoryTransOper,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeInvoiceItemsCost(tenantId, saleInvoiceId, true);
// Triggers `onSaleInvoiceEdited` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited);
}
/**
@@ -168,14 +247,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
SaleInvoice,
ItemEntry,
Customer,
Account,
InventoryTransaction,
AccountTransaction,
} = this.tenancy.models(tenantId);
const oldSaleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries');
const oldSaleInvoice = await this.getSaleInvoiceOrThrowError(tenantId, saleInvoiceId);
this.logger.info('[sale_invoice] delete sale invoice with entries.');
await SaleInvoice.query().where('id', saleInvoiceId).delete();
@@ -218,6 +294,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(tenantId, oldSaleInvoice)
await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted);
}
/**

View File

@@ -1,5 +1,10 @@
import { omit, difference, sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import JournalPosterService from 'services/Sales/JournalPosterService';
import HasItemEntries from 'services/Sales/HasItemsEntries';
import TenancyService from 'services/Tenancy/TenancyService';
@@ -21,6 +26,125 @@ export default class SalesReceiptService {
@Inject()
itemsEntriesService: HasItemEntries;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Validate whether sale receipt exists on the storage.
* @param {Request} req
* @param {Response} res
*/
async getSaleReceiptOrThrowError(tenantId: number, saleReceiptId: number) {
const { tenantId } = req;
const { id: saleReceiptId } = req.params;
const isSaleReceiptExists = await this.saleReceiptService
.isSaleReceiptExists(
tenantId,
saleReceiptId,
);
if (!isSaleReceiptExists) {
return res.status(404).send({
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validate whether sale receipt customer exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptCustomerExistance(req: Request, res: Response, next: Function) {
const saleReceipt = { ...req.body };
const { Customer } = req.models;
const foundCustomer = await Customer.query().findById(saleReceipt.customer_id);
if (!foundCustomer) {
return res.status(400).send({
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
});
}
next();
}
/**
* Validate whether sale receipt deposit account exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptDepositAccountExistance(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleReceipt = { ...req.body };
const isDepositAccountExists = await this.accountsService.isAccountExists(
tenantId,
saleReceipt.deposit_account_id
);
if (!isDepositAccountExists) {
return res.status(400).send({
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
});
}
next();
}
/**
* Validate whether receipt items ids exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptItemsIdsExistance(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleReceipt = { ...req.body };
const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id);
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(
tenantId,
estimateItemsIds
);
if (notFoundItemsIds.length > 0) {
return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }] });
}
next();
}
/**
* Validate receipt entries ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptEntriesIds(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleReceipt = { ...req.body };
const { id: saleReceiptId } = req.params;
// Validate the entries IDs that not stored or associated to the sale receipt.
const notExistsEntriesIds = await this.saleReceiptService
.isSaleReceiptEntriesIDsExists(
tenantId,
saleReceiptId,
saleReceipt,
);
if (notExistsEntriesIds.length > 0) {
return res.status(400).send({ errors: [{
type: 'ENTRIES.IDS.NOT.FOUND',
code: 500,
}]
});
}
next();
}
/**
* Creates a new sale receipt with associated entries.
* @async
@@ -38,20 +162,14 @@ export default class SalesReceiptService {
const storedSaleReceipt = await SaleReceipt.query()
.insert({
...omit(saleReceipt, ['entries']),
});
const storeSaleReceiptEntriesOpers: Array<any> = [];
saleReceipt.entries.forEach((entry: any) => {
const oper = ItemEntry.query()
.insert({
entries: saleReceipt.entries.map((entry) => ({
reference_type: 'SaleReceipt',
reference_id: storedSaleReceipt.id,
...omit(entry, ['id', 'amount']),
});
storeSaleReceiptEntriesOpers.push(oper);
});
await Promise.all([...storeSaleReceiptEntriesOpers]);
return storedSaleReceipt;
}))
});
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
}
/**
@@ -85,7 +203,9 @@ export default class SalesReceiptService {
'SaleReceipt',
saleReceiptId,
);
return Promise.all([patchItemsEntries]);
await Promise.all([patchItemsEntries]);
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
}
/**
@@ -110,11 +230,13 @@ export default class SalesReceiptService {
saleReceiptId,
'SaleReceipt'
);
return Promise.all([
await Promise.all([
deleteItemsEntriesOper,
deleteSaleReceiptOper,
deleteTransactionsOper,
]);
await this.eventDispatcher.dispatch(events.saleReceipts.onDeleted);
}
/**

View File

@@ -0,0 +1,83 @@
import { Container, Inject, Service } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import BillsService from 'services/Purchases/Bills';
import JournalPosterService from 'services/Sales/JournalPosterService';
import VendorRepository from 'repositories/VendorRepository';
@EventSubscriber()
export default class BillSubscriber {
tenancy: TenancyService;
billsService: BillsService;
logger: any;
journalPosterService: JournalPosterService;
constructor() {
this.tenancy = Container.get(TenancyService);
this.billsService = Container.get(BillsService);
this.logger = Container.get('logger');
this.journalPosterService = Container.get(JournalPosterService);
}
/**
* Handles vendor balance increment once bill created.
*/
@On(events.bills.onCreated)
async handleVendorBalanceIncrement({ tenantId, billId, bill }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Increments vendor balance.
this.logger.info('[bill] trying to increment vendor balance.', { tenantId, billId });
await vendorRepository.changeBalance(bill.vendorId, bill.amount);
}
/**
* Handles writing journal entries once bill created.
*/
@On(events.bills.onCreated)
@On(events.bills.onEdited)
async handlerWriteJournalEntries({ tenantId, billId, bill }) {
// Writes the journal entries for the given bill transaction.
this.logger.info('[bill] writing bill journal entries.', { tenantId });
await this.billsService.recordJournalTransactions(tenantId, bill);
}
/**
* Handles vendor balance decrement once bill deleted.
*/
@On(events.bills.onDeleted)
async handleVendorBalanceDecrement({ tenantId, billId, oldBill }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Decrements vendor balance.
this.logger.info('[bill] trying to decrement vendor balance.', { tenantId, billId });
await vendorRepository.changeBalance(oldBill.vendorId, oldBill.amount * -1);
}
/**
* Handles revert journal entries on bill deleted.
*/
@On(events.bills.onDeleted)
async handlerDeleteJournalEntries({ tenantId, billId }) {
// Delete associated bill journal transactions.
this.logger.info('[bill] trying to delete journal entries.', { tenantId, billId });
await this.journalPosterService.revertJournalTransactions(tenantId, billId, 'Bill');
}
@On(events.bills.onEdited)
async handleCustomerBalanceDiffChange({ tenantId, billId, oldBill, bill }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Changes the diff vendor balance between old and new amount.
this.logger.info('[bill[ change vendor the different balance.', { tenantId, billId });
await vendorRepository.changeDiffBalance(
bill.vendorId,
oldBill.vendorId,
bill.amount,
oldBill.amount,
);
}
}

View File

@@ -0,0 +1,46 @@
import { Container, Inject, Service } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import CustomersService from 'services/Contacts/CustomersService';
@EventSubscriber()
export default class CustomersSubscriber {
logger: any;
tenancy: TenancyService;
customersService: CustomersService;
constructor() {
this.logger = Container.get('logger');
this.customersService = Container.get(CustomersService);
}
@On(events.customers.onCreated)
async handleWriteOpenBalanceEntries({ tenantId, customerId, customer }) {
// Writes the customer opening balance journal entries.
if (customer.openingBalance) {
await this.customersService.writeCustomerOpeningBalanceJournal(
tenantId,
customer.id,
customer.openingBalance,
);
}
}
@On(events.customers.onDeleted)
async handleRevertOpeningBalanceEntries({ tenantId, customerId }) {
await this.customersService.revertOpeningBalanceEntries(
tenantId, customerId,
);
}
@On(events.customers.onBulkDeleted)
async handleBulkRevertOpeningBalanceEntries({ tenantId, customersIds }) {
await this.customersService.revertOpeningBalanceEntries(
tenantId, customersIds,
);
}
}

View File

@@ -1,5 +1,4 @@
export default {
/**
* Authentication service.
@@ -72,5 +71,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',
},
}

View File

@@ -0,0 +1,108 @@
import { Container, Inject, Service } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import BillPaymentsService from 'services/Purchases/BillPayments';
import TenancyService from 'services/Tenancy/TenancyService';
@EventSubscriber()
export default class PaymentMadesSubscriber {
tenancy: TenancyService;
billPaymentsService: BillPaymentsService;
logger: any;
constructor() {
this.tenancy = Container.get(TenancyService);
this.billPaymentsService = Container.get(BillPaymentsService);
this.logger = Container.get('logger');
}
/**
* Handles bills payment amount increment once payment made created.
*/
@On(events.billPayments.onCreated)
async handleBillsIncrement({ tenantId, billPayment, billPaymentId }) {
const { Bill } = this.tenancy.models(tenantId);
const storeOpers = [];
billPayment.entries.forEach((entry) => {
this.logger.info('[bill_payment] increment bill payment amount.', {
tenantId, billPaymentId,
billId: entry.billId,
amount: entry.paymentAmount,
})
// Increment the bill payment amount.
const billOper = Bill.changePaymentAmount(
entry.billId,
entry.paymentAmount,
);
storeOpers.push(billOper);
});
await Promise.all(storeOpers);
}
/**
* Handle vendor balance increment once payment made created.
*/
@On(events.billPayments.onCreated)
async handleVendorIncrement({ tenantId, billPayment, billPaymentId }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Increment the vendor balance after bills payments.
this.logger.info('[bill_payment] trying to increment vendor balance.', { tenantId });
await vendorRepository.changeBalance(
billPayment.vendorId,
billPayment.amount,
);
}
/**
* Handle bill payment writing journal entries once created.
*/
@On(events.billPayments.onCreated)
async handleWriteJournalEntries({ tenantId, billPayment }) {
// Records the journal transactions after bills payment
// and change diff acoount balance.
this.logger.info('[bill_payment] trying to write journal entries.', { tenantId, billPaymentId: billPayment.id });
await this.billPaymentsService.recordJournalEntries(tenantId, billPayment);
}
/**
* Decrements the vendor balance once bill payment deleted.
*/
@On(events.billPayments.onDeleted)
async handleVendorDecrement({ tenantId, paymentMadeId, oldPaymentMade }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
await vendorRepository.changeBalance(
oldPaymentMade.vendorId,
oldPaymentMade.amount * -1,
);
}
/**
* Reverts journal entries once bill payment deleted.
*/
@On(events.billPayments.onDeleted)
async handleRevertJournalEntries({ tenantId, billPaymentId }) {
await this.billPaymentsService.revertJournalEntries(
tenantId, billPaymentId,
);
}
/**
* Change the vendor balance different between old and new once
* bill payment edited.
*/
@On(events.billPayments.onEdited)
async handleVendorChangeDiffBalance({ tenantId, paymentMadeId, billPayment, oldBillPayment }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Change the different vendor balance between the new and old one.
await vendorRepository.changeDiffBalance(
billPayment.vendor_id,
oldBillPayment.vendorId,
billPayment.amount * -1,
oldBillPayment.amount * -1,
);
}
}

View File

@@ -0,0 +1,22 @@
import { Container } from 'typedi';
import { On, EventSubscriber } from "event-dispatch";
import events from 'subscribers/events';
@EventSubscriber()
export default class SaleInvoiceSubscriber {
@On(events.saleInvoice.onCreated)
public onSaleInvoiceCreated(payload) {
}
@On(events.saleInvoice.onEdited)
public onSaleInvoiceEdited(payload) {
}
@On(events.saleInvoice.onDeleted)
public onSaleInvoiceDeleted(payload) {
}
}

View File

@@ -0,0 +1,46 @@
import { Container, Inject, Service } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import VendorsService from 'services/Contacts/VendorsService';
@EventSubscriber()
export default class VendorsSubscriber {
logger: any;
tenancy: TenancyService;
vendorsService: VendorsService;
/**
* Constructor method.
*/
constructor() {
this.logger = Container.get('logger');
this.vendorsService = Container.get(VendorsService);
}
@On(events.vendors.onCreated)
async handleWriteOpeningBalanceEntries({ tenantId, vendorId, vendor }) {
// Writes the vendor opening balance journal entries.
if (vendor.openingBalance) {
await this.vendorsService.writeVendorOpeningBalanceJournal(
tenantId,
vendor.id,
vendor.openingBalance,
);
}
}
@On(events.vendors.onDeleted)
async handleRevertOpeningBalanceEntries({ tenantId, vendorId }) {
await this.vendorsService.revertOpeningBalanceEntries(
tenantId, vendorId,
);
}
@On(events.vendors.onBulkDeleted)
async handleBulkRevertOpeningBalanceEntries({ tenantId, vendorsIds }) {
await this.vendorsService.revertOpeningBalanceEntries(
tenantId, vendorsIds,
);
}
}