refactoring: payment receive and sale invoice actions.

This commit is contained in:
Ahmed Bouhuolia
2020-10-25 18:30:44 +02:00
parent 39d1875cbb
commit 426f9fcf55
20 changed files with 820 additions and 1069 deletions

View File

@@ -152,7 +152,11 @@ export default class BillsController extends BaseController {
try { try {
const storedBill = await this.billsService.createBill(tenantId, billDTO, user); const storedBill = await this.billsService.createBill(tenantId, billDTO, user);
return res.status(200).send({ id: storedBill.id });
return res.status(200).send({
id: storedBill.id,
message: 'The bill has been created successfully.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -170,7 +174,10 @@ export default class BillsController extends BaseController {
try { try {
const editedBill = await this.billsService.editBill(tenantId, billId, billDTO); const editedBill = await this.billsService.editBill(tenantId, billId, billDTO);
return res.status(200).send({ id: billId }); return res.status(200).send({
id: billId,
message: 'The bill has been edited successfully.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -1,13 +1,12 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain, matchedData } from 'express-validator'; import { check, param, query, ValidationChain } from 'express-validator';
import { difference } from 'lodash';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { IPaymentReceive, IPaymentReceiveOTD } from 'interfaces'; import { IPaymentReceiveDTO } from 'interfaces';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentReceiveService from 'services/Sales/PaymentsReceives'; import PaymentReceiveService from 'services/Sales/PaymentsReceives';
import SaleInvoiceService from 'services/Sales/SalesInvoices'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import AccountsService from 'services/Accounts/AccountsService'; import { ServiceError } from 'exceptions';
/** /**
* Payments receives controller. * Payments receives controller.
@@ -19,11 +18,8 @@ export default class PaymentReceivesController extends BaseController {
paymentReceiveService: PaymentReceiveService; paymentReceiveService: PaymentReceiveService;
@Inject() @Inject()
accountsService: AccountsService; dynamicListService: DynamicListingService;
@Inject()
saleInvoiceService: SaleInvoiceService;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -34,45 +30,37 @@ export default class PaymentReceivesController extends BaseController {
'/:id', '/:id',
this.editPaymentReceiveValidation, this.editPaymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)),
asyncMiddleware(this.validateCustomerExistance.bind(this)),
asyncMiddleware(this.validateDepositAccount.bind(this)),
asyncMiddleware(this.validateInvoicesIDs.bind(this)),
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
asyncMiddleware(this.validateInvoicesPaymentsAmount.bind(this)),
asyncMiddleware(this.editPaymentReceive.bind(this)), asyncMiddleware(this.editPaymentReceive.bind(this)),
this.handleServiceErrors,
); );
router.post( router.post(
'/', '/',
this.newPaymentReceiveValidation, this.newPaymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)),
asyncMiddleware(this.validateCustomerExistance.bind(this)),
asyncMiddleware(this.validateDepositAccount.bind(this)),
asyncMiddleware(this.validateInvoicesIDs.bind(this)),
asyncMiddleware(this.validateInvoicesPaymentsAmount.bind(this)),
asyncMiddleware(this.newPaymentReceive.bind(this)), asyncMiddleware(this.newPaymentReceive.bind(this)),
this.handleServiceErrors,
); );
router.get( router.get(
'/:id', '/:id',
this.paymentReceiveValidation, this.paymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)), asyncMiddleware(this.getPaymentReceive.bind(this)),
asyncMiddleware(this.getPaymentReceive.bind(this)) this.handleServiceErrors,
); );
router.get( router.get(
'/', '/',
this.validatePaymentReceiveList, this.validatePaymentReceiveList,
this.validationResult, this.validationResult,
asyncMiddleware(this.getPaymentReceiveList.bind(this)), asyncMiddleware(this.getPaymentReceiveList.bind(this)),
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse,
); );
router.delete( router.delete(
'/:id', '/:id',
this.paymentReceiveValidation, this.paymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.deletePaymentReceive.bind(this)), asyncMiddleware(this.deletePaymentReceive.bind(this)),
this.handleServiceErrors,
); );
return router; return router;
} }
@@ -126,198 +114,6 @@ export default class PaymentReceivesController extends BaseController {
return [...this.paymentReceiveSchema]; return [...this.paymentReceiveSchema];
} }
/**
* 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();
}
/**
* Records payment receive to the given customer with associated invoices.
*/
async newPaymentReceive(req: Request, res: Response) {
const { tenantId } = req;
const paymentReceive: IPaymentReceiveOTD = matchedData(req, {
locations: ['body'],
includeOptionals: true,
});
const storedPaymentReceive = await this.paymentReceiveService
.createPaymentReceive(
tenantId,
paymentReceive,
);
return res.status(200).send({ id: storedPaymentReceive.id });
}
/** /**
* Edit payment receive validation. * Edit payment receive validation.
*/ */
@@ -328,34 +124,48 @@ export default class PaymentReceivesController extends BaseController {
]; ];
} }
/**
* Records payment receive to the given customer with associated invoices.
*/
async newPaymentReceive(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
try {
const storedPaymentReceive = await this.paymentReceiveService
.createPaymentReceive(
tenantId,
paymentReceive,
);
return res.status(200).send({
id: storedPaymentReceive.id,
message: 'The payment receive has been created successfully.',
});
} catch (error) {
next(error);
}
}
/** /**
* Edit the given payment receive. * Edit the given payment receive.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async editPaymentReceive(req: Request, res: Response) { async editPaymentReceive(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
const { PaymentReceive } = req.models;
const paymentReceive: IPaymentReceiveOTD = matchedData(req, { const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
locations: ['body'],
});
// Retrieve the payment receive before updating. try {
const oldPaymentReceive: IPaymentReceive = await PaymentReceive.query() await this.paymentReceiveService.editPaymentReceive(
.where('id', paymentReceiveId) tenantId, paymentReceiveId, paymentReceive,
.withGraphFetched('entries') );
.first(); return res.status(200).send({ id: paymentReceiveId });
} catch (error) {
await this.paymentReceiveService.editPaymentReceive( next(error);
tenantId, }
paymentReceiveId,
paymentReceive,
oldPaymentReceive,
);
return res.status(200).send({ id: paymentReceiveId });
} }
/** /**
@@ -363,22 +173,22 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async deletePaymentReceive(req: Request, res: Response) { async deletePaymentReceive(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
const { PaymentReceive } = req.models;
const storedPaymentReceive = await PaymentReceive.query() try {
.where('id', paymentReceiveId) await this.paymentReceiveService.deletePaymentReceive(
.withGraphFetched('entries') tenantId,
.first(); paymentReceiveId,
);
await this.paymentReceiveService.deletePaymentReceive( return res.status(200).send({
tenantId, id: paymentReceiveId,
paymentReceiveId, message: 'The payment receive has been edited successfully',
storedPaymentReceive });
); } catch (error) {
return res.status(200).send({ id: paymentReceiveId }); next(error);
}
} }
/** /**
@@ -387,12 +197,18 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
async getPaymentReceive(req: Request, res: Response) { async getPaymentReceive(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
const paymentReceive = await PaymentReceiveService.getPaymentReceive(
paymentReceiveId try {
); const paymentReceive = await this.paymentReceiveService.getPaymentReceive(
return res.status(200).send({ paymentReceive }); tenantId, paymentReceiveId
);
return res.status(200).send({ paymentReceive });
} catch (error) {
next(error);
}
} }
/** /**
@@ -401,7 +217,88 @@ export default class PaymentReceivesController extends BaseController {
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async getPaymentReceiveList(req: Request, res: Response) { async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
paymentReceives,
pagination,
filterMeta,
} = await this.paymentReceiveService.listPaymentReceives(tenantId, filter);
return res.status(200).send({
payment_receives: paymentReceives,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error);
}
}
/**
* Handles service errors.
* @param error
* @param req
* @param res
* @param next
*/
handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
});
}
if (error.errorType === 'PAYMENT_RECEIVE_NO_EXISTS') {
return res.boom.badRequest(null, {
errors: [{ type: 'PAYMENT_RECEIVE_NO_EXISTS', code: 300 }],
});
}
if (error.errorType === 'PAYMENT_RECEIVE_NOT_EXISTS') {
return res.boom.badRequest(null, {
errors: [{ type: 'PAYMENT_RECEIVE_NOT_EXISTS', code: 300 }],
});
}
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') {
return res.boom.badRequest(null, {
errors: [{ type: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', code: 300 }],
});
}
if (error.errorType === 'INVALID_PAYMENT_AMOUNT_INVALID') {
return res.boom.badRequest(null, {
errors: [{ type: 'INVALID_PAYMENT_AMOUNT', code: 300 }],
});
}
if (error.errorType === 'INVOICES_IDS_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'INVOICES_IDS_NOT_FOUND', code: 300 }],
});
}
if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 300 }],
});
}
if (error.errorType === 'contact_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 300 }],
});
}
console.log(error.errorType);
}
next(error);
} }
} }

