Fees and structures added

This commit is contained in:
RIT Services
2026-08-20 06:43:00 +00:00
parent 9d83dec681
commit 6e52735484
9 changed files with 909 additions and 44 deletions

View File

@@ -75,6 +75,13 @@ public class WebhookController {
String messageType = messageNode.get("type").asText();
UserState currentState = sessionService.getUserState(fromPhoneNumber);
boolean isInMemory = sessionService.isSessionCached(fromPhoneNumber);
boolean hasDbRecord = sessionService.hasSavedCredentials(fromPhoneNumber);
if (!isInMemory && hasDbRecord) {
messageService.sendMessage(fromPhoneNumber, "Authenticating User with IMS... ⏳");
}
boolean isAuthenticated = sessionService.isAuthenticated(fromPhoneNumber);
// Global reset command
@@ -166,7 +173,7 @@ public class WebhookController {
messageService.sendMessage(phoneNumber, "This menu selection is no longer active.");
studentMenuHandler.sendMainMenu(phoneNumber);
}
} else if (selectedListId.startsWith("receipt_")) {
} else if (selectedListId.startsWith("receipt_") || selectedListId.startsWith("exam_") || selectedListId.startsWith("acad_") || selectedListId.startsWith("cons_")) {
// They clicked a receipt download option
if (currentState == UserState.AWAITING_RECEIPT_SELECTION) {
studentMenuHandler.handleReceiptSelection(phoneNumber, selectedListId);
@@ -191,6 +198,16 @@ public class WebhookController {
return;
}
if ("student_exam_fees".equals(selectedButtonId)) {
studentMenuHandler.handleExamFees(phoneNumber);
return;
}
if ("student_payment_history".equals(selectedButtonId)) {
studentMenuHandler.handlePaymentHistory(phoneNumber);
return;
}
if (currentState == UserState.AWAITING_GRIEVANCE_CATEGORY) {
grievanceHandler.handleCategorySelection(phoneNumber, selectedButtonId);
} else {

View File

@@ -97,30 +97,29 @@ public class StudentMenuHandler {
sendSemesterSelection(phoneNumber, token);
break;
case "student_fee_details":
case "student_fee_details": {
String rawFees = imsApiService.getFees(token);
String rawExamFees = imsApiService.getExamFees(token);
String bodyText = MessageFormatter.formatFeesAndExamFees(rawFees, rawExamFees);
String bodyText = MessageFormatter.formatAcademicFeesOnly(rawFees);
List<Map<String, String>> receiptRows = MessageFormatter.extractReceiptRows(rawExamFees);
List<Map<String, String>> feeButtons = new ArrayList<>();
Map<String, String> examBtn = new HashMap<>();
examBtn.put("id", "student_exam_fees");
examBtn.put("title", "Exam Fees");
feeButtons.add(examBtn);
if (!receiptRows.isEmpty()) {
// Max 10 items in a list. Limit to 10 if necessary.
if (receiptRows.size() > 10) receiptRows = receiptRows.subList(0, 10);
sessionService.setUserState(phoneNumber, UserState.AWAITING_RECEIPT_SELECTION);
// Meta API interactive list body limit is 1024 characters
if (bodyText.length() > 1024) bodyText = bodyText.substring(0, 1021) + "...";
Map<String, Object> listMessage = WhatsAppMessageBuilder.buildListMessage(
"Receipts", bodyText, "Download Receipt", receiptRows
);
messageService.sendInteractiveMessage(phoneNumber, listMessage);
} else {
sendResponseWithMenu(phoneNumber, bodyText);
Map<String, String> historyBtn = new HashMap<>();
historyBtn.put("id", "student_payment_history");
historyBtn.put("title", "Payment History");
feeButtons.add(historyBtn);
if (bodyText.length() > 1020) {
bodyText = bodyText.substring(0, 1017) + "...";
}
Map<String, Object> feeButtonMessage = WhatsAppMessageBuilder.buildButtonMessage(bodyText, feeButtons);
messageService.sendInteractiveMessage(phoneNumber, feeButtonMessage);
break;
}
case "student_raise_grievance":
grievanceHandler.startGrievanceFlow(phoneNumber);
@@ -209,15 +208,31 @@ public class StudentMenuHandler {
}
try {
String historyId = selectedOptionId.replace("receipt_", "");
byte[] pdfBytes = imsApiService.getExamFeeReceiptPdf(token, historyId);
byte[] pdfBytes = null;
String fileName = "Receipt.pdf";
String docMessage = "Here is your Fee Receipt!";
if (selectedOptionId.startsWith("exam_")) {
String longId = sessionService.getReceiptIdMapping(phoneNumber, selectedOptionId);
pdfBytes = imsApiService.getExamFeeReceiptPdf(token, longId);
fileName = "Exam_Receipt.pdf";
docMessage = "Here is your Exam Fee Receipt!";
} else if (selectedOptionId.startsWith("acad_")) {
String longId = sessionService.getReceiptIdMapping(phoneNumber, selectedOptionId);
pdfBytes = imsApiService.getAcademicFeeReceiptPdf(token, longId);
fileName = "Academic_Receipt.pdf";
docMessage = "Here is your Academic Fee Receipt!";
} else if (selectedOptionId.startsWith("cons_")) {
String longId = sessionService.getReceiptIdMapping(phoneNumber, selectedOptionId);
pdfBytes = imsApiService.getConsolidatedAcademicFeeReceiptPdf(token, longId);
fileName = "Consolidated_Receipt.pdf";
docMessage = "Here is your Consolidated Academic Fee Receipt!";
}
if (pdfBytes != null) {
String fileName = "Receipt_" + historyId + ".pdf";
String mediaId = messageService.uploadMedia(pdfBytes, fileName, "application/pdf");
if (mediaId != null) {
messageService.sendDocumentMessage(phoneNumber, mediaId, "Here is your Exam Fee Receipt!", fileName);
messageService.sendDocumentMessage(phoneNumber, mediaId, docMessage, fileName);
sendResponseWithMenu(phoneNumber, "Select an option from the menu below.");
} else {
sendResponseWithMenu(phoneNumber, "Failed to upload the receipt document to WhatsApp.");
@@ -249,6 +264,59 @@ public class StudentMenuHandler {
sendResponseWithMenu(phoneNumber, "Select an option from the menu below.");
}
public void handleExamFees(String phoneNumber) {
String token = sessionService.getImsToken(phoneNumber);
if (token == null) {
messageService.sendMessage(phoneNumber, "Your session has expired. Please type 'Reset' to login again.");
return;
}
String rawExamFees = imsApiService.getExamFees(token);
String formattedExamFees = MessageFormatter.formatExamFees(rawExamFees);
sendResponseWithMenu(phoneNumber, formattedExamFees);
}
public void handlePaymentHistory(String phoneNumber) {
String token = sessionService.getImsToken(phoneNumber);
if (token == null) {
messageService.sendMessage(phoneNumber, "Your session has expired. Please type 'Reset' to login again.");
return;
}
String rawExamFees = imsApiService.getExamFees(token);
String rawFeesHistory = imsApiService.getAcademicFeesHistory(token);
String formattedHistory = MessageFormatter.formatCombinedPaymentHistory(rawExamFees, rawFeesHistory);
Map<String, String> idMappings = new HashMap<>();
List<Map<String, String>> examRows = MessageFormatter.extractReceiptRows(rawExamFees, idMappings);
List<Map<String, String>> acadRows = MessageFormatter.extractAcademicReceiptRows(rawFeesHistory, idMappings);
List<Map<String, String>> consRows = MessageFormatter.extractConsolidatedReceiptRows(rawFeesHistory, idMappings);
sessionService.storeReceiptIdMappings(phoneNumber, idMappings);
List<Map<String, String>> receiptRows = new ArrayList<>();
receiptRows.addAll(examRows);
receiptRows.addAll(acadRows);
receiptRows.addAll(consRows);
if (!receiptRows.isEmpty()) {
if (receiptRows.size() > 10) receiptRows = receiptRows.subList(0, 10);
sessionService.setUserState(phoneNumber, UserState.AWAITING_RECEIPT_SELECTION);
if (formattedHistory.length() > 1024) {
formattedHistory = formattedHistory.substring(0, 1021) + "...";
}
Map<String, Object> listMessage = WhatsAppMessageBuilder.buildListMessage(
"Receipts", formattedHistory, "Download Receipt", receiptRows
);
messageService.sendInteractiveMessage(phoneNumber, listMessage);
} else {
sendResponseWithMenu(phoneNumber, formattedHistory);
}
}
private void sendResponseWithMenu(String phoneNumber, String bodyText) {
// Meta API interactive list body limit is 1024 characters
if (bodyText.length() > 1024) {

View File

@@ -134,7 +134,15 @@ public class ImsApiService {
}
public byte[] getExamFeeReceiptPdf(String token, String historyId) {
String url = BASE_URL + "/api/student/exam-fee/receipt?history_id=" + historyId;
String cleanId = historyId;
if (cleanId.startsWith("?history_id=")) {
cleanId = cleanId.substring(12);
} else if (cleanId.startsWith("?id=")) {
cleanId = cleanId.substring(4);
} else if (cleanId.startsWith("?")) {
cleanId = cleanId.substring(1);
}
String url = BASE_URL + "/api/student/exam-fee/receipt?history_id=" + cleanId;
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
@@ -186,4 +194,60 @@ public class ImsApiService {
}
return "Student";
}
public String getAcademicFeesHistory(String token) {
return fetchFromIms("/api/student/fees/history", token);
}
public byte[] getAcademicFeeReceiptPdf(String token, String historyId) {
String cleanId = historyId;
if (cleanId.startsWith("?history_id=")) {
cleanId = cleanId.substring(12);
} else if (cleanId.startsWith("?id=")) {
cleanId = cleanId.substring(4);
} else if (cleanId.startsWith("?")) {
cleanId = cleanId.substring(1);
}
String url = BASE_URL + "/api/student/fees/download-receipt?history_id=" + cleanId;
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<String> request = new HttpEntity<>(headers);
try {
ResponseEntity<byte[]> response = restTemplate.exchange(url, org.springframework.http.HttpMethod.GET, request, byte[].class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
}
} catch (Exception e) {
System.err.println("Failed to download Academic Fee receipt PDF: " + e.getMessage());
}
return null;
}
public byte[] getConsolidatedAcademicFeeReceiptPdf(String token, String id) {
String cleanId = id;
if (cleanId.startsWith("?id=")) {
cleanId = cleanId.substring(4);
} else if (cleanId.startsWith("?history_id=")) {
cleanId = cleanId.substring(12);
} else if (cleanId.startsWith("?")) {
cleanId = cleanId.substring(1);
}
String url = BASE_URL + "/api/student/fees/download-consolidated-receipt?id=" + cleanId;
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<String> request = new HttpEntity<>(headers);
try {
ResponseEntity<byte[]> response = restTemplate.exchange(url, org.springframework.http.HttpMethod.GET, request, byte[].class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
}
} catch (Exception e) {
System.err.println("Failed to download Consolidated Academic Fee receipt PDF: " + e.getMessage());
}
return null;
}
}

View File

@@ -42,6 +42,14 @@ public class SessionService {
userStates.put(phoneNumber, state);
}
public boolean isSessionCached(String phoneNumber) {
return sessionTokens.containsKey(phoneNumber);
}
public boolean hasSavedCredentials(String phoneNumber) {
return userRepository.existsById(phoneNumber);
}
public void resetState(String phoneNumber) {
userStates.remove(phoneNumber);
}
@@ -104,6 +112,20 @@ public class SessionService {
return name;
}
private final Map<String, Map<String, String>> receiptIdMappings = new ConcurrentHashMap<>();
public void storeReceiptIdMappings(String phoneNumber, Map<String, String> mappings) {
receiptIdMappings.put(phoneNumber, mappings);
}
public String getReceiptIdMapping(String phoneNumber, String shortId) {
Map<String, String> mappings = receiptIdMappings.get(phoneNumber);
if (mappings != null) {
return mappings.get(shortId);
}
return null;
}
public void setTempLoginId(String phoneNumber, String tempId) {
temporaryLoginIds.put(phoneNumber, tempId);
}

View File

@@ -51,10 +51,9 @@ public class MessageFormatter {
if (!foundData) {
sb.append("Details not available in this format.\n");
sb.append("\n*DEBUG RAW ATTENDANCE JSON:*\n").append(rawAttendance).append("\n");
}
} catch (Exception e) {
sb.append("⚠️ Error formatting attendance data.\nRAW JSON: ").append(rawAttendance).append("\n");
sb.append("⚠️ Error formatting attendance data.\n");
}
}
@@ -85,10 +84,9 @@ public class MessageFormatter {
if (!foundData) {
sb.append("Details not available in this format.\n");
sb.append("\n*DEBUG RAW LEAVES JSON:*\n").append(rawLeaves).append("\n");
}
} catch (Exception e) {
sb.append("⚠️ Error formatting leaves data.\nRAW JSON: ").append(rawLeaves).append("\n");
sb.append("⚠️ Error formatting leaves data.\n");
}
}
@@ -482,23 +480,159 @@ public class MessageFormatter {
return sb.toString();
}
public static java.util.List<java.util.Map<String, String>> extractReceiptRows(String rawExamFees) {
public static String formatAcademicFeesOnly(String rawFees) {
StringBuilder sb = new StringBuilder();
sb.append("💵 *Academic Dues / Fees*\n\n");
if (rawFees == null) {
sb.append("❌ Failed to retrieve academic fee details.\n");
} else {
try {
JsonNode root = mapper.readTree(rawFees);
boolean foundData = false;
if (root.has("data")) {
JsonNode data = root.get("data");
if (data.isArray() && data.size() > 0) {
for (JsonNode fee : data) {
String type = fee.has("type") ? fee.get("type").asText() :
(fee.has("description") ? fee.get("description").asText() : "Fee");
String amount = fee.has("amount") ? fee.get("amount").asText() : "0";
String status = fee.has("status") ? fee.get("status").asText() : "N/A";
sb.append("- *").append(type).append("*: ₹").append(amount).append(" (").append(status).append(")\n");
foundData = true;
}
} else if (data.isObject()) {
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = data.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> field = fields.next();
sb.append("- *").append(field.getKey().replace("_", " ")).append("*: ").append(field.getValue().asText()).append("\n");
foundData = true;
}
}
}
if (!foundData) {
sb.append("Details not available in this format.\n");
}
} catch (Exception e) {
sb.append("⚠️ Error formatting academic fee data.\n");
}
}
return sb.toString();
}
public static String formatExamFees(String rawExamFees) {
StringBuilder sb = new StringBuilder();
sb.append("📝 *Exam Fees*\n\n");
if (rawExamFees == null) {
sb.append("❌ Failed to retrieve exam fee details.\n");
} else {
try {
JsonNode root = mapper.readTree(rawExamFees);
boolean foundData = false;
if (root.has("data")) {
JsonNode data = root.get("data");
if (data.isArray() && data.size() > 0) {
for (JsonNode fee : data) {
String status = fee.has("status") ? fee.get("status").asText() : "";
if (!status.equalsIgnoreCase("Paid") && !status.equalsIgnoreCase("Success")) {
String semester = fee.has("semester") ? fee.get("semester").asText() : "Unknown Sem";
String amount = fee.has("amount") ? fee.get("amount").asText() : "0";
String displayStatus = status.isEmpty() ? "Unpaid" : status;
sb.append("- *Sem ").append(semester).append("*: ₹").append(amount).append(" (").append(displayStatus).append(")\n");
foundData = true;
}
}
}
}
if (!foundData) {
sb.append("No outstanding exam fees found.\n");
}
} catch (Exception e) {
sb.append("⚠️ Error formatting exam fee data.\n");
}
}
return sb.toString();
}
public static String formatPaymentHistory(String rawExamFees) {
StringBuilder sb = new StringBuilder();
sb.append("📜 *Payment History*\n\n");
if (rawExamFees == null) {
sb.append("❌ Failed to retrieve payment history.\n");
} else {
try {
JsonNode root = mapper.readTree(rawExamFees);
boolean foundData = false;
if (root.has("data")) {
JsonNode data = root.get("data");
if (data.isArray() && data.size() > 0) {
for (JsonNode fee : data) {
String status = fee.has("status") ? fee.get("status").asText() : "";
if (status.equalsIgnoreCase("Paid") || status.equalsIgnoreCase("Success") || fee.has("history_id")) {
String semester = fee.has("semester") ? fee.get("semester").asText() : "Unknown Sem";
String amount = fee.has("amount") ? fee.get("amount").asText() : "0";
String date = fee.has("date") ? " on " + fee.get("date").asText() : "";
sb.append("- *Sem ").append(semester).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n");
foundData = true;
}
}
}
}
if (!foundData) {
sb.append("No payment history found.\n");
}
} catch (Exception e) {
sb.append("⚠️ Error formatting payment history.\n");
}
}
return sb.toString();
}
public static java.util.List<java.util.Map<String, String>> extractReceiptRows(String rawExamFees, java.util.Map<String, String> idMappings) {
java.util.List<java.util.Map<String, String>> rows = new java.util.ArrayList<>();
if (rawExamFees == null) return rows;
try {
JsonNode root = mapper.readTree(rawExamFees);
if (root.has("data") && root.get("data").isArray()) {
for (JsonNode fee : root.get("data")) {
if (fee.has("history_id") || fee.has("id")) {
String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText();
String amount = fee.has("amount") ? fee.get("amount").asText() : "";
String date = fee.has("date") ? fee.get("date").asText() : "";
java.util.Map<String, String> row = new java.util.HashMap<>();
row.put("id", "receipt_" + id);
row.put("title", "Receipt " + id);
row.put("description", "₹" + amount + (date.isEmpty() ? "" : " on " + date));
rows.add(row);
if (root.has("data")) {
JsonNode data = root.get("data");
JsonNode historyNode = null;
if (data.isObject() && data.has("history")) {
historyNode = data.get("history");
}
if (historyNode != null && historyNode.isObject()) {
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> field = fields.next();
JsonNode arrayNode = field.getValue();
if (arrayNode.isArray()) {
for (JsonNode fee : arrayNode) {
if (fee.has("history_id") || fee.has("id")) {
String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText();
String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "");
String date = fee.has("paid_date") ? fee.get("paid_date").asText() : (fee.has("date") ? fee.get("date").asText() : "");
java.util.Map<String, String> row = new java.util.HashMap<>();
String shortId = "exam_" + (rows.size() + 1);
idMappings.put(shortId, id);
row.put("id", shortId);
String title = "Receipt " + date;
if (title.length() > 24 || date.isEmpty()) {
title = "Exam Receipt #" + (rows.size() + 1);
}
row.put("title", title);
row.put("description", "₹" + amount + (date.isEmpty() ? "" : " on " + date));
rows.add(row);
}
}
}
}
}
}
@@ -506,6 +640,225 @@ public class MessageFormatter {
return rows;
}
public static java.util.List<java.util.Map<String, String>> extractAcademicReceiptRows(String rawFeesHistory, java.util.Map<String, String> idMappings) {
java.util.List<java.util.Map<String, String>> rows = new java.util.ArrayList<>();
if (rawFeesHistory == null) return rows;
try {
JsonNode root = mapper.readTree(rawFeesHistory);
if (root.has("data")) {
JsonNode data = root.get("data");
JsonNode historyNode = null;
if (data.isObject()) {
if (data.has("history")) historyNode = data.get("history");
else if (data.has("payment_history")) historyNode = data.get("payment_history");
else if (data.has("paymentHistory")) historyNode = data.get("paymentHistory");
else if (data.has("logs")) historyNode = data.get("logs");
}
if (historyNode != null) {
if (historyNode.isObject()) {
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> field = fields.next();
JsonNode arrayNode = field.getValue();
if (arrayNode.isArray()) {
for (JsonNode fee : arrayNode) {
if (fee.has("history_id") || fee.has("id")) {
String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText();
String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "");
String date = fee.has("paid_date") ? fee.get("paid_date").asText() : (fee.has("date") ? fee.get("date").asText() : "");
String type = fee.has("type") ? fee.get("type").asText() :
(fee.has("description") ? fee.get("description").asText() : (fee.has("name") ? fee.get("name").asText() : "Academic Fee"));
java.util.Map<String, String> row = new java.util.HashMap<>();
String shortId = "acad_" + (rows.size() + 1);
idMappings.put(shortId, id);
row.put("id", shortId);
String title = "Receipt " + date;
if (title.length() > 24 || date.isEmpty()) {
title = "Acad Receipt #" + (rows.size() + 1);
}
row.put("title", title);
row.put("description", type + ": ₹" + amount + (date.isEmpty() ? "" : " on " + date));
rows.add(row);
}
}
}
}
} else if (historyNode.isArray()) {
for (JsonNode fee : historyNode) {
if (fee.has("history_id") || fee.has("id")) {
String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText();
String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "");
String date = fee.has("paid_date") ? fee.get("paid_date").asText() : (fee.has("date") ? fee.get("date").asText() : "");
String type = fee.has("type") ? fee.get("type").asText() :
(fee.has("description") ? fee.get("description").asText() : "Academic Fee");
java.util.Map<String, String> row = new java.util.HashMap<>();
String shortId = "acad_" + (rows.size() + 1);
idMappings.put(shortId, id);
row.put("id", shortId);
String title = "Receipt " + date;
if (title.length() > 24 || date.isEmpty()) {
title = "Acad Receipt #" + (rows.size() + 1);
}
row.put("title", title);
row.put("description", type + ": ₹" + amount + (date.isEmpty() ? "" : " on " + date));
rows.add(row);
}
}
}
}
}
} catch (Exception e) {}
return rows;
}
public static java.util.List<java.util.Map<String, String>> extractConsolidatedReceiptRows(String rawFeesHistory, java.util.Map<String, String> idMappings) {
java.util.List<java.util.Map<String, String>> rows = new java.util.ArrayList<>();
if (rawFeesHistory == null) return rows;
try {
JsonNode root = mapper.readTree(rawFeesHistory);
if (root.has("data") && root.get("data").isObject()) {
JsonNode data = root.get("data");
JsonNode consolidatedNode = null;
if (data.has("consolidated")) consolidatedNode = data.get("consolidated");
else if (data.has("consolidated_receipts")) consolidatedNode = data.get("consolidated_receipts");
else if (data.has("consolidatedReceipts")) consolidatedNode = data.get("consolidatedReceipts");
if (consolidatedNode != null && consolidatedNode.isArray()) {
for (JsonNode rec : consolidatedNode) {
if (rec.has("id")) {
String id = rec.get("id").asText();
String academicYear = rec.has("academic_year") ? rec.get("academic_year").asText() : "";
String totalFee = rec.has("total_fee") ? rec.get("total_fee").asText() : "";
String paidFee = rec.has("paid_fee") ? rec.get("paid_fee").asText() : "";
java.util.Map<String, String> row = new java.util.HashMap<>();
String shortId = "cons_" + (rows.size() + 1);
idMappings.put(shortId, id);
row.put("id", shortId);
// WhatsApp interactive list row titles must be <= 24 characters
String title = "Consolidated " + academicYear;
if (title.length() > 24 || academicYear.isEmpty()) {
title = "Consolidated #" + (rows.size() + 1);
}
row.put("title", title);
String description = "Paid: ₹" + paidFee + " / ₹" + totalFee;
if (description.length() > 72) {
description = description.substring(0, 69) + "...";
}
row.put("description", description);
rows.add(row);
}
}
}
}
} catch (Exception e) {}
return rows;
}
public static String formatCombinedPaymentHistory(String rawExamFees, String rawFeesHistory) {
StringBuilder sb = new StringBuilder();
// --- Parse Exam Fee History ---
sb.append("📝 *Exam Fee Payment History*\n\n");
boolean foundExam = false;
if (rawExamFees != null) {
try {
JsonNode root = mapper.readTree(rawExamFees);
if (root.has("data")) {
JsonNode data = root.get("data");
JsonNode historyNode = null;
if (data.isObject() && data.has("history")) {
historyNode = data.get("history");
}
if (historyNode != null && historyNode.isObject()) {
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> field = fields.next();
JsonNode arrayNode = field.getValue();
if (arrayNode.isArray()) {
for (JsonNode fee : arrayNode) {
String semester = fee.has("semester") ? fee.get("semester").asText() : "Unknown Sem";
String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "0");
String date = fee.has("paid_date") ? " on " + fee.get("paid_date").asText() : (fee.has("date") ? " on " + fee.get("date").asText() : "");
sb.append("- *Sem ").append(semester).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n");
foundExam = true;
}
}
}
}
}
} catch (Exception e) {}
}
if (!foundExam) {
sb.append("No exam fee payment history found.\n");
}
sb.append("\n");
// --- Parse Academic Fee History ---
sb.append("💵 *Academic Fee Payment History*\n\n");
boolean foundAcad = false;
if (rawFeesHistory != null) {
try {
JsonNode root = mapper.readTree(rawFeesHistory);
if (root.has("data")) {
JsonNode data = root.get("data");
JsonNode historyNode = null;
if (data.isObject()) {
if (data.has("history")) historyNode = data.get("history");
else if (data.has("payment_history")) historyNode = data.get("payment_history");
else if (data.has("paymentHistory")) historyNode = data.get("paymentHistory");
else if (data.has("logs")) historyNode = data.get("logs");
}
if (historyNode != null) {
if (historyNode.isObject()) {
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> field = fields.next();
JsonNode arrayNode = field.getValue();
if (arrayNode.isArray()) {
for (JsonNode fee : arrayNode) {
String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "");
String date = fee.has("paid_date") ? " on " + fee.get("paid_date").asText() : (fee.has("date") ? " on " + fee.get("date").asText() : "");
String type = fee.has("type") ? fee.get("type").asText() :
(fee.has("description") ? fee.get("description").asText() : (fee.has("name") ? fee.get("name").asText() : "Academic Fee"));
sb.append("- *").append(type).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n");
foundAcad = true;
}
}
}
} else if (historyNode.isArray()) {
for (JsonNode fee : historyNode) {
String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "");
String date = fee.has("paid_date") ? " on " + fee.get("paid_date").asText() : (fee.has("date") ? " on " + fee.get("date").asText() : "");
String type = fee.has("type") ? fee.get("type").asText() :
(fee.has("description") ? fee.get("description").asText() : "Academic Fee");
sb.append("- *").append(type).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n");
foundAcad = true;
}
}
}
}
} catch (Exception e) {}
}
if (!foundAcad) {
sb.append("No academic fee payment history found.\n");
}
return sb.toString();
}
private static String getNumberEmoji(int num) {
switch (num) {
case 1: return "1️⃣";