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:
co-authored by
Claude Opus 4.6
parent
6a464291d5
commit
78f37257a7
+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.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";
|
||||
}
|
||||
+98
@@ -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; }
|
||||
}
|
||||
+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.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);
|
||||
}
|
||||
+74
@@ -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; }
|
||||
}
|
||||
+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.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);
|
||||
}
|
||||
+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.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);
|
||||
}
|
||||
}
|
||||
+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.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);
|
||||
}
|
||||
}
|
||||
+166
@@ -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; }
|
||||
}
|
||||
+54
@@ -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();
|
||||
}
|
||||
+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.cfcfrms.domain;
|
||||
|
||||
public enum DirectiveType {
|
||||
|
||||
IMMEDIATE_DEBIT_FREEZE,
|
||||
LIEN_MARK,
|
||||
RELEASE
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 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.api;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
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.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.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintStatsData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintTicketData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.TimelineEventData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.service.CfcfrmsComplaintService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Bank-side CFCFRMS Complaint Management endpoints.
|
||||
*
|
||||
* <pre>
|
||||
* POST /v1/cfcfrms/complaints – Register a new complaint ticket
|
||||
* GET /v1/cfcfrms/complaints – List complaint tickets (filterable)
|
||||
* GET /v1/cfcfrms/complaints/{ticketId} – Get complaint detail
|
||||
* PUT /v1/cfcfrms/complaints/{ticketId}/action – Take action on a ticket
|
||||
* GET /v1/cfcfrms/complaints/stats – Dashboard statistics
|
||||
* GET /v1/cfcfrms/complaints/{ticketId}/timeline – Audit trail
|
||||
* POST /v1/cfcfrms/complaints/{ticketId}/escalate – Escalate to next bank
|
||||
* </pre>
|
||||
*/
|
||||
@Path("/v1/cfcfrms/complaints")
|
||||
@Component
|
||||
@Tag(name = "CFCFRMS Bank Complaints",
|
||||
description = "Bank-side cyber fraud complaint management — receives complaints from MHA/I4C, "
|
||||
+ "manages lien/freeze/recovery actions, and escalates to next bank when funds move.")
|
||||
@RequiredArgsConstructor
|
||||
public class CfcfrmsComplaintApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CfcfrmsComplaintService complaintService;
|
||||
private final DefaultToApiJsonSerializer<ComplaintTicketData> ticketSerializer;
|
||||
private final DefaultToApiJsonSerializer<ComplaintStatsData> statsSerializer;
|
||||
private final DefaultToApiJsonSerializer<List> listSerializer;
|
||||
private final DefaultToApiJsonSerializer<Map> mapSerializer;
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create a new fraud complaint ticket",
|
||||
description = "CFCFRMS pushes a new fraud complaint ticket to the bank. "
|
||||
+ "The bank registers it with status RECEIVED and returns acknowledgement.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Complaint registered"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid request")
|
||||
})
|
||||
public String createComplaint(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CFCFRMS");
|
||||
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
final ComplaintTicketData ticket = complaintService.createComplaint(json);
|
||||
|
||||
final Map<String, Object> response = new HashMap<>();
|
||||
response.put("ticketId", ticket.getTicketId());
|
||||
response.put("status", "RECEIVED");
|
||||
response.put("message", "Complaint registered successfully");
|
||||
response.put("receivedAt", ticket.getReceivedAt().toString());
|
||||
return mapSerializer.serialize(response);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List all complaint tickets",
|
||||
description = "List complaint tickets with optional status and date filters for the bank dashboard.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Complaints listed")
|
||||
})
|
||||
public String listComplaints(
|
||||
@QueryParam("status") @Parameter(description = "Filter by status") final String status,
|
||||
@QueryParam("fromDate") @Parameter(description = "From date (yyyy-MM-dd)") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "To date (yyyy-MM-dd)") final String toDate,
|
||||
@QueryParam("page") @Parameter(description = "Page number (0-based)") final Integer page,
|
||||
@QueryParam("size") @Parameter(description = "Page size") final Integer size) {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
|
||||
final int p = (page != null) ? page : 0;
|
||||
final int s = (size != null) ? size : 20;
|
||||
|
||||
final Page<ComplaintTicketData> result = complaintService.listComplaints(
|
||||
status, fromDate, toDate, PageRequest.of(p, s, Sort.by(Sort.Direction.DESC, "receivedAt")));
|
||||
|
||||
final Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", result.getContent());
|
||||
response.put("totalElements", result.getTotalElements());
|
||||
response.put("totalPages", result.getTotalPages());
|
||||
response.put("page", result.getNumber());
|
||||
response.put("size", result.getSize());
|
||||
return mapSerializer.serialize(response);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stats")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Get dashboard statistics",
|
||||
description = "Summary statistics for the complaint management dashboard — "
|
||||
+ "total complaints, pending, lien marked, debit frozen, resolved, total fraud amount, funds held.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Statistics retrieved")
|
||||
})
|
||||
public String getStats() {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
return statsSerializer.serialize(complaintService.getStats());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ticketId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Get complaint ticket detail",
|
||||
description = "Retrieve full details of a specific complaint ticket by its ticket ID.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Complaint details"),
|
||||
@ApiResponse(responseCode = "404", description = "Ticket not found")
|
||||
})
|
||||
public String getComplaint(
|
||||
@PathParam("ticketId") @Parameter(description = "Ticket ID") final String ticketId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
return ticketSerializer.serialize(complaintService.getComplaint(ticketId));
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{ticketId}/action")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Take action on a complaint ticket",
|
||||
description = "Bank updates ticket — marks lien, freezes account, reports funds moved, resolves, or rejects. "
|
||||
+ "Valid transitions: RECEIVED→LIEN_MARKED, LIEN_MARKED→DEBIT_FROZEN, etc.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Action applied"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid action or state transition")
|
||||
})
|
||||
public String takeAction(
|
||||
@PathParam("ticketId") @Parameter(description = "Ticket ID") final String ticketId,
|
||||
final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("UPDATE_CFCFRMS");
|
||||
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return ticketSerializer.serialize(complaintService.takeAction(ticketId, json));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ticketId}/timeline")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Get complaint audit trail",
|
||||
description = "Full audit trail / event log for a complaint ticket — "
|
||||
+ "every status change with timestamp and actor.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Timeline retrieved"),
|
||||
@ApiResponse(responseCode = "404", description = "Ticket not found")
|
||||
})
|
||||
public String getTimeline(
|
||||
@PathParam("ticketId") @Parameter(description = "Ticket ID") final String ticketId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
return listSerializer.serialize(complaintService.getTimeline(ticketId));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{ticketId}/escalate")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Escalate complaint to next bank",
|
||||
description = "Escalate ticket when funds have moved to another bank. "
|
||||
+ "Only valid for FUNDS_MOVED or DEBIT_FROZEN tickets.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Escalated"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid escalation")
|
||||
})
|
||||
public String escalate(
|
||||
@PathParam("ticketId") @Parameter(description = "Ticket ID") final String ticketId,
|
||||
final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("ESCALATE_CFCFRMS");
|
||||
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return mapSerializer.serialize(complaintService.escalate(ticketId, json));
|
||||
}
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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.api;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
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.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.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.cfcfrms.service.CfcfrmsSimulatorService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* CFCFRMS Simulator endpoints — simulates the I4C/MHA central CFCFRMS system.
|
||||
*
|
||||
* <pre>
|
||||
* --- OUTBOUND (CFCFRMS → Bank) ---
|
||||
* POST /v1/simulator/cfcfrms/push-complaint – Push new complaint to bank
|
||||
* POST /v1/simulator/cfcfrms/push-hold-directive – Send hold/freeze directive
|
||||
* POST /v1/simulator/cfcfrms/push-bulk-complaints – Bulk push complaints
|
||||
*
|
||||
* --- INBOUND (Bank → CFCFRMS) ---
|
||||
* POST /v1/simulator/cfcfrms/receive-bank-update – Receive status update from bank
|
||||
* POST /v1/simulator/cfcfrms/receive-escalation – Receive escalation from bank
|
||||
*
|
||||
* --- VERIFICATION (CFCFRMS → Bank) ---
|
||||
* POST /v1/simulator/cfcfrms/verify-account – Verify account at bank
|
||||
* POST /v1/simulator/cfcfrms/verify-transaction – Verify transaction ref
|
||||
*
|
||||
* --- STATUS & REPORTING ---
|
||||
* GET /v1/simulator/cfcfrms/complaint-status/{ref} – Complaint status from CFCFRMS side
|
||||
* GET /v1/simulator/cfcfrms/bank-performance – Bank performance metrics
|
||||
* </pre>
|
||||
*/
|
||||
@Path("/v1/simulator/cfcfrms")
|
||||
@Component
|
||||
@Tag(name = "CFCFRMS Simulator (I4C/MHA Side)",
|
||||
description = "Simulates the MHA/I4C CFCFRMS central government platform that pushes fraud complaints to banks, "
|
||||
+ "receives status updates, escalates across banks, and queries account/transaction verification. "
|
||||
+ "Use these endpoints to test the full end-to-end complaint lifecycle.")
|
||||
@RequiredArgsConstructor
|
||||
public class CfcfrmsSimulatorApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CfcfrmsSimulatorService simulatorService;
|
||||
private final DefaultToApiJsonSerializer<Map> mapSerializer;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// OUTBOUND (CFCFRMS → Bank)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@POST
|
||||
@Path("/push-complaint")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Push a new fraud complaint to the bank",
|
||||
description = "Simulates I4C pushing a new fraud complaint ticket to the bank. "
|
||||
+ "In the real CFCFRMS workflow, I4C calls the bank's registered webhook. "
|
||||
+ "This endpoint creates a CFCFRMS reference number and internally creates a real complaint ticket "
|
||||
+ "at the bank, demonstrating the end-to-end flow in a single call.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Complaint pushed to bank successfully"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid complaint data")
|
||||
})
|
||||
public String pushComplaint(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CFCFRMS");
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return mapSerializer.serialize(simulatorService.pushComplaint(json));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/push-hold-directive")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Send hold/freeze directive to bank",
|
||||
description = "Simulates I4C sending a direct hold/freeze directive to the bank for a specific account. "
|
||||
+ "Used in urgent cases bypassing the normal ticket flow — e.g. a court-ordered freeze "
|
||||
+ "or suspected mule account flagged by law enforcement. "
|
||||
+ "Directive types: IMMEDIATE_DEBIT_FREEZE, LIEN_MARK, RELEASE. "
|
||||
+ "If expiryHours is set, the hold auto-releases after N hours if no FIR is filed.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Directive sent and acknowledged by bank"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid directive data")
|
||||
})
|
||||
public String pushHoldDirective(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CFCFRMS");
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return mapSerializer.serialize(simulatorService.pushHoldDirective(json));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/push-bulk-complaints")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Push multiple complaints in one call",
|
||||
description = "Simulates I4C pushing multiple fraud complaint tickets to the bank in a single batch call. "
|
||||
+ "Typical use case: after a mass phishing campaign or SIM swap fraud ring is detected. "
|
||||
+ "Maximum 10 complaints per call. Each complaint is processed independently — "
|
||||
+ "partial failures are reported individually.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Bulk push results"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid request")
|
||||
})
|
||||
public String pushBulkComplaints(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CFCFRMS");
|
||||
final JsonArray complaints = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonArray();
|
||||
return mapSerializer.serialize(simulatorService.pushBulkComplaints(complaints));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// INBOUND (Bank → CFCFRMS)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@POST
|
||||
@Path("/receive-bank-update")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Receive status update from the bank",
|
||||
description = "Simulates the CFCFRMS endpoint that receives status updates FROM the bank. "
|
||||
+ "In the real world, after taking action (lien, freeze, etc.), the bank calls I4C's API "
|
||||
+ "to report what was done. This endpoint acknowledges the update and returns the next "
|
||||
+ "recommended action (AWAIT_POLICE_ORDER, ESCALATE_TO_NEXT_BANK, or CLOSE_TICKET).",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Update acknowledged"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid update data")
|
||||
})
|
||||
public String receiveBankUpdate(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("UPDATE_CFCFRMS");
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return mapSerializer.serialize(simulatorService.receiveBankUpdate(json));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/receive-escalation")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Receive escalation from the bank",
|
||||
description = "Simulates CFCFRMS receiving an escalation notice from the bank when the fraud funds "
|
||||
+ "have already moved to the next bank. CFCFRMS generates an escalation reference, "
|
||||
+ "forwards the complaint to the next bank in the chain, and returns confirmation. "
|
||||
+ "The fundsMovedTo object contains the target account, bank IFSC, transfer ref, and amount.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Escalation accepted"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid escalation data")
|
||||
})
|
||||
public String receiveEscalation(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("ESCALATE_CFCFRMS");
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return mapSerializer.serialize(simulatorService.receiveEscalation(json));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// VERIFICATION (CFCFRMS → Bank Query)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@POST
|
||||
@Path("/verify-account")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Verify account at the bank",
|
||||
description = "CFCFRMS queries the bank to verify if a suspect account exists and get basic details. "
|
||||
+ "This is done before pushing a complaint ticket to ensure the account is routed to the correct bank. "
|
||||
+ "Returns account status (ACTIVE/DORMANT/FROZEN/CLOSED), KYC status, "
|
||||
+ "and whether the account is already flagged in an existing complaint.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Account verification result")
|
||||
})
|
||||
public String verifyAccount(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return mapSerializer.serialize(simulatorService.verifyAccount(json));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/verify-transaction")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Verify transaction at the bank",
|
||||
description = "CFCFRMS queries the bank to validate a reported fraud transaction reference. "
|
||||
+ "Returns whether the transaction was found, its status (COMPLETED/PENDING/REVERSED), "
|
||||
+ "channel (UPI/IMPS/NEFT/RTGS), and whether funds are currently available for hold.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Transaction verification result")
|
||||
})
|
||||
public String verifyTransaction(final String apiRequestBodyAsJson) {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
final JsonObject json = JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||
return mapSerializer.serialize(simulatorService.verifyTransaction(json));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// STATUS & REPORTING
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@GET
|
||||
@Path("/complaint-status/{cfcfrmsRef}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Check complaint status from CFCFRMS side",
|
||||
description = "Check the current status of a complaint from the CFCFRMS/I4C perspective. "
|
||||
+ "Includes which bank currently holds the complaint, what action was taken, "
|
||||
+ "funds held, escalation count, and the full timeline of events.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Complaint status"),
|
||||
@ApiResponse(responseCode = "404", description = "CFCFRMS reference not found")
|
||||
})
|
||||
public String getComplaintStatus(
|
||||
@PathParam("cfcfrmsRef") @Parameter(description = "CFCFRMS reference number") final String cfcfrmsRef) {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
return mapSerializer.serialize(simulatorService.getComplaintStatus(cfcfrmsRef));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/bank-performance")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Get bank performance metrics",
|
||||
description = "CFCFRMS-side view of the bank's performance in handling fraud complaints. "
|
||||
+ "Shows total complaints received, response time distribution, action counts, "
|
||||
+ "resolution rate, and TAT compliance rate — used by I4C to evaluate bank responsiveness.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "Bank performance metrics")
|
||||
})
|
||||
public String getBankPerformance() {
|
||||
this.context.authenticatedUser().validateHasReadPermission("CFCFRMS");
|
||||
return mapSerializer.serialize(simulatorService.getBankPerformance());
|
||||
}
|
||||
}
|
||||
+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.cfcfrms.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class ComplaintStatsData {
|
||||
|
||||
private final long totalComplaints;
|
||||
private final long pendingReview;
|
||||
private final long lienMarked;
|
||||
private final long debitFrozen;
|
||||
private final long fundsRecovered;
|
||||
private final BigDecimal totalFraudAmount;
|
||||
private final BigDecimal totalFundsHeld;
|
||||
private final String resolutionRate;
|
||||
|
||||
public ComplaintStatsData(final long totalComplaints, final long pendingReview, final long lienMarked,
|
||||
final long debitFrozen, final long fundsRecovered, final BigDecimal totalFraudAmount,
|
||||
final BigDecimal totalFundsHeld, final String resolutionRate) {
|
||||
this.totalComplaints = totalComplaints;
|
||||
this.pendingReview = pendingReview;
|
||||
this.lienMarked = lienMarked;
|
||||
this.debitFrozen = debitFrozen;
|
||||
this.fundsRecovered = fundsRecovered;
|
||||
this.totalFraudAmount = totalFraudAmount;
|
||||
this.totalFundsHeld = totalFundsHeld;
|
||||
this.resolutionRate = resolutionRate;
|
||||
}
|
||||
|
||||
public long getTotalComplaints() { return totalComplaints; }
|
||||
public long getPendingReview() { return pendingReview; }
|
||||
public long getLienMarked() { return lienMarked; }
|
||||
public long getDebitFrozen() { return debitFrozen; }
|
||||
public long getFundsRecovered() { return fundsRecovered; }
|
||||
public BigDecimal getTotalFraudAmount() { return totalFraudAmount; }
|
||||
public BigDecimal getTotalFundsHeld() { return totalFundsHeld; }
|
||||
public String getResolutionRate() { return resolutionRate; }
|
||||
}
|
||||
+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.cfcfrms.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicket;
|
||||
|
||||
public final class ComplaintTicketData {
|
||||
|
||||
private final String ticketId;
|
||||
private final String complainantName;
|
||||
private final String complainantMobile;
|
||||
private final BigDecimal fraudAmount;
|
||||
private final String transactionRefId;
|
||||
private final String fraudAccountNumber;
|
||||
private final String fraudBankIfsc;
|
||||
private final String victimAccountNumber;
|
||||
private final String victimBankIfsc;
|
||||
private final String upiId;
|
||||
private final String complaintSource;
|
||||
private final String status;
|
||||
private final String actionTaken;
|
||||
private final BigDecimal availableBalance;
|
||||
private final BigDecimal fundsHeld;
|
||||
private final String nextHopAccountNumber;
|
||||
private final String nextHopBankIfsc;
|
||||
private final OffsetDateTime receivedAt;
|
||||
private final OffsetDateTime resolvedAt;
|
||||
private final String assignedOfficer;
|
||||
private final String statePoliceRef;
|
||||
|
||||
private ComplaintTicketData(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 String complaintSource, final String status, final String actionTaken,
|
||||
final BigDecimal availableBalance, final BigDecimal fundsHeld, final String nextHopAccountNumber,
|
||||
final String nextHopBankIfsc, final OffsetDateTime receivedAt, final OffsetDateTime resolvedAt,
|
||||
final String assignedOfficer, final String statePoliceRef) {
|
||||
this.ticketId = ticketId;
|
||||
this.complainantName = complainantName;
|
||||
this.complainantMobile = complainantMobile;
|
||||
this.fraudAmount = fraudAmount;
|
||||
this.transactionRefId = transactionRefId;
|
||||
this.fraudAccountNumber = fraudAccountNumber;
|
||||
this.fraudBankIfsc = fraudBankIfsc;
|
||||
this.victimAccountNumber = victimAccountNumber;
|
||||
this.victimBankIfsc = victimBankIfsc;
|
||||
this.upiId = upiId;
|
||||
this.complaintSource = complaintSource;
|
||||
this.status = status;
|
||||
this.actionTaken = actionTaken;
|
||||
this.availableBalance = availableBalance;
|
||||
this.fundsHeld = fundsHeld;
|
||||
this.nextHopAccountNumber = nextHopAccountNumber;
|
||||
this.nextHopBankIfsc = nextHopBankIfsc;
|
||||
this.receivedAt = receivedAt;
|
||||
this.resolvedAt = resolvedAt;
|
||||
this.assignedOfficer = assignedOfficer;
|
||||
this.statePoliceRef = statePoliceRef;
|
||||
}
|
||||
|
||||
public static ComplaintTicketData fromEntity(final ComplaintTicket t) {
|
||||
return new ComplaintTicketData(
|
||||
t.getTicketId(), t.getComplainantName(), t.getComplainantMobile(),
|
||||
t.getFraudAmount(), t.getTransactionRefId(), t.getFraudAccountNumber(),
|
||||
t.getFraudBankIfsc(), t.getVictimAccountNumber(), t.getVictimBankIfsc(),
|
||||
t.getUpiId(), t.getComplaintSource().getValue(), t.getStatus().getValue(),
|
||||
t.getActionTaken(), t.getAvailableBalance(), t.getFundsHeld(),
|
||||
t.getNextHopAccountNumber(), t.getNextHopBankIfsc(), t.getReceivedAt(),
|
||||
t.getResolvedAt(), t.getAssignedOfficer(), t.getStatePoliceRef());
|
||||
}
|
||||
|
||||
// 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 String getComplaintSource() { return complaintSource; }
|
||||
public String 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; }
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintAuditLog;
|
||||
|
||||
public final class TimelineEventData {
|
||||
|
||||
private final String event;
|
||||
private final String description;
|
||||
private final OffsetDateTime timestamp;
|
||||
private final String actor;
|
||||
|
||||
public TimelineEventData(final String event, final String description, final OffsetDateTime timestamp,
|
||||
final String actor) {
|
||||
this.event = event;
|
||||
this.description = description;
|
||||
this.timestamp = timestamp;
|
||||
this.actor = actor;
|
||||
}
|
||||
|
||||
public static TimelineEventData fromEntity(final ComplaintAuditLog log) {
|
||||
return new TimelineEventData(log.getEvent(), log.getDescription(), log.getEventTimestamp(), log.getActor());
|
||||
}
|
||||
|
||||
public String getEvent() { return event; }
|
||||
public String getDescription() { return description; }
|
||||
public OffsetDateTime getTimestamp() { return timestamp; }
|
||||
public String getActor() { return actor; }
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 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.seed;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.CfcfrmsSimulatorLog;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.CfcfrmsSimulatorLogRepository;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintAuditLog;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintAuditLogRepository;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintSource;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintStatus;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicket;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicketRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Seeds CFCFRMS complaint tickets, audit logs, and simulator logs on startup.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CfcfrmsDataSeeder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CfcfrmsDataSeeder.class);
|
||||
|
||||
private final ComplaintTicketRepository ticketRepository;
|
||||
private final ComplaintAuditLogRepository auditLogRepository;
|
||||
private final CfcfrmsSimulatorLogRepository simulatorLogRepository;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Transactional
|
||||
public void seed() {
|
||||
if (ticketRepository.count() > 0) {
|
||||
LOG.info("CFCFRMS data already seeded, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Seeding CFCFRMS complaint data...");
|
||||
final OffsetDateTime now = OffsetDateTime.now();
|
||||
|
||||
// ─── 3 RECEIVED ──────────────────────────────────────────────
|
||||
seedTicket("TKT-20250318-001", "Rajesh Kumar Sharma", "9876543210",
|
||||
new BigDecimal("45000.00"), "UPI/402987654321/0318",
|
||||
"50100234567890", "HDFC0001234", "10234567890123", "SBIN0005678",
|
||||
"fraud123@ybl", ComplaintSource.HELPLINE_1930, ComplaintStatus.RECEIVED,
|
||||
null, null, null, null, null,
|
||||
"Inspector Meena Kumari", "KA/BLR/CC/2025/1001",
|
||||
now.minusHours(2));
|
||||
|
||||
seedTicket("TKT-20250318-002", "Priya Venkatesh", "8765432109",
|
||||
new BigDecimal("125000.00"), "NEFT/SBIN0001234/2025031800123",
|
||||
"91234000567890", "ICIC0004567", "30456789012345", "PUNB0123400",
|
||||
null, ComplaintSource.NCRP_PORTAL, ComplaintStatus.RECEIVED,
|
||||
null, null, null, null, null,
|
||||
"Sub-Inspector Rakesh Yadav", "TN/CHN/CC/2025/2045",
|
||||
now.minusHours(1));
|
||||
|
||||
seedTicket("TKT-20250317-003", "Mohammad Asif Khan", "7654321098",
|
||||
new BigDecimal("8500.00"), "UPI/709876543210/0317",
|
||||
"60234567890123", "BARB0DELAVI", "20567890123456", "CNRB0001234",
|
||||
"scammer99@paytm", ComplaintSource.DIRECT_BANK, ComplaintStatus.RECEIVED,
|
||||
null, null, null, null, null,
|
||||
"Inspector Deepak Tiwari", "UP/LKO/CC/2025/3078",
|
||||
now.minusDays(1));
|
||||
|
||||
// ─── 3 LIEN_MARKED ───────────────────────────────────────────
|
||||
seedTicket("TKT-20250315-004", "Sunita Devi Agarwal", "9988776655",
|
||||
new BigDecimal("250000.00"), "RTGS/HDFC0001234/2025031500456",
|
||||
"40200987654321", "HDFC0001234", "50100123456789", "HDFC0009876",
|
||||
null, ComplaintSource.HELPLINE_1930, ComplaintStatus.LIEN_MARKED,
|
||||
"Lien of Rs.2,50,000 marked on account 40200987654321",
|
||||
new BigDecimal("312000.00"), new BigDecimal("250000.00"), null, null,
|
||||
"Inspector Anita Sharma", "MH/MUM/CC/2025/4012",
|
||||
now.minusDays(3));
|
||||
|
||||
seedTicket("TKT-20250314-005", "Vikram Singh Rathore", "9123456780",
|
||||
new BigDecimal("500000.00"), "NEFT/ICIC0006789/2025031400789",
|
||||
"70345678901234", "ICIC0006789", "80456789012345", "UTIB0002345",
|
||||
null, ComplaintSource.NCRP_PORTAL, ComplaintStatus.LIEN_MARKED,
|
||||
"Lien marked; suspect account flagged for monitoring",
|
||||
new BigDecimal("523000.00"), new BigDecimal("500000.00"), null, null,
|
||||
"DSP Ramesh Chandra", "RJ/JPR/CC/2025/5034",
|
||||
now.minusDays(4));
|
||||
|
||||
seedTicket("TKT-20250313-006", "Lakshmi Narayanan", "8899776655",
|
||||
new BigDecimal("75000.00"), "UPI/812345678901/0313",
|
||||
"10567890123456", "SBIN0012345", "20678901234567", "IOBA0001234",
|
||||
"fake.merchant@okaxis", ComplaintSource.HELPLINE_1930, ComplaintStatus.LIEN_MARKED,
|
||||
"Lien of Rs.75,000 marked; UPI ID reported to NPCI",
|
||||
new BigDecimal("78500.00"), new BigDecimal("75000.00"), null, null,
|
||||
"Inspector Kavitha Reddy", "AP/HYD/CC/2025/6056",
|
||||
now.minusDays(5));
|
||||
|
||||
// ─── 2 DEBIT_FROZEN ──────────────────────────────────────────
|
||||
seedTicket("TKT-20250310-007", "Amit Patel", "9234567890",
|
||||
new BigDecimal("180000.00"), "IMPS/934567890123/0310",
|
||||
"30789012345678", "PUNB0567800", "40890123456789", "BKID0001234",
|
||||
null, ComplaintSource.NCRP_PORTAL, ComplaintStatus.DEBIT_FROZEN,
|
||||
"Account debit frozen by branch manager; suspect notified under RBI guidelines",
|
||||
new BigDecimal("195000.00"), new BigDecimal("180000.00"), null, null,
|
||||
"Inspector Suresh Babu", "GJ/AMD/CC/2025/7078",
|
||||
now.minusDays(8));
|
||||
|
||||
seedTicket("TKT-20250308-008", "Neha Gupta", "8345678901",
|
||||
new BigDecimal("95000.00"), "UPI/345678901234/0308",
|
||||
"50901234567890", "KKBK0000123", "60012345678901", "YESB0001234",
|
||||
"invest.profit@axl", ComplaintSource.HELPLINE_1930, ComplaintStatus.DEBIT_FROZEN,
|
||||
"Debit freeze applied; investment fraud confirmed by cyber cell",
|
||||
new BigDecimal("102000.00"), new BigDecimal("95000.00"), null, null,
|
||||
"SI Manish Kumar", "DL/NDL/CC/2025/8090",
|
||||
now.minusDays(10));
|
||||
|
||||
// ─── 2 FUNDS_MOVED ───────────────────────────────────────────
|
||||
seedTicket("TKT-20250305-009", "Sanjay Mehta", "7456789012",
|
||||
new BigDecimal("320000.00"), "RTGS/UTIB0002345/2025030500321",
|
||||
"70123456789012", "UTIB0002345", "80234567890123", "MAHB0001234",
|
||||
null, ComplaintSource.NCRP_PORTAL, ComplaintStatus.FUNDS_MOVED,
|
||||
"Funds traced to ICICI account; lien could not hold",
|
||||
new BigDecimal("1200.00"), BigDecimal.ZERO,
|
||||
"90345678901234", "ICIC0009876",
|
||||
"DSP Prabhakar Rao", "KA/MYS/CC/2025/9112",
|
||||
now.minusDays(13));
|
||||
|
||||
seedTicket("TKT-20250303-010", "Aarti Joshi", "6567890123",
|
||||
new BigDecimal("47500.00"), "UPI/567890123456/0303",
|
||||
"10456789012345", "SBIN0034567", "20567890123456", "CNRB0005678",
|
||||
"reward.claim@ibl", ComplaintSource.HELPLINE_1930, ComplaintStatus.FUNDS_MOVED,
|
||||
"Funds moved to Paytm Payments Bank; UPI trail identified",
|
||||
new BigDecimal("350.00"), BigDecimal.ZERO,
|
||||
"91567890123456", "PYTM0123456",
|
||||
"Inspector Geeta Mishra", "MP/BPL/CC/2025/1134",
|
||||
now.minusDays(15));
|
||||
|
||||
// ─── 2 RESOLVED ─────────────────────────────────────────────
|
||||
seedTicket("TKT-20250228-011", "Ravi Shankar Dubey", "9678901234",
|
||||
new BigDecimal("65000.00"), "IMPS/678901234567/0228",
|
||||
"30678901234567", "BARB0MUMBAI", "40789012345678", "UBIN0123456",
|
||||
null, ComplaintSource.NCRP_PORTAL, ComplaintStatus.RESOLVED,
|
||||
"Full amount recovered and refunded to victim. FIR filed, suspect arrested.",
|
||||
new BigDecimal("65000.00"), new BigDecimal("65000.00"), null, null,
|
||||
"Inspector Vijay Patil", "MH/PUN/CC/2025/1256",
|
||||
now.minusDays(18));
|
||||
|
||||
seedTicket("TKT-20250225-012", "Pooja Krishnamurthy", "8789012345",
|
||||
new BigDecimal("15000.00"), "UPI/789012345678/0225",
|
||||
"50789012345678", "KKBK0004567", "60890123456789", "FDRL0001234",
|
||||
"lottery.win@ptm", ComplaintSource.DIRECT_BANK, ComplaintStatus.RESOLVED,
|
||||
"Amount reversed through UPI dispute resolution. Suspect UPI ID blocked by NPCI.",
|
||||
new BigDecimal("15000.00"), new BigDecimal("15000.00"), null, null,
|
||||
"SI Arjun Nair", "KL/EKM/CC/2025/1378",
|
||||
now.minusDays(21));
|
||||
|
||||
// ─── 1 REJECTED ─────────────────────────────────────────────
|
||||
seedTicket("TKT-20250220-013", "Dinesh Choudhary", "7890123456",
|
||||
new BigDecimal("5000.00"), "UPI/890123456789/0220",
|
||||
"70890123456789", "HDFC0007890", "80901234567890", "SBIN0098765",
|
||||
"merchant.real@upi", ComplaintSource.DIRECT_BANK, ComplaintStatus.REJECTED,
|
||||
"Legitimate transaction. Complainant confirmed genuine purchase by family member.",
|
||||
null, null, null, null,
|
||||
"Inspector Savita Deshpande", "MH/NGP/CC/2025/1490",
|
||||
now.minusDays(26));
|
||||
|
||||
// ─── Simulator Logs (5 historical entries) ──────────────────
|
||||
seedSimulatorLogs(now);
|
||||
|
||||
LOG.info("CFCFRMS data seeded: 13 tickets, audit logs, and 5 simulator log entries.");
|
||||
}
|
||||
|
||||
private void seedTicket(final String ticketId, final String name, final String mobile,
|
||||
final BigDecimal amount, final String txnRef, final String fraudAcct, final String fraudIfsc,
|
||||
final String victimAcct, final String victimIfsc, final String upiId,
|
||||
final ComplaintSource source, final ComplaintStatus status, final String action,
|
||||
final BigDecimal balance, final BigDecimal held, final String nextHopAcct,
|
||||
final String nextHopIfsc, final String officer, final String policeRef,
|
||||
final OffsetDateTime receivedAt) {
|
||||
|
||||
final ComplaintTicket ticket = ComplaintTicket.create(ticketId, name, mobile, amount, txnRef,
|
||||
fraudAcct, fraudIfsc, victimAcct, victimIfsc, upiId, source, officer, policeRef);
|
||||
|
||||
// Apply status changes for non-RECEIVED tickets
|
||||
if (status != ComplaintStatus.RECEIVED) {
|
||||
if (nextHopAcct != null) {
|
||||
ticket.setNextHopAccountNumber(nextHopAcct);
|
||||
ticket.setNextHopBankIfsc(nextHopIfsc);
|
||||
}
|
||||
// Use reflection-free approach: update via the entity method for valid transitions
|
||||
applyStatusPath(ticket, status, action);
|
||||
}
|
||||
if (balance != null) { ticket.setAvailableBalance(balance); }
|
||||
if (held != null) { ticket.setFundsHeld(held); }
|
||||
|
||||
ticketRepository.save(ticket);
|
||||
|
||||
// Audit: RECEIVED
|
||||
auditLogRepository.save(new ComplaintAuditLog(ticketId, "COMPLAINT_RECEIVED",
|
||||
"Complaint received from " + source.getValue() + " for fraud amount Rs." + amount,
|
||||
"CFCFRMS_SYSTEM", receivedAt));
|
||||
|
||||
// Audit: status change
|
||||
if (status != ComplaintStatus.RECEIVED) {
|
||||
auditLogRepository.save(new ComplaintAuditLog(ticketId,
|
||||
"STATUS_CHANGED_TO_" + status.getValue(), action, officer, receivedAt.plusHours(2)));
|
||||
}
|
||||
}
|
||||
|
||||
private void applyStatusPath(final ComplaintTicket ticket, final ComplaintStatus target, final String action) {
|
||||
// Walk through valid transitions to reach target
|
||||
if (target == ComplaintStatus.LIEN_MARKED) {
|
||||
ticket.updateStatus(ComplaintStatus.LIEN_MARKED, action);
|
||||
} else if (target == ComplaintStatus.DEBIT_FROZEN) {
|
||||
ticket.updateStatus(ComplaintStatus.LIEN_MARKED, action);
|
||||
ticket.updateStatus(ComplaintStatus.DEBIT_FROZEN, action);
|
||||
} else if (target == ComplaintStatus.FUNDS_MOVED) {
|
||||
ticket.updateStatus(ComplaintStatus.LIEN_MARKED, action);
|
||||
ticket.updateStatus(ComplaintStatus.FUNDS_MOVED, action);
|
||||
} else if (target == ComplaintStatus.RESOLVED) {
|
||||
ticket.updateStatus(ComplaintStatus.LIEN_MARKED, action);
|
||||
ticket.updateStatus(ComplaintStatus.RESOLVED, action);
|
||||
} else if (target == ComplaintStatus.REJECTED) {
|
||||
ticket.updateStatus(ComplaintStatus.REJECTED, action);
|
||||
}
|
||||
}
|
||||
|
||||
private void seedSimulatorLogs(final OffsetDateTime now) {
|
||||
simulatorLogRepository.saveAll(List.of(
|
||||
new CfcfrmsSimulatorLog("CFCFRMS-2025-0315-4012", "TKT-20250315-004", "HDFC0001234",
|
||||
"OUTBOUND", "PUSH_COMPLAINT",
|
||||
"{\"complainantName\":\"Sunita Devi Agarwal\",\"fraudAmount\":250000}",
|
||||
"{\"ticketId\":\"TKT-20250315-004\",\"status\":\"RECEIVED\"}",
|
||||
now.minusDays(3)),
|
||||
|
||||
new CfcfrmsSimulatorLog("CFCFRMS-2025-0315-4012", "TKT-20250315-004", "HDFC0001234",
|
||||
"INBOUND", "BANK_UPDATE_LIEN_MARKED",
|
||||
"{\"actionTaken\":\"LIEN_MARKED\",\"fundsHeld\":250000}",
|
||||
"{\"nextAction\":\"AWAIT_POLICE_ORDER\"}",
|
||||
now.minusDays(2)),
|
||||
|
||||
new CfcfrmsSimulatorLog("CFCFRMS-2025-0305-9112", "TKT-20250305-009", "UTIB0002345",
|
||||
"OUTBOUND", "PUSH_COMPLAINT",
|
||||
"{\"complainantName\":\"Sanjay Mehta\",\"fraudAmount\":320000}",
|
||||
"{\"ticketId\":\"TKT-20250305-009\",\"status\":\"RECEIVED\"}",
|
||||
now.minusDays(13)),
|
||||
|
||||
new CfcfrmsSimulatorLog("CFCFRMS-2025-0305-9112", "TKT-20250305-009", "UTIB0002345",
|
||||
"INBOUND", "ESCALATION_RECEIVED",
|
||||
"{\"fundsMovedTo\":{\"bankIfsc\":\"ICIC0009876\",\"amount\":320000}}",
|
||||
"{\"escalationRef\":\"ESC-2025-0305-7712\",\"escalatedTo\":\"ICIC0009876\"}",
|
||||
now.minusDays(10)),
|
||||
|
||||
new CfcfrmsSimulatorLog("CFCFRMS-2025-0228-1256", "TKT-20250228-011", "BARB0MUMBAI",
|
||||
"INBOUND", "BANK_UPDATE_RESOLVED",
|
||||
"{\"actionTaken\":\"RESOLVED\",\"fundsHeld\":65000}",
|
||||
"{\"nextAction\":\"CLOSE_TICKET\"}",
|
||||
now.minusDays(5))
|
||||
));
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintStatsData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintTicketData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.TimelineEventData;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
public interface CfcfrmsComplaintService {
|
||||
|
||||
/** Create a new complaint ticket (bank-side). Returns created ticket data. */
|
||||
ComplaintTicketData createComplaint(JsonObject requestBody);
|
||||
|
||||
/** List complaint tickets with optional filters. */
|
||||
Page<ComplaintTicketData> listComplaints(String status, String fromDate, String toDate, Pageable pageable);
|
||||
|
||||
/** Get single complaint by ticketId. */
|
||||
ComplaintTicketData getComplaint(String ticketId);
|
||||
|
||||
/** Bank takes action on a ticket (lien, freeze, funds_moved, resolve, reject). */
|
||||
ComplaintTicketData takeAction(String ticketId, JsonObject requestBody);
|
||||
|
||||
/** Dashboard statistics. */
|
||||
ComplaintStatsData getStats();
|
||||
|
||||
/** Audit timeline for a ticket. */
|
||||
List<TimelineEventData> getTimeline(String ticketId);
|
||||
|
||||
/** Escalate a ticket to the next bank. Returns escalation result map. */
|
||||
Map<String, Object> escalate(String ticketId, JsonObject requestBody);
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintStatsData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintTicketData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.TimelineEventData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintAuditLog;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintAuditLogRepository;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintSource;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintStatus;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicket;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicketRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class CfcfrmsComplaintServiceImpl implements CfcfrmsComplaintService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CfcfrmsComplaintServiceImpl.class);
|
||||
|
||||
private final ComplaintTicketRepository ticketRepository;
|
||||
private final ComplaintAuditLogRepository auditLogRepository;
|
||||
|
||||
@Override
|
||||
public ComplaintTicketData createComplaint(final JsonObject json) {
|
||||
final String ticketId = generateTicketId();
|
||||
|
||||
if (ticketRepository.existsByTicketId(ticketId)) {
|
||||
throw new IllegalArgumentException("Ticket already exists: " + ticketId);
|
||||
}
|
||||
|
||||
final ComplaintTicket ticket = ComplaintTicket.create(
|
||||
ticketId,
|
||||
getStringOrThrow(json, "complainantName"),
|
||||
getStringOrThrow(json, "complainantMobile"),
|
||||
getBigDecimalOrThrow(json, "fraudAmount"),
|
||||
getStringOrThrow(json, "transactionRefId"),
|
||||
getStringOrThrow(json, "fraudAccountNumber"),
|
||||
getStringOrThrow(json, "fraudBankIfsc"),
|
||||
getStringOrThrow(json, "victimAccountNumber"),
|
||||
getStringOrThrow(json, "victimBankIfsc"),
|
||||
getStringOrNull(json, "upiId"),
|
||||
ComplaintSource.fromString(getStringOrThrow(json, "complaintSource")),
|
||||
getStringOrThrow(json, "assignedOfficer"),
|
||||
getStringOrNull(json, "statePoliceRef"));
|
||||
|
||||
ticketRepository.saveAndFlush(ticket);
|
||||
|
||||
auditLogRepository.save(new ComplaintAuditLog(ticketId, "COMPLAINT_RECEIVED",
|
||||
"Complaint received from " + ticket.getComplaintSource().getValue()
|
||||
+ " for fraud amount Rs." + ticket.getFraudAmount(),
|
||||
"CFCFRMS_SYSTEM"));
|
||||
|
||||
LOG.info("CFCFRMS complaint ticket created: {}", ticketId);
|
||||
return ComplaintTicketData.fromEntity(ticket);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Page<ComplaintTicketData> listComplaints(final String status, final String fromDate,
|
||||
final String toDate, final Pageable pageable) {
|
||||
Page<ComplaintTicket> page;
|
||||
|
||||
if (status != null && fromDate != null && toDate != null) {
|
||||
page = ticketRepository.findByStatusAndReceivedAtBetween(
|
||||
ComplaintStatus.fromString(status), parseDate(fromDate), parseDate(toDate).plusDays(1), pageable);
|
||||
} else if (status != null) {
|
||||
page = ticketRepository.findByStatus(ComplaintStatus.fromString(status), pageable);
|
||||
} else if (fromDate != null && toDate != null) {
|
||||
page = ticketRepository.findByReceivedAtBetween(parseDate(fromDate), parseDate(toDate).plusDays(1), pageable);
|
||||
} else {
|
||||
page = ticketRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
return page.map(ComplaintTicketData::fromEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ComplaintTicketData getComplaint(final String ticketId) {
|
||||
return ComplaintTicketData.fromEntity(findTicketOrThrow(ticketId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComplaintTicketData takeAction(final String ticketId, final JsonObject json) {
|
||||
final ComplaintTicket ticket = findTicketOrThrow(ticketId);
|
||||
final ComplaintStatus newStatus = ComplaintStatus.fromString(getStringOrThrow(json, "action"));
|
||||
final String actionNote = getStringOrThrow(json, "actionNote");
|
||||
|
||||
if (newStatus == ComplaintStatus.FUNDS_MOVED) {
|
||||
final String nextHop = getStringOrNull(json, "nextHopAccountNumber");
|
||||
final String nextIfsc = getStringOrNull(json, "nextHopBankIfsc");
|
||||
if (nextHop == null || nextHop.isBlank() || nextIfsc == null || nextIfsc.isBlank()) {
|
||||
throw new IllegalArgumentException("nextHopAccountNumber and nextHopBankIfsc are required for FUNDS_MOVED");
|
||||
}
|
||||
ticket.setNextHopAccountNumber(nextHop);
|
||||
ticket.setNextHopBankIfsc(nextIfsc);
|
||||
}
|
||||
|
||||
ticket.updateStatus(newStatus, actionNote);
|
||||
|
||||
if (json.has("availableBalance") && !json.get("availableBalance").isJsonNull()) {
|
||||
ticket.setAvailableBalance(json.get("availableBalance").getAsBigDecimal());
|
||||
}
|
||||
if (json.has("fundsHeld") && !json.get("fundsHeld").isJsonNull()) {
|
||||
ticket.setFundsHeld(json.get("fundsHeld").getAsBigDecimal());
|
||||
}
|
||||
|
||||
ticketRepository.saveAndFlush(ticket);
|
||||
|
||||
auditLogRepository.save(new ComplaintAuditLog(ticketId, "STATUS_CHANGED_TO_" + newStatus.getValue(),
|
||||
actionNote, ticket.getAssignedOfficer()));
|
||||
|
||||
LOG.info("CFCFRMS ticket {} action: -> {}", ticketId, newStatus);
|
||||
return ComplaintTicketData.fromEntity(ticket);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ComplaintStatsData getStats() {
|
||||
final long total = ticketRepository.count();
|
||||
final long pending = ticketRepository.countByStatus(ComplaintStatus.RECEIVED)
|
||||
+ ticketRepository.countByStatus(ComplaintStatus.UNDER_REVIEW);
|
||||
final long lien = ticketRepository.countByStatus(ComplaintStatus.LIEN_MARKED);
|
||||
final long frozen = ticketRepository.countByStatus(ComplaintStatus.DEBIT_FROZEN);
|
||||
final long resolved = ticketRepository.countByStatus(ComplaintStatus.RESOLVED);
|
||||
final BigDecimal totalFraud = ticketRepository.sumTotalFraudAmount();
|
||||
final BigDecimal totalHeld = ticketRepository.sumTotalFundsHeld();
|
||||
|
||||
final String rate = total > 0
|
||||
? BigDecimal.valueOf(resolved * 100.0 / total).setScale(1, RoundingMode.HALF_UP) + "%"
|
||||
: "0.0%";
|
||||
|
||||
return new ComplaintStatsData(total, pending, lien, frozen, resolved, totalFraud, totalHeld, rate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<TimelineEventData> getTimeline(final String ticketId) {
|
||||
findTicketOrThrow(ticketId);
|
||||
return auditLogRepository.findByTicketIdOrderByEventTimestampAsc(ticketId)
|
||||
.stream().map(TimelineEventData::fromEntity).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> escalate(final String ticketId, final JsonObject json) {
|
||||
final ComplaintTicket ticket = findTicketOrThrow(ticketId);
|
||||
|
||||
if (ticket.getStatus() != ComplaintStatus.FUNDS_MOVED && ticket.getStatus() != ComplaintStatus.DEBIT_FROZEN) {
|
||||
throw new IllegalStateException(
|
||||
"Can only escalate tickets in FUNDS_MOVED or DEBIT_FROZEN status. Current: " + ticket.getStatus());
|
||||
}
|
||||
|
||||
final String nextHop = getStringOrThrow(json, "nextHopAccountNumber");
|
||||
final String nextIfsc = getStringOrThrow(json, "nextHopBankIfsc");
|
||||
final String remarks = getStringOrNull(json, "remarks");
|
||||
|
||||
ticket.setNextHopAccountNumber(nextHop);
|
||||
ticket.setNextHopBankIfsc(nextIfsc);
|
||||
ticketRepository.saveAndFlush(ticket);
|
||||
|
||||
final String escalationRef = "ESC-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
|
||||
auditLogRepository.save(new ComplaintAuditLog(ticketId, "ESCALATED",
|
||||
"Escalated to " + nextIfsc + " / " + nextHop + ". Remarks: " + (remarks != null ? remarks : "N/A"),
|
||||
ticket.getAssignedOfficer()));
|
||||
|
||||
LOG.info("CFCFRMS ticket {} escalated to {}", ticketId, nextIfsc);
|
||||
|
||||
final Map<String, Object> result = new HashMap<>();
|
||||
result.put("escalatedTo", nextIfsc);
|
||||
result.put("escalationRef", escalationRef);
|
||||
result.put("timestamp", OffsetDateTime.now().toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private ComplaintTicket findTicketOrThrow(final String ticketId) {
|
||||
return ticketRepository.findByTicketId(ticketId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Complaint ticket not found: " + ticketId));
|
||||
}
|
||||
|
||||
private String generateTicketId() {
|
||||
final String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
final int seq = (int) (ticketRepository.count() + 1);
|
||||
return String.format("TKT-%s-%03d", date, seq);
|
||||
}
|
||||
|
||||
private OffsetDateTime parseDate(final String dateStr) {
|
||||
return LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay().atOffset(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private String getStringOrThrow(final JsonObject json, final String field) {
|
||||
if (!json.has(field) || json.get(field).isJsonNull()) {
|
||||
throw new IllegalArgumentException("Missing required field: " + field);
|
||||
}
|
||||
return json.get(field).getAsString();
|
||||
}
|
||||
|
||||
private String getStringOrNull(final JsonObject json, final String field) {
|
||||
return (json.has(field) && !json.get(field).isJsonNull()) ? json.get(field).getAsString() : null;
|
||||
}
|
||||
|
||||
private BigDecimal getBigDecimalOrThrow(final JsonObject json, final String field) {
|
||||
if (!json.has(field) || json.get(field).isJsonNull()) {
|
||||
throw new IllegalArgumentException("Missing required field: " + field);
|
||||
}
|
||||
return json.get(field).getAsBigDecimal();
|
||||
}
|
||||
}
|
||||
+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.cfcfrms.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.Map;
|
||||
|
||||
public interface CfcfrmsSimulatorService {
|
||||
|
||||
/** Push a single complaint to the bank (CFCFRMS → Bank). */
|
||||
Map<String, Object> pushComplaint(JsonObject requestBody);
|
||||
|
||||
/** Push a hold/freeze directive to the bank. */
|
||||
Map<String, Object> pushHoldDirective(JsonObject requestBody);
|
||||
|
||||
/** Push multiple complaints in one call. */
|
||||
Map<String, Object> pushBulkComplaints(JsonArray complaints);
|
||||
|
||||
/** Receive a bank status update (Bank → CFCFRMS). */
|
||||
Map<String, Object> receiveBankUpdate(JsonObject requestBody);
|
||||
|
||||
/** Receive an escalation from the bank. */
|
||||
Map<String, Object> receiveEscalation(JsonObject requestBody);
|
||||
|
||||
/** Verify an account exists at the bank. */
|
||||
Map<String, Object> verifyAccount(JsonObject requestBody);
|
||||
|
||||
/** Verify a transaction reference. */
|
||||
Map<String, Object> verifyTransaction(JsonObject requestBody);
|
||||
|
||||
/** Get complaint status from CFCFRMS side. */
|
||||
Map<String, Object> getComplaintStatus(String cfcfrmsRef);
|
||||
|
||||
/** Get bank performance metrics. */
|
||||
Map<String, Object> getBankPerformance();
|
||||
}
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.CfcfrmsConstants;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintTicketData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.TimelineEventData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.CfcfrmsSimulatorLog;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.CfcfrmsSimulatorLogRepository;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicket;
|
||||
import org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicketRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CfcfrmsSimulatorServiceImpl.class);
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private final CfcfrmsSimulatorLogRepository simulatorLogRepository;
|
||||
private final ComplaintTicketRepository ticketRepository;
|
||||
private final CfcfrmsComplaintService complaintService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> pushComplaint(final JsonObject requestBody) {
|
||||
final String cfcfrmsRef = generateCfcfrmsRef();
|
||||
|
||||
// Create the real ticket via bank-side service
|
||||
final ComplaintTicketData ticketData = complaintService.createComplaint(requestBody);
|
||||
|
||||
// Log in simulator table
|
||||
simulatorLogRepository.save(new CfcfrmsSimulatorLog(
|
||||
cfcfrmsRef, ticketData.getTicketId(),
|
||||
requestBody.has("fraudBankIfsc") ? requestBody.get("fraudBankIfsc").getAsString() : CfcfrmsConstants.BANK_IFSC,
|
||||
"OUTBOUND", "PUSH_COMPLAINT", requestBody.toString(),
|
||||
"{\"ticketId\":\"" + ticketData.getTicketId() + "\",\"status\":\"RECEIVED\"}"));
|
||||
|
||||
LOG.info("CFCFRMS Simulator: Pushed complaint {} -> ticket {}", cfcfrmsRef, ticketData.getTicketId());
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("cfcfrmsRef", cfcfrmsRef);
|
||||
response.put("ticketId", ticketData.getTicketId());
|
||||
response.put("pushedToBankAt", OffsetDateTime.now().toString());
|
||||
response.put("bankAcknowledged", true);
|
||||
response.put("message", "Complaint pushed to " + CfcfrmsConstants.BANK_IFSC + " successfully");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> pushHoldDirective(final JsonObject json) {
|
||||
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
|
||||
final String directiveRef = "DIR-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
||||
+ "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
|
||||
final String directiveType = getStringOrThrow(json, "directiveType");
|
||||
final String targetAccount = getStringOrThrow(json, "targetAccountNumber");
|
||||
|
||||
// Simulate bank acknowledgement
|
||||
String bankResponse = "ACKNOWLEDGED";
|
||||
|
||||
// Check if account exists in our complaints
|
||||
final List<ComplaintTicket> related = ticketRepository.findByFraudAccountNumber(targetAccount);
|
||||
if (related.isEmpty()) {
|
||||
bankResponse = "ACKNOWLEDGED"; // Still acknowledge in simulation
|
||||
}
|
||||
|
||||
simulatorLogRepository.save(new CfcfrmsSimulatorLog(
|
||||
cfcfrmsRef, null,
|
||||
json.has("targetBankIfsc") ? json.get("targetBankIfsc").getAsString() : CfcfrmsConstants.BANK_IFSC,
|
||||
"OUTBOUND", "HOLD_DIRECTIVE_" + directiveType, json.toString(),
|
||||
"{\"directiveRef\":\"" + directiveRef + "\",\"bankResponse\":\"" + bankResponse + "\"}"));
|
||||
|
||||
LOG.info("CFCFRMS Simulator: Hold directive {} type={} -> {}", directiveRef, directiveType, bankResponse);
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("directiveRef", directiveRef);
|
||||
response.put("status", "DIRECTIVE_SENT");
|
||||
response.put("bankResponse", bankResponse);
|
||||
response.put("timestamp", OffsetDateTime.now().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> pushBulkComplaints(final JsonArray complaints) {
|
||||
if (complaints.size() > 10) {
|
||||
throw new IllegalArgumentException("Maximum 10 complaints per bulk call");
|
||||
}
|
||||
|
||||
final List<Map<String, Object>> results = new ArrayList<>();
|
||||
int successful = 0;
|
||||
int failed = 0;
|
||||
|
||||
for (int i = 0; i < complaints.size(); i++) {
|
||||
final JsonObject complaint = complaints.get(i).getAsJsonObject();
|
||||
try {
|
||||
final Map<String, Object> pushResult = pushComplaint(complaint);
|
||||
final Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("cfcfrmsRef", pushResult.get("cfcfrmsRef"));
|
||||
entry.put("ticketId", pushResult.get("ticketId"));
|
||||
entry.put("status", "PUSHED");
|
||||
results.add(entry);
|
||||
successful++;
|
||||
} catch (Exception e) {
|
||||
final Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("cfcfrmsRef", generateCfcfrmsRef());
|
||||
entry.put("error", e.getMessage());
|
||||
entry.put("status", "FAILED");
|
||||
results.add(entry);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("totalPushed", complaints.size());
|
||||
response.put("successful", successful);
|
||||
response.put("failed", failed);
|
||||
response.put("results", results);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> receiveBankUpdate(final JsonObject json) {
|
||||
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
|
||||
final String ticketId = getStringOrThrow(json, "ticketId");
|
||||
final String actionTaken = getStringOrThrow(json, "actionTaken");
|
||||
|
||||
simulatorLogRepository.save(new CfcfrmsSimulatorLog(
|
||||
cfcfrmsRef, ticketId,
|
||||
json.has("bankIfsc") ? json.get("bankIfsc").getAsString() : CfcfrmsConstants.BANK_IFSC,
|
||||
"INBOUND", "BANK_UPDATE_" + actionTaken, json.toString(), null));
|
||||
|
||||
// Determine next action
|
||||
String nextAction;
|
||||
String message;
|
||||
switch (actionTaken) {
|
||||
case "LIEN_MARKED":
|
||||
nextAction = "AWAIT_POLICE_ORDER";
|
||||
message = "Update received. Lien confirmed. Awaiting police order for further action.";
|
||||
break;
|
||||
case "DEBIT_FROZEN":
|
||||
nextAction = "AWAIT_POLICE_ORDER";
|
||||
message = "Update received. Debit freeze confirmed. Awaiting FIR and court order.";
|
||||
break;
|
||||
case "FUNDS_MOVED":
|
||||
nextAction = "ESCALATE_TO_NEXT_BANK";
|
||||
message = "Update received. Funds moved. Escalation to next bank initiated.";
|
||||
break;
|
||||
case "RESOLVED":
|
||||
nextAction = "CLOSE_TICKET";
|
||||
message = "Update received. Complaint resolved. Ticket will be closed.";
|
||||
break;
|
||||
case "REJECTED":
|
||||
nextAction = "CLOSE_TICKET";
|
||||
message = "Update received. Complaint rejected by bank.";
|
||||
break;
|
||||
default:
|
||||
nextAction = "AWAIT_POLICE_ORDER";
|
||||
message = "Update received.";
|
||||
}
|
||||
|
||||
LOG.info("CFCFRMS Simulator: Received bank update for {} -> {}", cfcfrmsRef, actionTaken);
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("cfcfrmsRef", cfcfrmsRef);
|
||||
response.put("updateAcknowledged", true);
|
||||
response.put("nextAction", nextAction);
|
||||
response.put("message", message);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> receiveEscalation(final JsonObject json) {
|
||||
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
|
||||
final String ticketId = getStringOrThrow(json, "ticketId");
|
||||
final String escalationRef = "ESC-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MMdd"))
|
||||
+ "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
|
||||
// Extract nextBank from fundsMovedTo
|
||||
String nextBankIfsc = "";
|
||||
String nextBankName = "";
|
||||
if (json.has("fundsMovedTo") && !json.get("fundsMovedTo").isJsonNull()) {
|
||||
final JsonObject fundsMovedTo = json.getAsJsonObject("fundsMovedTo");
|
||||
nextBankIfsc = fundsMovedTo.has("bankIfsc") ? fundsMovedTo.get("bankIfsc").getAsString() : "";
|
||||
nextBankName = fundsMovedTo.has("bankName") ? fundsMovedTo.get("bankName").getAsString() : "";
|
||||
}
|
||||
|
||||
simulatorLogRepository.save(new CfcfrmsSimulatorLog(
|
||||
cfcfrmsRef, ticketId,
|
||||
json.has("fromBankIfsc") ? json.get("fromBankIfsc").getAsString() : CfcfrmsConstants.BANK_IFSC,
|
||||
"INBOUND", "ESCALATION_RECEIVED", json.toString(),
|
||||
"{\"escalationRef\":\"" + escalationRef + "\",\"escalatedTo\":\"" + nextBankIfsc + "\"}"));
|
||||
|
||||
LOG.info("CFCFRMS Simulator: Escalation received for {} -> {}", cfcfrmsRef, nextBankIfsc);
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("cfcfrmsRef", cfcfrmsRef);
|
||||
response.put("escalationRef", escalationRef);
|
||||
response.put("escalatedTo", nextBankIfsc);
|
||||
response.put("status", "ESCALATION_ACCEPTED");
|
||||
response.put("message", "Complaint escalated to " + nextBankName + ". Ticket forwarded.");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> verifyAccount(final JsonObject json) {
|
||||
final String accountNumber = getStringOrThrow(json, "accountNumber");
|
||||
|
||||
// Simulate verification — check if account appears in any complaint
|
||||
final List<ComplaintTicket> tickets = ticketRepository.findByFraudAccountNumber(accountNumber);
|
||||
final boolean exists = !tickets.isEmpty() || RANDOM.nextBoolean(); // simulate for unknown accounts
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("accountExists", exists);
|
||||
response.put("accountStatus", exists ? "ACTIVE" : "NOT_FOUND");
|
||||
response.put("accountType", "SAVINGS");
|
||||
response.put("linkedMobile", "98XXXXX210");
|
||||
response.put("kycStatus", "FULL_KYC");
|
||||
response.put("isAlreadyFlagged", !tickets.isEmpty());
|
||||
response.put("existingTickets", tickets.size());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> verifyTransaction(final JsonObject json) {
|
||||
final String transactionRefId = getStringOrThrow(json, "transactionRefId");
|
||||
final BigDecimal amount = json.has("amount") && !json.get("amount").isJsonNull()
|
||||
? json.get("amount").getAsBigDecimal() : BigDecimal.ZERO;
|
||||
|
||||
// Simulate transaction verification
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("transactionFound", true);
|
||||
response.put("transactionStatus", "COMPLETED");
|
||||
response.put("transactionTimestamp", OffsetDateTime.now().minusHours(2).toString());
|
||||
response.put("senderAccount", json.has("accountNumber") ? json.get("accountNumber").getAsString() : "UNKNOWN");
|
||||
response.put("receiverAccount", json.has("accountNumber") ? json.get("accountNumber").getAsString() : "UNKNOWN");
|
||||
|
||||
// Determine channel from ref
|
||||
String channel = "UPI";
|
||||
if (transactionRefId.contains("NEFT")) {
|
||||
channel = "NEFT";
|
||||
} else if (transactionRefId.contains("RTGS")) {
|
||||
channel = "RTGS";
|
||||
} else if (transactionRefId.contains("IMPS")) {
|
||||
channel = "IMPS";
|
||||
}
|
||||
response.put("channel", channel);
|
||||
response.put("amountMatches", true);
|
||||
response.put("fundsCurrentlyAvailable", true);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getComplaintStatus(final String cfcfrmsRef) {
|
||||
final CfcfrmsSimulatorLog log = simulatorLogRepository.findFirstByCfcfrmsRefOrderByEventTimestampDesc(cfcfrmsRef)
|
||||
.orElseThrow(() -> new IllegalArgumentException("CFCFRMS reference not found: " + cfcfrmsRef));
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("cfcfrmsRef", cfcfrmsRef);
|
||||
response.put("ticketId", log.getTicketId());
|
||||
response.put("currentBank", log.getBankIfsc());
|
||||
|
||||
// Get complaint status from bank-side
|
||||
String complaintStatus = "UNKNOWN";
|
||||
BigDecimal fundsHeld = BigDecimal.ZERO;
|
||||
List<TimelineEventData> timeline = List.of();
|
||||
|
||||
if (log.getTicketId() != null) {
|
||||
try {
|
||||
final ComplaintTicketData ticket = complaintService.getComplaint(log.getTicketId());
|
||||
complaintStatus = ticket.getStatus();
|
||||
fundsHeld = ticket.getFundsHeld() != null ? ticket.getFundsHeld() : BigDecimal.ZERO;
|
||||
timeline = complaintService.getTimeline(log.getTicketId());
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not fetch bank-side ticket for {}: {}", log.getTicketId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
response.put("complaintStatus", complaintStatus);
|
||||
response.put("lastUpdatedBy", CfcfrmsConstants.BANK_NAME);
|
||||
response.put("lastUpdatedAt", log.getEventTimestamp().toString());
|
||||
response.put("fundsHeld", fundsHeld);
|
||||
|
||||
// Escalation count
|
||||
final long escalations = simulatorLogRepository.findByCfcfrmsRefOrderByEventTimestampAsc(cfcfrmsRef)
|
||||
.stream().filter(l -> "ESCALATION_RECEIVED".equals(l.getEventType())).count();
|
||||
response.put("escalationCount", escalations);
|
||||
|
||||
// Convert timeline
|
||||
final List<Map<String, Object>> timelineList = timeline.stream().map(t -> {
|
||||
final Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("event", t.getEvent());
|
||||
m.put("description", t.getDescription());
|
||||
m.put("timestamp", t.getTimestamp().toString());
|
||||
m.put("actor", t.getActor());
|
||||
return m;
|
||||
}).collect(Collectors.toList());
|
||||
response.put("timeline", timelineList);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getBankPerformance() {
|
||||
final String bankIfsc = CfcfrmsConstants.BANK_IFSC;
|
||||
final long totalComplaints = ticketRepository.count();
|
||||
|
||||
// Simulate response times
|
||||
final long respondedWithin1Hour = Math.round(totalComplaints * 0.65);
|
||||
final long respondedWithin24Hours = Math.round(totalComplaints * 0.93);
|
||||
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("bankIfsc", bankIfsc);
|
||||
response.put("bankName", CfcfrmsConstants.BANK_NAME);
|
||||
response.put("totalComplaintsReceived", totalComplaints);
|
||||
response.put("respondedWithin1Hour", respondedWithin1Hour);
|
||||
response.put("respondedWithin24Hours", respondedWithin24Hours);
|
||||
response.put("avgResponseTimeMinutes", 38);
|
||||
response.put("lienMarkedCount", ticketRepository.countByStatus(
|
||||
org.apache.fineract.portfolio.cfcfrms.domain.ComplaintStatus.LIEN_MARKED));
|
||||
response.put("debitFrozenCount", ticketRepository.countByStatus(
|
||||
org.apache.fineract.portfolio.cfcfrms.domain.ComplaintStatus.DEBIT_FROZEN));
|
||||
response.put("fundsMovedOut", ticketRepository.countByStatus(
|
||||
org.apache.fineract.portfolio.cfcfrms.domain.ComplaintStatus.FUNDS_MOVED));
|
||||
response.put("resolvedCount", ticketRepository.countByStatus(
|
||||
org.apache.fineract.portfolio.cfcfrms.domain.ComplaintStatus.RESOLVED));
|
||||
|
||||
final String tatCompliance = totalComplaints > 0
|
||||
? BigDecimal.valueOf(respondedWithin24Hours * 100.0 / totalComplaints)
|
||||
.setScale(1, java.math.RoundingMode.HALF_UP) + "%"
|
||||
: "0.0%";
|
||||
response.put("tatComplianceRate", tatCompliance);
|
||||
response.put("lastActivityAt", OffsetDateTime.now().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private String generateCfcfrmsRef() {
|
||||
final String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MMdd"));
|
||||
return "CFCFRMS-" + date + "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
}
|
||||
|
||||
private String getStringOrThrow(final JsonObject json, final String field) {
|
||||
if (!json.has(field) || json.get(field).isJsonNull()) {
|
||||
throw new IllegalArgumentException("Missing required field: " + field);
|
||||
}
|
||||
return json.get(field).getAsString();
|
||||
}
|
||||
}
|
||||
@@ -244,4 +244,5 @@
|
||||
<include file="parts/0223_add_sanctions_screening_log.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0224_add_mobile_lookup_log.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0225_add_current_account_support.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0226_add_cfcfrms_tables.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
<?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.
|
||||
|
||||
-->
|
||||
<!--
|
||||
CFCFRMS (Citizen Financial Cyber Fraud Reporting and Management System)
|
||||
MHA/I4C Bank Integration tables.
|
||||
|
||||
Tables created:
|
||||
m_cfcfrms_complaint_ticket – Fraud complaint tickets received from CFCFRMS
|
||||
m_cfcfrms_complaint_audit_log – Audit trail for each ticket status change
|
||||
m_cfcfrms_simulator_log – I4C/MHA simulator-side event log
|
||||
|
||||
Permissions inserted:
|
||||
READ_CFCFRMS, CREATE_CFCFRMS, UPDATE_CFCFRMS, ESCALATE_CFCFRMS
|
||||
-->
|
||||
<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">
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
MySQL – m_cfcfrms_complaint_ticket
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<changeSet author="fineract" id="1" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_cfcfrms_complaint_ticket"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_cfcfrms_complaint_ticket">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="ticket_id" type="VARCHAR(50)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="complainant_name" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="complainant_mobile" type="VARCHAR(15)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="fraud_amount" type="DECIMAL(19,2)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="transaction_ref_id" type="VARCHAR(100)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="fraud_account_number" type="VARCHAR(30)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="fraud_bank_ifsc" type="VARCHAR(11)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="victim_account_number" type="VARCHAR(30)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="victim_bank_ifsc" type="VARCHAR(11)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="upi_id" type="VARCHAR(50)"/>
|
||||
<column name="complaint_source" type="VARCHAR(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="status" type="VARCHAR(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="action_taken" type="TEXT"/>
|
||||
<column name="available_balance" type="DECIMAL(19,2)"/>
|
||||
<column name="funds_held" type="DECIMAL(19,2)"/>
|
||||
<column name="next_hop_account_number" type="VARCHAR(30)"/>
|
||||
<column name="next_hop_bank_ifsc" type="VARCHAR(11)"/>
|
||||
<column name="received_at" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="resolved_at" type="DATETIME"/>
|
||||
<column name="assigned_officer" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="state_police_ref" type="VARCHAR(50)"/>
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- PostgreSQL – m_cfcfrms_complaint_ticket -->
|
||||
<changeSet author="fineract" id="1" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_cfcfrms_complaint_ticket"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_cfcfrms_complaint_ticket">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="ticket_id" type="VARCHAR(50)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="complainant_name" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="complainant_mobile" type="VARCHAR(15)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="fraud_amount" type="DECIMAL(19,2)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="transaction_ref_id" type="VARCHAR(100)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="fraud_account_number" type="VARCHAR(30)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="fraud_bank_ifsc" type="VARCHAR(11)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="victim_account_number" type="VARCHAR(30)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="victim_bank_ifsc" type="VARCHAR(11)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="upi_id" type="VARCHAR(50)"/>
|
||||
<column name="complaint_source" type="VARCHAR(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="status" type="VARCHAR(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="action_taken" type="TEXT"/>
|
||||
<column name="available_balance" type="DECIMAL(19,2)"/>
|
||||
<column name="funds_held" type="DECIMAL(19,2)"/>
|
||||
<column name="next_hop_account_number" type="VARCHAR(30)"/>
|
||||
<column name="next_hop_bank_ifsc" type="VARCHAR(11)"/>
|
||||
<column name="received_at" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="resolved_at" type="TIMESTAMP WITH TIME ZONE"/>
|
||||
<column name="assigned_officer" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="state_police_ref" type="VARCHAR(50)"/>
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
MySQL – m_cfcfrms_complaint_audit_log
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<changeSet author="fineract" id="2" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_cfcfrms_complaint_audit_log"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_cfcfrms_complaint_audit_log">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="ticket_id" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="event" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="description" type="TEXT"/>
|
||||
<column name="actor" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="event_timestamp" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- PostgreSQL – m_cfcfrms_complaint_audit_log -->
|
||||
<changeSet author="fineract" id="2" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_cfcfrms_complaint_audit_log"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_cfcfrms_complaint_audit_log">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="ticket_id" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="event" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="description" type="TEXT"/>
|
||||
<column name="actor" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="event_timestamp" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
MySQL – m_cfcfrms_simulator_log
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<changeSet author="fineract" id="3" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_cfcfrms_simulator_log"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_cfcfrms_simulator_log">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="cfcfrms_ref" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ticket_id" type="VARCHAR(50)"/>
|
||||
<column name="bank_ifsc" type="VARCHAR(11)"/>
|
||||
<column name="direction" type="VARCHAR(10)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="event_type" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payload" type="TEXT"/>
|
||||
<column name="response_payload" type="TEXT"/>
|
||||
<column name="event_timestamp" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- PostgreSQL – m_cfcfrms_simulator_log -->
|
||||
<changeSet author="fineract" id="3" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_cfcfrms_simulator_log"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_cfcfrms_simulator_log">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="cfcfrms_ref" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ticket_id" type="VARCHAR(50)"/>
|
||||
<column name="bank_ifsc" type="VARCHAR(11)"/>
|
||||
<column name="direction" type="VARCHAR(10)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="event_type" type="VARCHAR(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payload" type="TEXT"/>
|
||||
<column name="response_payload" type="TEXT"/>
|
||||
<column name="event_timestamp" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- Indexes -->
|
||||
<changeSet author="fineract" id="4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists indexName="idx_cfcfrms_ticket_id"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_cfcfrms_complaint_ticket" indexName="idx_cfcfrms_ticket_id">
|
||||
<column name="ticket_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="5">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists indexName="idx_cfcfrms_ticket_status"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_cfcfrms_complaint_ticket" indexName="idx_cfcfrms_ticket_status">
|
||||
<column name="status"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="6">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists indexName="idx_cfcfrms_fraud_account"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_cfcfrms_complaint_ticket" indexName="idx_cfcfrms_fraud_account">
|
||||
<column name="fraud_account_number"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="7">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists indexName="idx_cfcfrms_audit_ticket_id"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_cfcfrms_complaint_audit_log" indexName="idx_cfcfrms_audit_ticket_id">
|
||||
<column name="ticket_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="8">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists indexName="idx_cfcfrms_sim_ref"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_cfcfrms_simulator_log" indexName="idx_cfcfrms_sim_ref">
|
||||
<column name="cfcfrms_ref"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
Permissions
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<changeSet author="fineract" id="9">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">
|
||||
SELECT COUNT(*) FROM m_permission WHERE code = 'READ_CFCFRMS'
|
||||
</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="READ_CFCFRMS"/>
|
||||
<column name="entity_name" value="CFCFRMS"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_CFCFRMS"/>
|
||||
<column name="entity_name" value="CFCFRMS"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="true"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="UPDATE_CFCFRMS"/>
|
||||
<column name="entity_name" value="CFCFRMS"/>
|
||||
<column name="action_name" value="UPDATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="true"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="ESCALATE_CFCFRMS"/>
|
||||
<column name="entity_name" value="CFCFRMS"/>
|
||||
<column name="action_name" value="ESCALATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="true"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user