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);
+ })
+ });
});