fix(Contacts): validate contact associated transcations.

This commit is contained in:
a.bouhuolia
2021-03-22 15:21:52 +02:00
parent 1f6aca63e2
commit d79be910f9
20 changed files with 382 additions and 384 deletions

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi';
import { omit, intersection, defaultTo } from 'lodash';
import { omit, defaultTo } from 'lodash';
import async from 'async';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -7,6 +8,7 @@ import {
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
import ContactsService from 'services/Contacts/ContactsService';
import moment from 'moment';
import {
ICustomerNewDTO,
ICustomerEditDTO,
@@ -16,15 +18,20 @@ import {
IContactNewDTO,
IContactEditDTO,
IContact,
ISaleInvoice,
ISystemUser,
ISalesInvoicesService,
ISalesReceiptsService,
ISalesEstimatesService,
IPaymentsReceiveService,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
import moment from 'moment';
const ERRORS = {
CUSTOMER_HAS_TRANSACTIONS: 'CUSTOMER_HAS_TRANSACTIONS',
};
@Service()
export default class CustomersService {
@Inject()
@@ -42,6 +49,18 @@ export default class CustomersService {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject('SalesInvoices')
invoicesService: ISalesInvoicesService;
@Inject('SalesReceipts')
receiptsService: ISalesReceiptsService;
@Inject('PaymentReceives')
paymentsService: IPaymentsReceiveService;
@Inject('SalesEstimates')
estimatesService: ISalesEstimatesService;
/**
* Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
@@ -158,6 +177,40 @@ export default class CustomersService {
return customer;
}
/**
* Validate the customer associated relations.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
private async validateCustomerAssociatedRelations(
tenantId: number,
customerId: number
) {
try {
// Validate whether the customer has no associated estimates transactions.
await this.estimatesService.validateCustomerHasNoEstimates(
tenantId,
customerId
);
// Validate whether the customer has no assocaited invoices tranasctions.
await this.invoicesService.validateCustomerHasNoInvoices(
tenantId,
customerId
);
// Validate whether the customer has no associated receipts transactions.
await this.receiptsService.validateCustomerHasNoReceipts(
tenantId,
customerId
);
// Validate whether the customer has no associated payment receives transactions.
await this.paymentsService.validateCustomerHasNoPayments(
tenantId,
customerId
);
} catch (error) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_TRANSACTIONS);
}
}
/**
* Deletes the given customer from the storage.
* @param {number} tenantId
@@ -176,9 +229,10 @@ export default class CustomersService {
// Retrieve the customer of throw not found service error.
await this.getCustomerByIdOrThrowError(tenantId, customerId);
// Validate whether the customer has no assocaited invoices tranasctions.
await this.customerHasNoInvoicesOrThrowError(tenantId, customerId);
// Validate the customer associated relations.
await this.validateCustomerAssociatedRelations(tenantId, customerId);
// Delete the customer from the storage.
await this.contactService.deleteContact(tenantId, customerId, 'customer');
// Throws `onCustomerDeleted` event.
@@ -225,12 +279,13 @@ export default class CustomersService {
filterMeta: IFilterMeta;
}> {
const { Customer } = this.tenancy.models(tenantId);
// Dynamic list.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Customer,
customersFilter
);
const { results, pagination } = await Customer.query()
.onBuild((query) => {
dynamicList.buildQuery()(query);
@@ -310,104 +365,6 @@ export default class CustomersService {
);
}
/**
* Retrieve the given customers or throw error if one of them not found.
* @param {numebr} tenantId
* @param {number[]} customersIds
*/
private getCustomersOrThrowErrorNotFound(
tenantId: number,
customersIds: number[]
) {
return this.contactService.getContactsOrThrowErrorNotFound(
tenantId,
customersIds,
'customer'
);
}
/**
* Deletes the given customers from the storage.
* @param {number} tenantId
* @param {number[]} customersIds
* @return {Promise<void>}
*/
public async deleteBulkCustomers(
tenantId: number,
customersIds: number[],
authorizedUser: ISystemUser,
): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
// Validate the customers existance on the storage.
await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds);
// Validate the customers have no associated invoices.
await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds);
// Deletes the given customers.
await Contact.query().whereIn('id', customersIds).delete();
// Triggers `onCustomersBulkDeleted` event.
await this.eventDispatcher.dispatch(events.customers.onBulkDeleted, {
tenantId,
customersIds,
authorizedUser,
});
}
/**
* Validates the customer has no associated sales invoice
* or throw service error.
* @param {number} tenantId
* @param {number} customerId
* @throws {ServiceError}
* @return {Promise<void>}
*/
private async customerHasNoInvoicesOrThrowError(
tenantId: number,
customerId: number
) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve the sales invoices that assocaited to the given customer.
const salesInvoice = await saleInvoiceRepository.find({
customer_id: customerId,
});
if (salesInvoice.length > 0) {
throw new ServiceError('customer_has_invoices');
}
}
/**
* Throws error in case one of customers have associated sales invoices.
* @param {number} tenantId
* @param {number[]} customersIds
* @throws {ServiceError}
* @return {Promise<void>}
*/
private async customersHaveNoInvoicesOrThrowError(
tenantId: number,
customersIds: number[]
) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const customersInvoices = await saleInvoiceRepository.findWhereIn(
'customer_id',
customersIds
);
const customersIdsWithInvoice = customersInvoices.map(
(saleInvoice: ISaleInvoice) => saleInvoice.customerId
);
const customersHaveInvoices = intersection(
customersIds,
customersIdsWithInvoice
);
if (customersHaveInvoices.length > 0) {
throw new ServiceError('some_customers_have_invoices');
}
}
/**
* Changes the opening balance of the given customer.
* @param {number} tenantId

View File

@@ -15,12 +15,18 @@ import {
IPaginationMeta,
IFilterMeta,
ISystemUser,
IBillsService,
IBillPaymentsService,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
import events from 'subscribers/events';
const ERRORS = {
VENDOR_HAS_TRANSACTIONS: 'VENDOR_HAS_TRANSACTIONS',
};
@Service()
export default class VendorsService {
@Inject()
@@ -38,6 +44,12 @@ export default class VendorsService {
@Inject('logger')
logger: any;
@Inject('Bills')
billsService: IBillsService;
@Inject('BillPayments')
billPaymentsService: IBillPaymentsService;
/**
* Converts vendor to contact DTO.
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
@@ -124,6 +136,27 @@ export default class VendorsService {
);
}
/**
* Validate the given vendor has no associated transactions.
* @param {number} tenantId
* @param {number} vendorId
*/
private async validateAssociatedTransactions(
tenantId: number,
vendorId: number
) {
try {
// Validate vendor has no bills.
await this.billsService.validateVendorHasNoBills(tenantId, vendorId);
// Validate vendor has no paymentys.
await this.billPaymentsService.validateVendorHasNoPayments(tenantId, vendorId);
} catch (error) {
throw new ServiceError(ERRORS.VENDOR_HAS_TRANSACTIONS);
}
}
/**
* Deletes the given vendor from the storage.
* @param {number} tenantId
@@ -138,8 +171,8 @@ export default class VendorsService {
// Validate the vendor existance on the storage.
await this.getVendorByIdOrThrowError(tenantId, vendorId);
// Validate the vendor has no associated bills.
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId);
// Validate associated vendor transactions.
await this.validateAssociatedTransactions(tenantId, vendorId);
this.logger.info('[vendor] trying to delete vendor.', {
tenantId,
@@ -222,102 +255,6 @@ export default class VendorsService {
);
}
/**
* Retrieve the given vendors or throw error if one of them not found.
* @param {numebr} tenantId
* @param {number[]} vendorsIds
*/
private getVendorsOrThrowErrorNotFound(
tenantId: number,
vendorsIds: number[]
) {
return this.contactService.getContactsOrThrowErrorNotFound(
tenantId,
vendorsIds,
'vendor'
);
}
/**
* Deletes the given vendors from the storage.
* @param {number} tenantId
* @param {number[]} vendorsIds
* @return {Promise<void>}
*/
public async deleteBulkVendors(
tenantId: number,
vendorsIds: number[],
authorizedUser: ISystemUser
): Promise<void> {
const { Contact } = this.tenancy.models(tenantId);
// Validate the given vendors exists on the storage.
await this.getVendorsOrThrowErrorNotFound(tenantId, vendorsIds);
// Validate the given vendors have no assocaited bills.
await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds);
await Contact.query().whereIn('id', vendorsIds).delete();
// Triggers `onVendorsBulkDeleted` event.
await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, {
tenantId,
vendorsIds,
authorizedUser,
});
this.logger.info('[vendor] bulk deleted successfully.', {
tenantId,
vendorsIds,
});
}
/**
* Validates the vendor has no associated bills or throw service error.
* @param {number} tenantId
* @param {number} vendorId
*/
private async vendorHasNoBillsOrThrowError(
tenantId: number,
vendorId: number
) {
const { billRepository } = this.tenancy.repositories(tenantId);
// Retrieve the bill that associated to the given vendor id.
const bills = await billRepository.find({ vendor_id: vendorId });
if (bills.length > 0) {
throw new ServiceError('vendor_has_bills');
}
}
/**
* Throws error in case one of vendors have associated bills.
* @param {number} tenantId
* @param {number[]} customersIds
* @throws {ServiceError}
*/
private async vendorsHaveNoBillsOrThrowError(
tenantId: number,
vendorsIds: number[]
) {
const { billRepository } = this.tenancy.repositories(tenantId);
// Retrieves bills that assocaited to the given vendors.
const vendorsBills = await billRepository.findWhereIn(
'vendor_id',
vendorsIds
);
const billsVendorsIds = vendorsBills.map((bill) => bill.vendorId);
// The intersection between vendors and vendors that have bills.
const vendorsHaveInvoices = intersection(vendorsIds, billsVendorsIds);
if (vendorsHaveInvoices.length > 0) {
throw new ServiceError('some_vendors_have_bills');
}
}
/**
* Retrieve vendors datatable list.
* @param {number} tenantId - Tenant id.

View File

@@ -33,8 +33,8 @@ import { ERRORS } from './constants';
* Bill payments service.
* @service
*/
@Service()
export default class BillPaymentsService {
@Service('BillPayments')
export default class BillPaymentsService implements IBillPaymentsService {
@Inject()
accountsService: AccountsService;
@@ -178,7 +178,7 @@ export default class BillPaymentsService {
if (notOpenedBills.length > 0) {
throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, {
notOpenedBills
notOpenedBills,
});
}
}
@@ -696,4 +696,19 @@ export default class BillPaymentsService {
);
await Promise.all(opers);
}
/**
* Validates the given vendor has no associated payments.
* @param {number} tenantId
* @param {number} vendorId
*/
public async validateVendorHasNoPayments(tenantId: number, vendorId: number) {
const { BillPayment } = this.tenancy.models(tenantId);
const payments = await BillPayment.query().where('vendor_id', vendorId);
if (payments.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS);
}
}
}

View File

@@ -9,5 +9,6 @@ export const ERRORS = {
BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND',
INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT',
PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY',
BILLS_NOT_OPENED_YET: 'BILLS_NOT_OPENED_YET'
BILLS_NOT_OPENED_YET: 'BILLS_NOT_OPENED_YET',
VENDOR_HAS_PAYMENTS: 'VENDOR_HAS_PAYMENTS'
};

View File

@@ -21,6 +21,7 @@ import {
IPaginationMeta,
IFilterMeta,
IBillsFilter,
IBillsService
} from 'interfaces';
import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService';
@@ -34,8 +35,8 @@ import { ERRORS } from './constants';
* Vendor bills services.
* @service
*/
@Service()
export default class BillsService extends SalesInvoicesCost {
@Service('Bills')
export default class BillsService extends SalesInvoicesCost implements IBillsService {
@Inject()
inventoryService: InventoryService;
@@ -141,13 +142,10 @@ export default class BillsService extends SalesInvoicesCost {
/**
* Validate the bill has no payment entries.
* @param {number} tenantId
* @param {number} tenantId
* @param {number} billId - Bill id.
*/
private async validateBillHasNoEntries(
tenantId,
billId: number,
) {
private async validateBillHasNoEntries(tenantId, billId: number) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
// Retireve the bill associate payment made entries.
@@ -578,4 +576,22 @@ export default class BillsService extends SalesInvoicesCost {
'Bill'
);
}
/**
* Validate the given vendor has no associated bills transactions.
* @param {number} tenantId
* @param {number} vendorId - Vendor id.
*/
public async validateVendorHasNoBills(
tenantId: number,
vendorId: number
) {
const { Bill } = this.tenancy.models(tenantId);
const bills = await Bill.query().where('vendor_id', vendorId);
if (bills.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_BILLS);
}
}
}

View File

@@ -8,5 +8,6 @@ export const ERRORS = {
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED',
BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES'
BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS'
};

View File

@@ -15,6 +15,7 @@ import {
IPaymentReceiveEntry,
IPaymentReceiveEntryDTO,
IPaymentReceivesFilter,
IPaymentsReceiveService,
ISaleInvoice,
ISystemUser,
} from 'interfaces';
@@ -37,8 +38,8 @@ import { ERRORS } from './constants';
* Payment receive service.
* @service
*/
@Service()
export default class PaymentReceiveService {
@Service('PaymentReceives')
export default class PaymentReceiveService implements IPaymentsReceiveService {
@Inject()
accountsService: AccountsService;
@@ -522,7 +523,7 @@ export default class PaymentReceiveService {
public async deletePaymentReceive(
tenantId: number,
paymentReceiveId: number,
authorizedUser: ISystemUser,
authorizedUser: ISystemUser
) {
const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models(
tenantId
@@ -772,4 +773,24 @@ export default class PaymentReceiveService {
});
await Promise.all([...opers]);
}
/**
* Validate the given customer has no payments receives.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoPayments(
tenantId: number,
customerId: number
) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceives = await PaymentReceive.query().where(
'customer_id',
customerId
);
if (paymentReceives.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_PAYMENT_RECEIVES);
}
}
}

View File

@@ -10,4 +10,5 @@ export const ERRORS = {
PAYMENT_RECEIVE_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED',
PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED',
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE',
CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES'
};

View File

@@ -6,6 +6,7 @@ import {
IPaginationMeta,
ISaleEstimate,
ISaleEstimateDTO,
ISalesEstimatesService,
} from 'interfaces';
import {
EventDispatcher,
@@ -32,14 +33,15 @@ const ERRORS = {
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
};
/**
* Sale estimate service.
* @Service
*/
@Service()
export default class SaleEstimateService {
@Service('SalesEstimates')
export default class SaleEstimateService implements ISalesEstimatesService{
@Inject()
tenancy: TenancyService;
@@ -174,7 +176,8 @@ export default class SaleEstimateService {
const autoNextNumber = this.getNextEstimateNumber(tenantId);
// Retreive the next estimate number.
const estimateNumber = estimateDTO.estimateNumber ||
const estimateNumber =
estimateDTO.estimateNumber ||
oldSaleEstimate?.estimateNumber ||
autoNextNumber;
@@ -201,9 +204,9 @@ export default class SaleEstimateService {
})),
// Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
!oldSaleEstimate?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
};
}
@@ -233,10 +236,7 @@ export default class SaleEstimateService {
this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
// Transform DTO object ot model object.
const estimateObj = await this.transformDTOToModel(
tenantId,
estimateDTO
);
const estimateObj = await this.transformDTOToModel(tenantId, estimateDTO);
// Validate estimate number uniquiness on the storage.
await this.validateEstimateNumberExistance(
tenantId,
@@ -583,4 +583,24 @@ export default class SaleEstimateService {
approvedAt: null,
});
}
/**
* Validate the given customer has no sales estimates.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoEstimates(
tenantId: number,
customerId: number
) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const estimates = await SaleEstimate.query().where(
'customer_id',
customerId
);
if (estimates.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_ESTIMATES);
}
}
}

View File

@@ -15,6 +15,7 @@ import {
ISystemUser,
IItem,
IItemEntry,
ISalesInvoicesService
} from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
@@ -36,8 +37,8 @@ import { ERRORS } from './constants';
* Sales invoices service
* @service
*/
@Service()
export default class SaleInvoicesService {
@Service('SalesInvoices')
export default class SaleInvoicesService implements ISalesInvoicesService {
@Inject()
tenancy: TenancyService;
@@ -665,4 +666,22 @@ export default class SaleInvoicesService {
return salesInvoices;
}
/**
* Validate the given customer has no sales invoices.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoInvoices(
tenantId: number,
customerId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoices = await SaleInvoice.query().where('customer_id', customerId);
if (invoices.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}

View File

@@ -8,11 +8,18 @@ import {
import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
import { ISaleReceipt, ISaleReceiptDTO, IItemEntry, IItem } from 'interfaces';
import {
IFilterMeta,
IPaginationMeta,
ISaleReceipt,
ISaleReceiptDTO,
ISalesReceiptsService,
IItemEntry,
IItem,
} from 'interfaces';
import JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
import { IFilterMeta, IPaginationMeta } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
@@ -29,10 +36,11 @@ const ERRORS = {
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
};
@Service()
export default class SalesReceiptService {
@Service('SalesReceipts')
export default class SalesReceiptService implements ISalesReceiptsService {
@Inject()
tenancy: TenancyService;
@@ -548,4 +556,22 @@ export default class SalesReceiptService {
'SaleReceipt'
);
}
/**
* Validate the given customer has no sales receipts.
* @param {number} tenantId
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoReceipts(
tenantId: number,
customerId: number
) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const receipts = await SaleReceipt.query().where('customer_id', customerId);
if (receipts.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}

View File

@@ -9,5 +9,6 @@ export const ERRORS = {
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED'
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES'
};