Compare commits
22
Commits
026d902199
...
uate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7d22382a5 | ||
|
|
8622dcaf90 | ||
|
|
7cdc25322e | ||
|
|
a4c3dbb6c4 | ||
|
|
dd8445de06 | ||
|
|
9345f2294c | ||
|
|
190cd92a85 | ||
|
|
81832b6bb7 | ||
|
|
0bd5296519 | ||
|
|
17e0dab435 | ||
|
|
67810bfaea | ||
|
|
2022dc5241 | ||
|
|
b1cfa996b4 | ||
|
|
02541eed6c | ||
|
|
7c15fbb0c7 | ||
|
|
6e3f195de0 | ||
|
|
a65d801250 | ||
|
|
f45d5890ea | ||
|
|
3c58bb231f | ||
|
|
aabda18b69 | ||
|
|
0e5671b706 | ||
|
|
67214ec0f8 |
@@ -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);
|
||||||
+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"), //
|
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
|
||||||
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
|
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
|
||||||
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
|
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;
|
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
|
||||||
|
}
|
||||||
+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.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);
|
||||||
|
}
|
||||||
+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
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
@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 AmlRuleType getRuleType() {
|
||||||
|
return AmlRuleType.valueOf(ruleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getWindowDays() {
|
||||||
|
return windowDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSourceClientRiskRuleId() {
|
||||||
|
return sourceClientRiskRuleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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 AmlTransactionRuleRepository extends JpaRepository<AmlTransactionRule, Long> {
|
||||||
|
|
||||||
|
List<AmlTransactionRule> findByActiveTrueAndAppliesTo(String appliesTo);
|
||||||
|
}
|
||||||
+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
|
||||||
|
}
|
||||||
+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 CLIENT_CLASSIFICATION = "ClientClassification";
|
||||||
|
|
||||||
public static final String OCCUPATION_CATEGORY = "OccupationCategory";
|
public static final String OCCUPATION_CATEGORY = "OccupationCategory";
|
||||||
|
public static final String PWD_CATEGORY = "PwdCategory";
|
||||||
public static final String OCCUPATION = "Occupation";
|
public static final String OCCUPATION = "Occupation";
|
||||||
|
|
||||||
public static final String CLIENT_NON_PERSON_CONSTITUTION = "Constitution";
|
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 religionParamName = "religion";
|
||||||
public static final String socialCategoryParamName = "socialCategory";
|
public static final String socialCategoryParamName = "socialCategory";
|
||||||
public static final String monthlyIncomeParamName = "monthlyIncome";
|
public static final String monthlyIncomeParamName = "monthlyIncome";
|
||||||
|
public static final String annualIncomeParamName = "annualIncome";
|
||||||
public static final String annualTurnoverParamName = "annualTurnover";
|
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 agricultureIncomeParamName = "agricultureIncome";
|
||||||
public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome";
|
public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome";
|
||||||
public static final String ageYearsParamName = "ageYears";
|
public static final String ageYearsParamName = "ageYears";
|
||||||
@@ -257,8 +264,9 @@ public class ClientApiConstants {
|
|||||||
staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName,
|
staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName,
|
||||||
salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName,
|
salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName,
|
||||||
educationLevelParamName, degreeQualificationParamName,
|
educationLevelParamName, degreeQualificationParamName,
|
||||||
religionParamName, socialCategoryParamName, monthlyIncomeParamName,
|
religionParamName, socialCategoryParamName, monthlyIncomeParamName, annualIncomeParamName,
|
||||||
annualTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
annualTurnoverParamName, monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||||
|
isSeniorCitizenParamName, isPwdParamName, pwdCategoryParamName,
|
||||||
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
||||||
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName,
|
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName,
|
||||||
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
||||||
|
|||||||
@@ -82,7 +82,13 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
private CodeValueData religion;
|
private CodeValueData religion;
|
||||||
private CodeValueData socialCategory;
|
private CodeValueData socialCategory;
|
||||||
private BigDecimal monthlyIncome;
|
private BigDecimal monthlyIncome;
|
||||||
|
private BigDecimal annualIncome;
|
||||||
private BigDecimal annualTurnover;
|
private BigDecimal annualTurnover;
|
||||||
|
private BigDecimal monthlyTurnover;
|
||||||
|
private Boolean isSeniorCitizen;
|
||||||
|
private Boolean isPwd;
|
||||||
|
private CodeValueData pwdCategory;
|
||||||
|
private Collection<CodeValueData> pwdCategoryOptions;
|
||||||
private BigDecimal agricultureIncome;
|
private BigDecimal agricultureIncome;
|
||||||
private BigDecimal nonAgricultureIncome;
|
private BigDecimal nonAgricultureIncome;
|
||||||
private Integer ageYears;
|
private Integer ageYears;
|
||||||
@@ -400,13 +406,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
||||||
data.religionOptions = templateData.religionOptions;
|
data.religionOptions = templateData.religionOptions;
|
||||||
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
||||||
|
data.pwdCategoryOptions = templateData.pwdCategoryOptions;
|
||||||
data.maritalStatus = clientData.maritalStatus;
|
data.maritalStatus = clientData.maritalStatus;
|
||||||
data.educationLevel = clientData.educationLevel;
|
data.educationLevel = clientData.educationLevel;
|
||||||
data.degreeQualification = clientData.degreeQualification;
|
data.degreeQualification = clientData.degreeQualification;
|
||||||
data.religion = clientData.religion;
|
data.religion = clientData.religion;
|
||||||
data.socialCategory = clientData.socialCategory;
|
data.socialCategory = clientData.socialCategory;
|
||||||
data.monthlyIncome = clientData.monthlyIncome;
|
data.monthlyIncome = clientData.monthlyIncome;
|
||||||
|
data.annualIncome = clientData.annualIncome;
|
||||||
data.annualTurnover = clientData.annualTurnover;
|
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.agricultureIncome = clientData.agricultureIncome;
|
||||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||||
data.ageYears = clientData.ageYears;
|
data.ageYears = clientData.ageYears;
|
||||||
@@ -456,13 +468,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
||||||
data.religionOptions = clientData.religionOptions;
|
data.religionOptions = clientData.religionOptions;
|
||||||
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
||||||
|
data.pwdCategoryOptions = clientData.pwdCategoryOptions;
|
||||||
data.maritalStatus = clientData.maritalStatus;
|
data.maritalStatus = clientData.maritalStatus;
|
||||||
data.educationLevel = clientData.educationLevel;
|
data.educationLevel = clientData.educationLevel;
|
||||||
data.degreeQualification = clientData.degreeQualification;
|
data.degreeQualification = clientData.degreeQualification;
|
||||||
data.religion = clientData.religion;
|
data.religion = clientData.religion;
|
||||||
data.socialCategory = clientData.socialCategory;
|
data.socialCategory = clientData.socialCategory;
|
||||||
data.monthlyIncome = clientData.monthlyIncome;
|
data.monthlyIncome = clientData.monthlyIncome;
|
||||||
|
data.annualIncome = clientData.annualIncome;
|
||||||
data.annualTurnover = clientData.annualTurnover;
|
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.agricultureIncome = clientData.agricultureIncome;
|
||||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||||
data.ageYears = clientData.ageYears;
|
data.ageYears = clientData.ageYears;
|
||||||
@@ -509,7 +527,12 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.religion = clientData.religion;
|
data.religion = clientData.religion;
|
||||||
data.socialCategory = clientData.socialCategory;
|
data.socialCategory = clientData.socialCategory;
|
||||||
data.monthlyIncome = clientData.monthlyIncome;
|
data.monthlyIncome = clientData.monthlyIncome;
|
||||||
|
data.annualIncome = clientData.annualIncome;
|
||||||
data.annualTurnover = clientData.annualTurnover;
|
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.agricultureIncome = clientData.agricultureIncome;
|
||||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||||
data.ageYears = clientData.ageYears;
|
data.ageYears = clientData.ageYears;
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "image_id")
|
@Column(name = "image_id")
|
||||||
private Long imageId;
|
private Long imageId;
|
||||||
|
|
||||||
|
@Column(name = "signature_id")
|
||||||
|
private Long signatureId;
|
||||||
|
|
||||||
@Column(name = "status_enum", nullable = false)
|
@Column(name = "status_enum", nullable = false)
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
@@ -231,9 +234,25 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "monthly_income")
|
@Column(name = "monthly_income")
|
||||||
private java.math.BigDecimal monthlyIncome;
|
private java.math.BigDecimal monthlyIncome;
|
||||||
|
|
||||||
|
@Column(name = "annual_income")
|
||||||
|
private java.math.BigDecimal annualIncome;
|
||||||
|
|
||||||
@Column(name = "annual_turnover")
|
@Column(name = "annual_turnover")
|
||||||
private java.math.BigDecimal annualTurnover;
|
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")
|
@Column(name = "agriculture_income")
|
||||||
private java.math.BigDecimal agricultureIncome;
|
private java.math.BigDecimal agricultureIncome;
|
||||||
|
|
||||||
@@ -598,6 +617,10 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return this.office.getId();
|
return this.office.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return this.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
public String mobileNo() {
|
public String mobileNo() {
|
||||||
return this.mobileNo;
|
return this.mobileNo;
|
||||||
}
|
}
|
||||||
@@ -832,6 +855,14 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return this.socialCategory != null ? this.socialCategory.getId() : null;
|
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) {
|
public void updateSocialCategory(CodeValue socialCategory) {
|
||||||
this.socialCategory = socialCategory;
|
this.socialCategory = socialCategory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,6 +586,15 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return riskCategory;
|
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() {
|
public String getPanNumber() {
|
||||||
return panNumber;
|
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")
|
@Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId")
|
||||||
void removeImageId(@Param("clientId") Long 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
|
* 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.
|
* (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();
|
||||||
|
}
|
||||||
+9
@@ -18,7 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.sanctions.domain;
|
package org.apache.fineract.portfolio.sanctions.domain;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
|
|||||||
void deleteAllByActiveTrue();
|
void deleteAllByActiveTrue();
|
||||||
|
|
||||||
long countByActiveTrue();
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
+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);
|
final var target = Files.exists(getPath(safePath, false)) ? getPath(safePath, false) : getPath(safePath, true);
|
||||||
|
|
||||||
try (var in = is) {
|
try (var in = is; var out = new FileOutputStream(target.toString())) {
|
||||||
IOUtils.copy(in, new FileOutputStream(target.toString()));
|
IOUtils.copy(in, out);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new ContentStoreException(e);
|
throw new ContentStoreException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final var relativePath = getRelativePath(target);
|
final var relativePath = getRelativePath(target);
|
||||||
|
|
||||||
try {
|
try (var in = getInputStream(target)) {
|
||||||
postUploadContentPolicy
|
postUploadContentPolicy.check(ContentPolicyContext.builder().path(path).inputStream(in).mimeType(mimeType).build());
|
||||||
.check(ContentPolicyContext.builder().path(path).inputStream(getInputStream(target)).mimeType(mimeType).build());
|
|
||||||
} catch (ContentPolicyException e) {
|
} catch (ContentPolicyException e) {
|
||||||
log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", target, e.getMessage());
|
log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", target, e.getMessage());
|
||||||
|
|
||||||
delete(relativePath.toString());
|
delete(relativePath.toString());
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ContentStoreException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return relativePath.toString();
|
return relativePath.toString();
|
||||||
|
|||||||
+24
-4
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.documentmanagement.service;
|
|||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.contentstore.service.ContentStoreService;
|
||||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
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.DocumentCreateRequest;
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
|
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
|
||||||
@@ -59,6 +61,7 @@ public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformSe
|
|||||||
private final ContentStoreService storeService;
|
private final ContentStoreService storeService;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final FineractProperties properties;
|
private final FineractProperties properties;
|
||||||
|
private final List<EntitySignatureIdAdapter> signatureIdAdapters;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -74,13 +77,30 @@ public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformSe
|
|||||||
|
|
||||||
path = storeService.upload(path, request.getStream(), request.getType());
|
path = storeService.upload(path, request.getStream(), request.getType());
|
||||||
|
|
||||||
final var doc = new Document().setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId())
|
final var docName = Optional.ofNullable(request.getName()).orElse(request.getFileName());
|
||||||
.setName(Optional.ofNullable(request.getName()).orElse(request.getFileName())).setFileName(request.getFileName())
|
|
||||||
.setSize(request.getSize()).setType(request.getType()).setDescription(request.getDescription()).setLocation(path)
|
// Some document "slots" (e.g. a client's signature) are single-slot, mirroring
|
||||||
.setStorageType(storeService.getType().getValue());
|
// 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);
|
documentRepository.save(doc);
|
||||||
|
|
||||||
|
signatureAdapter.ifPresent(adapter -> adapter.set(request.getEntityId(), doc.getId()));
|
||||||
|
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(documentMapper.map(doc)));
|
businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(documentMapper.map(doc)));
|
||||||
|
|
||||||
return DocumentCreateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
|
return DocumentCreateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build();
|
||||||
|
|||||||
+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;
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* 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.service.AmlAlertReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.aml.service.AmlAlertWritePlatformService;
|
||||||
|
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 DefaultToApiJsonSerializer<AmlAlertData> toApiJsonSerializer;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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;
|
||||||
|
}
|
||||||
+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 {
|
||||||
|
}
|
||||||
+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();
|
||||||
|
}
|
||||||
+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.service;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.AmlCtrReportData;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlCtrReport;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlCtrReportRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class AmlCtrReportReadPlatformServiceImpl implements AmlCtrReportReadPlatformService {
|
||||||
|
|
||||||
|
private final AmlCtrReportRepository repository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AmlCtrReportData> retrieveAll() {
|
||||||
|
return repository.findAll().stream().sorted(Comparator.comparing(AmlCtrReport::getReportMonth).reversed())
|
||||||
|
.map(this::mapToData).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AmlCtrReportData mapToData(AmlCtrReport report) {
|
||||||
|
return new AmlCtrReportData(report.getId(), report.getReportMonth(), report.getGeneratedOn(), report.getRecordCount(),
|
||||||
|
report.getFileReference(), report.getStatus().name(), report.getSubmittedOn());
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.AmlTransactionContext;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionType;
|
||||||
|
import org.apache.fineract.portfolio.aml.repository.AmlFundTransferQueryRepository;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||||
|
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for AML monitoring of NEFT/RTGS/IMPS/INTERNAL fund transfers. This module's
|
||||||
|
* fund-transfer code (unlike savings/loans) doesn't use Fineract's BusinessEvent framework - it
|
||||||
|
* has its own simpler {@code FundTransferEvent} append-only status log - so rather than bolting on
|
||||||
|
* an unrelated event bus, this is called directly from the write paths (synchronous strategy
|
||||||
|
* execution and the NEFT batch processor) at the point a transfer reaches SUCCESS.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Transactional
|
||||||
|
public class AmlFundTransferEvaluationService {
|
||||||
|
|
||||||
|
private static final int STRUCTURING_WINDOW_DAYS = 30;
|
||||||
|
|
||||||
|
private final AmlRuleEvaluationService ruleEvaluationService;
|
||||||
|
private final AmlFundTransferQueryRepository fundTransferQueryRepository;
|
||||||
|
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
||||||
|
|
||||||
|
public void evaluateIfSuccessful(FundTransfer transfer) {
|
||||||
|
if (transfer.getStatus() != FundTransferStatus.SUCCESS || transfer.getClientId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(transfer.getClientId());
|
||||||
|
final LocalDateTime since = LocalDateTime.now().minusDays(STRUCTURING_WINDOW_DAYS);
|
||||||
|
final BigDecimal windowAggregate = fundTransferQueryRepository.sumForClientSince(client.getId(), FundTransferStatus.SUCCESS,
|
||||||
|
since);
|
||||||
|
|
||||||
|
final AmlTransactionContext context = new AmlTransactionContext(AmlTransactionType.TRANSFER, transfer.getId(), client,
|
||||||
|
transfer.getAmount(), "INR",
|
||||||
|
transfer.getSettledAt() != null ? transfer.getSettledAt() : transfer.getInitiatedAt(), false, windowAggregate);
|
||||||
|
ruleEvaluationService.evaluate(context);
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
// AML alerting must never block the underlying fund transfer it is observing
|
||||||
|
log.error("AML rule evaluation failed for fund transfer {}", transfer.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.AmlTransactionContext;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlAlert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates a single transaction against the active {@code AmlTransactionRule} catalog and
|
||||||
|
* persists one {@link AmlAlert} per rule hit. Runs identically regardless of the client's current
|
||||||
|
* KYC risk category - risk category is never used to suppress evaluation.
|
||||||
|
*/
|
||||||
|
public interface AmlRuleEvaluationService {
|
||||||
|
|
||||||
|
List<AmlAlert> evaluate(AmlTransactionContext context);
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 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.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.AmlTransactionContext;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlAlert;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlAlertRepository;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlAppliesTo;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRuleRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional
|
||||||
|
public class AmlRuleEvaluationServiceImpl implements AmlRuleEvaluationService {
|
||||||
|
|
||||||
|
private final AmlTransactionRuleRepository ruleRepository;
|
||||||
|
private final AmlAlertRepository alertRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AmlAlert> evaluate(AmlTransactionContext context) {
|
||||||
|
final List<AmlTransactionRule> candidates = new ArrayList<>();
|
||||||
|
candidates.addAll(ruleRepository.findByActiveTrueAndAppliesTo(context.getTransactionType().name()));
|
||||||
|
candidates.addAll(ruleRepository.findByActiveTrueAndAppliesTo(AmlAppliesTo.ALL.name()));
|
||||||
|
|
||||||
|
final List<AmlAlert> raised = new ArrayList<>();
|
||||||
|
for (final AmlTransactionRule rule : candidates) {
|
||||||
|
if (isHit(rule, context)) {
|
||||||
|
final AmlAlert alert = AmlAlert.raise(context.getTransactionType(), context.getTransactionId(), context.getClient(), rule,
|
||||||
|
context.getAmount(), context.getCurrencyCode(), context.getOccurredOn());
|
||||||
|
raised.add(alertRepository.save(alert));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raised;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHit(AmlTransactionRule rule, AmlTransactionContext context) {
|
||||||
|
if (rule.getThresholdAmount() == null) {
|
||||||
|
// Pattern-only VELOCITY rules (e.g. same-day movement) need bespoke detection logic
|
||||||
|
// per rule and are not yet evaluated here - they exist in the catalog for future work.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (rule.getRuleType()) {
|
||||||
|
case THRESHOLD:
|
||||||
|
return context.getAmount().compareTo(rule.getThresholdAmount()) >= 0;
|
||||||
|
case STRUCTURING:
|
||||||
|
case VELOCITY:
|
||||||
|
return context.getWindowAggregateAmount() != null
|
||||||
|
&& context.getWindowAggregateAmount().compareTo(rule.getThresholdAmount()) >= 0;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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.AmlStrCaseData;
|
||||||
|
|
||||||
|
public interface AmlStrCaseReadPlatformService {
|
||||||
|
|
||||||
|
List<AmlStrCaseData> retrieveAll();
|
||||||
|
|
||||||
|
AmlStrCaseData retrieveOne(Long strCaseId);
|
||||||
|
|
||||||
|
List<AmlStrCaseData> weeklyDigest();
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* 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 java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.AmlStrCaseData;
|
||||||
|
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(readOnly = true)
|
||||||
|
public class AmlStrCaseReadPlatformServiceImpl implements AmlStrCaseReadPlatformService {
|
||||||
|
|
||||||
|
private final AmlStrCaseRepository repository;
|
||||||
|
private final AmlStrWeeklyDigestService digestService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AmlStrCaseData> retrieveAll() {
|
||||||
|
return repository.findAll().stream().map(this::mapToData).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AmlStrCaseData retrieveOne(Long strCaseId) {
|
||||||
|
return repository.findById(strCaseId).map(this::mapToData)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("AML STR case " + strCaseId + " does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AmlStrCaseData> weeklyDigest() {
|
||||||
|
return digestService.currentWeekDigest().stream().map(this::mapToData).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AmlStrCaseData mapToData(AmlStrCase strCase) {
|
||||||
|
final long cumulative = repository.countByClient_Id(strCase.getClient().getId());
|
||||||
|
return new AmlStrCaseData(strCase.getId(), strCase.getAlert().getId(), strCase.getClient().getId(),
|
||||||
|
strCase.getClient().getDisplayName(), strCase.getAssignedPo() == null ? null : strCase.getAssignedPo().getUsername(),
|
||||||
|
strCase.getInvestigationNotes(), strCase.getSourceOfFundsNotes(),
|
||||||
|
strCase.getConclusion() == null ? null : strCase.getConclusion().name(),
|
||||||
|
strCase.getConcludedBy() == null ? null : strCase.getConcludedBy().getUsername(), strCase.getConcludedOn(),
|
||||||
|
strCase.getFiuDueDate(), strCase.getStrFiledOn(), strCase.getStrReferenceNo(), strCase.getStatus().name(), cumulative);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCase;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrConclusion;
|
||||||
|
|
||||||
|
/** Principal-Officer-only actions on an escalated {@link AmlStrCase}. */
|
||||||
|
public interface AmlStrCaseWritePlatformService {
|
||||||
|
|
||||||
|
AmlStrCase assignPrincipalOfficer(Long strCaseId, Long principalOfficerUserId);
|
||||||
|
|
||||||
|
AmlStrCase conclude(Long strCaseId, AmlStrConclusion conclusion, String investigationNotes, String sourceOfFundsNotes);
|
||||||
|
|
||||||
|
AmlStrCase fileStr(Long strCaseId, String referenceNo);
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 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 java.time.LocalDate;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCase;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrConclusion;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional
|
||||||
|
public class AmlStrCaseWritePlatformServiceImpl implements AmlStrCaseWritePlatformService {
|
||||||
|
|
||||||
|
private final AmlStrCaseRepository strCaseRepository;
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final StrSlaCalculator strSlaCalculator;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AmlStrCase assignPrincipalOfficer(Long strCaseId, Long principalOfficerUserId) {
|
||||||
|
final AmlStrCase strCase = loadCase(strCaseId);
|
||||||
|
final AppUser po = appUserRepository.findById(principalOfficerUserId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("User " + principalOfficerUserId + " does not exist"));
|
||||||
|
strCase.assignPrincipalOfficer(po);
|
||||||
|
return strCaseRepository.save(strCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AmlStrCase conclude(Long strCaseId, AmlStrConclusion conclusion, String investigationNotes, String sourceOfFundsNotes) {
|
||||||
|
final AmlStrCase strCase = loadCase(strCaseId);
|
||||||
|
final AppUser principalOfficer = context.authenticatedUser();
|
||||||
|
final LocalDate concludedOn = LocalDate.now();
|
||||||
|
final LocalDate fiuDueDate = conclusion == AmlStrConclusion.SUSPICIOUS
|
||||||
|
? strSlaCalculator.calculateFiuDueDate(concludedOn, strCase.getClient().officeId())
|
||||||
|
: null;
|
||||||
|
strCase.conclude(principalOfficer, conclusion, investigationNotes, sourceOfFundsNotes, concludedOn, fiuDueDate);
|
||||||
|
return strCaseRepository.save(strCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AmlStrCase fileStr(Long strCaseId, String referenceNo) {
|
||||||
|
final AmlStrCase strCase = loadCase(strCaseId);
|
||||||
|
strCase.fileStr(referenceNo, LocalDate.now());
|
||||||
|
return strCaseRepository.save(strCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AmlStrCase loadCase(Long strCaseId) {
|
||||||
|
return strCaseRepository.findById(strCaseId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("AML STR case " + strCaseId + " does not exist"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* 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.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal MIS view of STR cases the Principal Officer concluded suspicious in the last 7 calendar
|
||||||
|
* days. This is separate from, and does not replace, each case's own individual 7-working-day FIU
|
||||||
|
* filing deadline ({@code AmlStrCase.fiuDueDate}) - it exists purely for compliance-team tracking.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class AmlStrWeeklyDigestService {
|
||||||
|
|
||||||
|
private static final int DIGEST_WINDOW_DAYS = 7;
|
||||||
|
|
||||||
|
private final AmlStrCaseRepository strCaseRepository;
|
||||||
|
|
||||||
|
public List<AmlStrCase> currentWeekDigest() {
|
||||||
|
final LocalDate today = LocalDate.now();
|
||||||
|
return strCaseRepository.findConcludedSuspiciousBetween(today.minusDays(DIGEST_WINDOW_DAYS), today);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AmlStrCase> digestFor(LocalDate from, LocalDate to) {
|
||||||
|
return strCaseRepository.findConcludedSuspiciousBetween(from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.ClientRiskCategoryHistoryData;
|
||||||
|
|
||||||
|
public interface ClientRiskCategoryHistoryReadPlatformService {
|
||||||
|
|
||||||
|
List<ClientRiskCategoryHistoryData> retrieveForClient(Long clientId);
|
||||||
|
}
|
||||||
+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.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.aml.data.ClientRiskCategoryHistoryData;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.ClientRiskCategoryHistory;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.ClientRiskCategoryHistoryRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ClientRiskCategoryHistoryReadPlatformServiceImpl implements ClientRiskCategoryHistoryReadPlatformService {
|
||||||
|
|
||||||
|
private final ClientRiskCategoryHistoryRepository repository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ClientRiskCategoryHistoryData> retrieveForClient(Long clientId) {
|
||||||
|
return repository.findByClient_IdOrderByChangedOnDesc(clientId).stream().map(this::mapToData).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRiskCategoryHistoryData mapToData(ClientRiskCategoryHistory history) {
|
||||||
|
return new ClientRiskCategoryHistoryData(history.getId(), history.getClient().getId(), history.getPreviousCategory(),
|
||||||
|
history.getNewCategory(), history.getReason(), history.getTriggeredBy().name(),
|
||||||
|
history.getStrCase() == null ? null : history.getStrCase().getId(), history.getChangedOn(),
|
||||||
|
history.getChangedBy().getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A client's existing static KYC risk category never suppresses AML monitoring - a Low/Medium
|
||||||
|
* risk client who breaches a rule still gets an alert and still goes through the STR pipeline.
|
||||||
|
* This service closes the feedback loop the other way: on a client's 3rd cumulative
|
||||||
|
* {@link AmlStrCase}, their risk category is automatically escalated one level, so repeated
|
||||||
|
* breaches are reflected back into their KYC profile rather than only living in the AML module.
|
||||||
|
*/
|
||||||
|
public interface ClientRiskEscalationService {
|
||||||
|
|
||||||
|
void onStrCaseEscalated(AmlStrCase strCase);
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 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.time.LocalDateTime;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCase;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.AmlStrCaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.ClientRiskCategoryHistory;
|
||||||
|
import org.apache.fineract.portfolio.aml.domain.ClientRiskCategoryHistoryRepository;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKyc;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKycRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.KycRiskCategory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Transactional
|
||||||
|
public class ClientRiskEscalationServiceImpl implements ClientRiskEscalationService {
|
||||||
|
|
||||||
|
private static final int ESCALATION_THRESHOLD = 3;
|
||||||
|
|
||||||
|
private final AmlStrCaseRepository strCaseRepository;
|
||||||
|
private final ClientKycRepositoryWrapper clientKycRepositoryWrapper;
|
||||||
|
private final ClientRiskCategoryHistoryRepository historyRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStrCaseEscalated(AmlStrCase strCase) {
|
||||||
|
final Client client = strCase.getClient();
|
||||||
|
final long cumulativeCases = strCaseRepository.countByClient_Id(client.getId());
|
||||||
|
if (cumulativeCases != ESCALATION_THRESHOLD) {
|
||||||
|
// only fires on the exact 3rd case, not every case from the 3rd onward
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ClientKyc kyc = clientKycRepositoryWrapper.findByClientIdWithNotFoundDetection(client.getId());
|
||||||
|
final Integer previousCategory = kyc.getRiskCategoryValue();
|
||||||
|
final Integer nextCategory = nextCategory(previousCategory);
|
||||||
|
if (nextCategory.equals(previousCategory)) {
|
||||||
|
log.info("Client {} reached {} cumulative AML STR cases but is already at the highest risk category", client.getId(),
|
||||||
|
cumulativeCases);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
kyc.escalateRiskCategory(nextCategory);
|
||||||
|
historyRepository.save(ClientRiskCategoryHistory.autoEscalation(client, previousCategory, nextCategory, strCase,
|
||||||
|
context.authenticatedUser(), LocalDateTime.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer nextCategory(Integer currentCategory) {
|
||||||
|
if (currentCategory == null || currentCategory.equals(KycRiskCategory.LOW.getValue())) {
|
||||||
|
return KycRiskCategory.MEDIUM.getValue();
|
||||||
|
}
|
||||||
|
if (currentCategory.equals(KycRiskCategory.MEDIUM.getValue())) {
|
||||||
|
return KycRiskCategory.HIGH.getValue();
|
||||||
|
}
|
||||||
|
return KycRiskCategory.HIGH.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
|
||||||
|
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
|
||||||
|
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
|
||||||
|
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the RBI/PMLA-mandated 7-working-day FIU-IND filing deadline from a Principal Officer's
|
||||||
|
* STR conclusion date. "Working day" excludes both the tenant's configured weekly off-days
|
||||||
|
* ({@link WorkingDays}) and declared office holidays - reuses the same calendar machinery loan
|
||||||
|
* repayment scheduling already depends on, rather than a bespoke calculator.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StrSlaCalculator {
|
||||||
|
|
||||||
|
private static final int STR_FILING_WORKING_DAYS = 7;
|
||||||
|
|
||||||
|
private final WorkingDaysRepositoryWrapper workingDaysRepositoryWrapper;
|
||||||
|
private final HolidayRepositoryWrapper holidayRepositoryWrapper;
|
||||||
|
|
||||||
|
public LocalDate calculateFiuDueDate(LocalDate concludedOn, Long officeId) {
|
||||||
|
final WorkingDays workingDays = workingDaysRepositoryWrapper.findOne();
|
||||||
|
LocalDate date = concludedOn;
|
||||||
|
int workingDaysElapsed = 0;
|
||||||
|
while (workingDaysElapsed < STR_FILING_WORKING_DAYS) {
|
||||||
|
date = date.plusDays(1);
|
||||||
|
if (isWorkingDay(workingDays, date, officeId)) {
|
||||||
|
workingDaysElapsed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWorkingDay(WorkingDays workingDays, LocalDate date, Long officeId) {
|
||||||
|
return WorkingDaysUtil.isWorkingDay(workingDays, date) && !holidayRepositoryWrapper.isHoliday(officeId, date);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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.starter;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
|
import org.apache.fineract.portfolio.aml.event.AmlDomainService;
|
||||||
|
import org.apache.fineract.portfolio.aml.event.AmlDomainServiceImpl;
|
||||||
|
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.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AmlConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(AmlDomainService.class)
|
||||||
|
public AmlDomainService amlDomainService(BusinessEventNotifierService businessEventNotifierService,
|
||||||
|
AmlRuleEvaluationService amlRuleEvaluationService, AmlSavingsTransactionQueryRepository savingsQueryRepository,
|
||||||
|
AmlLoanTransactionQueryRepository loanQueryRepository) {
|
||||||
|
return new AmlDomainServiceImpl(businessEventNotifierService, amlRuleEvaluationService, savingsQueryRepository,
|
||||||
|
loanQueryRepository);
|
||||||
|
}
|
||||||
|
}
|
||||||
+164
@@ -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.client.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DefaultValue;
|
||||||
|
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.Context;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
|
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientPendingEditData;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientEditWritePlatformService;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientPendingEditReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/clients")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Client Edit Approval",
|
||||||
|
description = "Maker-checker workflow for editing an existing Client's core profile details. Edits are held in "
|
||||||
|
+ "m_client_pending_edit (a pending row there is itself the lock) and only applied to the real client once a "
|
||||||
|
+ "different user (the checker) approves — see /clients/{clientId}/edit/approve and /reject below.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClientEditApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final ClientEditWritePlatformService clientEditWritePlatformService;
|
||||||
|
private final ClientPendingEditReadPlatformService clientPendingEditReadPlatformService;
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||||
|
private final DefaultToApiJsonSerializer<CommandProcessingResult> toApiJsonSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<ClientPendingEditData> pendingEditApiJsonSerializer;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{clientId}/edit")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Submit a details edit for a Client",
|
||||||
|
description = "Stages the proposed changes in m_client_pending_edit instead of applying them immediately — see "
|
||||||
|
+ "/clients/{clientId}/edit/approve and /reject below.")
|
||||||
|
public String submitEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
|
||||||
|
this.context.authenticatedUser().validateHasPermissionTo("SUBMITEDIT_CLIENT");
|
||||||
|
|
||||||
|
final JsonCommand command = JsonCommand.fromJsonElement(clientId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||||
|
this.fromApiJsonHelper);
|
||||||
|
|
||||||
|
final CommandProcessingResult result = this.clientEditWritePlatformService.submitEditForApproval(clientId, command);
|
||||||
|
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("edit/pending")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List Client details edits pending approval",
|
||||||
|
description = "Supports status/clientName/fromDate/toDate filtering (fromDate/toDate as yyyy-MM-dd, inclusive, filtered "
|
||||||
|
+ "on submitted date; clientName is a case-insensitive substring match) plus offset/limit pagination — mirrors "
|
||||||
|
+ "/chequebook/pending-requests.")
|
||||||
|
public Page<ClientPendingEditData> retrieveAllPendingEdits(@QueryParam("status") final String status,
|
||||||
|
@QueryParam("clientName") final String clientName, @QueryParam("fromDate") final String fromDate,
|
||||||
|
@QueryParam("toDate") final String toDate, @QueryParam("offset") @DefaultValue("0") final Integer offset,
|
||||||
|
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
|
||||||
|
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||||
|
|
||||||
|
final int safeLimit = Math.min(limit, 200);
|
||||||
|
return this.clientPendingEditReadPlatformService.retrieveAllPendingEdits(status, clientName, fromDate, toDate, offset, safeLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{clientId}/edit/pending")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve the pending details edit for a Client, if any")
|
||||||
|
public String retrievePendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||||
|
|
||||||
|
final ClientPendingEditData pendingEdit = this.clientPendingEditReadPlatformService.retrievePendingEdit(clientId);
|
||||||
|
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.pendingEditApiJsonSerializer.serialize(settings, pendingEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{clientId}/edit/approve")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Approve a pending Client details edit",
|
||||||
|
description = "Applies the proposed changes to the client. The maker of the edit cannot also approve it — no exceptions.")
|
||||||
|
public String approvePendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) {
|
||||||
|
|
||||||
|
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVEEDIT_CLIENT")) {
|
||||||
|
throw new NoAuthorizationException("User has no authority to approve client edits");
|
||||||
|
}
|
||||||
|
|
||||||
|
final CommandProcessingResult result = this.clientEditWritePlatformService.approveEdit(clientId);
|
||||||
|
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{clientId}/edit/reject")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Reject a pending Client details edit",
|
||||||
|
description = "Discards the proposed changes without applying them. The maker of the edit cannot also reject it — no exceptions.")
|
||||||
|
public String rejectPendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
|
||||||
|
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECTEDIT_CLIENT")) {
|
||||||
|
throw new NoAuthorizationException("User has no authority to reject client edits");
|
||||||
|
}
|
||||||
|
|
||||||
|
String rejectReason = null;
|
||||||
|
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||||
|
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||||
|
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||||
|
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final CommandProcessingResult result = this.clientEditWritePlatformService.rejectEdit(clientId, rejectReason);
|
||||||
|
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -160,10 +160,10 @@ public class ClientKycApiResource {
|
|||||||
template.put("clientTypeName", clientTypeName);
|
template.put("clientTypeName", clientTypeName);
|
||||||
template.put("legalFormId", legalFormId);
|
template.put("legalFormId", legalFormId);
|
||||||
|
|
||||||
// Minor detection mirrors the frontend's own rule (age < 10) so the document list
|
// Minor detection mirrors the frontend's own rule (age < 18) so the document list
|
||||||
// served here stays consistent with what the UI computes from the same date of birth.
|
// served here stays consistent with what the UI computes from the same date of birth.
|
||||||
final LocalDate dateOfBirth = client.dateOfBirth();
|
final LocalDate dateOfBirth = client.dateOfBirth();
|
||||||
final boolean isMinor = dateOfBirth != null && Period.between(dateOfBirth, LocalDate.now()).getYears() < 10;
|
final boolean isMinor = dateOfBirth != null && Period.between(dateOfBirth, LocalDate.now()).getYears() < 18;
|
||||||
template.put("isMinor", isMinor);
|
template.put("isMinor", isMinor);
|
||||||
|
|
||||||
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
|
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
|
||||||
|
|||||||
+13
-5
@@ -44,6 +44,7 @@ import java.time.LocalDate;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.commands.data.AuditData;
|
import org.apache.fineract.commands.data.AuditData;
|
||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
import org.apache.fineract.commands.service.AuditReadPlatformService;
|
import org.apache.fineract.commands.service.AuditReadPlatformService;
|
||||||
@@ -142,7 +143,8 @@ public class ClientsApiResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "List Clients", description = "The list capability of clients can support pagination and sorting.\n\n"
|
@Operation(summary = "List Clients", description = "The list capability of clients can support pagination and sorting.\n\n"
|
||||||
+ "Example Requests:\n" + "\n" + "clients\n" + "\n" + "clients?fields=displayName,officeName,timeline\n" + "\n"
|
+ "Example Requests:\n" + "\n" + "clients\n" + "\n" + "clients?fields=displayName,officeName,timeline\n" + "\n"
|
||||||
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC")
|
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC\n\n"
|
||||||
|
+ "clients?fromDate=2026-01-01&toDate=2026-01-31 (submittedon_date range, yyyy-MM-dd, inclusive)")
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.GetClientsResponse.class)))
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.GetClientsResponse.class)))
|
||||||
public String retrieveAll(@Context final UriInfo uriInfo,
|
public String retrieveAll(@Context final UriInfo uriInfo,
|
||||||
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
|
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
|
||||||
@@ -157,10 +159,15 @@ public class ClientsApiResource {
|
|||||||
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
|
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
|
||||||
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
|
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
|
||||||
@QueryParam("orphansOnly") @Parameter(description = "orphansOnly") final Boolean orphansOnly,
|
@QueryParam("orphansOnly") @Parameter(description = "orphansOnly") final Boolean orphansOnly,
|
||||||
@QueryParam("legalForm") final Integer legalForm) {
|
@QueryParam("legalForm") final Integer legalForm,
|
||||||
|
@QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDate,
|
||||||
|
@QueryParam("toDate") @Parameter(description = "toDate") final String toDate) {
|
||||||
|
|
||||||
|
final LocalDate fromDateParsed = StringUtils.isNotBlank(fromDate) ? LocalDate.parse(fromDate) : null;
|
||||||
|
final LocalDate toDateParsed = StringUtils.isNotBlank(toDate) ? LocalDate.parse(toDate) : null;
|
||||||
|
|
||||||
return retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm, hierarchy, offset, limit,
|
return retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm, hierarchy, offset, limit,
|
||||||
orderBy, sortOrder, orphansOnly, false);
|
orderBy, sortOrder, orphansOnly, false, fromDateParsed, toDateParsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -437,7 +444,7 @@ public class ClientsApiResource {
|
|||||||
public String retrieveAll(final UriInfo uriInfo, final Long officeId, final String externalId, final String displayName,
|
public String retrieveAll(final UriInfo uriInfo, final Long officeId, final String externalId, final String displayName,
|
||||||
final String firstname, final String lastname, final String status, final Integer legalForm, final String hierarchy,
|
final String firstname, final String lastname, final String status, final Integer legalForm, final String hierarchy,
|
||||||
final Integer offset, final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly,
|
final Integer offset, final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly,
|
||||||
final boolean isSelfUser) {
|
final boolean isSelfUser, final LocalDate fromDate, final LocalDate toDate) {
|
||||||
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||||
sqlValidator.validate(orderBy);
|
sqlValidator.validate(orderBy);
|
||||||
sqlValidator.validate(sortOrder);
|
sqlValidator.validate(sortOrder);
|
||||||
@@ -445,7 +452,8 @@ public class ClientsApiResource {
|
|||||||
sqlValidator.validate(hierarchy);
|
sqlValidator.validate(hierarchy);
|
||||||
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId)
|
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId)
|
||||||
.name(displayName).hierarchy(hierarchy).firstname(firstname).lastname(lastname).status(status).orphansOnly(orphansOnly)
|
.name(displayName).hierarchy(hierarchy).firstname(firstname).lastname(lastname).status(status).orphansOnly(orphansOnly)
|
||||||
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).build();
|
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).fromDate(fromDate)
|
||||||
|
.toDate(toDate).build();
|
||||||
final Page<ClientData> clientData = clientReadPlatformService.retrieveAll(searchParameters);
|
final Page<ClientData> clientData = clientReadPlatformService.retrieveAll(searchParameters);
|
||||||
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
return toApiJsonSerializer.serialize(settings, clientData, ClientApiConstants.CLIENT_RESPONSE_DATA_PARAMETERS);
|
return toApiJsonSerializer.serialize(settings, clientData, ClientApiConstants.CLIENT_RESPONSE_DATA_PARAMETERS);
|
||||||
|
|||||||
+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.client.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.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.client.data.KycRiskRuleData;
|
||||||
|
import org.apache.fineract.portfolio.client.service.KycRiskRulesReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/kyc/risk-rules")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "KYC Risk Rules", description = "Retrieve database-driven master rules for client AML/CFT risk rating calculation.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class KycRiskRulesApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final KycRiskRulesReadPlatformService readPlatformService;
|
||||||
|
private final DefaultToApiJsonSerializer<KycRiskRuleData> toApiJsonSerializer;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve KYC risk rules", description = "Returns list of database-defined risk rules ordered by precedence.")
|
||||||
|
public String retrieveRiskRules() {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
final List<KycRiskRuleData> rules = this.readPlatformService.retrieveAllRules();
|
||||||
|
return this.toApiJsonSerializer.serialize(rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
-35
@@ -32,7 +32,6 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -62,16 +61,22 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
|||||||
private static final DateTimeFormatter ZIP_NAME_TS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss");
|
private static final DateTimeFormatter ZIP_NAME_TS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Names of code values (from {@code KYCAddressProofType}) that unambiguously indicate an
|
* Code (m_code.code_name) under which an identifier's document type must be classified for it
|
||||||
* address proof. Aadhaar / Passport / DL / Voter ID can be both ID and address proof — we
|
* to count as an address proof. Matches the same m_code_value config that
|
||||||
* default them to ID (PI) unless the UI tags them explicitly.
|
* KycProofTypesApiResource (GET /kyc/proof-types) reads to populate the CKYC tab's
|
||||||
|
* "Address Proof Document" card — e.g. Aadhaar Card is seeded as a document type under BOTH
|
||||||
|
* KYCIdProofType and KYCAddressProofType (two separate m_client_identifier rows), so trusting
|
||||||
|
* the code a given identifier was actually tagged under is the only way this can agree with
|
||||||
|
* what the CKYC tab already shows as "available".
|
||||||
*/
|
*/
|
||||||
private static final Set<String> ADDRESS_PROOF_ONLY = Set.of(
|
private static final String ADDRESS_PROOF_CODE_NAME = "KYCAddressProofType";
|
||||||
"UTILITY BILLS", "UTILITY BILL", "ELECTRICITY BILL", "WATER BILL", "GAS BILL", "INTERNET BILL",
|
|
||||||
"BANK ACCOUNT STATEMENT / PASSBOOK", "BANK STATEMENT", "POSTPAID MOBILE BILL",
|
// Top-level ZIP folders — one per artefact type, so every client's file of a given type lands
|
||||||
"REGISTERED RENT AGREEMENT", "RENT AGREEMENT", "PROPERTY TAX RECEIPT",
|
// in the same folder instead of all 4 types being interleaved flat at the ZIP root.
|
||||||
"EMPLOYER LETTER (FOR SALARIED EMPLOYEES)", "EMPLOYER LETTER",
|
private static final String PHOTO_FOLDER = "PHOTO/";
|
||||||
"PENSION PAYMENT ORDERS (FOR RETIREES)", "PENSION PAYMENT ORDER");
|
private static final String SIGNATURE_FOLDER = "SIGNATURE/";
|
||||||
|
private static final String ID_PROOF_FOLDER = "ID_PROOF/";
|
||||||
|
private static final String ADDRESS_PROOF_FOLDER = "ADDRESS_PROOF/";
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final DocumentReadPlatformService documentReadPlatformService;
|
private final DocumentReadPlatformService documentReadPlatformService;
|
||||||
@@ -112,13 +117,30 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
|||||||
final byte[] zipBytes = zipBytesOut.toByteArray();
|
final byte[] zipBytes = zipBytesOut.toByteArray();
|
||||||
final String checksum = "sha256:" + sha256Hex(zipBytes);
|
final String checksum = "sha256:" + sha256Hex(zipBytes);
|
||||||
|
|
||||||
// Persist the ZIP as an m_document row (parent_entity_type='ckyc_batch', parent_entity_id=seq)
|
// Persist the batch registry row FIRST (without documentId) so we have its real,
|
||||||
// Persistence happens BEFORE we know m_ckyc_batch.id, so we use the synthetic seq as a
|
// DB-generated id before creating the ZIP's m_document row. The document's
|
||||||
// temporary parent_entity_id. The real FK is via m_ckyc_batch.document_id after save.
|
// parent_entity_id must be the actual m_ckyc_batch.id — using the daily `seq` here
|
||||||
|
// (as a stand-in for an id that doesn't exist yet) was wrong: seq resets every day
|
||||||
|
// while m_ckyc_batch.id is a global auto-increment, so GET
|
||||||
|
// /ckyc_batch/{id}/documents/{documentId}/attachment (which matches on the REAL batch
|
||||||
|
// id) 404'd for any batch whose seq didn't coincidentally equal its DB id.
|
||||||
|
final CkycBatch batch = new CkycBatch();
|
||||||
|
batch.setBatchId(batchId);
|
||||||
|
batch.setFilename(filename);
|
||||||
|
batch.setChecksumSha256(checksum);
|
||||||
|
batch.setRecordCount(includedCount);
|
||||||
|
batch.setSizeBytes((long) zipBytes.length);
|
||||||
|
batch.setCreatedOnDate(today);
|
||||||
|
batch.setUploadedToCersai(false);
|
||||||
|
batch.setManifestJson(new Gson().toJson(manifest));
|
||||||
|
|
||||||
|
final CkycBatch saved = ckycBatchRepository.saveAndFlush(batch);
|
||||||
|
|
||||||
|
// Now persist the ZIP as an m_document row keyed to the batch's real id.
|
||||||
final var docResponse = documentWritePlatformService.createDocument(
|
final var docResponse = documentWritePlatformService.createDocument(
|
||||||
DocumentCreateRequest.builder()
|
DocumentCreateRequest.builder()
|
||||||
.entityType("ckyc_batch")
|
.entityType("ckyc_batch")
|
||||||
.entityId((long) seq)
|
.entityId(saved.getId())
|
||||||
.name(batchId)
|
.name(batchId)
|
||||||
.fileName(filename)
|
.fileName(filename)
|
||||||
.description(StringUtils.defaultIfEmpty(remarks, "CERSAI CKYC batch ZIP"))
|
.description(StringUtils.defaultIfEmpty(remarks, "CERSAI CKYC batch ZIP"))
|
||||||
@@ -127,19 +149,8 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
|||||||
.stream(new ByteArrayInputStream(zipBytes))
|
.stream(new ByteArrayInputStream(zipBytes))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// Persist the batch registry row
|
saved.setDocumentId(docResponse.getResourceId() != null ? docResponse.getResourceId().longValue() : null);
|
||||||
final CkycBatch batch = new CkycBatch();
|
ckycBatchRepository.saveAndFlush(saved);
|
||||||
batch.setBatchId(batchId);
|
|
||||||
batch.setFilename(filename);
|
|
||||||
batch.setChecksumSha256(checksum);
|
|
||||||
batch.setRecordCount(includedCount);
|
|
||||||
batch.setSizeBytes((long) zipBytes.length);
|
|
||||||
batch.setDocumentId(docResponse.getResourceId() != null ? docResponse.getResourceId().longValue() : null);
|
|
||||||
batch.setCreatedOnDate(today);
|
|
||||||
batch.setUploadedToCersai(false);
|
|
||||||
batch.setManifestJson(new Gson().toJson(manifest));
|
|
||||||
|
|
||||||
final CkycBatch saved = ckycBatchRepository.saveAndFlush(batch);
|
|
||||||
|
|
||||||
log.info("CKYC batch created: id={}, batchId={}, filename={}, recordCount={}/{}, bytes={}",
|
log.info("CKYC batch created: id={}, batchId={}, filename={}, recordCount={}/{}, bytes={}",
|
||||||
saved.getId(), batchId, filename, includedCount, clientIds.size(), zipBytes.length);
|
saved.getId(), batchId, filename, includedCount, clientIds.size(), zipBytes.length);
|
||||||
@@ -185,32 +196,40 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
|||||||
entry.setCifNumber(cifNumber);
|
entry.setCifNumber(cifNumber);
|
||||||
entry.setDisplayName(displayName);
|
entry.setDisplayName(displayName);
|
||||||
|
|
||||||
// Photo — packaged as "<CIF>-PI-PHOTO.<ext>" using m_client.image_id → m_image
|
// Photo — packaged as "PHOTO/<CIF>-PI-PHOTO.<ext>" using m_client.image_id → m_image.
|
||||||
|
// Every artefact type gets its own top-level ZIP folder, so a batch of N clients produces
|
||||||
|
// one PHOTO/ folder containing all N client photos (and likewise for the other 3 types),
|
||||||
|
// rather than every client's files interleaved flat at the ZIP root.
|
||||||
final Map<String, Object> photo = new LinkedHashMap<>();
|
final Map<String, Object> photo = new LinkedHashMap<>();
|
||||||
photo.put("present", imageId != null);
|
photo.put("present", imageId != null);
|
||||||
entry.setPhoto(photo);
|
entry.setPhoto(photo);
|
||||||
if (imageId != null && cifNumber != null) {
|
if (imageId != null && cifNumber != null) {
|
||||||
packageClientImage(zip, clientId, cifNumber + "-PI-PHOTO", photo);
|
packageClientImage(zip, clientId, PHOTO_FOLDER + cifNumber + "-PI-PHOTO", photo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature — packaged as "<CIF>-PI-SIGNATURE.<ext>"
|
// Signature — packaged as "SIGNATURE/<CIF>-PI-SIGNATURE.<ext>"
|
||||||
final Long signatureDocId = findSignatureDocId(clientId);
|
final Long signatureDocId = findSignatureDocId(clientId);
|
||||||
final Map<String, Object> signature = new LinkedHashMap<>();
|
final Map<String, Object> signature = new LinkedHashMap<>();
|
||||||
signature.put("present", signatureDocId != null);
|
signature.put("present", signatureDocId != null);
|
||||||
entry.setSignature(signature);
|
entry.setSignature(signature);
|
||||||
if (signatureDocId != null && cifNumber != null) {
|
if (signatureDocId != null && cifNumber != null) {
|
||||||
packageFile(zip, "clients", clientId, signatureDocId,
|
packageFile(zip, "clients", clientId, signatureDocId,
|
||||||
cifNumber + "-PI-SIGNATURE", signature);
|
SIGNATURE_FOLDER + cifNumber + "-PI-SIGNATURE", signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID proof + Address proof: iterate m_client_identifier, classify by code value name.
|
// ID proof + Address proof: iterate m_client_identifier, classify by the m_code the
|
||||||
|
// identifier's document type actually belongs to (KYCIdProofType vs KYCAddressProofType) —
|
||||||
|
// the same source of truth KycProofTypesApiResource uses for the CKYC tab, so a document
|
||||||
|
// the client sees as "Address Proof: Available" is never silently dropped here.
|
||||||
// For each identifier, find the attached m_document (one-to-one, latest wins).
|
// For each identifier, find the attached m_document (one-to-one, latest wins).
|
||||||
// NOTE: the `active` column in m_client_identifier stores the status code (200 = active)
|
// NOTE: the `active` column in m_client_identifier stores the status code (200 = active)
|
||||||
// not a 1/0 boolean — so we filter on `status = 200` (ClientIdentifierStatus.ACTIVE).
|
// not a 1/0 boolean — so we filter on `status = 200` (ClientIdentifierStatus.ACTIVE).
|
||||||
final List<Map<String, Object>> identifiers = jdbcTemplate.queryForList(
|
final List<Map<String, Object>> identifiers = jdbcTemplate.queryForList(
|
||||||
"select ci.id as identifierId, ci.document_key as docKey, cv.code_value as codeValue "
|
"select ci.id as identifierId, ci.document_key as docKey, cv.code_value as codeValue, "
|
||||||
|
+ " c.code_name as codeName "
|
||||||
+ " from m_client_identifier ci "
|
+ " from m_client_identifier ci "
|
||||||
+ " join m_code_value cv on cv.id = ci.document_type_id "
|
+ " join m_code_value cv on cv.id = ci.document_type_id "
|
||||||
|
+ " join m_code c on c.id = cv.code_id "
|
||||||
+ " where ci.client_id = ? and (ci.status = 200 or ci.status is null)",
|
+ " where ci.client_id = ? and (ci.status = 200 or ci.status is null)",
|
||||||
clientId);
|
clientId);
|
||||||
|
|
||||||
@@ -222,8 +241,9 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
|||||||
for (final Map<String, Object> idRow : identifiers) {
|
for (final Map<String, Object> idRow : identifiers) {
|
||||||
final Long identifierId = ((Number) idRow.get("identifierId")).longValue();
|
final Long identifierId = ((Number) idRow.get("identifierId")).longValue();
|
||||||
final String codeValue = (String) idRow.get("codeValue");
|
final String codeValue = (String) idRow.get("codeValue");
|
||||||
|
final String codeName = (String) idRow.get("codeName");
|
||||||
final String docKey = (String) idRow.get("docKey");
|
final String docKey = (String) idRow.get("docKey");
|
||||||
final boolean isAddress = ADDRESS_PROOF_ONLY.contains(codeValue == null ? "" : codeValue.toUpperCase(Locale.ROOT));
|
final boolean isAddress = ADDRESS_PROOF_CODE_NAME.equalsIgnoreCase(codeName);
|
||||||
final String role = isAddress ? "PA" : "PI";
|
final String role = isAddress ? "PA" : "PI";
|
||||||
|
|
||||||
final Long attachedDocId = findLatestDocForIdentifier(identifierId);
|
final Long attachedDocId = findLatestDocForIdentifier(identifierId);
|
||||||
@@ -243,8 +263,9 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
|||||||
bucket.put("key", docKey);
|
bucket.put("key", docKey);
|
||||||
|
|
||||||
if (cifNumber != null) {
|
if (cifNumber != null) {
|
||||||
|
final String folder = isAddress ? ADDRESS_PROOF_FOLDER : ID_PROOF_FOLDER;
|
||||||
packageFile(zip, "client_identifiers", identifierId, attachedDocId,
|
packageFile(zip, "client_identifiers", identifierId, attachedDocId,
|
||||||
cifNumber + "-" + role + "-" + shortCode, bucket);
|
folder + cifNumber + "-" + role + "-" + shortCode, bucket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -71,6 +71,15 @@ public class ClientKycCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void validateForInitiate() {
|
public void validateForInitiate() {
|
||||||
|
validateForInitiate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param skipIdentityDocumentCheck true for minors, who identify via the Birth Certificate /
|
||||||
|
* School ID document-type picker (a separate upload path)
|
||||||
|
* rather than PAN/Aadhaar/GSTIN.
|
||||||
|
*/
|
||||||
|
public void validateForInitiate(final boolean skipIdentityDocumentCheck) {
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||||
|
|
||||||
@@ -101,7 +110,9 @@ public class ClientKycCommand {
|
|||||||
"GSTIN must be a valid 15-character GST Identification Number");
|
"GSTIN must be a valid 15-character GST Identification Number");
|
||||||
}
|
}
|
||||||
|
|
||||||
// At least one ID document must be provided
|
// At least one ID document must be provided — except for minors, who prove identity via
|
||||||
|
// the Birth Certificate / School ID document-type picker instead.
|
||||||
|
if (!skipIdentityDocumentCheck) {
|
||||||
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
||||||
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
||||||
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
||||||
@@ -109,6 +120,7 @@ public class ClientKycCommand {
|
|||||||
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
||||||
.notBlank();
|
.notBlank();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(errors);
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -32,8 +32,9 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
|
|||||||
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
||||||
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName,
|
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName,
|
||||||
educationLevelIdParamName, degreeQualificationIdParamName,
|
educationLevelIdParamName, degreeQualificationIdParamName,
|
||||||
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
|
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualIncomeParamName, annualTurnoverParamName,
|
||||||
agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||||
|
isSeniorCitizenParamName, isPwdParamName, pwdCategoryIdParamName,
|
||||||
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
||||||
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
|
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
|
||||||
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
||||||
@@ -55,8 +56,9 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
|
|||||||
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
||||||
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName,
|
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName,
|
||||||
educationLevelIdParamName, degreeQualificationIdParamName,
|
educationLevelIdParamName, degreeQualificationIdParamName,
|
||||||
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
|
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualIncomeParamName, annualTurnoverParamName,
|
||||||
agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||||
|
isSeniorCitizenParamName, isPwdParamName, pwdCategoryIdParamName,
|
||||||
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
||||||
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
|
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
|
||||||
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
||||||
|
|||||||
+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.client.data;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ClientPendingEditData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long clientId;
|
||||||
|
private final String clientDisplayName;
|
||||||
|
private final String payloadJson;
|
||||||
|
private final String originalJson;
|
||||||
|
private final String status;
|
||||||
|
private final Long makerId;
|
||||||
|
private final String makerName;
|
||||||
|
private final OffsetDateTime submittedDate;
|
||||||
|
private final Long checkerId;
|
||||||
|
private final String checkerName;
|
||||||
|
private final OffsetDateTime checkedDate;
|
||||||
|
private final String rejectReason;
|
||||||
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 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.math.BigDecimal;
|
||||||
|
|
||||||
|
public final class KycRiskRuleData implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String ruleName;
|
||||||
|
private final Integer legalFormId;
|
||||||
|
private final Long occupationCategoryCvId;
|
||||||
|
private final String occupationCategoryName;
|
||||||
|
private final Long occupationCvId;
|
||||||
|
private final String occupationName;
|
||||||
|
private final BigDecimal minMonthlyIncome;
|
||||||
|
private final BigDecimal maxMonthlyIncome;
|
||||||
|
private final BigDecimal minAnnualIncome;
|
||||||
|
private final BigDecimal maxAnnualIncome;
|
||||||
|
private final BigDecimal minAnnualTurnover;
|
||||||
|
private final BigDecimal maxAnnualTurnover;
|
||||||
|
private final Integer riskCategory;
|
||||||
|
private final String reason;
|
||||||
|
private final Integer orderPosition;
|
||||||
|
private final String amlCheckStatus;
|
||||||
|
private final String bannedEntityMatch;
|
||||||
|
private final String willfulDefaulter;
|
||||||
|
private final String ecgcSalFlag;
|
||||||
|
private final String dormantFlag;
|
||||||
|
|
||||||
|
public KycRiskRuleData(final Long id, final String ruleName, final Integer legalFormId,
|
||||||
|
final Long occupationCategoryCvId, final String occupationCategoryName,
|
||||||
|
final Long occupationCvId, final String occupationName,
|
||||||
|
final BigDecimal minMonthlyIncome, final BigDecimal maxMonthlyIncome,
|
||||||
|
final BigDecimal minAnnualIncome, final BigDecimal maxAnnualIncome,
|
||||||
|
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover,
|
||||||
|
final Integer riskCategory, final String reason, final Integer orderPosition,
|
||||||
|
final String amlCheckStatus, final String bannedEntityMatch,
|
||||||
|
final String willfulDefaulter, final String ecgcSalFlag, final String dormantFlag) {
|
||||||
|
this.id = id;
|
||||||
|
this.ruleName = ruleName;
|
||||||
|
this.legalFormId = legalFormId;
|
||||||
|
this.occupationCategoryCvId = occupationCategoryCvId;
|
||||||
|
this.occupationCategoryName = occupationCategoryName;
|
||||||
|
this.occupationCvId = occupationCvId;
|
||||||
|
this.occupationName = occupationName;
|
||||||
|
this.minMonthlyIncome = minMonthlyIncome;
|
||||||
|
this.maxMonthlyIncome = maxMonthlyIncome;
|
||||||
|
this.minAnnualIncome = minAnnualIncome;
|
||||||
|
this.maxAnnualIncome = maxAnnualIncome;
|
||||||
|
this.minAnnualTurnover = minAnnualTurnover;
|
||||||
|
this.maxAnnualTurnover = maxAnnualTurnover;
|
||||||
|
this.riskCategory = riskCategory;
|
||||||
|
this.reason = reason;
|
||||||
|
this.orderPosition = orderPosition;
|
||||||
|
this.amlCheckStatus = amlCheckStatus;
|
||||||
|
this.bannedEntityMatch = bannedEntityMatch;
|
||||||
|
this.willfulDefaulter = willfulDefaulter;
|
||||||
|
this.ecgcSalFlag = ecgcSalFlag;
|
||||||
|
this.dormantFlag = dormantFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRuleName() {
|
||||||
|
return ruleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getLegalFormId() {
|
||||||
|
return legalFormId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOccupationCategoryCvId() {
|
||||||
|
return occupationCategoryCvId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOccupationCategoryName() {
|
||||||
|
return occupationCategoryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOccupationCvId() {
|
||||||
|
return occupationCvId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOccupationName() {
|
||||||
|
return occupationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package org.apache.fineract.portfolio.client.domain;
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
@@ -52,4 +53,11 @@ public interface ClientFamilyMembersRepository
|
|||||||
@Param("relationshipId") Long relationshipId,
|
@Param("relationshipId") Long relationshipId,
|
||||||
@Param("excludeFamilyMemberId") Long excludeFamilyMemberId);
|
@Param("excludeFamilyMemberId") Long excludeFamilyMemberId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All non-deleted family members for a client, with their relationship CodeValue. Backs the
|
||||||
|
* "Mother required, plus Father or Caretaker" activation gate.
|
||||||
|
*/
|
||||||
|
@Query("SELECT fm FROM ClientFamilyMembers fm WHERE fm.client.id = :clientId AND fm.isDeleted = false")
|
||||||
|
List<ClientFamilyMembers> findAllActiveByClient(@Param("clientId") Long clientId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* 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.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bespoke maker-checker for Client edits — modelled on {@link ClientPendingEditStatus} /
|
||||||
|
* {@code m_share_account_pending_edit} elsewhere in this codebase rather than Fineract's native
|
||||||
|
* command-approval engine, because that engine's hold is bypassed by any role carrying
|
||||||
|
* ALL_FUNCTIONS (Super user, Clerk in this tenant), making it a no-op for this CBS's actual admin
|
||||||
|
* accounts.
|
||||||
|
*
|
||||||
|
* <p>Holds the maker's raw PUT /clients/{id}/edit payload as-is (rather than one typed column per
|
||||||
|
* editable Client field, which — given how many fields Client actually has across general,
|
||||||
|
* financial-classification and KYC-adjacent data — would mean a huge, fragile shadow-column table
|
||||||
|
* that has to be kept in lockstep with the entity forever). Approval reconstructs a
|
||||||
|
* {@code JsonCommand} from the stored JSON and replays it through the existing
|
||||||
|
* {@code ClientWritePlatformService.updateClient(...)}, so all of Client's existing validation and
|
||||||
|
* change-detection logic runs exactly as it would for a direct edit — just deferred until a
|
||||||
|
* different user approves it. The maker can never approve/reject their own submission — enforced
|
||||||
|
* in the write service, not just by role, with no bypass for CHECKER_SUPER_USER.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_client_pending_edit")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class ClientPendingEdit extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@Column(name = "payload_json", nullable = false)
|
||||||
|
private String payloadJson;
|
||||||
|
|
||||||
|
/** Point-in-time "before" snapshot the maker's client had when the edit was submitted — see
|
||||||
|
* client-edit-diff.util.ts's buildOriginalValuesSnapshot(). Null for edits submitted before
|
||||||
|
* this column existed. Lets the checker's compare view show a true historical diff regardless
|
||||||
|
* of whether the live client has since changed (e.g. once approved, current == proposed). */
|
||||||
|
@Column(name = "original_json")
|
||||||
|
private String originalJson;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "maker_id", nullable = false)
|
||||||
|
private Long makerId;
|
||||||
|
|
||||||
|
@Column(name = "submitted_date", nullable = false)
|
||||||
|
private OffsetDateTime submittedDate;
|
||||||
|
|
||||||
|
@Column(name = "checker_id")
|
||||||
|
private Long checkerId;
|
||||||
|
|
||||||
|
@Column(name = "checked_date")
|
||||||
|
private OffsetDateTime checkedDate;
|
||||||
|
|
||||||
|
@Column(name = "reject_reason", length = 500)
|
||||||
|
private String rejectReason;
|
||||||
|
|
||||||
|
@Column(name = "pending_lock_key", length = 60, unique = true)
|
||||||
|
private String pendingLockKey;
|
||||||
|
|
||||||
|
public static ClientPendingEdit submit(final Client client, final String payloadJson, final String originalJson,
|
||||||
|
final AppUser maker) {
|
||||||
|
final ClientPendingEdit edit = new ClientPendingEdit();
|
||||||
|
edit.client = client;
|
||||||
|
edit.payloadJson = payloadJson;
|
||||||
|
edit.originalJson = originalJson;
|
||||||
|
edit.status = ClientPendingEditStatus.PENDING.name();
|
||||||
|
edit.makerId = maker.getId();
|
||||||
|
edit.submittedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
edit.pendingLockKey = lockKeyFor(client.getId());
|
||||||
|
return edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String lockKeyFor(final Long clientId) {
|
||||||
|
return "CLIENT_EDIT:" + clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPending() {
|
||||||
|
return ClientPendingEditStatus.PENDING.name().equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The maker can never be their own checker — a distinct human must approve/reject an edit. */
|
||||||
|
public boolean isMadeBy(final Long userId) {
|
||||||
|
return this.makerId != null && this.makerId.equals(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void approve(final AppUser checker) {
|
||||||
|
this.status = ClientPendingEditStatus.APPROVED.name();
|
||||||
|
this.checkerId = checker.getId();
|
||||||
|
this.checkedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
this.pendingLockKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reject(final AppUser checker, final String reason) {
|
||||||
|
this.status = ClientPendingEditStatus.REJECTED.name();
|
||||||
|
this.checkerId = checker.getId();
|
||||||
|
this.checkedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
this.rejectReason = reason;
|
||||||
|
this.pendingLockKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.client.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ClientPendingEditRepository extends JpaRepository<ClientPendingEdit, Long> {
|
||||||
|
|
||||||
|
Optional<ClientPendingEdit> findByClientIdAndStatus(Long clientId, String status);
|
||||||
|
|
||||||
|
List<ClientPendingEdit> findByStatusOrderBySubmittedDateDesc(String status);
|
||||||
|
}
|
||||||
+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.client.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.client.exception.ClientNoPendingEditException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClientPendingEditRepositoryWrapper {
|
||||||
|
|
||||||
|
private final ClientPendingEditRepository repository;
|
||||||
|
|
||||||
|
public ClientPendingEdit findPendingByClientIdWithNotFoundDetection(final Long clientId) {
|
||||||
|
return this.repository.findByClientIdAndStatus(clientId, ClientPendingEditStatus.PENDING.name())
|
||||||
|
.orElseThrow(() -> new ClientNoPendingEditException(clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ClientPendingEdit> findPendingByClientId(final Long clientId) {
|
||||||
|
return this.repository.findByClientIdAndStatus(clientId, ClientPendingEditStatus.PENDING.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClientPendingEdit> findAllPending() {
|
||||||
|
return this.repository.findByStatusOrderBySubmittedDateDesc(ClientPendingEditStatus.PENDING.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientPendingEdit saveAndFlush(final ClientPendingEdit edit) {
|
||||||
|
return this.repository.saveAndFlush(edit);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.client.domain;
|
||||||
|
|
||||||
|
/** Lifecycle of a {@link ClientPendingEdit}. Terminal once APPROVED or REJECTED. */
|
||||||
|
public enum ClientPendingEditStatus {
|
||||||
|
|
||||||
|
PENDING, //
|
||||||
|
APPROVED, //
|
||||||
|
REJECTED; //
|
||||||
|
}
|
||||||
+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.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the user attempting to activate a client is the same user who created it — a
|
||||||
|
* four-eyes (maker-checker) requirement with no bypass, including for users holding
|
||||||
|
* CHECKER_SUPER_USER, since a bypass would recreate the exact "ALL_FUNCTIONS/super user ignores
|
||||||
|
* the hold" loophole this check exists to close.
|
||||||
|
*/
|
||||||
|
public class ClientActivationSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientActivationSelfApprovalNotAllowedException() {
|
||||||
|
super("error.msg.client.activation.self.approval.not.allowed",
|
||||||
|
"You cannot activate a client you created yourself — a different user must activate it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
public class ClientEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientEditPendingApprovalException(final Long clientId) {
|
||||||
|
super("error.msg.client.edit.pending.approval", "Client with identifier " + clientId + " already has an edit pending approval",
|
||||||
|
clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the user attempting to approve/reject a Client's pending edit is the same user who
|
||||||
|
* submitted it — no bypass, including for users holding CHECKER_SUPER_USER, since a bypass would
|
||||||
|
* recreate the exact "role bypasses the hold" loophole this check exists to close.
|
||||||
|
*/
|
||||||
|
public class ClientEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientEditSelfApprovalNotAllowedException(final Long clientId) {
|
||||||
|
super("error.msg.client.edit.self.approval.not.allowed",
|
||||||
|
"You cannot approve or reject an edit you submitted yourself for client " + clientId
|
||||||
|
+ " — a different user must review it.",
|
||||||
|
clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.client.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown on client activation when neither an active "Father" nor "Caretaker" family member has
|
||||||
|
* been recorded. Kept separate from {@link ClientFamilyMemberMotherRequiredException} so the two
|
||||||
|
* failure reasons surface as distinct errors instead of one merged message.
|
||||||
|
*/
|
||||||
|
public class ClientFamilyMemberFatherOrCaretakerRequiredException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientFamilyMemberFatherOrCaretakerRequiredException() {
|
||||||
|
super("error.msg.client.family.member.father.or.caretaker.required",
|
||||||
|
"Either Father or a Caretaker is required as a family member before this client can be activated.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.client.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown on client activation when no active "Mother" family member has been recorded. Kept
|
||||||
|
* separate from {@link ClientFamilyMemberFatherOrCaretakerRequiredException} so the two failure
|
||||||
|
* reasons surface as distinct errors instead of one merged message.
|
||||||
|
*/
|
||||||
|
public class ClientFamilyMemberMotherRequiredException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientFamilyMemberMotherRequiredException() {
|
||||||
|
super("error.msg.client.family.member.mother.required",
|
||||||
|
"Mother is required as a family member before this client can be activated.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.client.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
public class ClientNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientNoPendingEditException(final Long clientId) {
|
||||||
|
super("error.msg.client.no.pending.edit", "Client with identifier " + clientId + " has no edit pending approval", clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user