From cd46ecb58dcc2b1da89a4c99e70d5cd1cf841ccf Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 21 Jun 2020 21:10:30 +0200 Subject: [PATCH] feat: Bulk delete customers and expenses. --- client/src/config/sidebarMenu.js | 2 +- server/src/http/controllers/Customers.js | 41 ++++++++++++++- server/src/http/controllers/Expenses.js | 64 +++++++++++++++++++++++- server/tests/routes/accounts.test.js | 4 +- server/tests/routes/customers.test.js | 38 ++++++++++++++ server/tests/routes/expenses.test.js | 38 ++++++++++++++ 6 files changed, 182 insertions(+), 5 deletions(-) diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js index 933f2d836..1dd66974d 100644 --- a/client/src/config/sidebarMenu.js +++ b/client/src/config/sidebarMenu.js @@ -55,7 +55,7 @@ export default [ children: [ { text: , - // href: '/', + href: '/customers', }, { text: , diff --git a/server/src/http/controllers/Customers.js b/server/src/http/controllers/Customers.js index c706a2d62..8c68f14b1 100644 --- a/server/src/http/controllers/Customers.js +++ b/server/src/http/controllers/Customers.js @@ -5,7 +5,7 @@ import { query, validationResult, } from 'express-validator'; -import { pick } from 'lodash'; +import { pick, difference } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import { mapViewRolesToConditionals, @@ -77,6 +77,10 @@ export default { this.deleteCustomer.validation, asyncMiddleware(this.deleteCustomer.handler)); + router.delete('/', + this.deleteBulkCustomers.validation, + asyncMiddleware(this.deleteBulkCustomers.handler)); + router.get('/', this.listCustomers.validation, asyncMiddleware(this.listCustomers.handler)); @@ -380,5 +384,40 @@ export default { return res.status(200).send(); }, + }, + + /** + * Bulk delete customers. + */ + deleteBulkCustomers: { + validation: [ + query('ids').isArray({ min: 2 }), + query('ids.*').isNumeric().toInt(), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const filter = { ...req.query }; + const { Customer } = req.models; + + const customers = await Customer.query().whereIn('id', filter.ids); + const storedCustomersIds = customers.map((customer) => customer.id); + + const notFoundCustomers = difference(filter.ids, storedCustomersIds); + + if (notFoundCustomers.length > 0) { + return res.status(404).send({ + errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 200 }], + }); + } + await Customer.query().whereIn('id', storedCustomersIds).delete(); + + return res.status(200).send({ ids: storedCustomersIds }); + } } }; diff --git a/server/src/http/controllers/Expenses.js b/server/src/http/controllers/Expenses.js index f83c173d8..6a0f68a5c 100644 --- a/server/src/http/controllers/Expenses.js +++ b/server/src/http/controllers/Expenses.js @@ -42,6 +42,10 @@ export default { this.deleteExpense.validation, asyncMiddleware(this.deleteExpense.handler)); + router.delete('/', + this.deleteBulkExpenses.validation, + asyncMiddleware(this.deleteBulkExpenses.handler)); + router.post('/:id', this.updateExpense.validation, asyncMiddleware(this.updateExpense.handler)); @@ -195,7 +199,7 @@ export default { }); } const { id } = req.params; - const { Expense, AccountTransaction } = req.models; + const { Expense, Account, AccountTransaction } = req.models; const expense = await Expense.query().findById(id); const errorReasons = []; @@ -624,4 +628,62 @@ export default { }); }, }, + + /** + * Deletes bulk expenses. + */ + deleteBulkExpenses: { + validation: [ + query('ids').isArray({ min: 1 }), + query('ids.*').isNumeric().toInt(), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const filter = { ...req.query }; + const { Expense, AccountTransaction, Account, MediaLink } = req.models; + + const expenses = await Expense.query() + .whereIn('id', filter.ids) + + const storedExpensesIds = expenses.map(e => e.id); + const notFoundExpenses = difference(filter.ids, storedExpensesIds); + + if (notFoundExpenses.length > 0) { + return res.status(404).send({ + errors: [{ type: 'EXPENSES.NOT.FOUND', code: 200 }], + }); + } + + const deleteExpensesOper = Expense.query() + .whereIn('id', storedExpensesIds).delete(); + + const transactions = await AccountTransaction.query() + .whereIn('reference_type', ['Expense']) + .whereIn('reference_id', filter.ids) + + const accountsDepGraph = await Account.depGraph().query().remember(); + const journal = new JournalPoster(accountsDepGraph); + + journal.loadEntries(transactions); + journal.removeEntries(); + + await MediaLink.query() + .where('model_name', 'Expense') + .whereIn('model_id', filter.ids) + .delete(); + + await Promise.all([ + deleteExpensesOper, + journal.deleteEntries(), + journal.saveBalance(), + ]); + return res.status(200).send({ ids: filter.ids }); + } + } }; diff --git a/server/tests/routes/accounts.test.js b/server/tests/routes/accounts.test.js index 3c45399ce..bcaa5c166 100644 --- a/server/tests/routes/accounts.test.js +++ b/server/tests/routes/accounts.test.js @@ -10,7 +10,7 @@ import { } from '~/dbInit'; -describe.only('routes: /accounts/', () => { +describe('routes: /accounts/', () => { describe('POST `/accounts`', () => { it('Should `name` be required.', async () => { const res = await request() @@ -190,7 +190,7 @@ describe.only('routes: /accounts/', () => { }); }); - it.only('Should response success with correct data form.', async () => { + it('Should response success with correct data form.', async () => { const account = await tenantFactory.create('account'); const res = await request() .post('/api/accounts') diff --git a/server/tests/routes/customers.test.js b/server/tests/routes/customers.test.js index 0fcd7d52e..ac7e724ba 100644 --- a/server/tests/routes/customers.test.js +++ b/server/tests/routes/customers.test.js @@ -209,4 +209,42 @@ describe('route: `/customers`', () => { expect(foundCustomer[0].displayName).equals('Ahmed Bouhuolia, Bigcapital'); }) }); + + describe('DELETE: `customers`', () => { + it('Should response customers ids not found.', async () => { + const res = await request() + .delete('/api/customers') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .query({ + ids: [100, 200], + }) + .send(); + + expect(res.status).equals(404); + expect(res.body.errors).include.something.deep.equals({ + type: 'CUSTOMERS.NOT.FOUND', code: 200, + }); + }); + + it('Should delete the given customers.', async () => { + const customer1 = await tenantFactory.create('customer'); + const customer2 = await tenantFactory.create('customer'); + + const res = await request() + .delete('/api/customers') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .query({ + ids: [customer1.id, customer2.id], + }) + .send(); + + const foundCustomers = await Customer.tenant().query() + .whereIn('id', [customer1.id, customer2.id]); + + expect(res.status).equals(200); + expect(foundCustomers.length).equals(0); + }); + }) }); diff --git a/server/tests/routes/expenses.test.js b/server/tests/routes/expenses.test.js index a6588a3cd..5ef1fbde8 100644 --- a/server/tests/routes/expenses.test.js +++ b/server/tests/routes/expenses.test.js @@ -678,4 +678,42 @@ describe('routes: /expenses/', () => { expect(foundExpenseCategory[0].amount).equals(3000); }); }); + + describe('DELETE: `/api/expenses`', () => { + it('Should response not found expenses ids.', async () => { + const res = await request() + .delete('/api/expenses') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .query({ + ids: [100, 200], + }) + .send({}); + + expect(res.status).equals(404); + expect(res.body.errors).include.something.deep.equals({ + type: 'EXPENSES.NOT.FOUND', code: 200, + }); + }); + + it('Should delete the given expenses ids.', async () => { + const expense1 = await tenantFactory.create('expense'); + const expense2 = await tenantFactory.create('expense'); + + const res = await request() + .delete('/api/expenses') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .query({ + ids: [expense1.id, expense2.id], + }) + .send({}); + + const foundExpenses = await Expense.tenant().query() + .whereIn('id', [expense1.id, expense2.id]); + + expect(res.status).equals(200); + expect(foundExpenses.length).equals(0); + }) + }); });