From d35915b16b8b5d7d0f63c6c85e74b2fc0771f58e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 25 Feb 2026 19:27:53 +0200 Subject: [PATCH] feat(accounts): add account settings service - Add AccountsSettingsService for managing account-related settings - Update validators, create and edit services to use settings - Add constants for account configuration - Update frontend utils and translations Co-Authored-By: Claude Sonnet 4.6 --- .../src/modules/Accounts/Accounts.module.ts | 5 ++- .../Accounts/AccountsSettings.service.ts | 33 +++++++++++++++++++ .../CommandAccountValidators.service.ts | 14 ++++++++ .../modules/Accounts/CreateAccount.service.ts | 17 +++++++--- .../modules/Accounts/EditAccount.service.ts | 29 +++++++++++----- .../server/src/modules/Accounts/constants.ts | 1 + .../Dialogs/AccountDialog/utils.tsx | 8 ++++- packages/webapp/src/lang/en/index.json | 1 + 8 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 packages/server/src/modules/Accounts/AccountsSettings.service.ts diff --git a/packages/server/src/modules/Accounts/Accounts.module.ts b/packages/server/src/modules/Accounts/Accounts.module.ts index 85f0c6488..b43e72f8b 100644 --- a/packages/server/src/modules/Accounts/Accounts.module.ts +++ b/packages/server/src/modules/Accounts/Accounts.module.ts @@ -21,6 +21,7 @@ import { AccountsExportable } from './AccountsExportable.service'; import { AccountsImportable } from './AccountsImportable.service'; import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service'; import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service'; +import { AccountsSettingsService } from './AccountsSettings.service'; const models = [RegisterTenancyModel(BankAccount)]; @@ -29,6 +30,7 @@ const models = [RegisterTenancyModel(BankAccount)]; controllers: [AccountsController], providers: [ AccountsApplication, + AccountsSettingsService, CreateAccountService, TenancyContext, CommandAccountValidators, @@ -49,9 +51,10 @@ const models = [RegisterTenancyModel(BankAccount)]; exports: [ AccountRepository, CreateAccountService, + AccountsSettingsService, ...models, AccountsExportable, - AccountsImportable + AccountsImportable, ], }) export class AccountsModule {} diff --git a/packages/server/src/modules/Accounts/AccountsSettings.service.ts b/packages/server/src/modules/Accounts/AccountsSettings.service.ts new file mode 100644 index 000000000..da81bb2bc --- /dev/null +++ b/packages/server/src/modules/Accounts/AccountsSettings.service.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { SettingsStore } from '../Settings/SettingsStore'; +import { SETTINGS_PROVIDER } from '../Settings/Settings.types'; + +export interface IAccountsSettings { + accountCodeRequired: boolean; + accountCodeUnique: boolean; +} + +@Injectable() +export class AccountsSettingsService { + constructor( + @Inject(SETTINGS_PROVIDER) + private readonly settingsStore: () => SettingsStore, + ) {} + + /** + * Retrieves account settings (account code required, account code unique). + */ + public async getAccountsSettings(): Promise { + const settingsStore = await this.settingsStore(); + return { + accountCodeRequired: settingsStore.get( + { group: 'accounts', key: 'account_code_required' }, + false, + ), + accountCodeUnique: settingsStore.get( + { group: 'accounts', key: 'account_code_unique' }, + true, + ), + }; + } +} diff --git a/packages/server/src/modules/Accounts/CommandAccountValidators.service.ts b/packages/server/src/modules/Accounts/CommandAccountValidators.service.ts index ef6cdf3f8..f1499ac45 100644 --- a/packages/server/src/modules/Accounts/CommandAccountValidators.service.ts +++ b/packages/server/src/modules/Accounts/CommandAccountValidators.service.ts @@ -106,6 +106,20 @@ export class CommandAccountValidators { } } + /** + * Throws error if account code is missing or blank when required. + * @param {string|undefined} code - Account code. + */ + public validateAccountCodeRequiredOrThrow(code: string | undefined) { + const trimmed = typeof code === 'string' ? code.trim() : ''; + if (!trimmed) { + throw new ServiceError( + ERRORS.ACCOUNT_CODE_REQUIRED, + 'Account code is required.', + ); + } + } + /** * Validates the account name uniquiness. * @param {string} accountName - Account name. diff --git a/packages/server/src/modules/Accounts/CreateAccount.service.ts b/packages/server/src/modules/Accounts/CreateAccount.service.ts index c435bdbd4..ef4f7028f 100644 --- a/packages/server/src/modules/Accounts/CreateAccount.service.ts +++ b/packages/server/src/modules/Accounts/CreateAccount.service.ts @@ -15,6 +15,7 @@ import { events } from '@/common/events/events'; import { CreateAccountDTO } from './CreateAccount.dto'; import { PartialModelObject } from 'objection'; import { TenantModelProxy } from '../System/models/TenantBaseModel'; +import { AccountsSettingsService } from './AccountsSettings.service'; @Injectable() export class CreateAccountService { @@ -32,6 +33,7 @@ export class CreateAccountService { private readonly uow: UnitOfWork, private readonly validator: CommandAccountValidators, private readonly tenancyContext: TenancyContext, + private readonly accountsSettings: AccountsSettingsService, ) {} /** @@ -43,14 +45,21 @@ export class CreateAccountService { baseCurrency: string, params?: CreateAccountParams, ) => { + const { accountCodeRequired, accountCodeUnique } = + await this.accountsSettings.getAccountsSettings(); + + // Validate account code required when setting is enabled. + if (accountCodeRequired) { + this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code); + } + // Validate the account code uniquiness when setting is enabled. + if (accountCodeUnique && accountDTO.code?.trim()) { + await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code); + } // Validate account name uniquiness. if (!params.ignoreUniqueName) { await this.validator.validateAccountNameUniquiness(accountDTO.name); } - // Validate the account code uniquiness. - if (accountDTO.code) { - await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code); - } // Retrieve the account type meta or throw service error if not found. this.validator.getAccountTypeOrThrowError(accountDTO.accountType); diff --git a/packages/server/src/modules/Accounts/EditAccount.service.ts b/packages/server/src/modules/Accounts/EditAccount.service.ts index 22c0d6bcb..536b9ef87 100644 --- a/packages/server/src/modules/Accounts/EditAccount.service.ts +++ b/packages/server/src/modules/Accounts/EditAccount.service.ts @@ -7,6 +7,7 @@ import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service'; import { events } from '@/common/events/events'; import { EditAccountDTO } from './EditAccount.dto'; import { TenantModelProxy } from '../System/models/TenantBaseModel'; +import { AccountsSettingsService } from './AccountsSettings.service'; @Injectable() export class EditAccount { @@ -17,7 +18,8 @@ export class EditAccount { @Inject(Account.name) private readonly accountModel: TenantModelProxy, - ) { } + private readonly accountsSettings: AccountsSettingsService, + ) {} /** * Authorize the account editing. @@ -30,6 +32,24 @@ export class EditAccount { accountDTO: EditAccountDTO, oldAccount: Account, ) => { + const { accountCodeRequired, accountCodeUnique } = + await this.accountsSettings.getAccountsSettings(); + + // Validate account code required when setting is enabled. + if (accountCodeRequired) { + this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code); + } + // Validate the account code uniquiness when setting is enabled. + if ( + accountCodeUnique && + accountDTO.code?.trim() && + accountDTO.code !== oldAccount.code + ) { + await this.validator.isAccountCodeUniqueOrThrowError( + accountDTO.code, + oldAccount.id, + ); + } // Validate account name uniquiness. await this.validator.validateAccountNameUniquiness( accountDTO.name, @@ -40,13 +60,6 @@ export class EditAccount { oldAccount, accountDTO, ); - // Validate the account code not exists on the storage. - if (accountDTO.code && accountDTO.code !== oldAccount.code) { - await this.validator.isAccountCodeUniqueOrThrowError( - accountDTO.code, - oldAccount.id, - ); - } // Retrieve the parent account of throw not found service error. if (accountDTO.parentAccountId) { const parentAccount = await this.validator.getParentAccountOrThrowError( diff --git a/packages/server/src/modules/Accounts/constants.ts b/packages/server/src/modules/Accounts/constants.ts index c06fe95df..080e7b9dc 100644 --- a/packages/server/src/modules/Accounts/constants.ts +++ b/packages/server/src/modules/Accounts/constants.ts @@ -3,6 +3,7 @@ export const ERRORS = { ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found', PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found', ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique', + ACCOUNT_CODE_REQUIRED: 'account_code_required', ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue', PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type', ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed', diff --git a/packages/webapp/src/containers/Dialogs/AccountDialog/utils.tsx b/packages/webapp/src/containers/Dialogs/AccountDialog/utils.tsx index 3bb43df16..852e3f18d 100644 --- a/packages/webapp/src/containers/Dialogs/AccountDialog/utils.tsx +++ b/packages/webapp/src/containers/Dialogs/AccountDialog/utils.tsx @@ -15,10 +15,16 @@ export const AccountDialogAction = { */ export const transformApiErrors = (errors) => { const fields = {}; + if (errors.find((e) => e.type === 'account_code_required')) { + fields.code = intl.get('account_code_is_required'); + } if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) { fields.code = intl.get('account_code_is_not_unique'); } - if (errors.find((e) => e.type === 'ACCOUNT.NAME.NOT.UNIQUE')) { + if (errors.find((e) => e.type === 'account_code_not_unique')) { + fields.code = intl.get('account_code_is_not_unique'); + } + if (errors.find((e) => e.type === 'account_name_not_unqiue')) { fields.name = intl.get('account_name_is_already_used'); } if ( diff --git a/packages/webapp/src/lang/en/index.json b/packages/webapp/src/lang/en/index.json index d29309048..32c45611f 100644 --- a/packages/webapp/src/lang/en/index.json +++ b/packages/webapp/src/lang/en/index.json @@ -462,6 +462,7 @@ "should_total_of_credit_and_debit_be_equal": "Should total of credit and debit be equal.", "no_accounts": "No Accounts", "the_accounts_have_been_successfully_inactivated": "The accounts have been successfully inactivated.", + "account_code_is_required": "Account code is required.", "account_code_is_not_unique": "Account code is not unique.", "are_sure_to_publish_this_expense": "Are you sure you want to publish this expense?", "once_delete_these_journals_you_will_not_able_restore_them": "Once you delete these journals, you won't be able to retrieve them later. Are you sure you want to delete them?",