Compare commits
3
Commits
uate
...
7773a9e9c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7773a9e9c6 | ||
|
|
c890faee78 | ||
|
|
abc7eb22a2 |
+6
@@ -35,4 +35,10 @@ public interface AmlAlertRepository extends JpaRepository<AmlAlert, Long> {
|
|||||||
@Param("since") LocalDateTime since);
|
@Param("since") LocalDateTime since);
|
||||||
|
|
||||||
long countByClientId(Long clientId);
|
long countByClientId(Long clientId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alerts already raised against one transaction under one rule. Used to stop the same ledger entry being marked
|
||||||
|
* twice, which would put two STR cases in front of the Principal Officer for a single transaction.
|
||||||
|
*/
|
||||||
|
List<AmlAlert> findByRule_IdAndTransactionId(Long ruleId, Long transactionId);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -52,6 +52,26 @@ public class AmlTransactionRule {
|
|||||||
@Column(name = "threshold_amount", precision = 19, scale = 6)
|
@Column(name = "threshold_amount", precision = 19, scale = 6)
|
||||||
private BigDecimal thresholdAmount;
|
private BigDecimal thresholdAmount;
|
||||||
|
|
||||||
|
/** Risk-category-tiered overrides of {@link #thresholdAmount}. Null on both means the rule is
|
||||||
|
* not tiered; {@link #thresholdAmount} is used for every client regardless of risk category. */
|
||||||
|
@Column(name = "threshold_amount_medium_risk", precision = 19, scale = 6)
|
||||||
|
private BigDecimal thresholdAmountMediumRisk;
|
||||||
|
|
||||||
|
@Column(name = "threshold_amount_high_risk", precision = 19, scale = 6)
|
||||||
|
private BigDecimal thresholdAmountHighRisk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declarative band the rule covers, shown on the AML Rules Master screen. Purely
|
||||||
|
* descriptive: unlike the tiered columns above these never take part in matching, so
|
||||||
|
* changing them cannot alter which transactions raise an alert. A null maximum means
|
||||||
|
* the rule is genuinely open-ended.
|
||||||
|
*/
|
||||||
|
@Column(name = "min_limit_amount", precision = 19, scale = 6)
|
||||||
|
private BigDecimal minLimitAmount;
|
||||||
|
|
||||||
|
@Column(name = "max_limit_amount", precision = 19, scale = 6)
|
||||||
|
private BigDecimal maxLimitAmount;
|
||||||
|
|
||||||
@Column(name = "rule_type", nullable = false, length = 20)
|
@Column(name = "rule_type", nullable = false, length = 20)
|
||||||
private String ruleType;
|
private String ruleType;
|
||||||
|
|
||||||
@@ -86,6 +106,22 @@ public class AmlTransactionRule {
|
|||||||
return thresholdAmount;
|
return thresholdAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getThresholdAmountMediumRisk() {
|
||||||
|
return thresholdAmountMediumRisk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getThresholdAmountHighRisk() {
|
||||||
|
return thresholdAmountHighRisk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMinLimitAmount() {
|
||||||
|
return minLimitAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMaxLimitAmount() {
|
||||||
|
return maxLimitAmount;
|
||||||
|
}
|
||||||
|
|
||||||
public AmlRuleType getRuleType() {
|
public AmlRuleType getRuleType() {
|
||||||
return AmlRuleType.valueOf(ruleType);
|
return AmlRuleType.valueOf(ruleType);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -19,9 +19,17 @@
|
|||||||
package org.apache.fineract.portfolio.aml.domain;
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface AmlTransactionRuleRepository extends JpaRepository<AmlTransactionRule, Long> {
|
public interface AmlTransactionRuleRepository extends JpaRepository<AmlTransactionRule, Long> {
|
||||||
|
|
||||||
List<AmlTransactionRule> findByActiveTrueAndAppliesTo(String appliesTo);
|
List<AmlTransactionRule> findByActiveTrueAndAppliesTo(String appliesTo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks a rule up by its code irrespective of {@code is_active}. Needed for MANUAL_REFERRAL, which is deliberately
|
||||||
|
* seeded inactive so the evaluation engine never fires it, yet must still be attachable to an alert a user raises
|
||||||
|
* by hand from the ledger.
|
||||||
|
*/
|
||||||
|
Optional<AmlTransactionRule> findByRuleCode(String ruleCode);
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -33,8 +33,10 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.aml.data.AmlAlertData;
|
import org.apache.fineract.portfolio.aml.data.AmlAlertData;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.ManualReferralData;
|
||||||
import org.apache.fineract.portfolio.aml.service.AmlAlertReadPlatformService;
|
import org.apache.fineract.portfolio.aml.service.AmlAlertReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.aml.service.AmlAlertWritePlatformService;
|
import org.apache.fineract.portfolio.aml.service.AmlAlertWritePlatformService;
|
||||||
|
import org.apache.fineract.portfolio.aml.service.JournalEntryReferralService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Path("/v1/aml/alerts")
|
@Path("/v1/aml/alerts")
|
||||||
@@ -46,7 +48,9 @@ public class AmlAlertApiResource {
|
|||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final AmlAlertReadPlatformService readPlatformService;
|
private final AmlAlertReadPlatformService readPlatformService;
|
||||||
private final AmlAlertWritePlatformService writePlatformService;
|
private final AmlAlertWritePlatformService writePlatformService;
|
||||||
|
private final JournalEntryReferralService referralService;
|
||||||
private final DefaultToApiJsonSerializer<AmlAlertData> toApiJsonSerializer;
|
private final DefaultToApiJsonSerializer<AmlAlertData> toApiJsonSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<ManualReferralData> referralJsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@@ -76,4 +80,28 @@ public class AmlAlertApiResource {
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
return this.toApiJsonSerializer.serialize(updated);
|
return this.toApiJsonSerializer.serialize(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("manual-referrals")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List ledger entries marked by a user",
|
||||||
|
description = "Every entry marked cleared or suspicious from the general ledger screen, keyed on journalEntryId so the ledger can show what has already been dealt with.")
|
||||||
|
public String retrieveManualReferrals() {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
return this.referralJsonSerializer.serialize(this.referralService.retrieveAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("manual-referral")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Mark a ledger entry cleared or suspicious",
|
||||||
|
description = "WHITELIST clears the entry with no report. ESCALATE raises an alert against the MANUAL_REFERRAL rule and escalates it, so an STR case opens for the Principal Officer exactly as an engine alert would. The entry must have a customer behind it.")
|
||||||
|
public String referJournalEntry(final ManualReferralRequest request) {
|
||||||
|
this.context.authenticatedUser().validateHasPermissionTo("REVIEW_AML_ALERT");
|
||||||
|
final ManualReferralData referral = this.referralService.refer(request.getJournalEntryId(), request.getDisposition(),
|
||||||
|
request.getNotes());
|
||||||
|
return this.referralJsonSerializer.serialize(referral);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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.aml.api;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for {@code POST /v1/aml/alerts/manual-referral}.
|
||||||
|
*
|
||||||
|
* {@code journalEntryId} is acc_gl_journal_entry.id as shown in the ledger. {@code disposition} is WHITELIST (cleared,
|
||||||
|
* no report) or ESCALATE (raises an STR case for the Principal Officer).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class ManualReferralRequest {
|
||||||
|
|
||||||
|
private Long journalEntryId;
|
||||||
|
private String disposition;
|
||||||
|
private String notes;
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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.aml.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One ledger entry a user has marked, so the ledger screen can show what has already been dealt with.
|
||||||
|
*
|
||||||
|
* {@code journalEntryId} is the acc_gl_journal_entry row the mark was made against - it is what the ledger table keys
|
||||||
|
* on. {@code strCaseId} is populated only where the mark escalated into an STR case.
|
||||||
|
*/
|
||||||
|
public record ManualReferralData(Long alertId, Long journalEntryId, Long clientId, String clientDisplayName,
|
||||||
|
BigDecimal amount, String currencyCode, LocalDateTime raisedOn, String status, String disposition,
|
||||||
|
String reviewedByUsername, String reviewNotes, Long strCaseId) implements Serializable {}
|
||||||
+32
-3
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.aml.service;
|
package org.apache.fineract.portfolio.aml.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -27,6 +28,8 @@ import org.apache.fineract.portfolio.aml.domain.AmlAlertRepository;
|
|||||||
import org.apache.fineract.portfolio.aml.domain.AmlAppliesTo;
|
import org.apache.fineract.portfolio.aml.domain.AmlAppliesTo;
|
||||||
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule;
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule;
|
||||||
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRuleRepository;
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRuleRepository;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKyc;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKycRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -35,8 +38,12 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
@Transactional
|
@Transactional
|
||||||
public class AmlRuleEvaluationServiceImpl implements AmlRuleEvaluationService {
|
public class AmlRuleEvaluationServiceImpl implements AmlRuleEvaluationService {
|
||||||
|
|
||||||
|
private static final int MEDIUM_RISK = 2;
|
||||||
|
private static final int HIGH_RISK = 3;
|
||||||
|
|
||||||
private final AmlTransactionRuleRepository ruleRepository;
|
private final AmlTransactionRuleRepository ruleRepository;
|
||||||
private final AmlAlertRepository alertRepository;
|
private final AmlAlertRepository alertRepository;
|
||||||
|
private final ClientKycRepository clientKycRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AmlAlert> evaluate(AmlTransactionContext context) {
|
public List<AmlAlert> evaluate(AmlTransactionContext context) {
|
||||||
@@ -56,20 +63,42 @@ public class AmlRuleEvaluationServiceImpl implements AmlRuleEvaluationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isHit(AmlTransactionRule rule, AmlTransactionContext context) {
|
private boolean isHit(AmlTransactionRule rule, AmlTransactionContext context) {
|
||||||
if (rule.getThresholdAmount() == null) {
|
final BigDecimal threshold = resolveThreshold(rule, context);
|
||||||
|
if (threshold == null) {
|
||||||
// Pattern-only VELOCITY rules (e.g. same-day movement) need bespoke detection logic
|
// Pattern-only VELOCITY rules (e.g. same-day movement) need bespoke detection logic
|
||||||
// per rule and are not yet evaluated here - they exist in the catalog for future work.
|
// per rule and are not yet evaluated here - they exist in the catalog for future work.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (rule.getRuleType()) {
|
switch (rule.getRuleType()) {
|
||||||
case THRESHOLD:
|
case THRESHOLD:
|
||||||
return context.getAmount().compareTo(rule.getThresholdAmount()) >= 0;
|
return context.getAmount().compareTo(threshold) >= 0;
|
||||||
case STRUCTURING:
|
case STRUCTURING:
|
||||||
case VELOCITY:
|
case VELOCITY:
|
||||||
return context.getWindowAggregateAmount() != null
|
return context.getWindowAggregateAmount() != null
|
||||||
&& context.getWindowAggregateAmount().compareTo(rule.getThresholdAmount()) >= 0;
|
&& context.getWindowAggregateAmount().compareTo(threshold) >= 0;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rules with both tiered columns null use {@code thresholdAmount} for every client, unchanged
|
||||||
|
* from prior behaviour. Tiered rules look up the transacting client's current KYC risk category
|
||||||
|
* and pick the matching tier, falling back to {@code thresholdAmount} for Low-risk/unclassified
|
||||||
|
* clients.
|
||||||
|
*/
|
||||||
|
private BigDecimal resolveThreshold(AmlTransactionRule rule, AmlTransactionContext context) {
|
||||||
|
if (rule.getThresholdAmountMediumRisk() == null && rule.getThresholdAmountHighRisk() == null) {
|
||||||
|
return rule.getThresholdAmount();
|
||||||
|
}
|
||||||
|
final Integer riskCategory = clientKycRepository.findByClientId(context.getClient().getId()).map(ClientKyc::getRiskCategoryValue)
|
||||||
|
.orElse(null);
|
||||||
|
if (riskCategory != null && riskCategory == HIGH_RISK && rule.getThresholdAmountHighRisk() != null) {
|
||||||
|
return rule.getThresholdAmountHighRisk();
|
||||||
|
}
|
||||||
|
if (riskCategory != null && riskCategory == MEDIUM_RISK && rule.getThresholdAmountMediumRisk() != null) {
|
||||||
|
return rule.getThresholdAmountMediumRisk();
|
||||||
|
}
|
||||||
|
return rule.getThresholdAmount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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.aml.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.ManualReferralData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets a user mark a ledger entry as cleared or suspicious from the general ledger screen.
|
||||||
|
*
|
||||||
|
* The rule engine raises alerts on its own; this is the manual path for an entry a reviewer's eye catches that no rule
|
||||||
|
* fired on. A suspicious mark goes down exactly the same road as an escalated engine alert - it raises an
|
||||||
|
* {@link org.apache.fineract.portfolio.aml.domain.AmlAlert} against the MANUAL_REFERRAL rule and escalates it, so the
|
||||||
|
* case lands in the Principal Officer's STR queue with the usual seven working day FIU clock running.
|
||||||
|
*/
|
||||||
|
public interface JournalEntryReferralService {
|
||||||
|
|
||||||
|
/** disposition: WHITELIST | ESCALATE. */
|
||||||
|
ManualReferralData refer(Long journalEntryId, String disposition, String notes);
|
||||||
|
|
||||||
|
/** Every mark made from the ledger, so the screen can show what has already been dealt with. */
|
||||||
|
List<ManualReferralData> retrieveAll();
|
||||||
|
}
|
||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
/**
|
||||||
|
* 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.aml.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.ManualReferralData;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlAlert;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlAlertRepository;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCase;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRuleRepository;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionType;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||||
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional
|
||||||
|
public class JournalEntryReferralServiceImpl implements JournalEntryReferralService {
|
||||||
|
|
||||||
|
/** Seeded inactive by 0443 so the evaluation engine never fires it. */
|
||||||
|
private static final String MANUAL_RULE_CODE = "MANUAL_REFERRAL";
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final AmlAlertRepository alertRepository;
|
||||||
|
private final AmlStrCaseRepository strCaseRepository;
|
||||||
|
private final AmlTransactionRuleRepository ruleRepository;
|
||||||
|
private final ClientRepositoryWrapper clientRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final ClientRiskEscalationService clientRiskEscalationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A journal entry carries no customer of its own, so the customer has to be reached through whatever the entry is
|
||||||
|
* posted against.
|
||||||
|
*
|
||||||
|
* The entity_id / entity_type_enum pair is the primary route and the one that matters: it is exactly how
|
||||||
|
* JournalEntryReadPlatformServiceImpl derives the account number the ledger screen shows, so anything displaying an
|
||||||
|
* account number resolves here. entity_type_enum follows PortfolioProductType - LOAN(1), SAVING(2), CLIENT(5) - and
|
||||||
|
* a savings-type entity may be either a savings or a current account, which live in separate tables.
|
||||||
|
*
|
||||||
|
* The transaction-id columns are kept as a fallback for entries that carry one but no entity. A purely manual GL
|
||||||
|
* posting reaches none of these and comes back with a null clientId, which is what makes it unreferrable.
|
||||||
|
*/
|
||||||
|
private static final String RESOLVE_SQL = """
|
||||||
|
SELECT je.amount AS amount,
|
||||||
|
je.currency_code AS currencyCode,
|
||||||
|
je.savings_transaction_id AS savingsTxnId,
|
||||||
|
je.loan_transaction_id AS loanTxnId,
|
||||||
|
je.entity_type_enum AS entityType,
|
||||||
|
COALESCE(
|
||||||
|
esa.client_id,
|
||||||
|
eca.client_id,
|
||||||
|
eloan.client_id,
|
||||||
|
CASE WHEN je.entity_type_enum = 5 THEN je.entity_id END,
|
||||||
|
sa.client_id,
|
||||||
|
l.client_id,
|
||||||
|
ct.client_id
|
||||||
|
) AS clientId
|
||||||
|
FROM acc_gl_journal_entry je
|
||||||
|
LEFT JOIN m_savings_account esa ON esa.id = je.entity_id AND je.entity_type_enum = 2
|
||||||
|
LEFT JOIN m_current_account eca ON eca.id = je.entity_id AND je.entity_type_enum = 2
|
||||||
|
LEFT JOIN m_loan eloan ON eloan.id = je.entity_id AND je.entity_type_enum = 1
|
||||||
|
LEFT JOIN m_savings_account_transaction sat ON sat.id = je.savings_transaction_id
|
||||||
|
LEFT JOIN m_savings_account sa ON sa.id = sat.savings_account_id
|
||||||
|
LEFT JOIN m_loan_transaction lt ON lt.id = je.loan_transaction_id
|
||||||
|
LEFT JOIN m_loan l ON l.id = lt.loan_id
|
||||||
|
LEFT JOIN m_client_transaction ct ON ct.id = je.client_transaction_id
|
||||||
|
WHERE je.id = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
private static final String LIST_SQL = """
|
||||||
|
SELECT a.id AS alertId,
|
||||||
|
a.transaction_id AS journalEntryId,
|
||||||
|
a.client_id AS clientId,
|
||||||
|
c.display_name AS clientDisplayName,
|
||||||
|
a.amount AS amount,
|
||||||
|
a.currency_code AS currencyCode,
|
||||||
|
a.raised_on AS raisedOn,
|
||||||
|
a.status AS status,
|
||||||
|
a.disposition AS disposition,
|
||||||
|
u.username AS reviewedByUsername,
|
||||||
|
a.review_notes AS reviewNotes,
|
||||||
|
s.id AS strCaseId
|
||||||
|
FROM m_aml_alert a
|
||||||
|
JOIN m_aml_transaction_rule r ON r.id = a.rule_id AND r.rule_code = ?
|
||||||
|
LEFT JOIN m_client c ON c.id = a.client_id
|
||||||
|
LEFT JOIN m_appuser u ON u.id = a.reviewed_by
|
||||||
|
LEFT JOIN m_aml_str_case s ON s.alert_id = a.id
|
||||||
|
ORDER BY a.raised_on DESC
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ManualReferralData refer(final Long journalEntryId, final String disposition, final String notes) {
|
||||||
|
if (journalEntryId == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.aml.referral.journal.entry.required",
|
||||||
|
"A journal entry must be given to refer.");
|
||||||
|
}
|
||||||
|
final boolean escalate = "ESCALATE".equalsIgnoreCase(disposition);
|
||||||
|
if (!escalate && !"WHITELIST".equalsIgnoreCase(disposition)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.aml.referral.disposition.invalid",
|
||||||
|
"Disposition must be WHITELIST or ESCALATE.", disposition);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AmlTransactionRule rule = ruleRepository.findByRuleCode(MANUAL_RULE_CODE)
|
||||||
|
.orElseThrow(() -> new GeneralPlatformDomainRuleException("error.msg.aml.referral.rule.missing",
|
||||||
|
"The MANUAL_REFERRAL rule is not seeded; run the tenant migrations."));
|
||||||
|
|
||||||
|
// Marking the same entry twice would put two cases in front of the Principal Officer for
|
||||||
|
// one transaction, so the first mark stands until it is explicitly reworked.
|
||||||
|
if (!alertRepository.findByRule_IdAndTransactionId(rule.getId(), journalEntryId).isEmpty()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.aml.referral.already.marked",
|
||||||
|
"This ledger entry has already been marked.", journalEntryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object> row = loadEntry(journalEntryId);
|
||||||
|
final Number clientIdValue = (Number) row.get("clientId");
|
||||||
|
if (clientIdValue == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.aml.referral.no.client",
|
||||||
|
"This ledger entry has no customer behind it, so it cannot be referred.", journalEntryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientIdValue.longValue());
|
||||||
|
final BigDecimal amount = (BigDecimal) row.get("amount");
|
||||||
|
final String currencyCode = (String) row.get("currencyCode");
|
||||||
|
|
||||||
|
// transaction_id holds the journal entry id, which is what the ledger screen keys on. The
|
||||||
|
// MANUAL_REFERRAL rule tells these apart from engine alerts, whose transaction_id is a
|
||||||
|
// savings or loan transaction id.
|
||||||
|
final AmlAlert alert = AmlAlert.raise(transactionTypeOf(row), journalEntryId, client, rule, amount, currencyCode,
|
||||||
|
LocalDateTime.now());
|
||||||
|
alertRepository.save(alert);
|
||||||
|
|
||||||
|
Long strCaseId = null;
|
||||||
|
if (escalate) {
|
||||||
|
alert.escalate(context.authenticatedUser(), notes);
|
||||||
|
alertRepository.save(alert);
|
||||||
|
final AmlStrCase strCase = strCaseRepository.save(AmlStrCase.openFor(alert));
|
||||||
|
clientRiskEscalationService.onStrCaseEscalated(strCase);
|
||||||
|
strCaseId = strCase.getId();
|
||||||
|
} else {
|
||||||
|
alert.whitelist(context.authenticatedUser(), notes);
|
||||||
|
alertRepository.save(alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ManualReferralData(alert.getId(), journalEntryId, client.getId(), client.getDisplayName(), amount,
|
||||||
|
currencyCode, alert.getRaisedOn(), alert.getStatus().name(),
|
||||||
|
alert.getDisposition() == null ? null : alert.getDisposition().name(),
|
||||||
|
alert.getReviewedBy() == null ? null : alert.getReviewedBy().getUsername(), alert.getReviewNotes(),
|
||||||
|
strCaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ManualReferralData> retrieveAll() {
|
||||||
|
return jdbcTemplate.query(LIST_SQL, (rs, rowNum) -> new ManualReferralData(rs.getLong("alertId"),
|
||||||
|
rs.getLong("journalEntryId"), rs.getLong("clientId"), rs.getString("clientDisplayName"),
|
||||||
|
rs.getBigDecimal("amount"), rs.getString("currencyCode"),
|
||||||
|
rs.getTimestamp("raisedOn") == null ? null : rs.getTimestamp("raisedOn").toLocalDateTime(),
|
||||||
|
rs.getString("status"), rs.getString("disposition"), rs.getString("reviewedByUsername"),
|
||||||
|
rs.getString("reviewNotes"), rs.getObject("strCaseId") == null ? null : rs.getLong("strCaseId")),
|
||||||
|
MANUAL_RULE_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> loadEntry(final Long journalEntryId) {
|
||||||
|
try {
|
||||||
|
return jdbcTemplate.queryForMap(RESOLVE_SQL, journalEntryId);
|
||||||
|
} catch (final EmptyResultDataAccessException e) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.aml.referral.journal.entry.not.found",
|
||||||
|
"Journal entry " + journalEntryId + " does not exist.", journalEntryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TRANSFER covers everything that is neither a savings nor a loan posting.
|
||||||
|
*
|
||||||
|
* The entity type is checked first because a system-generated entry often carries entity_id without a transaction
|
||||||
|
* id; falling straight through to the transaction columns would mislabel those as transfers.
|
||||||
|
*/
|
||||||
|
private AmlTransactionType transactionTypeOf(final Map<String, Object> row) {
|
||||||
|
final Number entityType = (Number) row.get("entityType");
|
||||||
|
if (entityType != null) {
|
||||||
|
if (entityType.intValue() == 2) {
|
||||||
|
return AmlTransactionType.SAVINGS;
|
||||||
|
}
|
||||||
|
if (entityType.intValue() == 1) {
|
||||||
|
return AmlTransactionType.LOAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row.get("savingsTxnId") != null) {
|
||||||
|
return AmlTransactionType.SAVINGS;
|
||||||
|
}
|
||||||
|
if (row.get("loanTxnId") != null) {
|
||||||
|
return AmlTransactionType.LOAN;
|
||||||
|
}
|
||||||
|
return AmlTransactionType.TRANSFER;
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-1
@@ -46,6 +46,20 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
private final String willfulDefaulter;
|
private final String willfulDefaulter;
|
||||||
private final String ecgcSalFlag;
|
private final String ecgcSalFlag;
|
||||||
private final String dormantFlag;
|
private final String dormantFlag;
|
||||||
|
/** Populated only for rules that also have a live-transaction-monitoring counterpart in
|
||||||
|
* {@code m_aml_transaction_rule} (linked via source_client_risk_rule_id) - distinct from the
|
||||||
|
* client-onboarding income/turnover fields above, which drive KYC-time risk categorization. */
|
||||||
|
private final BigDecimal txnThresholdAmount;
|
||||||
|
private final String txnRuleType;
|
||||||
|
private final Integer txnWindowDays;
|
||||||
|
/** Risk-category-tiered overrides of {@link #txnThresholdAmount}, populated only when the
|
||||||
|
* linked {@code m_aml_transaction_rule} row has tiering configured (e.g. RTGS monitoring). */
|
||||||
|
private final BigDecimal txnThresholdAmountMediumRisk;
|
||||||
|
private final BigDecimal txnThresholdAmountHighRisk;
|
||||||
|
/** Declarative band the rule covers, for display on the AML Rules Master screen. Never
|
||||||
|
* used in matching; a null maximum means the rule is genuinely open-ended. */
|
||||||
|
private final BigDecimal txnMinLimitAmount;
|
||||||
|
private final BigDecimal txnMaxLimitAmount;
|
||||||
|
|
||||||
public KycRiskRuleData(final Long id, final String ruleName, final Integer legalFormId,
|
public KycRiskRuleData(final Long id, final String ruleName, final Integer legalFormId,
|
||||||
final Long occupationCategoryCvId, final String occupationCategoryName,
|
final Long occupationCategoryCvId, final String occupationCategoryName,
|
||||||
@@ -55,7 +69,10 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover,
|
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover,
|
||||||
final Integer riskCategory, final String reason, final Integer orderPosition,
|
final Integer riskCategory, final String reason, final Integer orderPosition,
|
||||||
final String amlCheckStatus, final String bannedEntityMatch,
|
final String amlCheckStatus, final String bannedEntityMatch,
|
||||||
final String willfulDefaulter, final String ecgcSalFlag, final String dormantFlag) {
|
final String willfulDefaulter, final String ecgcSalFlag, final String dormantFlag,
|
||||||
|
final BigDecimal txnThresholdAmount, final String txnRuleType, final Integer txnWindowDays,
|
||||||
|
final BigDecimal txnThresholdAmountMediumRisk, final BigDecimal txnThresholdAmountHighRisk,
|
||||||
|
final BigDecimal txnMinLimitAmount, final BigDecimal txnMaxLimitAmount) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.ruleName = ruleName;
|
this.ruleName = ruleName;
|
||||||
this.legalFormId = legalFormId;
|
this.legalFormId = legalFormId;
|
||||||
@@ -77,6 +94,13 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
this.willfulDefaulter = willfulDefaulter;
|
this.willfulDefaulter = willfulDefaulter;
|
||||||
this.ecgcSalFlag = ecgcSalFlag;
|
this.ecgcSalFlag = ecgcSalFlag;
|
||||||
this.dormantFlag = dormantFlag;
|
this.dormantFlag = dormantFlag;
|
||||||
|
this.txnThresholdAmount = txnThresholdAmount;
|
||||||
|
this.txnRuleType = txnRuleType;
|
||||||
|
this.txnWindowDays = txnWindowDays;
|
||||||
|
this.txnThresholdAmountMediumRisk = txnThresholdAmountMediumRisk;
|
||||||
|
this.txnThresholdAmountHighRisk = txnThresholdAmountHighRisk;
|
||||||
|
this.txnMinLimitAmount = txnMinLimitAmount;
|
||||||
|
this.txnMaxLimitAmount = txnMaxLimitAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
@@ -162,4 +186,32 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
public String getDormantFlag() {
|
public String getDormantFlag() {
|
||||||
return dormantFlag;
|
return dormantFlag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTxnThresholdAmount() {
|
||||||
|
return txnThresholdAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTxnRuleType() {
|
||||||
|
return txnRuleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTxnWindowDays() {
|
||||||
|
return txnWindowDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTxnThresholdAmountMediumRisk() {
|
||||||
|
return txnThresholdAmountMediumRisk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTxnThresholdAmountHighRisk() {
|
||||||
|
return txnThresholdAmountHighRisk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTxnMinLimitAmount() {
|
||||||
|
return txnMinLimitAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTxnMaxLimitAmount() {
|
||||||
|
return txnMaxLimitAmount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -19,8 +19,12 @@
|
|||||||
package org.apache.fineract.portfolio.client.service;
|
package org.apache.fineract.portfolio.client.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRuleRepository;
|
||||||
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
||||||
import org.apache.fineract.portfolio.client.domain.ClientRiskRule;
|
import org.apache.fineract.portfolio.client.domain.ClientRiskRule;
|
||||||
import org.apache.fineract.portfolio.client.domain.ClientRiskRuleRepository;
|
import org.apache.fineract.portfolio.client.domain.ClientRiskRuleRepository;
|
||||||
@@ -33,14 +37,18 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlatformService {
|
public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlatformService {
|
||||||
|
|
||||||
private final ClientRiskRuleRepository repository;
|
private final ClientRiskRuleRepository repository;
|
||||||
|
private final AmlTransactionRuleRepository amlTransactionRuleRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<KycRiskRuleData> retrieveAllRules() {
|
public List<KycRiskRuleData> retrieveAllRules() {
|
||||||
final List<ClientRiskRule> rules = this.repository.findAllOrderedByOrderPosition();
|
final List<ClientRiskRule> rules = this.repository.findAllOrderedByOrderPosition();
|
||||||
return rules.stream().map(this::mapToData).collect(Collectors.toList());
|
final Map<Long, AmlTransactionRule> txnRulesBySourceId = this.amlTransactionRuleRepository.findAll().stream()
|
||||||
|
.filter(txn -> txn.getSourceClientRiskRuleId() != null)
|
||||||
|
.collect(Collectors.toMap(AmlTransactionRule::getSourceClientRiskRuleId, Function.identity(), (a, b) -> a));
|
||||||
|
return rules.stream().map(rule -> mapToData(rule, txnRulesBySourceId.get(rule.getId()))).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private KycRiskRuleData mapToData(final ClientRiskRule rule) {
|
private KycRiskRuleData mapToData(final ClientRiskRule rule, final AmlTransactionRule txnRule) {
|
||||||
final Long occCatId = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getId() : null;
|
final Long occCatId = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getId() : null;
|
||||||
final String occCatName = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getLabel() : null;
|
final String occCatName = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getLabel() : null;
|
||||||
final Long occId = rule.getOccupation() != null ? rule.getOccupation().getId() : null;
|
final Long occId = rule.getOccupation() != null ? rule.getOccupation().getId() : null;
|
||||||
@@ -67,7 +75,14 @@ public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlat
|
|||||||
rule.getBannedEntityMatch(),
|
rule.getBannedEntityMatch(),
|
||||||
rule.getWillfulDefaulter(),
|
rule.getWillfulDefaulter(),
|
||||||
rule.getEcgcSalFlag(),
|
rule.getEcgcSalFlag(),
|
||||||
rule.getDormantFlag()
|
rule.getDormantFlag(),
|
||||||
|
txnRule != null ? txnRule.getThresholdAmount() : null,
|
||||||
|
txnRule != null ? txnRule.getRuleType().name() : null,
|
||||||
|
txnRule != null ? txnRule.getWindowDays() : null,
|
||||||
|
txnRule != null ? txnRule.getThresholdAmountMediumRisk() : null,
|
||||||
|
txnRule != null ? txnRule.getThresholdAmountHighRisk() : null,
|
||||||
|
txnRule != null ? txnRule.getMinLimitAmount() : null,
|
||||||
|
txnRule != null ? txnRule.getMaxLimitAmount() : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,4 +490,10 @@
|
|||||||
<include file="parts/0435_add_aml_officer_and_principal_officer_roles.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0435_add_aml_officer_and_principal_officer_roles.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0436_fix_missing_client_risk_rule_columns.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0436_fix_missing_client_risk_rule_columns.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0437_update_aml_columns_for_risk_rules.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0437_update_aml_columns_for_risk_rules.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0438_add_tiered_thresholds_and_low_balance_rule.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0439_fill_client_risk_rule_income_turnover_nulls.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0440_seed_aml_test_alerts.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0441_add_aml_rule_limit_bands.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0442_seed_branch_offices.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0443_seed_aml_manual_referral_rule.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||||
|
|
||||||
|
<!-- Optional risk-category-tiered thresholds. Null on both columns means the rule is not
|
||||||
|
tiered and threshold_amount alone is used (unchanged behaviour for every pre-existing rule). -->
|
||||||
|
<changeSet author="fineract" id="0438-1">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_aml_transaction_rule" columnName="threshold_amount_medium_risk"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_aml_transaction_rule">
|
||||||
|
<column name="threshold_amount_medium_risk" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0438-2">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_aml_transaction_rule" columnName="threshold_amount_high_risk"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_aml_transaction_rule">
|
||||||
|
<column name="threshold_amount_high_risk" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- RTGS monitoring: Medium-risk clients are flagged at INR 5,00,000, High-risk at INR 20,00,000.
|
||||||
|
threshold_amount (2,000,000) remains the fallback for Low-risk/unclassified clients. -->
|
||||||
|
<changeSet author="fineract" id="0438-3">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_aml_transaction_rule WHERE rule_code = 'RTGS_HIGH_VALUE' AND threshold_amount_medium_risk IS NOT NULL</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_aml_transaction_rule
|
||||||
|
SET threshold_amount_medium_risk = 500000,
|
||||||
|
threshold_amount_high_risk = 2000000
|
||||||
|
WHERE rule_code = 'RTGS_HIGH_VALUE';
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Rule 1 from the legacy Rules Parameter catalog: "Maintaining a low or overdrawn balance
|
||||||
|
with high activity" (order_position 1). The legacy screen reports a single INR 10,00,000
|
||||||
|
figure across three distinct parameters (average balance period, minimum balance, max
|
||||||
|
transaction count), which does not correspond to any threshold/window shape this catalog
|
||||||
|
currently evaluates (THRESHOLD/STRUCTURING/VELOCITY all key off a transaction AMOUNT, not
|
||||||
|
an account balance level). Seeded here for catalog/display parity with m_client_risk_rule,
|
||||||
|
with is_active = false so it is never picked up by AmlRuleEvaluationServiceImpl until a
|
||||||
|
balance-based rule type is built. -->
|
||||||
|
<changeSet author="fineract" id="0438-4">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_aml_transaction_rule WHERE rule_code = 'LOW_BALANCE_HIGH_ACTIVITY'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_aml_transaction_rule
|
||||||
|
(rule_code, description, applies_to, threshold_amount, rule_type, window_days, source_client_risk_rule_id, is_active)
|
||||||
|
VALUES
|
||||||
|
('LOW_BALANCE_HIGH_ACTIVITY', 'Maintaining a low or overdrawn balance with high activity', 'SAVINGS', 1000000, 'THRESHOLD', null,
|
||||||
|
(SELECT id FROM m_client_risk_rule WHERE order_position = 1 AND aml_check_status IS NOT NULL), false);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0439-1">
|
||||||
|
<validCheckSum>*</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
-- 1. High Risk Occupations (Real Estate, Importer/Exporter, Commission Agent, Financial Advisor)
|
||||||
|
UPDATE m_client_risk_rule
|
||||||
|
SET min_monthly_income = 200000.0,
|
||||||
|
min_annual_income = 2400000.0
|
||||||
|
WHERE order_position IN (20, 30, 40, 50);
|
||||||
|
|
||||||
|
-- 2. Medium Risk Occupations
|
||||||
|
UPDATE m_client_risk_rule
|
||||||
|
SET min_monthly_income = 50000.0,
|
||||||
|
max_monthly_income = 200000.0,
|
||||||
|
min_annual_income = 600000.0,
|
||||||
|
max_annual_income = 2400000.0
|
||||||
|
WHERE order_position BETWEEN 90 AND 240;
|
||||||
|
|
||||||
|
-- 3. Low Risk Occupations
|
||||||
|
UPDATE m_client_risk_rule
|
||||||
|
SET min_monthly_income = 0.0,
|
||||||
|
max_monthly_income = 50000.0,
|
||||||
|
min_annual_income = 0.0,
|
||||||
|
max_annual_income = 600000.0
|
||||||
|
WHERE order_position BETWEEN 280 AND 450;
|
||||||
|
|
||||||
|
-- 4. Set all remaining NULL values to 0.00
|
||||||
|
UPDATE m_client_risk_rule SET min_monthly_income = 0.0 WHERE min_monthly_income IS NULL;
|
||||||
|
UPDATE m_client_risk_rule SET max_monthly_income = 0.0 WHERE max_monthly_income IS NULL;
|
||||||
|
UPDATE m_client_risk_rule SET min_annual_income = 0.0 WHERE min_annual_income IS NULL;
|
||||||
|
UPDATE m_client_risk_rule SET max_annual_income = 0.0 WHERE max_annual_income IS NULL;
|
||||||
|
UPDATE m_client_risk_rule SET min_annual_turnover = 0.0 WHERE min_annual_turnover IS NULL;
|
||||||
|
UPDATE m_client_risk_rule SET max_annual_turnover = 0.0 WHERE max_annual_turnover IS NULL;
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0440-1">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_client WHERE external_id = 'CLIENT_RAJESH'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_client (
|
||||||
|
account_no, external_id, status_enum, office_id, display_name,
|
||||||
|
fullname, is_staff, activation_date, office_joining_date,
|
||||||
|
submittedon_date, legal_form_enum,
|
||||||
|
created_on_utc, created_by,
|
||||||
|
last_modified_on_utc, last_modified_by,
|
||||||
|
cif_number
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'ACC0000002', 'CLIENT_RAJESH', 300, 1, 'Rajesh Kumar',
|
||||||
|
'Rajesh Kumar', false, CURRENT_DATE, CURRENT_DATE,
|
||||||
|
CURRENT_DATE, 1,
|
||||||
|
NOW(), 1,
|
||||||
|
NOW(), 1,
|
||||||
|
'CIF_RAJESH'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'ACC0000003', 'CLIENT_SONIA', 300, 1, 'Sonia Sharma',
|
||||||
|
'Sonia Sharma', false, CURRENT_DATE, CURRENT_DATE,
|
||||||
|
CURRENT_DATE, 1,
|
||||||
|
NOW(), 1,
|
||||||
|
NOW(), 1,
|
||||||
|
'CIF_SONIA'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'ACC0000004', 'CLIENT_VIKRAM', 300, 1, 'Vikram Malhotra',
|
||||||
|
'Vikram Malhotra', false, CURRENT_DATE, CURRENT_DATE,
|
||||||
|
CURRENT_DATE, 1,
|
||||||
|
NOW(), 1,
|
||||||
|
NOW(), 1,
|
||||||
|
'CIF_VIKRAM'
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0440-2">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">
|
||||||
|
SELECT COUNT(*) FROM m_aml_alert a
|
||||||
|
JOIN m_client c ON a.client_id = c.id
|
||||||
|
WHERE c.external_id IN ('CLIENT_RAJESH', 'CLIENT_SONIA', 'CLIENT_VIKRAM')
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_aml_alert (
|
||||||
|
transaction_type, transaction_id, client_id, rule_id, amount, currency_code,
|
||||||
|
raised_on, status, created_by, created_on_utc, last_modified_by, last_modified_on_utc, version
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'SAVINGS', 1001,
|
||||||
|
(SELECT id FROM m_client WHERE external_id = 'CLIENT_RAJESH'),
|
||||||
|
(SELECT id FROM m_aml_transaction_rule WHERE rule_code = 'LARGE_CASH_TXN_25L'),
|
||||||
|
3500000.000000, 'INR',
|
||||||
|
NOW(), 'RAISED',
|
||||||
|
1, NOW(), 1, NOW(), 0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'SAVINGS', 1002,
|
||||||
|
(SELECT id FROM m_client WHERE external_id = 'CLIENT_SONIA'),
|
||||||
|
(SELECT id FROM m_aml_transaction_rule WHERE rule_code = 'STRUCTURING_BELOW_10L'),
|
||||||
|
995000.000000, 'INR',
|
||||||
|
NOW(), 'RAISED',
|
||||||
|
1, NOW(), 1, NOW(), 0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'SAVINGS', 1003,
|
||||||
|
(SELECT id FROM m_client WHERE external_id = 'CLIENT_VIKRAM'),
|
||||||
|
(SELECT id FROM m_aml_transaction_rule WHERE rule_code = 'PEP_ACCOUNT_MONITORING'),
|
||||||
|
75000.000000, 'INR',
|
||||||
|
NOW(), 'RAISED',
|
||||||
|
1, NOW(), 1, NOW(), 0
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Declarative min/max band for each AML transaction rule, used by the AML Rules
|
||||||
|
Master screen.
|
||||||
|
|
||||||
|
These are deliberately NOT the existing threshold_amount_medium_risk /
|
||||||
|
threshold_amount_high_risk columns: those are live alerting overrides consumed by
|
||||||
|
AmlRuleEvaluationServiceImpl.resolveThreshold(), and populating them purely to fill
|
||||||
|
a display column would change which transactions raise alerts. These new columns
|
||||||
|
are read-only documentation of the band a rule covers and never affect matching.
|
||||||
|
-->
|
||||||
|
<changeSet author="fineract" id="0441-1">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<columnExists tableName="m_aml_transaction_rule" columnName="min_limit_amount"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_aml_transaction_rule">
|
||||||
|
<column name="min_limit_amount" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="max_limit_amount" type="DECIMAL(19,6)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Band values. The minimum is the amount at which the rule starts to bite; the maximum
|
||||||
|
is the amount beyond which the exposure is treated as materially more serious and
|
||||||
|
escalated. A NULL maximum means the rule is genuinely open-ended.
|
||||||
|
-->
|
||||||
|
<changeSet author="fineract" id="0441-2">
|
||||||
|
<sql>
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 1000000, max_limit_amount = 2500000
|
||||||
|
WHERE rule_code = 'CTR_THRESHOLD_10L';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 1000000, max_limit_amount = 2500000
|
||||||
|
WHERE rule_code = 'STRUCTURED_CTR_10L_MONTHLY';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 2500000, max_limit_amount = 5000000
|
||||||
|
WHERE rule_code = 'LARGE_CASH_TXN_25L';
|
||||||
|
-- Structuring rules watch the band immediately BELOW the reporting threshold.
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 700000, max_limit_amount = 1000000
|
||||||
|
WHERE rule_code = 'STRUCTURING_BELOW_10L';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 35000, max_limit_amount = 50000
|
||||||
|
WHERE rule_code = 'STRUCTURING_BELOW_50K';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 900000, max_limit_amount = 1000000
|
||||||
|
WHERE rule_code = 'FREQUENT_CASH_NEAR_10L';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 50000, max_limit_amount = 200000
|
||||||
|
WHERE rule_code = 'FREQUENT_LOW_CASH_DEPOSIT';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 50000, max_limit_amount = 200000
|
||||||
|
WHERE rule_code = 'FREQUENT_LOW_CASH_WITHDRAWAL';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 500000, max_limit_amount = 2000000
|
||||||
|
WHERE rule_code = 'RTGS_HIGH_VALUE';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 2000000, max_limit_amount = 5000000
|
||||||
|
WHERE rule_code = 'JEWELLER_ACCOUNT_LARGE_TXN';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 50000, max_limit_amount = 200000
|
||||||
|
WHERE rule_code = 'CHARITABLE_ORG_CASH_WITHDRAWAL';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 50000, max_limit_amount = 500000
|
||||||
|
WHERE rule_code = 'PEP_ACCOUNT_MONITORING';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 500000, max_limit_amount = 1000000
|
||||||
|
WHERE rule_code = 'NEW_ACCOUNT_LARGE_TXN';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 200000, max_limit_amount = 500000
|
||||||
|
WHERE rule_code = 'LOAN_REPAYMENT_IN_CASH';
|
||||||
|
UPDATE m_aml_transaction_rule SET min_limit_amount = 1000000, max_limit_amount = 2500000
|
||||||
|
WHERE rule_code = 'LOW_BALANCE_HIGH_ACTIVITY';
|
||||||
|
-- RAPID_MOVEMENT_IN_ACCOUNTS is a pure velocity rule with no monetary threshold,
|
||||||
|
-- so it is intentionally left without a band.
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The three High-risk income rules were seeded with a floor but a zero ceiling, which the
|
||||||
|
UI cannot distinguish from "not captured". Normalise the zeros to NULL so an absent
|
||||||
|
ceiling reads as genuinely open-ended, and give the medium bands their missing floors.
|
||||||
|
-->
|
||||||
|
<changeSet author="fineract" id="0441-3">
|
||||||
|
<sql>
|
||||||
|
UPDATE m_client_risk_rule SET max_monthly_income = NULL WHERE max_monthly_income = 0;
|
||||||
|
UPDATE m_client_risk_rule SET max_annual_income = NULL WHERE max_annual_income = 0;
|
||||||
|
UPDATE m_client_risk_rule SET max_annual_turnover = NULL WHERE max_annual_turnover = 0;
|
||||||
|
UPDATE m_client_risk_rule SET min_monthly_income = NULL WHERE min_monthly_income = 0;
|
||||||
|
UPDATE m_client_risk_rule SET min_annual_income = NULL WHERE min_annual_income = 0;
|
||||||
|
UPDATE m_client_risk_rule SET min_annual_turnover = NULL WHERE min_annual_turnover = 0;
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Branch offices under Head Office.
|
||||||
|
|
||||||
|
Until now the tenant had a single office, so "see only my branch" had nothing to
|
||||||
|
scope to. external_id carries the branch code used by the General Ledger and the
|
||||||
|
RBI return screens, which is what lets a logged-in user's office be resolved to a
|
||||||
|
branch without matching on display names.
|
||||||
|
-->
|
||||||
|
<changeSet author="fineract" id="0442-1">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_office WHERE external_id IN ('0002','0003','0004','0005','0006')</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_office (parent_id, hierarchy, external_id, name, opening_date)
|
||||||
|
VALUES
|
||||||
|
((SELECT id FROM m_office WHERE hierarchy = '.'), '.2.', '0002', 'Andheri (E)', DATE '2018-04-01'),
|
||||||
|
((SELECT id FROM m_office WHERE hierarchy = '.'), '.3.', '0003', 'Bandra (W)', DATE '2018-04-01'),
|
||||||
|
((SELECT id FROM m_office WHERE hierarchy = '.'), '.4.', '0004', 'Borivali', DATE '2019-07-01'),
|
||||||
|
((SELECT id FROM m_office WHERE hierarchy = '.'), '.5.', '0005', 'Dadar', DATE '2020-01-02'),
|
||||||
|
((SELECT id FROM m_office WHERE hierarchy = '.'), '.6.', '0006', 'Thane', DATE '2021-06-01');
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Head Office carries the branch code the GL ledger uses for it. -->
|
||||||
|
<changeSet author="fineract" id="0442-2">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_office WHERE external_id = 'HO'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_office SET external_id = 'HO' WHERE hierarchy = '.';
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
m_aml_alert.rule_id is NOT NULL, so an alert a user raises by hand from the ledger still
|
||||||
|
needs a rule to hang off. MANUAL_REFERRAL is that rule.
|
||||||
|
|
||||||
|
It is seeded with is_active = false ON PURPOSE. AmlRuleEvaluationServiceImpl selects
|
||||||
|
candidates with findByActiveTrueAndAppliesTo, so an active ALL-scoped rule with a null
|
||||||
|
threshold would be evaluated against every live transaction. Inactive keeps it out of the
|
||||||
|
engine entirely; the referral service loads it by rule_code instead.
|
||||||
|
-->
|
||||||
|
<changeSet author="fineract" id="0443-1">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_aml_transaction_rule WHERE rule_code = 'MANUAL_REFERRAL'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_aml_transaction_rule (rule_code, description, applies_to, threshold_amount, rule_type, window_days, source_client_risk_rule_id, is_active)
|
||||||
|
VALUES ('MANUAL_REFERRAL', 'Marked suspicious by a user from the general ledger - no automated rule fired', 'ALL', null, 'THRESHOLD', null, null, false);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user