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 <noreply@anthropic.com>
This commit is contained in:
Ahmed Bouhuolia
2026-02-25 19:27:53 +02:00
parent b5d1a2c9d0
commit d35915b16b
8 changed files with 94 additions and 14 deletions

View File

@@ -21,6 +21,7 @@ import { AccountsExportable } from './AccountsExportable.service';
import { AccountsImportable } from './AccountsImportable.service'; import { AccountsImportable } from './AccountsImportable.service';
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service'; import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service'; import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
import { AccountsSettingsService } from './AccountsSettings.service';
const models = [RegisterTenancyModel(BankAccount)]; const models = [RegisterTenancyModel(BankAccount)];
@@ -29,6 +30,7 @@ const models = [RegisterTenancyModel(BankAccount)];
controllers: [AccountsController], controllers: [AccountsController],
providers: [ providers: [
AccountsApplication, AccountsApplication,
AccountsSettingsService,
CreateAccountService, CreateAccountService,
TenancyContext, TenancyContext,
CommandAccountValidators, CommandAccountValidators,
@@ -49,9 +51,10 @@ const models = [RegisterTenancyModel(BankAccount)];
exports: [ exports: [
AccountRepository, AccountRepository,
CreateAccountService, CreateAccountService,
AccountsSettingsService,
...models, ...models,
AccountsExportable, AccountsExportable,
AccountsImportable AccountsImportable,
], ],
}) })
export class AccountsModule {} export class AccountsModule {}

View File

@@ -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<IAccountsSettings> {
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,
),
};
}
}

View File

@@ -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. * Validates the account name uniquiness.
* @param {string} accountName - Account name. * @param {string} accountName - Account name.

View File

@@ -15,6 +15,7 @@ import { events } from '@/common/events/events';
import { CreateAccountDTO } from './CreateAccount.dto'; import { CreateAccountDTO } from './CreateAccount.dto';
import { PartialModelObject } from 'objection'; import { PartialModelObject } from 'objection';
import { TenantModelProxy } from '../System/models/TenantBaseModel'; import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { AccountsSettingsService } from './AccountsSettings.service';
@Injectable() @Injectable()
export class CreateAccountService { export class CreateAccountService {
@@ -32,6 +33,7 @@ export class CreateAccountService {
private readonly uow: UnitOfWork, private readonly uow: UnitOfWork,
private readonly validator: CommandAccountValidators, private readonly validator: CommandAccountValidators,
private readonly tenancyContext: TenancyContext, private readonly tenancyContext: TenancyContext,
private readonly accountsSettings: AccountsSettingsService,
) {} ) {}
/** /**
@@ -43,14 +45,21 @@ export class CreateAccountService {
baseCurrency: string, baseCurrency: string,
params?: CreateAccountParams, 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. // Validate account name uniquiness.
if (!params.ignoreUniqueName) { if (!params.ignoreUniqueName) {
await this.validator.validateAccountNameUniquiness(accountDTO.name); 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. // Retrieve the account type meta or throw service error if not found.
this.validator.getAccountTypeOrThrowError(accountDTO.accountType); this.validator.getAccountTypeOrThrowError(accountDTO.accountType);

View File

@@ -7,6 +7,7 @@ import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { EditAccountDTO } from './EditAccount.dto'; import { EditAccountDTO } from './EditAccount.dto';
import { TenantModelProxy } from '../System/models/TenantBaseModel'; import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { AccountsSettingsService } from './AccountsSettings.service';
@Injectable() @Injectable()
export class EditAccount { export class EditAccount {
@@ -17,7 +18,8 @@ export class EditAccount {
@Inject(Account.name) @Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>, private readonly accountModel: TenantModelProxy<typeof Account>,
) { } private readonly accountsSettings: AccountsSettingsService,
) {}
/** /**
* Authorize the account editing. * Authorize the account editing.
@@ -30,6 +32,24 @@ export class EditAccount {
accountDTO: EditAccountDTO, accountDTO: EditAccountDTO,
oldAccount: Account, 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. // Validate account name uniquiness.
await this.validator.validateAccountNameUniquiness( await this.validator.validateAccountNameUniquiness(
accountDTO.name, accountDTO.name,
@@ -40,13 +60,6 @@ export class EditAccount {
oldAccount, oldAccount,
accountDTO, 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. // Retrieve the parent account of throw not found service error.
if (accountDTO.parentAccountId) { if (accountDTO.parentAccountId) {
const parentAccount = await this.validator.getParentAccountOrThrowError( const parentAccount = await this.validator.getParentAccountOrThrowError(

View File

@@ -3,6 +3,7 @@ export const ERRORS = {
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found', ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found', PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique', ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_CODE_REQUIRED: 'account_code_required',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue', ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type', PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed', ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',

View File

@@ -15,10 +15,16 @@ export const AccountDialogAction = {
*/ */
export const transformApiErrors = (errors) => { export const transformApiErrors = (errors) => {
const fields = {}; 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')) { if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) {
fields.code = intl.get('account_code_is_not_unique'); 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'); fields.name = intl.get('account_name_is_already_used');
} }
if ( if (

View File

@@ -462,6 +462,7 @@
"should_total_of_credit_and_debit_be_equal": "Should total of credit and debit be equal.", "should_total_of_credit_and_debit_be_equal": "Should total of credit and debit be equal.",
"no_accounts": "No Accounts", "no_accounts": "No Accounts",
"the_accounts_have_been_successfully_inactivated": "The accounts have been successfully inactivated.", "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.", "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?", "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?", "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?",