diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index 96d3870b3..a3126f202 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -43,6 +43,15 @@ export default class AccountsController extends BaseController{ asyncMiddleware(this.inactivateAccount.bind(this)), this.catchServiceErrors, ); + router.post( + '/:id/close', [ + ...this.accountParamSchema, + ...this.closingAccountSchema, + ], + this.validationResult, + asyncMiddleware(this.closeAccount.bind(this)), + this.catchServiceErrors, + ) router.post( '/:id', [ ...this.accountDTOSchema, @@ -150,6 +159,13 @@ export default class AccountsController extends BaseController{ ]; } + get closingAccountSchema() { + return [ + check('to_account_id').exists().isNumeric().toInt(), + check('delete_after_closing').exists().isBoolean(), + ] + } + /** * Creates a new account. * @param {Request} req - @@ -330,6 +346,30 @@ export default class AccountsController extends BaseController{ } } + /** + * Closes the given account. + * @param {Request} req + * @param {Response} res + * @param next + */ + async closeAccount(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: accountId } = req.params; + const closeAccountQuery = this.matchedBodyData(req); + + try { + await this.accountsService.closeAccount( + tenantId, + accountId, + closeAccountQuery.toAccountId, + closeAccountQuery.deleteAfterClosing + ); + return res.status(200).send({ id: accountId }); + } catch (error) { + next(error); + } + } + /** * Transforms service errors to response. * @param {Error} @@ -411,6 +451,12 @@ export default class AccountsController extends BaseController{ { errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] } ); } + if (error.errorType === 'close_account_and_to_account_not_same_type') { + return res.boom.badRequest( + 'The close account has different root type with to account.', + { errors: [{ type: 'CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE', code: 1200 }] }, + ); + } } next(error) } diff --git a/server/src/repositories/AccountRepository.ts b/server/src/repositories/AccountRepository.ts index a036ea4f9..d8b0ba3c6 100644 --- a/server/src/repositories/AccountRepository.ts +++ b/server/src/repositories/AccountRepository.ts @@ -61,7 +61,7 @@ export default class AccountRepository extends TenantRepository { * @param {number} id - Account id. * @return {IAccount} */ - getById(id: number): IAccount { + findById(id: number): IAccount { const { Account } = this.models; return this.cache.get(`accounts.id.${id}`, () => { return Account.query().findById(id); @@ -93,10 +93,12 @@ export default class AccountRepository extends TenantRepository { * Inserts a new accounts to the storage. * @param {IAccount} account */ - async insert(account: IAccount): Promise { + async insert(accountInput: IAccount): Promise { const { Account } = this.models; - await Account.query().insertAndFetch({ ...account }); + const account = await Account.query().insertAndFetch({ ...accountInput }); this.flushCache(); + + return account; } /** @@ -121,6 +123,20 @@ export default class AccountRepository extends TenantRepository { this.flushCache(); } + /** + * Changes account balance. + * @param {number} accountId + * @param {number} amount + * @return {Promise} + */ + async balanceChange(accountId: number, amount: number): Promise { + const { Account } = this.models; + const method: string = (amount < 0) ? 'decrement' : 'increment'; + + await Account.query().where('id', accountId)[method]('amount', amount); + this.flushCache(); + } + /** * Flush repository cache. */ diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 4571218ba..2c5635073 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -10,6 +10,9 @@ import { } from 'decorators/eventDispatcher'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; +import JournalPoster from 'services/Accounting/JournalPoster'; +import { Account } from 'models'; +import AccountRepository from 'repositories/AccountRepository'; @Service() export default class AccountsService { @@ -475,4 +478,54 @@ export default class AccountsService { filterMeta: dynamicList.getResponseMeta(), }; } + + /** + * Closes the given account. + * ----------- + * Precedures. + * ----------- + * - Transfer the given account transactions to another account + * with the same root type. + * - Delete the given account. + * ------- + * @param {number} tenantId - + * @param {number} accountId - + * @param {number} toAccountId - + * @param {boolean} deleteAfterClosing - + */ + public async closeAccount( + tenantId: number, + accountId: number, + toAccountId: number, + deleteAfterClosing: boolean, + ) { + this.logger.info('[account] trying to close account.', { tenantId, accountId, toAccountId, deleteAfterClosing }); + + const { AccountTransaction } = this.tenancy.models(tenantId); + const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + + const account = await this.getAccountOrThrowError(tenantId, accountId); + const toAccount = await this.getAccountOrThrowError(tenantId, toAccountId); + + this.throwErrorIfAccountPredefined(account); + + const accountType = await accountTypeRepository.getTypeMeta(account.accountTypeId); + const toAccountType = await accountTypeRepository.getTypeMeta(toAccount.accountTypeId); + + if (accountType.rootType !== toAccountType.rootType) { + throw new ServiceError('close_account_and_to_account_not_same_type'); + } + const updateAccountBalanceOper = await accountRepository.balanceChange(accountId, account.balance || 0); + + // Move transactiosn operation. + const moveTransactionsOper = await AccountTransaction.query() + .where('account_id', accountId) + .patch({ accountId: toAccountId }); + + await Promise.all([ moveTransactionsOper, updateAccountBalanceOper ]); + + if (deleteAfterClosing) { + await accountRepository.deleteById(accountId); + } + } }