View File

@@ -1,6 +1,5 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator'; import { check, param, query } from 'express-validator';
import { difference } from 'lodash';
import { raw } from 'objection'; import { raw } from 'objection';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
@@ -8,6 +7,7 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleInvoiceService from 'services/Sales/SalesInvoices'; import SaleInvoiceService from 'services/Sales/SalesInvoices';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions';
import { ISaleInvoiceOTD, ISalesInvoicesFilter } from 'interfaces'; import { ISaleInvoiceOTD, ISalesInvoicesFilter } from 'interfaces';
@Service() @Service()
@@ -31,11 +31,8 @@ export default class SaleInvoicesController extends BaseController{
'/', '/',
this.saleInvoiceValidationSchema, this.saleInvoiceValidationSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), asyncMiddleware(this.newSaleInvoice.bind(this)),
// asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), this.handleServiceErrors,
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)),
asyncMiddleware(this.newSaleInvoice.bind(this))
); );
router.post( router.post(
'/:id', '/:id',
@@ -44,39 +41,36 @@ export default class SaleInvoicesController extends BaseController{
...this.specificSaleInvoiceValidation, ...this.specificSaleInvoiceValidation,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.editSaleInvoice.bind(this)),
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), this.handleServiceErrors,
// asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance.bind(this)),
asyncMiddleware(this.validateEntriesIdsExistance.bind(this)),
asyncMiddleware(this.validateNonSellableEntriesItems.bind(this)),
asyncMiddleware(this.editSaleInvoice.bind(this))
); );
router.delete( router.delete(
'/:id', '/:id',
this.specificSaleInvoiceValidation, this.specificSaleInvoiceValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.deleteSaleInvoice.bind(this)),
asyncMiddleware(this.deleteSaleInvoice.bind(this)) this.handleServiceErrors,
); );
router.get( router.get(
'/due_invoices', '/due_invoices',
this.dueSalesInvoicesListValidationSchema, this.dueSalesInvoicesListValidationSchema,
asyncMiddleware(this.getDueSalesInvoice.bind(this)), asyncMiddleware(this.getDueSalesInvoice.bind(this)),
this.handleServiceErrors,
); );
router.get( router.get(
'/:id', '/:id',
this.specificSaleInvoiceValidation, this.specificSaleInvoiceValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.getSaleInvoice.bind(this)),
asyncMiddleware(this.getSaleInvoice.bind(this)) this.handleServiceErrors,
); );
router.get( router.get(
'/', '/',
this.saleInvoiceListValidationSchema, this.saleInvoiceListValidationSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.getSalesInvoices.bind(this)) asyncMiddleware(this.getSalesInvoices.bind(this)),
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse,
) )
return router; return router;
} }
@@ -133,177 +127,8 @@ export default class SaleInvoicesController extends BaseController{
*/ */
get dueSalesInvoicesListValidationSchema() { get dueSalesInvoicesListValidationSchema() {
return [ return [
query('customer_id').optional().isNumeric().toInt(), query('customer_id').optional().isNumeric().toInt(),
] ];
}
/**
* Validate whether sale invoice customer exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateInvoiceCustomerExistance(req: Request, res: Response, next: Function) {
const saleInvoice = { ...req.body };
const { Customer } = req.models;
const isCustomerIDExists = await Customer.query().findById(saleInvoice.customer_id);
if (!isCustomerIDExists) {
return res.status(400).send({
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
});
}
next();
}
/**
* Validate whether sale invoice items ids esits on the storage.
* @param {Request} req -
* @param {Response} res -
* @param {Function} next -
*/
async validateInvoiceItemsIdsExistance(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleInvoice = { ...req.body };
const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id);
const isItemsIdsExists = await this.itemsService.isItemsIdsExists(
tenantId, entriesItemsIds,
);
if (isItemsIdsExists.length > 0) {
return res.status(400).send({
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }],
});
}
next();
}
/**
*
* Validate whether sale invoice number unqiue on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateInvoiceNumberUnique(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleInvoice = { ...req.body };
const isInvoiceNoExists = await this.saleInvoiceService.isSaleInvoiceNumberExists(
tenantId,
saleInvoice.invoice_no,
req.params.id
);
if (isInvoiceNoExists) {
return res
.status(400)
.send({
errors: [{ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }],
});
}
next();
}
/**
* Validate whether sale invoice exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateInvoiceExistance(req: Request, res: Response, next: Function) {
const { id: saleInvoiceId } = req.params;
const { tenantId } = req;
const isSaleInvoiceExists = await this.saleInvoiceService.isSaleInvoiceExists(
tenantId, saleInvoiceId,
);
if (!isSaleInvoiceExists) {
return res
.status(404)
.send({ errors: [{ type: 'SALE.INVOICE.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 { tenantId } = req;
const saleInvoice = { ...req.body };
const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id);
const isItemsIdsExists = await this.itemsService.isItemsIdsExists(
tenantId, entriesItemsIds,
);
if (isItemsIdsExists.length > 0) {
return res.status(400).send({
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }],
});
}
next();
}
/**
* Validate whether the sale estimate entries IDs exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateEntriesIdsExistance(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 storedEntries = await ItemEntry.query()
.whereIn('reference_id', [saleInvoiceId])
.whereIn('reference_type', ['SaleInvoice']);
const storedEntriesIds = storedEntries.map((entry) => entry.id);
const notFoundEntriesIds = difference(
entriesIds,
storedEntriesIds,
);
if (notFoundEntriesIds.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'SALE.INVOICE.ENTRIES.IDS.NOT.FOUND', code: 500 }],
});
}
next();
}
/**
* Validate the entries items that not sellable.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateNonSellableEntriesItems(req: Request, res: Response, next: Function) {
const { Item } = req.models;
const saleInvoice = { ...req.body };
const itemsIds = saleInvoice.entries.map(e => e.item_id);
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) {
return res.status(400).send({
errors: [{ type: 'NOT.SELLABLE.ITEMS', code: 600 }],
});
}
next();
} }
/** /**
@@ -372,14 +197,18 @@ export default class SaleInvoicesController extends BaseController{
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async getSaleInvoice(req: Request, res: Response) { async getSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
const { tenantId } = req; const { tenantId } = req;
const saleInvoice = await this.saleInvoiceService.getSaleInvoiceWithEntries( try {
tenantId, saleInvoiceId, const saleInvoice = await this.saleInvoiceService.getSaleInvoiceWithEntries(
); tenantId, saleInvoiceId,
return res.status(200).send({ sale_invoice: saleInvoice }); );
return res.status(200).send({ sale_invoice: saleInvoice });
} catch (error) {
next(error);
}
} }
/** /**
@@ -422,12 +251,20 @@ export default class SaleInvoicesController extends BaseController{
* @param {Function} next * @param {Function} next
*/ */
public async getSalesInvoices(req: Request, res: Response, next: NextFunction) { public async getSalesInvoices(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req.params; const { tenantId } = req;
const salesInvoicesFilter: ISalesInvoicesFilter = req.query; const filter: ISalesInvoicesFilter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'name',
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { salesInvoices, filterMeta, pagination } = await this.saleInvoiceService.salesInvoicesList( const { salesInvoices, filterMeta, pagination } = await this.saleInvoiceService.salesInvoicesList(
tenantId, salesInvoicesFilter, tenantId, filter,
); );
return res.status(200).send({ return res.status(200).send({
sales_invoices: salesInvoices, sales_invoices: salesInvoices,
@@ -438,4 +275,63 @@ export default class SaleInvoicesController extends BaseController{
next(error); next(error);
} }
} }
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'INVOICE_NUMBER_NOT_UNIQUE') {
return res.boom.badRequest(null, {
errors: [{ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }],
});
}
if (error.errorType === 'SALE_INVOICE_NOT_FOUND') {
return res.status(404).send({
errors: [{ type: 'SALE.INVOICE.NOT.FOUND', code: 200 }]
});
}
if (error.errorType === 'ENTRIES_ITEMS_IDS_NOT_EXISTS') {
return res.boom.badRequest(null, {
errors: [{ type: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', code: 200 }],
});
}
if (error.errorType === 'NOT_SELLABLE_ITEMS') {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_SELLABLE_ITEMS', code: 200 }],
});
}
if (error.errorType === 'SALE_INVOICE_NO_NOT_UNIQUE') {
return res.boom.badRequest(null, {
errors: [{ type: 'SALE_INVOICE_NO_NOT_UNIQUE', code: 200 }],
});
}
if (error.errorType === 'ITEMS_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'ITEMS_NOT_FOUND', code: 200 }],
});
}
if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 200 }],
});
}
if (error.errorType === 'NOT_SELL_ABLE_ITEMS') {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_SELL_ABLE_ITEMS', code: 200 }],
});
}
if (error.errorType === 'contact_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 200 }],
});
}
}
console.log(error.errorType);
next(error);
}
} }

