Compare commits
14
Commits
7c15fbb0c7
..
uate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7d22382a5 | ||
|
|
8622dcaf90 | ||
|
|
7cdc25322e | ||
|
|
a4c3dbb6c4 | ||
|
|
dd8445de06 | ||
|
|
9345f2294c | ||
|
|
190cd92a85 | ||
|
|
81832b6bb7 | ||
|
|
0bd5296519 | ||
|
|
17e0dab435 | ||
|
|
67810bfaea | ||
|
|
2022dc5241 | ||
|
|
b1cfa996b4 | ||
|
|
02541eed6c |
@@ -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);
|
||||
+2
@@ -65,6 +65,8 @@ public enum JobName {
|
||||
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
|
||||
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
|
||||
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
|
||||
CTR_REPORT_GENERATION("AML Monthly CTR Report Generation"), //
|
||||
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
||||
; //
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* One row per {@link AmlTransactionRule} hit on a live transaction. Raised regardless of the
|
||||
* client's current KYC risk category - alerting is never suppressed by a client being Low/Medium
|
||||
* risk. First reviewed by an AML Officer (whitelist or escalate); an ESCALATED alert spawns an
|
||||
* {@link AmlStrCase} for the Principal Officer.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_aml_alert")
|
||||
public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "transaction_type", nullable = false, length = 20)
|
||||
private String transactionType;
|
||||
|
||||
@Column(name = "transaction_id", nullable = false)
|
||||
private Long transactionId;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "rule_id", nullable = false)
|
||||
private AmlTransactionRule rule;
|
||||
|
||||
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "currency_code", length = 3)
|
||||
private String currencyCode;
|
||||
|
||||
@Column(name = "raised_on", nullable = false)
|
||||
private LocalDateTime raisedOn;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "reviewed_by")
|
||||
private AppUser reviewedBy;
|
||||
|
||||
@Column(name = "reviewed_on")
|
||||
private LocalDateTime reviewedOn;
|
||||
|
||||
@Column(name = "review_notes", length = 1000)
|
||||
private String reviewNotes;
|
||||
|
||||
@Column(name = "disposition", length = 20)
|
||||
private String disposition;
|
||||
|
||||
protected AmlAlert() {}
|
||||
|
||||
public static AmlAlert raise(AmlTransactionType transactionType, Long transactionId, Client client, AmlTransactionRule rule,
|
||||
BigDecimal amount, String currencyCode, LocalDateTime raisedOn) {
|
||||
final AmlAlert alert = new AmlAlert();
|
||||
alert.transactionType = transactionType.name();
|
||||
alert.transactionId = transactionId;
|
||||
alert.client = client;
|
||||
alert.rule = rule;
|
||||
alert.amount = amount;
|
||||
alert.currencyCode = currencyCode;
|
||||
alert.raisedOn = raisedOn;
|
||||
alert.status = AmlAlertStatus.RAISED.name();
|
||||
return alert;
|
||||
}
|
||||
|
||||
public void whitelist(AppUser reviewer, String notes) {
|
||||
this.reviewedBy = reviewer;
|
||||
this.reviewedOn = LocalDateTime.now();
|
||||
this.reviewNotes = notes;
|
||||
this.disposition = AmlAlertDisposition.WHITELIST.name();
|
||||
this.status = AmlAlertStatus.WHITELISTED.name();
|
||||
}
|
||||
|
||||
public void escalate(AppUser reviewer, String notes) {
|
||||
this.reviewedBy = reviewer;
|
||||
this.reviewedOn = LocalDateTime.now();
|
||||
this.reviewNotes = notes;
|
||||
this.disposition = AmlAlertDisposition.ESCALATE.name();
|
||||
this.status = AmlAlertStatus.ESCALATED.name();
|
||||
}
|
||||
|
||||
public Long getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public AmlTransactionType getTransactionType() {
|
||||
return AmlTransactionType.valueOf(transactionType);
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public AmlTransactionRule getRule() {
|
||||
return rule;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return currencyCode;
|
||||
}
|
||||
|
||||
public LocalDateTime getRaisedOn() {
|
||||
return raisedOn;
|
||||
}
|
||||
|
||||
public AmlAlertStatus getStatus() {
|
||||
return AmlAlertStatus.valueOf(status);
|
||||
}
|
||||
|
||||
public AppUser getReviewedBy() {
|
||||
return reviewedBy;
|
||||
}
|
||||
|
||||
public LocalDateTime getReviewedOn() {
|
||||
return reviewedOn;
|
||||
}
|
||||
|
||||
public String getReviewNotes() {
|
||||
return reviewNotes;
|
||||
}
|
||||
|
||||
public AmlAlertDisposition getDisposition() {
|
||||
return disposition == null ? null : AmlAlertDisposition.valueOf(disposition);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.aml.domain;
|
||||
|
||||
/** The AML Officer's first-level disposition of an {@link AmlAlert}. */
|
||||
public enum AmlAlertDisposition {
|
||||
|
||||
WHITELIST, ESCALATE
|
||||
}
|
||||
+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
|
||||
}
|
||||
@@ -617,6 +617,10 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.office.getId();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
public String mobileNo() {
|
||||
return this.mobileNo;
|
||||
}
|
||||
|
||||
@@ -586,6 +586,15 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return riskCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly sets the risk category outside the normal command-diff update path. Used by the AML
|
||||
* module to auto-escalate a client's risk category after their 3rd cumulative STR case -
|
||||
* every such change is recorded in {@code m_client_risk_category_history} by the caller.
|
||||
*/
|
||||
public void escalateRiskCategory(Integer newCategory) {
|
||||
this.riskCategory = newCategory;
|
||||
}
|
||||
|
||||
public String getPanNumber() {
|
||||
return panNumber;
|
||||
}
|
||||
|
||||
+36
-1
@@ -72,9 +72,24 @@ public class ClientRiskRule {
|
||||
@Column(name = "risk_category", nullable = false)
|
||||
private Integer riskCategory;
|
||||
|
||||
@Column(name = "reason", nullable = false, length = 255)
|
||||
@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;
|
||||
|
||||
@@ -135,4 +150,24 @@ public class ClientRiskRule {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.sanctions.domain;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
|
||||
void deleteAllByActiveTrue();
|
||||
|
||||
long countByActiveTrue();
|
||||
|
||||
/**
|
||||
* Timestamp the active sanctions list was last loaded into the database — i.e. the data
|
||||
* currency shown to callers so they know which list snapshot a screening ran against.
|
||||
*/
|
||||
@Query("SELECT MAX(s.createdDate) FROM SanctionsEntry s WHERE s.active = true")
|
||||
Optional<OffsetDateTime> findListLastLoadedOn();
|
||||
}
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -44,6 +44,7 @@ import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.commands.data.AuditData;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.AuditReadPlatformService;
|
||||
@@ -142,7 +143,8 @@ public class ClientsApiResource {
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@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"
|
||||
+ "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)))
|
||||
public String retrieveAll(@Context final UriInfo uriInfo,
|
||||
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
|
||||
@@ -157,10 +159,15 @@ public class ClientsApiResource {
|
||||
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
|
||||
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
|
||||
@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,
|
||||
orderBy, sortOrder, orphansOnly, false);
|
||||
orderBy, sortOrder, orphansOnly, false, fromDateParsed, toDateParsed);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -437,7 +444,7 @@ public class ClientsApiResource {
|
||||
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 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);
|
||||
sqlValidator.validate(orderBy);
|
||||
sqlValidator.validate(sortOrder);
|
||||
@@ -445,7 +452,8 @@ public class ClientsApiResource {
|
||||
sqlValidator.validate(hierarchy);
|
||||
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId)
|
||||
.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 ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return toApiJsonSerializer.serialize(settings, clientData, ClientApiConstants.CLIENT_RESPONSE_DATA_PARAMETERS);
|
||||
|
||||
+33
-1
@@ -41,6 +41,11 @@ public final class KycRiskRuleData implements Serializable {
|
||||
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,
|
||||
@@ -48,7 +53,9 @@ public final class KycRiskRuleData implements Serializable {
|
||||
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 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;
|
||||
@@ -65,6 +72,11 @@ public final class KycRiskRuleData implements Serializable {
|
||||
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() {
|
||||
@@ -130,4 +142,24 @@ public final class KycRiskRuleData implements Serializable {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -178,6 +178,16 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
extraCriteria += " and c.status_enum = " + clientStatus.getValue().toString() + " ";
|
||||
}
|
||||
|
||||
if (searchParameters.hasFromDate()) {
|
||||
paramList.add(searchParameters.getFromDate().toString());
|
||||
extraCriteria += " and c.submittedon_date >= ?::date ";
|
||||
}
|
||||
|
||||
if (searchParameters.hasToDate()) {
|
||||
paramList.add(searchParameters.getToDate().toString());
|
||||
extraCriteria += " and c.submittedon_date <= ?::date ";
|
||||
}
|
||||
|
||||
if (firstname != null) {
|
||||
paramList.add(firstname);
|
||||
extraCriteria += " and c.firstname like ? ";
|
||||
|
||||
+6
-1
@@ -62,7 +62,12 @@ public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlat
|
||||
rule.getMaxAnnualTurnover(),
|
||||
rule.getRiskCategory(),
|
||||
rule.getReason(),
|
||||
rule.getOrderPosition()
|
||||
rule.getOrderPosition(),
|
||||
rule.getAmlCheckStatus(),
|
||||
rule.getBannedEntityMatch(),
|
||||
rule.getWillfulDefaulter(),
|
||||
rule.getEcgcSalFlag(),
|
||||
rule.getDormantFlag()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -30,6 +30,7 @@ import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.data.PartyData;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlFundTransferEvaluationService;
|
||||
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.ResolvedLimits;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferResponseData;
|
||||
@@ -76,6 +77,7 @@ public class FundTransferWritePlatformServiceImpl implements FundTransferWritePl
|
||||
private final SanctionsScreeningService sanctionsScreeningService;
|
||||
private final TransferChannelWindowValidator windowValidator;
|
||||
private final SuspenseGlService suspenseGl;
|
||||
private final AmlFundTransferEvaluationService amlFundTransferEvaluationService;
|
||||
|
||||
// ─── /verify ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -168,6 +170,7 @@ public class FundTransferWritePlatformServiceImpl implements FundTransferWritePl
|
||||
|
||||
eventRepository.save(FundTransferEvent.of(saved.getId(), null, saved.getStatus(),
|
||||
needsApproval ? "INITIATED_PENDING_APPROVAL" : "INITIATED_AUTO_EXECUTE", null, cashier.getId()));
|
||||
amlFundTransferEvaluationService.evaluateIfSuccessful(saved);
|
||||
|
||||
return FundTransferReadPlatformServiceImpl.toData(saved);
|
||||
}
|
||||
@@ -190,6 +193,7 @@ public class FundTransferWritePlatformServiceImpl implements FundTransferWritePl
|
||||
executeStrategy(t);
|
||||
FundTransfer saved = fundTransferRepository.save(t);
|
||||
eventRepository.save(FundTransferEvent.of(saved.getId(), prev, saved.getStatus(), "APPROVED", null, approver.getId()));
|
||||
amlFundTransferEvaluationService.evaluateIfSuccessful(saved);
|
||||
return FundTransferReadPlatformServiceImpl.toData(saved);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -26,6 +26,7 @@ import org.apache.fineract.infrastructure.imps.data.AccountData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.data.PartyData;
|
||||
import org.apache.fineract.portfolio.aml.service.AmlFundTransferEvaluationService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEvent;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEventRepository;
|
||||
@@ -54,6 +55,7 @@ public class NeftBatchProcessorService {
|
||||
private final FundTransferEventRepository eventRepository;
|
||||
private final ImpsSwitchClient switchClient;
|
||||
private final SuspenseGlService suspenseGl;
|
||||
private final AmlFundTransferEvaluationService amlFundTransferEvaluationService;
|
||||
|
||||
@Value("${fineract.imps.our-default-ifsc:BANK0000001}")
|
||||
private String ourDefaultIfsc;
|
||||
@@ -109,6 +111,7 @@ public class NeftBatchProcessorService {
|
||||
fundTransferRepository.save(t);
|
||||
eventRepository.save(FundTransferEvent.of(t.getId(), prev, mapped, "NEFT_BATCH_PROCESSED",
|
||||
"code=" + code, null));
|
||||
amlFundTransferEvaluationService.evaluateIfSuccessful(t);
|
||||
log.info("[NeftBatchProcessor] txnRef={} {} → {} utr={}", t.getTxnRef(), prev, mapped, t.getUtr());
|
||||
return mapped;
|
||||
} catch (RuntimeException ex) {
|
||||
|
||||
+10
-5
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.sanctions.data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -32,10 +33,11 @@ public final class SanctionsScreeningResult {
|
||||
private final String message;
|
||||
private final int matchCount;
|
||||
private final List<SanctionsMatchData> matches;
|
||||
private final OffsetDateTime listDataAsOf;
|
||||
|
||||
private SanctionsScreeningResult(final Long screeningLogId, final String screenedName, final boolean matchFound,
|
||||
final String status, final String message, final int matchCount,
|
||||
final List<SanctionsMatchData> matches) {
|
||||
final List<SanctionsMatchData> matches, final OffsetDateTime listDataAsOf) {
|
||||
this.screeningLogId = screeningLogId;
|
||||
this.screenedName = screenedName;
|
||||
this.matchFound = matchFound;
|
||||
@@ -43,20 +45,22 @@ public final class SanctionsScreeningResult {
|
||||
this.message = message;
|
||||
this.matchCount = matchCount;
|
||||
this.matches = matches;
|
||||
this.listDataAsOf = listDataAsOf;
|
||||
}
|
||||
|
||||
public static SanctionsScreeningResult matched(final Long screeningLogId, final String screenedName,
|
||||
final List<SanctionsMatchData> matches) {
|
||||
final List<SanctionsMatchData> matches, final OffsetDateTime listDataAsOf) {
|
||||
return new SanctionsScreeningResult(screeningLogId, screenedName, true, "FLAGGED",
|
||||
"Potential match found in UN Security Council consolidated sanctions list. "
|
||||
+ "Manual review required before proceeding.",
|
||||
matches.size(), matches);
|
||||
matches.size(), matches, listDataAsOf);
|
||||
}
|
||||
|
||||
public static SanctionsScreeningResult clear(final Long screeningLogId, final String screenedName) {
|
||||
public static SanctionsScreeningResult clear(final Long screeningLogId, final String screenedName,
|
||||
final OffsetDateTime listDataAsOf) {
|
||||
return new SanctionsScreeningResult(screeningLogId, screenedName, false, "CLEAR",
|
||||
"No match found in UN Security Council consolidated sanctions list.",
|
||||
0, List.of());
|
||||
0, List.of(), listDataAsOf);
|
||||
}
|
||||
|
||||
public Long getScreeningLogId() { return screeningLogId; }
|
||||
@@ -66,4 +70,5 @@ public final class SanctionsScreeningResult {
|
||||
public String getMessage() { return message; }
|
||||
public int getMatchCount() { return matchCount; }
|
||||
public List<SanctionsMatchData> getMatches() { return matches; }
|
||||
public OffsetDateTime getListDataAsOf() { return listDataAsOf; }
|
||||
}
|
||||
|
||||
+5
-3
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.sanctions.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -54,18 +55,19 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
|
||||
final Long clientId, final String ipAddress) {
|
||||
if (name == null || name.isBlank()) {
|
||||
final Long logId = saveLog(name, "CLEAR", 0, null, null, source, clientId, ipAddress);
|
||||
return SanctionsScreeningResult.clear(logId, name);
|
||||
return SanctionsScreeningResult.clear(logId, name, sanctionsEntryRepository.findListLastLoadedOn().orElse(null));
|
||||
}
|
||||
|
||||
final String trimmedName = name.trim();
|
||||
LOG.info("Screening name against sanctions list: {}", trimmedName);
|
||||
|
||||
final OffsetDateTime listDataAsOf = sanctionsEntryRepository.findListLastLoadedOn().orElse(null);
|
||||
final List<SanctionsEntry> matches = sanctionsEntryRepository.searchByNameOrAlias(trimmedName);
|
||||
|
||||
if (matches.isEmpty()) {
|
||||
LOG.info("No sanctions match found for: {}", trimmedName);
|
||||
final Long logId = saveLog(trimmedName, "CLEAR", 0, null, null, source, clientId, ipAddress);
|
||||
return SanctionsScreeningResult.clear(logId, trimmedName);
|
||||
return SanctionsScreeningResult.clear(logId, trimmedName, listDataAsOf);
|
||||
}
|
||||
|
||||
LOG.warn("Sanctions match found for '{}': {} potential match(es)", trimmedName, matches.size());
|
||||
@@ -84,7 +86,7 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
|
||||
|
||||
final Long logId = saveLog(trimmedName, "FLAGGED", matches.size(), matchedEntryIds, matchedNames, source, clientId, ipAddress);
|
||||
|
||||
return SanctionsScreeningResult.matched(logId, trimmedName, matchDataList);
|
||||
return SanctionsScreeningResult.matched(logId, trimmedName, matchDataList, listDataAsOf);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ public class SelfClientsApiResource {
|
||||
final String hierarchy = null;
|
||||
final Boolean orphansOnly = null;
|
||||
return this.clientApiResource.retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm,
|
||||
hierarchy, offset, limit, orderBy, sortOrder, orphansOnly, true);
|
||||
hierarchy, offset, limit, orderBy, sortOrder, orphansOnly, true, null, null);
|
||||
}
|
||||
|
||||
@GET
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package org.apache.fineract.portfolio.shareaccounts.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData;
|
||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/shareaccounts/summary")
|
||||
@Component
|
||||
@Tag(name = "Share Account Summary", description = "Provides a highly optimized summary of all share accounts linked with their respective clients.")
|
||||
@RequiredArgsConstructor
|
||||
public class ShareAccountSummaryApiResource {
|
||||
|
||||
private final PlatformSecurityContext platformSecurityContext;
|
||||
private final ShareAccountReadPlatformService shareAccountReadPlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List all share accounts with client details", description = "Retrieves all share accounts along with their associated client details in a single optimized query.\n\n"
|
||||
+ "Example Requests:\n\n" + "shareaccounts/summary")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public Collection<ShareAccountClientSummaryData> retrieveAllSummaries(@QueryParam("searchTerm") final String searchTerm) {
|
||||
long start = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Start of retrieveAllSummaries (before auth)");
|
||||
this.platformSecurityContext.authenticatedUser();
|
||||
long afterAuth = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Auth check: " + (afterAuth - start) + "ms");
|
||||
Collection<ShareAccountClientSummaryData> result = this.shareAccountReadPlatformService.retrieveAllShareAccountClientSummaries(searchTerm);
|
||||
long afterService = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Service call total (including query): " + (afterService - afterAuth) + "ms");
|
||||
System.out.println("[TIMING] Right before response returned: " + (System.currentTimeMillis() - start) + "ms total so far");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package org.apache.fineract.portfolio.shareaccounts.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountStatusEnumData;
|
||||
|
||||
@Getter
|
||||
public class ShareAccountClientSummaryData implements Serializable {
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientName;
|
||||
private final Long officeId;
|
||||
private final String officeName;
|
||||
private final String accountNo;
|
||||
private final Long productId;
|
||||
private final String productName;
|
||||
private final Long totalApprovedShares;
|
||||
private final Long totalPendingForApprovalShares;
|
||||
private final CurrencyData currency;
|
||||
private final LocalDate activatedDate;
|
||||
private final ShareAccountStatusEnumData status;
|
||||
|
||||
public ShareAccountClientSummaryData(final Long id, final Long clientId, final String clientName, final Long officeId, final String officeName, final String accountNo,
|
||||
final Long productId, final String productName, final Long totalApprovedShares, final Long totalPendingForApprovalShares,
|
||||
final CurrencyData currency, final LocalDate activatedDate, final ShareAccountStatusEnumData status) {
|
||||
this.id = id;
|
||||
this.clientId = clientId;
|
||||
this.clientName = clientName;
|
||||
this.officeId = officeId;
|
||||
this.officeName = officeName;
|
||||
this.accountNo = accountNo;
|
||||
this.productId = productId;
|
||||
this.productName = productName;
|
||||
|
||||
if (totalApprovedShares == null) {
|
||||
this.totalApprovedShares = Long.valueOf(0);
|
||||
} else {
|
||||
this.totalApprovedShares = totalApprovedShares;
|
||||
}
|
||||
|
||||
if (totalPendingForApprovalShares == null) {
|
||||
this.totalPendingForApprovalShares = Long.valueOf(0);
|
||||
} else {
|
||||
this.totalPendingForApprovalShares = totalPendingForApprovalShares;
|
||||
}
|
||||
|
||||
this.currency = currency;
|
||||
this.activatedDate = activatedDate;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
+3
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.portfolio.accounts.service.AccountReadPlatformService;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountData;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData;
|
||||
|
||||
public interface ShareAccountReadPlatformService extends AccountReadPlatformService {
|
||||
|
||||
@@ -38,5 +39,7 @@ public interface ShareAccountReadPlatformService extends AccountReadPlatformServ
|
||||
Collection<ShareAccountData> retrieveAllShareAccountDataForDividends(Long productId, boolean fetchInActiveAccounts,
|
||||
LocalDate startDate);
|
||||
|
||||
Collection<ShareAccountClientSummaryData> retrieveAllShareAccountClientSummaries(String searchTerm);
|
||||
|
||||
String retrieveAccountNumberByAccountId(Long accountId);
|
||||
}
|
||||
|
||||
+81
@@ -62,6 +62,7 @@ import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountStatusType
|
||||
import org.apache.fineract.portfolio.shareproducts.data.ShareProductData;
|
||||
import org.apache.fineract.portfolio.shareproducts.data.ShareProductMarketPriceData;
|
||||
import org.apache.fineract.portfolio.shareproducts.service.ShareProductDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -221,6 +222,29 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
|
||||
}, mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ShareAccountClientSummaryData> retrieveAllShareAccountClientSummaries(String searchTerm) {
|
||||
long start = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Inside retrieveAllShareAccountClientSummaries, before query builder setup");
|
||||
ShareAccountClientSummaryMapper mapper = new ShareAccountClientSummaryMapper();
|
||||
StringBuilder queryBuilder = new StringBuilder("select ").append(mapper.schema());
|
||||
List<Object> paramList = new ArrayList<>();
|
||||
|
||||
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
|
||||
queryBuilder.append(" where (c.display_name ILIKE ? OR sa.account_no ILIKE ?)");
|
||||
String searchPattern = "%" + searchTerm + "%";
|
||||
paramList.add(searchPattern);
|
||||
paramList.add(searchPattern);
|
||||
}
|
||||
|
||||
long beforeQuery = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Before actual SQL query call. Time since service start: " + (beforeQuery - start) + "ms");
|
||||
Collection<ShareAccountClientSummaryData> result = this.jdbcTemplate.query(queryBuilder.toString(), mapper, paramList.toArray());
|
||||
long afterQuery = System.currentTimeMillis();
|
||||
System.out.println("[TIMING] Query execution (JdbcTemplate): " + (afterQuery - beforeQuery) + "ms");
|
||||
return result;
|
||||
}
|
||||
|
||||
public Collection<ShareAccountChargeData> convertChargesToShareAccountCharges(Collection<ChargeData> productCharges) {
|
||||
final Collection<ShareAccountChargeData> savingsCharges = new ArrayList<>();
|
||||
for (final ChargeData chargeData : productCharges) {
|
||||
@@ -495,6 +519,63 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ShareAccountClientSummaryMapper implements RowMapper<ShareAccountClientSummaryData> {
|
||||
|
||||
private final String schema;
|
||||
|
||||
ShareAccountClientSummaryMapper() {
|
||||
StringBuilder buff = new StringBuilder().append("sa.id as id, c.id as clientId, c.display_name as clientName, ")
|
||||
.append("sa.account_no as accountNo, p.id as productId, p.name as productName, ")
|
||||
.append("sa.total_approved_shares as approvedShares, sa.total_pending_shares as pendingShares, ")
|
||||
.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, ")
|
||||
.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ")
|
||||
.append("curr.display_symbol as currencyDisplaySymbol, sa.activated_date as activatedDate, ")
|
||||
.append("sa.status_enum as statusEnum, o.id as officeId, o.name as officeName ")
|
||||
.append("from m_share_account sa ")
|
||||
.append("inner join m_client c ON c.id = sa.client_id ")
|
||||
.append("inner join m_office o ON o.id = c.office_id ")
|
||||
.append("inner join m_share_product p on p.id = sa.product_id ")
|
||||
.append("inner join m_currency curr on curr.code = sa.currency_code");
|
||||
this.schema = buff.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShareAccountClientSummaryData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
|
||||
final Long id = rs.getLong("id");
|
||||
final Long clientId = rs.getLong("clientId");
|
||||
final String clientName = rs.getString("clientName");
|
||||
final String accountNo = rs.getString("accountNo");
|
||||
final Long productId = rs.getLong("productId");
|
||||
final String productName = rs.getString("productName");
|
||||
final Long totalApprovedShares = JdbcSupport.getLong(rs, "approvedShares");
|
||||
final Long totalPendingForApprovalShares = JdbcSupport.getLong(rs, "pendingShares");
|
||||
|
||||
final String currencyCode = rs.getString("currencyCode");
|
||||
final String currencyName = rs.getString("currencyName");
|
||||
final String currencyNameCode = rs.getString("currencyNameCode");
|
||||
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
|
||||
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
|
||||
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
|
||||
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
|
||||
currencyDisplaySymbol, currencyNameCode);
|
||||
|
||||
final LocalDate activatedDate = JdbcSupport.getLocalDate(rs, "activatedDate");
|
||||
|
||||
final Integer statusEnum = rs.getInt("statusEnum");
|
||||
final ShareAccountStatusEnumData status = SharesEnumerations.status(statusEnum);
|
||||
|
||||
final Long officeId = rs.getLong("officeId");
|
||||
final String officeName = rs.getString("officeName");
|
||||
|
||||
return new ShareAccountClientSummaryData(id, clientId, clientName, officeId, officeName, accountNo, productId, productName,
|
||||
totalApprovedShares, totalPendingForApprovalShares, currency, activatedDate, status);
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.schema;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retrieveAccountNumberByAccountId(Long accountId) {
|
||||
try {
|
||||
|
||||
+73
@@ -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.welcomekit.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitReadPlatformService;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/clients/{clientId}/welcomekit")
|
||||
@Component
|
||||
@Tag(name = "Welcome Kit Dispatch", description = "Manages the dispatching and tracking of welcome kits for clients.")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientWelcomeKitApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WELCOMEKIT";
|
||||
private final PlatformSecurityContext context;
|
||||
private final WelcomeKitReadPlatformService readPlatformService;
|
||||
private final WelcomeKitWritePlatformService writePlatformService;
|
||||
private final FromJsonHelper fromJsonHelper;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List all welcome kits for a Client")
|
||||
public ClientWelcomeKitData getClientWelcomeKits(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return this.readPlatformService.retrieveWelcomeKitsByClientId(clientId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create a new welcome kit dispatch")
|
||||
public CommandProcessingResult dispatchWelcomeKit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CREATE_" + RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(clientId, this.fromJsonHelper.parse(apiRequestBodyAsJson), this.fromJsonHelper);
|
||||
|
||||
return this.writePlatformService.dispatchWelcomeKit(clientId, command);
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.welcomekit.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitReadPlatformService;
|
||||
import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitWritePlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/welcomekit")
|
||||
@Component
|
||||
@Tag(name = "Welcome Kit Dispatch", description = "Manages the dispatching and tracking of welcome kits for clients.")
|
||||
@RequiredArgsConstructor
|
||||
public class WelcomeKitApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WELCOMEKIT";
|
||||
private final PlatformSecurityContext context;
|
||||
private final WelcomeKitWritePlatformService writePlatformService;
|
||||
private final FromJsonHelper fromJsonHelper;
|
||||
|
||||
@PUT
|
||||
@Path("{welcomeKitId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Update the status of an existing dispatch")
|
||||
public CommandProcessingResult updateWelcomeKitStatus(
|
||||
@PathParam("welcomeKitId") @Parameter(description = "welcomeKitId") final Long welcomeKitId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("UPDATE_" + RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(welcomeKitId, this.fromJsonHelper.parse(apiRequestBodyAsJson), this.fromJsonHelper);
|
||||
|
||||
return this.writePlatformService.updateWelcomeKitStatus(welcomeKitId, command);
|
||||
}
|
||||
}
|
||||
+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.welcomekit.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ClientWelcomeKitData implements Serializable {
|
||||
|
||||
private final String clientName;
|
||||
private final boolean canDispatchNew;
|
||||
private final List<WelcomeKitDispatchData> dispatches;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.welcomekit.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class WelcomeKitDispatchData implements Serializable {
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientName;
|
||||
private final String deliveryAddress;
|
||||
private final String deliveryMode;
|
||||
private final String deliveryModeOtherText;
|
||||
private final String trackingId;
|
||||
private final String status;
|
||||
private final LocalDate dispatchedOnDate;
|
||||
private final LocalDate deliveredOnDate;
|
||||
}
|
||||
+118
@@ -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.welcomekit.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_welcome_kit_dispatch")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class WelcomeKitDispatch extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@Column(name = "delivery_address", length = 500, nullable = false)
|
||||
private String deliveryAddress;
|
||||
|
||||
@Column(name = "delivery_mode", length = 50, nullable = false)
|
||||
private String deliveryMode;
|
||||
|
||||
@Column(name = "delivery_mode_other_text", length = 255)
|
||||
private String deliveryModeOtherText;
|
||||
|
||||
@Column(name = "tracking_id", length = 100, nullable = false, unique = true)
|
||||
private String trackingId;
|
||||
|
||||
@Column(name = "status", length = 30, nullable = false)
|
||||
private String status;
|
||||
|
||||
@Column(name = "dispatched_on_date", nullable = false)
|
||||
private LocalDate dispatchedOnDate;
|
||||
|
||||
@Column(name = "delivered_on_date")
|
||||
private LocalDate deliveredOnDate;
|
||||
|
||||
@CreatedBy
|
||||
@Column(name = "created_by", updatable = false)
|
||||
private Long createdBy;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_date", updatable = false)
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
@LastModifiedBy
|
||||
@Column(name = "last_modified_by")
|
||||
private Long lastModifiedBy;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "last_modified_date")
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
protected WelcomeKitDispatch() {
|
||||
// Default constructor for JPA
|
||||
}
|
||||
|
||||
private WelcomeKitDispatch(final Client client, final String deliveryAddress, final String deliveryMode,
|
||||
final String deliveryModeOtherText, final String trackingId, final String status,
|
||||
final LocalDate dispatchedOnDate) {
|
||||
this.client = client;
|
||||
this.deliveryAddress = deliveryAddress;
|
||||
this.deliveryMode = deliveryMode;
|
||||
this.deliveryModeOtherText = deliveryModeOtherText;
|
||||
this.trackingId = trackingId;
|
||||
this.status = status;
|
||||
this.dispatchedOnDate = dispatchedOnDate;
|
||||
}
|
||||
|
||||
public static WelcomeKitDispatch createNew(final Client client, final String deliveryAddress, final String deliveryMode,
|
||||
final String deliveryModeOtherText, final String trackingId, final LocalDate dispatchedOnDate) {
|
||||
return new WelcomeKitDispatch(client, deliveryAddress, deliveryMode, deliveryModeOtherText, trackingId, "DISPATCHED", dispatchedOnDate);
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public void updateStatus(final String newStatus, final LocalDate deliveredOnDate) {
|
||||
this.status = newStatus;
|
||||
if ("DELIVERED".equals(newStatus)) {
|
||||
this.deliveredOnDate = deliveredOnDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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.welcomekit.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface WelcomeKitDispatchRepository extends JpaRepository<WelcomeKitDispatch, Long>, JpaSpecificationExecutor<WelcomeKitDispatch> {
|
||||
|
||||
List<WelcomeKitDispatch> findByClientIdOrderByCreatedDateDesc(Long clientId);
|
||||
}
|
||||
+26
@@ -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.welcomekit.service;
|
||||
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
|
||||
public interface WelcomeKitReadPlatformService {
|
||||
|
||||
ClientWelcomeKitData retrieveWelcomeKitsByClientId(Long clientId);
|
||||
}
|
||||
+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.welcomekit.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData;
|
||||
import org.apache.fineract.portfolio.welcomekit.data.WelcomeKitDispatchData;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WelcomeKitReadPlatformServiceImpl implements WelcomeKitReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public ClientWelcomeKitData retrieveWelcomeKitsByClientId(Long clientId) {
|
||||
final WelcomeKitDispatchMapper rm = new WelcomeKitDispatchMapper();
|
||||
final String sql = "SELECT " + rm.schema() + " WHERE w.client_id = ? ORDER BY w.created_date DESC";
|
||||
|
||||
List<WelcomeKitDispatchData> dispatches = this.jdbcTemplate.query(sql, rm, clientId);
|
||||
|
||||
String clientName = this.jdbcTemplate.queryForObject("SELECT display_name FROM m_client WHERE id = ?", String.class, clientId);
|
||||
|
||||
boolean canDispatchNew = true;
|
||||
if (!dispatches.isEmpty()) {
|
||||
WelcomeKitDispatchData latest = dispatches.get(0);
|
||||
if ("RETURNED".equals(latest.getStatus())) {
|
||||
canDispatchNew = true;
|
||||
} else {
|
||||
canDispatchNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
return ClientWelcomeKitData.builder()
|
||||
.clientName(clientName)
|
||||
.canDispatchNew(canDispatchNew)
|
||||
.dispatches(dispatches)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static final class WelcomeKitDispatchMapper implements RowMapper<WelcomeKitDispatchData> {
|
||||
|
||||
public String schema() {
|
||||
return " w.id as id, w.client_id as clientId, c.display_name as clientName, "
|
||||
+ " w.delivery_address as deliveryAddress, w.delivery_mode as deliveryMode, "
|
||||
+ " w.delivery_mode_other_text as deliveryModeOtherText, w.tracking_id as trackingId, "
|
||||
+ " w.status as status, w.dispatched_on_date as dispatchedOnDate, w.delivered_on_date as deliveredOnDate "
|
||||
+ " FROM m_welcome_kit_dispatch w "
|
||||
+ " JOIN m_client c ON c.id = w.client_id ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WelcomeKitDispatchData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
final Long id = rs.getLong("id");
|
||||
final Long clientId = rs.getLong("clientId");
|
||||
final String clientName = rs.getString("clientName");
|
||||
final String deliveryAddress = rs.getString("deliveryAddress");
|
||||
final String deliveryMode = rs.getString("deliveryMode");
|
||||
final String deliveryModeOtherText = rs.getString("deliveryModeOtherText");
|
||||
final String trackingId = rs.getString("trackingId");
|
||||
final String status = rs.getString("status");
|
||||
final LocalDate dispatchedOnDate = JdbcSupport.getLocalDate(rs, "dispatchedOnDate");
|
||||
final LocalDate deliveredOnDate = JdbcSupport.getLocalDate(rs, "deliveredOnDate");
|
||||
|
||||
return WelcomeKitDispatchData.builder()
|
||||
.id(id)
|
||||
.clientId(clientId)
|
||||
.clientName(clientName)
|
||||
.deliveryAddress(deliveryAddress)
|
||||
.deliveryMode(deliveryMode)
|
||||
.deliveryModeOtherText(deliveryModeOtherText)
|
||||
.trackingId(trackingId)
|
||||
.status(status)
|
||||
.dispatchedOnDate(dispatchedOnDate)
|
||||
.deliveredOnDate(deliveredOnDate)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.welcomekit.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface WelcomeKitWritePlatformService {
|
||||
|
||||
CommandProcessingResult dispatchWelcomeKit(Long clientId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateWelcomeKitStatus(Long welcomeKitId, JsonCommand command);
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.welcomekit.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.welcomekit.domain.WelcomeKitDispatch;
|
||||
import org.apache.fineract.portfolio.welcomekit.domain.WelcomeKitDispatchRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WelcomeKitWritePlatformServiceImpl implements WelcomeKitWritePlatformService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientRepositoryWrapper clientRepository;
|
||||
private final WelcomeKitDispatchRepository welcomeKitDispatchRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult dispatchWelcomeKit(final Long clientId, final JsonCommand command) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final String deliveryAddress = command.stringValueOfParameterNamed("deliveryAddress");
|
||||
final String deliveryMode = command.stringValueOfParameterNamed("deliveryMode");
|
||||
final String deliveryModeOtherText = command.stringValueOfParameterNamed("deliveryModeOtherText");
|
||||
final LocalDate dispatchedOnDate = command.localDateValueOfParameterNamed("dispatchedOnDate");
|
||||
|
||||
if (deliveryAddress == null || deliveryAddress.trim().isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.deliveryAddress.cannot.be.blank",
|
||||
"Delivery address cannot be blank.");
|
||||
}
|
||||
if (deliveryMode == null || deliveryMode.trim().isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.deliveryMode.cannot.be.blank",
|
||||
"Delivery mode cannot be blank.");
|
||||
}
|
||||
if (dispatchedOnDate == null) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.dispatchedOnDate.cannot.be.blank",
|
||||
"Dispatched on date cannot be blank.");
|
||||
}
|
||||
|
||||
final String trackingId = "WK-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
|
||||
final WelcomeKitDispatch newDispatch = WelcomeKitDispatch.createNew(client, deliveryAddress, deliveryMode, deliveryModeOtherText,
|
||||
trackingId, dispatchedOnDate);
|
||||
|
||||
this.welcomeKitDispatchRepository.saveAndFlush(newDispatch);
|
||||
|
||||
return new CommandProcessingResultBuilder()
|
||||
.withCommandId(command.commandId())
|
||||
.withClientId(clientId)
|
||||
.withEntityId(newDispatch.getId())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult updateWelcomeKitStatus(final Long welcomeKitId, final JsonCommand command) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final WelcomeKitDispatch dispatch = this.welcomeKitDispatchRepository.findById(welcomeKitId).orElseThrow(
|
||||
() -> new PlatformDataIntegrityException("error.msg.welcomekit.not.found", "Welcome kit not found."));
|
||||
|
||||
final String status = command.stringValueOfParameterNamed("status");
|
||||
|
||||
if (status == null || status.trim().isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.welcomekit.status.cannot.be.blank", "Status cannot be blank.");
|
||||
}
|
||||
|
||||
LocalDate deliveredOnDate = null;
|
||||
if ("DELIVERED".equals(status)) {
|
||||
deliveredOnDate = LocalDate.now();
|
||||
}
|
||||
|
||||
dispatch.updateStatus(status, deliveredOnDate);
|
||||
|
||||
this.welcomeKitDispatchRepository.saveAndFlush(dispatch);
|
||||
|
||||
return new CommandProcessingResultBuilder()
|
||||
.withCommandId(command.commandId())
|
||||
.withClientId(dispatch.getClient().getId())
|
||||
.withEntityId(dispatch.getId())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
|
||||
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966}
|
||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
|
||||
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
|
||||
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
||||
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966}
|
||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
|
||||
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||
|
||||
@@ -435,6 +435,7 @@
|
||||
<include file="parts/0412_activate_outward_clearing_batch_job.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0413_inward_item_verify_signature_permission.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0414_inward_item_full_source_details.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0415_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0415_add_otp_delivery_method_and_first_login_gate_to_appuser.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0416_create_user_agreement_tables.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0417_add_first_login_gate_config.xml" relativeToChangelogFile="true"/>
|
||||
@@ -483,4 +484,11 @@
|
||||
<include file="parts/0362_add_share_account_edit_approval_permissions.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0421_add_master_salutation_value.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0428_add_client_risk_rules.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0429_add_aml_fields_to_risk_rules.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0430_seed_aml_compliance_rules.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0433_create_aml_transaction_monitoring_tables.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0434_seed_aml_transaction_rules.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0435_add_aml_officer_and_principal_officer_roles.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0436_fix_missing_client_risk_rule_columns.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0437_update_aml_columns_for_risk_rules.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+24
-16
@@ -23,24 +23,32 @@
|
||||
|
||||
<!-- Dedicated liability GL account for the Sponsor DD Payable balance owed to SBI, as a child
|
||||
of the existing "OTHER LIABILITY" account (gl_code 9), matching the pattern already used
|
||||
by "INTEREST PAYABLE" (gl_code 9.2). -->
|
||||
<changeSet author="fineract" id="0345-1-insert-sponsor-dd-payable-sbi-gl-account">
|
||||
by "INTEREST PAYABLE" (gl_code 9.2).
|
||||
|
||||
parent_id is resolved by gl_code (not hardcoded) — the "OTHER LIABILITY" row's numeric id
|
||||
is an auto-increment value that depends on each tenant's own insert history, so a
|
||||
hardcoded id (previously 22) only happened to match the `default` tenant and broke FK
|
||||
inserts on any tenant whose chart of accounts was seeded differently (e.g. `saga`), which
|
||||
aborted that tenant's whole Liquibase run — and with it, the entire application startup.
|
||||
runOnChange lets this corrected version safely re-validate on tenants where the old,
|
||||
already-recorded version succeeded: the "gl_code 9.3 doesn't exist yet" precondition below
|
||||
is already false there, so it's skipped rather than re-inserted. -->
|
||||
<changeSet author="fineract" id="0345-1-insert-sponsor-dd-payable-sbi-gl-account" runOnChange="true">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<sqlCheck expectedResult="1">select count(*) from acc_gl_account where gl_code = '9.3'</sqlCheck>
|
||||
</not>
|
||||
<and>
|
||||
<not>
|
||||
<sqlCheck expectedResult="1">select count(*) from acc_gl_account where gl_code = '9.3'</sqlCheck>
|
||||
</not>
|
||||
<sqlCheck expectedResult="1">select count(*) from acc_gl_account where gl_code = '9'</sqlCheck>
|
||||
</and>
|
||||
</preConditions>
|
||||
<insert tableName="acc_gl_account">
|
||||
<column name="name" value="Sponsor DD Payable - SBI"/>
|
||||
<column name="gl_code" value="9.3"/>
|
||||
<column name="parent_id" valueNumeric="22"/>
|
||||
<column name="hierarchy" value="."/>
|
||||
<column name="account_usage" valueNumeric="1"/>
|
||||
<column name="classification_enum" valueNumeric="2"/>
|
||||
<column name="disabled" valueBoolean="false"/>
|
||||
<column name="manual_journal_entries_allowed" valueBoolean="true"/>
|
||||
<column name="description" value="Liability owed to SBI for sponsor bank demand drafts issued on their behalf, pending clearing."/>
|
||||
</insert>
|
||||
<sql>
|
||||
INSERT INTO acc_gl_account (name, gl_code, parent_id, hierarchy, account_usage, classification_enum, disabled, manual_journal_entries_allowed, description)
|
||||
SELECT 'Sponsor DD Payable - SBI', '9.3', parent.id, '.', 1, 2, FALSE, TRUE,
|
||||
'Liability owed to SBI for sponsor bank demand drafts issued on their behalf, pending clearing.'
|
||||
FROM acc_gl_account parent
|
||||
WHERE parent.gl_code = '9';
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<!-- Fineract's GL hierarchy path embeds the row's own auto-generated id, so it can only be
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<changeSet id="0415_welcome_kit_dispatch" author="CBS" runOnChange="false">
|
||||
<sql>
|
||||
CREATE TABLE m_welcome_kit_dispatch (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
client_id BIGINT NOT NULL REFERENCES m_client(id),
|
||||
delivery_address VARCHAR(500) NOT NULL,
|
||||
delivery_mode VARCHAR(50) NOT NULL,
|
||||
delivery_mode_other_text VARCHAR(255),
|
||||
tracking_id VARCHAR(100) NOT NULL UNIQUE,
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'DISPATCHED',
|
||||
dispatched_on_date DATE NOT NULL,
|
||||
delivered_on_date DATE,
|
||||
created_by BIGINT,
|
||||
created_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
last_modified_by BIGINT,
|
||||
last_modified_date TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'READ_WELCOMEKIT', 'WELCOMEKIT', 'READ', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_WELCOMEKIT');
|
||||
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'CREATE_WELCOMEKIT', 'WELCOMEKIT', 'CREATE', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_WELCOMEKIT');
|
||||
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'UPDATE_WELCOMEKIT', 'WELCOMEKIT', 'UPDATE', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_WELCOMEKIT');
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||
<changeSet author="fineract" id="0429-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_client_risk_rule" columnName="aml_check_status"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_client_risk_rule">
|
||||
<column name="aml_check_status" type="VARCHAR(20)"/>
|
||||
<column name="banned_entity_match" type="VARCHAR(10)"/>
|
||||
<column name="willful_defaulter" type="VARCHAR(10)"/>
|
||||
<column name="ecgc_sal_flag" type="VARCHAR(10)"/>
|
||||
<column name="dormant_flag" type="VARCHAR(20)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="0429-2">
|
||||
<modifyDataType tableName="m_client_risk_rule" columnName="rule_name" newDataType="VARCHAR(500)"/>
|
||||
<modifyDataType tableName="m_client_risk_rule" columnName="reason" newDataType="VARCHAR(500)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user