Enhance transaction fetching with pagination support and improved error handling
Java CI / build (push) Successful in 45s
.NET+Docker CI/CD / Unit and Integration tests (push) Failing after 27s
.NET+Docker CI/CD / Push Docker image to ghcr.io (push) Has been skipped

Signed-off-by: Dmitrii <computer@yawaflua.tech>

Took 22 minutes
This commit is contained in:
Dmitrii
2026-06-29 03:34:36 +03:00
parent 77e525b890
commit 0841f51a10
5 changed files with 187 additions and 30 deletions
@@ -3,6 +3,7 @@ package git.yawaflua.tech.spmega.client.ui;
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
import git.yawaflua.tech.spmega.client.ui.service.BankDatabase.LocalTransaction;
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
@@ -19,6 +20,8 @@ public class TransactionHistoryScreen extends Screen {
private final List<LocalTransaction> transactions = new ArrayList<>();
private boolean loading = true;
private String errorMessage = "";
private int scrollOffset = 0;
private int currentPage = 1;
public TransactionHistoryScreen(Screen parent, String cardId, String cardTitle) {
super(Text.literal("История транзакций"));
@@ -36,40 +39,112 @@ public class TransactionHistoryScreen extends Screen {
.dimensions(centerX - 60, startY + 10, 120, 20)
.build());
// Scroll Up/Down buttons
this.addDrawableChild(ButtonWidget.builder(Text.literal(""), button -> scrollUp())
.dimensions(centerX + 140, 65, 20, 20)
.build());
this.addDrawableChild(ButtonWidget.builder(Text.literal(""), button -> scrollDown())
.dimensions(centerX + 140, 155, 20, 20)
.build());
// Prev Page button
this.addDrawableChild(ButtonWidget.builder(Text.literal("<"), button -> {
if (currentPage > 1) {
currentPage--;
scrollOffset = 0;
loadTransactions();
}
}).dimensions(centerX - 100, startY - 15, 30, 20).build());
// Next Page button
this.addDrawableChild(ButtonWidget.builder(Text.literal(">"), button -> {
currentPage++;
scrollOffset = 0;
loadTransactions();
}).dimensions(centerX + 70, startY - 15, 30, 20).build());
loadTransactions();
}
private void scrollUp() {
if (scrollOffset > 0) {
scrollOffset--;
}
}
private void scrollDown() {
if (scrollOffset < Math.max(0, transactions.size() - 6)) {
scrollOffset++;
}
}
// Support pre-1.20.2 mouse scroll
public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
if (amount > 0) {
scrollUp();
} else if (amount < 0) {
scrollDown();
}
return true;
}
// Support 1.20.2+ mouse scroll
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
if (verticalAmount > 0) {
scrollUp();
} else if (verticalAmount < 0) {
scrollDown();
}
return true;
}
private void loadTransactions() {
loading = true;
errorMessage = "";
transactions.clear();
UiNotifications.instance().show(Text.literal("Загрузка..."));
CompletableFuture.runAsync(() -> {
try {
System.out.println("[SPMEGA] Transaction history loading started for card: " + cardId + ", page: " + currentPage);
List<LocalTransaction> list = null;
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
if (config != null && config.allowBackend()) {
try {
list = BackendAuthenticator.fetchTransactionsFromBackend(cardId);
list = BackendAuthenticator.fetchTransactionsFromBackend(cardId, currentPage);
System.out.println("[SPMEGA] Fetched " + (list != null ? list.size() : "null") + " transactions from backend.");
} catch (Exception e) {
System.err.println("[SPMEGA] Failed to fetch transactions from server, falling back to DB: " + e.getMessage());
e.printStackTrace();
}
}
if (list == null) {
list = BankUiService.instance().getDatabase().loadTransferHistory(cardId);
System.out.println("[SPMEGA] Loaded " + (list != null ? list.size() : "null") + " transactions from database.");
}
List<LocalTransaction> finalList = list;
if (this.client != null) {
this.client.execute(() -> {
List<LocalTransaction> finalList = list != null ? list : new ArrayList<>();
MinecraftClient minecraftClient = MinecraftClient.getInstance();
if (minecraftClient != null) {
minecraftClient.execute(() -> {
System.out.println("[SPMEGA] Setting transactions list on main thread. Count: " + finalList.size());
this.transactions.addAll(finalList);
this.scrollOffset = 0;
this.loading = false;
});
} else {
System.out.println("[SPMEGA] MinecraftClient.getInstance() is null in loadTransactions callback!");
}
} catch (Exception e) {
if (this.client != null) {
this.client.execute(() -> {
System.err.println("[SPMEGA] Outer exception in loadTransactions: " + e.getMessage());
e.printStackTrace();
MinecraftClient minecraftClient = MinecraftClient.getInstance();
if (minecraftClient != null) {
minecraftClient.execute(() -> {
this.errorMessage = e.getMessage();
this.loading = false;
});
@@ -91,30 +166,38 @@ public class TransactionHistoryScreen extends Screen {
int centerX = this.width / 2;
int startY = 50;
int bottomStartY = this.height / 2 + 50;
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFF);
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Карта: " + cardTitle), centerX, 35, 0xBFBFBF);
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFFFF);
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Карта: " + cardTitle), centerX, 35, 0xFFBFBFBF);
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Стр. " + currentPage), centerX, bottomStartY - 9, 0xFFFFFFFF);
if (loading) {
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Загрузка транзакций..."), centerX, this.height / 2 - 10, 0xCCCCCC);
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Загрузка транзакций..."), centerX, this.height / 2 - 10, 0xFFCCCCCC);
} else if (!errorMessage.isEmpty()) {
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Ошибка: " + errorMessage), centerX, this.height / 2 - 10, 0xFF5555);
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Ошибка: " + errorMessage), centerX, this.height / 2 - 10, 0xFFFF5555);
} else if (transactions.isEmpty()) {
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Транзакций не найдено"), centerX, this.height / 2 - 10, 0xCCCCCC);
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Транзакций не найдено"), centerX, this.height / 2 - 10, 0xFFCCCCCC);
} else {
int y = startY + 15;
for (int i = 0; i < Math.min(6, transactions.size()); i++) {
LocalTransaction tx = transactions.get(i);
int limit = Math.min(6, transactions.size() - scrollOffset);
for (int i = 0; i < limit; i++) {
LocalTransaction tx = transactions.get(scrollOffset + i);
String amountText = tx.amount() + " АР";
String dateText = tx.createdAt();
if (dateText == null) {
dateText = "";
}
if (dateText.length() > 19) {
dateText = dateText.substring(0, 19).replace("T", " ");
}
String commentText = tx.comment().isEmpty() ? "" : " (" + tx.comment() + ")";
String line = String.format("%s -> %s | %s%s", dateText, tx.receiver(), amountText, commentText);
int color = tx.status().equalsIgnoreCase("SUCCESS") ? 0x55FF55 : 0xFF5555;
String status = tx.status();
int color = (status != null && status.equalsIgnoreCase("SUCCESS")) ? 0xFF55FF55 : 0xFFFF5555;
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(line), centerX, y, color);
y += 18;
@@ -1,5 +1,6 @@
package git.yawaflua.tech.spmega.client.ui.service;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.authlib.exceptions.AuthenticationException;
@@ -314,9 +315,11 @@ public class BackendAuthenticator {
return true;
}
public static List<BankDatabase.LocalTransaction> fetchTransactionsFromBackend(String cardId) throws IOException, InterruptedException {
public static List<BankDatabase.LocalTransaction> fetchTransactionsFromBackend(String cardId, int page) throws IOException, InterruptedException {
System.out.println("[SPMEGA] fetchTransactionsFromBackend called for cardId: " + cardId + ", page: " + page);
ModConfig config = SPMega.getConfig();
if (config == null || !config.allowBackend()) {
System.out.println("[SPMEGA] fetchTransactionsFromBackend aborted: config is null or backend not allowed.");
return List.of();
}
@@ -328,7 +331,8 @@ public class BackendAuthenticator {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
String url = apiDomain + "/api/v1/transactions?cardId=" + cardId;
String url = apiDomain + "/api/v1/transactions?p=" + page;
System.out.println("[SPMEGA] Requesting transactions from URL: " + url);
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
@@ -338,22 +342,85 @@ public class BackendAuthenticator {
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("[SPMEGA] Response status code: " + response.statusCode());
if (response.statusCode() != 200) {
System.err.println("[SPMEGA] Failed response body: " + response.body());
throw new IOException("Server returned status code " + response.statusCode() + " on fetch transactions.");
}
com.google.gson.JsonArray transactionsArray = JsonParser.parseString(response.body()).getAsJsonArray();
String body = response.body();
System.out.println("[SPMEGA] Response body: " + (body.length() > 500 ? body.substring(0, 500) + "..." : body));
JsonElement parsed = JsonParser.parseString(body);
com.google.gson.JsonArray transactionsArray = null;
if (parsed.isJsonArray()) {
transactionsArray = parsed.getAsJsonArray();
} else if (parsed.isJsonObject()) {
JsonObject obj = parsed.getAsJsonObject();
if (obj.has("$values") && obj.get("$values").isJsonArray()) {
transactionsArray = obj.getAsJsonArray("$values");
} else if (obj.has("transactions") && obj.get("transactions").isJsonArray()) {
transactionsArray = obj.getAsJsonArray("transactions");
} else if (obj.has("values") && obj.get("values").isJsonArray()) {
transactionsArray = obj.getAsJsonArray("values");
}
}
if (transactionsArray == null) {
System.err.println("[SPMEGA] Response body is not a JSON array or wrapped array: " + body);
return List.of();
}
System.out.println("[SPMEGA] Total transactions returned from server: " + transactionsArray.size());
List<BankDatabase.LocalTransaction> list = new ArrayList<>();
for (com.google.gson.JsonElement el : transactionsArray) {
JsonObject json = el.getAsJsonObject();
String receiver = json.has("receiver") ? json.get("receiver").getAsString() : (json.has("recipient") ? json.get("recipient").getAsString() : "unknown");
long amount = json.has("amount") ? json.get("amount").getAsLong() : 0L;
String comment = json.has("comment") && !json.get("comment").isJsonNull() ? json.get("comment").getAsString() : "";
String status = json.has("status") ? json.get("status").getAsString() : "SUCCESS";
String createdAt = json.has("createdAt") ? json.get("createdAt").getAsString() : (json.has("created_at") ? json.get("created_at").getAsString() : "");
try {
JsonObject json = el.getAsJsonObject();
list.add(new BankDatabase.LocalTransaction(receiver, amount, comment, status, createdAt));
// Check cardId filter
String senderCardNumber = "";
if (json.has("senderCardNumber") && !json.get("senderCardNumber").isJsonNull()) {
senderCardNumber = json.get("senderCardNumber").getAsString();
}
if (!senderCardNumber.equalsIgnoreCase(cardId)) {
continue;
}
String receiver = "unknown";
if (json.has("receiver") && !json.get("receiver").isJsonNull()) {
receiver = json.get("receiver").getAsString();
} else if (json.has("receiverName") && !json.get("receiverName").isJsonNull()) {
receiver = json.get("receiverName").getAsString();
} else if (json.has("receiverCardNumber") && !json.get("receiverCardNumber").isJsonNull()) {
receiver = json.get("receiverCardNumber").getAsString();
} else if (json.has("recipient") && !json.get("recipient").isJsonNull()) {
receiver = json.get("recipient").getAsString();
}
long amount = json.has("amount") ? json.get("amount").getAsLong() : 0L;
String comment = json.has("comment") && !json.get("comment").isJsonNull() ? json.get("comment").getAsString() : "";
String status = json.has("status") && !json.get("status").isJsonNull() ? json.get("status").getAsString() : "SUCCESS";
String createdAt = "";
if (json.has("transactionDate") && !json.get("transactionDate").isJsonNull()) {
createdAt = json.get("transactionDate").getAsString();
} else if (json.has("createdAt") && !json.get("createdAt").isJsonNull()) {
createdAt = json.get("createdAt").getAsString();
} else if (json.has("created_at") && !json.get("created_at").isJsonNull()) {
createdAt = json.get("created_at").getAsString();
}
System.out.printf("[SPMEGA] Parsed transaction: %s -> %s, amount=%d, comment=%s, status=%s%n",
createdAt, receiver, amount, comment, status);
list.add(new BankDatabase.LocalTransaction(receiver, amount, comment, status, createdAt));
} catch (Exception parseEx) {
System.err.println("[SPMEGA] Error parsing transaction element: " + parseEx.getMessage());
}
}
System.out.println("[SPMEGA] Returning " + list.size() + " filtered transactions for card " + cardId);
return list;
}
}