View File

@@ -13,10 +13,10 @@ export default class SalesController {
router() { router() {
const router = Router(); const router = Router();
// router.use('/invoices', Container.get(SalesInvoices).router()); router.use('/invoices', Container.get(SalesInvoices).router());
router.use('/estimates', Container.get(SalesEstimates).router()); router.use('/estimates', Container.get(SalesEstimates).router());
router.use('/receipts', Container.get(SalesReceipts).router()); router.use('/receipts', Container.get(SalesReceipts).router());
// router.use('/payment_receives', Container.get(PaymentReceives).router()); router.use('/payment_receives', Container.get(PaymentReceives).router());
return router; return router;
} }

View File

@@ -1,4 +1,43 @@
import { IDynamicListFilterDTO } from "./DynamicFilter";
export interface IPaymentReceive { }; export interface IPaymentReceive {
export interface IPaymentReceiveOTD { }; id?: number,
customerId: number,
paymentDate: Date,
amount: number,
referenceNo: string,
depositAccountId: number,
paymentReceiveNo: string,
description: string,
entries: IPaymentReceiveEntry[],
userId: number,
};
export interface IPaymentReceiveDTO {
customerId: number,
paymentDate: Date,
amount: number,
referenceNo: string,
depositAccountId: number,
paymentReceiveNo: string,
description: string,
entries: IPaymentReceiveEntryDTO[],
};
export interface IPaymentReceiveEntry {
id?: number,
paymentReceiveId: number,
invoiceId: number,
paymentAmount: number,
};
export interface IPaymentReceiveEntryDTO {
id?: number,
paymentReceiveId: number,
invoiceId: number,
paymentAmount: number,
};
export interface IPaymentReceivesFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string,
}

View File

@@ -13,6 +13,8 @@ export interface ISaleInvoiceOTD {
invoiceDate: Date, invoiceDate: Date,
dueDate: Date, dueDate: Date,
referenceNo: string, referenceNo: string,
invoiceNo: string,
customerId: number,
invoiceMessage: string, invoiceMessage: string,
termsConditions: string, termsConditions: string,
entries: IItemEntryDTO[], entries: IItemEntryDTO[],

View File

@@ -5,7 +5,8 @@ import 'subscribers/organization';
import 'subscribers/manualJournals'; import 'subscribers/manualJournals';
import 'subscribers/expenses'; import 'subscribers/expenses';
import 'subscribers/bills'; import 'subscribers/bills';
// import 'subscribers/saleInvoices'; import 'subscribers/saleInvoices';
import 'subscribers/customers'; import 'subscribers/customers';
import 'subscribers/vendors'; import 'subscribers/vendors';
import 'subscribers/paymentMades'; import 'subscribers/paymentMades';
import 'subscribers/paymentReceives';

View File

@@ -69,4 +69,17 @@ export default class PaymentReceive extends TenantModel {
} }
}; };
} }
/**
* Model defined fields.
*/
static get fields() {
return {
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
}
} }

View File

@@ -123,6 +123,4 @@ export default class SaleInvoice extends TenantModel {
.where('id', invoiceId) .where('id', invoiceId)
[changeMethod]('payment_amount', Math.abs(amount)); [changeMethod]('payment_amount', Math.abs(amount));
} }
} }

View File

@@ -68,4 +68,36 @@ export default class CustomerRepository extends TenantRepository {
.whereIn('id', customersIds) .whereIn('id', customersIds)
.withGraphFetched('salesInvoices'); .withGraphFetched('salesInvoices');
} }
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));
}
async changeDiffBalance(
vendorId: number,
amount: number,
oldAmount: number,
oldVendorId?: number,
) {
const diffAmount = amount - oldAmount;
const asyncOpers = [];
const _oldVendorId = oldVendorId || vendorId;
if (vendorId != _oldVendorId) {
const oldCustomerOper = this.changeBalance(_oldVendorId, (oldAmount * -1));
const customerOper = this.changeBalance(vendorId, amount);
asyncOpers.push(customerOper);
asyncOpers.push(oldCustomerOper);
} else {
const balanceChangeOper = this.changeBalance(vendorId, diffAmount);
asyncOpers.push(balanceChangeOper);
}
await Promise.all(asyncOpers);
}
} }

View File

@@ -68,13 +68,27 @@ export default class VendorRepository extends TenantRepository {
[changeMethod]('balance', Math.abs(amount)); [changeMethod]('balance', Math.abs(amount));
} }
changeDiffBalance( async changeDiffBalance(
vendorId: number, vendorId: number,
amount: number, amount: number,
oldAmount: number, oldAmount: number,
oldVendorId?: number, oldVendorId?: number,
) { ) {
const diffAmount = amount - oldAmount;
const asyncOpers = [];
const _oldVendorId = oldVendorId || vendorId;
if (vendorId != _oldVendorId) {
const oldCustomerOper = this.changeBalance(_oldVendorId, (oldAmount * -1));
const customerOper = this.changeBalance(vendorId, amount);
asyncOpers.push(customerOper);
asyncOpers.push(oldCustomerOper);
} else {
const balanceChangeOper = this.changeBalance(vendorId, diffAmount);
asyncOpers.push(balanceChangeOper);
}
await Promise.all(asyncOpers);
} }
} }

View File

@@ -46,15 +46,15 @@ export default class ItemsEntriesService {
* @param {number} billId - * @param {number} billId -
* @param {IItemEntry[]} billEntries - * @param {IItemEntry[]} billEntries -
*/ */
public async validateEntriesIdsExistance(tenantId: number, billId: number, modelName: string, billEntries: IItemEntryDTO[]) { public async validateEntriesIdsExistance(tenantId: number, referenceId: number, referenceType: string, billEntries: IItemEntryDTO[]) {
const { ItemEntry } = this.tenancy.models(tenantId); const { ItemEntry } = this.tenancy.models(tenantId);
const entriesIds = billEntries const entriesIds = billEntries
.filter((e: IItemEntry) => e.id) .filter((e: IItemEntry) => e.id)
.map((e: IItemEntry) => e.id); .map((e: IItemEntry) => e.id);
const storedEntries = await ItemEntry.query() const storedEntries = await ItemEntry.query()
.whereIn('reference_id', [billId]) .whereIn('reference_id', [referenceId])
.whereIn('reference_type', [modelName]); .whereIn('reference_type', [referenceType]);
const storedEntriesIds = storedEntries.map((entry) => entry.id); const storedEntriesIds = storedEntries.map((entry) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);

View File

@@ -316,7 +316,9 @@ export default class BillPaymentsService {
...omit(billPaymentObj, ['entries']), ...omit(billPaymentObj, ['entries']),
entries: billPaymentDTO.entries, entries: billPaymentDTO.entries,
}); });
await this.eventDispatcher.dispatch(events.billPayments.onEdited); await this.eventDispatcher.dispatch(events.billPayments.onEdited, {
tenantId, billPaymentId, billPayment, oldPaymentMade,
});
this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldPaymentMade }); this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldPaymentMade });
return billPayment; return billPayment;

View File

@@ -184,7 +184,10 @@ export default class BillsService extends SalesInvoicesCost {
await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
await this.validateBillNumberExists(tenantId, billDTO.billNumber); await this.validateBillNumberExists(tenantId, billDTO.billNumber);
// Validate items IDs existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries); await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries);
// Validate non-purchasable items.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
const bill = await Bill.query() const bill = await Bill.query()
@@ -232,7 +235,7 @@ export default class BillsService extends SalesInvoicesCost {
this.logger.info('[bill] trying to edit bill.', { tenantId, billId }); this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
const oldBill = await this.getBillOrThrowError(tenantId, billId); const oldBill = await this.getBillOrThrowError(tenantId, billId);
const billObj = this.billDTOToModel(tenantId, billDTO, oldBill); const billObj = await this.billDTOToModel(tenantId, billDTO, oldBill);
await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId); await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId);
@@ -242,7 +245,7 @@ export default class BillsService extends SalesInvoicesCost {
await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
// Update the bill transaction. // Update the bill transaction.
const bill = await Bill.query().upsertGraph({ const bill = await Bill.query().upsertGraphAndFetch({
id: billId, id: billId,
...omit(billObj, ['entries', 'invLotNumber']), ...omit(billObj, ['entries', 'invLotNumber']),

View File

@@ -1,4 +1,4 @@
import { omit, sumBy, chain } from 'lodash'; import { omit, sumBy, chain, difference } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { import {
@@ -6,18 +6,35 @@ import {
EventDispatcherInterface, EventDispatcherInterface,
} from 'decorators/eventDispatcher'; } from 'decorators/eventDispatcher';
import events from 'subscribers/events'; import events from 'subscribers/events';
import { IPaymentReceiveOTD } from 'interfaces'; import {
IAccount,
IFilterMeta,
IPaginationMeta,
IPaymentReceive,
IPaymentReceiveDTO,
IPaymentReceiveEntryDTO,
IPaymentReceivesFilter,
ISaleInvoice
} from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry'; import JournalEntry from 'services/Accounting/JournalEntry';
import JournalPosterService from 'services/Sales/JournalPosterService'; import JournalPosterService from 'services/Sales/JournalPosterService';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository';
import CustomerRepository from 'repositories/CustomerRepository';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import { ServiceError } from 'exceptions';
import CustomersService from 'services/Contacts/CustomersService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE',
INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT',
INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND'
};
/** /**
* Payment receive service. * Payment receive service.
* @service * @service
@@ -27,6 +44,12 @@ export default class PaymentReceiveService {
@Inject() @Inject()
accountsService: AccountsService; accountsService: AccountsService;
@Inject()
customersService: CustomersService;
@Inject()
itemsEntries: ItemsEntriesService;
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@@ -42,112 +65,88 @@ export default class PaymentReceiveService {
@EventDispatcher() @EventDispatcher()
eventDispatcher: EventDispatcherInterface; eventDispatcher: EventDispatcherInterface;
/** /**
* Validates the payment receive number existance. * Validates the payment receive number existance.
* @param {Request} req * @param {number} tenantId -
* @param {Response} res * @param {string} paymentReceiveNo -
* @param {Function} next
*/ */
async validatePaymentReceiveNoExistance(req: Request, res: Response, next: Function) { async validatePaymentReceiveNoExistance(
const tenantId = req.tenantId; tenantId: number,
const isPaymentNoExists = await this.paymentReceiveService.isPaymentReceiveNoExists( paymentReceiveNo: string,
tenantId, notPaymentReceiveId?: number
req.body.payment_receive_no, ): Promise<void> {
req.params.id, const { PaymentReceive } = this.tenancy.models(tenantId);
); const paymentReceive = await PaymentReceive.query().findOne('payment_receive_no', paymentReceiveNo)
if (isPaymentNoExists) { .onBuild((builder) => {
return res.status(400).send({ if (notPaymentReceiveId) {
errors: [{ type: 'PAYMENT.RECEIVE.NUMBER.EXISTS', code: 400 }], builder.whereNot('id', notPaymentReceiveId);
}
}); });
if (paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_EXISTS);
} }
next();
} }
/** /**
* Validates the payment receive existance. * Validates the payment receive existance.
* @param {Request} req * @param {number} tenantId -
* @param {Response} res * @param {number} paymentReceiveId -
* @param {Function} next
*/ */
async validatePaymentReceiveExistance(req: Request, res: Response, next: Function) { async getPaymentReceiveOrThrowError(
const tenantId = req.tenantId; tenantId: number,
const isPaymentNoExists = await this.paymentReceiveService paymentReceiveId: number
.isPaymentReceiveExists( ): Promise<IPaymentReceive> {
tenantId, const { PaymentReceive } = this.tenancy.models(tenantId);
req.params.id const paymentReceive = await PaymentReceive.query()
); .withGraphFetched('entries')
if (!isPaymentNoExists) { .findById(paymentReceiveId);
return res.status(400).send({
errors: [{ type: 'PAYMENT.RECEIVE.NOT.EXISTS', code: 600 }], if (!paymentReceive) {
}); throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
} }
next(); return paymentReceive;
} }
/** /**
* Validate the deposit account id existance. * Validate the deposit account id existance.
* @param {Request} req * @param {number} tenantId -
* @param {Response} res * @param {number} depositAccountId -
* @param {Function} next
*/ */
async validateDepositAccount(req: Request, res: Response, next: Function) { async getDepositAccountOrThrowError(tenantId: number, depositAccountId: number): Promise<IAccount> {
const tenantId = req.tenantId; const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
const isDepositAccExists = await this.accountsService.isAccountExists(
tenantId, const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset');
req.body.deposit_account_id const depositAccount = await accountRepository.findById(depositAccountId);
);
if (!isDepositAccExists) { const currentAssetTypesIds = currentAssetTypes.map(type => type.id);
return res.status(400).send({
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }], if (!depositAccount) {
}); throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
} }
next(); if (currentAssetTypesIds.indexOf(depositAccount.accountTypeId) === -1) {
} throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE);
/**
* 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(); return depositAccount;
} }
/** /**
* Validates the invoices IDs existance. * Validates the invoices IDs existance.
* @param {Request} req - * @param {number} tenantId -
* @param {Response} res - * @param {} paymentReceiveEntries -
* @param {Function} next -
*/ */
async validateInvoicesIDs(req: Request, res: Response, next: Function) { async validateInvoicesIDsExistance(tenantId: number, paymentReceiveEntries: any): Promise<void> {
const paymentReceive = { ...req.body }; const { SaleInvoice } = this.tenancy.models(tenantId);
const { tenantId } = req;
const invoicesIds = paymentReceive.entries const invoicesIds = paymentReceiveEntries.map((e) => e.invoiceId);
.map((e) => e.invoice_id); const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds);
const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id);
const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds);
const notFoundInvoicesIDs = await this.saleInvoiceService.isInvoicesExist(
tenantId,
invoicesIds,
paymentReceive.customer_id,
);
if (notFoundInvoicesIDs.length > 0) { if (notFoundInvoicesIDs.length > 0) {
return res.status(400).send({ throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND);
errors: [{ type: 'INVOICES.IDS.NOT.FOUND', code: 500 }],
});
} }
next();
} }
/** /**
@@ -156,69 +155,30 @@ export default class PaymentReceiveService {
* @param {Response} res - * @param {Response} res -
* @param {Function} next - * @param {Function} next -
*/ */
async validateInvoicesPaymentsAmount(req: Request, res: Response, next: Function) { async validateInvoicesPaymentsAmount(tenantId: number, paymentReceiveEntries: IPaymentReceiveEntryDTO[]) {
const { SaleInvoice } = req.models; const { SaleInvoice } = this.tenancy.models(tenantId);
const invoicesIds = req.body.entries.map((e) => e.invoice_id); const invoicesIds = paymentReceiveEntries.map((e: IPaymentReceiveEntryDTO) => e.invoiceId);
const storedInvoices = await SaleInvoice.query() const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds);
.whereIn('id', invoicesIds);
const storedInvoicesMap = new Map( const storedInvoicesMap = new Map(
storedInvoices.map((invoice) => [invoice.id, invoice]) storedInvoices.map((invoice: ISaleInvoice) => [invoice.id, invoice])
); );
const hasWrongPaymentAmount: any[] = []; const hasWrongPaymentAmount: any[] = [];
req.body.entries.forEach((entry, index: number) => { paymentReceiveEntries.forEach((entry: IPaymentReceiveEntryDTO, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoice_id); const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
const { dueAmount } = entryInvoice; const { dueAmount } = entryInvoice;
if (dueAmount < entry.payment_amount) { if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
} }
}); });
if (hasWrongPaymentAmount.length > 0) { if (hasWrongPaymentAmount.length > 0) {
return res.status(400).send({ throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT);
errors: [
{
type: 'INVOICE.PAYMENT.AMOUNT',
code: 200,
indexes: hasWrongPaymentAmount,
},
],
});
} }
next();
} }
/**
* Validate the payment receive entries IDs existance.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
const paymentReceive = { id: req.params.id, ...req.body };
const entriesIds = paymentReceive.entries
.filter(entry => entry.id)
.map(entry => entry.id);
const { PaymentReceiveEntry } = req.models;
const storedEntries = await PaymentReceiveEntry.query()
.where('payment_receive_id', paymentReceive.id);
const storedEntriesIds = storedEntries.map((entry) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
return res.status(400).send({
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
});
}
next();
}
/** /**
* Creates a new payment receive and store it to the storage * Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions. * with associated invoices payment and journal transactions.
@@ -226,61 +186,42 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive * @param {IPaymentReceive} paymentReceive
*/ */
public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) { public async createPaymentReceive(tenantId: number, paymentReceiveDTO: IPaymentReceiveDTO) {
const { const { PaymentReceive } = this.tenancy.models(tenantId);
PaymentReceive, const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
PaymentReceiveEntry,
SaleInvoice,
Customer,
} = this.tenancy.models(tenantId);
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); // Validate payment receive number uniquiness.
await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, paymentReceiveDTO.customerId);
// Validate the deposit account existance and type.
await this.getDepositAccountOrThrowError(tenantId, paymentReceiveDTO.depositAccountId);
// Validate payment receive invoices IDs existance.
await this.validateInvoicesIDsExistance(tenantId, paymentReceiveDTO.entries);
// Validate invoice payment amount.
await this.validateInvoicesPaymentsAmount(tenantId, paymentReceiveDTO.entries);
this.logger.info('[payment_receive] inserting to the storage.'); this.logger.info('[payment_receive] inserting to the storage.');
const storedPaymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query()
.insertGraph({ .insertGraphAndFetch({
amount: paymentAmount, amount: paymentAmount,
...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), ...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']),
entries: paymentReceive.entries.map((entry) => ({ ...entry })),
entries: paymentReceiveDTO.entries.map((entry) => ({
...omit(entry, ['id']),
})),
}); });
const storeOpers: Array<any> = [];
this.logger.info('[payment_receive] inserting associated entries to the storage.'); await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated, {
paymentReceive.entries.forEach((entry: any) => { tenantId, paymentReceive, paymentReceiveId: paymentReceive.id,
const oper = PaymentReceiveEntry.query()
.insert({
payment_receive_id: storedPaymentReceive.id,
...entry,
});
this.logger.info('[payment_receive] increment the sale invoice payment amount.');
// Increment the invoice payment amount.
const invoice = SaleInvoice.query()
.where('id', entry.invoice_id)
.increment('payment_amount', entry.payment_amount);
storeOpers.push(oper);
storeOpers.push(invoice);
}); });
this.logger.info('[payment_receive] updated successfully.', { tenantId, paymentReceive });
this.logger.info('[payment_receive] decrementing customer balance.'); return paymentReceive;
const customerIncrementOper = Customer.decrementBalance(
paymentReceive.customer_id,
paymentAmount,
);
// Records the sale invoice journal transactions.
const recordJournalTransactions = this.recordPaymentReceiveJournalEntries(tenantId,{
id: storedPaymentReceive.id,
...paymentReceive,
});
await Promise.all([
...storeOpers,
customerIncrementOper,
recordJournalTransactions,
]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated);
return storedPaymentReceive;
} }
/** /**
@@ -297,84 +238,52 @@ export default class PaymentReceiveService {
* @param {number} tenantId - * @param {number} tenantId -
* @param {Integer} paymentReceiveId - * @param {Integer} paymentReceiveId -
* @param {IPaymentReceive} paymentReceive - * @param {IPaymentReceive} paymentReceive -
* @param {IPaymentReceive} oldPaymentReceive -
*/ */
public async editPaymentReceive( public async editPaymentReceive(
tenantId: number, tenantId: number,
paymentReceiveId: number, paymentReceiveId: number,
paymentReceive: any, paymentReceiveDTO: any,
oldPaymentReceive: any
) { ) {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
this.logger.info('[payment_receive] trying to edit payment receive.', { tenantId, paymentReceiveId, paymentReceiveDTO });
// Validate the payment receive existance.
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId);
// Validate payment receive number uniquiness.
await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo, paymentReceiveId);
// Validate the deposit account existance and type.
this.getDepositAccountOrThrowError(tenantId, paymentReceiveDTO.depositAccountId);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, paymentReceiveDTO.customerId);
// Validate the entries ids existance on payment receive type.
await this.itemsEntries.validateEntriesIdsExistance(
tenantId, paymentReceiveId, 'PaymentReceive', paymentReceiveDTO.entries
);
// Validate payment receive invoices IDs existance.
await this.validateInvoicesIDsExistance(tenantId, paymentReceiveDTO.entries);
// Validate invoice payment amount.
await this.validateInvoicesPaymentsAmount(tenantId, paymentReceiveDTO.entries);
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
// Update the payment receive transaction. // Update the payment receive transaction.
const updatePaymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query()
.where('id', paymentReceiveId) .upsertGraphAndFetch({
.update({ id: paymentReceiveId,
amount: paymentAmount, amount: paymentAmount,
...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), ...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']),
entries: paymentReceiveDTO.entries,
}); });
const opers = [];
const entriesIds = paymentReceive.entries.filter((i: any) => i.id);
const entriesShouldInsert = paymentReceive.entries.filter((i: any) => !i.id);
// Detarmines which entries ids should be deleted. await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited, {
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive
oldPaymentReceive.entries, });
entriesIds this.logger.info('[payment_receive] upserted successfully.', { tenantId, paymentReceiveId });
);
if (entriesIdsShouldDelete.length > 0) {
// Deletes the given payment receive entries.
const deleteOper = PaymentReceiveEntryRepository.deleteBulk(
entriesIdsShouldDelete
);
opers.push(deleteOper);
}
// Entries that should be updated to the storage.
if (entriesIds.length > 0) {
const updateOper = PaymentReceiveEntryRepository.updateBulk(entriesIds);
opers.push(updateOper);
}
// Entries should insert to the storage.
if (entriesShouldInsert.length > 0) {
const insertOper = PaymentReceiveEntryRepository.insertBulk(
entriesShouldInsert,
paymentReceiveId
);
opers.push(insertOper);
}
// Re-write the journal transactions of the given payment receive.
const recordJournalTransactions = this.recordPaymentReceiveJournalEntries(
tenantId,
{
id: oldPaymentReceive.id,
...paymentReceive,
},
paymentReceiveId,
);
// Increment/decrement the customer balance after calc the diff
// between old and new value.
const changeCustomerBalance = CustomerRepository.changeDiffBalance(
paymentReceive.customer_id,
oldPaymentReceive.customerId,
paymentAmount * -1,
oldPaymentReceive.amount * -1,
);
// Change the difference between the old and new invoice payment amount.
const diffInvoicePaymentAmount = this.saveChangeInvoicePaymentAmount(
tenantId,
oldPaymentReceive.entries,
paymentReceive.entries
);
// Await the async operations.
await Promise.all([
...opers,
recordJournalTransactions,
changeCustomerBalance,
diffInvoicePaymentAmount,
]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited);
} }
/** /**
@@ -392,43 +301,20 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive - Payment receive object. * @param {IPaymentReceive} paymentReceive - Payment receive object.
*/ */
async deletePaymentReceive(tenantId: number, paymentReceiveId: number, paymentReceive: any) { async deletePaymentReceive(tenantId: number, paymentReceiveId: number, paymentReceive: any) {
const { PaymentReceive, PaymentReceiveEntry, Customer } = this.tenancy.models(tenantId); const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models(tenantId);
// Deletes the payment receive transaction. const oldPaymentReceive = this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId);
await PaymentReceive.query()
.where('id', paymentReceiveId)
.delete();
// Deletes the payment receive associated entries. // Deletes the payment receive associated entries.
await PaymentReceiveEntry.query() await PaymentReceiveEntry.query().where('payment_receive_id', paymentReceiveId).delete();
.where('payment_receive_id', paymentReceiveId)
.delete();
// Delete all associated journal transactions to payment receive transaction. // Deletes the payment receive transaction.
const deleteTransactionsOper = this.journalService.deleteJournalTransactions( await PaymentReceive.query().where('id', paymentReceiveId).delete();
tenantId,
paymentReceiveId, await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted, {
'PaymentReceive' tenantId, paymentReceiveId, oldPaymentReceive,
); });
// Revert the customer balance. this.logger.info('[payment_receive] deleted successfully.', { tenantId, paymentReceiveId });
const revertCustomerBalance = Customer.incrementBalance(
paymentReceive.customerId,
paymentReceive.amount
);
// Revert the invoices payments amount.
const revertInvoicesPaymentAmount = this.revertInvoicePaymentAmount(
tenantId,
paymentReceive.entries.map((entry: any) => ({
invoiceId: entry.invoiceId,
revertAmount: entry.paymentAmount,
}))
);
await Promise.all([
deleteTransactionsOper,
revertCustomerBalance,
revertInvoicesPaymentAmount,
]);
await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted);
} }
/** /**
@@ -439,9 +325,12 @@ export default class PaymentReceiveService {
public async getPaymentReceive(tenantId: number, paymentReceiveId: number) { public async getPaymentReceive(tenantId: number, paymentReceiveId: number) {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query()
.where('id', paymentReceiveId) .findById(paymentReceiveId)
.withGraphFetched('entries.invoice') .withGraphFetched('entries.invoice');
.first();
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return paymentReceive; return paymentReceive;
} }
@@ -450,7 +339,10 @@ export default class PaymentReceiveService {
* @param {number} tenantId * @param {number} tenantId
* @param {IPaymentReceivesFilter} paymentReceivesFilter * @param {IPaymentReceivesFilter} paymentReceivesFilter
*/ */
public async listPaymentReceives(tenantId: number, paymentReceivesFilter: IPaymentReceivesFilter) { public async listPaymentReceives(
tenantId: number,
paymentReceivesFilter: IPaymentReceivesFilter,
): Promise<{ paymentReceives: IPaymentReceive[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter);
@@ -481,41 +373,6 @@ export default class PaymentReceiveService {
.first(); .first();
} }
/**
* Detarmines whether the payment receive exists on the storage.
* @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId
*/
async isPaymentReceiveExists(tenantId: number, paymentReceiveId: number) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceives = await PaymentReceive.query()
.where('id', paymentReceiveId);
return paymentReceives.length > 0;
}
/**
* Detarmines the payment receive number existance.
* @async
* @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveNumber - Payment receive number.
* @param {Integer} paymentReceiveId - Payment receive id.
*/
async isPaymentReceiveNoExists(
tenantId: number,
paymentReceiveNumber: string|number,
paymentReceiveId: number
) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceives = await PaymentReceive.query()
.where('payment_receive_no', paymentReceiveNumber)
.onBuild((query) => {
if (paymentReceiveId) {
query.whereNot('id', paymentReceiveId);
}
});
return paymentReceives.length > 0;
}
/** /**
* Records payment receive journal transactions. * Records payment receive journal transactions.
* *
@@ -581,26 +438,6 @@ export default class PaymentReceiveService {
]); ]);
} }
/**
* Revert the payment amount of the given invoices ids.
* @async
* @param {number} tenantId - Tenant id.
* @param {Array} revertInvoices
* @return {Promise}
*/
private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<T>[] = [];
revertInvoices.forEach((revertInvoice) => {
const { revertAmount, invoiceId } = revertInvoice;
const oper = SaleInvoice.query()
.where('id', invoiceId)
.decrement('payment_amount', revertAmount);
opers.push(oper);
});
await Promise.all(opers);
}
/** /**
* Saves difference changing between old and new invoice payment amount. * Saves difference changing between old and new invoice payment amount.
@@ -610,34 +447,35 @@ export default class PaymentReceiveService {
* @param {Array} newPaymentReceiveEntries * @param {Array} newPaymentReceiveEntries
* @return * @return
*/ */
private async saveChangeInvoicePaymentAmount( public async saveChangeInvoicePaymentAmount(
tenantId: number, tenantId: number,
paymentReceiveEntries: [], newPaymentReceiveEntries: IPaymentReceiveEntryDTO[],
newPaymentReceiveEntries: [], oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[],
) { ): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<T>[] = []; const opers: Promise<T>[] = [];
const newEntriesTable = chain(newPaymentReceiveEntries)
.groupBy('invoice_id')
.mapValues((group) => (sumBy(group, 'payment_amount') || 0) * -1)
.value();
const diffEntries = chain(paymentReceiveEntries) const oldEntriesTable = chain(oldPaymentReceiveEntries)
.groupBy('invoiceId') .groupBy('invoiceId')
.mapValues((group) => (sumBy(group, 'paymentAmount') || 0) * -1) .mapValues((group) => (sumBy(group, 'paymentAmount') || 0) * -1)
.mapValues((value, key) => value - (newEntriesTable[key] || 0)) .value();
.mapValues((value, key) => ({ invoice_id: key, payment_amount: value }))
.filter((entry) => entry.payment_amount != 0) const diffEntries = chain(newPaymentReceiveEntries)
.groupBy('invoiceId')
.mapValues((group) => (sumBy(group, 'paymentAmount') || 0))
.mapValues((value, key) => value - (oldEntriesTable[key] || 0))
.mapValues((value, key) => ({ invoiceId: key, paymentAmount: value }))
.filter((entry) => entry.paymentAmount != 0)
.values() .values()
.value(); .value();
diffEntries.forEach((diffEntry: any) => { diffEntries.forEach((diffEntry: any) => {
const oper = SaleInvoice.changePaymentAmount( const oper = SaleInvoice.changePaymentAmount(
diffEntry.invoice_id, diffEntry.invoiceId,
diffEntry.payment_amount diffEntry.paymentAmount
); );
opers.push(oper); opers.push(oper);
}); });
return Promise.all([ ...opers ]); await Promise.all([ ...opers ]);
} }
} }

