Add CFCFRMS (MHA/I4C) Bank API and Simulator module

CFCFRMS integration for cyber fraud complaint management:
- Bank-side: complaint CRUD, lien/freeze/resolve actions, escalation, audit trail, dashboard stats
- Simulator (I4C/MHA): push complaints, hold directives, bulk push, receive updates/escalations, account/transaction verification, bank performance metrics
- Domain: ComplaintTicket, ComplaintAuditLog, CfcfrmsSimulatorLog entities
- Liquibase 0226: 3 tables (MySQL+PostgreSQL), indexes, permissions
- Seed data: 13 realistic complaint tickets + 5 simulator log entries
- JAX-RS endpoints at /v1/cfcfrms/complaints/* and /v1/simulator/cfcfrms/*
- Full Swagger @Operation annotations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-18 17:49:54 +05:30
co-authored by Claude Opus 4.6
parent 6a464291d5
commit 78f37257a7
22 changed files with 2758 additions and 0 deletions
@@ -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.cfcfrms.data;
public final class CfcfrmsConstants {
private CfcfrmsConstants() {}
public static final String CFCFRMS_RESOURCE_NAME = "CFCFRMS";
public static final String BANK_IFSC = "LINC0000001";
public static final String BANK_NAME = "LincPay Solutions Pvt Ltd";
}
@@ -0,0 +1,98 @@
/**
* 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.cfcfrms.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;
/**
* Simulator-side audit log for CFCFRMS (I4C/MHA) events.
* Separate from the bank-side complaint tables.
*/
@Entity
@Table(name = "m_cfcfrms_simulator_log")
public class CfcfrmsSimulatorLog extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "cfcfrms_ref", nullable = false, length = 50)
private String cfcfrmsRef;
@Column(name = "ticket_id", length = 50)
private String ticketId;
@Column(name = "bank_ifsc", length = 11)
private String bankIfsc;
/** INBOUND (bank → CFCFRMS) or OUTBOUND (CFCFRMS → bank) */
@Column(name = "direction", nullable = false, length = 10)
private String direction;
@Column(name = "event_type", nullable = false, length = 50)
private String eventType;
@Column(name = "payload", columnDefinition = "TEXT")
private String payload;
@Column(name = "response_payload", columnDefinition = "TEXT")
private String responsePayload;
@Column(name = "event_timestamp", nullable = false)
private OffsetDateTime eventTimestamp;
protected CfcfrmsSimulatorLog() {}
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
final String direction, final String eventType, final String payload,
final String responsePayload) {
this.cfcfrmsRef = cfcfrmsRef;
this.ticketId = ticketId;
this.bankIfsc = bankIfsc;
this.direction = direction;
this.eventType = eventType;
this.payload = payload;
this.responsePayload = responsePayload;
this.eventTimestamp = OffsetDateTime.now();
}
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
final String direction, final String eventType, final String payload,
final String responsePayload, final OffsetDateTime eventTimestamp) {
this.cfcfrmsRef = cfcfrmsRef;
this.ticketId = ticketId;
this.bankIfsc = bankIfsc;
this.direction = direction;
this.eventType = eventType;
this.payload = payload;
this.responsePayload = responsePayload;
this.eventTimestamp = eventTimestamp;
}
public String getCfcfrmsRef() { return cfcfrmsRef; }
public String getTicketId() { return ticketId; }
public String getBankIfsc() { return bankIfsc; }
public String getDirection() { return direction; }
public String getEventType() { return eventType; }
public String getPayload() { return payload; }
public String getResponsePayload() { return responsePayload; }
public OffsetDateTime getEventTimestamp() { return eventTimestamp; }
public void setResponsePayload(final String responsePayload) { this.responsePayload = responsePayload; }
}
@@ -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.cfcfrms.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CfcfrmsSimulatorLogRepository extends JpaRepository<CfcfrmsSimulatorLog, Long> {
Optional<CfcfrmsSimulatorLog> findFirstByCfcfrmsRefOrderByEventTimestampDesc(String cfcfrmsRef);
List<CfcfrmsSimulatorLog> findByCfcfrmsRefOrderByEventTimestampAsc(String cfcfrmsRef);
List<CfcfrmsSimulatorLog> findByTicketIdOrderByEventTimestampAsc(String ticketId);
List<CfcfrmsSimulatorLog> findByBankIfscOrderByEventTimestampDesc(String bankIfsc);
long countByBankIfsc(String bankIfsc);
}
@@ -0,0 +1,74 @@
/**
* 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.cfcfrms.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;
/**
* Audit trail for every status change on a CFCFRMS complaint ticket.
*/
@Entity
@Table(name = "m_cfcfrms_complaint_audit_log")
public class ComplaintAuditLog extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "ticket_id", nullable = false, length = 50)
private String ticketId;
@Column(name = "event", nullable = false, length = 50)
private String event;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "actor", nullable = false, length = 200)
private String actor;
@Column(name = "event_timestamp", nullable = false)
private OffsetDateTime eventTimestamp;
protected ComplaintAuditLog() {}
public ComplaintAuditLog(final String ticketId, final String event, final String description,
final String actor) {
this.ticketId = ticketId;
this.event = event;
this.description = description;
this.actor = actor;
this.eventTimestamp = OffsetDateTime.now();
}
public ComplaintAuditLog(final String ticketId, final String event, final String description,
final String actor, final OffsetDateTime eventTimestamp) {
this.ticketId = ticketId;
this.event = event;
this.description = description;
this.actor = actor;
this.eventTimestamp = eventTimestamp;
}
public String getTicketId() { return ticketId; }
public String getEvent() { return event; }
public String getDescription() { return description; }
public String getActor() { return actor; }
public OffsetDateTime getEventTimestamp() { return eventTimestamp; }
}
@@ -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.cfcfrms.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ComplaintAuditLogRepository extends JpaRepository<ComplaintAuditLog, Long> {
List<ComplaintAuditLog> findByTicketIdOrderByEventTimestampAsc(String ticketId);
}
@@ -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.cfcfrms.domain;
public enum ComplaintSource {
HELPLINE_1930("HELPLINE_1930"),
NCRP_PORTAL("NCRP_PORTAL"),
DIRECT_BANK("DIRECT_BANK");
private final String value;
ComplaintSource(final String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static ComplaintSource fromString(final String text) {
for (ComplaintSource source : ComplaintSource.values()) {
if (source.value.equalsIgnoreCase(text)) {
return source;
}
}
throw new IllegalArgumentException("Unknown ComplaintSource: " + text);
}
}
@@ -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.cfcfrms.domain;
public enum ComplaintStatus {
RECEIVED("RECEIVED"),
UNDER_REVIEW("UNDER_REVIEW"),
LIEN_MARKED("LIEN_MARKED"),
DEBIT_FROZEN("DEBIT_FROZEN"),
FUNDS_MOVED("FUNDS_MOVED"),
RESOLVED("RESOLVED"),
REJECTED("REJECTED");
private final String value;
ComplaintStatus(final String value) {
this.value = value;
}
public String getValue() {
return value;
}
public boolean canTransitionTo(final ComplaintStatus next) {
return switch (this) {
case RECEIVED -> next == UNDER_REVIEW || next == LIEN_MARKED || next == REJECTED;
case UNDER_REVIEW -> next == LIEN_MARKED || next == DEBIT_FROZEN || next == REJECTED;
case LIEN_MARKED -> next == DEBIT_FROZEN || next == FUNDS_MOVED || next == RESOLVED || next == REJECTED;
case DEBIT_FROZEN -> next == FUNDS_MOVED || next == RESOLVED || next == REJECTED;
case FUNDS_MOVED -> next == RESOLVED;
case RESOLVED, REJECTED -> false;
};
}
public static ComplaintStatus fromString(final String text) {
for (ComplaintStatus status : ComplaintStatus.values()) {
if (status.value.equalsIgnoreCase(text)) {
return status;
}
}
throw new IllegalArgumentException("Unknown ComplaintStatus: " + text);
}
}
@@ -0,0 +1,166 @@
/**
* 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.cfcfrms.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* CFCFRMS Complaint Ticket — represents a cyber fraud complaint pushed by MHA/I4C to the bank.
*/
@Entity
@Table(name = "m_cfcfrms_complaint_ticket")
public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "ticket_id", nullable = false, unique = true, length = 50)
private String ticketId;
@Column(name = "complainant_name", nullable = false, length = 200)
private String complainantName;
@Column(name = "complainant_mobile", nullable = false, length = 15)
private String complainantMobile;
@Column(name = "fraud_amount", nullable = false, precision = 19, scale = 2)
private BigDecimal fraudAmount;
@Column(name = "transaction_ref_id", nullable = false, length = 100)
private String transactionRefId;
@Column(name = "fraud_account_number", nullable = false, length = 30)
private String fraudAccountNumber;
@Column(name = "fraud_bank_ifsc", nullable = false, length = 11)
private String fraudBankIfsc;
@Column(name = "victim_account_number", nullable = false, length = 30)
private String victimAccountNumber;
@Column(name = "victim_bank_ifsc", nullable = false, length = 11)
private String victimBankIfsc;
@Column(name = "upi_id", length = 50)
private String upiId;
@Enumerated(EnumType.STRING)
@Column(name = "complaint_source", nullable = false, length = 20)
private ComplaintSource complaintSource;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private ComplaintStatus status;
@Column(name = "action_taken", columnDefinition = "TEXT")
private String actionTaken;
@Column(name = "available_balance", precision = 19, scale = 2)
private BigDecimal availableBalance;
@Column(name = "funds_held", precision = 19, scale = 2)
private BigDecimal fundsHeld;
@Column(name = "next_hop_account_number", length = 30)
private String nextHopAccountNumber;
@Column(name = "next_hop_bank_ifsc", length = 11)
private String nextHopBankIfsc;
@Column(name = "received_at", nullable = false)
private OffsetDateTime receivedAt;
@Column(name = "resolved_at")
private OffsetDateTime resolvedAt;
@Column(name = "assigned_officer", nullable = false, length = 200)
private String assignedOfficer;
@Column(name = "state_police_ref", length = 50)
private String statePoliceRef;
protected ComplaintTicket() {}
public static ComplaintTicket create(final String ticketId, final String complainantName, final String complainantMobile,
final BigDecimal fraudAmount, final String transactionRefId, final String fraudAccountNumber,
final String fraudBankIfsc, final String victimAccountNumber, final String victimBankIfsc,
final String upiId, final ComplaintSource complaintSource, final String assignedOfficer,
final String statePoliceRef) {
final ComplaintTicket ticket = new ComplaintTicket();
ticket.ticketId = ticketId;
ticket.complainantName = complainantName;
ticket.complainantMobile = complainantMobile;
ticket.fraudAmount = fraudAmount;
ticket.transactionRefId = transactionRefId;
ticket.fraudAccountNumber = fraudAccountNumber;
ticket.fraudBankIfsc = fraudBankIfsc;
ticket.victimAccountNumber = victimAccountNumber;
ticket.victimBankIfsc = victimBankIfsc;
ticket.upiId = upiId;
ticket.complaintSource = complaintSource;
ticket.status = ComplaintStatus.RECEIVED;
ticket.assignedOfficer = assignedOfficer;
ticket.statePoliceRef = statePoliceRef;
ticket.receivedAt = OffsetDateTime.now();
return ticket;
}
public void updateStatus(final ComplaintStatus newStatus, final String actionNote) {
if (!this.status.canTransitionTo(newStatus)) {
throw new IllegalStateException("Cannot transition from " + this.status + " to " + newStatus);
}
this.status = newStatus;
this.actionTaken = actionNote;
if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) {
this.resolvedAt = OffsetDateTime.now();
}
}
public void setAvailableBalance(final BigDecimal availableBalance) { this.availableBalance = availableBalance; }
public void setFundsHeld(final BigDecimal fundsHeld) { this.fundsHeld = fundsHeld; }
public void setNextHopAccountNumber(final String nextHopAccountNumber) { this.nextHopAccountNumber = nextHopAccountNumber; }
public void setNextHopBankIfsc(final String nextHopBankIfsc) { this.nextHopBankIfsc = nextHopBankIfsc; }
// ── Accessors ──
public String getTicketId() { return ticketId; }
public String getComplainantName() { return complainantName; }
public String getComplainantMobile() { return complainantMobile; }
public BigDecimal getFraudAmount() { return fraudAmount; }
public String getTransactionRefId() { return transactionRefId; }
public String getFraudAccountNumber() { return fraudAccountNumber; }
public String getFraudBankIfsc() { return fraudBankIfsc; }
public String getVictimAccountNumber() { return victimAccountNumber; }
public String getVictimBankIfsc() { return victimBankIfsc; }
public String getUpiId() { return upiId; }
public ComplaintSource getComplaintSource() { return complaintSource; }
public ComplaintStatus getStatus() { return status; }
public String getActionTaken() { return actionTaken; }
public BigDecimal getAvailableBalance() { return availableBalance; }
public BigDecimal getFundsHeld() { return fundsHeld; }
public String getNextHopAccountNumber() { return nextHopAccountNumber; }
public String getNextHopBankIfsc() { return nextHopBankIfsc; }
public OffsetDateTime getReceivedAt() { return receivedAt; }
public OffsetDateTime getResolvedAt() { return resolvedAt; }
public String getAssignedOfficer() { return assignedOfficer; }
public String getStatePoliceRef() { return statePoliceRef; }
}
@@ -0,0 +1,54 @@
/**
* 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.cfcfrms.domain;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface ComplaintTicketRepository extends JpaRepository<ComplaintTicket, Long> {
Optional<ComplaintTicket> findByTicketId(String ticketId);
boolean existsByTicketId(String ticketId);
Page<ComplaintTicket> findByStatus(ComplaintStatus status, Pageable pageable);
Page<ComplaintTicket> findByReceivedAtBetween(OffsetDateTime from, OffsetDateTime to, Pageable pageable);
Page<ComplaintTicket> findByStatusAndReceivedAtBetween(ComplaintStatus status, OffsetDateTime from, OffsetDateTime to,
Pageable pageable);
long countByStatus(ComplaintStatus status);
List<ComplaintTicket> findByFraudAccountNumber(String fraudAccountNumber);
@Query("SELECT COALESCE(SUM(t.fraudAmount), 0) FROM ComplaintTicket t")
BigDecimal sumTotalFraudAmount();
@Query("SELECT COALESCE(SUM(t.fundsHeld), 0) FROM ComplaintTicket t WHERE t.fundsHeld IS NOT NULL")
BigDecimal sumTotalFundsHeld();
}
@@ -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.cfcfrms.domain;
public enum DirectiveType {
IMMEDIATE_DEBIT_FREEZE,
LIEN_MARK,
RELEASE
}