mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
refactoring: bills service.
refactoring: bills payments made service.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import JournalCommands from 'services/Accounting/JournalCommands';
|
||||
|
||||
@Service()
|
||||
export default class JournalPosterService {
|
||||
@@ -19,20 +20,14 @@ export default class JournalPosterService {
|
||||
referenceId: number,
|
||||
referenceType: string
|
||||
) {
|
||||
const { Account, AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommand = new JournalCommands(journal);
|
||||
|
||||
const transactions = await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('reference_type', [referenceType])
|
||||
.where('reference_id', referenceId)
|
||||
.withGraphFetched('account.type');
|
||||
await journalCommand.revertJournalEntries(referenceId, referenceType);
|
||||
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
journal.loadEntries(transactions);
|
||||
journal.removeEntries();
|
||||
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveBalance()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { omit, sumBy, chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import { IPaymentReceiveOTD } from 'interfaces';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
@@ -34,6 +39,186 @@ export default class PaymentReceiveService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Validates the payment receive number existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validatePaymentReceiveNoExistance(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const isPaymentNoExists = await this.paymentReceiveService.isPaymentReceiveNoExists(
|
||||
tenantId,
|
||||
req.body.payment_receive_no,
|
||||
req.params.id,
|
||||
);
|
||||
if (isPaymentNoExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.RECEIVE.NUMBER.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment receive existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validatePaymentReceiveExistance(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const isPaymentNoExists = await this.paymentReceiveService
|
||||
.isPaymentReceiveExists(
|
||||
tenantId,
|
||||
req.params.id
|
||||
);
|
||||
if (!isPaymentNoExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.RECEIVE.NOT.EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the deposit account id existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateDepositAccount(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const isDepositAccExists = await this.accountsService.isAccountExists(
|
||||
tenantId,
|
||||
req.body.deposit_account_id
|
||||
);
|
||||
if (!isDepositAccExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the `customer_id` existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const { Customer } = req.models;
|
||||
|
||||
const isCustomerExists = await Customer.query().findById(req.body.customer_id);
|
||||
|
||||
if (!isCustomerExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the invoices IDs existance.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {Function} next -
|
||||
*/
|
||||
async validateInvoicesIDs(req: Request, res: Response, next: Function) {
|
||||
const paymentReceive = { ...req.body };
|
||||
const { tenantId } = req;
|
||||
const invoicesIds = paymentReceive.entries
|
||||
.map((e) => e.invoice_id);
|
||||
|
||||
const notFoundInvoicesIDs = await this.saleInvoiceService.isInvoicesExist(
|
||||
tenantId,
|
||||
invoicesIds,
|
||||
paymentReceive.customer_id,
|
||||
);
|
||||
if (notFoundInvoicesIDs.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVOICES.IDS.NOT.FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates entries invoice payment amount.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {Function} next -
|
||||
*/
|
||||
async validateInvoicesPaymentsAmount(req: Request, res: Response, next: Function) {
|
||||
const { SaleInvoice } = req.models;
|
||||
const invoicesIds = req.body.entries.map((e) => e.invoice_id);
|
||||
|
||||
const storedInvoices = await SaleInvoice.query()
|
||||
.whereIn('id', invoicesIds);
|
||||
|
||||
const storedInvoicesMap = new Map(
|
||||
storedInvoices.map((invoice) => [invoice.id, invoice])
|
||||
);
|
||||
const hasWrongPaymentAmount: any[] = [];
|
||||
|
||||
req.body.entries.forEach((entry, index: number) => {
|
||||
const entryInvoice = storedInvoicesMap.get(entry.invoice_id);
|
||||
const { dueAmount } = entryInvoice;
|
||||
|
||||
if (dueAmount < entry.payment_amount) {
|
||||
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
|
||||
}
|
||||
});
|
||||
if (hasWrongPaymentAmount.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVOICE.PAYMENT.AMOUNT',
|
||||
code: 200,
|
||||
indexes: hasWrongPaymentAmount,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the payment receive entries IDs existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async validateEntriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const paymentReceive = { id: req.params.id, ...req.body };
|
||||
const entriesIds = paymentReceive.entries
|
||||
.filter(entry => entry.id)
|
||||
.map(entry => entry.id);
|
||||
|
||||
const { PaymentReceiveEntry } = req.models;
|
||||
|
||||
const storedEntries = await PaymentReceiveEntry.query()
|
||||
.where('payment_receive_id', paymentReceive.id);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new payment receive and store it to the storage
|
||||
* with associated invoices payment and journal transactions.
|
||||
@@ -53,9 +238,10 @@ export default class PaymentReceiveService {
|
||||
|
||||
this.logger.info('[payment_receive] inserting to the storage.');
|
||||
const storedPaymentReceive = await PaymentReceive.query()
|
||||
.insert({
|
||||
.insertGraph({
|
||||
amount: paymentAmount,
|
||||
...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']),
|
||||
entries: paymentReceive.entries.map((entry) => ({ ...entry })),
|
||||
});
|
||||
const storeOpers: Array<any> = [];
|
||||
|
||||
@@ -92,6 +278,8 @@ export default class PaymentReceiveService {
|
||||
customerIncrementOper,
|
||||
recordJournalTransactions,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.paymentReceipts.onCreated);
|
||||
|
||||
return storedPaymentReceive;
|
||||
}
|
||||
|
||||
@@ -186,6 +374,7 @@ export default class PaymentReceiveService {
|
||||
changeCustomerBalance,
|
||||
diffInvoicePaymentAmount,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.paymentReceipts.onEdited);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,6 +428,7 @@ export default class PaymentReceiveService {
|
||||
revertCustomerBalance,
|
||||
revertInvoicesPaymentAmount,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.paymentReceipts.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { omit, difference, sumBy, mixin } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces';
|
||||
import { IEstimatesFilter, IFilterMeta, IPaginationMeta, ISaleEstimate, ISaleEstimateDTO } from 'interfaces';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import { formatDateFields } from 'utils';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import events from 'subscribers/events';
|
||||
|
||||
/**
|
||||
* Sale estimate service.
|
||||
@@ -24,14 +29,132 @@ export default class SaleEstimateService {
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
|
||||
/**
|
||||
* Validate whether the estimate customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const estimate = { ...req.body };
|
||||
const { Customer } = req.models
|
||||
|
||||
const foundCustomer = await Customer.query().findById(estimate.customer_id);
|
||||
|
||||
if (!foundCustomer) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the estimate number unique on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateNumberExistance(req: Request, res: Response, next: Function) {
|
||||
const estimate = { ...req.body };
|
||||
const { tenantId } = req;
|
||||
|
||||
const isEstNumberUnqiue = await this.saleEstimateService.isEstimateNumberUnique(
|
||||
tenantId,
|
||||
estimate.estimate_number,
|
||||
req.params.id,
|
||||
);
|
||||
if (isEstNumberUnqiue) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the estimate entries items ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateEntriesItemsExistance(req: Request, res: Response, next: Function) {
|
||||
const tenantId = req.tenantId;
|
||||
const estimate = { ...req.body };
|
||||
const estimateItemsIds = estimate.entries.map(e => e.item_id);
|
||||
|
||||
// Validate items ids in estimate entries exists.
|
||||
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(tenantId, estimateItemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the sale estimate id exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateEstimateIdExistance(req: Request, res: Response, next: Function) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
const storedEstimate = await this.saleEstimateService
|
||||
.getEstimate(tenantId, estimateId);
|
||||
|
||||
if (!storedEstimate) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sale invoice entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async valdiateInvoiceEntriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { ItemEntry } = req.models;
|
||||
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoice = { ...req.body };
|
||||
const entriesIds = saleInvoice.entries
|
||||
.filter(e => e.id)
|
||||
.map((e) => e.id);
|
||||
|
||||
const foundEntries = await ItemEntry.query()
|
||||
.whereIn('id', entriesIds)
|
||||
.where('reference_type', 'SaleInvoice')
|
||||
.where('reference_id', saleInvoiceId);
|
||||
|
||||
if (foundEntries.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new estimate with associated entries.
|
||||
* @async
|
||||
* @param {number} tenantId - The tenant id.
|
||||
* @param {EstimateDTO} estimate
|
||||
* @return {void}
|
||||
* @return {Promise<ISaleEstimate>}
|
||||
*/
|
||||
async createEstimate(tenantId: number, estimateDTO: any) {
|
||||
async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
|
||||
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
@@ -44,22 +167,15 @@ export default class SaleEstimateService {
|
||||
const storedEstimate = await SaleEstimate.query()
|
||||
.insert({
|
||||
...omit(estimate, ['entries']),
|
||||
});
|
||||
const storeEstimateEntriesOpers: any[] = [];
|
||||
|
||||
this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.');
|
||||
estimate.entries.forEach((entry: any) => {
|
||||
const oper = ItemEntry.query()
|
||||
.insert({
|
||||
entries: estimate.entries.map((entry) => ({
|
||||
reference_type: 'SaleEstimate',
|
||||
reference_id: storedEstimate.id,
|
||||
...omit(entry, ['total', 'amount', 'id']),
|
||||
});
|
||||
storeEstimateEntriesOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...storeEstimateEntriesOpers]);
|
||||
}))
|
||||
});
|
||||
|
||||
this.logger.info('[sale_estimate] insert sale estimated success.');
|
||||
await this.eventDispatcher.dispatch(events.saleEstimates.onCreated);
|
||||
|
||||
return storedEstimate;
|
||||
}
|
||||
@@ -72,7 +188,7 @@ export default class SaleEstimateService {
|
||||
* @param {EstimateDTO} estimate
|
||||
* @return {void}
|
||||
*/
|
||||
async editEstimate(tenantId: number, estimateId: number, estimateDTO: any) {
|
||||
async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise<void> {
|
||||
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
|
||||
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
|
||||
@@ -89,16 +205,14 @@ export default class SaleEstimateService {
|
||||
.where('reference_id', estimateId)
|
||||
.where('reference_type', 'SaleEstimate');
|
||||
|
||||
const patchItemsEntries = this.itemsEntriesService.patchItemsEntries(
|
||||
await this.itemsEntriesService.patchItemsEntries(
|
||||
tenantId,
|
||||
estimate.entries,
|
||||
storedEstimateEntries,
|
||||
'SaleEstimate',
|
||||
estimateId,
|
||||
);
|
||||
return Promise.all([
|
||||
patchItemsEntries,
|
||||
]);
|
||||
await this.eventDispatcher.dispatch(events.saleEstimates.onEdited);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +232,9 @@ export default class SaleEstimateService {
|
||||
.delete();
|
||||
|
||||
await SaleEstimate.query().where('id', estimateId).delete();
|
||||
this.logger.info('[sale_estimate] deleted successfully.', { tenantId, estimateId });
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleEstimates.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,15 @@ import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceOTD,
|
||||
IItemEntry,
|
||||
ISalesInvoicesFilter,
|
||||
IPaginationMeta,
|
||||
IFilterMeta
|
||||
} from 'interfaces';
|
||||
import events from 'subscribers/events';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import HasItemsEntries from 'services/Sales/HasItemsEntries';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
@@ -12,6 +20,16 @@ import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import ItemsService from 'services/Items/ItemsService';
|
||||
|
||||
|
||||
const ERRORS = {
|
||||
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
|
||||
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
|
||||
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
|
||||
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
|
||||
}
|
||||
|
||||
/**
|
||||
* Sales invoices service
|
||||
@@ -37,6 +55,81 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject()
|
||||
itemsService: ItemsService;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
*/
|
||||
private async getSaleInvoiceOrThrowError(tenantId: number, saleInvoiceId: number): Promise<ISaleInvoice> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const saleInvoice = await SaleInvoice.query().where('id', saleInvoiceId);
|
||||
|
||||
if (!saleInvoice) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
|
||||
}
|
||||
return saleInvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale invoice number unqiue on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceNo
|
||||
* @param {number} notSaleInvoiceId
|
||||
*/
|
||||
private async validateSaleInvoiceNoUniquiness(tenantId: number, saleInvoiceNo: string, notSaleInvoiceId: number) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundSaleInvoice = await SaleInvoice.query()
|
||||
.onBuild((query: any) => {
|
||||
query.where('invoice_no', saleInvoiceNo);
|
||||
|
||||
if (notSaleInvoiceId) {
|
||||
query.whereNot('id', notSaleInvoiceId);
|
||||
}
|
||||
return query;
|
||||
});
|
||||
|
||||
if (foundSaleInvoice.length > 0) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NO_NOT_UNIQUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates sale invoice items that not sellable.
|
||||
*/
|
||||
private async validateNonSellableEntriesItems(tenantId: number, saleInvoiceEntries: any) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const itemsIds = saleInvoiceEntries.map(e => e.itemId);
|
||||
|
||||
const sellableItems = await Item.query().where('sellable', true).whereIn('id', itemsIds);
|
||||
|
||||
const sellableItemsIds = sellableItems.map((item) => item.id);
|
||||
const notSellableItems = difference(itemsIds, sellableItemsIds);
|
||||
|
||||
if (notSellableItems.length > 0) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {} saleInvoiceEntries
|
||||
*/
|
||||
validateEntriesIdsExistance(tenantId: number, saleInvoiceEntries: any) {
|
||||
const entriesItemsIds = saleInvoiceEntries.map((e) => e.item_id);
|
||||
|
||||
const isItemsIdsExists = await this.itemsService.isItemsIdsExists(
|
||||
tenantId, entriesItemsIds,
|
||||
);
|
||||
if (isItemsIdsExists.length > 0) {
|
||||
throw new ServiceError(ERRORS.ENTRIES_ITEMS_IDS_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sale invoices and store it to the storage
|
||||
* with associated to entries and journal transactions.
|
||||
@@ -58,6 +151,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
invLotNumber,
|
||||
};
|
||||
|
||||
await this.validateSaleInvoiceNoUniquiness(tenantId, saleInvoiceDTO.invoiceNo);
|
||||
await this.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
|
||||
|
||||
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
|
||||
const storedInvoice = await SaleInvoice.query()
|
||||
.insert({
|
||||
@@ -95,6 +191,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
// method and starting date.
|
||||
await this.scheduleComputeInvoiceItemsCost(tenantId, storedInvoice.id);
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated);
|
||||
|
||||
return storedInvoice;
|
||||
}
|
||||
|
||||
@@ -131,30 +229,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
.where('reference_type', 'SaleInvoice');
|
||||
|
||||
// Patch update the sale invoice items entries.
|
||||
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries(
|
||||
await this.itemsEntriesService.patchItemsEntries(
|
||||
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
|
||||
);
|
||||
|
||||
this.logger.info('[sale_invoice] change customer different balance.');
|
||||
// Changes the diff customer balance between old and new amount.
|
||||
const changeCustomerBalanceOper = Customer.changeDiffBalance(
|
||||
saleInvoice.customer_id,
|
||||
oldSaleInvoice.customerId,
|
||||
balance,
|
||||
oldSaleInvoice.balance,
|
||||
);
|
||||
// Records the inventory transactions for inventory items.
|
||||
const recordInventoryTransOper = this.recordInventoryTranscactions(
|
||||
tenantId, saleInvoice, saleInvoiceId, true,
|
||||
);
|
||||
await Promise.all([
|
||||
patchItemsEntriesOper,
|
||||
changeCustomerBalanceOper,
|
||||
recordInventoryTransOper,
|
||||
]);
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeInvoiceItemsCost(tenantId, saleInvoiceId, true);
|
||||
// Triggers `onSaleInvoiceEdited` event.
|
||||
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,14 +247,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
SaleInvoice,
|
||||
ItemEntry,
|
||||
Customer,
|
||||
Account,
|
||||
InventoryTransaction,
|
||||
AccountTransaction,
|
||||
} = this.tenancy.models(tenantId);
|
||||
|
||||
const oldSaleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries');
|
||||
const oldSaleInvoice = await this.getSaleInvoiceOrThrowError(tenantId, saleInvoiceId);
|
||||
|
||||
this.logger.info('[sale_invoice] delete sale invoice with entries.');
|
||||
await SaleInvoice.query().where('id', saleInvoiceId).delete();
|
||||
@@ -218,6 +294,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeItemsCost(tenantId, oldSaleInvoice)
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { omit, difference, sumBy } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import HasItemEntries from 'services/Sales/HasItemsEntries';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
@@ -21,6 +26,125 @@ export default class SalesReceiptService {
|
||||
@Inject()
|
||||
itemsEntriesService: HasItemEntries;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getSaleReceiptOrThrowError(tenantId: number, saleReceiptId: number) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
const isSaleReceiptExists = await this.saleReceiptService
|
||||
.isSaleReceiptExists(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
);
|
||||
if (!isSaleReceiptExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptCustomerExistance(req: Request, res: Response, next: Function) {
|
||||
const saleReceipt = { ...req.body };
|
||||
const { Customer } = req.models;
|
||||
|
||||
const foundCustomer = await Customer.query().findById(saleReceipt.customer_id);
|
||||
|
||||
if (!foundCustomer) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt deposit account exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptDepositAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const isDepositAccountExists = await this.accountsService.isAccountExists(
|
||||
tenantId,
|
||||
saleReceipt.deposit_account_id
|
||||
);
|
||||
if (!isDepositAccountExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether receipt items ids exist on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptItemsIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id);
|
||||
|
||||
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(
|
||||
tenantId,
|
||||
estimateItemsIds
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }] });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate receipt entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateReceiptEntriesIds(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const saleReceipt = { ...req.body };
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
// Validate the entries IDs that not stored or associated to the sale receipt.
|
||||
const notExistsEntriesIds = await this.saleReceiptService
|
||||
.isSaleReceiptEntriesIDsExists(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
saleReceipt,
|
||||
);
|
||||
if (notExistsEntriesIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{
|
||||
type: 'ENTRIES.IDS.NOT.FOUND',
|
||||
code: 500,
|
||||
}]
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @async
|
||||
@@ -38,20 +162,14 @@ export default class SalesReceiptService {
|
||||
const storedSaleReceipt = await SaleReceipt.query()
|
||||
.insert({
|
||||
...omit(saleReceipt, ['entries']),
|
||||
});
|
||||
const storeSaleReceiptEntriesOpers: Array<any> = [];
|
||||
|
||||
saleReceipt.entries.forEach((entry: any) => {
|
||||
const oper = ItemEntry.query()
|
||||
.insert({
|
||||
entries: saleReceipt.entries.map((entry) => ({
|
||||
reference_type: 'SaleReceipt',
|
||||
reference_id: storedSaleReceipt.id,
|
||||
...omit(entry, ['id', 'amount']),
|
||||
});
|
||||
storeSaleReceiptEntriesOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...storeSaleReceiptEntriesOpers]);
|
||||
return storedSaleReceipt;
|
||||
}))
|
||||
});
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +203,9 @@ export default class SalesReceiptService {
|
||||
'SaleReceipt',
|
||||
saleReceiptId,
|
||||
);
|
||||
return Promise.all([patchItemsEntries]);
|
||||
await Promise.all([patchItemsEntries]);
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onCreated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,11 +230,13 @@ export default class SalesReceiptService {
|
||||
saleReceiptId,
|
||||
'SaleReceipt'
|
||||
);
|
||||
return Promise.all([
|
||||
await Promise.all([
|
||||
deleteItemsEntriesOper,
|
||||
deleteSaleReceiptOper,
|
||||
deleteTransactionsOper,
|
||||
]);
|
||||
|
||||
await this.eventDispatcher.dispatch(events.saleReceipts.onDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user