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

@@ -63,13 +63,6 @@ export default class CustomersController extends ContactsController {
asyncMiddleware(this.deleteCustomer.bind(this)), asyncMiddleware(this.deleteCustomer.bind(this)),
this.handlerServiceErrors this.handlerServiceErrors
); );
router.delete(
'/',
[...this.bulkContactsSchema],
this.validationResult,
asyncMiddleware(this.deleteBulkCustomers.bind(this)),
this.handlerServiceErrors
);
router.get( router.get(
'/', '/',
[...this.validateListQuerySchema], [...this.validateListQuerySchema],
@@ -263,32 +256,6 @@ export default class CustomersController extends ContactsController {
} }
} }
/**
* Deletes customers in bulk.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteBulkCustomers(req: Request, res: Response, next: NextFunction) {
const { ids: contactsIds } = req.query;
const { tenantId, user } = req;
try {
await this.customersService.deleteBulkCustomers(
tenantId,
contactsIds,
user
);
return res.status(200).send({
ids: contactsIds,
message: 'The customers have been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/** /**
* Retrieve customers paginated and filterable list. * Retrieve customers paginated and filterable list.
* @param {Request} req * @param {Request} req
@@ -350,21 +317,16 @@ export default class CustomersController extends ContactsController {
errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 200 }], errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 200 }],
}); });
} }
if (error.errorType === 'some_customers_have_invoices') {
return res.boom.badRequest(null, {
errors: [{ type: 'SOME.CUSTOMERS.HAVE.SALES_INVOICES', code: 300 }],
});
}
if (error.errorType === 'customer_has_invoices') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOMER.HAS.SALES_INVOICES', code: 400 }],
});
}
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') { if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }], errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
}); });
} }
if (error.errorType === 'CUSTOMER_HAS_TRANSACTIONS') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOMER_HAS_TRANSACTIONS', code: 600 }],
});
}
} }
next(error); next(error);
} }

View File

@@ -55,13 +55,6 @@ export default class VendorsController extends ContactsController {
asyncMiddleware(this.deleteVendor.bind(this)), asyncMiddleware(this.deleteVendor.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors,
); );
router.delete('/', [
...this.bulkContactsSchema,
],
this.validationResult,
asyncMiddleware(this.deleteBulkVendors.bind(this)),
this.handlerServiceErrors,
);
router.get('/:id', [ router.get('/:id', [
...this.specificContactSchema, ...this.specificContactSchema,
], ],
@@ -297,21 +290,16 @@ export default class VendorsController extends ContactsController {
errors: [{ type: 'VENDORS.NOT.FOUND', code: 200 }], errors: [{ type: 'VENDORS.NOT.FOUND', code: 200 }],
}); });
} }
if (error.errorType === 'some_vendors_have_bills') {
return res.boom.badRequest(null, {
errors: [{ type: 'SOME.VENDORS.HAVE.ASSOCIATED.BILLS', code: 300 }],
});
}
if (error.errorType === 'vendor_has_bills') {
return res.status(400).send({
errors: [{ type: 'VENDOR.HAS.ASSOCIATED.BILLS', code: 400 }],
});
}
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') { if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }], errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
}); });
} }
if (error.errorType === 'VENDOR_HAS_TRANSACTIONS') {
return res.boom.badRequest(null, {
errors: [{ type: 'VENDOR_HAS_TRANSACTIONS', code: 600 }],
});
}
} }
next(error); next(error);
} }

View File

@@ -56,3 +56,7 @@ export interface IBill {
export interface IBillsFilter extends IDynamicListFilterDTO { export interface IBillsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
} }
export interface IBillsService {
validateVendorHasNoBills(tenantId: number, vendorId: number): Promise<void>;
}

View File

@@ -46,3 +46,8 @@ export interface IBillReceivePageEntry {
currencyCode: string, currencyCode: string,
date: Date|string, date: Date|string,
}; };
export interface IBillPaymentsService {
validateVendorHasNoPayments(tenantId: number, vendorId): Promise<void>;
}

View File

@@ -1,71 +1,77 @@
import { IDynamicListFilterDTO } from './DynamicFilter';
import { IDynamicListFilterDTO } from "./DynamicFilter";
export interface IPaymentReceive { export interface IPaymentReceive {
id?: number, id?: number;
customerId: number, customerId: number;
paymentDate: Date, paymentDate: Date;
amount: number, amount: number;
referenceNo: string, referenceNo: string;
depositAccountId: number, depositAccountId: number;
paymentReceiveNo: string, paymentReceiveNo: string;
statement: string, statement: string;
entries: IPaymentReceiveEntry[], entries: IPaymentReceiveEntry[];
userId: number, userId: number;
}; }
export interface IPaymentReceiveCreateDTO { export interface IPaymentReceiveCreateDTO {
customerId: number, customerId: number;
paymentDate: Date, paymentDate: Date;
amount: number, amount: number;
referenceNo: string, referenceNo: string;
depositAccountId: number, depositAccountId: number;
paymentReceiveNo?: string, paymentReceiveNo?: string;
statement: string, statement: string;
entries: IPaymentReceiveEntryDTO[], entries: IPaymentReceiveEntryDTO[];
}; }
export interface IPaymentReceiveEditDTO { export interface IPaymentReceiveEditDTO {
customerId: number, customerId: number;
paymentDate: Date, paymentDate: Date;
amount: number, amount: number;
referenceNo: string, referenceNo: string;
depositAccountId: number, depositAccountId: number;
paymentReceiveNo?: string, paymentReceiveNo?: string;
statement: string, statement: string;
entries: IPaymentReceiveEntryDTO[], entries: IPaymentReceiveEntryDTO[];
}; }
export interface IPaymentReceiveEntry { export interface IPaymentReceiveEntry {
id?: number, id?: number;
paymentReceiveId: number, paymentReceiveId: number;
invoiceId: number, invoiceId: number;
paymentAmount: number, paymentAmount: number;
}; }
export interface IPaymentReceiveEntryDTO { export interface IPaymentReceiveEntryDTO {
id?: number, id?: number;
paymentReceiveId: number, paymentReceiveId: number;
invoiceId: number, invoiceId: number;
paymentAmount: number, paymentAmount: number;
}; }
export interface IPaymentReceivesFilter extends IDynamicListFilterDTO { export interface IPaymentReceivesFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string;
} }
export interface IPaymentReceivePageEntry { export interface IPaymentReceivePageEntry {
invoiceId: number, invoiceId: number;
entryType: string, entryType: string;
invoiceNo: string, invoiceNo: string;
dueAmount: number, dueAmount: number;
amount: number, amount: number;
totalPaymentAmount: number, totalPaymentAmount: number;
paymentAmount: number, paymentAmount: number;
currencyCode: string, currencyCode: string;
date: Date|string, date: Date | string;
}; }
export interface IPaymentReceiveEditPage { export interface IPaymentReceiveEditPage {
paymentReceive: IPaymentReceive, paymentReceive: IPaymentReceive;
entries: IPaymentReceivePageEntry[]; entries: IPaymentReceivePageEntry[];
}; }
export interface IPaymentsReceiveService {
validateCustomerHasNoPayments(
tenantId: number,
customerId: number
): Promise<void>;
}

View File

@@ -33,3 +33,11 @@ export interface ISaleEstimateDTO {
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO { export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
} }
export interface ISalesEstimatesService {
validateCustomerHasNoEstimates(
tenantId: number,
customerId: number,
): Promise<void>;
}

View File

@@ -1,44 +1,49 @@
import { IDynamicListFilter } from 'interfaces/DynamicFilter'; import { IDynamicListFilter } from 'interfaces/DynamicFilter';
import { IItemEntry, IItemEntryDTO } from "./ItemEntry"; import { IItemEntry, IItemEntryDTO } from './ItemEntry';
export interface ISaleInvoice { export interface ISaleInvoice {
id: number, id: number;
balance: number, balance: number;
paymentAmount: number, paymentAmount: number;
currencyCode: string, currencyCode: string;
invoiceDate: Date, invoiceDate: Date;
dueDate: Date, dueDate: Date;
dueAmount: number, dueAmount: number;
overdueDays: number, overdueDays: number;
customerId: number, customerId: number;
referenceNo: string, referenceNo: string;
invoiceNo: string, invoiceNo: string;
entries: IItemEntry[], entries: IItemEntry[];
deliveredAt: string | Date, deliveredAt: string | Date;
userId: number, userId: number;
} }
export interface ISaleInvoiceDTO { export interface ISaleInvoiceDTO {
invoiceDate: Date, invoiceDate: Date;
dueDate: Date, dueDate: Date;
referenceNo: string, referenceNo: string;
invoiceNo: string, invoiceNo: string;
customerId: number, customerId: number;
invoiceMessage: string, invoiceMessage: string;
termsConditions: string, termsConditions: string;
entries: IItemEntryDTO[], entries: IItemEntryDTO[];
delivered: boolean, delivered: boolean;
} }
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO { export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO {
fromEstimateId: number, fromEstimateId: number;
}; }
export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO { export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO {}
}; export interface ISalesInvoicesFilter extends IDynamicListFilter {
page: number;
pageSize: number;
}
export interface ISalesInvoicesFilter extends IDynamicListFilter{ export interface ISalesInvoicesService {
page: number, validateCustomerHasNoInvoices(
pageSize: number, tenantId: number,
}; customerId: number
): Promise<void>;
}

View File

@@ -10,7 +10,7 @@ export interface ISaleReceipt {
receiptMessage: string; receiptMessage: string;
receiptNumber: string; receiptNumber: string;
amount: number; amount: number;
currencyCode: string, currencyCode: string;
statement: string; statement: string;
closedAt: Date | string; closedAt: Date | string;
entries: any[]; entries: any[];
@@ -24,14 +24,14 @@ export interface ISaleReceiptDTO {
receiptDate: Date; receiptDate: Date;
sendToEmail: string; sendToEmail: string;
referenceNo?: string; referenceNo?: string;
receiptNumber?: string, receiptNumber?: string;
receiptMessage: string; receiptMessage: string;
statement: string; statement: string;
closed: boolean; closed: boolean;
entries: any[]; entries: any[];
} }
export interface ISalesReceiptService { export interface ISalesReceiptsService {
createSaleReceipt( createSaleReceipt(
tenantId: number, tenantId: number,
saleReceiptDTO: ISaleReceiptDTO saleReceiptDTO: ISaleReceiptDTO
@@ -49,4 +49,9 @@ export interface ISalesReceiptService {
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}>; }>;
validateCustomerHasNoReceipts(
tenantId: number,
customerId: number
): Promise<void>;
} }

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { omit, intersection, defaultTo } from 'lodash'; import { omit, defaultTo } from 'lodash';
import async from 'async';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -7,6 +8,7 @@ import {
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
import ContactsService from 'services/Contacts/ContactsService'; import ContactsService from 'services/Contacts/ContactsService';
import moment from 'moment';
import { import {
ICustomerNewDTO, ICustomerNewDTO,
ICustomerEditDTO, ICustomerEditDTO,
@@ -16,15 +18,20 @@ import {
IContactNewDTO, IContactNewDTO,
IContactEditDTO, IContactEditDTO,
IContact, IContact,
ISaleInvoice,
ISystemUser, ISystemUser,
ISalesInvoicesService,
ISalesReceiptsService,
ISalesEstimatesService,
IPaymentsReceiveService,
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
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 events from 'subscribers/events'; import events from 'subscribers/events';
import moment from 'moment';
const ERRORS = {
CUSTOMER_HAS_TRANSACTIONS: 'CUSTOMER_HAS_TRANSACTIONS',
};
@Service() @Service()
export default class CustomersService { export default class CustomersService {
@Inject() @Inject()
@@ -42,6 +49,18 @@ export default class CustomersService {
@EventDispatcher() @EventDispatcher()
eventDispatcher: EventDispatcherInterface; eventDispatcher: EventDispatcherInterface;
@Inject('SalesInvoices')
invoicesService: ISalesInvoicesService;
@Inject('SalesReceipts')
receiptsService: ISalesReceiptsService;
@Inject('PaymentReceives')
paymentsService: IPaymentsReceiveService;
@Inject('SalesEstimates')
estimatesService: ISalesEstimatesService;
/** /**
* Converts customer to contact DTO. * Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
@@ -158,6 +177,40 @@ export default class CustomersService {
return customer; 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. * Deletes the given customer from the storage.
* @param {number} tenantId * @param {number} tenantId
@@ -176,9 +229,10 @@ export default class CustomersService {
// Retrieve the customer of throw not found service error. // Retrieve the customer of throw not found service error.
await this.getCustomerByIdOrThrowError(tenantId, customerId); await this.getCustomerByIdOrThrowError(tenantId, customerId);
// Validate whether the customer has no assocaited invoices tranasctions. // Validate the customer associated relations.
await this.customerHasNoInvoicesOrThrowError(tenantId, customerId); await this.validateCustomerAssociatedRelations(tenantId, customerId);
// Delete the customer from the storage.
await this.contactService.deleteContact(tenantId, customerId, 'customer'); await this.contactService.deleteContact(tenantId, customerId, 'customer');
// Throws `onCustomerDeleted` event. // Throws `onCustomerDeleted` event.
@@ -225,12 +279,13 @@ export default class CustomersService {
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { Customer } = this.tenancy.models(tenantId); const { Customer } = this.tenancy.models(tenantId);
// Dynamic list.
const dynamicList = await this.dynamicListService.dynamicList( const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
Customer, Customer,
customersFilter customersFilter
); );
const { results, pagination } = await Customer.query() const { results, pagination } = await Customer.query()
.onBuild((query) => { .onBuild((query) => {
dynamicList.buildQuery()(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. * Changes the opening balance of the given customer.
* @param {number} tenantId * @param {number} tenantId

View File

@@ -15,12 +15,18 @@ import {
IPaginationMeta, IPaginationMeta,
IFilterMeta, IFilterMeta,
ISystemUser, ISystemUser,
IBillsService,
IBillPaymentsService,
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import events from 'subscribers/events'; import events from 'subscribers/events';
const ERRORS = {
VENDOR_HAS_TRANSACTIONS: 'VENDOR_HAS_TRANSACTIONS',
};
@Service() @Service()
export default class VendorsService { export default class VendorsService {
@Inject() @Inject()
@@ -38,6 +44,12 @@ export default class VendorsService {
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject('Bills')
billsService: IBillsService;
@Inject('BillPayments')
billPaymentsService: IBillPaymentsService;
/** /**
* Converts vendor to contact DTO. * Converts vendor to contact DTO.
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO * @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. * Deletes the given vendor from the storage.
* @param {number} tenantId * @param {number} tenantId
@@ -138,8 +171,8 @@ export default class VendorsService {
// Validate the vendor existance on the storage. // Validate the vendor existance on the storage.
await this.getVendorByIdOrThrowError(tenantId, vendorId); await this.getVendorByIdOrThrowError(tenantId, vendorId);
// Validate the vendor has no associated bills. // Validate associated vendor transactions.
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId); await this.validateAssociatedTransactions(tenantId, vendorId);
this.logger.info('[vendor] trying to delete vendor.', { this.logger.info('[vendor] trying to delete vendor.', {
tenantId, 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. * Retrieve vendors datatable list.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.

View File

@@ -33,8 +33,8 @@ import { ERRORS } from './constants';
* Bill payments service. * Bill payments service.
* @service * @service
*/ */
@Service() @Service('BillPayments')
export default class BillPaymentsService { export default class BillPaymentsService implements IBillPaymentsService {
@Inject() @Inject()
accountsService: AccountsService; accountsService: AccountsService;
@@ -178,7 +178,7 @@ export default class BillPaymentsService {
if (notOpenedBills.length > 0) { if (notOpenedBills.length > 0) {
throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, { throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, {
notOpenedBills notOpenedBills,
}); });
} }
} }
@@ -696,4 +696,19 @@ export default class BillPaymentsService {
); );
await Promise.all(opers); 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', BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND',
INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT', INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT',
PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', 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, IPaginationMeta,
IFilterMeta, IFilterMeta,
IBillsFilter, IBillsFilter,
IBillsService
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
@@ -34,8 +35,8 @@ import { ERRORS } from './constants';
* Vendor bills services. * Vendor bills services.
* @service * @service
*/ */
@Service() @Service('Bills')
export default class BillsService extends SalesInvoicesCost { export default class BillsService extends SalesInvoicesCost implements IBillsService {
@Inject() @Inject()
inventoryService: InventoryService; inventoryService: InventoryService;
@@ -144,10 +145,7 @@ export default class BillsService extends SalesInvoicesCost {
* @param {number} tenantId * @param {number} tenantId
* @param {number} billId - Bill id. * @param {number} billId - Bill id.
*/ */
private async validateBillHasNoEntries( private async validateBillHasNoEntries(tenantId, billId: number) {
tenantId,
billId: number,
) {
const { BillPaymentEntry } = this.tenancy.models(tenantId); const { BillPaymentEntry } = this.tenancy.models(tenantId);
// Retireve the bill associate payment made entries. // Retireve the bill associate payment made entries.
@@ -578,4 +576,22 @@ export default class BillsService extends SalesInvoicesCost {
'Bill' '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', NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN', BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED', 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, IPaymentReceiveEntry,
IPaymentReceiveEntryDTO, IPaymentReceiveEntryDTO,
IPaymentReceivesFilter, IPaymentReceivesFilter,
IPaymentsReceiveService,
ISaleInvoice, ISaleInvoice,
ISystemUser, ISystemUser,
} from 'interfaces'; } from 'interfaces';
@@ -37,8 +38,8 @@ import { ERRORS } from './constants';
* Payment receive service. * Payment receive service.
* @service * @service
*/ */
@Service() @Service('PaymentReceives')
export default class PaymentReceiveService { export default class PaymentReceiveService implements IPaymentsReceiveService {
@Inject() @Inject()
accountsService: AccountsService; accountsService: AccountsService;
@@ -522,7 +523,7 @@ export default class PaymentReceiveService {
public async deletePaymentReceive( public async deletePaymentReceive(
tenantId: number, tenantId: number,
paymentReceiveId: number, paymentReceiveId: number,
authorizedUser: ISystemUser, authorizedUser: ISystemUser
) { ) {
const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models( const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models(
tenantId tenantId
@@ -772,4 +773,24 @@ export default class PaymentReceiveService {
}); });
await Promise.all([...opers]); 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_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED',
PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED', PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED',
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', 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, IPaginationMeta,
ISaleEstimate, ISaleEstimate,
ISaleEstimateDTO, ISaleEstimateDTO,
ISalesEstimatesService,
} from 'interfaces'; } from 'interfaces';
import { import {
EventDispatcher, EventDispatcher,
@@ -32,14 +33,15 @@ const ERRORS = {
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED', SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED', SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED', SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
}; };
/** /**
* Sale estimate service. * Sale estimate service.
* @Service * @Service
*/ */
@Service() @Service('SalesEstimates')
export default class SaleEstimateService { export default class SaleEstimateService implements ISalesEstimatesService{
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@@ -174,7 +176,8 @@ export default class SaleEstimateService {
const autoNextNumber = this.getNextEstimateNumber(tenantId); const autoNextNumber = this.getNextEstimateNumber(tenantId);
// Retreive the next estimate number. // Retreive the next estimate number.
const estimateNumber = estimateDTO.estimateNumber || const estimateNumber =
estimateDTO.estimateNumber ||
oldSaleEstimate?.estimateNumber || oldSaleEstimate?.estimateNumber ||
autoNextNumber; autoNextNumber;
@@ -201,9 +204,9 @@ export default class SaleEstimateService {
})), })),
// Avoid rewrite the deliver date in edit mode when already published. // Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered && ...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && { !oldSaleEstimate?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(), deliveredAt: moment().toMySqlDateTime(),
}), }),
}; };
} }
@@ -233,10 +236,7 @@ export default class SaleEstimateService {
this.logger.info('[sale_estimate] inserting sale estimate to the storage.'); this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
// Transform DTO object ot model object. // Transform DTO object ot model object.
const estimateObj = await this.transformDTOToModel( const estimateObj = await this.transformDTOToModel(tenantId, estimateDTO);
tenantId,
estimateDTO
);
// Validate estimate number uniquiness on the storage. // Validate estimate number uniquiness on the storage.
await this.validateEstimateNumberExistance( await this.validateEstimateNumberExistance(
tenantId, tenantId,
@@ -583,4 +583,24 @@ export default class SaleEstimateService {
approvedAt: null, 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, ISystemUser,
IItem, IItem,
IItemEntry, IItemEntry,
ISalesInvoicesService
} from 'interfaces'; } from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
@@ -36,8 +37,8 @@ import { ERRORS } from './constants';
* Sales invoices service * Sales invoices service
* @service * @service
*/ */
@Service() @Service('SalesInvoices')
export default class SaleInvoicesService { export default class SaleInvoicesService implements ISalesInvoicesService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@@ -665,4 +666,22 @@ export default class SaleInvoicesService {
return salesInvoices; 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 events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; 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 JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import { IFilterMeta, IPaginationMeta } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService';
@@ -29,10 +36,11 @@ const ERRORS = {
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE', SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED', SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED', SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
}; };
@Service() @Service('SalesReceipts')
export default class SalesReceiptService { export default class SalesReceiptService implements ISalesReceiptsService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@@ -548,4 +556,22 @@ export default class SalesReceiptService {
'SaleReceipt' '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_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES: INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'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'
}; };