Compare commits
22
Commits
bc30241950
...
demo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9aa29fb35 | ||
|
|
84f8aa2274 | ||
|
|
7773a9e9c6 | ||
|
|
c890faee78 | ||
|
|
abc7eb22a2 | ||
|
|
8622dcaf90 | ||
|
|
81832b6bb7 | ||
|
|
0bd5296519 | ||
|
|
17e0dab435 | ||
|
|
67810bfaea | ||
|
|
2022dc5241 | ||
|
|
b1cfa996b4 | ||
|
|
02541eed6c | ||
|
|
7c15fbb0c7 | ||
|
|
6e3f195de0 | ||
|
|
a65d801250 | ||
|
|
f45d5890ea | ||
|
|
3c58bb231f | ||
|
|
aabda18b69 | ||
|
|
0e5671b706 | ||
|
|
67214ec0f8 | ||
|
|
026d902199 |
@@ -0,0 +1,18 @@
|
||||
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);
|
||||
-1
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.fineract.accounting.journalentry;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||
|
||||
+14
-14
@@ -277,14 +277,14 @@ public class ProductToGLAccountMappingHelper {
|
||||
}
|
||||
|
||||
final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element);
|
||||
/**
|
||||
/*
|
||||
* Variable stores a map representation of charges (key) and their associated income Id's (value) extracted from
|
||||
* the passed in Jsoncommand
|
||||
**/
|
||||
*/
|
||||
final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>();
|
||||
/***
|
||||
/*
|
||||
* Variable stores all charges which have already been mapped to Income Accounts in the system
|
||||
**/
|
||||
*/
|
||||
final Set<Long> existingCharges = new HashSet<>();
|
||||
if (chargeToIncomeAccountMappingArray != null) {
|
||||
if (changes != null) {
|
||||
@@ -301,13 +301,13 @@ public class ProductToGLAccountMappingHelper {
|
||||
// If input map is empty, delete all existing mappings
|
||||
if (inputChargeToIncomeAccountMap.size() == 0) {
|
||||
this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings);
|
||||
} /**
|
||||
} /*
|
||||
* Else, <br/>
|
||||
* update existing mappings OR <br/>
|
||||
* delete old mappings (which are already present, but not passed in as a part of Jsoncommand)<br/>
|
||||
* Create new mappings for charges that are passed in as a part of the Jsoncommand but not already present
|
||||
*
|
||||
**/
|
||||
*/
|
||||
else {
|
||||
for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) {
|
||||
final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId();
|
||||
@@ -356,14 +356,14 @@ public class ProductToGLAccountMappingHelper {
|
||||
.findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue());
|
||||
final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper
|
||||
.extractJsonArrayNamed(LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element);
|
||||
/**
|
||||
/*
|
||||
* Variable stores a map representation of Payment channels (key) and their fund sources (value) extracted from
|
||||
* the passed in Jsoncommand
|
||||
**/
|
||||
*/
|
||||
final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>();
|
||||
/***
|
||||
/*
|
||||
* Variable stores all payment types which have already been mapped to Fund Sources in the system
|
||||
**/
|
||||
*/
|
||||
final Set<Long> existingPaymentTypes = new HashSet<>();
|
||||
if (paymentChannelMappingArray != null) {
|
||||
if (changes != null) {
|
||||
@@ -381,14 +381,14 @@ public class ProductToGLAccountMappingHelper {
|
||||
// If input map is empty, delete all existing mappings
|
||||
if (inputPaymentChannelFundSourceMap.isEmpty()) {
|
||||
this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings);
|
||||
} /**
|
||||
} /*
|
||||
* Else, <br/>
|
||||
* update existing mappings OR <br/>
|
||||
* delete old mappings (which re already present, but not passed in as a part of Jsoncommand)<br/>
|
||||
* Create new mappings for payment types that are passed in as a part of the Jsoncommand but not already
|
||||
* present
|
||||
*
|
||||
**/
|
||||
*/
|
||||
else {
|
||||
for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) {
|
||||
final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId();
|
||||
@@ -584,9 +584,9 @@ public class ProductToGLAccountMappingHelper {
|
||||
// based on input condition
|
||||
|
||||
GLAccount glAccount;
|
||||
/**
|
||||
/*
|
||||
* Both CASH and Accrual placeholders have the same value for income from Interest and penalties
|
||||
**/
|
||||
*/
|
||||
CashAccountsForLoan placeHolderAccountType;
|
||||
if (isPenalty) {
|
||||
glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME,
|
||||
|
||||
+2
-2
@@ -181,12 +181,12 @@ public class TellerApiResource {
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Create Cashiers", description = """
|
||||
Mandatory Fields:\s
|
||||
Mandatory Fields:
|
||||
Cashier/staff, Fromm Date, To Date, Full Day or From time and To time
|
||||
|
||||
|
||||
|
||||
Optional Fields:\s
|
||||
Optional Fields:
|
||||
Description/Notes""")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class)))
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class)))
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.documentmanagement.adapter;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Decouples the entity "current signature" pointer from the generic document service, the same
|
||||
* way {@link EntityImageIdAdapter} decouples the "current photo" pointer — lets
|
||||
* DocumentWritePlatformServiceImpl (fineract-document module) give single-slot upsert semantics to
|
||||
* signature uploads without depending on portfolio-module entities directly.
|
||||
*/
|
||||
public interface EntitySignatureIdAdapter {
|
||||
|
||||
/** @param documentName the m_document.name of the document being created/removed */
|
||||
boolean accept(String entityType, String documentName);
|
||||
|
||||
Optional<SignatureIdResult> get(Long entityId);
|
||||
|
||||
Optional<SignatureIdResult> set(Long entityId, Long signatureId);
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
class SignatureIdResult implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String displayName;
|
||||
}
|
||||
}
|
||||
+2
@@ -65,6 +65,8 @@ public enum JobName {
|
||||
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
|
||||
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
|
||||
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
|
||||
CTR_REPORT_GENERATION("AML Monthly CTR Report Generation"), //
|
||||
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
||||
; //
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* One row per {@link AmlTransactionRule} hit on a live transaction. Raised regardless of the
|
||||
* client's current KYC risk category - alerting is never suppressed by a client being Low/Medium
|
||||
* risk. First reviewed by an AML Officer (whitelist or escalate); an ESCALATED alert spawns an
|
||||
* {@link AmlStrCase} for the Principal Officer.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_aml_alert")
|
||||
public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "transaction_type", nullable = false, length = 20)
|
||||
private String transactionType;
|
||||
|
||||
@Column(name = "transaction_id", nullable = false)
|
||||
private Long transactionId;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "rule_id", nullable = false)
|
||||
private AmlTransactionRule rule;
|
||||
|
||||
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "currency_code", length = 3)
|
||||
private String currencyCode;
|
||||
|
||||
@Column(name = "raised_on", nullable = false)
|
||||
private LocalDateTime raisedOn;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "reviewed_by")
|
||||
private AppUser reviewedBy;
|
||||
|
||||
@Column(name = "reviewed_on")
|
||||
private LocalDateTime reviewedOn;
|
||||
|
||||
@Column(name = "review_notes", length = 1000)
|
||||
private String reviewNotes;
|
||||
|
||||
@Column(name = "disposition", length = 20)
|
||||
private String disposition;
|
||||
|
||||
protected AmlAlert() {}
|
||||
|
||||
public static AmlAlert raise(AmlTransactionType transactionType, Long transactionId, Client client, AmlTransactionRule rule,
|
||||
BigDecimal amount, String currencyCode, LocalDateTime raisedOn) {
|
||||
final AmlAlert alert = new AmlAlert();
|
||||
alert.transactionType = transactionType.name();
|
||||
alert.transactionId = transactionId;
|
||||
alert.client = client;
|
||||
alert.rule = rule;
|
||||
alert.amount = amount;
|
||||
alert.currencyCode = currencyCode;
|
||||
alert.raisedOn = raisedOn;
|
||||
alert.status = AmlAlertStatus.RAISED.name();
|
||||
return alert;
|
||||
}
|
||||
|
||||
public void whitelist(AppUser reviewer, String notes) {
|
||||
this.reviewedBy = reviewer;
|
||||
this.reviewedOn = LocalDateTime.now();
|
||||
this.reviewNotes = notes;
|
||||
this.disposition = AmlAlertDisposition.WHITELIST.name();
|
||||
this.status = AmlAlertStatus.WHITELISTED.name();
|
||||
}
|
||||
|
||||
public void escalate(AppUser reviewer, String notes) {
|
||||
this.reviewedBy = reviewer;
|
||||
this.reviewedOn = LocalDateTime.now();
|
||||
this.reviewNotes = notes;
|
||||
this.disposition = AmlAlertDisposition.ESCALATE.name();
|
||||
this.status = AmlAlertStatus.ESCALATED.name();
|
||||
}
|
||||
|
||||
public Long getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public AmlTransactionType getTransactionType() {
|
||||
return AmlTransactionType.valueOf(transactionType);
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public AmlTransactionRule getRule() {
|
||||
return rule;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return currencyCode;
|
||||
}
|
||||
|
||||
public LocalDateTime getRaisedOn() {
|
||||
return raisedOn;
|
||||
}
|
||||
|
||||
public AmlAlertStatus getStatus() {
|
||||
return AmlAlertStatus.valueOf(status);
|
||||
}
|
||||
|
||||
public AppUser getReviewedBy() {
|
||||
return reviewedBy;
|
||||
}
|
||||
|
||||
public LocalDateTime getReviewedOn() {
|
||||
return reviewedOn;
|
||||
}
|
||||
|
||||
public String getReviewNotes() {
|
||||
return reviewNotes;
|
||||
}
|
||||
|
||||
public AmlAlertDisposition getDisposition() {
|
||||
return disposition == null ? null : AmlAlertDisposition.valueOf(disposition);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
/** The AML Officer's first-level disposition of an {@link AmlAlert}. */
|
||||
public enum AmlAlertDisposition {
|
||||
|
||||
WHITELIST, ESCALATE
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface AmlAlertRepository extends JpaRepository<AmlAlert, Long> {
|
||||
|
||||
List<AmlAlert> findByStatusOrderByRaisedOnAsc(String status);
|
||||
|
||||
@Query("SELECT COALESCE(SUM(a.amount), 0) FROM AmlAlert a WHERE a.client.id = :clientId "
|
||||
+ "AND a.transactionType = :transactionType AND a.raisedOn >= :since")
|
||||
BigDecimal sumAmountForClientSince(@Param("clientId") Long clientId, @Param("transactionType") String transactionType,
|
||||
@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);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
public enum AmlAlertStatus {
|
||||
|
||||
RAISED, UNDER_REVIEW, WHITELISTED, ESCALATED
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
|
||||
public enum AmlAppliesTo {
|
||||
|
||||
SAVINGS, LOAN, TRANSFER, ALL;
|
||||
|
||||
public boolean matches(AmlTransactionType transactionType) {
|
||||
return this == ALL || this.name().equals(transactionType.name());
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* Monthly, mechanical CTR batch header - purely threshold-based (₹10L+ cash, individually or
|
||||
* connected), no Principal Officer judgment involved. One row per calendar month.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_aml_ctr_report")
|
||||
public class AmlCtrReport extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "report_month", nullable = false, length = 7)
|
||||
private String reportMonth;
|
||||
|
||||
@Column(name = "generated_on", nullable = false)
|
||||
private LocalDateTime generatedOn;
|
||||
|
||||
@Column(name = "record_count", nullable = false)
|
||||
private int recordCount;
|
||||
|
||||
@Column(name = "file_reference", length = 150)
|
||||
private String fileReference;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
@Column(name = "submitted_on")
|
||||
private LocalDateTime submittedOn;
|
||||
|
||||
protected AmlCtrReport() {}
|
||||
|
||||
public static AmlCtrReport draft(String reportMonth, LocalDateTime generatedOn, int recordCount) {
|
||||
final AmlCtrReport report = new AmlCtrReport();
|
||||
report.reportMonth = reportMonth;
|
||||
report.generatedOn = generatedOn;
|
||||
report.recordCount = recordCount;
|
||||
report.status = AmlCtrReportStatus.DRAFT.name();
|
||||
return report;
|
||||
}
|
||||
|
||||
public void markGenerated(String fileReference) {
|
||||
this.fileReference = fileReference;
|
||||
this.status = AmlCtrReportStatus.GENERATED.name();
|
||||
}
|
||||
|
||||
public void markSubmitted(LocalDateTime submittedOn) {
|
||||
this.submittedOn = submittedOn;
|
||||
this.status = AmlCtrReportStatus.SUBMITTED.name();
|
||||
}
|
||||
|
||||
public String getReportMonth() {
|
||||
return reportMonth;
|
||||
}
|
||||
|
||||
public LocalDateTime getGeneratedOn() {
|
||||
return generatedOn;
|
||||
}
|
||||
|
||||
public int getRecordCount() {
|
||||
return recordCount;
|
||||
}
|
||||
|
||||
public String getFileReference() {
|
||||
return fileReference;
|
||||
}
|
||||
|
||||
public AmlCtrReportStatus getStatus() {
|
||||
return AmlCtrReportStatus.valueOf(status);
|
||||
}
|
||||
|
||||
public LocalDateTime getSubmittedOn() {
|
||||
return submittedOn;
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* One line per transaction (or connected-transaction group, when {@link #connectedGroup} is true)
|
||||
* feeding a monthly {@link AmlCtrReport}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_aml_ctr_report_line")
|
||||
public class AmlCtrReportLine {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "ctr_report_id", nullable = false)
|
||||
private AmlCtrReport ctrReport;
|
||||
|
||||
@Column(name = "transaction_type", nullable = false, length = 20)
|
||||
private String transactionType;
|
||||
|
||||
@Column(name = "transaction_id")
|
||||
private Long transactionId;
|
||||
|
||||
@Column(name = "client_id", nullable = false)
|
||||
private Long clientId;
|
||||
|
||||
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "is_connected_group", nullable = false)
|
||||
private boolean connectedGroup;
|
||||
|
||||
protected AmlCtrReportLine() {}
|
||||
|
||||
public static AmlCtrReportLine of(AmlCtrReport ctrReport, AmlTransactionType transactionType, Long transactionId, Long clientId,
|
||||
BigDecimal amount, boolean connectedGroup) {
|
||||
final AmlCtrReportLine line = new AmlCtrReportLine();
|
||||
line.ctrReport = ctrReport;
|
||||
line.transactionType = transactionType.name();
|
||||
line.transactionId = transactionId;
|
||||
line.clientId = clientId;
|
||||
line.amount = amount;
|
||||
line.connectedGroup = connectedGroup;
|
||||
return line;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public AmlCtrReport getCtrReport() {
|
||||
return ctrReport;
|
||||
}
|
||||
|
||||
public AmlTransactionType getTransactionType() {
|
||||
return AmlTransactionType.valueOf(transactionType);
|
||||
}
|
||||
|
||||
public Long getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public Long getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public boolean isConnectedGroup() {
|
||||
return connectedGroup;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AmlCtrReportLineRepository extends JpaRepository<AmlCtrReportLine, Long> {
|
||||
|
||||
List<AmlCtrReportLine> findByCtrReport_Id(Long ctrReportId);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AmlCtrReportRepository extends JpaRepository<AmlCtrReport, Long> {
|
||||
|
||||
Optional<AmlCtrReport> findByReportMonth(String reportMonth);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
public enum AmlCtrReportStatus {
|
||||
|
||||
DRAFT, GENERATED, SUBMITTED
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
/**
|
||||
* How an {@link AmlTransactionRule} is evaluated.
|
||||
*
|
||||
* <ul>
|
||||
* <li>THRESHOLD - a single transaction at or above {@code thresholdAmount}</li>
|
||||
* <li>STRUCTURING - connected transactions summing at/above {@code thresholdAmount} within {@code windowDays}</li>
|
||||
* <li>VELOCITY - unusual frequency of transactions within {@code windowDays}, independent of a fixed amount</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum AmlRuleType {
|
||||
|
||||
THRESHOLD, STRUCTURING, VELOCITY
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* Created when an {@link AmlAlert} is escalated past first-level review. {@link #concludedOn} is
|
||||
* the Principal Officer's conclusion date - the RBI-mandated 7-working-day FIU filing clock
|
||||
* ({@link #fiuDueDate}) starts here, never at the alert or transaction date.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_aml_str_case")
|
||||
public class AmlStrCase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "alert_id", nullable = false)
|
||||
private AmlAlert alert;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "assigned_po")
|
||||
private AppUser assignedPo;
|
||||
|
||||
@Column(name = "investigation_notes", length = 2000)
|
||||
private String investigationNotes;
|
||||
|
||||
@Column(name = "source_of_funds_notes", length = 2000)
|
||||
private String sourceOfFundsNotes;
|
||||
|
||||
@Column(name = "conclusion", length = 20)
|
||||
private String conclusion;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "concluded_by")
|
||||
private AppUser concludedBy;
|
||||
|
||||
@Column(name = "concluded_on")
|
||||
private LocalDate concludedOn;
|
||||
|
||||
@Column(name = "fiu_due_date")
|
||||
private LocalDate fiuDueDate;
|
||||
|
||||
@Column(name = "str_filed_on")
|
||||
private LocalDate strFiledOn;
|
||||
|
||||
@Column(name = "str_reference_no", length = 100)
|
||||
private String strReferenceNo;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 25)
|
||||
private String status;
|
||||
|
||||
protected AmlStrCase() {}
|
||||
|
||||
public static AmlStrCase openFor(AmlAlert alert) {
|
||||
final AmlStrCase strCase = new AmlStrCase();
|
||||
strCase.alert = alert;
|
||||
strCase.client = alert.getClient();
|
||||
strCase.status = AmlStrCaseStatus.OPEN.name();
|
||||
return strCase;
|
||||
}
|
||||
|
||||
public void assignPrincipalOfficer(AppUser po) {
|
||||
this.assignedPo = po;
|
||||
}
|
||||
|
||||
/** {@code fiuDueDate} is computed by the caller using the working-days calendar, not here. */
|
||||
public void conclude(AppUser po, AmlStrConclusion conclusion, String investigationNotes, String sourceOfFundsNotes,
|
||||
LocalDate concludedOn, LocalDate fiuDueDate) {
|
||||
this.concludedBy = po;
|
||||
this.conclusion = conclusion.name();
|
||||
this.investigationNotes = investigationNotes;
|
||||
this.sourceOfFundsNotes = sourceOfFundsNotes;
|
||||
this.concludedOn = concludedOn;
|
||||
this.status = conclusion == AmlStrConclusion.SUSPICIOUS ? AmlStrCaseStatus.CONCLUDED_SUSPICIOUS.name()
|
||||
: AmlStrCaseStatus.CONCLUDED_CLEAR.name();
|
||||
this.fiuDueDate = conclusion == AmlStrConclusion.SUSPICIOUS ? fiuDueDate : null;
|
||||
}
|
||||
|
||||
public void fileStr(String referenceNo, LocalDate filedOn) {
|
||||
this.strReferenceNo = referenceNo;
|
||||
this.strFiledOn = filedOn;
|
||||
this.status = AmlStrCaseStatus.FILED.name();
|
||||
}
|
||||
|
||||
public Long getClientId() {
|
||||
return client.getId();
|
||||
}
|
||||
|
||||
public AmlAlert getAlert() {
|
||||
return alert;
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public AppUser getAssignedPo() {
|
||||
return assignedPo;
|
||||
}
|
||||
|
||||
public String getInvestigationNotes() {
|
||||
return investigationNotes;
|
||||
}
|
||||
|
||||
public String getSourceOfFundsNotes() {
|
||||
return sourceOfFundsNotes;
|
||||
}
|
||||
|
||||
public AmlStrConclusion getConclusion() {
|
||||
return conclusion == null ? null : AmlStrConclusion.valueOf(conclusion);
|
||||
}
|
||||
|
||||
public AppUser getConcludedBy() {
|
||||
return concludedBy;
|
||||
}
|
||||
|
||||
public LocalDate getConcludedOn() {
|
||||
return concludedOn;
|
||||
}
|
||||
|
||||
public LocalDate getFiuDueDate() {
|
||||
return fiuDueDate;
|
||||
}
|
||||
|
||||
public LocalDate getStrFiledOn() {
|
||||
return strFiledOn;
|
||||
}
|
||||
|
||||
public String getStrReferenceNo() {
|
||||
return strReferenceNo;
|
||||
}
|
||||
|
||||
public AmlStrCaseStatus getStatus() {
|
||||
return AmlStrCaseStatus.valueOf(status);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface AmlStrCaseRepository extends JpaRepository<AmlStrCase, Long> {
|
||||
|
||||
long countByClient_Id(Long clientId);
|
||||
|
||||
List<AmlStrCase> findByStatus(String status);
|
||||
|
||||
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.concludedOn BETWEEN :from AND :to")
|
||||
List<AmlStrCase> findConcludedSuspiciousBetween(@Param("from") LocalDate from, @Param("to") LocalDate to);
|
||||
|
||||
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.fiuDueDate < :asOf")
|
||||
List<AmlStrCase> findOverdueForFiling(@Param("asOf") LocalDate asOf);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
public enum AmlStrCaseStatus {
|
||||
|
||||
OPEN, CONCLUDED_CLEAR, CONCLUDED_SUSPICIOUS, FILED
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
/** The Principal Officer's final conclusion on an {@link AmlStrCase}. */
|
||||
public enum AmlStrConclusion {
|
||||
|
||||
NOT_SUSPICIOUS, SUSPICIOUS
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* A machine-evaluable transaction-monitoring rule. Distinct from {@code ClientRiskRule}
|
||||
* (client-profile, KYC-time scoring) - these are checked against live savings/loan/transfer
|
||||
* transactions as they post. {@link #sourceClientRiskRuleId} optionally traces a rule back to its
|
||||
* originating entry in the 221-rule AML catalog seeded in {@code m_client_risk_rule}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_aml_transaction_rule")
|
||||
public class AmlTransactionRule {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "rule_code", nullable = false, length = 50)
|
||||
private String ruleCode;
|
||||
|
||||
@Column(name = "description", nullable = false, length = 500)
|
||||
private String description;
|
||||
|
||||
@Column(name = "applies_to", nullable = false, length = 20)
|
||||
private String appliesTo;
|
||||
|
||||
@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;
|
||||
|
||||
@Column(name = "window_days")
|
||||
private Integer windowDays;
|
||||
|
||||
@Column(name = "source_client_risk_rule_id")
|
||||
private Long sourceClientRiskRuleId;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean active;
|
||||
|
||||
protected AmlTransactionRule() {}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getRuleCode() {
|
||||
return ruleCode;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public AmlAppliesTo getAppliesTo() {
|
||||
return AmlAppliesTo.valueOf(appliesTo);
|
||||
}
|
||||
|
||||
public BigDecimal getThresholdAmount() {
|
||||
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);
|
||||
}
|
||||
|
||||
public Integer getWindowDays() {
|
||||
return windowDays;
|
||||
}
|
||||
|
||||
public Long getSourceClientRiskRuleId() {
|
||||
return sourceClientRiskRuleId;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.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);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
/** The kind of ledger transaction an {@link AmlAlert} was raised against. */
|
||||
public enum AmlTransactionType {
|
||||
|
||||
SAVINGS, LOAN, TRANSFER
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* Immutable, append-only audit trail of a client's KYC risk-category changes. Backs the AML menu's
|
||||
* existing "Risk Update History Report" item, which previously had no data behind it. Written both
|
||||
* for manual re-categorization and for the automatic escalation triggered on a client's 3rd
|
||||
* cumulative {@link AmlStrCase}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_client_risk_category_history")
|
||||
public class ClientRiskCategoryHistory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@Column(name = "previous_category", nullable = false)
|
||||
private Integer previousCategory;
|
||||
|
||||
@Column(name = "new_category", nullable = false)
|
||||
private Integer newCategory;
|
||||
|
||||
@Column(name = "reason", nullable = false, length = 500)
|
||||
private String reason;
|
||||
|
||||
@Column(name = "triggered_by", nullable = false, length = 30)
|
||||
private String triggeredBy;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "str_case_id")
|
||||
private AmlStrCase strCase;
|
||||
|
||||
@Column(name = "changed_on", nullable = false)
|
||||
private LocalDateTime changedOn;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "changed_by", nullable = false)
|
||||
private AppUser changedBy;
|
||||
|
||||
protected ClientRiskCategoryHistory() {}
|
||||
|
||||
public static ClientRiskCategoryHistory autoEscalation(Client client, Integer previousCategory, Integer newCategory,
|
||||
AmlStrCase triggeringCase, AppUser systemActor, LocalDateTime changedOn) {
|
||||
final ClientRiskCategoryHistory history = new ClientRiskCategoryHistory();
|
||||
history.client = client;
|
||||
history.previousCategory = previousCategory;
|
||||
history.newCategory = newCategory;
|
||||
history.reason = "Auto-escalated after client's 3rd cumulative AML STR case (case #" + triggeringCase.getId() + ")";
|
||||
history.triggeredBy = RiskCategoryChangeTrigger.AML_AUTO_ESCALATION.name();
|
||||
history.strCase = triggeringCase;
|
||||
history.changedOn = changedOn;
|
||||
history.changedBy = systemActor;
|
||||
return history;
|
||||
}
|
||||
|
||||
public static ClientRiskCategoryHistory manual(Client client, Integer previousCategory, Integer newCategory, String reason,
|
||||
AppUser changedBy, LocalDateTime changedOn) {
|
||||
final ClientRiskCategoryHistory history = new ClientRiskCategoryHistory();
|
||||
history.client = client;
|
||||
history.previousCategory = previousCategory;
|
||||
history.newCategory = newCategory;
|
||||
history.reason = reason;
|
||||
history.triggeredBy = RiskCategoryChangeTrigger.MANUAL.name();
|
||||
history.changedBy = changedBy;
|
||||
history.changedOn = changedOn;
|
||||
return history;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public Integer getPreviousCategory() {
|
||||
return previousCategory;
|
||||
}
|
||||
|
||||
public Integer getNewCategory() {
|
||||
return newCategory;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public RiskCategoryChangeTrigger getTriggeredBy() {
|
||||
return RiskCategoryChangeTrigger.valueOf(triggeredBy);
|
||||
}
|
||||
|
||||
public AmlStrCase getStrCase() {
|
||||
return strCase;
|
||||
}
|
||||
|
||||
public LocalDateTime getChangedOn() {
|
||||
return changedOn;
|
||||
}
|
||||
|
||||
public AppUser getChangedBy() {
|
||||
return changedBy;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ClientRiskCategoryHistoryRepository extends JpaRepository<ClientRiskCategoryHistory, Long> {
|
||||
|
||||
List<ClientRiskCategoryHistory> findByClient_IdOrderByChangedOnDesc(Long clientId);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
public enum RiskCategoryChangeTrigger {
|
||||
|
||||
AML_AUTO_ESCALATION, MANUAL
|
||||
}
|
||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Simulator-side audit log for CFCFRMS (I4C/MHA) events.
|
||||
@@ -69,7 +70,7 @@ public class CfcfrmsSimulatorLog extends AbstractAuditableWithUTCDateTimeCustom<
|
||||
this.eventType = eventType;
|
||||
this.payload = payload;
|
||||
this.responsePayload = responsePayload;
|
||||
this.eventTimestamp = OffsetDateTime.now();
|
||||
this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Audit trail for every status change on a CFCFRMS complaint ticket.
|
||||
@@ -54,7 +55,7 @@ public class ComplaintAuditLog extends AbstractAuditableWithUTCDateTimeCustom<Lo
|
||||
this.event = event;
|
||||
this.description = description;
|
||||
this.actor = actor;
|
||||
this.eventTimestamp = OffsetDateTime.now();
|
||||
this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public ComplaintAuditLog(final String ticketId, final String event, final String description,
|
||||
|
||||
+3
-2
@@ -26,6 +26,7 @@ import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* CFCFRMS Complaint Ticket — represents a cyber fraud complaint pushed by MHA/I4C to the bank.
|
||||
@@ -121,7 +122,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
ticket.status = ComplaintStatus.RECEIVED;
|
||||
ticket.assignedOfficer = assignedOfficer;
|
||||
ticket.statePoliceRef = statePoliceRef;
|
||||
ticket.receivedAt = OffsetDateTime.now();
|
||||
ticket.receivedAt = DateUtils.getOffsetDateTimeOfTenant();
|
||||
return ticket;
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
this.status = newStatus;
|
||||
this.actionTaken = actionNote;
|
||||
if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) {
|
||||
this.resolvedAt = OffsetDateTime.now();
|
||||
this.resolvedAt = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.client.adapter;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntitySignatureIdAdapter;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
class ClientSignatureIdAdapter implements EntitySignatureIdAdapter {
|
||||
|
||||
private static final String ENTITY_TYPE = "clients";
|
||||
private static final String DOCUMENT_NAME = "clientSignature";
|
||||
|
||||
private final ClientRepository repository;
|
||||
|
||||
@Override
|
||||
public boolean accept(String entityType, String documentName) {
|
||||
return ENTITY_TYPE.equalsIgnoreCase(entityType) && DOCUMENT_NAME.equalsIgnoreCase(documentName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<SignatureIdResult> get(Long entityId) {
|
||||
return repository.findById(entityId).filter(client -> nonNull(client.getSignatureId()))
|
||||
.map(client -> SignatureIdResult.builder().id(client.getSignatureId()).displayName(client.getDisplayName()).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Optional<SignatureIdResult> set(Long entityId, Long signatureId) {
|
||||
final var result = get(entityId);
|
||||
|
||||
if (signatureId == null) {
|
||||
repository.removeSignatureId(entityId);
|
||||
} else {
|
||||
repository.updateByIdAndSignatureId(entityId, signatureId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -63,6 +63,7 @@ public class ClientApiConstants {
|
||||
public static final String CLIENT_CLASSIFICATION = "ClientClassification";
|
||||
|
||||
public static final String OCCUPATION_CATEGORY = "OccupationCategory";
|
||||
public static final String PWD_CATEGORY = "PwdCategory";
|
||||
public static final String OCCUPATION = "Occupation";
|
||||
|
||||
public static final String CLIENT_NON_PERSON_CONSTITUTION = "Constitution";
|
||||
@@ -147,7 +148,13 @@ public class ClientApiConstants {
|
||||
public static final String religionParamName = "religion";
|
||||
public static final String socialCategoryParamName = "socialCategory";
|
||||
public static final String monthlyIncomeParamName = "monthlyIncome";
|
||||
public static final String annualIncomeParamName = "annualIncome";
|
||||
public static final String annualTurnoverParamName = "annualTurnover";
|
||||
public static final String monthlyTurnoverParamName = "monthlyTurnover";
|
||||
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
||||
public static final String isPwdParamName = "isPwd";
|
||||
public static final String pwdCategoryIdParamName = "pwdCategoryId";
|
||||
public static final String pwdCategoryParamName = "pwdCategory";
|
||||
public static final String agricultureIncomeParamName = "agricultureIncome";
|
||||
public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome";
|
||||
public static final String ageYearsParamName = "ageYears";
|
||||
@@ -257,8 +264,9 @@ public class ClientApiConstants {
|
||||
staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName,
|
||||
salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName,
|
||||
educationLevelParamName, degreeQualificationParamName,
|
||||
religionParamName, socialCategoryParamName, monthlyIncomeParamName,
|
||||
annualTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||
religionParamName, socialCategoryParamName, monthlyIncomeParamName, annualIncomeParamName,
|
||||
annualTurnoverParamName, monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||
isSeniorCitizenParamName, isPwdParamName, pwdCategoryParamName,
|
||||
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
||||
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName,
|
||||
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
||||
|
||||
@@ -82,7 +82,13 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
||||
private CodeValueData religion;
|
||||
private CodeValueData socialCategory;
|
||||
private BigDecimal monthlyIncome;
|
||||
private BigDecimal annualIncome;
|
||||
private BigDecimal annualTurnover;
|
||||
private BigDecimal monthlyTurnover;
|
||||
private Boolean isSeniorCitizen;
|
||||
private Boolean isPwd;
|
||||
private CodeValueData pwdCategory;
|
||||
private Collection<CodeValueData> pwdCategoryOptions;
|
||||
private BigDecimal agricultureIncome;
|
||||
private BigDecimal nonAgricultureIncome;
|
||||
private Integer ageYears;
|
||||
@@ -400,13 +406,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
||||
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
||||
data.religionOptions = templateData.religionOptions;
|
||||
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
||||
data.pwdCategoryOptions = templateData.pwdCategoryOptions;
|
||||
data.maritalStatus = clientData.maritalStatus;
|
||||
data.educationLevel = clientData.educationLevel;
|
||||
data.degreeQualification = clientData.degreeQualification;
|
||||
data.religion = clientData.religion;
|
||||
data.socialCategory = clientData.socialCategory;
|
||||
data.monthlyIncome = clientData.monthlyIncome;
|
||||
data.annualIncome = clientData.annualIncome;
|
||||
data.annualTurnover = clientData.annualTurnover;
|
||||
data.monthlyTurnover = clientData.monthlyTurnover;
|
||||
data.isSeniorCitizen = clientData.isSeniorCitizen;
|
||||
data.isPwd = clientData.isPwd;
|
||||
data.pwdCategory = clientData.pwdCategory;
|
||||
data.agricultureIncome = clientData.agricultureIncome;
|
||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||
data.ageYears = clientData.ageYears;
|
||||
@@ -456,13 +468,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
||||
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
||||
data.religionOptions = clientData.religionOptions;
|
||||
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
||||
data.pwdCategoryOptions = clientData.pwdCategoryOptions;
|
||||
data.maritalStatus = clientData.maritalStatus;
|
||||
data.educationLevel = clientData.educationLevel;
|
||||
data.degreeQualification = clientData.degreeQualification;
|
||||
data.religion = clientData.religion;
|
||||
data.socialCategory = clientData.socialCategory;
|
||||
data.monthlyIncome = clientData.monthlyIncome;
|
||||
data.annualIncome = clientData.annualIncome;
|
||||
data.annualTurnover = clientData.annualTurnover;
|
||||
data.monthlyTurnover = clientData.monthlyTurnover;
|
||||
data.isSeniorCitizen = clientData.isSeniorCitizen;
|
||||
data.isPwd = clientData.isPwd;
|
||||
data.pwdCategory = clientData.pwdCategory;
|
||||
data.agricultureIncome = clientData.agricultureIncome;
|
||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||
data.ageYears = clientData.ageYears;
|
||||
@@ -509,7 +527,12 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
||||
data.religion = clientData.religion;
|
||||
data.socialCategory = clientData.socialCategory;
|
||||
data.monthlyIncome = clientData.monthlyIncome;
|
||||
data.annualIncome = clientData.annualIncome;
|
||||
data.annualTurnover = clientData.annualTurnover;
|
||||
data.monthlyTurnover = clientData.monthlyTurnover;
|
||||
data.isSeniorCitizen = clientData.isSeniorCitizen;
|
||||
data.isPwd = clientData.isPwd;
|
||||
data.pwdCategory = clientData.pwdCategory;
|
||||
data.agricultureIncome = clientData.agricultureIncome;
|
||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||
data.ageYears = clientData.ageYears;
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public final class ClientFamilyMemberChildData implements Serializable {
|
||||
private final Long id;
|
||||
private final String fullName;
|
||||
private final Long genderId;
|
||||
private final String gender;
|
||||
private final LocalDate dateOfBirth;
|
||||
}
|
||||
+3
@@ -84,6 +84,9 @@ public final class ClientFamilyMembersData implements Serializable {
|
||||
private final LocalDate marriageDate;
|
||||
private final String marriageAuthority;
|
||||
|
||||
@lombok.Setter
|
||||
private java.util.List<ClientFamilyMemberChildData> children;
|
||||
|
||||
// template holder
|
||||
private final Collection<CodeValueData> relationshipIdOptions;
|
||||
private final Collection<CodeValueData> genderIdOptions;
|
||||
|
||||
@@ -77,6 +77,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@Column(name = "image_id")
|
||||
private Long imageId;
|
||||
|
||||
@Column(name = "signature_id")
|
||||
private Long signatureId;
|
||||
|
||||
@Column(name = "status_enum", nullable = false)
|
||||
private Integer status;
|
||||
|
||||
@@ -231,9 +234,25 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@Column(name = "monthly_income")
|
||||
private java.math.BigDecimal monthlyIncome;
|
||||
|
||||
@Column(name = "annual_income")
|
||||
private java.math.BigDecimal annualIncome;
|
||||
|
||||
@Column(name = "annual_turnover")
|
||||
private java.math.BigDecimal annualTurnover;
|
||||
|
||||
@Column(name = "monthly_turnover")
|
||||
private java.math.BigDecimal monthlyTurnover;
|
||||
|
||||
@Column(name = "is_senior_citizen")
|
||||
private Boolean isSeniorCitizen;
|
||||
|
||||
@Column(name = "is_pwd")
|
||||
private Boolean isPwd;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pwd_category_cv_id")
|
||||
private CodeValue pwdCategory;
|
||||
|
||||
@Column(name = "agriculture_income")
|
||||
private java.math.BigDecimal agricultureIncome;
|
||||
|
||||
@@ -598,6 +617,10 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.office.getId();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
public String mobileNo() {
|
||||
return this.mobileNo;
|
||||
}
|
||||
@@ -832,6 +855,14 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.socialCategory != null ? this.socialCategory.getId() : null;
|
||||
}
|
||||
|
||||
public Long pwdCategoryId() {
|
||||
return this.pwdCategory != null ? this.pwdCategory.getId() : null;
|
||||
}
|
||||
|
||||
public void updatePwdCategory(CodeValue pwdCategory) {
|
||||
this.pwdCategory = pwdCategory;
|
||||
}
|
||||
|
||||
public void updateSocialCategory(CodeValue socialCategory) {
|
||||
this.socialCategory = socialCategory;
|
||||
}
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Form 60 declaration as per Rule 114B of Income Tax Rules, 1962.
|
||||
@@ -87,7 +88,7 @@ public class ClientForm60 extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
form.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome");
|
||||
form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null);
|
||||
form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration");
|
||||
form.submittedOn = LocalDate.now();
|
||||
form.submittedOn = DateUtils.getLocalDateOfTenant();
|
||||
return form;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-10
@@ -32,6 +32,7 @@ import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India
|
||||
@@ -260,7 +261,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.gstinVerified = false;
|
||||
this.cinVerified = false;
|
||||
this.dinVerified = false;
|
||||
this.initiatedOn = LocalDate.now();
|
||||
this.initiatedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
|
||||
@@ -456,13 +457,13 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public void markCkycUploaded(final String ckycNumber, final String batchId) {
|
||||
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
||||
this.ckycBatchId = StringUtils.defaultIfEmpty(batchId, null);
|
||||
this.ckycUploadedOn = LocalDate.now();
|
||||
this.ckycUploadedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
/** Called after successful 3rd-party verification of all required documents. */
|
||||
public void markVerified(final String ckycNumber) {
|
||||
this.kycStatus = ClientKycStatus.VERIFIED.getValue();
|
||||
this.verifiedOn = LocalDate.now();
|
||||
this.verifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
||||
|
||||
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
|
||||
@@ -474,14 +475,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
/** Called when verification fails. */
|
||||
public void markRejected(final String remarks) {
|
||||
this.kycStatus = ClientKycStatus.REJECTED.getValue();
|
||||
this.rejectedOn = LocalDate.now();
|
||||
this.rejectedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
|
||||
}
|
||||
|
||||
public void markPanVerified(final String fullName, final String firstName, final String lastName,
|
||||
final String aadhaarSeedingStatus, final String category, final String status) {
|
||||
this.panVerified = true;
|
||||
this.panVerifiedOn = LocalDate.now();
|
||||
this.panVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
|
||||
this.panFirstName = StringUtils.defaultIfEmpty(firstName, null);
|
||||
this.panLastName = StringUtils.defaultIfEmpty(lastName, null);
|
||||
@@ -507,28 +508,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
public void markAadhaarVerified() {
|
||||
this.aadhaarVerified = true;
|
||||
this.aadhaarVerifiedOn = LocalDate.now();
|
||||
this.aadhaarVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markGstinVerified() {
|
||||
this.gstinVerified = true;
|
||||
this.gstinVerifiedOn = LocalDate.now();
|
||||
this.gstinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markCinVerified() {
|
||||
this.cinVerified = true;
|
||||
this.cinVerifiedOn = LocalDate.now();
|
||||
this.cinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markDinVerified() {
|
||||
this.dinVerified = true;
|
||||
this.dinVerifiedOn = LocalDate.now();
|
||||
this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markDinVerified(final String dinNumber, final String mcaResponseJson) {
|
||||
this.dinNumber = dinNumber;
|
||||
this.dinVerified = true;
|
||||
this.dinVerifiedOn = LocalDate.now();
|
||||
this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.isDirector = true;
|
||||
this.dinMcaResponse = mcaResponseJson;
|
||||
}
|
||||
@@ -585,6 +586,15 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return riskCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly sets the risk category outside the normal command-diff update path. Used by the AML
|
||||
* module to auto-escalate a client's risk category after their 3rd cumulative STR case -
|
||||
* every such change is recorded in {@code m_client_risk_category_history} by the caller.
|
||||
*/
|
||||
public void escalateRiskCategory(Integer newCategory) {
|
||||
this.riskCategory = newCategory;
|
||||
}
|
||||
|
||||
public String getPanNumber() {
|
||||
return panNumber;
|
||||
}
|
||||
|
||||
+8
@@ -55,6 +55,14 @@ public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecif
|
||||
@Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId")
|
||||
void removeImageId(@Param("clientId") Long clientId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Client client SET client.signatureId = :signatureId WHERE client.id = :clientId")
|
||||
void updateByIdAndSignatureId(@Param("clientId") Long clientId, @Param("signatureId") Long signatureId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Client client SET client.signatureId = null WHERE client.id = :clientId")
|
||||
void removeSignatureId(@Param("clientId") Long clientId);
|
||||
|
||||
/**
|
||||
* Count clients whose cif_number begins with the given prefix. Used by the CIF number generator
|
||||
* (TTYYYYNNNNNN format) to compute the next sequence within a given (type, year) bucket.
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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.client.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_client_risk_rule")
|
||||
public class ClientRiskRule {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "rule_name", nullable = false, length = 100)
|
||||
private String ruleName;
|
||||
|
||||
@Column(name = "legal_form_id")
|
||||
private Integer legalFormId;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "occupation_category_cv_id")
|
||||
private CodeValue occupationCategory;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "occupation_cv_id")
|
||||
private CodeValue occupation;
|
||||
|
||||
@Column(name = "min_monthly_income", precision = 19, scale = 6)
|
||||
private BigDecimal minMonthlyIncome;
|
||||
|
||||
@Column(name = "max_monthly_income", precision = 19, scale = 6)
|
||||
private BigDecimal maxMonthlyIncome;
|
||||
|
||||
@Column(name = "min_annual_income", precision = 19, scale = 6)
|
||||
private BigDecimal minAnnualIncome;
|
||||
|
||||
@Column(name = "max_annual_income", precision = 19, scale = 6)
|
||||
private BigDecimal maxAnnualIncome;
|
||||
|
||||
@Column(name = "min_annual_turnover", precision = 19, scale = 6)
|
||||
private BigDecimal minAnnualTurnover;
|
||||
|
||||
@Column(name = "max_annual_turnover", precision = 19, scale = 6)
|
||||
private BigDecimal maxAnnualTurnover;
|
||||
|
||||
@Column(name = "risk_category", nullable = false)
|
||||
private Integer riskCategory;
|
||||
|
||||
@Column(name = "reason", nullable = false, length = 500)
|
||||
private String reason;
|
||||
|
||||
@Column(name = "aml_check_status", length = 20)
|
||||
private String amlCheckStatus;
|
||||
|
||||
@Column(name = "banned_entity_match", length = 10)
|
||||
private String bannedEntityMatch;
|
||||
|
||||
@Column(name = "willful_defaulter", length = 10)
|
||||
private String willfulDefaulter;
|
||||
|
||||
@Column(name = "ecgc_sal_flag", length = 10)
|
||||
private String ecgcSalFlag;
|
||||
|
||||
@Column(name = "dormant_flag", length = 20)
|
||||
private String dormantFlag;
|
||||
|
||||
@Column(name = "order_position", nullable = false)
|
||||
private Integer orderPosition;
|
||||
|
||||
protected ClientRiskRule() {}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getRuleName() {
|
||||
return ruleName;
|
||||
}
|
||||
|
||||
public Integer getLegalFormId() {
|
||||
return legalFormId;
|
||||
}
|
||||
|
||||
public CodeValue getOccupationCategory() {
|
||||
return occupationCategory;
|
||||
}
|
||||
|
||||
public CodeValue getOccupation() {
|
||||
return occupation;
|
||||
}
|
||||
|
||||
public BigDecimal getMinMonthlyIncome() {
|
||||
return minMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxMonthlyIncome() {
|
||||
return maxMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualIncome() {
|
||||
return minAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualIncome() {
|
||||
return maxAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualTurnover() {
|
||||
return minAnnualTurnover;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualTurnover() {
|
||||
return maxAnnualTurnover;
|
||||
}
|
||||
|
||||
public Integer getRiskCategory() {
|
||||
return riskCategory;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public Integer getOrderPosition() {
|
||||
return orderPosition;
|
||||
}
|
||||
|
||||
public String getAmlCheckStatus() {
|
||||
return amlCheckStatus;
|
||||
}
|
||||
|
||||
public String getBannedEntityMatch() {
|
||||
return bannedEntityMatch;
|
||||
}
|
||||
|
||||
public String getWillfulDefaulter() {
|
||||
return willfulDefaulter;
|
||||
}
|
||||
|
||||
public String getEcgcSalFlag() {
|
||||
return ecgcSalFlag;
|
||||
}
|
||||
|
||||
public String getDormantFlag() {
|
||||
return dormantFlag;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.client.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface ClientRiskRuleRepository extends JpaRepository<ClientRiskRule, Long> {
|
||||
|
||||
@Query("SELECT r FROM ClientRiskRule r ORDER BY r.orderPosition ASC")
|
||||
List<ClientRiskRule> findAllOrderedByOrderPosition();
|
||||
}
|
||||
+2
-1
@@ -24,6 +24,7 @@ import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Activity log for each DIP / MNRL mobile number lookup request.
|
||||
@@ -78,6 +79,6 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
this.rawResponse = rawResponse;
|
||||
this.lookupSource = lookupSource;
|
||||
this.clientId = clientId;
|
||||
this.lookedUpOn = OffsetDateTime.now();
|
||||
this.lookedUpOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.sanctions.domain;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
|
||||
void deleteAllByActiveTrue();
|
||||
|
||||
long countByActiveTrue();
|
||||
|
||||
/**
|
||||
* Timestamp the active sanctions list was last loaded into the database — i.e. the data
|
||||
* currency shown to callers so they know which list snapshot a screening ran against.
|
||||
*/
|
||||
@Query("SELECT MAX(s.createdDate) FROM SanctionsEntry s WHERE s.active = true")
|
||||
Optional<OffsetDateTime> findListLastLoadedOn();
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Activity log for each sanctions screening request.
|
||||
@@ -73,7 +74,7 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
||||
this.screeningSource = screeningSource;
|
||||
this.clientId = clientId;
|
||||
this.ipAddress = ipAddress;
|
||||
this.screenedOn = OffsetDateTime.now();
|
||||
this.screenedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||
|
||||
+7
-6
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.search.data;
|
||||
|
||||
import java.util.Locale;
|
||||
import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources;
|
||||
|
||||
public class SearchConditions {
|
||||
@@ -37,17 +38,17 @@ public class SearchConditions {
|
||||
this.searchResource = searchResource;
|
||||
this.exactMatch = exactMatch;
|
||||
this.clientSearch = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTS.name().toLowerCase(Locale.ROOT));
|
||||
this.groupSearch = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.GROUPS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.GROUPS.name().toLowerCase(Locale.ROOT));
|
||||
this.loanSeach = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.LOANS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.LOANS.name().toLowerCase(Locale.ROOT));
|
||||
this.savingSeach = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.SAVINGS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SAVINGS.name().toLowerCase(Locale.ROOT));
|
||||
this.shareSeach = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.SHARES.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SHARES.name().toLowerCase(Locale.ROOT));
|
||||
this.clientIdentifierSearch = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public SearchConditions(final String searchQueryParam, final String searchResource, final Boolean clientSearch,
|
||||
|
||||
+1
-1
@@ -321,6 +321,6 @@ public class SearchUtil {
|
||||
|
||||
public String camelToSnake(final String camelStr) {
|
||||
return camelStr == null ? null
|
||||
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
||||
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public interface BankUserDscRepository extends JpaRepository<BankUserDsc, Long>
|
||||
|
||||
/**
|
||||
* Login gate: returns true if the user has at least one usable DSC mapping that's also not
|
||||
* past its notAfter date. The {@code dscExpiryDate >= today} clause prevents 2FA-gating a
|
||||
* past its notAfter date. The {@code dscExpiryDate >= today} clause prevents 2FA-gating a
|
||||
* user who's enrolled but whose cert silently expired between log-ins — they'd otherwise be
|
||||
* stuck in a verification loop with a cert the helper would sign with but the server would
|
||||
* accept (the verify side filters too, but the gate is what kicks the user into the DSC flow
|
||||
|
||||
+4
-3
@@ -29,6 +29,7 @@ import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
@@ -452,7 +453,7 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
public CurrentAccountPendingEdit(final CurrentAccount currentAccount, final AppUser maker) {
|
||||
this.currentAccount = currentAccount;
|
||||
this.maker = maker;
|
||||
this.createdOn = LocalDateTime.now();
|
||||
this.createdOn = DateUtils.getLocalDateTimeOfTenant();
|
||||
this.status = STATUS_PENDING;
|
||||
this.pendingLockKey = lockKeyFor(currentAccount.getId());
|
||||
}
|
||||
@@ -582,14 +583,14 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
public void markApproved(final AppUser checker) {
|
||||
this.status = STATUS_APPROVED;
|
||||
this.checker = checker;
|
||||
this.checkedOn = LocalDateTime.now();
|
||||
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
|
||||
public void markRejected(final AppUser checker, final String reason) {
|
||||
this.status = STATUS_REJECTED;
|
||||
this.checker = checker;
|
||||
this.checkedOn = LocalDateTime.now();
|
||||
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
|
||||
this.rejectReason = reason;
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
|
||||
+3
-1
@@ -51,7 +51,9 @@ public class CurrentAccountNumberGenerator {
|
||||
if (val > maxVal) {
|
||||
maxVal = val;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// non-numeric suffix - skip when computing the max existing sequence
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-2
@@ -54,7 +54,6 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.MonthDay;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
@@ -76,7 +75,6 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator;
|
||||
|
||||
-7
@@ -56,18 +56,11 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountT
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productCodeParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.glAccountIdParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveDateParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveToParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.endDateParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dormancyAlertParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculateSelectorParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.averagePeriodParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoPostParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.frequencyFixDayParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lastInterestDateParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enableLockinPeriodParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName;
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_current_product_deposit_limit_kyc")
|
||||
@@ -57,12 +58,12 @@ public class CurrentProductDepositLimitKyc extends AbstractPersistableCustom<Lon
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
protected CurrentProductDepositLimitKyc() {
|
||||
this.createdDate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) {
|
||||
this();
|
||||
this.currentProduct = currentProduct;
|
||||
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_current_product_facility_config")
|
||||
@@ -87,12 +88,12 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
protected CurrentProductFacilityConfig() {
|
||||
this.createdDate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public CurrentProductFacilityConfig(CurrentProduct currentProduct) {
|
||||
this();
|
||||
this.currentProduct = currentProduct;
|
||||
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_current_product_facility_limits")
|
||||
@@ -51,7 +52,6 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
protected CurrentProductFacilityLimit() {
|
||||
this.createdDate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
private CurrentProductFacilityLimit(CurrentProduct currentProduct, String limitType,
|
||||
@@ -62,6 +62,7 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
|
||||
this.generalLimit = generalLimit;
|
||||
this.minorAccountLimit = minorAccountLimit;
|
||||
this.smallSavingLimit = smallSavingLimit;
|
||||
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public static CurrentProductFacilityLimit create(CurrentProduct currentProduct, String limitType,
|
||||
|
||||
-2
@@ -19,13 +19,11 @@
|
||||
package org.apache.fineract.portfolio.currentproduct.service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
+6
-5
@@ -95,23 +95,24 @@ public class FileContentStoreService implements ContentStoreService {
|
||||
|
||||
final var target = Files.exists(getPath(safePath, false)) ? getPath(safePath, false) : getPath(safePath, true);
|
||||
|
||||
try (var in = is) {
|
||||
IOUtils.copy(in, new FileOutputStream(target.toString()));
|
||||
try (var in = is; var out = new FileOutputStream(target.toString())) {
|
||||
IOUtils.copy(in, out);
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
|
||||
final var relativePath = getRelativePath(target);
|
||||
|
||||
try {
|
||||
postUploadContentPolicy
|
||||
.check(ContentPolicyContext.builder().path(path).inputStream(getInputStream(target)).mimeType(mimeType).build());
|
||||
try (var in = getInputStream(target)) {
|
||||
postUploadContentPolicy.check(ContentPolicyContext.builder().path(path).inputStream(in).mimeType(mimeType).build());
|
||||
} catch (ContentPolicyException e) {
|
||||
log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", target, e.getMessage());
|
||||
|
||||
delete(relativePath.toString());
|
||||
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
|
||||
return relativePath.toString();
|
||||
|
||||
+24
-4
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -31,6 +32,7 @@ import org.apache.commons.lang3.Strings;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntitySignatureIdAdapter;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
|
||||
@@ -59,6 +61,7 @@ public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformSe
|
||||
private final ContentStoreService storeService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final FineractProperties properties;
|
||||
private final List<EntitySignatureIdAdapter> signatureIdAdapters;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -74,13 +77,30 @@ public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformSe
|
||||
|
||||
path = storeService.upload(path, request.getStream(), request.getType());
|
||||
|
||||
final var doc = new Document().setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId())
|
||||
.setName(Optional.ofNullable(request.getName()).orElse(request.getFileName())).setFileName(request.getFileName())
|
||||
.setSize(request.getSize()).setType(request.getType()).setDescription(request.getDescription()).setLocation(path)
|
||||
.setStorageType(storeService.getType().getValue());
|
||||
final var docName = Optional.ofNullable(request.getName()).orElse(request.getFileName());
|
||||
|
||||
// Some document "slots" (e.g. a client's signature) are single-slot, mirroring
|
||||
// m_client.image_id: re-upload should replace the existing row in place rather than
|
||||
// leave the old one behind as an orphan whose ordering readers then disagree about.
|
||||
// Adapters recognize their own (entityType, name) convention and own the pointer.
|
||||
final var signatureAdapter = signatureIdAdapters.stream()
|
||||
.filter(adapter -> adapter.accept(request.getEntityType(), docName)).findFirst();
|
||||
|
||||
final var doc = signatureAdapter.flatMap(adapter -> adapter.get(request.getEntityId()))
|
||||
.flatMap(result -> documentRepository.findById(result.getId())).map(existing -> {
|
||||
// replacing an existing single-slot document: drop the old file, reuse the row
|
||||
storeService.delete(existing.getLocation());
|
||||
return existing;
|
||||
}).orElseGet(Document::new);
|
||||
|
||||
doc.setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId()).setName(docName)
|
||||
.setFileName(request.getFileName()).setSize(request.getSize()).setType(request.getType())
|
||||
.setDescription(request.getDescription()).setLocation(path).setStorageType(storeService.getType().getValue());
|
||||
|
||||
documentRepository.save(doc);
|
||||
|
||||
signatureAdapter.ifPresent(adapter -> adapter.set(request.getEntityId(), doc.getId()));
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(documentMapper.map(doc)));
|
||||
|
||||
return DocumentCreateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ import org.springframework.stereotype.Component;
|
||||
@Path("/v1/journalentries")
|
||||
@Component
|
||||
@Tag(name = "Journal Entries", description = """
|
||||
A journal entry refers to the logging of transactions against general ledger accounts. A journal entry may consist of several line items, each of which is either a "debit" or a "credit". The total amount of the debits must equal the total amount of the credits or the journal entry is said to be "unbalanced"\s
|
||||
A journal entry refers to the logging of transactions against general ledger accounts. A journal entry may consist of several line items, each of which is either a "debit" or a "credit". The total amount of the debits must equal the total amount of the credits or the journal entry is said to be "unbalanced"
|
||||
|
||||
A journal entry directly changes the account balances on the general ledger""")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
+5
@@ -154,6 +154,11 @@ public class SecurityConfig {
|
||||
// a Fineract Bearer token; security is enforced at the JWT envelope
|
||||
// (signature + encryption) by ImpsJwtUtil / ImpsCryptoUtil inside the handler.
|
||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/imps/**")).permitAll()
|
||||
// Client edit approval email open-tracking pixel — hit by the client's email client
|
||||
// loading a 1×1 <img> tag; no Fineract session available. The token in the URL
|
||||
// path is the only capability credential; the endpoint always returns a GIF so
|
||||
// token validity is not leaked via different HTTP responses.
|
||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/edit/pixel/*")).permitAll()
|
||||
// NPCI clearing-house receiver — the real, separate NPCI service pushes routed
|
||||
// packages here without a Fineract Bearer token; security is enforced by the
|
||||
// envelope's RSA signature/encryption (NpciCryptoUtil) inside the handler.
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.email.service;
|
||||
import java.time.Year;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
public class EmailTemplateServiceImpl implements EmailTemplateService {
|
||||
@@ -129,7 +130,7 @@ public class EmailTemplateServiceImpl implements EmailTemplateService {
|
||||
+ escape(supportEmail) + "</a>"
|
||||
+ " </p>"
|
||||
+ " <p style=\"margin:0;font-size:12px;color:#94a3b8;\">"
|
||||
+ " © " + Year.now().getValue() + " " + escape(brandName) + ". All rights reserved."
|
||||
+ " © " + Year.now(DateUtils.getDateTimeZoneOfTenant()).getValue() + " " + escape(brandName) + ". All rights reserved."
|
||||
+ " </p>"
|
||||
+ " </td></tr>"
|
||||
+ " </table>"
|
||||
|
||||
+1
@@ -109,6 +109,7 @@ public class ImpsCryptoUtil {
|
||||
}
|
||||
|
||||
/** Decrypt inbound payload using the CBS private key. */
|
||||
@SuppressWarnings("StringSplitter")
|
||||
public String decryptJson(String encryptedPayload) {
|
||||
if (cbsPrivateKey == null) {
|
||||
throw new IllegalStateException("CBS private key not loaded — check fineract.imps.cbs.private-key-path");
|
||||
|
||||
+2
-1
@@ -34,6 +34,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* STUB implementation — returns hard-coded SUCCESS for every request and
|
||||
@@ -108,7 +109,7 @@ public class ImpsCbsServiceImpl implements ImpsCbsService {
|
||||
resp.setRefId(request.getRefId());
|
||||
resp.setUtr(request.getUtr());
|
||||
resp.setAmount(request.getAmount());
|
||||
resp.setTimeStamp(OffsetDateTime.now().format(ISO_OFFSET));
|
||||
resp.setTimeStamp(OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(ISO_OFFSET));
|
||||
resp.setStatus("SUCCESS");
|
||||
resp.setResponseCode("00");
|
||||
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
|
||||
|
||||
+2
-2
@@ -28,7 +28,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.MediaType;
|
||||
@@ -42,6 +41,7 @@ import org.apache.fineract.infrastructure.openproject.data.OpenProjectProjectDat
|
||||
import org.apache.fineract.infrastructure.openproject.data.OpenProjectStatusSummaryData;
|
||||
import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -164,7 +164,7 @@ public class OpenProjectService {
|
||||
}
|
||||
|
||||
// Set required custom field: "Last Follow-up Date" (customField8)
|
||||
body.addProperty("customField8", LocalDate.now().toString());
|
||||
body.addProperty("customField8", LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
|
||||
if (typeName != null && !typeName.isBlank()) {
|
||||
JsonObject links = new JsonObject();
|
||||
|
||||
+2
-1
@@ -47,6 +47,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -127,7 +128,7 @@ public class RbiFeedFetchService {
|
||||
NodeList items = doc.getElementsByTagName("item");
|
||||
log.debug("RBI FeedFetchService - Parsed {} items from RSS for category: {}", items.getLength(), category);
|
||||
List<RbiFeedItem> newItems = new ArrayList<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
for (int i = 0; i < items.getLength(); i++) {
|
||||
Element item = (Element) items.item(i);
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.security.otp.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -107,7 +109,7 @@ public class EmailLoginOtpApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.security.otp.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -107,7 +109,7 @@ public class LoginOtpApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
public class EmailLoginOtpServiceImpl implements EmailLoginOtpService {
|
||||
@@ -128,7 +129,7 @@ public class EmailLoginOtpServiceImpl implements EmailLoginOtpService {
|
||||
|
||||
// Render the branded template — same look and feel for OTP, welcome, password-reset, alerts.
|
||||
final String html = emailTemplateService.renderOtp(recipientName, otp, validityMinutes);
|
||||
final String subject = "Your OTP for Login - " + appName + " (" + LocalDateTime.now().format(SUBJECT_TIMESTAMP) + ")";
|
||||
final String subject = "Your OTP for Login - " + appName + " (" + LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(SUBJECT_TIMESTAMP) + ")";
|
||||
|
||||
final boolean sent = emailSenderService.sendHtml(email, subject, html);
|
||||
if (sent) {
|
||||
|
||||
-2
@@ -20,7 +20,6 @@ package org.apache.fineract.organisation.teller.service;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
@@ -44,7 +43,6 @@ import org.apache.fineract.organisation.teller.domain.CashierTransactionReposito
|
||||
import org.apache.fineract.organisation.teller.domain.CashierTxnType;
|
||||
import org.apache.fineract.organisation.teller.domain.Teller;
|
||||
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
|
||||
import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException;
|
||||
import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
|
||||
import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException;
|
||||
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Request body for {@code POST /v1/aml/alerts/{id}/review}. {@code disposition}: WHITELIST | ESCALATE. */
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class AlertReviewRequest {
|
||||
|
||||
private String disposition;
|
||||
private String notes;
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
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")
|
||||
@Component
|
||||
@Tag(name = "AML Alerts", description = "Live transaction-monitoring alerts raised by the AML rule engine against savings, loan and fund-transfer activity.")
|
||||
@RequiredArgsConstructor
|
||||
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 })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List AML alerts",
|
||||
description = "Optionally filter by status via ?status= : RAISED, UNDER_REVIEW, WHITELISTED, ESCALATED.")
|
||||
public String retrieveAll(@QueryParam("status") final String status) {
|
||||
this.context.authenticatedUser();
|
||||
final List<AmlAlertData> alerts = this.readPlatformService.retrieve(status);
|
||||
return this.toApiJsonSerializer.serialize(alerts);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{alertId}/review")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "AML Officer first-level review of an alert",
|
||||
description = "disposition WHITELIST closes the alert as a false positive; ESCALATE creates an STR case for Principal Officer review.")
|
||||
public String review(@PathParam("alertId") final Long alertId, final AlertReviewRequest request) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("REVIEW_AML_ALERT");
|
||||
if ("ESCALATE".equalsIgnoreCase(request.getDisposition())) {
|
||||
this.writePlatformService.escalate(alertId, request.getNotes());
|
||||
} else {
|
||||
this.writePlatformService.whitelist(alertId, request.getNotes());
|
||||
}
|
||||
final AmlAlertData updated = this.readPlatformService.retrieve(null).stream().filter(a -> a.id().equals(alertId)).findFirst()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.YearMonth;
|
||||
import java.util.List;
|
||||
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.AmlCtrReportData;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlCtrReportGenerationService;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlCtrReportReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/aml/ctr-reports")
|
||||
@Component
|
||||
@Tag(name = "AML CTR Reports",
|
||||
description = "Monthly, mechanical Cash Transaction Reports - threshold-based, no Principal Officer judgment involved.")
|
||||
@RequiredArgsConstructor
|
||||
public class AmlCtrReportApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final AmlCtrReportReadPlatformService readPlatformService;
|
||||
private final AmlCtrReportGenerationService generationService;
|
||||
private final DefaultToApiJsonSerializer<AmlCtrReportData> toApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List generated CTR reports")
|
||||
public String retrieveAll() {
|
||||
this.context.authenticatedUser();
|
||||
final List<AmlCtrReportData> reports = this.readPlatformService.retrieveAll();
|
||||
return this.toApiJsonSerializer.serialize(reports);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("generate")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Generate the CTR report for a month", description = "month format: yyyy-MM. Idempotent - fails if already generated.")
|
||||
public String generate(final CtrGenerateRequest request) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("REVIEW_AML_ALERT");
|
||||
final YearMonth month = YearMonth.parse(request.getMonth());
|
||||
this.generationService.generateForMonth(month);
|
||||
return this.toApiJsonSerializer.serialize(this.readPlatformService.retrieveAll());
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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 java.util.List;
|
||||
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.AmlStrCaseData;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlStrConclusion;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlStrCaseReadPlatformService;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlStrCaseWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/aml/str-cases")
|
||||
@Component
|
||||
@Tag(name = "AML STR Cases",
|
||||
description = "Principal-Officer-only investigation and conclusion of escalated AML alerts, and STR filing to FIU-IND.")
|
||||
@RequiredArgsConstructor
|
||||
public class AmlStrCaseApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final AmlStrCaseReadPlatformService readPlatformService;
|
||||
private final AmlStrCaseWritePlatformService writePlatformService;
|
||||
private final DefaultToApiJsonSerializer<AmlStrCaseData> toApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List AML STR cases")
|
||||
public String retrieveAll() {
|
||||
this.context.authenticatedUser();
|
||||
final List<AmlStrCaseData> cases = this.readPlatformService.retrieveAll();
|
||||
return this.toApiJsonSerializer.serialize(cases);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{strCaseId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve one AML STR case")
|
||||
public String retrieveOne(@PathParam("strCaseId") final Long strCaseId) {
|
||||
this.context.authenticatedUser();
|
||||
return this.toApiJsonSerializer.serialize(this.readPlatformService.retrieveOne(strCaseId));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("weekly-digest")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Internal MIS digest of STR cases concluded suspicious in the last 7 days",
|
||||
description = "For compliance tracking only - does not replace each case's own 7-working-day FIU filing deadline.")
|
||||
public String weeklyDigest() {
|
||||
this.context.authenticatedUser();
|
||||
final List<AmlStrCaseData> digest = this.readPlatformService.weeklyDigest();
|
||||
return this.toApiJsonSerializer.serialize(digest);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{strCaseId}/assign")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Assign a Principal Officer to an STR case")
|
||||
public String assign(@PathParam("strCaseId") final Long strCaseId, final StrCaseAssignRequest request) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CONCLUDE_AML_STR");
|
||||
this.writePlatformService.assignPrincipalOfficer(strCaseId, request.getPrincipalOfficerUserId());
|
||||
return this.toApiJsonSerializer.serialize(this.readPlatformService.retrieveOne(strCaseId));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{strCaseId}/conclude")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Principal Officer conclusion",
|
||||
description = "conclusion NOT_SUSPICIOUS closes the case; SUSPICIOUS sets the 7-working-day FIU filing due date starting today.")
|
||||
public String conclude(@PathParam("strCaseId") final Long strCaseId, final StrCaseConcludeRequest request) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CONCLUDE_AML_STR");
|
||||
final AmlStrConclusion conclusion = AmlStrConclusion.valueOf(request.getConclusion());
|
||||
this.writePlatformService.conclude(strCaseId, conclusion, request.getInvestigationNotes(), request.getSourceOfFundsNotes());
|
||||
return this.toApiJsonSerializer.serialize(this.readPlatformService.retrieveOne(strCaseId));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{strCaseId}/file")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Record that the STR has been filed with FIU-IND")
|
||||
public String fileStr(@PathParam("strCaseId") final Long strCaseId, final StrCaseFileRequest request) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CONCLUDE_AML_STR");
|
||||
this.writePlatformService.fileStr(strCaseId, request.getReferenceNo());
|
||||
return this.toApiJsonSerializer.serialize(this.readPlatformService.retrieveOne(strCaseId));
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
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.ClientRiskCategoryHistoryData;
|
||||
import org.apache.fineract.portfolio.aml.service.ClientRiskCategoryHistoryReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/clients/{clientId}/risk-category-history")
|
||||
@Component
|
||||
@Tag(name = "Client Risk Category History",
|
||||
description = "Audit trail of a client's KYC risk-category changes, including automatic escalation after repeated AML STR cases.")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientRiskCategoryHistoryApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientRiskCategoryHistoryReadPlatformService readPlatformService;
|
||||
private final DefaultToApiJsonSerializer<ClientRiskCategoryHistoryData> toApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve a client's risk-category change history")
|
||||
public String retrieveForClient(@PathParam("clientId") final Long clientId) {
|
||||
this.context.authenticatedUser();
|
||||
final List<ClientRiskCategoryHistoryData> history = this.readPlatformService.retrieveForClient(clientId);
|
||||
return this.toApiJsonSerializer.serialize(history);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Request body for {@code POST /v1/aml/ctr-reports/generate}. {@code month} format: yyyy-MM. */
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class CtrGenerateRequest {
|
||||
|
||||
private String month;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Request body for {@code POST /v1/aml/alerts/manual-referral}.
|
||||
*
|
||||
* {@code journalEntryId} is acc_gl_journal_entry.id as shown in the ledger. {@code disposition} is WHITELIST (cleared,
|
||||
* no report) or ESCALATE (raises an STR case for the Principal Officer).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ManualReferralRequest {
|
||||
|
||||
private Long journalEntryId;
|
||||
private String disposition;
|
||||
private String notes;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Request body for {@code POST /v1/aml/str-cases/{id}/assign}. */
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StrCaseAssignRequest {
|
||||
|
||||
private Long principalOfficerUserId;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Request body for {@code POST /v1/aml/str-cases/{id}/conclude}. {@code conclusion}: NOT_SUSPICIOUS | SUSPICIOUS. */
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StrCaseConcludeRequest {
|
||||
|
||||
private String conclusion;
|
||||
private String investigationNotes;
|
||||
private String sourceOfFundsNotes;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Request body for {@code POST /v1/aml/str-cases/{id}/file}. */
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class StrCaseFileRequest {
|
||||
|
||||
private String referenceNo;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record AmlAlertData(Long id, String transactionType, Long transactionId, Long clientId, String clientDisplayName,
|
||||
String ruleCode, String ruleDescription, BigDecimal amount, String currencyCode, LocalDateTime raisedOn, String status,
|
||||
String reviewedByUsername, LocalDateTime reviewedOn, String reviewNotes, String disposition) implements Serializable {
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record AmlCtrReportData(Long id, String reportMonth, LocalDateTime generatedOn, int recordCount, String fileReference,
|
||||
String status, LocalDateTime submittedOn) implements Serializable {
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record AmlStrCaseData(Long id, Long alertId, Long clientId, String clientDisplayName, String assignedPoUsername,
|
||||
String investigationNotes, String sourceOfFundsNotes, String conclusion, String concludedByUsername, LocalDate concludedOn,
|
||||
LocalDate fiuDueDate, LocalDate strFiledOn, String strReferenceNo, String status, long cumulativeStrCaseCountForClient)
|
||||
implements Serializable {
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlTransactionType;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
|
||||
/**
|
||||
* Normalized view of a savings/loan/transfer transaction handed to {@code AmlRuleEvaluationService}
|
||||
* by the business-event listener that observed it. Keeps the evaluation service itself decoupled
|
||||
* from the savings/loan/fund-transfer domain models - each listener is responsible for building
|
||||
* this context (including, where relevant, {@link #windowAggregateAmount}) from the repositories
|
||||
* it already has access to.
|
||||
*/
|
||||
public final class AmlTransactionContext {
|
||||
|
||||
private final AmlTransactionType transactionType;
|
||||
private final Long transactionId;
|
||||
private final Client client;
|
||||
private final BigDecimal amount;
|
||||
private final String currencyCode;
|
||||
private final LocalDateTime occurredOn;
|
||||
private final boolean cash;
|
||||
/**
|
||||
* For STRUCTURING/VELOCITY rules: the sum of this client's qualifying transactions of this
|
||||
* type within the rule's window, INCLUSIVE of this transaction. Null if not computed - such
|
||||
* rules are simply skipped for this context rather than guessed at.
|
||||
*/
|
||||
private final BigDecimal windowAggregateAmount;
|
||||
|
||||
public AmlTransactionContext(AmlTransactionType transactionType, Long transactionId, Client client, BigDecimal amount,
|
||||
String currencyCode, LocalDateTime occurredOn, boolean cash, BigDecimal windowAggregateAmount) {
|
||||
this.transactionType = transactionType;
|
||||
this.transactionId = transactionId;
|
||||
this.client = client;
|
||||
this.amount = amount;
|
||||
this.currencyCode = currencyCode;
|
||||
this.occurredOn = occurredOn;
|
||||
this.cash = cash;
|
||||
this.windowAggregateAmount = windowAggregateAmount;
|
||||
}
|
||||
|
||||
public AmlTransactionType getTransactionType() {
|
||||
return transactionType;
|
||||
}
|
||||
|
||||
public Long getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return currencyCode;
|
||||
}
|
||||
|
||||
public LocalDateTime getOccurredOn() {
|
||||
return occurredOn;
|
||||
}
|
||||
|
||||
public boolean isCash() {
|
||||
return cash;
|
||||
}
|
||||
|
||||
public BigDecimal getWindowAggregateAmount() {
|
||||
return windowAggregateAmount;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record ClientRiskCategoryHistoryData(Long id, Long clientId, Integer previousCategory, Integer newCategory, String reason,
|
||||
String triggeredBy, Long strCaseId, LocalDateTime changedOn, String changedByUsername) implements Serializable {
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* One ledger entry a user has marked, so the ledger screen can show what has already been dealt with.
|
||||
*
|
||||
* {@code journalEntryId} is the acc_gl_journal_entry row the mark was made against - it is what the ledger table keys
|
||||
* on. {@code strCaseId} is populated only where the mark escalated into an STR case.
|
||||
*/
|
||||
public record ManualReferralData(Long alertId, Long journalEntryId, Long clientId, String clientDisplayName,
|
||||
BigDecimal amount, String currencyCode, LocalDateTime raisedOn, String status, String disposition,
|
||||
String reviewedByUsername, String reviewNotes, Long strCaseId) implements Serializable {}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.event;
|
||||
|
||||
/** Marker interface whose only implementation registers AML business-event listeners on startup. */
|
||||
public interface AmlDomainService {
|
||||
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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.event;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.portfolio.aml.data.AmlTransactionContext;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlTransactionType;
|
||||
import org.apache.fineract.portfolio.aml.repository.AmlLoanTransactionQueryRepository;
|
||||
import org.apache.fineract.portfolio.aml.repository.AmlSavingsTransactionQueryRepository;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlRuleEvaluationService;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
|
||||
/**
|
||||
* Registers post-business-event listeners that evaluate every live savings and loan transaction
|
||||
* against the AML rule catalog, purely additively - listeners observe transactions that already
|
||||
* completed successfully and never alter their outcome. Runs synchronously in the same request,
|
||||
* mirroring {@code org.apache.fineract.notification.service.NotificationDomainServiceImpl}, so an
|
||||
* alert is atomic with the transaction that raised it.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AmlDomainServiceImpl implements AmlDomainService {
|
||||
|
||||
/**
|
||||
* Fixed lookback used for every STRUCTURING/VELOCITY rule regardless of its own configured
|
||||
* window_days - a v1 approximation (errs toward over-inclusion, the safer failure mode for AML
|
||||
* monitoring) until per-rule windows are wired through.
|
||||
*/
|
||||
private static final int STRUCTURING_WINDOW_DAYS = 30;
|
||||
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final AmlRuleEvaluationService ruleEvaluationService;
|
||||
private final AmlSavingsTransactionQueryRepository savingsQueryRepository;
|
||||
private final AmlLoanTransactionQueryRepository loanQueryRepository;
|
||||
|
||||
@PostConstruct
|
||||
public void addListeners() {
|
||||
businessEventNotifierService.addPostBusinessEventListener(SavingsDepositBusinessEvent.class, new SavingsDepositListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(SavingsWithdrawalBusinessEvent.class, new SavingsWithdrawalListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanDisbursalTransactionBusinessEvent.class,
|
||||
new LoanDisbursalListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanTransactionMakeRepaymentPostBusinessEvent.class,
|
||||
new LoanRepaymentListener());
|
||||
}
|
||||
|
||||
private final class SavingsDepositListener implements BusinessEventListener<SavingsDepositBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(SavingsDepositBusinessEvent event) {
|
||||
evaluateSavingsTransaction(event.get(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private final class SavingsWithdrawalListener implements BusinessEventListener<SavingsWithdrawalBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(SavingsWithdrawalBusinessEvent event) {
|
||||
evaluateSavingsTransaction(event.get(), false);
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanDisbursalListener implements BusinessEventListener<LoanDisbursalTransactionBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(LoanDisbursalTransactionBusinessEvent event) {
|
||||
evaluateLoanTransaction(event.get());
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanRepaymentListener implements BusinessEventListener<LoanTransactionMakeRepaymentPostBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(LoanTransactionMakeRepaymentPostBusinessEvent event) {
|
||||
evaluateLoanTransaction(event.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void evaluateSavingsTransaction(SavingsAccountTransaction transaction, boolean depositDirection) {
|
||||
final Client client = transaction.getSavingsAccount().getClient();
|
||||
if (client == null) {
|
||||
// group/JLG savings with no individual client behind it are out of scope for this rule set
|
||||
return;
|
||||
}
|
||||
final LocalDate since = LocalDate.now().minusDays(STRUCTURING_WINDOW_DAYS);
|
||||
final List<SavingsAccountTransaction> recent = savingsQueryRepository.findForClientSince(client.getId(), since);
|
||||
final BigDecimal windowAggregate = recent.stream().filter(t -> depositDirection ? t.isDeposit() : t.isWithdrawal())
|
||||
.map(SavingsAccountTransaction::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
final AmlTransactionContext context = new AmlTransactionContext(AmlTransactionType.SAVINGS, transaction.getId(), client,
|
||||
transaction.getAmount(), transaction.getSavingsAccount().getCurrency().getCode(),
|
||||
transaction.getDateOf().atStartOfDay(), isCash(transaction.getPaymentDetail()), windowAggregate);
|
||||
try {
|
||||
ruleEvaluationService.evaluate(context);
|
||||
} catch (final RuntimeException e) {
|
||||
// AML alerting must never block the underlying savings transaction it is observing
|
||||
log.error("AML rule evaluation failed for savings transaction {}", transaction.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void evaluateLoanTransaction(LoanTransaction transaction) {
|
||||
final Client client = transaction.getLoan().getClient();
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
final LocalDate since = LocalDate.now().minusDays(STRUCTURING_WINDOW_DAYS);
|
||||
final List<LoanTransaction> recent = loanQueryRepository.findForClientSince(client.getId(), since);
|
||||
final BigDecimal windowAggregate = recent.stream().filter(LoanTransaction::isRepaymentLikeType)
|
||||
.map(t -> t.getAmount(transaction.getLoan().getCurrency()).getAmount()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
final AmlTransactionContext context = new AmlTransactionContext(AmlTransactionType.LOAN, transaction.getId(), client,
|
||||
transaction.getAmount(transaction.getLoan().getCurrency()).getAmount(), transaction.getLoan().getCurrency().getCode(),
|
||||
transaction.getTransactionDate().atStartOfDay(), isCash(transaction.getPaymentDetail()), windowAggregate);
|
||||
try {
|
||||
ruleEvaluationService.evaluate(context);
|
||||
} catch (final RuntimeException e) {
|
||||
log.error("AML rule evaluation failed for loan transaction {}", transaction.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCash(PaymentDetail paymentDetail) {
|
||||
return paymentDetail == null || Boolean.TRUE.equals(paymentDetail.getPaymentType().getIsCashPayment());
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.job;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlCtrReportGenerationService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class AmlCtrReportBatchJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private AmlCtrReportGenerationService generationService;
|
||||
|
||||
@Bean
|
||||
public AmlCtrReportBatchTasklet amlCtrReportBatchTasklet() {
|
||||
return new AmlCtrReportBatchTasklet(generationService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step amlCtrReportBatchStep() {
|
||||
return new StepBuilder(JobName.CTR_REPORT_GENERATION.name(), jobRepository)
|
||||
.tasklet(amlCtrReportBatchTasklet(), transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job amlCtrReportBatchJob() {
|
||||
return new JobBuilder(JobName.CTR_REPORT_GENERATION.name(), jobRepository).start(amlCtrReportBatchStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.job;
|
||||
|
||||
import java.time.YearMonth;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlCtrReport;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlCtrReportGenerationService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
/**
|
||||
* Generates the CTR report for the previous calendar month as a {@code DRAFT} - mechanical
|
||||
* threshold aggregation only. Admin-triggered today (see {@code AmlCtrReportApiResource}); wiring
|
||||
* to a real monthly cron is a follow-up, matching the same "driven by an admin endpoint today"
|
||||
* stance already used for {@code NeftBatchProcessorService}.
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AmlCtrReportBatchTasklet implements Tasklet {
|
||||
|
||||
private final AmlCtrReportGenerationService generationService;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
final YearMonth previousMonth = YearMonth.now().minusMonths(1);
|
||||
try {
|
||||
final AmlCtrReport report = generationService.generateForMonth(previousMonth);
|
||||
log.info("[Job:CTR_REPORT_GENERATION] generated CTR report for {} with {} line(s)", previousMonth,
|
||||
report.getRecordCount());
|
||||
} catch (final IllegalStateException e) {
|
||||
log.info("[Job:CTR_REPORT_GENERATION] {} — skipping", e.getMessage());
|
||||
}
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.job;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlStrWeeklyDigestService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class AmlStrWeeklyDigestJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private AmlStrWeeklyDigestService digestService;
|
||||
|
||||
@Bean
|
||||
public AmlStrWeeklyDigestTasklet amlStrWeeklyDigestTasklet() {
|
||||
return new AmlStrWeeklyDigestTasklet(digestService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step amlStrWeeklyDigestStep() {
|
||||
return new StepBuilder(JobName.AML_STR_WEEKLY_DIGEST.name(), jobRepository)
|
||||
.tasklet(amlStrWeeklyDigestTasklet(), transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job amlStrWeeklyDigestJob() {
|
||||
return new JobBuilder(JobName.AML_STR_WEEKLY_DIGEST.name(), jobRepository).start(amlStrWeeklyDigestStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.job;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlStrCase;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlStrWeeklyDigestService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
/**
|
||||
* Periodic heartbeat that surfaces the current week's suspicious-STR count in the application log
|
||||
* for ops visibility. The actual digest content (for compliance MIS use) is pulled on demand via
|
||||
* {@code AmlStrCaseApiResource#weeklyDigest}, mirroring how the CTR job only drafts a report that a
|
||||
* human then reviews/exports.
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AmlStrWeeklyDigestTasklet implements Tasklet {
|
||||
|
||||
private final AmlStrWeeklyDigestService digestService;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
final List<AmlStrCase> digest = digestService.currentWeekDigest();
|
||||
log.info("[Job:AML_STR_WEEKLY_DIGEST] {} STR case(s) concluded suspicious in the last 7 days", digest.size());
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* Read-only aggregation over {@code FundTransfer} for AML window-based rules (structuring /
|
||||
* velocity), across all channels (INTERNAL/IMPS/NEFT/RTGS).
|
||||
*
|
||||
* <p>
|
||||
* Lives under {@code .repository} (not {@code .service}) because {@code JPAConfig} only scans
|
||||
* {@code org.apache.fineract.**.domain} and {@code org.apache.fineract.**.repository} for
|
||||
* {@code @EnableJpaRepositories}.
|
||||
*/
|
||||
public interface AmlFundTransferQueryRepository extends JpaRepository<FundTransfer, Long> {
|
||||
|
||||
@Query("SELECT COALESCE(SUM(t.amount), 0) FROM FundTransfer t WHERE t.clientId = :clientId "
|
||||
+ "AND t.status = :status AND t.initiatedAt >= :since")
|
||||
BigDecimal sumForClientSince(@Param("clientId") Long clientId, @Param("status") FundTransferStatus status,
|
||||
@Param("since") LocalDateTime since);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* Read-only aggregation queries over {@code LoanTransaction} for AML window-based rules
|
||||
* (structuring / velocity). Direction/kind (repayment-like) is filtered in Java via
|
||||
* {@link LoanTransaction#isRepaymentLikeType()} rather than in JPQL.
|
||||
*
|
||||
* <p>
|
||||
* Lives under {@code .repository} (not {@code .service}) because {@code JPAConfig} only scans
|
||||
* {@code org.apache.fineract.**.domain} and {@code org.apache.fineract.**.repository} for
|
||||
* {@code @EnableJpaRepositories}.
|
||||
*/
|
||||
public interface AmlLoanTransactionQueryRepository extends JpaRepository<LoanTransaction, Long> {
|
||||
|
||||
@Query("SELECT t FROM LoanTransaction t WHERE t.loan.client.id = :clientId AND t.dateOf >= :since AND t.reversed = false")
|
||||
List<LoanTransaction> findForClientSince(@Param("clientId") Long clientId, @Param("since") LocalDate since);
|
||||
|
||||
@Query("SELECT t FROM LoanTransaction t WHERE t.dateOf BETWEEN :start AND :end AND t.reversed = false")
|
||||
List<LoanTransaction> findInDateRange(@Param("start") LocalDate start, @Param("end") LocalDate end);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* Read-only aggregation queries over {@code SavingsAccountTransaction} for AML window-based rules
|
||||
* (structuring / velocity). Kept separate from {@code SavingsAccountTransactionRepository} so the
|
||||
* AML module doesn't modify savings module code - direction (deposit/withdrawal) is filtered in
|
||||
* Java via {@link SavingsAccountTransaction#isDeposit()}/{@code isWithdrawal()} rather than in
|
||||
* JPQL, since {@code typeOf} there is a raw internal code.
|
||||
*
|
||||
* <p>
|
||||
* Lives under {@code .repository} (not {@code .service}) because {@code JPAConfig} only scans
|
||||
* {@code org.apache.fineract.**.domain} and {@code org.apache.fineract.**.repository} for
|
||||
* {@code @EnableJpaRepositories}.
|
||||
*/
|
||||
public interface AmlSavingsTransactionQueryRepository extends JpaRepository<SavingsAccountTransaction, Long> {
|
||||
|
||||
@Query("SELECT t FROM SavingsAccountTransaction t WHERE t.savingsAccount.client.id = :clientId "
|
||||
+ "AND t.dateOf >= :since AND t.reversed = false")
|
||||
List<SavingsAccountTransaction> findForClientSince(@Param("clientId") Long clientId, @Param("since") LocalDate since);
|
||||
|
||||
@Query("SELECT t FROM SavingsAccountTransaction t WHERE t.dateOf BETWEEN :start AND :end AND t.reversed = false")
|
||||
List<SavingsAccountTransaction> findInDateRange(@Param("start") LocalDate start, @Param("end") LocalDate end);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.aml.data.AmlAlertData;
|
||||
|
||||
public interface AmlAlertReadPlatformService {
|
||||
|
||||
/** @param statusFilter one of RAISED/UNDER_REVIEW/WHITELISTED/ESCALATED, or null/blank for all. */
|
||||
List<AmlAlertData> retrieve(String statusFilter);
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.aml.data.AmlAlertData;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlAlert;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlAlertRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class AmlAlertReadPlatformServiceImpl implements AmlAlertReadPlatformService {
|
||||
|
||||
private final AmlAlertRepository repository;
|
||||
|
||||
@Override
|
||||
public List<AmlAlertData> retrieve(String statusFilter) {
|
||||
final List<AmlAlert> alerts = (statusFilter == null || statusFilter.isBlank()) ? repository.findAll()
|
||||
: repository.findByStatusOrderByRaisedOnAsc(statusFilter);
|
||||
return alerts.stream().sorted(Comparator.comparing(AmlAlert::getRaisedOn).reversed()).map(this::mapToData)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private AmlAlertData mapToData(AmlAlert alert) {
|
||||
return new AmlAlertData(alert.getId(), alert.getTransactionType().name(), alert.getTransactionId(), alert.getClient().getId(),
|
||||
alert.getClient().getDisplayName(), alert.getRule().getRuleCode(), alert.getRule().getDescription(), alert.getAmount(),
|
||||
alert.getCurrencyCode(), alert.getRaisedOn(), alert.getStatus().name(),
|
||||
alert.getReviewedBy() == null ? null : alert.getReviewedBy().getUsername(), alert.getReviewedOn(),
|
||||
alert.getReviewNotes(), alert.getDisposition() == null ? null : alert.getDisposition().name());
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 org.apache.fineract.portfolio.aml.domain.AmlAlert;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlStrCase;
|
||||
|
||||
/** First-level AML Officer disposition on an {@link AmlAlert}. */
|
||||
public interface AmlAlertWritePlatformService {
|
||||
|
||||
AmlAlert whitelist(Long alertId, String notes);
|
||||
|
||||
AmlStrCase escalate(Long alertId, String notes);
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
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.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class AmlAlertWritePlatformServiceImpl implements AmlAlertWritePlatformService {
|
||||
|
||||
private final AmlAlertRepository alertRepository;
|
||||
private final AmlStrCaseRepository strCaseRepository;
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientRiskEscalationService clientRiskEscalationService;
|
||||
|
||||
@Override
|
||||
public AmlAlert whitelist(Long alertId, String notes) {
|
||||
final AmlAlert alert = loadAlert(alertId);
|
||||
alert.whitelist(context.authenticatedUser(), notes);
|
||||
return alertRepository.save(alert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AmlStrCase escalate(Long alertId, String notes) {
|
||||
final AmlAlert alert = loadAlert(alertId);
|
||||
alert.escalate(context.authenticatedUser(), notes);
|
||||
alertRepository.save(alert);
|
||||
|
||||
final AmlStrCase strCase = strCaseRepository.save(AmlStrCase.openFor(alert));
|
||||
clientRiskEscalationService.onStrCaseEscalated(strCase);
|
||||
return strCase;
|
||||
}
|
||||
|
||||
private AmlAlert loadAlert(Long alertId) {
|
||||
return alertRepository.findById(alertId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("AML alert " + alertId + " does not exist"));
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.YearMonth;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlCtrReport;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlCtrReportLine;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlCtrReportLineRepository;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlCtrReportRepository;
|
||||
import org.apache.fineract.portfolio.aml.domain.AmlTransactionType;
|
||||
import org.apache.fineract.portfolio.aml.repository.AmlLoanTransactionQueryRepository;
|
||||
import org.apache.fineract.portfolio.aml.repository.AmlSavingsTransactionQueryRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Monthly, mechanical CTR batch: every individually-large cash transaction (savings or loan, ≥
|
||||
* ₹10L) gets its own line; every client whose smaller cash transactions of a single type sum to ≥
|
||||
* ₹10L within the month gets one connected-group line for that type. Purely threshold-based - no
|
||||
* Principal Officer judgment, matching the regulation. NEFT/RTGS transfers are non-cash by nature
|
||||
* and are out of scope for CTR.
|
||||
*
|
||||
* <p>
|
||||
* Known v1 limitation: a client splitting cash activity across BOTH savings and loan channels to
|
||||
* stay under ₹10L in each is not caught by this aggregation - each channel is summed separately.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class AmlCtrReportGenerationService {
|
||||
|
||||
private static final BigDecimal CTR_THRESHOLD = new BigDecimal("1000000");
|
||||
|
||||
private final AmlCtrReportRepository ctrReportRepository;
|
||||
private final AmlCtrReportLineRepository ctrReportLineRepository;
|
||||
private final AmlSavingsTransactionQueryRepository savingsQueryRepository;
|
||||
private final AmlLoanTransactionQueryRepository loanQueryRepository;
|
||||
|
||||
public AmlCtrReport generateForMonth(YearMonth month) {
|
||||
final String reportMonth = month.toString();
|
||||
if (ctrReportRepository.findByReportMonth(reportMonth).isPresent()) {
|
||||
throw new IllegalStateException("CTR report for " + reportMonth + " has already been generated");
|
||||
}
|
||||
|
||||
final LocalDate start = month.atDay(1);
|
||||
final LocalDate end = month.atEndOfMonth();
|
||||
|
||||
final List<PendingLine> pendingLines = new ArrayList<>();
|
||||
final Map<Long, BigDecimal> smallSavingsCashByClient = new HashMap<>();
|
||||
final Map<Long, BigDecimal> smallLoanCashByClient = new HashMap<>();
|
||||
|
||||
for (final SavingsAccountTransaction t : savingsQueryRepository.findInDateRange(start, end)) {
|
||||
if (!(t.isDeposit() || t.isWithdrawal()) || !isCash(t.getPaymentDetail())) {
|
||||
continue;
|
||||
}
|
||||
final Client client = t.getSavingsAccount().getClient();
|
||||
if (client == null) {
|
||||
continue;
|
||||
}
|
||||
if (t.getAmount().compareTo(CTR_THRESHOLD) >= 0) {
|
||||
pendingLines.add(new PendingLine(AmlTransactionType.SAVINGS, t.getId(), client.getId(), t.getAmount(), false));
|
||||
} else {
|
||||
smallSavingsCashByClient.merge(client.getId(), t.getAmount(), BigDecimal::add);
|
||||
}
|
||||
}
|
||||
|
||||
for (final LoanTransaction t : loanQueryRepository.findInDateRange(start, end)) {
|
||||
if (!t.isRepaymentLikeType() || !isCash(t.getPaymentDetail())) {
|
||||
continue;
|
||||
}
|
||||
final Client client = t.getLoan().getClient();
|
||||
if (client == null) {
|
||||
continue;
|
||||
}
|
||||
final BigDecimal amount = t.getAmount(t.getLoan().getCurrency()).getAmount();
|
||||
if (amount.compareTo(CTR_THRESHOLD) >= 0) {
|
||||
pendingLines.add(new PendingLine(AmlTransactionType.LOAN, t.getId(), client.getId(), amount, false));
|
||||
} else {
|
||||
smallLoanCashByClient.merge(client.getId(), amount, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
|
||||
addConnectedGroupLines(pendingLines, smallSavingsCashByClient, AmlTransactionType.SAVINGS);
|
||||
addConnectedGroupLines(pendingLines, smallLoanCashByClient, AmlTransactionType.LOAN);
|
||||
|
||||
final AmlCtrReport report = ctrReportRepository
|
||||
.save(AmlCtrReport.draft(reportMonth, LocalDateTime.now(), pendingLines.size()));
|
||||
for (final PendingLine pending : pendingLines) {
|
||||
ctrReportLineRepository.save(AmlCtrReportLine.of(report, pending.transactionType(), pending.transactionId(),
|
||||
pending.clientId(), pending.amount(), pending.connectedGroup()));
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
private void addConnectedGroupLines(List<PendingLine> pendingLines, Map<Long, BigDecimal> smallAmountsByClient,
|
||||
AmlTransactionType transactionType) {
|
||||
smallAmountsByClient.forEach((clientId, total) -> {
|
||||
if (total.compareTo(CTR_THRESHOLD) >= 0) {
|
||||
pendingLines.add(new PendingLine(transactionType, null, clientId, total, true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Holds a line's values before the parent {@link AmlCtrReport} row exists to attach them to. */
|
||||
private record PendingLine(AmlTransactionType transactionType, Long transactionId, Long clientId, BigDecimal amount,
|
||||
boolean connectedGroup) {
|
||||
}
|
||||
|
||||
private boolean isCash(PaymentDetail paymentDetail) {
|
||||
return paymentDetail == null || Boolean.TRUE.equals(paymentDetail.getPaymentType().getIsCashPayment());
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.aml.data.AmlCtrReportData;
|
||||
|
||||
public interface AmlCtrReportReadPlatformService {
|
||||
|
||||
List<AmlCtrReportData> retrieveAll();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user