feat(savings): facility charge mapping admin API, enforce mapped-charge validation, INR-only currency, GL chart backfill
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Adds the admin read/write API for m_facility_charge_mapping (list, template, update) so facilities can be pointed at a real charge instead of relying on the seeder's implicit auto-link, and makes enable-on-account paths (activation and edit-approval, both savings and current accounts) throw instead of silently skipping the charge when a facility isn't mapped or is inactive. Migrations 0497-0509 back this up: reset all facility mappings to unmapped/inactive by default, backfill lost roles/permissions/charges, enforce INR as the only organisation currency, backfill the full real-world chart of accounts (44 core + 60 priority GL heads) with standardized gl_codes, and retire the unused system-defined payment types (Share Redemption Transfer and friends) in favor of Cash/Transfer only, matching the teller dashboard's simplified payment-type flow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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.portfolio.savings.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* One row of {@code m_facility_charge_mapping}: which {@link org.apache.fineract.portfolio.charge.domain.Charge}
|
||||
* is applied when a named account facility (ATM_FACILITY, UPI_FACILITY, ...) is enabled. {@link #chargeOptions}
|
||||
* is populated only on the template response, for the admin screen's "change the mapped charge" dropdown.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class FacilityChargeMappingData {
|
||||
|
||||
private Long id;
|
||||
private String facilityType;
|
||||
private Long chargeId;
|
||||
private String chargeName;
|
||||
private BigDecimal defaultAmount;
|
||||
private Integer chargeTimeType;
|
||||
private Integer chargeAppliesTo;
|
||||
private Boolean isActive;
|
||||
|
||||
/**
|
||||
* False when the mapped charge has since been deleted or deactivated (and true whenever there is simply no
|
||||
* charge mapped at all). {@link #chargeName} still shows whatever name was cached at mapping time either
|
||||
* way, so the admin screen needs this flag to tell "correctly mapped" apart from "was mapped, now broken".
|
||||
*/
|
||||
private Boolean chargeAvailable;
|
||||
|
||||
private List<FacilityChargeOptionData> chargeOptions;
|
||||
|
||||
public FacilityChargeMappingData() {}
|
||||
|
||||
public FacilityChargeMappingData(final Long id, final String facilityType, final Long chargeId, final String chargeName,
|
||||
final BigDecimal defaultAmount, final Integer chargeTimeType, final Integer chargeAppliesTo, final Boolean isActive,
|
||||
final Boolean chargeAvailable) {
|
||||
this.id = id;
|
||||
this.facilityType = facilityType;
|
||||
this.chargeId = chargeId;
|
||||
this.chargeName = chargeName;
|
||||
this.defaultAmount = defaultAmount;
|
||||
this.chargeTimeType = chargeTimeType;
|
||||
this.chargeAppliesTo = chargeAppliesTo;
|
||||
this.isActive = isActive;
|
||||
this.chargeAvailable = chargeAvailable;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FacilityChargeOptionData {
|
||||
|
||||
private final Long id;
|
||||
private final String name;
|
||||
private final BigDecimal amount;
|
||||
|
||||
public FacilityChargeOptionData(final Long id, final String name, final BigDecimal amount) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.portfolio.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class FacilityChargeMappingNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public FacilityChargeMappingNotFoundException(final Long id) {
|
||||
super("error.msg.facilitychargemapping.id.invalid", "Facility charge mapping with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+49
-29
@@ -25,6 +25,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
@@ -117,18 +118,13 @@ public class FacilityChargeAutoApplyService {
|
||||
public List<SavingsAccountCharge> applyChargesForFacilities(final SavingsAccount account, final java.util.Set<String> facilityTypes) {
|
||||
final List<SavingsAccountCharge> chargesToApply = new ArrayList<>();
|
||||
for (final String facilityType : facilityTypes) {
|
||||
mappingRepository.findByFacilityType(facilityType).ifPresent(mapping -> {
|
||||
final Charge charge = mapping.getCharge();
|
||||
if (charge == null) {
|
||||
return;
|
||||
}
|
||||
final MonthDay feeOnMonthDay = resolveFeeOnMonthDay(charge);
|
||||
final SavingsAccountCharge accountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount(charge, charge.getAmount(),
|
||||
null, null, resolveDueDate(charge), true, feeOnMonthDay, charge.getFeeInterval());
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
chargesToApply.add(accountCharge);
|
||||
});
|
||||
final Charge charge = requireMappedCharge(facilityType);
|
||||
final MonthDay feeOnMonthDay = resolveFeeOnMonthDay(charge);
|
||||
final SavingsAccountCharge accountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount(charge, charge.getAmount(),
|
||||
null, null, resolveDueDate(charge), true, feeOnMonthDay, charge.getFeeInterval());
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
chargesToApply.add(accountCharge);
|
||||
}
|
||||
if (!chargesToApply.isEmpty()) {
|
||||
savingsAccountChargeRepository.saveAll(chargesToApply);
|
||||
@@ -219,25 +215,49 @@ public class FacilityChargeAutoApplyService {
|
||||
&& c.getCharge().getName().equals(getChargeName(facilityType)));
|
||||
if (alreadyApplied) return;
|
||||
|
||||
mappingRepository.findByFacilityType(facilityType).ifPresent(mapping -> {
|
||||
final Charge charge = mapping.getCharge();
|
||||
if (charge == null) return;
|
||||
final Charge charge = requireMappedCharge(facilityType);
|
||||
|
||||
final MonthDay feeOnMonthDay = resolveFeeOnMonthDay(charge);
|
||||
final SavingsAccountCharge accountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount(
|
||||
charge,
|
||||
charge.getAmount(),
|
||||
null, // chargeTime derived from Charge definition
|
||||
null, // chargeCalculation derived from Charge definition
|
||||
resolveDueDate(charge),
|
||||
true,
|
||||
feeOnMonthDay,
|
||||
charge.getFeeInterval());
|
||||
final MonthDay feeOnMonthDay = resolveFeeOnMonthDay(charge);
|
||||
final SavingsAccountCharge accountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount(
|
||||
charge,
|
||||
charge.getAmount(),
|
||||
null, // chargeTime derived from Charge definition
|
||||
null, // chargeCalculation derived from Charge definition
|
||||
resolveDueDate(charge),
|
||||
true,
|
||||
feeOnMonthDay,
|
||||
charge.getFeeInterval());
|
||||
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
collector.add(accountCharge);
|
||||
});
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
collector.add(accountCharge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the charge mapped to {@code facilityType}, throwing rather than silently letting the facility be
|
||||
* enabled with no charge (or a charge that no longer exists) if the mapping is missing, has no charge
|
||||
* configured, or its charge has since been deleted/deactivated. Configure it via Products {@literal >}
|
||||
* Facility Charge Mappings first.
|
||||
*/
|
||||
private Charge requireMappedCharge(final String facilityType) {
|
||||
final FacilityChargeMapping mapping = mappingRepository.findByFacilityType(facilityType).orElse(null);
|
||||
// An inactive mapping is treated the same as an unmapped one: the Facility Charge Mappings screen's
|
||||
// "Active" toggle is how an admin marks a mapping as genuinely configured, so a charge sitting on an
|
||||
// inactive row must not silently apply.
|
||||
if (mapping == null || mapping.getCharge() == null || !Boolean.TRUE.equals(mapping.getIsActive())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.facility.charge.not.mapped",
|
||||
"A charge has not been mapped for the '" + facilityType
|
||||
+ "' facility. Configure it under Products > Facility Charge Mappings before enabling it.",
|
||||
facilityType);
|
||||
}
|
||||
final Charge charge = mapping.getCharge();
|
||||
if (charge.isDeleted() || !charge.isActive()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.facility.charge.deleted",
|
||||
"The charge mapped to the '" + facilityType
|
||||
+ "' facility has been deleted or deactivated. Update the mapping under Products > Facility Charge Mappings before enabling it.",
|
||||
facilityType);
|
||||
}
|
||||
return charge;
|
||||
}
|
||||
|
||||
private MonthDay resolveFeeOnMonthDay(final Charge charge) {
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.savings.data.FacilityChargeMappingData;
|
||||
|
||||
public interface FacilityChargeMappingReadPlatformService {
|
||||
|
||||
List<FacilityChargeMappingData> retrieveAll();
|
||||
|
||||
FacilityChargeMappingData retrieveOne(Long id);
|
||||
|
||||
/** All active charges, for the admin screen's "change the mapped charge" dropdown. */
|
||||
FacilityChargeMappingData retrieveTemplate();
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.data.FacilityChargeMappingData;
|
||||
import org.apache.fineract.portfolio.savings.data.FacilityChargeMappingData.FacilityChargeOptionData;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMapping;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository;
|
||||
import org.apache.fineract.portfolio.savings.exception.FacilityChargeMappingNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FacilityChargeMappingReadPlatformServiceImpl implements FacilityChargeMappingReadPlatformService {
|
||||
|
||||
private final FacilityChargeMappingRepository facilityChargeMappingRepository;
|
||||
private final ChargeReadPlatformService chargeReadPlatformService;
|
||||
|
||||
@Override
|
||||
public List<FacilityChargeMappingData> retrieveAll() {
|
||||
return facilityChargeMappingRepository.findAll().stream().sorted(Comparator.comparing(FacilityChargeMapping::getFacilityType))
|
||||
.map(this::toData).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FacilityChargeMappingData retrieveOne(final Long id) {
|
||||
final FacilityChargeMapping mapping = facilityChargeMappingRepository.findById(id)
|
||||
.orElseThrow(() -> new FacilityChargeMappingNotFoundException(id));
|
||||
return toData(mapping);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FacilityChargeMappingData retrieveTemplate() {
|
||||
final FacilityChargeMappingData template = new FacilityChargeMappingData();
|
||||
template.setChargeOptions(chargeReadPlatformService.retrieveAllCharges().stream()
|
||||
.map(charge -> new FacilityChargeOptionData(charge.getId(), charge.getName(), charge.getAmount())).toList());
|
||||
return template;
|
||||
}
|
||||
|
||||
private FacilityChargeMappingData toData(final FacilityChargeMapping mapping) {
|
||||
final Charge charge = mapping.getCharge();
|
||||
final boolean chargeAvailable = charge == null || (!charge.isDeleted() && charge.isActive());
|
||||
// FacilityChargeAutoApplyService always charges charge.getAmount() (the charge's own, live amount) --
|
||||
// mapping.getDefaultAmount() is a separate, unrelated stored column that nothing in the actual charging
|
||||
// path reads, so showing it here would silently drift from what an account is really charged the moment
|
||||
// someone edits the charge itself. Show the charge's real amount instead; edits to it happen on the
|
||||
// Charge (Products > Charges), not here.
|
||||
// When no charge is mapped, mapping.getChargeName()/getDefaultAmount() are stale leftovers from the
|
||||
// pre-seed migration -- report null so the screen shows the facility as genuinely unmapped, not as
|
||||
// if a charge (with a name and amount) were already chosen.
|
||||
final String chargeName = charge != null ? charge.getName() : null;
|
||||
final java.math.BigDecimal amount = charge != null ? charge.getAmount() : null;
|
||||
return new FacilityChargeMappingData(mapping.getId(), mapping.getFacilityType(), charge == null ? null : charge.getId(),
|
||||
chargeName, amount, mapping.getChargeTimeType(), mapping.getChargeAppliesTo(), mapping.getIsActive(),
|
||||
chargeAvailable);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface FacilityChargeMappingWritePlatformService {
|
||||
|
||||
CommandProcessingResult updateMapping(Long id, JsonCommand command);
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMapping;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository;
|
||||
import org.apache.fineract.portfolio.savings.exception.FacilityChargeMappingNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FacilityChargeMappingWritePlatformServiceImpl implements FacilityChargeMappingWritePlatformService {
|
||||
|
||||
private static final String CHARGE_ID_PARAM = "chargeId";
|
||||
private static final String IS_ACTIVE_PARAM = "isActive";
|
||||
|
||||
private final FacilityChargeMappingRepository facilityChargeMappingRepository;
|
||||
private final ChargeRepositoryWrapper chargeRepositoryWrapper;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult updateMapping(final Long id, final JsonCommand command) {
|
||||
final FacilityChargeMapping mapping = facilityChargeMappingRepository.findById(id)
|
||||
.orElseThrow(() -> new FacilityChargeMappingNotFoundException(id));
|
||||
|
||||
final Map<String, Object> changes = new HashMap<>();
|
||||
|
||||
final Long existingChargeId = mapping.getCharge() == null ? null : mapping.getCharge().getId();
|
||||
if (command.isChangeInLongParameterNamed(CHARGE_ID_PARAM, existingChargeId)) {
|
||||
final Long newChargeId = command.longValueOfParameterNamed(CHARGE_ID_PARAM);
|
||||
final Charge charge = chargeRepositoryWrapper.findOneWithNotFoundDetection(newChargeId);
|
||||
|
||||
// Each facility's charge must be its own: FacilityChargeAutoApplyService#applyIfEnabled dedups a
|
||||
// newly-enabled facility's charge by matching an existing SavingsAccountCharge's charge NAME, so if
|
||||
// two facilities shared the same charge, enabling the second one on an account that already has the
|
||||
// first would be silently skipped as "already applied" -- under-charging the account by one facility
|
||||
// worth of fee. Reject the mapping change here instead of letting that surface downstream.
|
||||
final Optional<FacilityChargeMapping> conflict = facilityChargeMappingRepository.findAllByIsActiveTrue().stream()
|
||||
.filter(other -> !other.getId().equals(mapping.getId())) //
|
||||
.filter(other -> other.getCharge() != null && newChargeId.equals(other.getCharge().getId())) //
|
||||
.findFirst();
|
||||
if (conflict.isPresent()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.facility.charge.already.mapped",
|
||||
"This charge is already mapped to the '" + conflict.get().getFacilityType()
|
||||
+ "' facility. Each charge can only be mapped to one facility, to avoid a shared charge being"
|
||||
+ " under-applied when both facilities are enabled on the same account.",
|
||||
conflict.get().getFacilityType());
|
||||
}
|
||||
|
||||
mapping.setCharge(charge);
|
||||
mapping.setChargeName(charge.getName());
|
||||
// Keep the stored column in step with the charge actually being applied, even though nothing
|
||||
// currently reads it directly (the read side reports charge.getAmount() live) -- avoids a future
|
||||
// reader finding a stale value here.
|
||||
mapping.setDefaultAmount(charge.getAmount());
|
||||
changes.put(CHARGE_ID_PARAM, newChargeId);
|
||||
}
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed(IS_ACTIVE_PARAM, mapping.getIsActive())) {
|
||||
final Boolean newIsActive = command.booleanObjectValueOfParameterNamed(IS_ACTIVE_PARAM);
|
||||
mapping.setIsActive(newIsActive);
|
||||
changes.put(IS_ACTIVE_PARAM, newIsActive);
|
||||
}
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
facilityChargeMappingRepository.saveAndFlush(mapping);
|
||||
}
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(id) //
|
||||
.with(changes) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+19
-33
@@ -29,11 +29,12 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Seeds default facility charges on service startup via JDBC.
|
||||
* For each facility type (ATM, UPI, NEFT etc.), ensures a charge exists in m_charge
|
||||
* and a mapping exists in m_facility_charge_mapping.
|
||||
* Bank staff can change amounts from the Charges admin screen later.
|
||||
* Seeder only creates records that are missing — safe to run repeatedly.
|
||||
* Seeds default facility charges on service startup via JDBC, one per facility type (ATM, UPI,
|
||||
* NEFT etc.), so the Facility Charge Mappings admin screen has ready-made charges to choose from.
|
||||
* Deliberately does NOT link a charge to its facility's m_facility_charge_mapping row or activate
|
||||
* it — that mapping must be an explicit admin choice made through that screen (an unmapped facility
|
||||
* is blocked from being enabled on an account by FacilityChargeAutoApplyService). Seeder only
|
||||
* creates charges that are missing — safe to run repeatedly.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -82,23 +83,13 @@ public class FacilityChargeSeederService {
|
||||
final int chargeTimeType = (int) def[2];
|
||||
final int chargeAppliesTo = (int) def[3];
|
||||
|
||||
// 1. Check if mapping row exists and already has a charge linked
|
||||
List<Long> existingChargeId = jdbcTemplate.queryForList(
|
||||
"SELECT charge_id FROM m_facility_charge_mapping WHERE facility_type = ? AND charge_id IS NOT NULL",
|
||||
Long.class, facilityType);
|
||||
|
||||
if (!existingChargeId.isEmpty()) {
|
||||
continue; // Already seeded
|
||||
}
|
||||
|
||||
// 2. Check if charge with this name already exists in m_charge
|
||||
// 1. Ensure a default charge exists in m_charge for this facility (creates it if missing) —
|
||||
// purely so it's available as a ready-made option in the mapping screen's charge dropdown.
|
||||
List<Long> chargeIds = jdbcTemplate.queryForList(
|
||||
"SELECT id FROM m_charge WHERE name = ? AND is_deleted = false LIMIT 1",
|
||||
Long.class, chargeName);
|
||||
|
||||
Long chargeId;
|
||||
if (chargeIds.isEmpty()) {
|
||||
// 3. Insert charge into m_charge
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO m_charge (name, currency_code, amount, charge_applies_to_enum, "
|
||||
+ "charge_time_enum, charge_calculation_enum, is_penalty, is_active, is_deleted, "
|
||||
@@ -106,29 +97,24 @@ public class FacilityChargeSeederService {
|
||||
+ "VALUES (?, 'INR', ?, ?, ?, true, false, true, false, false, false, ?)",
|
||||
chargeName, amount, chargeAppliesTo, chargeTimeType, incomeGlAccountId);
|
||||
|
||||
chargeId = jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM m_charge WHERE name = ? AND is_deleted = false ORDER BY id DESC LIMIT 1",
|
||||
Long.class, chargeName);
|
||||
|
||||
log.info("FacilityChargeSeeder: inserted charge '{}' id={}", chargeName, chargeId);
|
||||
} else {
|
||||
chargeId = chargeIds.get(0);
|
||||
log.info("FacilityChargeSeeder: inserted charge '{}'", chargeName);
|
||||
}
|
||||
|
||||
// 4. Update or insert mapping row
|
||||
int updated = jdbcTemplate.update(
|
||||
"UPDATE m_facility_charge_mapping SET charge_id = ? WHERE facility_type = ?",
|
||||
chargeId, facilityType);
|
||||
// 2. Ensure the facility_charge_mapping row itself exists so the admin screen lists every
|
||||
// facility type, but leave it unmapped/inactive — an admin must explicitly pick a charge
|
||||
// for it via the Facility Charge Mappings screen before it can be enabled on an account.
|
||||
List<Long> existingMapping = jdbcTemplate.queryForList(
|
||||
"SELECT id FROM m_facility_charge_mapping WHERE facility_type = ?", Long.class, facilityType);
|
||||
|
||||
if (updated == 0) {
|
||||
if (existingMapping.isEmpty()) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO m_facility_charge_mapping "
|
||||
+ "(facility_type, charge_id, charge_name, default_amount, charge_time_type, charge_applies_to, is_active) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, 1)",
|
||||
facilityType, chargeId, chargeName, amount, chargeTimeType, chargeAppliesTo);
|
||||
}
|
||||
+ "VALUES (?, NULL, ?, ?, ?, ?, false)",
|
||||
facilityType, chargeName, amount, chargeTimeType, chargeAppliesTo);
|
||||
|
||||
log.info("FacilityChargeSeeder: mapped facility '{}' → charge id={}", facilityType, chargeId);
|
||||
log.info("FacilityChargeSeeder: added unmapped facility row '{}'", facilityType);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("FacilityChargeSeeder: seeding complete.");
|
||||
|
||||
Reference in New Issue
Block a user