View File

@@ -14,7 +14,6 @@ import {
} from 'interfaces'; } from 'interfaces';
import events from 'subscribers/events'; import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import InventoryService from 'services/Inventory/Inventory'; import InventoryService from 'services/Inventory/Inventory';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
@@ -22,14 +21,17 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
const ERRORS = { const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND', SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS', NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE' SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
} };
/** /**
* Sales invoices service * Sales invoices service
@@ -44,7 +46,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
inventoryService: InventoryService; inventoryService: InventoryService;
@Inject() @Inject()
itemsEntriesService: HasItemsEntries; itemsEntriesService: ItemsEntriesService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@@ -58,78 +60,50 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject() @Inject()
itemsService: ItemsService; itemsService: ItemsService;
@Inject()
customersService: CustomersService;
/** /**
* Retrieve sale invoice or throw not found error. *
* @param {number} tenantId * Validate whether sale invoice number unqiue on the storage.
* @param {number} saleInvoiceId * @param {Request} req
* @param {Response} res
* @param {Function} next
*/ */
private async getSaleInvoiceOrThrowError(tenantId: number, saleInvoiceId: number): Promise<ISaleInvoice> { async validateInvoiceNumberUnique(tenantId: number, invoiceNumber: string, notInvoiceId?: number) {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query().where('id', saleInvoiceId);
this.logger.info('[sale_invoice] validating sale invoice number existance.', { tenantId, invoiceNumber });
const saleInvoice = await SaleInvoice.query()
.findOne('invoice_no', invoiceNumber)
.onBuild((builder) => {
if (notInvoiceId) {
builder.whereNot('id', notInvoiceId);
}
});
if (saleInvoice) {
this.logger.info('[sale_invoice] sale invoice number not unique.', { tenantId, invoiceNumber });
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE)
}
}
/**
* Validate whether sale invoice exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async getInvoiceOrThrowError(tenantId: number, saleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
if (!saleInvoice) { if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
} }
return saleInvoice; return saleInvoice;
} }
/**
* Validate whether sale invoice number unqiue on the storage.
* @param {number} tenantId
* @param {number} saleInvoiceNo
* @param {number} notSaleInvoiceId
*/
private async validateSaleInvoiceNoUniquiness(tenantId: number, saleInvoiceNo: string, notSaleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const foundSaleInvoice = await SaleInvoice.query()
.onBuild((query: any) => {
query.where('invoice_no', saleInvoiceNo);
if (notSaleInvoiceId) {
query.whereNot('id', notSaleInvoiceId);
}
return query;
});
if (foundSaleInvoice.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_NO_NOT_UNIQUE);
}
}
/**
* Validates sale invoice items that not sellable.
*/
private async validateNonSellableEntriesItems(tenantId: number, saleInvoiceEntries: any) {
const { Item } = this.tenancy.models(tenantId);
const itemsIds = saleInvoiceEntries.map(e => e.itemId);
const sellableItems = await Item.query().where('sellable', true).whereIn('id', itemsIds);
const sellableItemsIds = sellableItems.map((item) => item.id);
const notSellableItems = difference(itemsIds, sellableItemsIds);
if (notSellableItems.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
}
/**
*
* @param {number} tenantId
* @param {} saleInvoiceEntries
*/
validateEntriesIdsExistance(tenantId: number, saleInvoiceEntries: any) {
const entriesItemsIds = saleInvoiceEntries.map((e) => e.item_id);
const isItemsIdsExists = await this.itemsService.isItemsIdsExists(
tenantId, entriesItemsIds,
);
if (isItemsIdsExists.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_ITEMS_IDS_NOT_EXISTS);
}
}
/** /**
* Creates a new sale invoices and store it to the storage * Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions. * with associated to entries and journal transactions.
@@ -138,102 +112,100 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {ISaleInvoice} saleInvoiceDTO - * @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice} * @return {ISaleInvoice}
*/ */
public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) { public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD): Promise<ISaleInvoice> {
const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId); const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const invLotNumber = await this.inventoryService.nextLotNumber(tenantId); const invLotNumber = 1;
const saleInvoice: ISaleInvoice = { const saleInvoiceObj: ISaleInvoice = {
...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']), ...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']),
balance, balance,
paymentAmount: 0, paymentAmount: 0,
invLotNumber, // invLotNumber,
}; };
await this.validateSaleInvoiceNoUniquiness(tenantId, saleInvoiceDTO.invoiceNo); // Validate customer existance.
await this.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries); await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
// Validate sale invoice number uniquiness.
await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo);
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
this.logger.info('[sale_invoice] inserting sale invoice to the storage.'); this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
const storedInvoice = await SaleInvoice.query() const saleInvoice = await SaleInvoice.query()
.insert({ .insertGraph({
...omit(saleInvoice, ['entries']), ...omit(saleInvoiceObj, ['entries']),
});
const opers: Array<any> = [];
this.logger.info('[sale_invoice] inserting sale invoice entries to the storage.'); entries: saleInvoiceObj.entries.map((entry) => ({
saleInvoice.entries.forEach((entry: any) => {
const oper = ItemEntry.query()
.insertAndFetch({
reference_type: 'SaleInvoice', reference_type: 'SaleInvoice',
reference_id: storedInvoice.id,
...omit(entry, ['amount', 'id']), ...omit(entry, ['amount', 'id']),
}).then((itemEntry) => { }))
entry.id = itemEntry.id; });
});
opers.push(oper); await this.eventDispatcher.dispatch(events.saleInvoice.onCreated, {
tenantId, saleInvoice, saleInvoiceId: saleInvoice.id,
}); });
this.logger.info('[sale_invoice] successfully inserted.', { tenantId, saleInvoice });
this.logger.info('[sale_invoice] trying to increment the customer balance.'); return saleInvoice;
// Increment the customer balance after deliver the sale invoice.
const incrementOper = Customer.incrementBalance(
saleInvoice.customer_id,
balance,
);
// Await all async operations.
await Promise.all([ ...opers, incrementOper ]);
// Records the inventory transactions for inventory items.
await this.recordInventoryTranscactions(tenantId, saleInvoice, storedInvoice.id);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id);
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated);
return storedInvoice;
} }
/** /**
* Edit the given sale invoice. * Edit the given sale invoice.
* @async * @async
* @param {number} tenantId - * @param {number} tenantId -
* @param {Number} saleInvoiceId - * @param {Number} saleInvoiceId -
* @param {ISaleInvoice} saleInvoice - * @param {ISaleInvoice} saleInvoice -
*/ */
public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) { public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any): Promise<ISaleInvoice> {
const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId); const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const oldSaleInvoice = await SaleInvoice.query() const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
.where('id', saleInvoiceId)
.first();
const saleInvoice = { const saleInvoiceObj = {
...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']), ...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']),
balance, balance,
invLotNumber: oldSaleInvoice.invLotNumber, // invLotNumber: oldSaleInvoice.invLotNumber,
}; };
this.logger.info('[sale_invoice] trying to update sale invoice.'); // Validate customer existance.
const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.query() await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
.where('id', saleInvoiceId)
.update({ // Validate sale invoice number uniquiness.
...omit(saleInvoice, ['entries', 'invLotNumber']), await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo, saleInvoiceId);
});
// Fetches the sale invoice items entries. // Validate items ids existance.
const storedEntries = await ItemEntry.query() await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice'); // Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, saleInvoiceId, 'SaleInvoice', saleInvoiceDTO.entries);
this.logger.info('[sale_invoice] trying to update sale invoice.');
const saleInvoice: ISaleInvoice = await SaleInvoice.query()
.upsertGraph({
id: saleInvoiceId,
...omit(saleInvoiceObj, ['entries', 'invLotNumber']),
entries: saleInvoiceObj.entries.map((entry) => ({
reference_type: 'SaleInvoice',
...omit(entry, ['amount']),
}))
});
// Patch update the sale invoice items entries.
await this.itemsEntriesService.patchItemsEntries(
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
);
// Triggers `onSaleInvoiceEdited` event. // Triggers `onSaleInvoiceEdited` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited); await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId,
});
return saleInvoice;
} }
/** /**
@@ -242,16 +214,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async * @async
* @param {Number} saleInvoiceId - The given sale invoice id. * @param {Number} saleInvoiceId - The given sale invoice id.
*/ */
public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) { public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number): Promise<void> {
const { const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
SaleInvoice,
ItemEntry,
Customer,
InventoryTransaction,
AccountTransaction,
} = this.tenancy.models(tenantId);
const oldSaleInvoice = await this.getSaleInvoiceOrThrowError(tenantId, saleInvoiceId); const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
this.logger.info('[sale_invoice] delete sale invoice with entries.'); this.logger.info('[sale_invoice] delete sale invoice with entries.');
await SaleInvoice.query().where('id', saleInvoiceId).delete(); await SaleInvoice.query().where('id', saleInvoiceId).delete();
@@ -260,42 +226,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
.where('reference_type', 'SaleInvoice') .where('reference_type', 'SaleInvoice')
.delete(); .delete();
this.logger.info('[sale_invoice] revert the customer balance.'); await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted, {
const revertCustomerBalanceOper = Customer.changeBalance( tenantId, oldSaleInvoice,
oldSaleInvoice.customerId, });
oldSaleInvoice.balance * -1,
);
const invoiceTransactions = await AccountTransaction.query()
.whereIn('reference_type', ['SaleInvoice'])
.where('reference_id', saleInvoiceId)
.withGraphFetched('account.type');
const journal = new JournalPoster(tenantId);
journal.loadEntries(invoiceTransactions);
journal.removeEntries();
const inventoryTransactions = await InventoryTransaction.query()
.where('transaction_type', 'SaleInvoice')
.where('transaction_id', saleInvoiceId);
// Revert inventory transactions.
const revertInventoryTransactionsOper = this.revertInventoryTransactions(
tenantId,
inventoryTransactions,
);
// Await all async operations.
await Promise.all([
journal.deleteEntries(),
journal.saveBalance(),
revertCustomerBalanceOper,
revertInventoryTransactionsOper,
]);
// 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);
} }
/** /**
@@ -378,60 +311,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
.first(); .first();
} }
/**
* Detarmines the sale invoice number id exists on the storage.
* @param {Integer} saleInvoiceId
* @return {Boolean}
*/
async isSaleInvoiceExists(tenantId: number, saleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const foundSaleInvoice = await SaleInvoice.query()
.where('id', saleInvoiceId);
return foundSaleInvoice.length !== 0;
}
/**
* Detarmines the sale invoice number exists on the storage.
* @async
* @param {Number|String} saleInvoiceNumber
* @param {Number} saleInvoiceId
* @return {Boolean}
*/
async isSaleInvoiceNumberExists(
tenantId: number,
saleInvoiceNumber: string|number,
saleInvoiceId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const foundSaleInvoice = await SaleInvoice.query()
.onBuild((query: any) => {
query.where('invoice_no', saleInvoiceNumber);
if (saleInvoiceId) {
query.whereNot('id', saleInvoiceId);
}
return query;
});
return (foundSaleInvoice.length !== 0);
}
/**
* Detarmine the invoices IDs in bulk and returns the not found ones.
* @param {Array} invoicesIds
* @return {Array}
*/
async isInvoicesExist(tenantId: number, invoicesIds: Array<number>) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const storedInvoices = await SaleInvoice.query()
.onBuild((builder: any) => {
builder.whereIn('id', invoicesIds);
return builder;
});
const storedInvoicesIds = storedInvoices.map((i) => i.id);
const notStoredInvoices = difference(invoicesIds, storedInvoicesIds);
return notStoredInvoices;
}
/** /**
* Schedules compute sale invoice items cost based on each item * Schedules compute sale invoice items cost based on each item
* cost method. * cost method.
@@ -501,8 +380,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
public async salesInvoicesList(tenantId: number, salesInvoicesFilter: ISalesInvoicesFilter): public async salesInvoicesList(
Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { tenantId: number,
salesInvoicesFilter: ISalesInvoicesFilter
): Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter);
@@ -515,6 +396,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
salesInvoicesFilter.page - 1, salesInvoicesFilter.page - 1,
salesInvoicesFilter.pageSize, salesInvoicesFilter.pageSize,
); );
return { salesInvoices: results, pagination, filterMeta: dynamicFilter.getResponseMeta() }; return {
salesInvoices: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
} }
} }

View File

@@ -41,7 +41,7 @@ export default class BillSubscriber {
async handlerWriteJournalEntries({ tenantId, billId, bill }) { async handlerWriteJournalEntries({ tenantId, billId, bill }) {
// Writes the journal entries for the given bill transaction. // Writes the journal entries for the given bill transaction.
this.logger.info('[bill] writing bill journal entries.', { tenantId }); this.logger.info('[bill] writing bill journal entries.', { tenantId });
await this.billsService.recordJournalTransactions(tenantId, bill); await this.billsService.recordJournalTransactions(tenantId, bill, billId);
} }
/** /**
@@ -66,18 +66,20 @@ export default class BillSubscriber {
await this.journalPosterService.revertJournalTransactions(tenantId, billId, 'Bill'); await this.journalPosterService.revertJournalTransactions(tenantId, billId, 'Bill');
} }
/**
* Handles vendor balance difference change.
*/
@On(events.bills.onEdited) @On(events.bills.onEdited)
async handleCustomerBalanceDiffChange({ tenantId, billId, oldBill, bill }) { async handleVendorBalanceDiffChange({ tenantId, billId, oldBill, bill }) {
const { vendorRepository } = this.tenancy.repositories(tenantId); const { vendorRepository } = this.tenancy.repositories(tenantId);
// Changes the diff vendor balance between old and new amount. // Changes the diff vendor balance between old and new amount.
this.logger.info('[bill[ change vendor the different balance.', { tenantId, billId }); this.logger.info('[bill[ change vendor the different balance.', { tenantId, billId });
await vendorRepository.changeDiffBalance( await vendorRepository.changeDiffBalance(
bill.vendorId, bill.vendorId,
oldBill.vendorId,
bill.amount, bill.amount,
oldBill.amount, oldBill.amount,
oldBill.vendorId,
); );
} }
} }

