fix: delete sale invoice issue.

This commit is contained in:
a.bouhuolia
2021-01-02 11:59:20 +02:00
parent cdee2e5314
commit b5a849abda
3 changed files with 204 additions and 117 deletions

View File

@@ -201,27 +201,6 @@ export default class CustomersController extends ContactsController {
}
}
/**
* Deletes the given customer from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteCustomer(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: contactId } = req.params;
try {
await this.customersService.deleteCustomer(tenantId, contactId)
return res.status(200).send({
id: contactId,
message: 'The customer has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieve details of the given customer id.
* @param {Request} req
@@ -243,6 +222,28 @@ export default class CustomersController extends ContactsController {
}
}
/**
* Deletes the given customer from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteCustomer(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: contactId } = req.params;
try {
await this.customersService.deleteCustomer(tenantId, contactId);
return res.status(200).send({
id: contactId,
message: 'The customer has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Deletes customers in bulk.
* @param {Request} req

View File

@@ -4,10 +4,10 @@ import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
import ContactsService from 'services/Contacts/ContactsService';
import {
import {
ICustomerNewDTO,
ICustomerEditDTO,
ICustomer,
@@ -17,7 +17,7 @@ import {
IContactEditDTO,
IContact,
ISaleInvoice,
} from 'interfaces';
} from 'interfaces';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@@ -43,11 +43,11 @@ export default class CustomersService {
/**
* Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
* @returns {IContactDTO}
*/
private customerToContactDTO(
customerDTO: ICustomerNewDTO | ICustomerEditDTO,
customerDTO: ICustomerNewDTO | ICustomerEditDTO
): IContactNewDTO | IContactEditDTO {
return {
...omit(customerDTO, ['customerType']),
@@ -58,14 +58,17 @@ export default class CustomersService {
/**
* Transforms new customer DTO to contact.
* @param customerDTO
* @param customerDTO
*/
private transformNewCustomerDTO(customerDTO: ICustomerNewDTO): IContactNewDTO {
private transformNewCustomerDTO(
customerDTO: ICustomerNewDTO
): IContactNewDTO {
return {
...this.customerToContactDTO(customerDTO),
openingBalanceAt: customerDTO?.openingBalanceAt
? moment(customerDTO.openingBalanceAt).toMySqlDateTime() : null,
}
? moment(customerDTO.openingBalanceAt).toMySqlDateTime()
: null,
};
}
private transformContactToCustomer(contactModel: IContact) {
@@ -77,22 +80,34 @@ export default class CustomersService {
/**
* Creates a new customer.
* @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO
* @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO
* @return {Promise<ICustomer>}
*/
public async newCustomer(
tenantId: number,
customerDTO: ICustomerNewDTO
): Promise<ICustomer> {
this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO });
this.logger.info('[customer] trying to create a new customer.', {
tenantId,
customerDTO,
});
const customerObj = this.transformNewCustomerDTO(customerDTO);
const customer = await this.contactService.newContact(tenantId, customerObj, 'customer');
const customer = await this.contactService.newContact(
tenantId,
customerObj,
'customer'
);
this.logger.info('[customer] created successfully.', { tenantId, customerDTO });
this.logger.info('[customer] created successfully.', {
tenantId,
customerDTO,
});
await this.eventDispatcher.dispatch(events.customers.onCreated, {
customer, tenantId, customerId: customer.id,
customer,
tenantId,
customerId: customer.id,
});
return customer;
@@ -100,24 +115,35 @@ export default class CustomersService {
/**
* Edits details of the given customer.
* @param {number} tenantId
* @param {number} tenantId
* @param {number} customerId
* @param {ICustomerEditDTO} customerDTO
* @param {ICustomerEditDTO} customerDTO
* @return {Promise<ICustomer>}
*/
public async editCustomer(
tenantId: number,
customerId: number,
customerDTO: ICustomerEditDTO,
customerDTO: ICustomerEditDTO
): Promise<ICustomer> {
const contactDTO = this.customerToContactDTO(customerDTO);
this.logger.info('[customer] trying to edit customer.', { tenantId, customerId, customerDTO });
const customer = this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
this.logger.info('[customer] trying to edit customer.', {
tenantId,
customerId,
customerDTO,
});
const customer = this.contactService.editContact(
tenantId,
customerId,
contactDTO,
'customer'
);
this.eventDispatcher.dispatch(events.customers.onEdited);
this.logger.info('[customer] edited successfully.', {
tenantId, customerId, customer,
tenantId,
customerId,
customer,
});
return customer;
@@ -125,20 +151,35 @@ export default class CustomersService {
/**
* Deletes the given customer from the storage.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
* @return {Promise<void>}
*/
public async deleteCustomer(tenantId: number, customerId: number): Promise<void> {
this.logger.info('[customer] trying to delete customer.', { tenantId, customerId });
public async deleteCustomer(
tenantId: number,
customerId: number
): Promise<void> {
this.logger.info('[customer] trying to delete customer.', {
tenantId,
customerId,
});
// 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);
await this.contactService.deleteContact(tenantId, customerId, 'customer');
await this.eventDispatcher.dispatch(events.customers.onDeleted, { tenantId, customerId });
this.logger.info('[customer] deleted successfully.', { tenantId, customerId });
await this.eventDispatcher.dispatch(events.customers.onDeleted, {
tenantId,
customerId,
});
this.logger.info('[customer] deleted successfully.', {
tenantId,
customerId,
});
}
/**
@@ -147,22 +188,34 @@ export default class CustomersService {
* @param {number} customerId -
* @return {Promise<void>}
*/
public async revertOpeningBalanceEntries(tenantId: number, customerId: number|number[]) {
public async revertOpeningBalanceEntries(
tenantId: number,
customerId: number | number[]
) {
const id = Array.isArray(customerId) ? customerId : [customerId];
this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId });
this.logger.info(
'[customer] trying to revert opening balance journal entries.',
{ tenantId, customerId }
);
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, id, 'customer',
tenantId,
id,
'customer'
);
}
/**
* Retrieve the given customer details.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
*/
public async getCustomer(tenantId: number, customerId: number) {
const contact = await this.contactService.getContact(tenantId, customerId, 'customer');
const contact = await this.contactService.getContact(
tenantId,
customerId,
'customer'
);
return this.transformContactToCustomer(contact);
}
@@ -175,20 +228,23 @@ export default class CustomersService {
tenantId: number,
customersFilter: ICustomersFilter
): Promise<{
customers: ICustomer[],
pagination: IPaginationMeta,
filterMeta: IFilterMeta,
customers: ICustomer[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Customer } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Customer, customersFilter);
const { results, pagination } = await Customer.query().onBuild((query) => {
dynamicList.buildQuery()(query);
}).pagination(
customersFilter.page - 1,
customersFilter.pageSize,
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Customer,
customersFilter
);
const { results, pagination } = await Customer.query()
.onBuild((query) => {
dynamicList.buildQuery()(query);
})
.pagination(customersFilter.page - 1, customersFilter.pageSize);
return {
customers: results.map(this.transformContactToCustomer),
pagination,
@@ -198,49 +254,57 @@ export default class CustomersService {
/**
* Writes customer opening balance journal entries.
* @param {number} tenantId
* @param {number} customerId
* @param {number} openingBalance
* @param {number} tenantId
* @param {number} customerId
* @param {number} openingBalance
* @return {Promise<void>}
*/
public async writeCustomerOpeningBalanceJournal(
tenantId: number,
customerId: number,
openingBalance: number,
openingBalance: number
) {
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
await journalCommands.customerOpeningBalance(customerId, openingBalance)
await journalCommands.customerOpeningBalance(customerId, openingBalance);
await Promise.all([
journal.saveBalance(),
journal.saveEntries(),
]);
await Promise.all([journal.saveBalance(), journal.saveEntries()]);
}
/**
* Retrieve the given customer by id or throw not found.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
*/
public getCustomerByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer');
return this.contactService.getContactByIdOrThrowError(
tenantId,
customerId,
'customer'
);
}
/**
* Retrieve the given customers or throw error if one of them not found.
* @param {numebr} tenantId
* @param {numebr} tenantId
* @param {number[]} customersIds
*/
private getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) {
return this.contactService.getContactsOrThrowErrorNotFound(tenantId, customersIds, 'customer');
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
* @param {number} tenantId
* @param {number[]} customersIds
* @return {Promise<void>}
*/
public async deleteBulkCustomers(tenantId: number, customersIds: number[]) {
@@ -256,16 +320,21 @@ export default class CustomersService {
/**
* Validates the customer has no associated sales invoice
* or throw service error.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
* @throws {ServiceError}
* @return {Promise<void>}
*/
private async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) {
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 });
const salesInvoice = await saleInvoiceRepository.find({
customer_id: customerId,
});
if (salesInvoice.length > 0) {
throw new ServiceError('customer_has_invoices');
@@ -274,21 +343,29 @@ export default class CustomersService {
/**
* Throws error in case one of customers have associated sales invoices.
* @param {number} tenantId
* @param {number[]} customersIds
* @param {number} tenantId
* @param {number[]} customersIds
* @throws {ServiceError}
* @return {Promise<void>}
*/
private async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) {
private async customersHaveNoInvoicesOrThrowError(
tenantId: number,
customersIds: number[]
) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const customersInvoices = await saleInvoiceRepository.findWhereIn(
'customer_id', customersIds
'customer_id',
customersIds
);
const customersIdsWithInvoice = customersInvoices.map(
(saleInvoice: ISaleInvoice) => saleInvoice.customerId
);
const customersIdsWithInvoice = customersInvoices
.map((saleInvoice: ISaleInvoice) => saleInvoice.customerId);
const customersHaveInvoices = difference(customersIds, customersIdsWithInvoice);
const customersHaveInvoices = difference(
customersIds,
customersIdsWithInvoice
);
if (customersHaveInvoices.length > 0) {
throw new ServiceError('some_customers_have_invoices');
@@ -297,28 +374,33 @@ export default class CustomersService {
/**
* Changes the opening balance of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {number} openingBalance
* @param {string|Date} openingBalanceAt
* @param {number} tenantId
* @param {number} customerId
* @param {number} openingBalance
* @param {string|Date} openingBalanceAt
*/
public async changeOpeningBalance(
tenantId: number,
customerId: number,
openingBalance: number,
openingBalanceAt: Date|string,
openingBalanceAt: Date | string
) {
await this.contactService.changeOpeningBalance(
tenantId,
customerId,
'customer',
openingBalance,
openingBalanceAt,
openingBalanceAt
);
// Triggers `onOpeingBalanceChanged` event.
await this.eventDispatcher.dispatch(events.customers.onOpeningBalanceChanged, {
tenantId, customerId, openingBalance, openingBalanceAt
});
await this.eventDispatcher.dispatch(
events.customers.onOpeningBalanceChanged,
{
tenantId,
customerId,
openingBalance,
openingBalanceAt,
}
);
}
}

View File

@@ -24,7 +24,6 @@ import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import { PaymentReceiveEntry } from 'models';
const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
@@ -33,7 +32,8 @@ const ERRORS = {
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',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
};
/**
@@ -107,11 +107,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {Function} next
*/
async getInvoiceOrThrowError(tenantId: number, saleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries');
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const saleInvoice = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries'
);
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
@@ -223,7 +224,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
saleInvoiceId: number,
saleInvoiceDTO: any
): Promise<ISaleInvoice> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const { SaleInvoice } = this.tenancy.models(tenantId);
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
@@ -328,7 +329,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
// Retrieve the sale invoice associated payment receive entries.
const entries = await PaymentReceiveEntry.query().where(
'invoice_id',
saleInvoiceId,
saleInvoiceId
);
if (entries.length > 0) {
throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES);
@@ -346,7 +347,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
tenantId: number,
saleInvoiceId: number
): Promise<void> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const { ItemEntry } = this.tenancy.models(tenantId);
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given sale invoice with associated entries or throw not found error.
const oldSaleInvoice = await this.getInvoiceOrThrowError(
@@ -369,7 +371,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
);
this.logger.info('[sale_invoice] delete sale invoice with entries.');
await SaleInvoice.query().where('id', saleInvoiceId).delete();
await saleInvoiceRepository.deleteById(saleInvoiceId);
await ItemEntry.query()
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice')
@@ -453,7 +456,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
saleInvoiceId: number,
override: boolean = false
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries = await this.itemsEntriesService.getInventoryEntries(
@@ -464,9 +467,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
// Can't continue if the sale invoice has inventory items entries.
if (inventoryEntries.length > 0) return;
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries.item');
const saleInvoice = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries.item'
);
await this.writeNonInventoryInvoiceEntries(tenantId, saleInvoice, override);
}