fix: delete customer with associated opening journal entries.

This commit is contained in:
a.bouhuolia
2021-01-02 13:17:43 +02:00
parent b5a849abda
commit db70d04743
6 changed files with 70 additions and 38 deletions

View File

@@ -212,7 +212,7 @@ export default class ContactsController extends BaseController {
*/ */
get bulkContactsSchema(): ValidationChain[] { get bulkContactsSchema(): ValidationChain[] {
return [ return [
query('ids').isArray({ min: 2 }), query('ids').isArray({ min: 1 }),
query('ids.*').isNumeric().toInt(), query('ids.*').isNumeric().toInt(),
]; ];
} }

View File

@@ -10,6 +10,7 @@ import Bill from 'models/Bill';
import BillPayment from 'models/BillPayment'; import BillPayment from 'models/BillPayment';
import BillPaymentEntry from 'models/BillPaymentEntry'; import BillPaymentEntry from 'models/BillPaymentEntry';
import Currency from 'models/Currency'; import Currency from 'models/Currency';
import Contact from 'models/Contact';
import Vendor from 'models/Vendor'; import Vendor from 'models/Vendor';
import Customer from 'models/Customer'; import Customer from 'models/Customer';
import ExchangeRate from 'models/ExchangeRate'; import ExchangeRate from 'models/ExchangeRate';
@@ -69,6 +70,7 @@ export default (knex) => {
MediaLink, MediaLink,
Vendor, Vendor,
Customer, Customer,
Contact,
}; };
return mapValues(models, (model) => model.bindKnex(knex)); return mapValues(models, (model) => model.bindKnex(knex));
} }

View File

