Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,24 @@ public interface ChargeRepository extends JpaRepository<Charge, Long>, JpaSpecif

@Query("select lc.id from WorkingCapitalLoanCharge lc where lc.charge.id = :chargeId and lc.active = true")
Optional<Long> isAnyWorkingCapitalLoansAssociateWithThisCharge(@Param("chargeId") Long chargeId);

/**
* Checks if any Charge exists that references a TaxGroup containing the specified TaxComponent. This is used to
* determine if a TaxComponent is "in use" (linked to charges via tax groups).
*
* @param taxComponentId
* the ID of the TaxComponent to check
* @return true if at least one Charge exists with a TaxGroup that contains this TaxComponent, false otherwise
*/
@Query(value = """
SELECT EXISTS (
SELECT 1
FROM m_charge c
INNER JOIN m_tax_group_mappings tgm
ON tgm.tax_group_id = c.tax_group_id
WHERE c.tax_group_id IS NOT NULL
AND tgm.tax_component_id = ?1
)
""", nativeQuery = true)
boolean existsByTaxGroupContainingTaxComponent(Long taxComponentId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,30 @@ public final class TaxComponentData implements Serializable {
private final LocalDate startDate;
private final Collection<TaxComponentHistoryData> taxComponentHistories;

// editability indicator
private final Boolean accountsEditable;

// template options
private final Map<String, List<GLAccountData>> glAccountOptions;
private final Collection<EnumOptionData> glAccountTypeOptions;

public static TaxComponentData instance(final Long id, final String name, final BigDecimal percentage,
final EnumOptionData debitAccountType, final GLAccountData debitAccount, final EnumOptionData creditAccountType,
final GLAccountData creditAccount, final LocalDate startDate, final Collection<TaxComponentHistoryData> taxComponentHistories) {
final Boolean accountsEditable = null;
final Map<String, List<GLAccountData>> glAccountOptions = null;
final Collection<EnumOptionData> glAccountTypeOptions = null;
return new TaxComponentData(id, name, percentage, debitAccountType, debitAccount, creditAccountType, creditAccount, startDate,
taxComponentHistories, glAccountOptions, glAccountTypeOptions);
taxComponentHistories, accountsEditable, glAccountOptions, glAccountTypeOptions);
}

public static TaxComponentData instance(final Long id, final String name, final BigDecimal percentage,
final EnumOptionData debitAccountType, final GLAccountData debitAccount, final EnumOptionData creditAccountType,
final GLAccountData creditAccount, final LocalDate startDate, final Collection<TaxComponentHistoryData> taxComponentHistories,
final Boolean accountsEditable, final Map<String, List<GLAccountData>> glAccountOptions,
final Collection<EnumOptionData> glAccountTypeOptions) {
return new TaxComponentData(id, name, percentage, debitAccountType, debitAccount, creditAccountType, creditAccount, startDate,
taxComponentHistories, accountsEditable, glAccountOptions, glAccountTypeOptions);
}

public static TaxComponentData lookup(final Long id, final String name) {
Expand All @@ -65,10 +78,11 @@ public static TaxComponentData lookup(final Long id, final String name) {
final GLAccountData creditAccount = null;
final LocalDate startDate = null;
final Collection<TaxComponentHistoryData> taxComponentHistories = null;
final Boolean accountsEditable = null;
final Map<String, List<GLAccountData>> glAccountOptions = null;
final Collection<EnumOptionData> glAccountTypeOptions = null;
return new TaxComponentData(id, name, percentage, debitAccountType, debitAccount, creditAccountType, creditAccount, startDate,
taxComponentHistories, glAccountOptions, glAccountTypeOptions);
taxComponentHistories, accountsEditable, glAccountOptions, glAccountTypeOptions);
}

public static TaxComponentData template(final Map<String, List<GLAccountData>> glAccountOptions,
Expand All @@ -82,8 +96,9 @@ public static TaxComponentData template(final Map<String, List<GLAccountData>> g
final GLAccountData creditAccount = null;
final LocalDate startDate = null;
final Collection<TaxComponentHistoryData> taxComponentHistories = null;
final Boolean accountsEditable = null;
return new TaxComponentData(id, name, percentage, debitAccountType, debitAccount, creditAccountType, creditAccount, startDate,
taxComponentHistories, glAccountOptions, glAccountTypeOptions);
taxComponentHistories, accountsEditable, glAccountOptions, glAccountTypeOptions);
}

private TaxComponentData(final Long id, final BigDecimal percentage, final GLAccountData debitAccount,
Expand All @@ -97,6 +112,7 @@ private TaxComponentData(final Long id, final BigDecimal percentage, final GLAcc
this.creditAccount = creditAccount;
this.startDate = null;
this.taxComponentHistories = null;
this.accountsEditable = null;
this.glAccountOptions = null;
this.glAccountTypeOptions = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@

import java.util.Collection;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.charge.domain.ChargeRepository;
import org.apache.fineract.portfolio.tax.data.TaxComponentData;
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.apache.fineract.portfolio.tax.domain.TaxComponent;
import org.apache.fineract.portfolio.tax.domain.TaxComponentRepository;
import org.apache.fineract.portfolio.tax.domain.TaxComponentRepositoryWrapper;
import org.apache.fineract.portfolio.tax.domain.TaxGroupRepository;
Expand All @@ -41,6 +46,7 @@ public class TaxReadPlatformServiceImpl implements TaxReadPlatformService {
private final TaxGroupRepository taxGroupRepository;
private final TaxGroupRepositoryWrapper taxGroupRepositoryWrapper;
private final TaxGroupMapper taxGroupMapper;
private final ChargeRepository chargeRepository;

@Override
public List<TaxComponentData> retrieveAllTaxComponents() {
Expand All @@ -49,7 +55,25 @@ public List<TaxComponentData> retrieveAllTaxComponents() {

@Override
public TaxComponentData retrieveTaxComponentData(final Long id) {
return taxComponentMapper.map(taxComponentRepositoryWrapper.findOneWithNotFoundDetection(id));
final TaxComponent taxComponent = taxComponentRepositoryWrapper.findOneWithNotFoundDetection(id);
final TaxComponentData result = taxComponentMapper.map(taxComponent);

// Check if accounts are editable (component is not in use)
final boolean accountsEditable = !taxComponent
.isInUse(() -> chargeRepository.existsByTaxGroupContainingTaxComponent(taxComponent.getId()));

// Conditionally include account options if editable
Map<String, List<GLAccountData>> glAccountOptions = null;
Collection<EnumOptionData> glAccountTypeOptions = null;
if (accountsEditable) {
glAccountOptions = accountingDropdownReadPlatformService.retrieveAccountMappingOptions();
glAccountTypeOptions = accountingDropdownReadPlatformService.retrieveGLAccountTypeOptions();
}

// Return enhanced data with accountsEditable and conditional options
return TaxComponentData.instance(result.getId(), result.getName(), result.getPercentage(), result.getDebitAccountType(),
result.getDebitAccount(), result.getCreditAccountType(), result.getCreditAccount(), result.getStartDate(),
result.getTaxComponentHistories(), accountsEditable, glAccountOptions, glAccountTypeOptions);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@
*/
package org.apache.fineract.portfolio.tax.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.charge.domain.ChargeRepository;
import org.apache.fineract.portfolio.tax.api.TaxApiConstants;
import org.apache.fineract.portfolio.tax.domain.TaxComponent;
import org.apache.fineract.portfolio.tax.domain.TaxComponentRepository;
import org.apache.fineract.portfolio.tax.domain.TaxComponentRepositoryWrapper;
Expand All @@ -42,6 +52,8 @@ public class TaxWritePlatformServiceImpl implements TaxWritePlatformService {
private final TaxComponentRepositoryWrapper taxComponentRepositoryWrapper;
private final TaxGroupRepository taxGroupRepository;
private final TaxGroupRepositoryWrapper taxGroupRepositoryWrapper;
private final ChargeRepository chargeRepository;
private final GLAccountRepositoryWrapper glAccountRepositoryWrapper;

@Override
public CommandProcessingResult createTaxComponent(final JsonCommand command) {
Expand All @@ -59,7 +71,50 @@ public CommandProcessingResult updateTaxComponent(final Long id, final JsonComma
this.validator.validateForTaxComponentUpdate(command.json());
final TaxComponent taxComponent = this.taxComponentRepositoryWrapper.findOneWithNotFoundDetection(id);
this.validator.validateStartDate(taxComponent.startDate(), command);
Map<String, Object> changes = taxComponent.update(command);

// Enforce edit restrictions based on usage
// Check if tax component is in use (linked to charges via tax groups)
final boolean inUse = taxComponent.isInUse(() -> chargeRepository.existsByTaxGroupContainingTaxComponent(taxComponent.getId()));
if (inUse) {
validateRestrictedFieldsForInUseComponent(command);
}

// Load GLAccounts and convert account types if provided in the command
GLAccountType debitAccountType = null;
GLAccount debitAccount = null;
GLAccountType creditAccountType = null;
GLAccount creditAccount = null;

if (command.parameterExists(TaxApiConstants.debitAccountTypeParamName)) {
final Integer debitAccountTypeValue = command.integerValueSansLocaleOfParameterNamed(TaxApiConstants.debitAccountTypeParamName);
if (debitAccountTypeValue != null) {
debitAccountType = GLAccountType.fromInt(debitAccountTypeValue);
}
}

if (command.parameterExists(TaxApiConstants.debitAccountIdParamName)) {
final Long debitAccountId = command.longValueOfParameterNamed(TaxApiConstants.debitAccountIdParamName);
if (debitAccountId != null) {
debitAccount = this.glAccountRepositoryWrapper.findOneWithNotFoundDetection(debitAccountId);
}
}

if (command.parameterExists(TaxApiConstants.creditAccountTypeParamName)) {
final Integer creditAccountTypeValue = command
.integerValueSansLocaleOfParameterNamed(TaxApiConstants.creditAccountTypeParamName);
if (creditAccountTypeValue != null) {
creditAccountType = GLAccountType.fromInt(creditAccountTypeValue);
}
}

if (command.parameterExists(TaxApiConstants.creditAccountIdParamName)) {
final Long creditAccountId = command.longValueOfParameterNamed(TaxApiConstants.creditAccountIdParamName);
if (creditAccountId != null) {
creditAccount = this.glAccountRepositoryWrapper.findOneWithNotFoundDetection(creditAccountId);
}
}

Map<String, Object> changes = taxComponent.update(command, debitAccountType, debitAccount, creditAccountType, creditAccount);
this.validator.validateTaxComponentForUpdate(taxComponent);
this.taxComponentRepository.saveAndFlush(taxComponent);
return new CommandProcessingResultBuilder() //
Expand All @@ -68,6 +123,62 @@ public CommandProcessingResult updateTaxComponent(final Long id, final JsonComma
.build();
}

/**
* Validates that restricted fields are not being modified when tax component is in use. When a tax component is
* linked to tax groups (and potentially used in transactions), only the name field can be modified. All other
* fields (percentage, GL accounts, start date) must remain unchanged to maintain accounting integrity.
*
* @param command
* the JSON command containing update parameters
* @throws PlatformApiDataValidationException
* if any restricted field is present in the update request
*/
private void validateRestrictedFieldsForInUseComponent(final JsonCommand command) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("tax.component");

// Check if any restricted fields are being modified
if (command.parameterExists(TaxApiConstants.percentageParamName)) {
baseDataValidator.reset().parameter(TaxApiConstants.percentageParamName).failWithCode(
"only.name.can.be.modified.once.tax.component.is.linked.or.used.in.transactions",
"Only name can be modified once tax component is linked or used in transactions.");
}

if (command.parameterExists(TaxApiConstants.startDateParamName)) {
baseDataValidator.reset().parameter(TaxApiConstants.startDateParamName).failWithCode(
"only.name.can.be.modified.once.tax.component.is.linked.or.used.in.transactions",
"Only name can be modified once tax component is linked or used in transactions.");
}

if (command.parameterExists(TaxApiConstants.debitAccountTypeParamName)) {
baseDataValidator.reset().parameter(TaxApiConstants.debitAccountTypeParamName).failWithCode(
"only.name.can.be.modified.once.tax.component.is.linked.or.used.in.transactions",
"Only name can be modified once tax component is linked or used in transactions.");
}

if (command.parameterExists(TaxApiConstants.debitAccountIdParamName)) {
baseDataValidator.reset().parameter(TaxApiConstants.debitAccountIdParamName).failWithCode(
"only.name.can.be.modified.once.tax.component.is.linked.or.used.in.transactions",
"Only name can be modified once tax component is linked or used in transactions.");
}

if (command.parameterExists(TaxApiConstants.creditAccountTypeParamName)) {
baseDataValidator.reset().parameter(TaxApiConstants.creditAccountTypeParamName).failWithCode(
"only.name.can.be.modified.once.tax.component.is.linked.or.used.in.transactions",
"Only name can be modified once tax component is linked or used in transactions.");
}

if (command.parameterExists(TaxApiConstants.creditAccountIdParamName)) {
baseDataValidator.reset().parameter(TaxApiConstants.creditAccountIdParamName).failWithCode(
"only.name.can.be.modified.once.tax.component.is.linked.or.used.in.transactions",
"Only name can be modified once tax component is linked or used in transactions.");
}

if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}

@Override
public CommandProcessingResult createTaxGroup(final JsonCommand command) {
this.validator.validateForTaxGroupCreate(command.json());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.charge.domain.ChargeRepository;
import org.apache.fineract.portfolio.tax.domain.TaxComponentRepository;
import org.apache.fineract.portfolio.tax.domain.TaxComponentRepositoryWrapper;
import org.apache.fineract.portfolio.tax.domain.TaxGroupRepository;
Expand Down Expand Up @@ -52,17 +53,19 @@ public TaxAssembler taxAssembler(FromJsonHelper fromApiJsonHelper, GLAccountRepo
public TaxReadPlatformService taxReadPlatformService(final TaxComponentRepository taxComponentRepository,
final TaxComponentRepositoryWrapper taxComponentRepositoryWrapper, final TaxComponentMapper taxComponentMapper,
final TaxGroupRepository taxGroupRepository, final TaxGroupRepositoryWrapper taxGroupRepositoryWrapper,
final TaxGroupMapper taxGroupMapper, AccountingDropdownReadPlatformService accountingDropdownReadPlatformService) {
final TaxGroupMapper taxGroupMapper, AccountingDropdownReadPlatformService accountingDropdownReadPlatformService,
final ChargeRepository chargeRepository) {
return new TaxReadPlatformServiceImpl(accountingDropdownReadPlatformService, taxComponentRepository, taxComponentRepositoryWrapper,
taxComponentMapper, taxGroupRepository, taxGroupRepositoryWrapper, taxGroupMapper);
taxComponentMapper, taxGroupRepository, taxGroupRepositoryWrapper, taxGroupMapper, chargeRepository);
}

@Bean
@ConditionalOnMissingBean(TaxWritePlatformService.class)
public TaxWritePlatformService taxWritePlatformService(TaxValidator validator, TaxAssembler taxAssembler,
TaxComponentRepository taxComponentRepository, TaxGroupRepository taxGroupRepository,
TaxComponentRepositoryWrapper taxComponentRepositoryWrapper, TaxGroupRepositoryWrapper taxGroupRepositoryWrapper) {
TaxComponentRepositoryWrapper taxComponentRepositoryWrapper, TaxGroupRepositoryWrapper taxGroupRepositoryWrapper,
final ChargeRepository chargeRepository, final GLAccountRepositoryWrapper glAccountRepositoryWrapper) {
return new TaxWritePlatformServiceImpl(validator, taxAssembler, taxComponentRepository, taxComponentRepositoryWrapper,
taxGroupRepository, taxGroupRepositoryWrapper);
taxGroupRepository, taxGroupRepositoryWrapper, chargeRepository, glAccountRepositoryWrapper);
}
}
Loading
Loading