Add all CBS custom modules: KYC, Sanctions, RBI Feed, OpenProject, Mobile Lookup, Charges, Avro schemas

Includes all prior work not yet committed on this branch:
- KYC module (LincPay eKYC: PAN, Aadhaar OTP, GSTIN verification)
- Sanctions screening (UN Security Council consolidated list)
- RBI Feed integration with scheduled jobs
- OpenProject API integration
- Mobile lookup service
- Charge module enhancements (current account support)
- Client family member nominee fields
- Avro schema updates
- Liquibase migrations 0216-0224
- Postman collections for OpenProject, RBI Feeds, Sanctions
- SQL seed scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-18 17:54:10 +05:30
co-authored by Claude Opus 4.6
parent 78f37257a7
commit ba5365d4af
102 changed files with 9757 additions and 18 deletions
@@ -60,6 +60,7 @@ public enum JobName {
ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), //
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), //
JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), //
FETCH_RBI_RSS_FEEDS("Fetch RBI RSS Feeds"), //
; //
private final String name;
@@ -0,0 +1,37 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.openproject.data;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public final class OpenProjectProjectData {
private Long id;
private String identifier;
private String name;
private boolean active;
private String description;
private String createdAt;
private String updatedAt;
}
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.openproject.data;
import java.util.Map;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public final class OpenProjectStatusSummaryData {
private int totalWorkPackages;
private Map<String, Integer> byStatus;
private Map<String, Integer> byType;
private Map<String, Integer> byPriority;
}
@@ -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.infrastructure.openproject.data;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public final class OpenProjectWorkPackageData {
private Long id;
private String subject;
private String description;
private String typeName;
private String statusName;
private String priorityName;
private String assigneeName;
private Integer percentageDone;
private String startDate;
private String dueDate;
private String createdAt;
private String updatedAt;
}
@@ -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.infrastructure.rbifeed.data;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public final class RbiFeedItemData implements Serializable {
private Long id;
private String title;
private String description;
private String link;
private LocalDateTime pubDate;
private String category;
private LocalDateTime fetchedOn;
private Long openProjectWpId;
private String openProjectWpStatus;
private Integer openProjectWpProgress;
public static RbiFeedItemData instance(final Long id, final String title, final String description, final String link,
final LocalDateTime pubDate, final String category, final LocalDateTime fetchedOn,
final Long openProjectWpId, final String openProjectWpStatus, final Integer openProjectWpProgress) {
return new RbiFeedItemData().setId(id).setTitle(title).setDescription(description).setLink(link).setPubDate(pubDate)
.setCategory(category).setFetchedOn(fetchedOn)
.setOpenProjectWpId(openProjectWpId).setOpenProjectWpStatus(openProjectWpStatus)
.setOpenProjectWpProgress(openProjectWpProgress);
}
}
@@ -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.infrastructure.rbifeed.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@Entity
@Table(name = "m_rbi_feed_item", uniqueConstraints = {
@UniqueConstraint(columnNames = { "link" }, name = "uk_rbi_feed_item_link") })
@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class RbiFeedItem extends AbstractPersistableCustom<Long> {
@Column(name = "title", nullable = false, length = 1000)
private String title;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "link", nullable = false, length = 1000)
private String link;
@Column(name = "pub_date")
private LocalDateTime pubDate;
@Column(name = "category", nullable = false, length = 50)
private String category;
@Column(name = "fetched_on")
private LocalDateTime fetchedOn;
@Column(name = "openproject_wp_id")
private Long openProjectWpId;
@Column(name = "openproject_wp_status", length = 100)
private String openProjectWpStatus;
@Column(name = "openproject_wp_progress")
private Integer openProjectWpProgress;
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.rbifeed.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface RbiFeedItemRepository extends JpaRepository<RbiFeedItem, Long>, JpaSpecificationExecutor<RbiFeedItem> {
boolean existsByLink(String link);
List<RbiFeedItem> findByCategoryOrderByPubDateDesc(String category);
List<RbiFeedItem> findAllByOrderByPubDateDesc();
List<RbiFeedItem> findByCategoryAndOpenProjectWpIdIsNotNull(String category);
List<RbiFeedItem> findByCategoryAndOpenProjectWpIdIsNull(String category);
}
@@ -87,6 +87,8 @@ public final class ChargeData implements Comparable<ChargeData>, Serializable {
private final List<EnumOptionData> clientChargeTimeTypeOptions;
private final List<EnumOptionData> shareChargeCalculationTypeOptions;
private final List<EnumOptionData> shareChargeTimeTypeOptions;
private final List<EnumOptionData> currentAccountChargeCalculationTypeOptions;
private final List<EnumOptionData> currentAccountChargeTimeTypeOptions;
private final List<EnumOptionData> feeFrequencyOptions;
@@ -113,6 +115,8 @@ public final class ChargeData implements Comparable<ChargeData>, Serializable {
.taxGroupOptions(template.getTaxGroupOptions())
.shareChargeCalculationTypeOptions(template.getShareChargeCalculationTypeOptions())
.shareChargeTimeTypeOptions(template.getShareChargeTimeTypeOptions())
.currentAccountChargeCalculationTypeOptions(template.getCurrentAccountChargeCalculationTypeOptions())
.currentAccountChargeTimeTypeOptions(template.getCurrentAccountChargeTimeTypeOptions())
.accountMappingForChargeConfig(template.getAccountMappingForChargeConfig())
.expenseAccountOptions(template.getExpenseAccountOptions()).assetAccountOptions(template.getAssetAccountOptions()).build();
}
@@ -72,6 +72,13 @@ public enum ChargeTimeType {
ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue() };
}
public static Object[] validCurrentAccountValues() {
return new Integer[] { ChargeTimeType.SPECIFIED_DUE_DATE.getValue(), ChargeTimeType.SAVINGS_ACTIVATION.getValue(),
ChargeTimeType.SAVINGS_CLOSURE.getValue(), ChargeTimeType.WITHDRAWAL_FEE.getValue(), ChargeTimeType.ANNUAL_FEE.getValue(),
ChargeTimeType.MONTHLY_FEE.getValue(), ChargeTimeType.OVERDRAFT_FEE.getValue(), ChargeTimeType.WEEKLY_FEE.getValue(),
ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue() };
}
public static Object[] validClientValues() {
return new Integer[] { ChargeTimeType.SPECIFIED_DUE_DATE.getValue() };
}
@@ -181,6 +181,7 @@ public class ClientApiConstants {
public static final String datatables = "datatables";
public static final String obligeeData = "ObligeeDetails";
public static final String kycParamName = "kyc";
public static final String clientEntityName = "clients";
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.client.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -53,4 +54,7 @@ public class ClientFamilyMemberRequest implements Serializable {
private Long id;
private Long age;
private Long professionId;
private String nominationType;
private BigDecimal percentageShare;
private String remarks;
}
@@ -20,6 +20,7 @@
package org.apache.fineract.portfolio.client.data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import lombok.Builder;
@@ -66,6 +67,12 @@ public final class ClientFamilyMembersData implements Serializable {
private final Boolean isDependent;
private final String nominationType;
private final BigDecimal percentageShare;
private final String remarks;
// template holder
private final Collection<CodeValueData> relationshipIdOptions;
private final Collection<CodeValueData> genderIdOptions;
@@ -0,0 +1,380 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India
* Master Direction on KYC (updated 2023).
*
* <p>One record per client. Contains:
* <ul>
* <li>PAN verification (Permanent Account Number – Income Tax Dept)</li>
* <li>Aadhaar verification (12-digit UIDAI number, stored masked)</li>
* <li>GSTIN verification (for non-individual/business clients)</li>
* <li>Risk category, CKYCRR number, expiry &amp; lifecycle timestamps</li>
* </ul>
*/
@Entity
@Table(name = "m_client_kyc", uniqueConstraints = {
@UniqueConstraint(columnNames = { "client_id" }, name = "uq_m_client_kyc_client_id") })
public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
/** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */
@Column(name = "kyc_status", nullable = false)
private Integer kycStatus;
/** 1 = INDIVIDUAL, 2 = NON_INDIVIDUAL */
@Column(name = "kyc_type", nullable = false)
private Integer kycType;
/** 1 = LOW, 2 = MEDIUM, 3 = HIGH */
@Column(name = "risk_category", nullable = false)
private Integer riskCategory;
// ── PAN (Permanent Account Number) ──────────────────────────────────────
@Column(name = "pan_number", length = 10)
private String panNumber;
@Column(name = "pan_verified", nullable = false)
private boolean panVerified;
@Column(name = "pan_verified_on")
private LocalDate panVerifiedOn;
@Column(name = "pan_full_name", length = 200)
private String panFullName;
/** Y = YES, N = NO, NA = Not Available (Aadhaar seeding status on PAN card) */
@Column(name = "pan_aadhaar_seeding_status", length = 5)
private String panAadhaarSeedingStatus;
/** PAN card category: individual, company, firm, etc. */
@Column(name = "pan_category", length = 50)
private String panCategory;
/** PAN validation status: valid or invalid */
@Column(name = "pan_status", length = 20)
private String panStatus;
// ── Aadhaar ──────────────────────────────────────────────────────────────
/** Stored masked (last 4 digits visible). Full number must never be persisted as per UIDAI directive. */
@Column(name = "aadhaar_number_masked", length = 12)
private String aadhaarNumberMasked;
@Column(name = "aadhaar_verified", nullable = false)
private boolean aadhaarVerified;
@Column(name = "aadhaar_verified_on")
private LocalDate aadhaarVerifiedOn;
// ── GSTIN (mandatory for NON_INDIVIDUAL clients) ─────────────────────────
@Column(name = "gstin_number", length = 15)
private String gstinNumber;
@Column(name = "gstin_verified", nullable = false)
private boolean gstinVerified;
@Column(name = "gstin_verified_on")
private LocalDate gstinVerifiedOn;
// ── CKYCRR ───────────────────────────────────────────────────────────────
/** Central KYC Records Registry number issued by CERSAI */
@Column(name = "ckyc_number", length = 14)
private String ckycNumber;
// ── Lifecycle dates ───────────────────────────────────────────────────────
@Column(name = "initiated_on")
private LocalDate initiatedOn;
@Column(name = "verified_on")
private LocalDate verifiedOn;
@Column(name = "rejected_on")
private LocalDate rejectedOn;
/** KYC expiry date calculated based on risk category and verification date */
@Column(name = "expiry_date")
private LocalDate expiryDate;
@Column(name = "remarks", length = 500)
private String remarks;
// ─────────────────────────────────────────────────────────────────────────
protected ClientKyc() {}
private ClientKyc(final Client client, final Integer kycType, final Integer riskCategory,
final String panNumber, final String aadhaarNumberMasked, final String gstinNumber,
final String remarks) {
this.client = client;
this.kycStatus = ClientKycStatus.PENDING.getValue();
this.kycType = kycType;
this.riskCategory = riskCategory;
this.panNumber = StringUtils.defaultIfEmpty(panNumber, null);
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(aadhaarNumberMasked, null);
this.gstinNumber = StringUtils.defaultIfEmpty(gstinNumber, null);
this.remarks = StringUtils.defaultIfEmpty(remarks, null);
this.panVerified = false;
this.aadhaarVerified = false;
this.gstinVerified = false;
this.initiatedOn = LocalDate.now();
}
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
final Integer kycType = command.integerValueOfParameterNamed("kycType");
final Integer riskCategory = command.integerValueOfParameterNamed("riskCategory");
final String panNumber = command.stringValueOfParameterNamed("panNumber");
final String aadhaarNumberMasked = command.stringValueOfParameterNamed("aadhaarNumberMasked");
final String gstinNumber = command.stringValueOfParameterNamed("gstinNumber");
final String remarks = command.stringValueOfParameterNamed("remarks");
return new ClientKyc(client, kycType, riskCategory, panNumber, aadhaarNumberMasked, gstinNumber, remarks);
}
/**
* Factory method used when KYC is initiated inline during client registration.
* Accepts individual field values directly (avoids needing a JsonCommand sub-object).
*/
public static ClientKyc initiateKyc(final Client client, final Integer kycType, final Integer riskCategory,
final String panNumber, final String aadhaarNumberMasked, final String gstinNumber, final String remarks) {
return new ClientKyc(client, kycType, riskCategory, panNumber, aadhaarNumberMasked, gstinNumber, remarks);
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>();
final String panNumberParam = "panNumber";
if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) {
final String newValue = command.stringValueOfParameterNamed(panNumberParam);
actualChanges.put(panNumberParam, newValue);
this.panNumber = StringUtils.defaultIfEmpty(newValue, null);
}
final String aadhaarParam = "aadhaarNumberMasked";
if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) {
final String newValue = command.stringValueOfParameterNamed(aadhaarParam);
actualChanges.put(aadhaarParam, newValue);
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
}
final String gstinParam = "gstinNumber";
if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) {
final String newValue = command.stringValueOfParameterNamed(gstinParam);
actualChanges.put(gstinParam, newValue);
this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null);
}
final String riskParam = "riskCategory";
if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) {
final Integer newValue = command.integerValueOfParameterNamed(riskParam);
actualChanges.put(riskParam, newValue);
this.riskCategory = newValue;
}
final String remarksParam = "remarks";
if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) {
final String newValue = command.stringValueOfParameterNamed(remarksParam);
actualChanges.put(remarksParam, newValue);
this.remarks = StringUtils.defaultIfEmpty(newValue, null);
}
return actualChanges;
}
/** Called when documents are submitted and 3rd-party verification is initiated. */
public void markInProgress() {
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
}
/** Called after successful 3rd-party verification of all required documents. */
public void markVerified(final String ckycNumber) {
this.kycStatus = ClientKycStatus.VERIFIED.getValue();
this.verifiedOn = LocalDate.now();
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
if (risk.getReKycYears() > 0) {
this.expiryDate = this.verifiedOn.plusYears(risk.getReKycYears());
}
}
/** Called when verification fails. */
public void markRejected(final String remarks) {
this.kycStatus = ClientKycStatus.REJECTED.getValue();
this.rejectedOn = LocalDate.now();
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
}
public void markPanVerified(final String fullName, final String aadhaarSeedingStatus,
final String category, final String status) {
this.panVerified = true;
this.panVerifiedOn = LocalDate.now();
this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
this.panAadhaarSeedingStatus = normalizeAadhaarSeedingStatus(aadhaarSeedingStatus);
this.panCategory = StringUtils.defaultIfEmpty(category, null);
this.panStatus = StringUtils.defaultIfEmpty(status, null);
}
/** Normalize aadhaar seeding status: y→Y, n→N, anything else→NA */
private String normalizeAadhaarSeedingStatus(final String raw) {
if (raw == null || raw.isBlank()) {
return "NA";
}
final String trimmed = raw.trim().toUpperCase();
if ("Y".equals(trimmed) || "YES".equals(trimmed)) {
return "Y";
}
if ("N".equals(trimmed) || "NO".equals(trimmed)) {
return "N";
}
return "NA";
}
public void markAadhaarVerified() {
this.aadhaarVerified = true;
this.aadhaarVerifiedOn = LocalDate.now();
}
public void markGstinVerified() {
this.gstinVerified = true;
this.gstinVerifiedOn = LocalDate.now();
}
// ── Accessors ─────────────────────────────────────────────────────────────
public Client getClient() {
return client;
}
public ClientKycStatus getKycStatus() {
return ClientKycStatus.fromInt(this.kycStatus);
}
public Integer getKycStatusValue() {
return kycStatus;
}
public ClientKycType getKycType() {
return ClientKycType.fromInt(this.kycType);
}
public Integer getKycTypeValue() {
return kycType;
}
public KycRiskCategory getRiskCategory() {
return KycRiskCategory.fromInt(this.riskCategory);
}
public Integer getRiskCategoryValue() {
return riskCategory;
}
public String getPanNumber() {
return panNumber;
}
public boolean isPanVerified() {
return panVerified;
}
public LocalDate getPanVerifiedOn() {
return panVerifiedOn;
}
public String getPanFullName() {
return panFullName;
}
public String getPanAadhaarSeedingStatus() {
return panAadhaarSeedingStatus;
}
public String getPanCategory() {
return panCategory;
}
public String getPanStatus() {
return panStatus;
}
public String getAadhaarNumberMasked() {
return aadhaarNumberMasked;
}
public boolean isAadhaarVerified() {
return aadhaarVerified;
}
public LocalDate getAadhaarVerifiedOn() {
return aadhaarVerifiedOn;
}
public String getGstinNumber() {
return gstinNumber;
}
public boolean isGstinVerified() {
return gstinVerified;
}
public LocalDate getGstinVerifiedOn() {
return gstinVerifiedOn;
}
public String getCkycNumber() {
return ckycNumber;
}
public LocalDate getInitiatedOn() {
return initiatedOn;
}
public LocalDate getVerifiedOn() {
return verifiedOn;
}
public LocalDate getRejectedOn() {
return rejectedOn;
}
public LocalDate getExpiryDate() {
return expiryDate;
}
public String getRemarks() {
return remarks;
}
}
@@ -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.client.domain;
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;
public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
Optional<ClientKyc> findByClientId(Long clientId);
@Query("SELECT k FROM ClientKyc k WHERE k.client.id = :clientId")
Optional<ClientKyc> findKycByClientId(@Param("clientId") Long clientId);
boolean existsByClientId(Long clientId);
@Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status")
List<ClientKyc> findAllByStatus(@Param("status") Integer status);
}
@@ -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.client.domain;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Wraps {@link ClientKycRepository} to surface consistent not-found exceptions.
*/
@Service
@RequiredArgsConstructor
public class ClientKycRepositoryWrapper {
private final ClientKycRepository repository;
@Transactional(readOnly = true)
public ClientKyc findOneWithNotFoundDetection(final Long id) {
return repository.findById(id).orElseThrow(
() -> new org.apache.fineract.portfolio.client.exception.ClientKycNotFoundException(id));
}
@Transactional(readOnly = true)
public ClientKyc findByClientIdWithNotFoundDetection(final Long clientId) {
return repository.findByClientId(clientId).orElseThrow(
() -> new org.apache.fineract.portfolio.client.exception.ClientKycNotFoundException(clientId));
}
public ClientKyc saveAndFlush(final ClientKyc kyc) {
return repository.saveAndFlush(kyc);
}
public void delete(final ClientKyc kyc) {
repository.delete(kyc);
}
public boolean existsByClientId(final Long clientId) {
return repository.existsByClientId(clientId);
}
}
@@ -0,0 +1,88 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
/**
* KYC status lifecycle as per RBI Master Direction on KYC (updated 2023).
*
* <pre>
* PENDING -> IN_PROGRESS (documents submitted)
* IN_PROGRESS -> VERIFIED (3rd-party check passed)
* IN_PROGRESS -> REJECTED (3rd-party check failed / fraud detected)
* VERIFIED -> EXPIRED (re-KYC due date crossed)
* EXPIRED -> IN_PROGRESS (re-KYC initiated)
* </pre>
*/
public enum ClientKycStatus {
INVALID(0, "clientKycStatus.invalid"),
PENDING(100, "clientKycStatus.pending"),
IN_PROGRESS(200, "clientKycStatus.inProgress"),
VERIFIED(300, "clientKycStatus.verified"),
REJECTED(400, "clientKycStatus.rejected"),
EXPIRED(500, "clientKycStatus.expired");
private final Integer value;
private final String code;
ClientKycStatus(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return value;
}
public String getCode() {
return code;
}
public boolean isPending() {
return this == PENDING;
}
public boolean isInProgress() {
return this == IN_PROGRESS;
}
public boolean isVerified() {
return this == VERIFIED;
}
public boolean isRejected() {
return this == REJECTED;
}
public boolean isExpired() {
return this == EXPIRED;
}
public static ClientKycStatus fromInt(final Integer value) {
if (value == null) {
return INVALID;
}
for (ClientKycStatus status : values()) {
if (status.getValue().equals(value)) {
return status;
}
}
return INVALID;
}
}
@@ -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.client.domain;
/**
* KYC entity type as defined under RBI Master Direction on KYC.
* INDIVIDUAL – natural persons.
* NON_INDIVIDUAL – companies, trusts, partnerships, HUFs, etc.
*/
public enum ClientKycType {
INVALID(0, "clientKycType.invalid"),
INDIVIDUAL(1, "clientKycType.individual"),
NON_INDIVIDUAL(2, "clientKycType.nonIndividual");
private final Integer value;
private final String code;
ClientKycType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return value;
}
public String getCode() {
return code;
}
public boolean isIndividual() {
return this == INDIVIDUAL;
}
public boolean isNonIndividual() {
return this == NON_INDIVIDUAL;
}
public static ClientKycType fromInt(final Integer value) {
if (value == null) {
return INVALID;
}
for (ClientKycType type : values()) {
if (type.getValue().equals(value)) {
return type;
}
}
return INVALID;
}
}
@@ -0,0 +1,71 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
/**
* Customer risk category used for periodic re-KYC frequency as per RBI guidelines.
*
* <ul>
* <li>LOW – re-KYC every 10 years</li>
* <li>MEDIUM – re-KYC every 8 years</li>
* <li>HIGH – re-KYC every 2 years</li>
* </ul>
*/
public enum KycRiskCategory {
INVALID(0, "kycRiskCategory.invalid", 0),
LOW(1, "kycRiskCategory.low", 10),
MEDIUM(2, "kycRiskCategory.medium", 8),
HIGH(3, "kycRiskCategory.high", 2);
private final Integer value;
private final String code;
/** Re-KYC validity in years */
private final int reKycYears;
KycRiskCategory(final Integer value, final String code, final int reKycYears) {
this.value = value;
this.code = code;
this.reKycYears = reKycYears;
}
public Integer getValue() {
return value;
}
public String getCode() {
return code;
}
public int getReKycYears() {
return reKycYears;
}
public static KycRiskCategory fromInt(final Integer value) {
if (value == null) {
return INVALID;
}
for (KycRiskCategory cat : values()) {
if (cat.getValue().equals(value)) {
return cat;
}
}
return INVALID;
}
}
@@ -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.client.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ClientKycAlreadyExistsException extends AbstractPlatformDomainRuleException {
public ClientKycAlreadyExistsException(final Long clientId) {
super("error.msg.client.kyc.already.exists",
"A KYC record already exists for client with identifier " + clientId
+ ". Use the update endpoint to modify it.",
clientId);
}
}
@@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ClientKycInvalidStateTransitionException extends AbstractPlatformDomainRuleException {
public ClientKycInvalidStateTransitionException(final String action, final String currentState) {
super("error.msg.client.kyc.invalid.state.transition",
"KYC action [" + action + "] is not permitted when the KYC status is [" + currentState + "]",
action, currentState);
}
}
@@ -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.client.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ClientKycNotFoundException extends AbstractPlatformResourceNotFoundException {
public ClientKycNotFoundException(final Long id) {
super("error.msg.client.kyc.not.found", "Client KYC record with identifier " + id + " does not exist", id);
}
public ClientKycNotFoundException(final Long clientId, final boolean byClient) {
super("error.msg.client.kyc.not.found",
"No KYC record found for client with identifier " + clientId, clientId);
}
}
@@ -0,0 +1,82 @@
/**
* 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.mobilelookup.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Activity log for each DIP / MNRL mobile number lookup request.
*/
@Entity
@Table(name = "m_mobile_lookup_log")
@Getter
public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "mobile_number", nullable = false, length = 15)
private String mobileNumber;
/** CLEAN, SUSPECTED, DISCONNECTED, SUSPECTED_AND_DISCONNECTED, ERROR */
@Column(name = "lookup_status", nullable = false, length = 50)
private String lookupStatus;
@Column(name = "suspected", nullable = false)
private boolean suspected;
@Column(name = "disconnected", nullable = false)
private boolean disconnected;
@Column(name = "response_message", length = 500)
private String responseMessage;
/** Full JSON response from the external API (for audit trail) */
@Column(name = "raw_response", columnDefinition = "TEXT")
private String rawResponse;
/** CLIENT_REGISTRATION, MANUAL_CHECK, API */
@Column(name = "lookup_source", length = 50)
private String lookupSource;
@Column(name = "client_id")
private Long clientId;
@Column(name = "looked_up_on", nullable = false)
private OffsetDateTime lookedUpOn;
protected MobileLookupLog() {}
public MobileLookupLog(final String mobileNumber, final String lookupStatus,
final boolean suspected, final boolean disconnected,
final String responseMessage, final String rawResponse,
final String lookupSource, final Long clientId) {
this.mobileNumber = mobileNumber;
this.lookupStatus = lookupStatus;
this.suspected = suspected;
this.disconnected = disconnected;
this.responseMessage = responseMessage;
this.rawResponse = rawResponse;
this.lookupSource = lookupSource;
this.clientId = clientId;
this.lookedUpOn = OffsetDateTime.now();
}
}
@@ -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.mobilelookup.domain;
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;
import org.springframework.stereotype.Repository;
@Repository
public interface MobileLookupLogRepository extends JpaRepository<MobileLookupLog, Long> {
@Query("SELECT l FROM MobileLookupLog l ORDER BY l.lookedUpOn DESC")
List<MobileLookupLog> findAllOrderByLookedUpOnDesc();
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber ORDER BY l.lookedUpOn DESC")
List<MobileLookupLog> findByMobileNumber(@Param("mobileNumber") String mobileNumber);
@Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC")
List<MobileLookupLog> findByLookupStatus(@Param("status") String status);
}
@@ -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.sanctions.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Represents an alias (also-known-as) name for a sanctions list entry.
*/
@Entity
@Table(name = "m_sanctions_alias")
public class SanctionsAlias extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sanctions_entry_id", nullable = false)
private SanctionsEntry sanctionsEntry;
@Column(name = "alias_quality", length = 50)
private String aliasQuality;
@Column(name = "alias_name", nullable = false, length = 500)
private String aliasName;
protected SanctionsAlias() {}
public SanctionsAlias(final String aliasQuality, final String aliasName) {
this.aliasQuality = aliasQuality;
this.aliasName = aliasName;
}
public void setSanctionsEntry(SanctionsEntry sanctionsEntry) {
this.sanctionsEntry = sanctionsEntry;
}
public SanctionsEntry getSanctionsEntry() { return sanctionsEntry; }
public String getAliasQuality() { return aliasQuality; }
public String getAliasName() { return aliasName; }
}
@@ -0,0 +1,157 @@
/**
* 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.sanctions.domain;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Represents an individual or entity from the UN Security Council consolidated sanctions list.
*/
@Entity
@Table(name = "m_sanctions_entry")
public class SanctionsEntry extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "data_id", nullable = false, length = 50)
private String dataId;
/** INDIVIDUAL or ENTITY */
@Column(name = "entry_type", nullable = false, length = 20)
private String entryType;
@Column(name = "first_name", length = 255)
private String firstName;
@Column(name = "second_name", length = 255)
private String secondName;
@Column(name = "third_name", length = 255)
private String thirdName;
@Column(name = "fourth_name", length = 255)
private String fourthName;
@Column(name = "full_name", length = 500)
private String fullName;
@Column(name = "un_list_type", length = 255)
private String unListType;
@Column(name = "reference_number", length = 255)
private String referenceNumber;
@Column(name = "listed_on")
private LocalDate listedOn;
@Column(name = "gender", length = 20)
private String gender;
@Column(name = "nationality", length = 500)
private String nationality;
@Column(name = "comments", columnDefinition = "TEXT")
private String comments;
@Column(name = "designation", columnDefinition = "TEXT")
private String designation;
@Column(name = "date_of_birth", length = 500)
private String dateOfBirth;
@Column(name = "place_of_birth", length = 500)
private String placeOfBirth;
@Column(name = "is_active", nullable = false)
private boolean active = true;
@OneToMany(mappedBy = "sanctionsEntry", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<SanctionsAlias> aliases = new ArrayList<>();
protected SanctionsEntry() {}
public SanctionsEntry(final String dataId, final String entryType, final String firstName,
final String secondName, final String thirdName, final String fourthName,
final String unListType, final String referenceNumber, final LocalDate listedOn,
final String gender, final String nationality, final String comments,
final String designation, final String dateOfBirth, final String placeOfBirth) {
this.dataId = dataId;
this.entryType = entryType;
this.firstName = firstName;
this.secondName = secondName;
this.thirdName = thirdName;
this.fourthName = fourthName;
this.fullName = buildFullName(firstName, secondName, thirdName, fourthName);
this.unListType = unListType;
this.referenceNumber = referenceNumber;
this.listedOn = listedOn;
this.gender = gender;
this.nationality = nationality;
this.comments = comments;
this.designation = designation;
this.dateOfBirth = dateOfBirth;
this.placeOfBirth = placeOfBirth;
}
private static String buildFullName(String... parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
if (part != null && !part.isBlank()) {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append(part.trim());
}
}
return sb.length() > 0 ? sb.toString() : null;
}
public void addAlias(SanctionsAlias alias) {
aliases.add(alias);
alias.setSanctionsEntry(this);
}
// ── Accessors ─────────────────────────────────────────────────────────────
public String getDataId() { return dataId; }
public String getEntryType() { return entryType; }
public String getFirstName() { return firstName; }
public String getSecondName() { return secondName; }
public String getThirdName() { return thirdName; }
public String getFourthName() { return fourthName; }
public String getFullName() { return fullName; }
public String getUnListType() { return unListType; }
public String getReferenceNumber() { return referenceNumber; }
public LocalDate getListedOn() { return listedOn; }
public String getGender() { return gender; }
public String getNationality() { return nationality; }
public String getComments() { return comments; }
public String getDesignation() { return designation; }
public String getDateOfBirth() { return dateOfBirth; }
public String getPlaceOfBirth() { return placeOfBirth; }
public boolean isActive() { return active; }
public List<SanctionsAlias> getAliases() { return aliases; }
}
@@ -0,0 +1,43 @@
/**
* 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.sanctions.domain;
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;
import org.springframework.stereotype.Repository;
@Repository
public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry, Long> {
@Query("SELECT s FROM SanctionsEntry s WHERE s.active = true AND LOWER(s.fullName) LIKE LOWER(CONCAT('%', :name, '%'))")
List<SanctionsEntry> searchByFullName(@Param("name") String name);
@Query("SELECT DISTINCT s FROM SanctionsEntry s LEFT JOIN s.aliases a WHERE s.active = true " +
"AND (LOWER(s.fullName) LIKE LOWER(CONCAT('%', :name, '%')) " +
"OR LOWER(a.aliasName) LIKE LOWER(CONCAT('%', :name, '%')))")
List<SanctionsEntry> searchByNameOrAlias(@Param("name") String name);
boolean existsByDataId(String dataId);
void deleteAllByActiveTrue();
long countByActiveTrue();
}
@@ -0,0 +1,90 @@
/**
* 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.sanctions.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Activity log for each sanctions screening request.
*/
@Entity
@Table(name = "m_sanctions_screening_log")
public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "screened_name", nullable = false, length = 500)
private String screenedName;
/** FLAGGED or CLEAR */
@Column(name = "screening_result", nullable = false, length = 20)
private String screeningResult;
@Column(name = "match_count", nullable = false)
private Integer matchCount;
@Column(name = "matched_entry_ids", length = 1000)
private String matchedEntryIds;
@Column(name = "matched_names", length = 2000)
private String matchedNames;
/** CLIENT_CREATION, MANUAL_CHECK, API */
@Column(name = "screening_source", length = 50)
private String screeningSource;
@Column(name = "client_id")
private Long clientId;
@Column(name = "ip_address", length = 50)
private String ipAddress;
@Column(name = "screened_on", nullable = false)
private OffsetDateTime screenedOn;
protected SanctionsScreeningLog() {}
public SanctionsScreeningLog(final String screenedName, final String screeningResult,
final Integer matchCount, final String matchedEntryIds, final String matchedNames,
final String screeningSource, final Long clientId, final String ipAddress) {
this.screenedName = screenedName;
this.screeningResult = screeningResult;
this.matchCount = matchCount;
this.matchedEntryIds = matchedEntryIds;
this.matchedNames = matchedNames;
this.screeningSource = screeningSource;
this.clientId = clientId;
this.ipAddress = ipAddress;
this.screenedOn = OffsetDateTime.now();
}
// ── Accessors ─────────────────────────────────────────────────────────────
public String getScreenedName() { return screenedName; }
public String getScreeningResult() { return screeningResult; }
public Integer getMatchCount() { return matchCount; }
public String getMatchedEntryIds() { return matchedEntryIds; }
public String getMatchedNames() { return matchedNames; }
public String getScreeningSource() { return screeningSource; }
public Long getClientId() { return clientId; }
public String getIpAddress() { return ipAddress; }
public OffsetDateTime getScreenedOn() { return screenedOn; }
}
@@ -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.sanctions.domain;
import java.time.OffsetDateTime;
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;
import org.springframework.stereotype.Repository;
@Repository
public interface SanctionsScreeningLogRepository extends JpaRepository<SanctionsScreeningLog, Long> {
@Query("SELECT l FROM SanctionsScreeningLog l ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findAllOrderByScreenedOnDesc();
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.screeningResult = :result ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findByScreeningResult(@Param("result") String result);
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.screenedOn >= :from AND l.screenedOn <= :to ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findByDateRange(@Param("from") OffsetDateTime from, @Param("to") OffsetDateTime to);
@Query("SELECT l FROM SanctionsScreeningLog l WHERE LOWER(l.screenedName) LIKE LOWER(CONCAT('%', :name, '%')) ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findByScreenedNameContaining(@Param("name") String name);
}