@@ -182,14 +182,14 @@ export default class ContactsService {
async getContactsOrThrowErrorNotFound( async getContactsOrThrowErrorNotFound(
tenantId: number, tenantId: number,
contactsIds: number[], contactsIds: number[],
contactService: TContactService contactService: TContactService,
) { ) {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
const contacts = await Contact.query() const contacts = await Contact.query()
.whereIn('id', contactsIds) .whereIn('id', contactsIds)
.where('contact_service', contactService); .where('contact_service', contactService);
const storedContactsIds = contacts.map((contact: IContact) => contact.id);
const storedContactsIds = contacts.map((contact: IContact) => contact.id);
const notFoundCustomers = difference(contactsIds, storedContactsIds); const notFoundCustomers = difference(contactsIds, storedContactsIds);
if (notFoundCustomers.length > 0) { if (notFoundCustomers.length > 0) {
@@ -211,6 +211,8 @@ export default class ContactsService {
contactService: TContactService contactService: TContactService
) { ) {
const { contactRepository } = this.tenancy.repositories(tenantId); const { contactRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given contacts or throw not found service error.
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService); this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
await contactRepository.deleteWhereIdIn(contactsIds); await contactRepository.deleteWhereIdIn(contactsIds);
@@ -230,11 +232,12 @@ export default class ContactsService {
const { AccountTransaction } = this.tenancy.models(tenantId); const { AccountTransaction } = this.tenancy.models(tenantId);
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
// Loads the contact opening balance journal transactions.
const contactsTransactions = await AccountTransaction.query() const contactsTransactions = await AccountTransaction.query()
.whereIn('reference_id', contactsIds) .whereIn('reference_id', contactsIds)
.where('reference_type', `${upperFirst(contactService)}OpeningBalance`); .where('reference_type', `${upperFirst(contactService)}OpeningBalance`);
journal.loadEntries(contactsTransactions); journal.fromTransactions(contactsTransactions);
journal.removeEntries(); journal.removeEntries();
await Promise.all([journal.saveBalance(), journal.deleteEntries()]); await Promise.all([journal.saveBalance(), journal.deleteEntries()]);

View File

@@ -1,5 +1,5 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { omit, difference, defaultTo } from 'lodash'; import { omit, intersection, defaultTo } from 'lodash';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -71,6 +71,10 @@ export default class CustomersService {
}; };
} }
/**
* Transforms the contact model to customer model.
* @param {IContact} contactModel
*/
private transformContactToCustomer(contactModel: IContact) { private transformContactToCustomer(contactModel: IContact) {
return { return {
...omit(contactModel.toJSON(), ['contactService', 'contactType']), ...omit(contactModel.toJSON(), ['contactService', 'contactType']),
@@ -172,6 +176,7 @@ export default class CustomersService {
await this.contactService.deleteContact(tenantId, customerId, 'customer'); await this.contactService.deleteContact(tenantId, customerId, 'customer');
// Throws `onCustomerDeleted` event.
await this.eventDispatcher.dispatch(events.customers.onDeleted, { await this.eventDispatcher.dispatch(events.customers.onDeleted, {
tenantId, tenantId,
customerId, customerId,
@@ -182,29 +187,6 @@ export default class CustomersService {
}); });
} }
/**
* Reverts customer opening balance journal entries.
* @param {number} tenantId -
* @param {number} customerId -
* @return {Promise<void>}
*/
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 }
);
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId,
id,
'customer'
);
}
/** /**
* Retrieve the given customer details. * Retrieve the given customer details.
* @param {number} tenantId * @param {number} tenantId
@@ -262,16 +244,43 @@ export default class CustomersService {
public async writeCustomerOpeningBalanceJournal( public async writeCustomerOpeningBalanceJournal(
tenantId: number, tenantId: number,
customerId: number, customerId: number,
openingBalance: number openingBalance: number,
openingBalanceAt: Date | string
) { ) {
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
await journalCommands.customerOpeningBalance(customerId, openingBalance); await journalCommands.customerOpeningBalance(
customerId,
openingBalance,
openingBalanceAt
);
await Promise.all([journal.saveBalance(), journal.saveEntries()]); await Promise.all([journal.saveBalance(), journal.saveEntries()]);
} }
/**
* Reverts customer opening balance journal entries.
* @param {number} tenantId -
* @param {number} customerId -
* @return {Promise<void>}
*/
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 }
);
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId,
id,
'customer'
);
}
/** /**
* Retrieve the given customer by id or throw not found. * Retrieve the given customer by id or throw not found.
* @param {number} tenantId * @param {number} tenantId
@@ -310,11 +319,20 @@ export default class CustomersService {
public async deleteBulkCustomers(tenantId: number, customersIds: number[]) { public async deleteBulkCustomers(tenantId: number, customersIds: number[]) {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
// Validate the customers existance on the storage.
await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds); await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds);
// Validate the customers have no associated invoices.
await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds); await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds);
// Deletes the given customers.
await Contact.query().whereIn('id', customersIds).delete(); await Contact.query().whereIn('id', customersIds).delete();
await this.eventDispatcher.dispatch(events.customers.onBulkDeleted);
// Triggers `onCustomersBulkDeleted` event.
await this.eventDispatcher.dispatch(events.customers.onBulkDeleted, {
tenantId,
customersIds,
});
} }
/** /**
@@ -361,12 +379,10 @@ export default class CustomersService {
const customersIdsWithInvoice = customersInvoices.map( const customersIdsWithInvoice = customersInvoices.map(
(saleInvoice: ISaleInvoice) => saleInvoice.customerId (saleInvoice: ISaleInvoice) => saleInvoice.customerId
); );
const customersHaveInvoices = intersection(
const customersHaveInvoices = difference(
customersIds, customersIds,
customersIdsWithInvoice customersIdsWithInvoice
); );
if (customersHaveInvoices.length > 0) { if (customersHaveInvoices.length > 0) {
throw new ServiceError('some_customers_have_invoices'); throw new ServiceError('some_customers_have_invoices');
} }

View File

@@ -465,7 +465,7 @@ export default class ItemsService implements IItemsService {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
const storedItems = await Item.query().whereIn('id', itemsIDs); const storedItems = await Item.query().whereIn('id', itemsIDs);
const storedItemsIds = storedItems.map((t) => t.id); const storedItemsIds = storedItems.map((t: IItem) => t.id);
const notFoundItemsIds = difference(itemsIDs, storedItemsIds); const notFoundItemsIds = difference(itemsIDs, storedItemsIds);
return notFoundItemsIds; return notFoundItemsIds;
@@ -474,7 +474,7 @@ export default class ItemsService implements IItemsService {
/** /**
* Deletes items in bulk. * Deletes items in bulk.
* @param {number} tenantId * @param {number} tenantId
* @param {number[]} itemsIds * @param {number[]} itemsIds - Items ids.
*/ */
public async bulkDeleteItems(tenantId: number, itemsIds: number[]) { public async bulkDeleteItems(tenantId: number, itemsIds: number[]) {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);

View File

@@ -1,4 +1,4 @@
import { Container, Inject, Service } from 'typedi'; import { Container } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch'; import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events'; import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
@@ -15,6 +15,9 @@ export default class CustomersSubscriber {
this.customersService = Container.get(CustomersService); this.customersService = Container.get(CustomersService);
} }
/**
* Handles the writing opening balance journal entries once the customer created.
*/
@On(events.customers.onCreated) @On(events.customers.onCreated)
async handleWriteOpenBalanceEntries({ tenantId, customerId, customer }) { async handleWriteOpenBalanceEntries({ tenantId, customerId, customer }) {
@@ -24,10 +27,14 @@ export default class CustomersSubscriber {
tenantId, tenantId,
customer.id, customer.id,
customer.openingBalance, customer.openingBalance,
customer.openingBalanceAt,
); );
} }
} }
/**
* Handles the deleting opeing balance journal entrise once the customer deleted.
*/
@On(events.customers.onDeleted) @On(events.customers.onDeleted)
async handleRevertOpeningBalanceEntries({ tenantId, customerId }) { async handleRevertOpeningBalanceEntries({ tenantId, customerId }) {
@@ -36,6 +43,10 @@ export default class CustomersSubscriber {
); );
} }
/**
* Handles the deleting opening balance journal entries once the given
* customers deleted.
*/
@On(events.customers.onBulkDeleted) @On(events.customers.onBulkDeleted)
async handleBulkRevertOpeningBalanceEntries({ tenantId, customersIds }) { async handleBulkRevertOpeningBalanceEntries({ tenantId, customersIds }) {