diff --git a/server/src/http/controllers/Accounting.js b/server/src/http/controllers/Accounting.js index 8925028ef..dc7165a69 100644 --- a/server/src/http/controllers/Accounting.js +++ b/server/src/http/controllers/Accounting.js @@ -37,6 +37,10 @@ export default { this.makeJournalEntries.validation, asyncMiddleware(this.makeJournalEntries.handler)); + router.post('/manual-journals/:id/publish', + this.publishManualJournal.validation, + asyncMiddleware(this.publishManualJournal.handler)); + router.post('/manual-journals/:id', this.editManualJournal.validation, asyncMiddleware(this.editManualJournal.handler)); @@ -143,6 +147,7 @@ export default { check('transaction_type').optional({ nullable: true }).trim().escape(), check('reference').optional({ nullable: true }), check('description').optional().trim().escape(), + check('status').optional().isBoolean().toBoolean(), check('entries').isArray({ min: 2 }), check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(), @@ -163,7 +168,6 @@ export default { reference: '', ...req.body, }; - let totalCredit = 0; let totalDebit = 0; @@ -180,7 +184,6 @@ export default { totalDebit += entry.debit; } }); - if (totalCredit <= 0 || totalDebit <= 0) { errorReasons.push({ type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO', @@ -209,7 +212,6 @@ export default { if (errorReasons.length > 0) { return res.status(400).send({ errors: errorReasons }); } - // Save manual journal transaction. const manualJournal = await ManualJournal.query().insert({ reference: form.reference, @@ -218,13 +220,13 @@ export default { amount: totalCredit, date: formattedDate, description: form.description, + status: form.status, user_id: user.id, }); const journalPoster = new JournalPoster(); entries.forEach((entry) => { const account = accounts.find((a) => a.id === entry.account_id); - const jouranlEntry = new JournalEntry({ debit: entry.debit, credit: entry.credit, @@ -235,6 +237,7 @@ export default { note: entry.note, date: formattedDate, userId: user.id, + draft: !form.status, }); if (entry.debit) { journalPoster.debit(jouranlEntry); @@ -246,7 +249,7 @@ export default { // Saves the journal entries and accounts balance changes. await Promise.all([ journalPoster.saveEntries(), - journalPoster.saveBalance(), + (form.status) && journalPoster.saveBalance(), ]); return res.status(200).send({ id: manualJournal.id }); }, @@ -409,6 +412,53 @@ export default { }, }, + publishManualJournal: { + validation: [ + param('id').exists().isNumeric().toInt(), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const { id } = req.params; + const manualJournal = await ManualJournal.query() + .where('id', id).first(); + + if (!manualJournal) { + return res.status(404).send({ + errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }], + }); + } + if (!manualJournal.status) { + return res.status(400).send({ + errors: [{ type: 'MANUAL.JOURNAL.PUBLISHED.ALREADY', code: 200 }], + }); + } + const transactions = await AccountTransaction.query() + .whereIn('reference_type', ['Journal', 'ManualJournal']) + .where('reference_id', manualJournal.id) + .withGraphFetched('account.type'); + + const journal = new JournalPoster(); + journal.loadEntries(transactions); + journal.calculateEntriesBalanceChange(); + + const updateAccountsTransactionsOper = AccountTransaction.query() + .whereIn('id', transactions.map((t) => t.id)) + .update({ draft: 0 }); + + await Promise.all([ + updateAccountsTransactionsOper, + journal.saveBalance(), + ]); + return res.status(200).send({ id }); + }, + }, + getManualJournal: { validation: [ param('id').exists().isNumeric().toInt(), diff --git a/server/src/services/Accounting/JournalPoster.js b/server/src/services/Accounting/JournalPoster.js index 07a53affc..b74086408 100644 --- a/server/src/services/Accounting/JournalPoster.js +++ b/server/src/services/Accounting/JournalPoster.js @@ -255,6 +255,17 @@ export default class JournalPoster { }); } + calculateEntriesBalanceChange() { + this.entries.forEach((entry) => { + if (entry.credit) { + this.setAccountBalanceChange(entry, 'credit'); + } + if (entry.debit) { + this.setAccountBalanceChange(entry, 'debit'); + } + }); + } + static loadAccounts() { } diff --git a/server/tests/routes/accounting.test.js b/server/tests/routes/accounting.test.js index abb47b890..43c8d3b53 100644 --- a/server/tests/routes/accounting.test.js +++ b/server/tests/routes/accounting.test.js @@ -560,7 +560,7 @@ describe('routes: `/accounting`', () => { }); }); - describe.only('route: GET `accounting/manual-journals/:id`', () => { + describe('route: GET `accounting/manual-journals/:id`', () => { it('Should response not found in case manual transaction id was not exists.', async () => { const res = await request() .delete('/api/accounting/manual-journals/100') @@ -608,6 +608,64 @@ describe('routes: `/accounting`', () => { }); }); + describe.only('route: POST `accounting/manual-journals/:id/publish`', () => { + + it('Should response not found in case the manual journal id was not exists.', async () => { + const manualJournal = await create('manual_journal'); + + const res = await request() + .post('/api/accounting/manual-journals/123/publish') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(404); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100, + }); + }); + + it('Should response published ready.', async () => { + const manualJournal = await create('manual_journal', { status: 0 }); + const res = await request() + .post(`/api/accounting/manual-journals/${manualJournal.id}/publish`) + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'MANUAL.JOURNAL.PUBLISHED.ALREADY', code: 200, + }); + }); + + it('Should update all accounts transactions to not draft.', async () => { + const manualJournal = await create('manual_journal'); + const transaction = await create('account_transaction', { + reference_type: 'Journal', + reference_id: manualJournal.id, + draft: 1, + }); + const transaction2 = await create('account_transaction', { + reference_type: 'Journal', + reference_id: manualJournal.id, + draft: 1, + }); + const res = await request() + .post(`/api/accounting/manual-journals/${manualJournal.id}/publish`) + .set('x-access-token', loginRes.body.token) + .send(); + + const foundTransactions = await AccountTransaction.query() + .whereIn('id', [transaction.id, transaction2.id]); + + expect(foundTransactions[0].draft).equals(0); + expect(foundTransactions[1].draft).equals(0); + }); + + it('Should increment/decrement accounts balance.', () => { + + }); + }); + describe('route: `/accounting/quick-journal-entries`', async () => { it('Shoud `credit_account_id` be required', () => {