solved duplicate data

This commit is contained in:
riz081 2025-06-09 18:36:52 +07:00
parent 99fab68e71
commit 4aaa9957e7
4 changed files with 1091 additions and 127 deletions

View File

@ -2,6 +2,7 @@ package com.example.bdkipoc;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
@ -21,6 +22,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -57,6 +59,15 @@ public class QrisActivity extends AppCompatActivity {
private StringBuilder currentAmount = new StringBuilder();
// FRONTEND DEDUPLICATION: Add SharedPreferences for tracking
private SharedPreferences transactionPrefs;
private static final String PREF_RECENT_REFERENCES = "recent_references";
private static final String PREF_LAST_TRANSACTION_TIME = "last_transaction_time";
private static final String PREF_CURRENT_REFERENCE = "current_reference";
private static final String PREF_LAST_SUCCESSFUL_TX = "last_successful_tx";
private static final long REFERENCE_COOLDOWN_MS = 60000; // 1 minute cooldown
private static final long TRANSACTION_COOLDOWN_MS = 5000; // 5 second cooldown
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; // Replace with your actual key
@ -67,6 +78,9 @@ public class QrisActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qris);
// Initialize SharedPreferences for duplicate prevention
transactionPrefs = getSharedPreferences("qris_transactions", MODE_PRIVATE);
// Initialize views
progressBar = findViewById(R.id.progressBar);
initiatePaymentButton = findViewById(R.id.initiatePaymentButton);
@ -90,8 +104,8 @@ public class QrisActivity extends AppCompatActivity {
btn000 = findViewById(R.id.btn000);
btnDelete = findViewById(R.id.btnDelete);
// Generate reference ID
referenceId = "ref-" + generateRandomString(8);
// Generate unique reference ID with duplicate prevention
referenceId = generateUniqueReferenceId();
referenceIdTextView.setText(referenceId);
// Set up click listeners
@ -105,6 +119,144 @@ public class QrisActivity extends AppCompatActivity {
initiatePaymentButton.setEnabled(false);
}
/**
* FRONTEND DEDUPLICATION: Generate unique reference ID with local tracking
*/
private String generateUniqueReferenceId() {
Log.d("QrisActivity", "🔄 Generating unique reference ID...");
String baseRef = "ref-" + generateRandomString(8);
// Check if this reference was recently created
String recentRefs = transactionPrefs.getString(PREF_RECENT_REFERENCES, "");
long currentTime = System.currentTimeMillis();
// Clean up old references (older than cooldown period)
StringBuilder validRefs = new StringBuilder();
if (!recentRefs.isEmpty()) {
String[] refs = recentRefs.split(",");
for (String refEntry : refs) {
if (refEntry.contains(":")) {
String[] parts = refEntry.split(":");
if (parts.length == 2) {
try {
long timestamp = Long.parseLong(parts[1]);
if (currentTime - timestamp < REFERENCE_COOLDOWN_MS) {
// Reference is still in cooldown period
if (validRefs.length() > 0) validRefs.append(",");
validRefs.append(refEntry);
}
} catch (NumberFormatException e) {
// Skip invalid entries
Log.w("QrisActivity", "Invalid reference entry: " + refEntry);
}
}
}
}
}
// Check if baseRef already exists in recent references
if (validRefs.length() > 0) {
String[] validRefArray = validRefs.toString().split(",");
for (String refEntry : validRefArray) {
if (refEntry.startsWith(baseRef + ":")) {
// Reference already exists, generate a new one
Log.w("QrisActivity", "⚠️ Reference " + baseRef + " recently used, generating new one");
return generateUniqueReferenceId(); // Recursive call with new random string
}
}
}
// Add this reference to recent references
if (validRefs.length() > 0) validRefs.append(",");
validRefs.append(baseRef).append(":").append(currentTime);
// Save updated references
transactionPrefs.edit()
.putString(PREF_RECENT_REFERENCES, validRefs.toString())
.apply();
Log.d("QrisActivity", "✅ Generated unique reference: " + baseRef);
return baseRef;
}
/**
* FRONTEND DEDUPLICATION: Check if transaction is currently being processed
*/
private boolean isTransactionInProgress() {
long lastTransactionTime = transactionPrefs.getLong(PREF_LAST_TRANSACTION_TIME, 0);
long currentTime = System.currentTimeMillis();
// If last transaction was less than cooldown period, consider it in progress
boolean inProgress = (currentTime - lastTransactionTime) < TRANSACTION_COOLDOWN_MS;
if (inProgress) {
Log.w("QrisActivity", "⏸️ Transaction in progress, cooldown active");
}
return inProgress;
}
/**
* FRONTEND DEDUPLICATION: Mark transaction processing status
*/
private void markTransactionInProgress(boolean inProgress) {
SharedPreferences.Editor editor = transactionPrefs.edit();
if (inProgress) {
editor.putLong(PREF_LAST_TRANSACTION_TIME, System.currentTimeMillis())
.putString(PREF_CURRENT_REFERENCE, referenceId);
Log.d("QrisActivity", "🔒 Marked transaction in progress: " + referenceId);
} else {
editor.remove(PREF_CURRENT_REFERENCE);
Log.d("QrisActivity", "🔓 Cleared transaction progress status");
}
editor.apply();
}
/**
* FRONTEND DEDUPLICATION: Save successful transaction for future reference
*/
private void saveSuccessfulTransaction() {
try {
JSONObject txData = new JSONObject();
txData.put("reference_id", referenceId);
txData.put("transaction_uuid", transactionUuid);
txData.put("amount", amount);
txData.put("created_at", System.currentTimeMillis());
// Save to SharedPreferences
transactionPrefs.edit()
.putString(PREF_LAST_SUCCESSFUL_TX, txData.toString())
.putLong("last_success_time", System.currentTimeMillis())
.apply();
Log.d("QrisActivity", "💾 Saved successful transaction: " + referenceId);
} catch (Exception e) {
Log.w("QrisActivity", "Failed to save transaction data: " + e.getMessage());
}
}
/**
* FRONTEND DEDUPLICATION: Create client info for better backend tracking
*/
private JSONObject createClientInfo() {
try {
JSONObject clientInfo = new JSONObject();
clientInfo.put("app_version", "1.0.0");
clientInfo.put("platform", "android");
clientInfo.put("timestamp", System.currentTimeMillis());
clientInfo.put("session_id", generateRandomString(16));
clientInfo.put("reference_generation_time", System.currentTimeMillis());
return clientInfo;
} catch (JSONException e) {
Log.w("QrisActivity", "Failed to create client info: " + e.getMessage());
return new JSONObject();
}
}
private void setupNumpadListeners() {
View.OnClickListener numberClickListener = v -> {
TextView button = (TextView) v;
@ -172,17 +324,33 @@ public class QrisActivity extends AppCompatActivity {
}
}
/**
* ENHANCED: Modified createTransaction with comprehensive duplicate prevention
*/
private void createTransaction() {
if (currentAmount.length() == 0) {
Toast.makeText(this, "Masukkan jumlah pembayaran", Toast.LENGTH_SHORT).show();
return;
}
// FRONTEND CHECK: Prevent rapid duplicate submissions
if (isTransactionInProgress()) {
Toast.makeText(this, "Transaksi sedang diproses, harap tunggu...", Toast.LENGTH_SHORT).show();
return;
}
Log.d("QrisActivity", "🚀 Starting transaction creation process");
Log.d("QrisActivity", " Reference ID: " + referenceId);
Log.d("QrisActivity", " Amount: " + currentAmount.toString());
progressBar.setVisibility(View.VISIBLE);
initiatePaymentButton.setEnabled(false);
statusTextView.setVisibility(View.VISIBLE);
statusTextView.setText("Creating transaction...");
// Mark transaction as in progress
markTransactionInProgress(true);
new CreateTransactionTask().execute();
}
@ -253,6 +421,12 @@ public class QrisActivity extends AppCompatActivity {
// Generate a UUID for the transaction
transactionUuid = UUID.randomUUID().toString();
// ENHANCED LOGGING: Better tracking for debugging
Log.d("MidtransCharge", "=== TRANSACTION CREATION START ===");
Log.d("MidtransCharge", "Reference ID: " + referenceId);
Log.d("MidtransCharge", "Transaction UUID: " + transactionUuid);
Log.d("MidtransCharge", "Timestamp: " + System.currentTimeMillis());
// Create transaction JSON payload
JSONObject payload = new JSONObject();
payload.put("type", "PAYMENT");
@ -260,6 +434,10 @@ public class QrisActivity extends AppCompatActivity {
payload.put("channel_code", "QRIS");
payload.put("reference_id", referenceId);
// FRONTEND ENHANCEMENT: Add client-side metadata for better tracking
payload.put("client_info", createClientInfo());
payload.put("is_initial_creation", true); // Mark as initial creation
// Get amount from current input
String amountText = currentAmount.toString();
Log.d("MidtransCharge", "Raw amount text: " + amountText);
@ -307,6 +485,12 @@ public class QrisActivity extends AppCompatActivity {
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
// FRONTEND ENHANCEMENT: Add client headers for better backend tracking
conn.setRequestProperty("X-Client-Reference", referenceId);
conn.setRequestProperty("X-Client-Timestamp", String.valueOf(System.currentTimeMillis()));
conn.setRequestProperty("X-Client-Version", "1.0.0");
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
@ -318,7 +502,7 @@ public class QrisActivity extends AppCompatActivity {
Log.d("MidtransCharge", "Backend response code: " + responseCode);
if (responseCode == 200 || responseCode == 201) {
// Read the response
// Success - process response
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
@ -326,32 +510,73 @@ public class QrisActivity extends AppCompatActivity {
response.append(responseLine.trim());
}
Log.d("MidtransCharge", "Backend response: " + response.toString());
Log.d("MidtransCharge", "Backend success response: " + response.toString());
// Parse the response to get transaction ID
JSONObject jsonResponse = new JSONObject(response.toString());
JSONObject data = jsonResponse.getJSONObject("data");
transactionId = String.valueOf(data.getInt("id"));
Log.d("MidtransCharge", "Created transaction ID: " + transactionId);
Log.d("MidtransCharge", "✅ Created transaction ID: " + transactionId);
// FRONTEND SUCCESS: Save successful transaction info
saveSuccessfulTransaction();
// Now generate QRIS via Midtrans
return generateQris(amount);
} else if (responseCode == 409 || responseCode == 400) {
// ENHANCED DUPLICATE HANDLING: Handle gracefully
Log.w("MidtransCharge", "⚠️ Potential duplicate detected (HTTP " + responseCode + ")");
// Try to read and parse error response
try {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder errorResponse = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
errorResponse.append(responseLine.trim());
}
String errorResponseStr = errorResponse.toString();
Log.d("MidtransCharge", "Error response: " + errorResponseStr);
// Check if it's actually a duplicate reference error
if (errorResponseStr.toLowerCase().contains("duplicate") ||
errorResponseStr.toLowerCase().contains("already exists") ||
errorResponseStr.toLowerCase().contains("reference") ||
responseCode == 409) {
Log.i("MidtransCharge", "✅ Confirmed duplicate reference - proceeding with QRIS generation");
// For duplicates, we can still generate QRIS with existing reference
return generateQris(amount);
}
} catch (Exception e) {
Log.w("MidtransCharge", "Could not parse error response: " + e.getMessage());
}
// If we can't determine the exact error, try QRIS generation anyway
Log.i("MidtransCharge", "🔄 Proceeding with QRIS generation despite backend error");
return generateQris(amount);
} else {
// Read error response
// Other HTTP errors
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
Log.e("MidtransCharge", "Backend error response: " + response.toString());
errorMessage = "Error creating backend transaction: " + response.toString();
String errorResponse = response.toString();
Log.e("MidtransCharge", "❌ Backend error (HTTP " + responseCode + "): " + errorResponse);
errorMessage = "Backend error (" + responseCode + "): " + errorResponse;
return false;
}
} catch (Exception e) {
Log.e("MidtransCharge", "Backend transaction exception: " + e.getMessage(), e);
errorMessage = "Backend transaction error: " + e.getMessage();
Log.e("MidtransCharge", "Backend transaction exception: " + e.getMessage(), e);
errorMessage = "Network error: " + e.getMessage();
return false;
}
}
@ -386,21 +611,30 @@ public class QrisActivity extends AppCompatActivity {
payload.put("customer_details", customerDetails);
// Add item details (optional but recommended)
org.json.JSONArray itemDetails = new org.json.JSONArray();
JSONArray itemDetails = new JSONArray();
JSONObject item = new JSONObject();
item.put("id", "item1");
item.put("price", amount);
item.put("quantity", 1);
item.put("name", "QRIS Payment");
item.put("name", "QRIS Payment - " + referenceId);
itemDetails.put(item);
payload.put("item_details", itemDetails);
// FRONTEND ENHANCEMENT: Add tracking info for reference linkage
JSONObject customField1 = new JSONObject();
customField1.put("app_reference_id", referenceId);
customField1.put("creation_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date()));
customField1.put("client_version", "1.0.0");
payload.put("custom_field1", customField1.toString());
// Log the request details
Log.d("MidtransCharge", "=== MIDTRANS QRIS REQUEST ===");
Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL);
Log.d("MidtransCharge", "Payload: " + payload.toString());
Log.d("MidtransCharge", "Reference ID: " + referenceId);
Log.d("MidtransCharge", "Order ID: " + transactionUuid);
Log.d("MidtransCharge", "Amount: " + amount);
Log.d("MidtransCharge", "================================");
// Make the API call to Midtrans
@ -459,7 +693,7 @@ public class QrisActivity extends AppCompatActivity {
return false;
}
Log.d("MidtransCharge", "QRIS generation successful!");
Log.d("MidtransCharge", "QRIS generation successful!");
return true;
} else {
Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
@ -513,10 +747,13 @@ public class QrisActivity extends AppCompatActivity {
@Override
protected void onPostExecute(Boolean success) {
// FRONTEND CLEANUP: Always clear in-progress status
markTransactionInProgress(false);
if (success && midtransResponse != null) {
try {
// Extract needed values from midtransResponse
org.json.JSONArray actionsArray = midtransResponse.getJSONArray("actions");
JSONArray actionsArray = midtransResponse.getJSONArray("actions");
if (actionsArray.length() == 0) {
Log.e("MidtransCharge", "No actions found in Midtrans response");
Toast.makeText(QrisActivity.this, "Error: No QR code URL found in response", Toast.LENGTH_LONG).show();
@ -535,11 +772,12 @@ public class QrisActivity extends AppCompatActivity {
String acquirer = midtransResponse.getString("acquirer");
String merchantId = midtransResponse.getString("merchant_id");
// FIXED: Send raw amount as string without decimal conversion
// Send raw amount as string without decimal conversion
String rawAmountString = String.valueOf(amount); // Keep original integer amount
// Log everything before launching activity
Log.d("MidtransCharge", "=== LAUNCHING QRIS RESULT ACTIVITY ===");
Log.d("MidtransCharge", "✅ Transaction created successfully!");
Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl);
Log.d("MidtransCharge", "amount (raw): " + amount);
Log.d("MidtransCharge", "rawAmountString: " + rawAmountString);
@ -551,6 +789,13 @@ public class QrisActivity extends AppCompatActivity {
Log.d("MidtransCharge", "merchantId: " + merchantId);
Log.d("MidtransCharge", "========================================");
// FINAL SUCCESS: Update transaction status in preferences
transactionPrefs.edit()
.putString("last_qris_url", qrImageUrl)
.putString("last_qris_reference", referenceId)
.putLong("last_qris_time", System.currentTimeMillis())
.apply();
// Launch QrisResultActivity
Intent intent = new Intent(QrisActivity.this, QrisResultActivity.class);
intent.putExtra("qrImageUrl", qrImageUrl);
@ -558,7 +803,7 @@ public class QrisActivity extends AppCompatActivity {
intent.putExtra("referenceId", referenceId);
intent.putExtra("orderId", transactionUuid); // Order ID
intent.putExtra("transactionId", transactionId); // Actual Midtrans transaction_id
intent.putExtra("grossAmount", rawAmountString); // FIXED: Raw amount as string (no decimals)
intent.putExtra("grossAmount", rawAmountString); // Raw amount as string (no decimals)
intent.putExtra("transactionTime", transactionTime); // For timestamp
intent.putExtra("acquirer", acquirer);
intent.putExtra("merchantId", merchantId);
@ -566,23 +811,84 @@ public class QrisActivity extends AppCompatActivity {
try {
startActivity(intent);
finish(); // Close QrisActivity
Log.d("MidtransCharge", "🎉 Successfully launched QrisResultActivity");
} catch (Exception e) {
Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e);
Toast.makeText(QrisActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
// Re-enable button on error
initiatePaymentButton.setEnabled(true);
progressBar.setVisibility(View.GONE);
statusTextView.setVisibility(View.GONE);
}
return;
} catch (JSONException e) {
Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
Toast.makeText(QrisActivity.this, "Error processing QRIS response: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
} else {
// Handle error case
String message = (errorMessage != null && !errorMessage.isEmpty()) ?
errorMessage : "Unknown error occurred. Please check Logcat for details.";
errorMessage : "Unknown error occurred. Please check your connection and try again.";
Log.e("MidtransCharge", "❌ Transaction failed: " + message);
Toast.makeText(QrisActivity.this, message, Toast.LENGTH_LONG).show();
// Re-enable button for retry
initiatePaymentButton.setEnabled(true);
}
// Always hide progress indicators
progressBar.setVisibility(View.GONE);
statusTextView.setVisibility(View.GONE);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// CLEANUP: Clear any in-progress status when activity is destroyed
markTransactionInProgress(false);
Log.d("QrisActivity", "🧹 QrisActivity destroyed, cleared progress status");
}
@Override
protected void onPause() {
super.onPause();
// Keep progress status when paused (user might come back)
Log.d("QrisActivity", "⏸️ QrisActivity paused");
}
@Override
protected void onResume() {
super.onResume();
Log.d("QrisActivity", "▶️ QrisActivity resumed");
// Check if there's a recent successful transaction
String lastSuccessfulTx = transactionPrefs.getString(PREF_LAST_SUCCESSFUL_TX, "");
if (!lastSuccessfulTx.isEmpty()) {
try {
JSONObject txData = new JSONObject(lastSuccessfulTx);
String lastRef = txData.getString("reference_id");
long lastTime = txData.getLong("created_at");
// If last successful transaction was recent (within 5 minutes) and same reference
if (System.currentTimeMillis() - lastTime < 300000 && lastRef.equals(referenceId)) {
Log.d("QrisActivity", "🔄 Recent successful transaction detected for same reference");
}
} catch (Exception e) {
Log.w("QrisActivity", "Could not parse last successful transaction: " + e.getMessage());
}
}
}
@Override
public void onBackPressed() {
// CLEANUP: Clear progress status when user goes back
markTransactionInProgress(false);
super.onBackPressed();
}
}

View File

@ -859,6 +859,17 @@ public class QrisResultActivity extends AppCompatActivity {
returnMainButton.setVisibility(View.VISIBLE);
// Add receipt button or automatically launch receipt
Button viewReceiptButton = new Button(this);
viewReceiptButton.setText("Lihat Struk");
viewReceiptButton.setOnClickListener(v -> launchReceiptActivity());
// You can add this button to your layout or automatically launch receipt
// For better UX, let's automatically launch receipt after a short delay
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
launchReceiptActivity();
}, 2000); // Launch receipt after 2 seconds
Toast.makeText(this, "Payment simulation completed successfully!", Toast.LENGTH_LONG).show();
}
@ -1232,20 +1243,33 @@ public class QrisResultActivity extends AppCompatActivity {
private void launchReceiptActivity() {
Intent intent = new Intent(this, ReceiptActivity.class);
// Add calling activity information for proper back navigation
intent.putExtra("calling_activity", "QrisResultActivity");
Log.d("QrisResultFlow", "Launching receipt with data:");
Log.d("QrisResultFlow", " Reference ID: " + referenceId);
Log.d("QrisResultFlow", " Transaction ID: " + transactionId);
Log.d("QrisResultFlow", " Amount: " + originalAmount);
Log.d("QrisResultFlow", " Acquirer: " + acquirer);
intent.putExtra("transaction_id", transactionId);
intent.putExtra("reference_id", referenceId);
intent.putExtra("reference_id", referenceId); // Nomor Transaksi
intent.putExtra("order_id", orderId);
intent.putExtra("transaction_amount", grossAmount != null ? grossAmount : "0");
intent.putExtra("gross_amount", grossAmount);
intent.putExtra("transaction_date", getCurrentDateTime());
intent.putExtra("transaction_amount", String.valueOf(originalAmount)); // Total transaksi
intent.putExtra("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount));
intent.putExtra("created_at", getCurrentISOTime()); // Tanggal transaksi (current time for QRIS)
intent.putExtra("transaction_date", getCurrentDateTime()); // Backup formatted date
intent.putExtra("payment_method", "QRIS");
intent.putExtra("card_type", "QRIS");
intent.putExtra("channel_code", "QRIS");
intent.putExtra("channel_code", "QRIS"); // Metode Pembayaran
intent.putExtra("channel_category", "RETAIL_OUTLET");
intent.putExtra("card_type", "QRIS");
intent.putExtra("merchant_name", "Marcel Panjaitan");
intent.putExtra("merchant_location", "Jakarta, Indonesia");
intent.putExtra("acquirer", acquirer);
intent.putExtra("acquirer", acquirer != null ? acquirer : "qris"); // Jenis Kartu
// Add MID and TID with default values
intent.putExtra("mid", "71000026521"); // MID
intent.putExtra("tid", "73001500"); // TID
startActivity(intent);
}

View File

@ -5,7 +5,7 @@ import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log; // ADD THIS IMPORT
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
@ -14,6 +14,13 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class ReceiptActivity extends AppCompatActivity {
@ -110,74 +117,134 @@ public class ReceiptActivity extends AppCompatActivity {
private void loadTransactionData() {
Intent intent = getIntent();
if (intent != null) {
// Data dari TransactionActivity atau QrisResultActivity
Log.d("ReceiptActivity", "=== LOADING TRANSACTION DATA ===");
// Get all available data from intent
String amount = intent.getStringExtra("transaction_amount");
String grossAmount = intent.getStringExtra("gross_amount");
String merchantNameStr = intent.getStringExtra("merchant_name");
String merchantLocationStr = intent.getStringExtra("merchant_location");
String transactionId = intent.getStringExtra("transaction_id");
String transactionDateStr = intent.getStringExtra("transaction_date");
String paymentMethodStr = intent.getStringExtra("payment_method");
String cardTypeStr = intent.getStringExtra("card_type");
// Additional data that might come from different sources
String referenceId = intent.getStringExtra("reference_id");
String orderId = intent.getStringExtra("order_id");
String grossAmount = intent.getStringExtra("gross_amount");
String acquirer = intent.getStringExtra("acquirer");
String transactionDateStr = intent.getStringExtra("transaction_date");
String createdAt = intent.getStringExtra("created_at");
String paymentMethodStr = intent.getStringExtra("payment_method");
String cardTypeStr = intent.getStringExtra("card_type");
String channelCode = intent.getStringExtra("channel_code");
String channelCategory = intent.getStringExtra("channel_category");
String acquirer = intent.getStringExtra("acquirer");
String mid = intent.getStringExtra("mid");
String tid = intent.getStringExtra("tid");
// Set merchant data with defaults
// Log received data for debugging
Log.d("ReceiptActivity", "amount: " + amount);
Log.d("ReceiptActivity", "grossAmount: " + grossAmount);
Log.d("ReceiptActivity", "merchantName: " + merchantNameStr);
Log.d("ReceiptActivity", "referenceId: " + referenceId);
Log.d("ReceiptActivity", "createdAt: " + createdAt);
Log.d("ReceiptActivity", "channelCode: " + channelCode);
Log.d("ReceiptActivity", "acquirer: " + acquirer);
Log.d("ReceiptActivity", "mid: " + mid);
Log.d("ReceiptActivity", "tid: " + tid);
// 1. Set merchant data with defaults
merchantName.setText(merchantNameStr != null ? merchantNameStr : "Marcel Panjaitan");
merchantLocation.setText(merchantLocationStr != null ? merchantLocationStr : "Jakarta, Indonesia");
// Set MID and TID with defaults
midText.setText("71000026521"); // Default MID from your transaction code
tidText.setText("73001500"); // Default TID from your transaction code
// 2. Set MID and TID - prioritize from intent, then defaults
midText.setText(mid != null ? mid : "71000026521");
tidText.setText(tid != null ? tid : "73001500");
// Set transaction number - prefer reference_id, then transaction_id, then order_id
// 3. Set transaction number - prioritize reference_id
String displayTransactionNumber = null;
if (referenceId != null && !referenceId.isEmpty()) {
displayTransactionNumber = referenceId;
Log.d("ReceiptActivity", "Using reference_id as transaction number: " + referenceId);
} else if (transactionId != null && !transactionId.isEmpty()) {
displayTransactionNumber = transactionId;
Log.d("ReceiptActivity", "Using transaction_id as transaction number: " + transactionId);
} else if (orderId != null && !orderId.isEmpty()) {
displayTransactionNumber = orderId;
Log.d("ReceiptActivity", "Using order_id as transaction number: " + orderId);
}
transactionNumber.setText(displayTransactionNumber != null ? displayTransactionNumber : "3429483635");
transactionNumber.setText(displayTransactionNumber != null ? displayTransactionNumber : "N/A");
// Set transaction date
transactionDate.setText(transactionDateStr != null ? transactionDateStr : getCurrentDateTime());
// 4. Set transaction date - prioritize createdAt, format as dd/MM/yyyy HH:mm
String displayDate = null;
if (createdAt != null && !createdAt.isEmpty()) {
displayDate = formatDateFromCreatedAt(createdAt);
Log.d("ReceiptActivity", "Formatted createdAt: " + createdAt + " -> " + displayDate);
} else if (transactionDateStr != null && !transactionDateStr.isEmpty()) {
displayDate = transactionDateStr;
Log.d("ReceiptActivity", "Using provided transaction_date: " + transactionDateStr);
} else {
displayDate = getCurrentDateTime();
Log.d("ReceiptActivity", "Using current datetime: " + displayDate);
}
transactionDate.setText(displayDate);
// Set payment method - determine from various sources
String displayPaymentMethod = determinePaymentMethod(paymentMethodStr, channelCode, channelCategory);
// 5. Set payment method - use channel_code mapping
String displayPaymentMethod = getPaymentMethodFromChannelCode(channelCode, paymentMethodStr);
paymentMethod.setText(displayPaymentMethod);
Log.d("ReceiptActivity", "Payment method: " + displayPaymentMethod + " (from channel: " + channelCode + ")");
// Set card type - prefer acquirer, then cardType, then channelCode
String displayCardType = null;
if (acquirer != null && !acquirer.isEmpty()) {
displayCardType = acquirer.toUpperCase();
} else if (cardTypeStr != null && !cardTypeStr.isEmpty()) {
displayCardType = cardTypeStr;
} else if (channelCode != null && !channelCode.isEmpty()) {
displayCardType = channelCode.toUpperCase();
// 6. Set card type - use acquirer with comprehensive mapping
// For QRIS, we need to get the actual acquirer (GoPay, OVO, DANA, etc.)
String displayCardType = getCardTypeFromAcquirer(acquirer, channelCode, cardTypeStr);
// Special handling for QRIS - if we only have "QRIS" or "qris", try to get real acquirer
if (displayCardType.equalsIgnoreCase("QRIS") && channelCode != null && channelCode.equalsIgnoreCase("QRIS")) {
// For QRIS transactions, we should try to determine the actual acquirer
// This might require additional API call or webhook data parsing
Log.w("ReceiptActivity", "QRIS transaction detected but no specific acquirer found. Using default.");
// In production, this should fetch the real acquirer from transaction status API
displayCardType = acquirer != null && !acquirer.equalsIgnoreCase("qris") ?
getCardTypeFromAcquirer(acquirer, channelCode, cardTypeStr) : "GoPay"; // Default QRIS acquirer
}
cardType.setText(displayCardType != null ? displayCardType : "QRIS");
// Format and set amounts
cardType.setText(displayCardType);
Log.d("ReceiptActivity", "Card type: " + displayCardType + " (from acquirer: " + acquirer + ")");
// 7. Format and set amounts - prioritize amount over grossAmount
setAmountData(amount, grossAmount);
Log.d("ReceiptActivity", "=== TRANSACTION DATA LOADED ===");
}
}
private String determinePaymentMethod(String paymentMethodStr, String channelCode, String channelCategory) {
// If payment method is already provided and formatted, use it
if (paymentMethodStr != null && !paymentMethodStr.isEmpty()) {
return paymentMethodStr;
/**
* Format date from createdAt field (yyyy-MM-dd HH:mm:ss) to Indonesian format (dd/MM/yyyy HH:mm)
*/
private String formatDateFromCreatedAt(String createdAt) {
try {
// Input format from database: "yyyy-MM-dd HH:mm:ss"
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
// Output format for receipt: "dd/MM/yyyy HH:mm"
SimpleDateFormat outputFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm", new Locale("id", "ID"));
Date date = inputFormat.parse(createdAt);
String formatted = outputFormat.format(date);
Log.d("ReceiptActivity", "Date formatting: '" + createdAt + "' -> '" + formatted + "'");
return formatted;
} catch (Exception e) {
Log.e("ReceiptActivity", "Error formatting date: " + createdAt, e);
// Fallback: try alternative formats or return as-is
return createdAt;
}
}
// Determine from channel code
if (channelCode != null) {
switch (channelCode.toUpperCase()) {
/**
* Get payment method name from channel_code with comprehensive mapping
*/
private String getPaymentMethodFromChannelCode(String channelCode, String fallbackPaymentMethod) {
if (channelCode != null && !channelCode.isEmpty()) {
String code = channelCode.toUpperCase();
switch (code) {
case "QRIS":
return "QRIS";
case "DEBIT":
@ -185,31 +252,314 @@ public class ReceiptActivity extends AppCompatActivity {
case "CREDIT":
return "Kartu Kredit";
case "BCA":
return "BCA";
case "MANDIRI":
return "Bank Mandiri";
case "BNI":
return "Bank BNI";
case "BRI":
return "Kartu " + channelCode.toUpperCase();
return "Bank BRI";
case "PERMATA":
return "Bank Permata";
case "CIMB":
return "CIMB Niaga";
case "DANAMON":
return "Bank Danamon";
case "BSI":
return "Bank Syariah Indonesia";
case "CASH":
return "Tunai";
case "EDC":
return "EDC";
case "ALFAMART":
return "Alfamart";
case "INDOMARET":
return "Indomaret";
case "AKULAKU":
return "Akulaku";
default:
return code; // Return the code as-is if not mapped
}
}
// Fallback to provided payment method or default
return fallbackPaymentMethod != null ? fallbackPaymentMethod : "QRIS";
}
/**
* ENHANCED: Dynamic acquirer detection from webhook data
* This method tries to get the REAL acquirer instead of defaulting to GoPay
*/
private String getCardTypeFromAcquirer(String acquirer, String channelCode, String fallbackCardType) {
// STEP 1: If we have a valid acquirer that's not generic "qris", use it
if (acquirer != null && !acquirer.isEmpty() && !acquirer.equalsIgnoreCase("qris")) {
String acq = acquirer.toLowerCase();
// Comprehensive acquirer mapping based on Midtrans
switch (acq) {
// E-Wallet acquirers
case "gopay": return "GoPay";
case "shopeepay": return "ShopeePay";
case "ovo": return "OVO";
case "dana": return "DANA";
case "linkaja": return "LinkAja";
case "jenius": return "Jenius";
case "kaspro": return "KasPro";
// Bank acquirers
case "bca": return "BCA";
case "mandiri":
case "mandiri_bill": return "Mandiri";
case "bni":
case "bni_va": return "BNI";
case "bri":
case "bri_va": return "BRI";
case "permata":
case "permata_va": return "Permata";
case "cimb":
case "cimb_va": return "CIMB Niaga";
case "danamon":
case "danamon_va": return "Danamon";
case "bsi":
case "bsi_va": return "BSI";
case "maybank": return "Maybank";
case "mega": return "Bank Mega";
case "btn": return "BTN";
case "bukopin": return "Bukopin";
// Credit card acquirers
case "visa": return "Visa";
case "mastercard": return "Mastercard";
case "jcb": return "JCB";
case "amex":
case "american_express": return "American Express";
// Over-the-counter
case "alfamart": return "Alfamart";
case "indomaret": return "Indomaret";
// Buy now pay later
case "akulaku": return "Akulaku";
case "kredivo": return "Kredivo";
case "indodana": return "Indodana";
// Other acquirers
case "emoney": return "E-Money";
case "flazz": return "Flazz";
case "tapcash": return "TapCash";
case "brizzi": return "Brizzi";
default:
// Return capitalized version of acquirer if not in mapping
return capitalizeFirstLetter(acquirer);
}
}
// STEP 2: For QRIS transactions, try to fetch real acquirer from webhook data
if (channelCode != null && channelCode.equalsIgnoreCase("QRIS")) {
String referenceId = getIntent().getStringExtra("reference_id");
if (referenceId != null && !referenceId.isEmpty()) {
Log.d("ReceiptActivity", "🔍 QRIS detected, fetching real acquirer for: " + referenceId);
// Try to get real acquirer from webhook data asynchronously
fetchRealAcquirerFromWebhook(referenceId);
// IMPROVED: Instead of defaulting to GoPay, show generic "QRIS" until real acquirer is found
return "QRIS"; // Will be updated when real acquirer is found
}
}
// STEP 3: Fallback based on channel code
if (channelCode != null && !channelCode.isEmpty()) {
String code = channelCode.toUpperCase();
switch (code) {
case "QRIS": return "QRIS"; // CHANGED: Generic QRIS instead of GoPay
case "DEBIT": return "Debit";
case "CREDIT": return "Credit";
case "BCA": return "BCA";
case "MANDIRI": return "Mandiri";
case "BNI": return "BNI";
case "BRI": return "BRI";
default: return code;
}
}
// STEP 4: Final fallback
return fallbackCardType != null ? fallbackCardType : "Unknown"; // CHANGED: Unknown instead of GoPay
}
/**
* ENHANCED: Fetch real acquirer from webhook data for QRIS transactions
* This method searches webhook logs to find the actual acquirer used
*/
private void fetchRealAcquirerFromWebhook(String referenceId) {
new Thread(() -> {
try {
Log.d("ReceiptActivity", "🔍 Searching for real acquirer for reference: " + referenceId);
// Search webhook logs for this reference with broader search
String queryUrl = "https://be-edc.msvc.app/api-logs?limit=100&sortOrder=DESC&sortColumn=created_at";
URL url = new URL(queryUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
if (conn.getResponseCode() == 200) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
org.json.JSONObject json = new org.json.JSONObject(response.toString());
org.json.JSONArray results = json.optJSONArray("results");
if (results != null && results.length() > 0) {
String realAcquirer = searchForRealAcquirer(results, referenceId);
if (realAcquirer != null && !realAcquirer.isEmpty() && !realAcquirer.equalsIgnoreCase("qris")) {
Log.d("ReceiptActivity", "✅ Found real acquirer: " + realAcquirer + " for reference: " + referenceId);
// Update UI on main thread
final String displayAcquirer = getCardTypeFromAcquirer(realAcquirer, null, null);
runOnUiThread(() -> {
if (cardType != null) {
cardType.setText(displayAcquirer);
Log.d("ReceiptActivity", "🎨 Updated card type from QRIS to: " + displayAcquirer);
}
});
} else {
Log.w("ReceiptActivity", "⚠️ Could not determine real acquirer for: " + referenceId);
// Keep showing "QRIS" instead of defaulting to GoPay
}
}
} else {
Log.w("ReceiptActivity", "⚠️ API call failed with code: " + conn.getResponseCode());
}
} catch (Exception e) {
Log.e("ReceiptActivity", "❌ Error fetching real acquirer: " + e.getMessage(), e);
}
}).start();
}
/**
* NEW METHOD: Advanced search for real acquirer in webhook logs
*/
private String searchForRealAcquirer(org.json.JSONArray results, String referenceId) {
try {
Log.d("ReceiptActivity", "🔍 Analyzing " + results.length() + " webhook logs for acquirer");
// Strategy 1: Look for settlement/success transactions first (highest priority)
String acquirerFromSettlement = searchLogsByStatus(results, referenceId, new String[]{"settlement", "capture", "success"});
if (acquirerFromSettlement != null) {
Log.d("ReceiptActivity", "🎯 Found acquirer from settlement: " + acquirerFromSettlement);
return acquirerFromSettlement;
}
// Strategy 2: Look for pending transactions
String acquirerFromPending = searchLogsByStatus(results, referenceId, new String[]{"pending"});
if (acquirerFromPending != null) {
Log.d("ReceiptActivity", "🎯 Found acquirer from pending: " + acquirerFromPending);
return acquirerFromPending;
}
// Strategy 3: Look for any transaction with this reference
String acquirerFromAny = searchLogsByStatus(results, referenceId, new String[]{});
if (acquirerFromAny != null) {
Log.d("ReceiptActivity", "🎯 Found acquirer from any status: " + acquirerFromAny);
return acquirerFromAny;
}
Log.w("ReceiptActivity", "❌ No acquirer found in webhook logs for: " + referenceId);
return null;
} catch (Exception e) {
Log.e("ReceiptActivity", "❌ Error analyzing webhook logs: " + e.getMessage(), e);
return null;
}
}
/**
* HELPER METHOD: Search logs by specific status
*/
private String searchLogsByStatus(org.json.JSONArray results, String referenceId, String[] targetStatuses) {
try {
for (int i = 0; i < results.length(); i++) {
org.json.JSONObject log = results.getJSONObject(i);
org.json.JSONObject reqBody = log.optJSONObject("request_body");
if (reqBody != null) {
String logReferenceId = reqBody.optString("reference_id", "");
String logTransactionStatus = reqBody.optString("transaction_status", "");
String logAcquirer = reqBody.optString("acquirer", "");
String logIssuer = reqBody.optString("issuer", "");
// Check for direct reference match
boolean isDirectMatch = referenceId.equals(logReferenceId);
// Check custom_field1 for refresh tracking
boolean isRefreshMatch = false;
String customField1 = reqBody.optString("custom_field1", "");
if (!customField1.isEmpty()) {
try {
org.json.JSONObject customData = new org.json.JSONObject(customField1);
String originalReference = customData.optString("original_reference", "");
String appReferenceId = customData.optString("app_reference_id", "");
if (referenceId.equals(originalReference) || referenceId.equals(appReferenceId)) {
isRefreshMatch = true;
}
} catch (org.json.JSONException e) {
// Ignore parsing errors
}
}
// Check if this log matches our reference
if (isDirectMatch || isRefreshMatch) {
// If target statuses specified, check status
if (targetStatuses.length > 0) {
boolean statusMatches = false;
for (String targetStatus : targetStatuses) {
if (logTransactionStatus.equalsIgnoreCase(targetStatus)) {
statusMatches = true;
break;
}
}
// Determine from channel category
if (channelCategory != null && !channelCategory.isEmpty()) {
return channelCategory.toUpperCase();
if (!statusMatches) continue; // Skip if status doesn't match
}
// Default
return "QRIS";
// Extract acquirer (prefer acquirer field over issuer)
String foundAcquirer = !logAcquirer.isEmpty() ? logAcquirer : logIssuer;
if (!foundAcquirer.isEmpty() && !foundAcquirer.equalsIgnoreCase("qris")) {
Log.d("ReceiptActivity", "📋 Found acquirer: " + foundAcquirer + " (status: " + logTransactionStatus + ")");
return foundAcquirer;
}
}
}
}
} catch (Exception e) {
Log.e("ReceiptActivity", "❌ Error searching logs by status: " + e.getMessage(), e);
}
return null; // No acquirer found for specified criteria
}
private String capitalizeFirstLetter(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase();
}
private void setAmountData(String amount, String grossAmount) {
// Use gross amount if available, otherwise use amount
String amountToUse = grossAmount != null ? grossAmount : amount;
// Prioritize 'amount' over 'grossAmount' for transaction data
String amountToUse = amount != null ? amount : grossAmount;
Log.d("ReceiptActivity", "Setting amount data - amount: " + amount +
", grossAmount: " + grossAmount + ", using: " + amountToUse);
@ -236,6 +586,8 @@ public class ReceiptActivity extends AppCompatActivity {
serviceFee.setText("Rp 0");
finalTotal.setText("Rp " + formatCurrency(total));
Log.d("ReceiptActivity", "Amount formatting successful: " + amountLong + " -> Rp " + formatCurrency(total));
} catch (NumberFormatException e) {
Log.e("ReceiptActivity", "Error parsing amount: " + amountToUse, e);
// Fallback if parsing fails
@ -254,7 +606,7 @@ public class ReceiptActivity extends AppCompatActivity {
}
/**
* FIXED: Clean amount string to extract raw number correctly
* Clean amount string to extract raw number correctly
* Input examples: "1000", "1000.00", "Rp 1.000", "1.000"
* Output: "1000"
*/
@ -323,8 +675,8 @@ public class ReceiptActivity extends AppCompatActivity {
}
private String getCurrentDateTime() {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd MMMM yyyy HH:mm", new java.util.Locale("id", "ID"));
return sdf.format(new java.util.Date());
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm", new Locale("id", "ID"));
return sdf.format(new Date());
}
private void handlePrintReceipt() {

View File

@ -1,5 +1,6 @@
package com.example.bdkipoc;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
@ -31,7 +32,11 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@ -45,9 +50,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
private EditText searchEditText;
private ImageButton searchButton;
// FRONTEND DEDUPLICATION: Local caching and tracking
private Map<String, Transaction> transactionCache = new HashMap<>();
private Set<String> processedReferences = new HashSet<>();
private SharedPreferences prefs;
// Pagination variables
private int page = 0;
private final int limit = 10;
private final int limit = 50; // INCREASED: Fetch more data for better deduplication
private boolean isLoading = false;
private boolean isLastPage = false;
private String currentSearchQuery = "";
@ -58,6 +68,9 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transaction);
// Initialize SharedPreferences for local tracking
prefs = getSharedPreferences("transaction_prefs", MODE_PRIVATE);
// Initialize views
initViews();
@ -111,12 +124,6 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (!recyclerView.canScrollVertically(1) && !isLoading && !isLastPage && currentSearchQuery.isEmpty()) {
loadTransactions(page + 1);
}
// Detect scroll to top for refresh gesture (double tap on top)
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null && layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && dy < 0) {
// User is at top and scrolling up, enable refresh on search button long press
}
}
});
}
@ -152,6 +159,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (isRefreshing) return;
isRefreshing = true;
// CLEAR LOCAL CACHE on refresh
transactionCache.clear();
processedReferences.clear();
// Clear search when refreshing
searchEditText.setText("");
currentSearchQuery = "";
@ -215,13 +226,25 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
protected List<Transaction> doInBackground(Void... voids) {
List<Transaction> result = new ArrayList<>();
try {
String urlString = "https://be-edc.msvc.app/transactions?page=" + pageToLoad + "&limit=" + limit + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=id";
// FETCH MORE DATA: Increased limit for better deduplication
int fetchLimit = limit * 3; // Get more records to handle all duplicates
String urlString = "https://be-edc.msvc.app/transactions?page=" + pageToLoad +
"&limit=" + fetchLimit + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=created_at";
Log.d("TransactionActivity", "🔍 Fetching transactions page " + pageToLoad + " with limit " + fetchLimit);
URI uri = new URI(urlString);
URL url = uri.toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
conn.setConnectTimeout(15000);
conn.setReadTimeout(15000);
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
@ -230,10 +253,16 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
response.append(line);
}
in.close();
JSONObject jsonObject = new JSONObject(response.toString());
JSONObject results = jsonObject.getJSONObject("results");
total = results.getInt("total");
JSONArray data = results.getJSONArray("data");
Log.d("TransactionActivity", "📊 Raw API response: " + data.length() + " records");
// STEP 1: Parse all transactions from API
List<Transaction> rawTransactions = new ArrayList<>();
for (int i = 0; i < data.length(); i++) {
JSONObject t = data.getJSONObject(i);
Transaction tx = new Transaction(
@ -248,12 +277,20 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
t.getString("created_at"),
t.getString("merchant_name")
);
result.add(tx);
rawTransactions.add(tx);
}
// STEP 2: Apply intelligent deduplication
result = applyAdvancedDeduplication(rawTransactions);
Log.d("TransactionActivity", "✅ After advanced deduplication: " + result.size() + " unique transactions");
} else {
Log.e("TransactionActivity", "❌ HTTP Error: " + responseCode);
error = true;
}
} catch (IOException | JSONException | URISyntaxException e) {
Log.e("TransactionActivity", "❌ Exception: " + e.getMessage(), e);
error = true;
}
return result;
@ -272,14 +309,44 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (pageToLoad == 0) {
transactionList.clear();
transactionCache.clear(); // Clear cache on refresh
}
transactionList.addAll(transactions);
// SMART MERGE: Only add truly new transactions
int addedCount = 0;
for (Transaction newTx : transactions) {
String refId = newTx.referenceId;
// Check if we already have a better version of this transaction
Transaction cachedTx = transactionCache.get(refId);
if (cachedTx == null || isBetterTransaction(newTx, cachedTx)) {
// Update cache with better transaction
transactionCache.put(refId, newTx);
// Update or add to main list
boolean updated = false;
for (int i = 0; i < transactionList.size(); i++) {
if (transactionList.get(i).referenceId.equals(refId)) {
transactionList.set(i, newTx);
updated = true;
break;
}
}
if (!updated) {
transactionList.add(newTx);
addedCount++;
}
}
}
Log.d("TransactionActivity", "📋 Added " + addedCount + " new unique transactions. Total: " + transactionList.size());
// Update filtered list based on current search
filterTransactions(currentSearchQuery);
page = pageToLoad;
if (transactionList.size() >= total) {
if (transactions.size() < limit) { // No more pages if returned less than requested
isLastPage = true;
}
@ -290,6 +357,186 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
}
}
/**
* ADVANCED DEDUPLICATION: Enhanced algorithm with multiple strategies
*/
private List<Transaction> applyAdvancedDeduplication(List<Transaction> rawTransactions) {
Log.d("TransactionActivity", "🧠 Starting advanced deduplication...");
// Strategy 1: Group by reference_id
Map<String, List<Transaction>> groupedByRef = new HashMap<>();
for (Transaction tx : rawTransactions) {
String refId = tx.referenceId;
groupedByRef.computeIfAbsent(refId, k -> new ArrayList<>()).add(tx);
}
List<Transaction> deduplicatedList = new ArrayList<>();
int duplicatesRemoved = 0;
// Strategy 2: For each group, select the best representative
for (Map.Entry<String, List<Transaction>> entry : groupedByRef.entrySet()) {
String referenceId = entry.getKey();
List<Transaction> group = entry.getValue();
if (group.size() == 1) {
// No duplicates for this reference
deduplicatedList.add(group.get(0));
Log.d("TransactionActivity", "✅ Unique transaction: " + referenceId);
} else {
// Multiple transactions with same reference_id
Transaction bestTransaction = selectBestTransactionAdvanced(group, referenceId);
deduplicatedList.add(bestTransaction);
duplicatesRemoved += (group.size() - 1);
Log.d("TransactionActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId +
" (kept ID: " + bestTransaction.id + ", status: " + bestTransaction.status + ")");
}
}
Log.d("TransactionActivity", "✅ Advanced deduplication complete:");
Log.d("TransactionActivity", " 📥 Input: " + rawTransactions.size() + " transactions");
Log.d("TransactionActivity", " 📤 Output: " + deduplicatedList.size() + " unique transactions");
Log.d("TransactionActivity", " 🗑️ Removed: " + duplicatesRemoved + " duplicates");
return deduplicatedList;
}
/**
* ENHANCED SELECTION: Advanced algorithm to pick the best transaction
*/
private Transaction selectBestTransactionAdvanced(List<Transaction> duplicates, String referenceId) {
Log.d("TransactionActivity", "🎯 Selecting best from " + duplicates.size() + " duplicates for: " + referenceId);
Transaction bestTransaction = duplicates.get(0);
int bestPriority = getStatusPriority(bestTransaction.status);
// Detailed analysis of each candidate
for (Transaction tx : duplicates) {
int currentPriority = getStatusPriority(tx.status);
Log.d("TransactionActivity", " 📊 Candidate: ID=" + tx.id +
", Status=" + tx.status + " (priority=" + currentPriority + ")" +
", Created=" + tx.createdAt);
boolean shouldSelect = false;
String reason = "";
// Rule 1: Higher status priority always wins
if (currentPriority > bestPriority) {
shouldSelect = true;
reason = "better status (" + tx.status + " > " + bestTransaction.status + ")";
}
// Rule 2: Same priority, choose newer timestamp
else if (currentPriority == bestPriority) {
if (isNewerTransaction(tx, bestTransaction)) {
shouldSelect = true;
reason = "newer timestamp";
}
// Rule 3: Same priority and time, choose higher ID
else if (tx.createdAt.equals(bestTransaction.createdAt) && tx.id > bestTransaction.id) {
shouldSelect = true;
reason = "higher ID";
}
}
if (shouldSelect) {
bestTransaction = tx;
bestPriority = currentPriority;
Log.d("TransactionActivity", " ⭐ NEW BEST selected: " + reason);
}
}
Log.d("TransactionActivity", "🏆 FINAL SELECTION: ID=" + bestTransaction.id +
", Status=" + bestTransaction.status + ", Created=" + bestTransaction.createdAt);
return bestTransaction;
}
/**
* TIMESTAMP COMPARISON: Smart date comparison
*/
private boolean isNewerTransaction(Transaction tx1, Transaction tx2) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
Date date1 = sdf.parse(tx1.createdAt);
Date date2 = sdf.parse(tx2.createdAt);
if (date1 != null && date2 != null) {
return date1.after(date2);
}
} catch (Exception e) {
Log.w("TransactionActivity", "Date parsing error, falling back to ID comparison");
}
// Fallback: higher ID usually means newer
return tx1.id > tx2.id;
}
/**
* COMPARISON HELPER: Check if one transaction is better than another
*/
private boolean isBetterTransaction(Transaction newTx, Transaction existingTx) {
int newPriority = getStatusPriority(newTx.status);
int existingPriority = getStatusPriority(existingTx.status);
if (newPriority > existingPriority) {
return true; // Better status
} else if (newPriority == existingPriority) {
return isNewerTransaction(newTx, existingTx); // Same status, check if newer
}
return false; // Existing transaction is better
}
/**
* STATUS PRIORITY SYSTEM: Comprehensive status ranking
*/
private int getStatusPriority(String status) {
if (status == null) return 0;
String statusLower = status.toLowerCase().trim();
switch (statusLower) {
// Tier 1: Confirmed payments (highest priority)
case "paid":
case "settlement":
case "capture":
case "success":
case "completed":
return 100;
// Tier 2: Processing states
case "pending":
case "processing":
case "authorized":
return 50;
// Tier 3: Initial states
case "init":
case "initialized":
case "created":
return 20;
// Tier 4: Failed/cancelled states (lowest priority)
case "failed":
case "failure":
case "error":
case "declined":
case "expire":
case "expired":
case "cancel":
case "cancelled":
case "timeout":
return 10;
// Tier 5: Unknown status
default:
Log.w("TransactionActivity", "Unknown status encountered: " + status);
return 1;
}
}
// REST OF THE CLASS: Existing methods remain the same
@Override
public void onPrintClick(Transaction transaction) {
// Open ReceiptActivity with transaction data
@ -298,40 +545,95 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// Add calling activity information for proper back navigation
intent.putExtra("calling_activity", "TransactionActivity");
// FIXED: Extract and send raw amount properly
// Extract and send raw amount properly
String rawAmount = extractRawAmount(transaction.amount);
Log.d("TransactionActivity", "Opening receipt for transaction: " + transaction.referenceId +
", original amount: '" + transaction.amount + "', extracted: '" + rawAmount + "'");
", channel: " + transaction.channelCode + ", original amount: '" + transaction.amount + "'");
// Send transaction data to ReceiptActivity
intent.putExtra("transaction_id", transaction.referenceId);
intent.putExtra("reference_id", transaction.referenceId);
intent.putExtra("transaction_amount", rawAmount); // Send raw amount
intent.putExtra("reference_id", transaction.referenceId); // Nomor Transaksi
intent.putExtra("transaction_amount", rawAmount); // Total transaksi
intent.putExtra("gross_amount", rawAmount); // Consistent with transaction_amount
intent.putExtra("transaction_date", formatDate(transaction.createdAt));
intent.putExtra("created_at", transaction.createdAt); // Tanggal transaksi (will be formatted)
intent.putExtra("transaction_date", formatDate(transaction.createdAt)); // Backup formatted date
intent.putExtra("payment_method", getPaymentMethodName(transaction.channelCode, transaction.channelCategory));
intent.putExtra("card_type", transaction.channelCategory);
intent.putExtra("channel_code", transaction.channelCode);
intent.putExtra("channel_code", transaction.channelCode); // Metode Pembayaran
intent.putExtra("channel_category", transaction.channelCategory);
intent.putExtra("card_type", transaction.channelCategory);
intent.putExtra("merchant_name", transaction.merchantName);
intent.putExtra("merchant_location", "Jakarta, Indonesia");
// Add MID and TID (default values since not available in Transaction model)
intent.putExtra("mid", "71000026521"); // MID
intent.putExtra("tid", "73001500"); // TID
// ENHANCED: Use improved acquirer determination
String acquirer = getRealAcquirerForQris(transaction.referenceId, transaction.channelCode);
intent.putExtra("acquirer", acquirer); // Jenis Kartu
Log.d("TransactionActivity", "🎯 Determined acquirer: " + acquirer + " for channel: " + transaction.channelCode);
startActivity(intent);
}
/**
* FIXED: Extract raw amount from formatted string properly
* Handles various amount formats from backend
* ENHANCED: Dynamic acquirer determination instead of defaulting to GoPay
*/
private String determineAcquirerFromChannelCode(String channelCode) {
if (channelCode == null) return "unknown"; // CHANGED: unknown instead of gopay
switch (channelCode.toLowerCase()) {
case "qris":
// IMPROVED: For QRIS, try to get real acquirer or return generic
return "qris"; // Will be processed by receipt activity to find real acquirer
case "bca":
return "bca";
case "mandiri":
return "mandiri";
case "bni":
return "bni";
case "bri":
return "bri";
case "permata":
return "permata";
case "cimb":
return "cimb";
case "danamon":
return "danamon";
case "bsi":
return "bsi";
case "debit":
return "visa"; // Default for debit cards
case "credit":
return "mastercard"; // Default for credit cards
default:
return "unknown"; // CHANGED: unknown instead of gopay for unknown channels
}
}
/**
* NEW METHOD: Try to get real acquirer for QRIS transactions from current transaction data
* This method can be enhanced to query webhook API for real acquirer
*/
private String getRealAcquirerForQris(String referenceId, String channelCode) {
// If not QRIS, return channel code
if (!"QRIS".equalsIgnoreCase(channelCode)) {
return determineAcquirerFromChannelCode(channelCode);
}
// For QRIS, we could implement real-time acquirer lookup here
// For now, return "qris" and let ReceiptActivity handle the detection
Log.d("TransactionActivity", "🔍 QRIS transaction detected, deferring acquirer detection to ReceiptActivity");
return "qris";
}
private String extractRawAmount(String formattedAmount) {
if (formattedAmount == null || formattedAmount.isEmpty()) {
return "0";
}
Log.d("TransactionActivity", "Extracting raw amount from: '" + formattedAmount + "'");
// Remove currency symbols and spaces
String cleaned = formattedAmount
.replace("Rp. ", "")
.replace("Rp ", "")
@ -339,48 +641,35 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
.replace(" ", "")
.trim();
// Handle dots correctly
if (cleaned.contains(".")) {
String[] parts = cleaned.split("\\.");
if (parts.length == 2) {
String beforeDot = parts[0];
String afterDot = parts[1];
// If after dot is "00" or single "0", it's decimal format
if (afterDot.equals("00") || afterDot.equals("0")) {
cleaned = beforeDot;
}
// If after dot has 3 digits, it's thousand separator
else if (afterDot.length() == 3) {
} else if (afterDot.length() == 3) {
cleaned = beforeDot + afterDot;
}
// Handle other cases based on context
else {
// If beforeDot is 1-3 digits and afterDot is 3 digits, likely thousand separator
} else {
if (beforeDot.length() <= 3 && afterDot.length() == 3) {
cleaned = beforeDot + afterDot;
} else {
// Otherwise treat as decimal and remove decimal part
cleaned = beforeDot;
}
}
} else if (parts.length > 2) {
// Multiple dots - treat as thousand separators
cleaned = String.join("", parts);
}
}
// Remove any remaining commas
cleaned = cleaned.replace(",", "");
// Validate result is numeric
try {
Long.parseLong(cleaned);
Log.d("TransactionActivity", "Successfully extracted: '" + formattedAmount + "' -> '" + cleaned + "'");
return cleaned;
} catch (NumberFormatException e) {
Log.e("TransactionActivity", "Invalid amount after cleaning: '" + cleaned + "' from '" + formattedAmount + "'");
Log.e("TransactionActivity", "Invalid amount: " + formattedAmount);
return "0";
}
}
@ -389,21 +678,15 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (channelCode == null) return "Unknown";
switch (channelCode.toUpperCase()) {
case "QRIS":
return "QRIS";
case "DEBIT":
return "Kartu Debit";
case "CREDIT":
return "Kartu Kredit";
case "QRIS": return "QRIS";
case "DEBIT": return "Kartu Debit";
case "CREDIT": return "Kartu Kredit";
case "BCA":
case "MANDIRI":
case "BNI":
case "BRI":
return "Kartu " + channelCode.toUpperCase();
case "CASH":
return "Tunai";
case "EDC":
return "EDC";
case "BRI": return "Kartu " + channelCode.toUpperCase();
case "CASH": return "Tunai";
case "EDC": return "EDC";
default:
if (channelCategory != null && !channelCategory.isEmpty()) {
return channelCategory.toUpperCase();
@ -419,11 +702,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
Date date = inputFormat.parse(rawDate);
return outputFormat.format(date);
} catch (Exception e) {
return rawDate; // Fallback ke format asli jika parsing gagal
return rawDate;
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {