Compare commits
3
Commits
uate
..
7773a9e9c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7773a9e9c6 | ||
|
|
c890faee78 | ||
|
|
abc7eb22a2 |
@@ -1,18 +0,0 @@
|
||||
const https = require('https');
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
|
||||
fetch('https://localhost:8443/fineract-provider/api/v1/authentication?username=mifos&password=password&tenantIdentifier=default', {
|
||||
method: 'POST',
|
||||
agent: agent
|
||||
}).then(res => res.json()).then(data => {
|
||||
const token = data.base64EncodedAuthenticationKey;
|
||||
return fetch('https://localhost:8443/fineract-provider/api/v1/clients/9/accounts?tenantIdentifier=default', {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + token,
|
||||
'Fineract-Platform-TenantId': 'default'
|
||||
},
|
||||
agent: agent
|
||||
});
|
||||
}).then(res => res.json()).then(data => {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}).catch(console.error);
|
||||
+6
@@ -35,4 +35,10 @@ public interface AmlAlertRepository extends JpaRepository<AmlAlert, Long> {
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
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)
|
||||
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)
|
||||
private String ruleType;
|
||||
|
||||
@@ -86,6 +106,22 @@ public class AmlTransactionRule {
|
||||
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() {
|
||||
return AmlRuleType.valueOf(ruleType);
|
||||
}
|
||||
|
||||
+8
@@ -19,9 +19,17 @@
|
||||
package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AmlTransactionRuleRepository extends JpaRepository<AmlTransactionRule, Long> {
|
||||
|
||||
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.security.service.PlatformSecurityContext;
|
||||
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.AmlAlertWritePlatformService;
|
||||
import org.apache.fineract.portfolio.aml.service.JournalEntryReferralService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/aml/alerts")
|
||||
@@ -46,7 +48,9 @@ public class AmlAlertApiResource {
|
||||
private final PlatformSecurityContext context;
|
||||
private final AmlAlertReadPlatformService readPlatformService;
|
||||
private final AmlAlertWritePlatformService writePlatformService;
|
||||
private final JournalEntryReferralService referralService;
|
||||
private final DefaultToApiJsonSerializer<AmlAlertData> toApiJsonSerializer;
|
||||
private final DefaultToApiJsonSerializer<ManualReferralData> referralJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@@ -76,4 +80,28 @@ public class AmlAlertApiResource {
|
||||
.orElse(null);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-7
@@ -16,14 +16,24 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.welcomekit.service;
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
public interface WelcomeKitWritePlatformService {
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
CommandProcessingResult dispatchWelcomeKit(Long clientId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateWelcomeKitStatus(Long welcomeKitId, JsonCommand command);
|
||||
private Long journalEntryId;
|
||||
private String disposition;
|
||||
private String notes;
|
||||
}
|
||||
+12
-12
@@ -16,18 +16,18 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.welcomekit.data;
|
||||
package org.apache.fineract.portfolio.aml.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ClientWelcomeKitData implements Serializable {
|
||||
|
||||
private final String clientName;
|
||||
private final boolean canDispatchNew;
|
||||
private final List<WelcomeKitDispatchData> dispatches;
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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.AmlTransactionRule;
|
||||
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.transaction.annotation.Transactional;
|
||||
|
||||
@@ -35,8 +38,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Transactional
|
||||
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 AmlAlertRepository alertRepository;
|
||||
private final ClientKycRepository clientKycRepository;
|
||||
|
||||
@Override
|
||||
public List<AmlAlert> evaluate(AmlTransactionContext context) {
|
||||
@@ -56,20 +63,42 @@ public class AmlRuleEvaluationServiceImpl implements AmlRuleEvaluationService {
|
||||
}
|
||||
|
||||
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
|
||||
// per rule and are not yet evaluated here - they exist in the catalog for future work.
|
||||
return false;
|
||||
}
|
||||
switch (rule.getRuleType()) {
|
||||
case THRESHOLD:
|
||||
return context.getAmount().compareTo(rule.getThresholdAmount()) >= 0;
|
||||
return context.getAmount().compareTo(threshold) >= 0;
|
||||
case STRUCTURING:
|
||||
case VELOCITY:
|
||||
return context.getWindowAggregateAmount() != null
|
||||
&& context.getWindowAggregateAmount().compareTo(rule.getThresholdAmount()) >= 0;
|
||||
&& context.getWindowAggregateAmount().compareTo(threshold) >= 0;
|
||||
default:
|
||||
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 ecgcSalFlag;
|
||||
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,
|
||||
final Long occupationCategoryCvId, final String occupationCategoryName,
|
||||
@@ -55,7 +69,10 @@ public final class KycRiskRuleData implements Serializable {
|
||||
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover,
|
||||
final Integer riskCategory, final String reason, final Integer orderPosition,
|
||||
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.ruleName = ruleName;
|
||||
this.legalFormId = legalFormId;
|
||||
@@ -77,6 +94,13 @@ public final class KycRiskRuleData implements Serializable {
|
||||
this.willfulDefaulter = willfulDefaulter;
|
||||
this.ecgcSalFlag = ecgcSalFlag;
|
||||
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() {
|
||||
@@ -162,4 +186,32 @@ public final class KycRiskRuleData implements Serializable {
|
||||
public String getDormantFlag() {
|
||||
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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
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.domain.ClientRiskRule;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRiskRuleRepository;
|
||||
@@ -33,14 +37,18 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlatformService {
|
||||
|
||||
private final ClientRiskRuleRepository repository;
|
||||
private final AmlTransactionRuleRepository amlTransactionRuleRepository;
|
||||
|
||||
@Override
|
||||
public List<KycRiskRuleData> retrieveAllRules() {
|
||||
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 String occCatName = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getLabel() : null;
|
||||
final Long occId = rule.getOccupation() != null ? rule.getOccupation().getId() : null;
|
||||
@@ -67,7 +75,14 @@ public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlat
|
||||
rule.getBannedEntityMatch(),
|
||||
rule.getWillfulDefaulter(),
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
package org.apache.fineract.portfolio.shareaccounts.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData;
|
||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/shareaccounts/summary")
|
||||
@Component
|
||||
@Tag(name = "Share Account Summary", description = "Provides a highly optimized summary of all share accounts linked with their respective clients.")
|
||||
@RequiredArgsConstructor
|
||||
public class ShareAccountSummaryApiResource {
|
||||
|
||||
private final PlatformSecurityContext platformSecurityContext;
|
||||
private final ShareAccountReadPlatformService shareAccountReadPlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List all share accounts with client details", description = "Retrieves all share accounts along with their associated client details in a single optimized query.\n\n"
|
||||
+ "Example Requests:\n\n" + "shareaccounts/summary")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public Collection<ShareAccountClientSummaryData> retrieveAllSummaries(@QueryParam("searchTerm") final String searchTerm) {
|
||||
long start = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Start of retrieveAllSummaries (before auth)");
|
||||
this.platformSecurityContext.authenticatedUser();
|
||||
long afterAuth = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Auth check: " + (afterAuth - start) + "ms");
|
||||
Collection<ShareAccountClientSummaryData> result = this.shareAccountReadPlatformService.retrieveAllShareAccountClientSummaries(searchTerm);
|
||||
long afterService = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Service call total (including query): " + (afterService - afterAuth) + "ms");
|
||||
System.out.println("[TIMING] Right before response returned: " + (System.currentTimeMillis() - start) + "ms total so far");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package org.apache.fineract.portfolio.shareaccounts.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountStatusEnumData;
|
||||
|
||||
@Getter
|
||||
public class ShareAccountClientSummaryData implements Serializable {
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientName;
|
||||
private final Long officeId;
|
||||
private final String officeName;
|
||||
private final String accountNo;
|
||||
private final Long productId;
|
||||
private final String productName;
|
||||
private final Long totalApprovedShares;
|
||||
private final Long totalPendingForApprovalShares;
|
||||
private final CurrencyData currency;
|
||||
private final LocalDate activatedDate;
|
||||
private final ShareAccountStatusEnumData status;
|
||||
|
||||
public ShareAccountClientSummaryData(final Long id, final Long clientId, final String clientName, final Long officeId, final String officeName, final String accountNo,
|
||||
final Long productId, final String productName, final Long totalApprovedShares, final Long totalPendingForApprovalShares,
|
||||
final CurrencyData currency, final LocalDate activatedDate, final ShareAccountStatusEnumData status) {
|
||||
this.id = id;
|
||||
this.clientId = clientId;
|
||||
this.clientName = clientName;
|
||||
this.officeId = officeId;
|
||||
this.officeName = officeName;
|
||||
this.accountNo = accountNo;
|
||||
this.productId = productId;
|
||||
this.productName = productName;
|
||||
|
||||
if (totalApprovedShares == null) {
|
||||
this.totalApprovedShares = Long.valueOf(0);
|
||||
} else {
|
||||
this.totalApprovedShares = totalApprovedShares;
|
||||
}
|
||||
|
||||
if (totalPendingForApprovalShares == null) {
|
||||
this.totalPendingForApprovalShares = Long.valueOf(0);
|
||||
} else {
|
||||
this.totalPendingForApprovalShares = totalPendingForApprovalShares;
|
||||
}
|
||||
|
||||
this.currency = currency;
|
||||
this.activatedDate = activatedDate;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
-3
@@ -23,7 +23,6 @@ import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.portfolio.accounts.service.AccountReadPlatformService;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountData;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData;
|
||||
|
||||
public interface ShareAccountReadPlatformService extends AccountReadPlatformService {
|
||||
|
||||
@@ -39,7 +38,5 @@ public interface ShareAccountReadPlatformService extends AccountReadPlatformServ
|
||||
Collection<ShareAccountData> retrieveAllShareAccountDataForDividends(Long productId, boolean fetchInActiveAccounts,
|
||||
LocalDate startDate);
|
||||
|
||||
Collection<ShareAccountClientSummaryData> retrieveAllShareAccountClientSummaries(String searchTerm);
|
||||
|
||||
String retrieveAccountNumberByAccountId(Long accountId);
|
||||
}
|
||||
|
||||
-81
@@ -62,7 +62,6 @@ import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountStatusType
|
||||
import org.apache.fineract.portfolio.shareproducts.data.ShareProductData;
|
||||
import org.apache.fineract.portfolio.shareproducts.data.ShareProductMarketPriceData;
|
||||
import org.apache.fineract.portfolio.shareproducts.service.ShareProductDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -222,29 +221,6 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
|
||||
}, mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ShareAccountClientSummaryData> retrieveAllShareAccountClientSummaries(String searchTerm) {
|
||||
long start = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Inside retrieveAllShareAccountClientSummaries, before query builder setup");
|
||||
ShareAccountClientSummaryMapper mapper = new ShareAccountClientSummaryMapper();
|
||||
StringBuilder queryBuilder = new StringBuilder("select ").append(mapper.schema());
|
||||
List<Object> paramList = new ArrayList<>();
|
||||
|
||||
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
|
||||
queryBuilder.append(" where (c.display_name ILIKE ? OR sa.account_no ILIKE ?)");
|
||||
String searchPattern = "%" + searchTerm + "%";
|
||||
paramList.add(searchPattern);
|
||||
paramList.add(searchPattern);
|
||||
}
|
||||
|
||||
long beforeQuery = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Before actual SQL query call. Time since service start: " + (beforeQuery - start) + "ms");
|
||||
Collection<ShareAccountClientSummaryData> result = this.jdbcTemplate.query(queryBuilder.toString(), mapper, paramList.toArray());
|
||||
long afterQuery = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Query execution (JdbcTemplate): " + (afterQuery - beforeQuery) + "ms");
|
||||
return result;
|
||||
}
|
||||
|
||||
public Collection<ShareAccountChargeData> convertChargesToShareAccountCharges(Collection<ChargeData> productCharges) {
|
||||
final Collection<ShareAccountChargeData> savingsCharges = new ArrayList<>();
|
||||
for (final ChargeData chargeData : productCharges) {
|
||||
@@ -519,63 +495,6 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ShareAccountClientSummaryMapper implements RowMapper<ShareAccountClientSummaryData> {
|
||||
|
||||
private final String schema;
|
||||
|
||||
ShareAccountClientSummaryMapper() {
|
||||
StringBuilder buff = new StringBuilder().append("sa.id as id, c.id as clientId, c.display_name as clientName, ")
|
||||
.append("sa.account_no as accountNo, p.id as productId, p.name as productName, ")
|
||||
.append("sa.total_approved_shares as approvedShares, sa.total_pending_shares as pendingShares, ")
|
||||
.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, ")
|
||||
.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ")
|
||||
.append("curr.display_symbol as currencyDisplaySymbol, sa.activated_date as activatedDate, ")
|
||||
.append("sa.status_enum as statusEnum, o.id as officeId, o.name as officeName ")
|
||||
.append("from m_share_account sa ")
|
||||
.append("inner join m_client c ON c.id = sa.client_id ")
|
||||
.append("inner join m_office o ON o.id = c.office_id ")
|
||||
.append("inner join m_share_product p on p.id = sa.product_id ")
|
||||
.append("inner join m_currency curr on curr.code = sa.currency_code");
|
||||
this.schema = buff.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShareAccountClientSummaryData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
|
||||
final Long id = rs.getLong("id");
|
||||
final Long clientId = rs.getLong("clientId");
|
||||
final String clientName = rs.getString("clientName");
|
||||
final String accountNo = rs.getString("accountNo");
|
||||
final Long productId = rs.getLong("productId");
|
||||
final String productName = rs.getString("productName");
|
||||
final Long totalApprovedShares = JdbcSupport.getLong(rs, "approvedShares");
|
||||
final Long totalPendingForApprovalShares = JdbcSupport.getLong(rs, "pendingShares");
|
||||
|
||||
final String currencyCode = rs.getString("currencyCode");
|
||||
final String currencyName = rs.getString("currencyName");
|
||||
final String currencyNameCode = rs.getString("currencyNameCode");
|
||||
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
|
||||
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
|
||||
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
|
||||
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
|
||||
currencyDisplaySymbol, currencyNameCode);
|
||||
|
||||
final LocalDate activatedDate = JdbcSupport.getLocalDate(rs, "activatedDate");
|
||||
|
||||
final Integer statusEnum = rs.getInt("statusEnum");
|
||||
final ShareAccountStatusEnumData status = SharesEnumerations.status(statusEnum);
|
||||
|
||||
final Long officeId = rs.getLong("officeId");
|
||||
final String officeName = rs.getString("officeName");
|
||||
|
||||
return new ShareAccountClientSummaryData(id, clientId, clientName, officeId, officeName, accountNo, productId, productName,
|
||||
totalApprovedShares, totalPendingForApprovalShares, currency, activatedDate, status);
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.schema;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retrieveAccountNumberByAccountId(Long accountId) {
|
||||
try {
|
||||
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
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.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitReadPlatformService;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/clients/{clientId}/welcomekit")
|
||||
@Component
|
||||
@Tag(name = "Welcome Kit Dispatch", description = "Manages the dispatching and tracking of welcome kits for clients.")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientWelcomeKitApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WELCOMEKIT";
|
||||
private final PlatformSecurityContext context;
|
||||
private final WelcomeKitReadPlatformService readPlatformService;
|
||||
private final WelcomeKitWritePlatformService writePlatformService;
|
||||
private final FromJsonHelper fromJsonHelper;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List all welcome kits for a Client")
|
||||
public ClientWelcomeKitData getClientWelcomeKits(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return this.readPlatformService.retrieveWelcomeKitsByClientId(clientId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create a new welcome kit dispatch")
|
||||
public CommandProcessingResult dispatchWelcomeKit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CREATE_" + RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(clientId, this.fromJsonHelper.parse(apiRequestBodyAsJson), this.fromJsonHelper);
|
||||
|
||||
return this.writePlatformService.dispatchWelcomeKit(clientId, command);
|
||||
}
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
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.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitReadPlatformService;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/welcomekit")
|
||||
@Component
|
||||
@Tag(name = "Welcome Kit Dispatch", description = "Manages the dispatching and tracking of welcome kits for clients.")
|
||||
@RequiredArgsConstructor
|
||||
public class WelcomeKitApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WELCOMEKIT";
|
||||
private final PlatformSecurityContext context;
|
||||
private final WelcomeKitWritePlatformService writePlatformService;
|
||||
private final FromJsonHelper fromJsonHelper;
|
||||
|
||||
@PUT
|
||||
@Path("{welcomeKitId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Update the status of an existing dispatch")
|
||||
public CommandProcessingResult updateWelcomeKitStatus(
|
||||
@PathParam("welcomeKitId") @Parameter(description = "welcomeKitId") final Long welcomeKitId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("UPDATE_" + RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(welcomeKitId, this.fromJsonHelper.parse(apiRequestBodyAsJson), this.fromJsonHelper);
|
||||
|
||||
return this.writePlatformService.updateWelcomeKitStatus(welcomeKitId, command);
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class WelcomeKitDispatchData implements Serializable {
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientName;
|
||||
private final String deliveryAddress;
|
||||
private final String deliveryMode;
|
||||
private final String deliveryModeOtherText;
|
||||
private final String trackingId;
|
||||
private final String status;
|
||||
private final LocalDate dispatchedOnDate;
|
||||
private final LocalDate deliveredOnDate;
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_welcome_kit_dispatch")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class WelcomeKitDispatch extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@Column(name = "delivery_address", length = 500, nullable = false)
|
||||
private String deliveryAddress;
|
||||
|
||||
@Column(name = "delivery_mode", length = 50, nullable = false)
|
||||
private String deliveryMode;
|
||||
|
||||
@Column(name = "delivery_mode_other_text", length = 255)
|
||||
private String deliveryModeOtherText;
|
||||
|
||||
@Column(name = "tracking_id", length = 100, nullable = false, unique = true)
|
||||
private String trackingId;
|
||||
|
||||
@Column(name = "status", length = 30, nullable = false)
|
||||
private String status;
|
||||
|
||||
@Column(name = "dispatched_on_date", nullable = false)
|
||||
private LocalDate dispatchedOnDate;
|
||||
|
||||
@Column(name = "delivered_on_date")
|
||||
private LocalDate deliveredOnDate;
|
||||
|
||||
@CreatedBy
|
||||
@Column(name = "created_by", updatable = false)
|
||||
private Long createdBy;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_date", updatable = false)
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
@LastModifiedBy
|
||||
@Column(name = "last_modified_by")
|
||||
private Long lastModifiedBy;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "last_modified_date")
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
protected WelcomeKitDispatch() {
|
||||
// Default constructor for JPA
|
||||
}
|
||||
|
||||
private WelcomeKitDispatch(final Client client, final String deliveryAddress, final String deliveryMode,
|
||||
final String deliveryModeOtherText, final String trackingId, final String status,
|
||||
final LocalDate dispatchedOnDate) {
|
||||
this.client = client;
|
||||
this.deliveryAddress = deliveryAddress;
|
||||
this.deliveryMode = deliveryMode;
|
||||
this.deliveryModeOtherText = deliveryModeOtherText;
|
||||
this.trackingId = trackingId;
|
||||
this.status = status;
|
||||
this.dispatchedOnDate = dispatchedOnDate;
|
||||
}
|
||||
|
||||
public static WelcomeKitDispatch createNew(final Client client, final String deliveryAddress, final String deliveryMode,
|
||||
final String deliveryModeOtherText, final String trackingId, final LocalDate dispatchedOnDate) {
|
||||
return new WelcomeKitDispatch(client, deliveryAddress, deliveryMode, deliveryModeOtherText, trackingId, "DISPATCHED", dispatchedOnDate);
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public void updateStatus(final String newStatus, final LocalDate deliveredOnDate) {
|
||||
this.status = newStatus;
|
||||
if ("DELIVERED".equals(newStatus)) {
|
||||
this.deliveredOnDate = deliveredOnDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface WelcomeKitDispatchRepository extends JpaRepository<WelcomeKitDispatch, Long>, JpaSpecificationExecutor<WelcomeKitDispatch> {
|
||||
|
||||
List<WelcomeKitDispatch> findByClientIdOrderByCreatedDateDesc(Long clientId);
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.service;
|
||||
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
|
||||
public interface WelcomeKitReadPlatformService {
|
||||
|
||||
ClientWelcomeKitData retrieveWelcomeKitsByClientId(Long clientId);
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.WelcomeKitDispatchData;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WelcomeKitReadPlatformServiceImpl implements WelcomeKitReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public ClientWelcomeKitData retrieveWelcomeKitsByClientId(Long clientId) {
|
||||
final WelcomeKitDispatchMapper rm = new WelcomeKitDispatchMapper();
|
||||
final String sql = "SELECT " + rm.schema() + " WHERE w.client_id = ? ORDER BY w.created_date DESC";
|
||||
|
||||
List<WelcomeKitDispatchData> dispatches = this.jdbcTemplate.query(sql, rm, clientId);
|
||||
|
||||
String clientName = this.jdbcTemplate.queryForObject("SELECT display_name FROM m_client WHERE id = ?", String.class, clientId);
|
||||
|
||||
boolean canDispatchNew = true;
|
||||
if (!dispatches.isEmpty()) {
|
||||
WelcomeKitDispatchData latest = dispatches.get(0);
|
||||
if ("RETURNED".equals(latest.getStatus())) {
|
||||
canDispatchNew = true;
|
||||
} else {
|
||||
canDispatchNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
return ClientWelcomeKitData.builder()
|
||||
.clientName(clientName)
|
||||
.canDispatchNew(canDispatchNew)
|
||||
.dispatches(dispatches)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static final class WelcomeKitDispatchMapper implements RowMapper<WelcomeKitDispatchData> {
|
||||
|
||||
public String schema() {
|
||||
return " w.id as id, w.client_id as clientId, c.display_name as clientName, "
|
||||
+ " w.delivery_address as deliveryAddress, w.delivery_mode as deliveryMode, "
|
||||
+ " w.delivery_mode_other_text as deliveryModeOtherText, w.tracking_id as trackingId, "
|
||||
+ " w.status as status, w.dispatched_on_date as dispatchedOnDate, w.delivered_on_date as deliveredOnDate "
|
||||
+ " FROM m_welcome_kit_dispatch w "
|
||||
+ " JOIN m_client c ON c.id = w.client_id ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WelcomeKitDispatchData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
final Long id = rs.getLong("id");
|
||||
final Long clientId = rs.getLong("clientId");
|
||||
final String clientName = rs.getString("clientName");
|
||||
final String deliveryAddress = rs.getString("deliveryAddress");
|
||||
final String deliveryMode = rs.getString("deliveryMode");
|
||||
final String deliveryModeOtherText = rs.getString("deliveryModeOtherText");
|
||||
final String trackingId = rs.getString("trackingId");
|
||||
final String status = rs.getString("status");
|
||||
final LocalDate dispatchedOnDate = JdbcSupport.getLocalDate(rs, "dispatchedOnDate");
|
||||
final LocalDate deliveredOnDate = JdbcSupport.getLocalDate(rs, "deliveredOnDate");
|
||||
|
||||
return WelcomeKitDispatchData.builder()
|
||||
.id(id)
|
||||
.clientId(clientId)
|
||||
.clientName(clientName)
|
||||
.deliveryAddress(deliveryAddress)
|
||||
.deliveryMode(deliveryMode)
|
||||
.deliveryModeOtherText(deliveryModeOtherText)
|
||||
.trackingId(trackingId)
|
||||
.status(status)
|
||||
.dispatchedOnDate(dispatchedOnDate)
|
||||
.deliveredOnDate(deliveredOnDate)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* 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.welcomekit.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
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.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.welcomekit.domain.WelcomeKitDispatch;
|
||||
import org.apache.fineract.portfolio.welcomekit.domain.WelcomeKitDispatchRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WelcomeKitWritePlatformServiceImpl implements WelcomeKitWritePlatformService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientRepositoryWrapper clientRepository;
|
||||
private final WelcomeKitDispatchRepository welcomeKitDispatchRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult dispatchWelcomeKit(final Long clientId, final JsonCommand command) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final String deliveryAddress = command.stringValueOfParameterNamed("deliveryAddress");
|
||||
final String deliveryMode = command.stringValueOfParameterNamed("deliveryMode");
|
||||
final String deliveryModeOtherText = command.stringValueOfParameterNamed("deliveryModeOtherText");
|
||||
final LocalDate dispatchedOnDate = command.localDateValueOfParameterNamed("dispatchedOnDate");
|
||||
|
||||
if (deliveryAddress == null || deliveryAddress.trim().isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.deliveryAddress.cannot.be.blank",
|
||||
"Delivery address cannot be blank.");
|
||||
}
|
||||
if (deliveryMode == null || deliveryMode.trim().isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.deliveryMode.cannot.be.blank",
|
||||
"Delivery mode cannot be blank.");
|
||||
}
|
||||
if (dispatchedOnDate == null) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.dispatchedOnDate.cannot.be.blank",
|
||||
"Dispatched on date cannot be blank.");
|
||||
}
|
||||
|
||||
final String trackingId = "WK-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
|
||||
final WelcomeKitDispatch newDispatch = WelcomeKitDispatch.createNew(client, deliveryAddress, deliveryMode, deliveryModeOtherText,
|
||||
trackingId, dispatchedOnDate);
|
||||
|
||||
this.welcomeKitDispatchRepository.saveAndFlush(newDispatch);
|
||||
|
||||
return new CommandProcessingResultBuilder()
|
||||
.withCommandId(command.commandId())
|
||||
.withClientId(clientId)
|
||||
.withEntityId(newDispatch.getId())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult updateWelcomeKitStatus(final Long welcomeKitId, final JsonCommand command) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final WelcomeKitDispatch dispatch = this.welcomeKitDispatchRepository.findById(welcomeKitId).orElseThrow(
|
||||
() -> new PlatformDataIntegrityException("error.msg.welcomekit.not.found", "Welcome kit not found."));
|
||||
|
||||
final String status = command.stringValueOfParameterNamed("status");
|
||||
|
||||
if (status == null || status.trim().isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.status.cannot.be.blank", "Status cannot be blank.");
|
||||
}
|
||||
|
||||
LocalDate deliveredOnDate = null;
|
||||
if ("DELIVERED".equals(status)) {
|
||||
deliveredOnDate = LocalDate.now();
|
||||
}
|
||||
|
||||
dispatch.updateStatus(status, deliveredOnDate);
|
||||
|
||||
this.welcomeKitDispatchRepository.saveAndFlush(dispatch);
|
||||
|
||||
return new CommandProcessingResultBuilder()
|
||||
.withCommandId(command.commandId())
|
||||
.withClientId(dispatch.getClient().getId())
|
||||
.withEntityId(dispatch.getId())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
|
||||
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
|
||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966}
|
||||
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
|
||||
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
||||
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
|
||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966}
|
||||
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||
|
||||
@@ -435,7 +435,6 @@
|
||||
<include file="parts/0412_activate_outward_clearing_batch_job.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0413_inward_item_verify_signature_permission.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0414_inward_item_full_source_details.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0415_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0415_add_otp_delivery_method_and_first_login_gate_to_appuser.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0416_create_user_agreement_tables.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0417_add_first_login_gate_config.xml" relativeToChangelogFile="true"/>
|
||||
@@ -491,4 +490,10 @@
|
||||
<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/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>
|
||||
|
||||
+16
-24
@@ -23,32 +23,24 @@
|
||||
|
||||
<!-- Dedicated liability GL account for the Sponsor DD Payable balance owed to SBI, as a child
|
||||
of the existing "OTHER LIABILITY" account (gl_code 9), matching the pattern already used
|
||||
by "INTEREST PAYABLE" (gl_code 9.2).
|
||||
|
||||
parent_id is resolved by gl_code (not hardcoded) — the "OTHER LIABILITY" row's numeric id
|
||||
is an auto-increment value that depends on each tenant's own insert history, so a
|
||||
hardcoded id (previously 22) only happened to match the `default` tenant and broke FK
|
||||
inserts on any tenant whose chart of accounts was seeded differently (e.g. `saga`), which
|
||||
aborted that tenant's whole Liquibase run — and with it, the entire application startup.
|
||||
runOnChange lets this corrected version safely re-validate on tenants where the old,
|
||||
already-recorded version succeeded: the "gl_code 9.3 doesn't exist yet" precondition below
|
||||
is already false there, so it's skipped rather than re-inserted. -->
|
||||
<changeSet author="fineract" id="0345-1-insert-sponsor-dd-payable-sbi-gl-account" runOnChange="true">
|
||||
by "INTEREST PAYABLE" (gl_code 9.2). -->
|
||||
<changeSet author="fineract" id="0345-1-insert-sponsor-dd-payable-sbi-gl-account">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<and>
|
||||
<not>
|
||||
<sqlCheck expectedResult="1">select count(*) from acc_gl_account where gl_code = '9.3'</sqlCheck>
|
||||
</not>
|
||||
<sqlCheck expectedResult="1">select count(*) from acc_gl_account where gl_code = '9'</sqlCheck>
|
||||
</and>
|
||||
<not>
|
||||
<sqlCheck expectedResult="1">select count(*) from acc_gl_account where gl_code = '9.3'</sqlCheck>
|
||||
</not>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO acc_gl_account (name, gl_code, parent_id, hierarchy, account_usage, classification_enum, disabled, manual_journal_entries_allowed, description)
|
||||
SELECT 'Sponsor DD Payable - SBI', '9.3', parent.id, '.', 1, 2, FALSE, TRUE,
|
||||
'Liability owed to SBI for sponsor bank demand drafts issued on their behalf, pending clearing.'
|
||||
FROM acc_gl_account parent
|
||||
WHERE parent.gl_code = '9';
|
||||
</sql>
|
||||
<insert tableName="acc_gl_account">
|
||||
<column name="name" value="Sponsor DD Payable - SBI"/>
|
||||
<column name="gl_code" value="9.3"/>
|
||||
<column name="parent_id" valueNumeric="22"/>
|
||||
<column name="hierarchy" value="."/>
|
||||
<column name="account_usage" valueNumeric="1"/>
|
||||
<column name="classification_enum" valueNumeric="2"/>
|
||||
<column name="disabled" valueBoolean="false"/>
|
||||
<column name="manual_journal_entries_allowed" valueBoolean="true"/>
|
||||
<column name="description" value="Liability owed to SBI for sponsor bank demand drafts issued on their behalf, pending clearing."/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<!-- Fineract's GL hierarchy path embeds the row's own auto-generated id, so it can only be
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.1.xsd">
|
||||
|
||||
<changeSet id="0415_welcome_kit_dispatch" author="CBS" runOnChange="false">
|
||||
<sql>
|
||||
CREATE TABLE m_welcome_kit_dispatch (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
client_id BIGINT NOT NULL REFERENCES m_client(id),
|
||||
delivery_address VARCHAR(500) NOT NULL,
|
||||
delivery_mode VARCHAR(50) NOT NULL,
|
||||
delivery_mode_other_text VARCHAR(255),
|
||||
tracking_id VARCHAR(100) NOT NULL UNIQUE,
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'DISPATCHED',
|
||||
dispatched_on_date DATE NOT NULL,
|
||||
delivered_on_date DATE,
|
||||
created_by BIGINT,
|
||||
created_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
last_modified_by BIGINT,
|
||||
last_modified_date TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'READ_WELCOMEKIT', 'WELCOMEKIT', 'READ', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_WELCOMEKIT');
|
||||
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'CREATE_WELCOMEKIT', 'WELCOMEKIT', 'CREATE', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_WELCOMEKIT');
|
||||
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'UPDATE_WELCOMEKIT', 'WELCOMEKIT', 'UPDATE', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_WELCOMEKIT');
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+13
-20
@@ -32,13 +32,6 @@
|
||||
Threshold numbers marked "illustrative default" mirror the bank's own Rules Parameter
|
||||
screens where a configured value was available (Rules 1, 6, 10, 11, 12, 13); others are
|
||||
reasonable starting points meant to be tuned via the AML rule administration UI.
|
||||
|
||||
order_position is only unique WITHIN 0430's own 221-row insert - it collides with the
|
||||
older, differently-numbered seed rows from 0428_add_client_risk_rules.xml (e.g. both
|
||||
insert a row at order_position 10 and 30), which made every subquery below ambiguous
|
||||
("more than one row returned") and aborted the whole tenant migration. aml_check_status
|
||||
is populated only on 0430's rows (0428's predate that column and are NULL there), so
|
||||
pairing it with order_position uniquely targets the intended 0430 row.
|
||||
-->
|
||||
<changeSet author="fineract" id="0434-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
@@ -49,19 +42,19 @@
|
||||
VALUES
|
||||
('CTR_THRESHOLD_10L', 'Mandatory CTR reporting threshold - single cash transaction of INR 10,00,000 or more', 'ALL', 1000000, 'THRESHOLD', null, null, true),
|
||||
('STRUCTURED_CTR_10L_MONTHLY', 'Connected cash transactions summing to INR 10,00,000 or more within a calendar month (CTR aggregation rule)', 'ALL', 1000000, 'STRUCTURING', 30, null, true),
|
||||
('LARGE_CASH_TXN_25L', 'Large cash transaction', 'ALL', 2500000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 6 AND aml_check_status IS NOT NULL), true),
|
||||
('STRUCTURING_BELOW_10L', 'Splitting of cash deposits just below INR 10,00,000 in multiple accounts in a month', 'SAVINGS', 1000000, 'STRUCTURING', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 28 AND aml_check_status IS NOT NULL), true),
|
||||
('STRUCTURING_BELOW_50K', 'Splitting of cash deposits just below INR 50,000', 'SAVINGS', 50000, 'STRUCTURING', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 29 AND aml_check_status IS NOT NULL), true),
|
||||
('FREQUENT_LOW_CASH_DEPOSIT', 'Frequent low cash deposits', 'SAVINGS', 50000, 'VELOCITY', 7, (SELECT id FROM m_client_risk_rule WHERE order_position = 30 AND aml_check_status IS NOT NULL), true),
|
||||
('FREQUENT_LOW_CASH_WITHDRAWAL', 'Frequent low cash withdrawals', 'SAVINGS', 50000, 'VELOCITY', 7, (SELECT id FROM m_client_risk_rule WHERE order_position = 31 AND aml_check_status IS NOT NULL), true),
|
||||
('FREQUENT_CASH_NEAR_10L', 'Frequent cash deposits just below INR 10,00,000', 'SAVINGS', 1000000, 'STRUCTURING', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 65 AND aml_check_status IS NOT NULL), true),
|
||||
('RTGS_HIGH_VALUE', 'Monitoring RTGS Transactions', 'TRANSFER', 2000000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 12 AND aml_check_status IS NOT NULL), true),
|
||||
('JEWELLER_ACCOUNT_LARGE_TXN', 'Monitoring accounts of Jewellers', 'ALL', 2000000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 13 AND aml_check_status IS NOT NULL), true),
|
||||
('CHARITABLE_ORG_CASH_WITHDRAWAL', 'Substantial cash withdrawals in accounts of charitable organization', 'SAVINGS', 50000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 10 AND aml_check_status IS NOT NULL), true),
|
||||
('PEP_ACCOUNT_MONITORING', 'Monitoring accounts of PEP (politically exposed person)', 'ALL', 50000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 11 AND aml_check_status IS NOT NULL), true),
|
||||
('NEW_ACCOUNT_LARGE_TXN', 'New account Large transaction', 'ALL', 500000, 'THRESHOLD', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 14 AND aml_check_status IS NOT NULL), true),
|
||||
('RAPID_MOVEMENT_IN_ACCOUNTS', 'Rapid Movement in accounts (same-day credit followed by debit)', 'SAVINGS', null, 'VELOCITY', 1, (SELECT id FROM m_client_risk_rule WHERE order_position = 24 AND aml_check_status IS NOT NULL), true),
|
||||
('LOAN_REPAYMENT_IN_CASH', 'Repayment of loan in cash', 'LOAN', 200000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 73 AND aml_check_status IS NOT NULL), true);
|
||||
('LARGE_CASH_TXN_25L', 'Large cash transaction', 'ALL', 2500000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 6), true),
|
||||
('STRUCTURING_BELOW_10L', 'Splitting of cash deposits just below INR 10,00,000 in multiple accounts in a month', 'SAVINGS', 1000000, 'STRUCTURING', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 28), true),
|
||||
('STRUCTURING_BELOW_50K', 'Splitting of cash deposits just below INR 50,000', 'SAVINGS', 50000, 'STRUCTURING', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 29), true),
|
||||
('FREQUENT_LOW_CASH_DEPOSIT', 'Frequent low cash deposits', 'SAVINGS', 50000, 'VELOCITY', 7, (SELECT id FROM m_client_risk_rule WHERE order_position = 30), true),
|
||||
('FREQUENT_LOW_CASH_WITHDRAWAL', 'Frequent low cash withdrawals', 'SAVINGS', 50000, 'VELOCITY', 7, (SELECT id FROM m_client_risk_rule WHERE order_position = 31), true),
|
||||
('FREQUENT_CASH_NEAR_10L', 'Frequent cash deposits just below INR 10,00,000', 'SAVINGS', 1000000, 'STRUCTURING', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 65), true),
|
||||
('RTGS_HIGH_VALUE', 'Monitoring RTGS Transactions', 'TRANSFER', 2000000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 12), true),
|
||||
('JEWELLER_ACCOUNT_LARGE_TXN', 'Monitoring accounts of Jewellers', 'ALL', 2000000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 13), true),
|
||||
('CHARITABLE_ORG_CASH_WITHDRAWAL', 'Substantial cash withdrawals in accounts of charitable organization', 'SAVINGS', 50000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 10), true),
|
||||
('PEP_ACCOUNT_MONITORING', 'Monitoring accounts of PEP (politically exposed person)', 'ALL', 50000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 11), true),
|
||||
('NEW_ACCOUNT_LARGE_TXN', 'New account Large transaction', 'ALL', 500000, 'THRESHOLD', 30, (SELECT id FROM m_client_risk_rule WHERE order_position = 14), true),
|
||||
('RAPID_MOVEMENT_IN_ACCOUNTS', 'Rapid Movement in accounts (same-day credit followed by debit)', 'SAVINGS', null, 'VELOCITY', 1, (SELECT id FROM m_client_risk_rule WHERE order_position = 24), true),
|
||||
('LOAN_REPAYMENT_IN_CASH', 'Repayment of loan in cash', 'LOAN', 200000, 'THRESHOLD', null, (SELECT id FROM m_client_risk_rule WHERE order_position = 73), true);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
|
||||
+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>
|
||||
+1
-2
@@ -16,11 +16,10 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=null
|
||||
org.gradle.jvmargs=-Xmx6g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=null
|
||||
buildType=BUILD
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.workers.max=2
|
||||
org.gradle.daemon.idletimeout=10800000
|
||||
# Temporarily disabled until we fix configuration cache compatibility
|
||||
#org.gradle.configuration-cache=true
|
||||
|
||||
-2097
File diff suppressed because it is too large
Load Diff
-2115
File diff suppressed because it is too large
Load Diff
-2577
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
[33m17e0dab43[m Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev
|
||||
[33m67810bfae[m Add Welcome Kit Dispatch feature and Share Account summary API
|
||||
[33m7c15fbb0c[m fix(documentmanagement): close leaked file handles causing Windows re-upload failures; add entity KYC document CodeValues
|
||||
[33m6e3f195de[m Merge remote-tracking branch 'origin/uate' into uate
|
||||
[33ma65d80125[m feat(clients): maker-checker for client creation and edits (backend)
|
||||
[33mf45d5890e[m CTR/STR report and Risk Category auto mark on bases of person occupation and anual salary enter
|
||||
[33m3c58bb231[m fix(clients): pwdCategoryOptions on edit template, minor-age threshold to 18, minor KYC identity-proof bypass
|
||||
[33maabda18b6[m feat(clients): enforce Mother + Father/Caretaker family members on client activation
|
||||
[33m0e5671b70[m Merge remote-tracking branch 'origin/uate' into uate
|
||||
[33m67214ec0f[m fix(clients): stale signature bug, CKYC fixes, monthly turnover/annual income, senior citizen/PWD classification
|
||||
@@ -1,46 +0,0 @@
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
|
||||
|
||||
$headers = @{
|
||||
"fineract-platform-tenantid" = "default"
|
||||
"Content-Type" = "application/json"
|
||||
}
|
||||
$body = '{"username":"mifos", "password":"password"}'
|
||||
|
||||
Write-Host "--- AUTHENTICATION ---"
|
||||
$authResponse = Invoke-RestMethod -Method Post -Uri "https://localhost:8443/fineract-provider/api/v1/authentication?username=mifos&password=password" -Headers $headers
|
||||
$authKey = $authResponse.base64EncodedAuthenticationKey
|
||||
$headers.Add("Authorization", "Basic $authKey")
|
||||
|
||||
Write-Host "`n--- 1. GET /v1/clients/95/welcomekit ---"
|
||||
$res1 = Invoke-RestMethod -Method Get -Uri "https://localhost:8443/fineract-provider/api/v1/clients/95/welcomekit" -Headers $headers
|
||||
$res1 | ConvertTo-Json -Depth 10
|
||||
|
||||
Write-Host "`n--- 2. POST /v1/clients/95/welcomekit ---"
|
||||
$postBody = @{
|
||||
deliveryAddress = "123 Main St, Springfield"
|
||||
deliveryMode = "POSTMAN"
|
||||
dispatchedOnDate = "30 July 2026"
|
||||
dateFormat = "dd MMMM yyyy"
|
||||
locale = "en"
|
||||
} | ConvertTo-Json
|
||||
Write-Host "Request Body: $postBody"
|
||||
$res2 = Invoke-RestMethod -Method Post -Uri "https://localhost:8443/fineract-provider/api/v1/clients/95/welcomekit" -Headers $headers -Body $postBody
|
||||
$res2 | ConvertTo-Json -Depth 10
|
||||
$welcomeKitId = $res2.resourceId
|
||||
|
||||
Write-Host "`n--- 3. GET /v1/clients/95/welcomekit (after POST) ---"
|
||||
$res3 = Invoke-RestMethod -Method Get -Uri "https://localhost:8443/fineract-provider/api/v1/clients/95/welcomekit" -Headers $headers
|
||||
$res3 | ConvertTo-Json -Depth 10
|
||||
|
||||
Write-Host "`n--- 4. PUT /v1/welcomekit/$welcomeKitId ---"
|
||||
$putBody = @{
|
||||
status = "DELIVERED"
|
||||
} | ConvertTo-Json
|
||||
Write-Host "Request Body: $putBody"
|
||||
$res4 = Invoke-RestMethod -Method Put -Uri "https://localhost:8443/fineract-provider/api/v1/welcomekit/$welcomeKitId" -Headers $headers -Body $putBody
|
||||
$res4 | ConvertTo-Json -Depth 10
|
||||
|
||||
Write-Host "`n--- 5. GET /v1/clients/95/welcomekit (after PUT) ---"
|
||||
$res5 = Invoke-RestMethod -Method Get -Uri "https://localhost:8443/fineract-provider/api/v1/clients/95/welcomekit" -Headers $headers
|
||||
$res5 | ConvertTo-Json -Depth 10
|
||||
Reference in New Issue
Block a user