FINERACT-2181: Moneyhelper multitenant configuration fix, rework it to be a utility class

This commit is contained in:
Attila Budai
2025-07-21 18:03:33 +02:00
committed by Adam Saghy
parent ecbefb04c4
commit 27bf6d05d5
7 changed files with 727 additions and 48 deletions
@@ -30,8 +30,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty;
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper;
import org.apache.fineract.infrastructure.configuration.service.MoneyHelperInitializationService;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@@ -44,6 +46,7 @@ import org.springframework.stereotype.Component;
public class InternalConfigurationsApiResource implements InitializingBean {
private final GlobalConfigurationRepositoryWrapper repository;
private final MoneyHelperInitializationService moneyHelperInitializationService;
@Override
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
@@ -75,7 +78,14 @@ public class InternalConfigurationsApiResource implements InitializingBean {
repository.save(config);
log.warn("Config {} updated to {}", config.getName(), config.getValue());
repository.removeFromCache(config.getName());
MoneyHelper.fetchRoundingModeFromGlobalConfig();
// Update MoneyHelper when rounding mode configuration changes
if (GlobalConfigurationConstants.ROUNDING_MODE.equals(configName) && configValue != null) {
FineractPlatformTenant currentTenant = ThreadLocalContextUtil.getTenant();
if (currentTenant != null) {
moneyHelperInitializationService.initializeTenantRoundingMode(currentTenant);
}
}
return Response.status(Response.Status.OK).build();
}
@@ -0,0 +1,115 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.configuration.service;
import java.math.RoundingMode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty;
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.springframework.stereotype.Service;
/**
* Service to initialize MoneyHelper configurations for multi-tenant environments. This service bridges the gap between
* global configuration and MoneyHelper's tenant-specific caching.
*
* Note: MoneyHelper rounding mode is immutable once initialized to maintain data integrity. Updates require application
* restart to take effect.
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class MoneyHelperInitializationService {
private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository;
/**
* Initialize MoneyHelper for a specific tenant. This method should be called during tenant setup and whenever
* rounding mode configuration changes.
*
* @param tenant
* the tenant to initialize
*/
public void initializeTenantRoundingMode(FineractPlatformTenant tenant) {
if (tenant == null) {
throw new IllegalArgumentException("Tenant cannot be null");
}
String tenantIdentifier = tenant.getTenantIdentifier();
try {
// Set tenant context to read configuration
ThreadLocalContextUtil.setTenant(tenant);
// Get rounding mode from configuration with fallback to default
int roundingModeValue = getRoundingModeFromConfiguration();
// Initialize MoneyHelper for this tenant
MoneyHelper.initializeTenantRoundingMode(tenantIdentifier, roundingModeValue);
log.info("MoneyHelper initialized for tenant '{}' with rounding mode: {}", tenantIdentifier, roundingModeValue);
} catch (Exception e) {
log.error("Failed to initialize MoneyHelper for tenant '{}'", tenantIdentifier, e);
throw new RuntimeException("Failed to initialize MoneyHelper for tenant: " + tenantIdentifier, e);
} finally {
// Clear tenant context
ThreadLocalContextUtil.clearTenant();
}
}
/**
* Check if MoneyHelper is initialized for a tenant.
*
* @param tenantIdentifier
* the tenant identifier
* @return true if initialized, false otherwise
*/
public boolean isTenantInitialized(String tenantIdentifier) {
return MoneyHelper.isTenantInitialized(tenantIdentifier);
}
/**
* Get the rounding mode from configuration with fallback to default. This method safely handles cases where
* configuration might not be available.
*
* @return the rounding mode value
*/
private int getRoundingModeFromConfiguration() {
try {
GlobalConfigurationProperty roundingModeProperty = globalConfigurationRepository
.findOneByNameWithNotFoundDetection(GlobalConfigurationConstants.ROUNDING_MODE);
if (roundingModeProperty != null && roundingModeProperty.getValue() != null) {
return roundingModeProperty.getValue().intValue();
}
} catch (Exception e) {
log.warn("Failed to read rounding mode from configuration: {}", e.getMessage());
}
// Default to HALF_UP if configuration is not available
int defaultRoundingMode = RoundingMode.HALF_UP.ordinal();
log.info("Using default rounding mode: {} (HALF_UP)", defaultRoundingMode);
return defaultRoundingMode;
}
}
@@ -0,0 +1,96 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.configuration.service;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
/**
* Service to initialize MoneyHelper for all tenants during application startup. This service runs after the application
* is fully started to ensure all database migrations and tenant configurations are complete.
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class MoneyHelperStartupInitializationService {
private final TenantDetailsService tenantDetailsService;
private final MoneyHelperInitializationService moneyHelperInitializationService;
/**
* Initialize MoneyHelper for all tenants after the application is ready. This method runs after
* ApplicationReadyEvent to ensure all database migrations and tenant configurations are complete.
*/
@EventListener(ApplicationReadyEvent.class)
@Order(1000) // Run after other startup processes
public void initializeMoneyHelperForAllTenants() {
log.info("Starting MoneyHelper initialization for all tenants...");
try {
List<FineractPlatformTenant> tenants = tenantDetailsService.findAllTenants();
if (tenants.isEmpty()) {
log.warn("No tenants found during MoneyHelper initialization");
return;
}
int successCount = 0;
int failureCount = 0;
for (FineractPlatformTenant tenant : tenants) {
try {
String tenantIdentifier = tenant.getTenantIdentifier();
// Check if already initialized (in case of restart scenarios)
if (moneyHelperInitializationService.isTenantInitialized(tenantIdentifier)) {
log.debug("MoneyHelper already initialized for tenant: {}", tenantIdentifier);
successCount++;
continue;
}
// Initialize MoneyHelper for this tenant
moneyHelperInitializationService.initializeTenantRoundingMode(tenant);
successCount++;
} catch (Exception e) {
failureCount++;
log.error("Failed to initialize MoneyHelper for tenant '{}'", tenant.getTenantIdentifier(), e);
}
}
log.info("MoneyHelper initialization completed - Success: {}, Failures: {}, Total: {}", successCount, failureCount,
tenants.size());
if (failureCount > 0) {
log.warn("Some tenants failed MoneyHelper initialization. "
+ "These tenants may experience issues with rounding mode configuration.");
}
} catch (Exception e) {
log.error("Critical error during MoneyHelper initialization for all tenants", e);
}
}
}