View File

@@ -20,7 +20,7 @@ export default class PaymentMadesSubscriber {
* Handles bills payment amount increment once payment made created. * Handles bills payment amount increment once payment made created.
*/ */
@On(events.billPayments.onCreated) @On(events.billPayments.onCreated)
async handleBillsIncrement({ tenantId, billPayment, billPaymentId }) { async handleBillsIncrementPaymentAmount({ tenantId, billPayment, billPaymentId }) {
const { Bill } = this.tenancy.models(tenantId); const { Bill } = this.tenancy.models(tenantId);
const storeOpers = []; const storeOpers = [];

View File

@@ -0,0 +1,88 @@
import { Container, Inject, Service } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import PaymentReceiveService from 'services/Sales/PaymentsReceives';
@EventSubscriber()
export default class PaymentReceivesSubscriber {
tenancy: TenancyService;
logger: any;
paymentReceivesService: PaymentReceiveService;
constructor() {
this.tenancy = Container.get(TenancyService);
this.logger = Container.get('logger');
this.paymentReceivesService = Container.get(PaymentReceiveService);
}
/**
* Handle customer balance decrement once payment receive created.
*/
@On(events.paymentReceipts.onCreated)
async handleCustomerBalanceDecrement({ tenantId, paymentReceiveId, paymentReceive }) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[payment_receive] trying to decrement customer balance.');
await customerRepository.changeBalance(paymentReceive.customerId, paymentReceive.amount * -1);
}
/**
* Handle sale invoice increment/decrement payment amount once created, edited or deleted.
*/
@On(events.paymentReceipts.onCreated)
@On(events.paymentReceipts.onEdited)
async handleInvoiceIncrementPaymentAmount({ tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive }) {
this.logger.info('[payment_receive] trying to change sale invoice payment amount.', { tenantId });
await this.paymentReceivesService.saveChangeInvoicePaymentAmount(
tenantId,
paymentReceive.entries,
oldPaymentReceive?.entries || null,
);
}
/**
* Handle sale invoice diff payment amount change on payment receive edited.
*/
@On(events.paymentReceipts.onEdited)
async handleInvoiceDecrementPaymentAmount({ tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive }) {
this.logger.info('[payment_receive] trying to decrement sale invoice payment amount.');
await this.paymentReceivesService.saveChangeInvoicePaymentAmount(
tenantId,
paymentReceive.entries.map((entry) => ({
...entry,
paymentAmount: entry.paymentAmount * -1,
})),
);
}
/**
* Handle customer balance increment once payment receive deleted.
*/
@On(events.paymentReceipts.onDeleted)
async handleCustomerBalanceIncrement({ tenantId, paymentReceiveId, oldPaymentReceive }) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[payment_receive] trying to increment customer balance.');
await customerRepository.changeBalance(oldPaymentReceive.customerId, oldPaymentReceive.amount);
}
/**
* Handles customer balance diff balance change once payment receive edited.
*/
@On(events.paymentReceipts.onEdited)
async handleCustomerBalanceDiffChange({ tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive }) {
const { customerRepository } = this.tenancy.repositories(tenantId);
console.log(paymentReceive, oldPaymentReceive, 'XX');
await customerRepository.changeDiffBalance(
paymentReceive.customerId,
paymentReceive.amount * -1,
oldPaymentReceive.amount * -1,
oldPaymentReceive.customerId,
);
}
}

View File

@@ -1,22 +1,56 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { On, EventSubscriber } from "event-dispatch"; import { On, EventSubscriber } from "event-dispatch";
import events from 'subscribers/events'; import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
@EventSubscriber() @EventSubscriber()
export default class SaleInvoiceSubscriber { export default class SaleInvoiceSubscriber {
logger: any;
tenancy: TenancyService;
constructor() {
this.logger = Container.get('logger');
this.tenancy = Container.get(TenancyService);
}
/**
* Handles customer balance increment once sale invoice created.
*/
@On(events.saleInvoice.onCreated) @On(events.saleInvoice.onCreated)
public onSaleInvoiceCreated(payload) { public async handleCustomerBalanceIncrement({ tenantId, saleInvoice, saleInvoiceId }) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[sale_invoice] trying to increment customer balance.', { tenantId });
await customerRepository.changeBalance(saleInvoice.customerId, saleInvoice.balance);
} }
/**
* Handles customer balance diff balnace change once sale invoice edited.
*/
@On(events.saleInvoice.onEdited) @On(events.saleInvoice.onEdited)
public onSaleInvoiceEdited(payload) { public async onSaleInvoiceEdited({ tenantId, saleInvoice, oldSaleInvoice, saleInvoiceId }) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[sale_invoice] trying to change diff customer balance.', { tenantId });
await customerRepository.changeDiffBalance(
saleInvoice.customerId,
saleInvoice.balance,
oldSaleInvoice.balance,
oldSaleInvoice.customerId,
)
} }
/**
* Handles customer balance decrement once sale invoice deleted.
*/
@On(events.saleInvoice.onDeleted) @On(events.saleInvoice.onDeleted)
public onSaleInvoiceDeleted(payload) { public async handleCustomerBalanceDecrement({ tenantId, saleInvoiceId, oldSaleInvoice }) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[sale_invoice] trying to decrement customer balance.', { tenantId });
await customerRepository.changeBalance(
oldSaleInvoice.customerId,
oldSaleInvoice.balance * -1,
);
} }
} }