diff --git a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java index 12a1174..c06ae57 100644 --- a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java +++ b/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java @@ -32,6 +32,8 @@ import java.net.URI; import java.net.URL; import java.text.NumberFormat; import java.util.Locale; +import java.util.List; +import java.util.ArrayList; public class QrisResultActivity extends AppCompatActivity { private ImageView qrImageView; @@ -42,6 +44,15 @@ public class QrisResultActivity extends AppCompatActivity { private TextView statusTextView; private Button returnMainButton; private ProgressBar progressBar; + + // QR Refresh Components + private TextView timerTextView; + private TextView qrStatusTextView; + private Handler qrRefreshHandler; + private Runnable qrRefreshRunnable; + private int countdownSeconds = 60; + private boolean isQrRefreshActive = true; + private String orderId; private String grossAmount; private String referenceId; @@ -49,29 +60,47 @@ public class QrisResultActivity extends AppCompatActivity { private String transactionTime; private String acquirer; private String merchantId; + private String currentQrImageUrl; + private int originalAmount; + private String backendBase = "https://be-edc.msvc.app"; private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans"; // Server key for signature generation private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; + private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge"; + + private List allOrderIds = new ArrayList<>(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_qris_result); - // Set up the toolbar - Toolbar toolbar = findViewById(R.id.toolbar); - if (toolbar != null) { - setSupportActionBar(toolbar); - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowHomeEnabled(true); - getSupportActionBar().setTitle("QRIS Payment"); - } - } - // Initialize views + initializeViews(); + + // Get intent data + getIntentData(); + + // Initialize order ID tracking + allOrderIds.add(orderId); // Tambahkan order ID pertama + Log.d("QrisResultFlow", "🆔 Initial order ID added: " + orderId); + + // Setup UI + setupUI(); + + // Start QR refresh timer + startQrRefreshTimer(); + + // Start polling for pending payment log + pollPendingPaymentLog(orderId); + + // Set up click listeners + setupClickListeners(); + } + + private void initializeViews() { qrImageView = findViewById(R.id.qrImageView); amountTextView = findViewById(R.id.amountTextView); referenceTextView = findViewById(R.id.referenceTextView); @@ -80,11 +109,17 @@ public class QrisResultActivity extends AppCompatActivity { statusTextView = findViewById(R.id.statusTextView); returnMainButton = findViewById(R.id.returnMainButton); progressBar = findViewById(R.id.progressBar); + timerTextView = findViewById(R.id.timerTextView); + qrStatusTextView = findViewById(R.id.qrStatusTextView); + + // Initialize handler for QR refresh + qrRefreshHandler = new Handler(Looper.getMainLooper()); + } - // Get intent data + private void getIntentData() { Intent intent = getIntent(); - String qrImageUrl = intent.getStringExtra("qrImageUrl"); - int amount = intent.getIntExtra("amount", 0); + currentQrImageUrl = intent.getStringExtra("qrImageUrl"); + originalAmount = intent.getIntExtra("amount", 0); referenceId = intent.getStringExtra("referenceId"); orderId = intent.getStringExtra("orderId"); grossAmount = intent.getStringExtra("grossAmount"); @@ -95,84 +130,460 @@ public class QrisResultActivity extends AppCompatActivity { // Enhanced logging for debugging Log.d("QrisResultFlow", "=== QRIS RESULT ACTIVITY STARTED ==="); - Log.d("QrisResultFlow", "QR Image URL: " + qrImageUrl); - Log.d("QrisResultFlow", "Amount (int): " + amount); + Log.d("QrisResultFlow", "QR Image URL: " + currentQrImageUrl); + Log.d("QrisResultFlow", "Amount (int): " + originalAmount); Log.d("QrisResultFlow", "Gross Amount (string): " + grossAmount); Log.d("QrisResultFlow", "Reference ID: " + referenceId); Log.d("QrisResultFlow", "Order ID: " + orderId); Log.d("QrisResultFlow", "Transaction ID: " + transactionId); - Log.d("QrisResultFlow", "Transaction Time: " + transactionTime); - Log.d("QrisResultFlow", "Acquirer: " + acquirer); - Log.d("QrisResultFlow", "Merchant ID: " + merchantId); Log.d("QrisResultFlow", "======================================"); + } + private void setupUI() { // Validate required data if (orderId == null || transactionId == null) { Log.e("QrisResultFlow", "Critical error: orderId or transactionId is null!"); - Log.e("QrisResultFlow", "Intent extras: " + intent.getExtras()); Toast.makeText(this, "Missing transaction details! Cannot proceed.", Toast.LENGTH_LONG).show(); finish(); return; } - if (qrImageUrl == null || qrImageUrl.isEmpty()) { - Log.e("QrisResultFlow", "Critical error: QR image URL is null or empty!"); - Toast.makeText(this, "Missing QR code URL! Cannot display QR code.", Toast.LENGTH_LONG).show(); - } - // Display formatted amount - String formattedAmount = formatCurrency(grossAmount != null ? grossAmount : String.valueOf(amount)); - amountTextView.setText("Amount: " + formattedAmount); + String formattedAmount = formatCurrency(grossAmount != null ? grossAmount : String.valueOf(originalAmount)); + amountTextView.setText(formattedAmount); referenceTextView.setText("Reference ID: " + referenceId); - // Load QR image if URL is available - if (qrImageUrl != null && !qrImageUrl.isEmpty()) { - Log.d("QrisResultFlow", "Loading QR image from: " + qrImageUrl); - new DownloadImageTask(qrImageView).execute(qrImageUrl); - } else { - Log.w("QrisResultFlow", "QR image URL is not available"); - qrImageView.setVisibility(View.GONE); - downloadQrisButton.setEnabled(false); - } + // Load initial QR image + loadQrImage(currentQrImageUrl); // Initialize UI state checkStatusButton.setEnabled(false); statusTextView.setText("Waiting for payment..."); - // Start polling for pending payment log - pollPendingPaymentLog(orderId); + // Setup QR refresh status text + qrStatusTextView.setText("QR Code akan refresh dalam"); + qrStatusTextView.setVisibility(View.VISIBLE); + timerTextView.setVisibility(View.VISIBLE); - // Set up click listeners - setupClickListeners(); + startContinuousPaymentMonitoring(); + } + + private void startQrRefreshTimer() { + countdownSeconds = 60; + isQrRefreshActive = true; + + qrRefreshRunnable = new Runnable() { + @Override + public void run() { + if (!isQrRefreshActive) { + return; + } + + if (countdownSeconds > 0) { + // Update countdown display + timerTextView.setText(String.valueOf(countdownSeconds)); + countdownSeconds--; + + // Schedule next update in 1 second + qrRefreshHandler.postDelayed(this, 1000); + } else { + // Time to refresh QR code + Log.d("QrisResultFlow", "🔄 QR Code refresh time reached - generating new QR"); + refreshQrCode(); + } + } + }; + + qrRefreshHandler.post(qrRefreshRunnable); + Log.d("QrisResultFlow", "🕒 QR refresh timer started - 60 seconds countdown"); + } + + private void refreshQrCode() { + if (!isQrRefreshActive) { + return; + } + + Log.d("QrisResultFlow", "🔄 Starting QR code refresh..."); + + // Show loading state + timerTextView.setText("..."); + qrStatusTextView.setText("Generating new QR Code..."); + + // Generate new QR code in background + new Thread(() -> { + try { + String newQrUrl = generateNewQrCode(); + + runOnUiThread(() -> { + if (newQrUrl != null && !newQrUrl.isEmpty()) { + // Successfully generated new QR + currentQrImageUrl = newQrUrl; + loadQrImage(newQrUrl); + + // ✅ TAMBAHKAN: Track new order ID + allOrderIds.add(orderId); // orderId sudah diupdate di generateNewQrCode() + Log.d("QrisResultFlow", "🆔 New order ID added: " + orderId + " (Total: " + allOrderIds.size() + ")"); + + // Update UI with new data + updateUIAfterRefresh(); + + // Restart timer + countdownSeconds = 60; + qrStatusTextView.setText("QR Code akan refresh dalam"); + Log.d("QrisResultFlow", "✅ QR code refreshed successfully"); + Toast.makeText(QrisResultActivity.this, "QR Code refreshed", Toast.LENGTH_SHORT).show(); + } else { + // Failed to generate new QR + Log.e("QrisResultFlow", "❌ Failed to refresh QR code"); + qrStatusTextView.setText("Failed to refresh QR - trying again in 30s"); + countdownSeconds = 30; // Retry in 30 seconds + Toast.makeText(QrisResultActivity.this, "QR refresh failed, retrying...", Toast.LENGTH_SHORT).show(); + } + + // Continue timer + qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000); + }); + + } catch (Exception e) { + Log.e("QrisResultFlow", "❌ QR refresh error: " + e.getMessage(), e); + + runOnUiThread(() -> { + qrStatusTextView.setText("QR refresh error - retrying in 30s"); + countdownSeconds = 30; + qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000); + Toast.makeText(QrisResultActivity.this, "QR refresh error", Toast.LENGTH_SHORT).show(); + }); + } + }).start(); + } + + private void pollMultipleOrderIds() { + Log.d("QrisResultFlow", "🔍 Starting polling for multiple order IDs"); + + Handler multiPollHandler = new Handler(Looper.getMainLooper()); + + Runnable multiPollRunnable = new Runnable() { + @Override + public void run() { + // Check semua order IDs yang pernah dibuat + checkAllOrderIdsStatus(); + + // Schedule next check jika activity masih aktif + if (!isFinishing() && isQrRefreshActive) { + multiPollHandler.postDelayed(this, 3000); // Check every 3 seconds + } + } + }; + + multiPollHandler.post(multiPollRunnable); + } + + // ✅ METHOD BARU untuk check status dari semua order IDs + private void checkAllOrderIdsStatus() { + new Thread(() -> { + try { + Log.d("QrisResultFlow", "🔍 Checking status for " + allOrderIds.size() + " order IDs"); + + for (String checkOrderId : allOrderIds) { + if (checkOrderId == null || checkOrderId.isEmpty()) continue; + + // Check di webhook logs + String urlStr = backendBase + "/api-logs?request_body_search_strict=" + + java.net.URLEncoder.encode("{\"order_id\":\"" + checkOrderId + "\"}", "UTF-8"); + + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + int responseCode = conn.getResponseCode(); + + if (responseCode == 200) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + + JSONObject json = new JSONObject(response.toString()); + JSONArray results = json.optJSONArray("results"); + + if (results != null && results.length() > 0) { + for (int i = 0; i < results.length(); i++) { + JSONObject log = results.getJSONObject(i); + JSONObject reqBody = log.optJSONObject("request_body"); + + if (reqBody != null) { + String transactionStatus = reqBody.optString("transaction_status"); + String logOrderId = reqBody.optString("order_id"); + + Log.d("QrisResultFlow", "📊 Order ID: " + logOrderId + " -> Status: " + transactionStatus); + + // Check untuk settlement/success status + if (checkOrderId.equals(logOrderId) && + (transactionStatus.equals("settlement") || + transactionStatus.equals("capture") || + transactionStatus.equals("success"))) { + + Log.d("QrisResultFlow", "🎉 PAYMENT FOUND! Order ID: " + checkOrderId + " Status: " + transactionStatus); + + // Update transaction ID untuk sync + String foundTransactionId = reqBody.optString("transaction_id", transactionId); + if (!foundTransactionId.isEmpty()) { + transactionId = foundTransactionId; + } + + // Stop polling dan tampilkan success + runOnUiThread(() -> { + stopQrRefresh(); + syncTransactionStatusToBackend("PAID"); + showPaymentSuccess(); + Toast.makeText(QrisResultActivity.this, + "Payment Successful! 🎉 (QR #" + (allOrderIds.indexOf(checkOrderId) + 1) + ")", + Toast.LENGTH_LONG).show(); + }); + + return; // Exit method + } + } + } + } + } + } + + } catch (Exception e) { + Log.e("QrisResultFlow", "❌ Multi-order polling error: " + e.getMessage(), e); + } + }).start(); + } + + private void updateUIAfterRefresh() { + // You can update reference text to show it's refreshed + String refreshTime = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()); + referenceTextView.setText("Reference ID: " + referenceId + " (Refreshed at " + refreshTime + ")"); + + // Reset polling for new order ID if needed + // Note: You might want to restart polling for the new orderId + Log.d("QrisResultFlow", "🔄 UI updated after QR refresh"); + } + + private String generateNewQrCode() { + try { + Log.d("QrisResultFlow", "🔧 Generating new QR code with fresh UUID"); + + // FIXED: Generate NEW UUID for refresh to avoid duplicate order_id + String newOrderId = java.util.UUID.randomUUID().toString(); + Log.d("QrisResultFlow", "🆕 New Order ID for refresh: " + newOrderId); + Log.d("QrisResultFlow", "🔄 Original Order ID: " + orderId); + + // ✅ FIXED: Create custom_field FIRST, before logging + JSONObject customField1 = new JSONObject(); + customField1.put("refresh_of", orderId); // Track original order + customField1.put("refresh_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date())); + customField1.put("original_reference", referenceId); // Key untuk tracking! + customField1.put("refresh_count", System.currentTimeMillis()); // Unique identifier + customField1.put("parent_transaction_id", transactionId); // Link ke parent transaction + + // ✅ NOW log AFTER customField1 is defined + Log.d("QrisResultFlow", "🔄 Enhanced refresh tracking:"); + Log.d("QrisResultFlow", " Original Order: " + orderId); + Log.d("QrisResultFlow", " New Order: " + newOrderId); + Log.d("QrisResultFlow", " Reference ID: " + referenceId); + Log.d("QrisResultFlow", " Custom Field: " + customField1.toString()); + + // Create QRIS charge JSON payload with NEW order_id + JSONObject payload = new JSONObject(); + payload.put("payment_type", "qris"); + + JSONObject transactionDetails = new JSONObject(); + transactionDetails.put("order_id", newOrderId); // Use NEW UUID + transactionDetails.put("gross_amount", originalAmount); + payload.put("transaction_details", transactionDetails); + + // Add customer details + JSONObject customerDetails = new JSONObject(); + customerDetails.put("first_name", "Test"); + customerDetails.put("last_name", "Customer"); + customerDetails.put("email", "test@example.com"); + customerDetails.put("phone", "081234567890"); + payload.put("customer_details", customerDetails); + + // Add item details + JSONArray itemDetails = new JSONArray(); + JSONObject item = new JSONObject(); + item.put("id", "item1_refresh_" + System.currentTimeMillis()); + item.put("price", originalAmount); + item.put("quantity", 1); + item.put("name", "QRIS Payment - Refreshed " + new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()) + + " (Ref: " + referenceId + ")"); // Include reference in item name + itemDetails.put(item); + payload.put("item_details", itemDetails); + + // ✅ Add custom_field for tracking refresh (AFTER it's created) + payload.put("custom_field1", customField1.toString()); + + Log.d("QrisResultFlow", "📤 Refresh payload: " + payload.toString()); + + // Make API call to Midtrans + URL url = new URI(MIDTRANS_CHARGE_URL).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Authorization", MIDTRANS_AUTH); + conn.setRequestProperty("X-Override-Notification", webhookUrl); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0 QR-Refresh"); + conn.setDoOutput(true); + conn.setConnectTimeout(30000); + conn.setReadTimeout(30000); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = payload.toString().getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + Log.d("QrisResultFlow", "📥 QR refresh response code: " + responseCode); + + if (responseCode == 200 || responseCode == 201) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); + StringBuilder response = new StringBuilder(); + String responseLine; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + + Log.d("QrisResultFlow", "📥 QR refresh response preview: " + + (response.length() > 200 ? response.substring(0, 200) + "..." : response.toString())); + + JSONObject jsonResponse = new JSONObject(response.toString()); + + // Check for errors in response + if (jsonResponse.has("status_code")) { + String statusCode = jsonResponse.getString("status_code"); + if (!statusCode.equals("201")) { + String statusMessage = jsonResponse.optString("status_message", "Unknown error"); + Log.e("QrisResultFlow", "❌ QR refresh failed with status: " + statusCode + " - " + statusMessage); + + // Log full response for debugging + Log.e("QrisResultFlow", "❌ Full error response: " + response.toString()); + return null; + } + } + + // Extract new QR URL and update transaction info + if (jsonResponse.has("actions")) { + JSONArray actionsArray = jsonResponse.getJSONArray("actions"); + if (actionsArray.length() > 0) { + JSONObject actions = actionsArray.getJSONObject(0); + String newQrUrl = actions.getString("url"); + + // Update transaction info with new data + String newTransactionId = jsonResponse.optString("transaction_id", transactionId); + String newTransactionTime = jsonResponse.optString("transaction_time", transactionTime); + + // Update class variables for webhook simulation + this.transactionId = newTransactionId; + this.transactionTime = newTransactionTime; + this.orderId = newOrderId; // Update to new order ID + + Log.d("QrisResultFlow", "✅ QR refresh successful!"); + Log.d("QrisResultFlow", "🆕 New QR URL: " + newQrUrl); + Log.d("QrisResultFlow", "🆕 New Transaction ID: " + newTransactionId); + Log.d("QrisResultFlow", "🆕 New Order ID: " + newOrderId); + + return newQrUrl; + } + } + + Log.e("QrisResultFlow", "❌ No actions found in refresh response"); + return null; + + } else { + // Handle error response + InputStream errorStream = conn.getErrorStream(); + String errorResponse = ""; + + if (errorStream != null) { + BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8")); + StringBuilder errorBuilder = new StringBuilder(); + String responseLine; + while ((responseLine = br.readLine()) != null) { + errorBuilder.append(responseLine.trim()); + } + errorResponse = errorBuilder.toString(); + } + + Log.e("QrisResultFlow", "❌ QR refresh HTTP error " + responseCode + ": " + errorResponse); + + // Try to parse error for better logging + try { + if (!errorResponse.isEmpty()) { + JSONObject errorJson = new JSONObject(errorResponse); + String errorMessage = errorJson.optString("error_messages", + errorJson.optString("status_message", + errorJson.optString("message", "Unknown error"))); + Log.e("QrisResultFlow", "❌ Parsed error message: " + errorMessage); + } + } catch (JSONException e) { + Log.e("QrisResultFlow", "❌ Could not parse error response"); + } + + return null; + } + + } catch (Exception e) { + Log.e("QrisResultFlow", "❌ QR refresh exception: " + e.getMessage(), e); + return null; + } + } + + private void loadQrImage(String qrImageUrl) { + if (qrImageUrl != null && !qrImageUrl.isEmpty()) { + Log.d("QrisResultFlow", "🖼️ Loading QR image from: " + qrImageUrl); + new DownloadImageTask(qrImageView).execute(qrImageUrl); + } else { + Log.w("QrisResultFlow", "⚠️ QR image URL is not available"); + qrImageView.setVisibility(View.GONE); + downloadQrisButton.setEnabled(false); + } + } + + private void stopQrRefresh() { + isQrRefreshActive = false; + if (qrRefreshHandler != null && qrRefreshRunnable != null) { + qrRefreshHandler.removeCallbacks(qrRefreshRunnable); + } + + // Hide timer elements + timerTextView.setVisibility(View.GONE); + qrStatusTextView.setVisibility(View.GONE); + + Log.d("QrisResultFlow", "🛑 QR refresh timer stopped"); } private void setupClickListeners() { // Download QRIS button - downloadQrisButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - downloadQrCode(); - } - }); + downloadQrisButton.setOnClickListener(v -> downloadQrCode()); // Check Payment Status button - checkStatusButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d("QrisResultFlow", "Check status button clicked"); - simulateWebhook(); - } + checkStatusButton.setOnClickListener(v -> { + Log.d("QrisResultFlow", "Check status button clicked"); + stopQrRefresh(); // Stop QR refresh when payment is being processed + simulateWebhook(); }); // Return to Main Screen button - returnMainButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finishAffinity(); - } + returnMainButton.setOnClickListener(v -> { + stopQrRefresh(); // Stop timer when leaving + Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finishAffinity(); }); } @@ -222,8 +633,8 @@ public class QrisResultActivity extends AppCompatActivity { URL url = new URI(urlDisplay).toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setDoInput(true); - connection.setConnectTimeout(10000); // 10 seconds - connection.setReadTimeout(10000); // 10 seconds + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); connection.connect(); @@ -259,7 +670,6 @@ public class QrisResultActivity extends AppCompatActivity { } else { Log.e("QrisResultFlow", "Failed to display QR code image"); bmImage.setImageResource(android.R.drawable.ic_menu_report_image); - // Show error message to user if available if (errorMessage != null && bmImage.getContext() != null) { Toast.makeText(bmImage.getContext(), errorMessage, Toast.LENGTH_LONG).show(); } @@ -285,22 +695,217 @@ public class QrisResultActivity extends AppCompatActivity { } } + private void syncTransactionStatusToBackend(String finalStatus) { + Log.d("QrisResultFlow", "🔄 Syncing status '" + finalStatus + "' to backend for reference: " + referenceId); + + new Thread(() -> { + try { + // Update status di backend berdasarkan reference_id (bukan order_id yang berubah) + JSONObject updatePayload = new JSONObject(); + updatePayload.put("status", finalStatus); + updatePayload.put("transaction_status", finalStatus); + updatePayload.put("updated_at", getCurrentISOTime()); + updatePayload.put("settlement_time", getCurrentISOTime()); + + // FIXED: Update berdasarkan reference_id yang tetap, bukan order_id yang berubah + String updateUrl = backendBase + "/transactions/update-by-reference"; + URL url = new URI(updateUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setDoOutput(true); + conn.setConnectTimeout(15000); + conn.setReadTimeout(15000); + + // Body dengan reference_id sebagai identifier + JSONObject body = new JSONObject(); + body.put("reference_id", referenceId); // FIXED: Gunakan reference_id yang tidak berubah + body.put("update_data", updatePayload); + + Log.d("QrisResultFlow", "📤 Backend update payload: " + body.toString()); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = body.toString().getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + Log.d("QrisResultFlow", "📥 Backend update response: " + responseCode); + + if (responseCode == 200 || responseCode == 201) { + // Read response + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + + Log.d("QrisResultFlow", "✅ Backend status updated successfully: " + response.toString()); + } else { + // Read error response + InputStream errorStream = conn.getErrorStream(); + if (errorStream != null) { + BufferedReader br = new BufferedReader(new InputStreamReader(errorStream)); + StringBuilder errorResponse = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + errorResponse.append(line); + } + Log.e("QrisResultFlow", "❌ Backend update failed: " + responseCode + " - " + errorResponse.toString()); + } + } + + } catch (Exception e) { + Log.e("QrisResultFlow", "❌ Backend sync error: " + e.getMessage(), e); + } + }).start(); + } + + private String getCurrentISOTime() { + return new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + .format(new java.util.Date()); + } + + private void syncStatusToBackend(String referenceId, String finalStatus, String paidOrderId) { + new Thread(() -> { + try { + Log.d("QrisResultFlow", "🔄 Syncing status to backend - Ref: " + referenceId + " Status: " + finalStatus); + + // Create update payload + JSONObject updatePayload = new JSONObject(); + updatePayload.put("status", finalStatus); + updatePayload.put("payment_status", finalStatus); + updatePayload.put("paid_order_id", paidOrderId); // Order ID yang actual dibayar + updatePayload.put("updated_at", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date())); + updatePayload.put("payment_completed_at", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date())); + + // API call to backend untuk update transaction berdasarkan reference_id + String updateUrl = backendBase + "/transactions/update-by-reference"; + URL url = new URI(updateUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setDoOutput(true); + conn.setConnectTimeout(15000); + conn.setReadTimeout(15000); + + // Request body dengan reference_id sebagai filter + JSONObject requestBody = new JSONObject(); + requestBody.put("reference_id", referenceId); + requestBody.put("update_data", updatePayload); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = requestBody.toString().getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + Log.d("QrisResultFlow", "📥 Backend sync response: " + responseCode); + + if (responseCode == 200 || responseCode == 201) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + Log.d("QrisResultFlow", "✅ Backend sync successful: " + response.toString()); + } else { + // Log error response + InputStream errorStream = conn.getErrorStream(); + if (errorStream != null) { + BufferedReader br = new BufferedReader(new InputStreamReader(errorStream)); + StringBuilder errorResponse = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + errorResponse.append(line); + } + Log.e("QrisResultFlow", "❌ Backend sync failed: " + responseCode + " - " + errorResponse.toString()); + } + } + + } catch (Exception e) { + Log.e("QrisResultFlow", "❌ Backend sync error: " + e.getMessage(), e); + } + }).start(); + } + + private void showPaymentSuccess() { + Log.d("QrisResultFlow", "Showing payment success screen"); + + // Stop QR refresh when payment is successful + stopQrRefresh(); + + // ✅ SYNC STATUS KE BACKEND + syncStatusToBackend(referenceId, "PAID", orderId); + + // Hide payment elements + qrImageView.setVisibility(View.GONE); + amountTextView.setVisibility(View.GONE); + referenceTextView.setVisibility(View.GONE); + downloadQrisButton.setVisibility(View.GONE); + checkStatusButton.setVisibility(View.GONE); + + // Show success elements + statusTextView.setVisibility(View.VISIBLE); + statusTextView.setText("✅ Payment Successful!\n\nTransaction ID: " + transactionId + + "\nReference: " + referenceId + + "\nAmount: " + formatCurrency(grossAmount)); + + returnMainButton.setVisibility(View.VISIBLE); + + Toast.makeText(this, "Payment simulation completed successfully!", Toast.LENGTH_LONG).show(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + stopQrRefresh(); // Cleanup timer when activity is destroyed + } + + @Override + protected void onPause() { + super.onPause(); + // Keep timer running in background, but could be paused if needed + } + + @Override + protected void onResume() { + super.onResume(); + // Resume timer if it was paused + } + + @Override + public void onBackPressed() { + stopQrRefresh(); // Stop timer when user goes back + Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finishAffinity(); + } + private void pollPendingPaymentLog(final String orderId) { Log.d("QrisResultFlow", "Starting polling for orderId: " + orderId); progressBar.setVisibility(View.VISIBLE); statusTextView.setText("Checking payment status..."); new Thread(() -> { - int maxAttempts = 12; // Increased attempts - int intervalMs = 2000; // 2 seconds interval + int maxAttempts = 12; + int intervalMs = 2000; int attempt = 0; boolean found = false; while (attempt < maxAttempts && !found) { try { - String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}"; - Log.d("QrisResultFlow", "Polling attempt " + (attempt + 1) + "/" + maxAttempts); - Log.d("QrisResultFlow", "Polling URL: " + urlStr); + // FIXED: Poll for CURRENT orderId (might be refreshed) + String currentOrderId = this.orderId; // Use current order ID + String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + currentOrderId + "\"}"; + Log.d("QrisResultFlow", "Polling attempt " + (attempt + 1) + "/" + maxAttempts + " for: " + currentOrderId); URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); @@ -311,7 +916,6 @@ public class QrisResultActivity extends AppCompatActivity { conn.setReadTimeout(5000); int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "Polling response code: " + responseCode); if (responseCode == 200) { BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); @@ -321,8 +925,6 @@ public class QrisResultActivity extends AppCompatActivity { response.append(line); } - Log.d("QrisResultFlow", "Polling response: " + response.toString()); - JSONObject json = new JSONObject(response.toString()); JSONArray results = json.optJSONArray("results"); @@ -338,11 +940,34 @@ public class QrisResultActivity extends AppCompatActivity { String logOrderId = reqBody.optString("order_id"); Log.d("QrisResultFlow", "Log entry " + i + ": order_id=" + logOrderId + - ", transaction_status=" + transactionStatus); + ", transaction_status=" + transactionStatus); - if ("pending".equals(transactionStatus) && orderId.equals(logOrderId)) { + // FIXED: Check for any valid payment status, not just pending + if (currentOrderId.equals(logOrderId) && + (transactionStatus.equals("pending") || + transactionStatus.equals("settlement") || + transactionStatus.equals("capture") || + transactionStatus.equals("success"))) { found = true; - Log.d("QrisResultFlow", "Found matching pending payment log!"); + + // FIXED: If payment is already settled, show success immediately + if (transactionStatus.equals("settlement") || + transactionStatus.equals("capture") || + transactionStatus.equals("success")) { + + Log.d("QrisResultFlow", "🎉 Payment already completed with status: " + transactionStatus); + + // Stop polling and show success + new Handler(Looper.getMainLooper()).post(() -> { + progressBar.setVisibility(View.GONE); + stopQrRefresh(); // Stop QR refresh + showPaymentSuccess(); + Toast.makeText(QrisResultActivity.this, "Payment completed!", Toast.LENGTH_LONG).show(); + }); + return; // Exit polling thread + } + + Log.d("QrisResultFlow", "Found matching payment log with status: " + transactionStatus); break; } } @@ -354,7 +979,7 @@ public class QrisResultActivity extends AppCompatActivity { Log.w("QrisResultFlow", "Polling failed with HTTP code: " + responseCode); } } catch (Exception e) { - Log.e("QrisResultFlow", "Polling error on attempt " + (attempt + 1) + ": " + e.getMessage(), e); + Log.e("QrisResultFlow", "Polling error on attempt " + (attempt + 1) + ": " + e.getMessage()); } if (!found) { @@ -363,7 +988,6 @@ public class QrisResultActivity extends AppCompatActivity { try { Thread.sleep(intervalMs); } catch (InterruptedException ignored) { - Log.d("QrisResultFlow", "Polling interrupted"); break; } } @@ -376,19 +1000,17 @@ public class QrisResultActivity extends AppCompatActivity { if (logFound) { checkStatusButton.setEnabled(true); statusTextView.setText("Ready to simulate payment"); - Toast.makeText(QrisResultActivity.this, "Pending payment log found! You can now simulate payment.", Toast.LENGTH_SHORT).show(); - Log.d("QrisResultFlow", "Polling completed successfully - payment log found"); + Toast.makeText(QrisResultActivity.this, "Payment log found!", Toast.LENGTH_SHORT).show(); } else { statusTextView.setText("Payment log not found"); - Toast.makeText(QrisResultActivity.this, "Pending payment log NOT found. You may still try to simulate payment.", Toast.LENGTH_LONG).show(); - Log.w("QrisResultFlow", "Polling completed - payment log NOT found after " + maxAttempts + " attempts"); - // Enable button anyway to allow manual testing + Toast.makeText(QrisResultActivity.this, "Payment log not found. Manual simulation available.", Toast.LENGTH_LONG).show(); checkStatusButton.setEnabled(true); } }); }).start(); } + private String getServerKey() { try { String base64 = MIDTRANS_AUTH.replace("Basic ", ""); @@ -419,43 +1041,152 @@ public class QrisResultActivity extends AppCompatActivity { } } - // Simulate webhook callback + private void startContinuousPaymentMonitoring() { + Log.d("QrisResultFlow", "🔍 Starting continuous payment monitoring"); + + // Create a monitoring handler separate from QR refresh + Handler paymentMonitorHandler = new Handler(Looper.getMainLooper()); + + Runnable paymentMonitorRunnable = new Runnable() { + @Override + public void run() { + // Check payment status every 5 seconds + checkCurrentPaymentStatus(); + + // Schedule next check if activity is still active + if (!isFinishing() && isQrRefreshActive) { + paymentMonitorHandler.postDelayed(this, 5000); // Check every 5 seconds + } + } + }; + + // Start monitoring + paymentMonitorHandler.post(paymentMonitorRunnable); + } + + private void checkCurrentPaymentStatus() { + new Thread(() -> { + try { + // Check status untuk current transaction ID + String statusUrl = "https://api.sandbox.midtrans.com/v2/" + transactionId + "/status"; + + Log.d("QrisResultFlow", "🔍 Checking payment status for: " + transactionId); + + URL url = new URI(statusUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Authorization", MIDTRANS_AUTH); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + + int responseCode = conn.getResponseCode(); + + if (responseCode == 200) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + + JSONObject statusResponse = new JSONObject(response.toString()); + String transactionStatus = statusResponse.optString("transaction_status", ""); + String paymentType = statusResponse.optString("payment_type", ""); + String grossAmount = statusResponse.optString("gross_amount", ""); + + Log.d("QrisResultFlow", "💳 Payment status check result:"); + Log.d("QrisResultFlow", " Status: " + transactionStatus); + Log.d("QrisResultFlow", " Payment Type: " + paymentType); + Log.d("QrisResultFlow", " Amount: " + grossAmount); + + // Check for success/settlement status + if (transactionStatus.equals("settlement") || + transactionStatus.equals("capture") || + transactionStatus.equals("success")) { + + Log.d("QrisResultFlow", "🎉 Payment detected as PAID! Status: " + transactionStatus); + + // ✅ TAMBAHKAN: Sync ke backend sebelum update UI + syncTransactionStatusToBackend("PAID"); + + // Update UI on main thread + runOnUiThread(() -> { + stopQrRefresh(); // Stop QR refresh + showPaymentSuccess(); + Toast.makeText(QrisResultActivity.this, "Payment Successful! 🎉", Toast.LENGTH_LONG).show(); + }); + + } else if (transactionStatus.equals("pending")) { + Log.d("QrisResultFlow", "⏳ Payment still pending"); + } else if (transactionStatus.equals("expire") || transactionStatus.equals("cancel")) { + Log.w("QrisResultFlow", "⚠️ Payment expired or cancelled: " + transactionStatus); + + // ✅ TAMBAHKAN: Sync status failed ke backend + syncTransactionStatusToBackend("FAILED"); + } else { + Log.d("QrisResultFlow", "📊 Payment status: " + transactionStatus); + } + + } else { + Log.w("QrisResultFlow", "⚠️ Payment status check failed: HTTP " + responseCode); + } + + } catch (Exception e) { + Log.e("QrisResultFlow", "❌ Payment status check error: " + e.getMessage(), e); + } + }).start(); + } + + // FIXED: Update simulateWebhook to use current transaction data private void simulateWebhook() { - Log.d("QrisResultFlow", "Starting webhook simulation"); + Log.d("QrisResultFlow", "🚀 Starting webhook simulation"); progressBar.setVisibility(View.VISIBLE); statusTextView.setText("Simulating payment..."); checkStatusButton.setEnabled(false); + // Stop QR refresh during payment simulation + stopQrRefresh(); + new Thread(() -> { try { - // Generate proper signature String serverKey = getServerKey(); - String signatureKey = generateSignature(orderId, "200", grossAmount, serverKey); + + // FIXED: Use current transaction data (after potential refresh) + String currentOrderId = this.orderId; + String currentTransactionId = this.transactionId; + String currentGrossAmount = this.grossAmount; + + Log.d("QrisResultFlow", "🎯 Simulating webhook for:"); + Log.d("QrisResultFlow", " Order ID: " + currentOrderId); + Log.d("QrisResultFlow", " Transaction ID: " + currentTransactionId); + Log.d("QrisResultFlow", " Amount: " + currentGrossAmount); + + String signatureKey = generateSignature(currentOrderId, "200", currentGrossAmount, serverKey); JSONObject payload = new JSONObject(); payload.put("transaction_type", "on-us"); payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z"); payload.put("transaction_status", "settlement"); - payload.put("transaction_id", transactionId); + payload.put("transaction_id", currentTransactionId); payload.put("status_message", "midtrans payment notification"); payload.put("status_code", "200"); payload.put("signature_key", signatureKey); payload.put("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z"); payload.put("payment_type", "qris"); - payload.put("order_id", orderId); + payload.put("order_id", currentOrderId); payload.put("merchant_id", merchantId != null ? merchantId : "DUMMY_MERCHANT_ID"); payload.put("issuer", acquirer != null ? acquirer : "gopay"); - payload.put("gross_amount", grossAmount); + payload.put("gross_amount", currentGrossAmount); payload.put("fraud_status", "accept"); payload.put("currency", "IDR"); payload.put("acquirer", acquirer != null ? acquirer : "gopay"); payload.put("shopeepay_reference_number", ""); payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID"); - Log.d("QrisResultFlow", "=== WEBHOOK SIMULATION ==="); - Log.d("QrisResultFlow", "Webhook URL: " + webhookUrl); - Log.d("QrisResultFlow", "Webhook payload: " + payload.toString()); - Log.d("QrisResultFlow", "=========================="); + Log.d("QrisResultFlow", "📤 Webhook payload preview: " + + (payload.toString().length() > 200 ? payload.toString().substring(0, 200) + "..." : payload.toString())); URL url = new URL(webhookUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); @@ -472,7 +1203,7 @@ public class QrisResultActivity extends AppCompatActivity { os.close(); int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "Webhook response code: " + responseCode); + Log.d("QrisResultFlow", "📥 Webhook response code: " + responseCode); BufferedReader br = new BufferedReader(new InputStreamReader( responseCode < 400 ? conn.getInputStream() : conn.getErrorStream())); @@ -482,13 +1213,13 @@ public class QrisResultActivity extends AppCompatActivity { response.append(line); } - Log.d("QrisResultFlow", "Webhook response: " + response.toString()); + Log.d("QrisResultFlow", "📥 Webhook response: " + + (response.length() > 200 ? response.substring(0, 200) + "..." : response.toString())); - // Wait a bit for processing Thread.sleep(2000); } catch (Exception e) { - Log.e("QrisResultFlow", "Webhook simulation error: " + e.getMessage(), e); + Log.e("QrisResultFlow", "❌ Webhook simulation error: " + e.getMessage(), e); } new Handler(Looper.getMainLooper()).post(() -> { @@ -498,59 +1229,23 @@ public class QrisResultActivity extends AppCompatActivity { }).start(); } - private void showPaymentSuccess() { - Log.d("QrisResultFlow", "Showing payment success screen"); - - // Hide payment elements - qrImageView.setVisibility(View.GONE); - amountTextView.setVisibility(View.GONE); - referenceTextView.setVisibility(View.GONE); - downloadQrisButton.setVisibility(View.GONE); - checkStatusButton.setVisibility(View.GONE); - - // Show success elements - statusTextView.setVisibility(View.VISIBLE); - statusTextView.setText("✅ Payment Successful!\n\nTransaction ID: " + transactionId + - "\nReference: " + referenceId + - "\nAmount: " + formatCurrency(grossAmount)); - - // Add receipt button - Button showReceiptButton = new Button(this); - showReceiptButton.setText("Show Receipt"); - showReceiptButton.setOnClickListener(v -> launchReceiptActivity()); - - // You can add this button to your layout programmatically - // or add it to your XML and show it here - - returnMainButton.setVisibility(View.VISIBLE); - - Toast.makeText(this, "Payment simulation completed successfully!", Toast.LENGTH_LONG).show(); - } - - // Fixed method for launching ReceiptActivity private void launchReceiptActivity() { Intent intent = new Intent(this, ReceiptActivity.class); - // Add calling activity information for proper back navigation intent.putExtra("calling_activity", "QrisResultActivity"); - - // Pass all the transaction data using available class variables - intent.putExtra("transaction_id", transactionId); // Midtrans transaction_id - intent.putExtra("reference_id", referenceId); // Your reference ID - intent.putExtra("order_id", orderId); // Order ID (UUID) - - // Use grossAmount for both transaction_amount and gross_amount + intent.putExtra("transaction_id", transactionId); + intent.putExtra("reference_id", referenceId); + intent.putExtra("order_id", orderId); intent.putExtra("transaction_amount", grossAmount != null ? grossAmount : "0"); - intent.putExtra("gross_amount", grossAmount); // From Midtrans response - + intent.putExtra("gross_amount", grossAmount); intent.putExtra("transaction_date", getCurrentDateTime()); intent.putExtra("payment_method", "QRIS"); intent.putExtra("card_type", "QRIS"); intent.putExtra("channel_code", "QRIS"); intent.putExtra("channel_category", "RETAIL_OUTLET"); - intent.putExtra("merchant_name", "Marcel Panjaitan"); // From your transaction data + intent.putExtra("merchant_name", "Marcel Panjaitan"); intent.putExtra("merchant_location", "Jakarta, Indonesia"); - intent.putExtra("acquirer", acquirer); // From Midtrans response + intent.putExtra("acquirer", acquirer); startActivity(intent); } @@ -568,12 +1263,4 @@ public class QrisResultActivity extends AppCompatActivity { } return super.onOptionsItemSelected(item); } - - @Override - public void onBackPressed() { - Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finishAffinity(); - } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java b/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java index 3da909e..0b13e85 100644 --- a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java +++ b/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java @@ -1,6 +1,6 @@ package com.example.bdkipoc; -import android.util.Log; // ADD THIS IMPORT +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,10 +11,22 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.core.content.ContextCompat; +// ✅ TAMBAHKAN MISSING IMPORTS INI: +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; import java.util.List; import java.text.NumberFormat; import java.util.Locale; +// ✅ TAMBAHKAN JSON IMPORTS: +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + public class TransactionAdapter extends RecyclerView.Adapter { private List transactionList; private OnPrintClickListener printClickListener; @@ -42,32 +54,58 @@ public class TransactionAdapter extends RecyclerView.Adapter Cleaned: '" + - cleanAmount + "' -> Formatted: '" + formattedAmount + "'"); - + Log.d("TransactionAdapter", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'"); + } catch (NumberFormatException e) { - Log.e("TransactionAdapter", "Error formatting amount: " + t.amount, e); - // Fallback: show original amount with Rp prefix if not already present + Log.e("TransactionAdapter", "❌ Amount format error: " + t.amount, e); String fallback = t.amount.startsWith("Rp") ? t.amount : "Rp " + t.amount; holder.amount.setText(fallback); } - // Set status with appropriate color - holder.status.setText(t.status.toUpperCase()); - setStatusColor(holder.status, t.status); + // ✅ ENHANCED STATUS HANDLING dengan comprehensive checking + String displayStatus = t.status; + + Log.d("TransactionAdapter", "🔍 Checking status for: " + t.referenceId + " (current: " + displayStatus + ")"); + + // Jika status adalah INIT atau PENDING, lakukan comprehensive check + if ("INIT".equalsIgnoreCase(t.status) || "PENDING".equalsIgnoreCase(t.status)) { + if (t.referenceId != null && !t.referenceId.isEmpty()) { + // Show checking state + holder.status.setText("CHECKING..."); + holder.status.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), + android.R.color.holo_orange_dark)); + + Log.d("TransactionAdapter", "🔄 Starting comprehensive check for: " + t.referenceId); + + // Check real status dari semua kemungkinan sources + checkMidtransStatus(t.referenceId, holder.status); + } else { + // No reference ID to check + holder.status.setText(displayStatus.toUpperCase()); + setStatusColor(holder.status, displayStatus); + Log.w("TransactionAdapter", "⚠️ No reference ID for status check"); + } + } else { + // Use existing status yang sudah confirmed + holder.status.setText(displayStatus.toUpperCase()); + setStatusColor(holder.status, displayStatus); + Log.d("TransactionAdapter", "✅ Using confirmed status: " + displayStatus); + } // Set payment method String paymentMethod = getPaymentMethodName(t.channelCode, t.channelCategory); @@ -85,12 +123,10 @@ public class TransactionAdapter extends RecyclerView.Adapter { + try { + Log.d("TransactionAdapter", "🔍 Comprehensive status check for reference: " + referenceId); + + // STEP 1: Query ALL webhook logs (tidak filter specific order_id) + // Karena kita perlu cari semua order_id yang terkait dengan reference_id ini + 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.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); + } + + JSONObject json = new JSONObject(response.toString()); + JSONArray results = json.optJSONArray("results"); + + String realStatus = "INIT"; // Default + String foundOrderId = null; + String foundTransactionStatus = ""; + + if (results != null && results.length() > 0) { + Log.d("TransactionAdapter", "📊 Processing " + results.length() + " log entries"); + + // STEP 2: Comprehensive search untuk semua kemungkinan relasi + for (int i = 0; i < results.length(); i++) { + JSONObject log = results.getJSONObject(i); + JSONObject reqBody = log.optJSONObject("request_body"); + + if (reqBody != null) { + String logOrderId = reqBody.optString("order_id", ""); + String logTransactionStatus = reqBody.optString("transaction_status", ""); + String logReferenceId = reqBody.optString("reference_id", ""); + + // ✅ METHOD 1: Direct reference_id match + boolean isDirectMatch = referenceId.equals(logReferenceId); + + // ✅ METHOD 2: Check custom_field1 untuk QR refresh tracking + boolean isRefreshMatch = false; + String customField1 = reqBody.optString("custom_field1", ""); + if (!customField1.isEmpty()) { + try { + JSONObject customData = new JSONObject(customField1); + String originalReference = customData.optString("original_reference", ""); + if (referenceId.equals(originalReference)) { + isRefreshMatch = true; + Log.d("TransactionAdapter", "🔄 Found refresh match: " + logOrderId + + " (original ref: " + originalReference + ")"); + } + } catch (JSONException e) { + // Ignore custom field parsing errors + } + } + + // ✅ METHOD 3: Check item details untuk backup tracking + boolean isItemMatch = false; + JSONArray itemDetails = reqBody.optJSONArray("item_details"); + if (itemDetails != null && itemDetails.length() > 0) { + for (int j = 0; j < itemDetails.length(); j++) { + JSONObject item = itemDetails.optJSONObject(j); + if (item != null) { + String itemName = item.optString("name", ""); + if (itemName.contains("(Ref: " + referenceId + ")")) { + isItemMatch = true; + Log.d("TransactionAdapter", "📦 Found item match: " + logOrderId + + " (item: " + itemName + ")"); + break; + } + } + } + } + + // ✅ COMPREHENSIVE MATCH: Any of the three methods + boolean isRelatedTransaction = isDirectMatch || isRefreshMatch || isItemMatch; + + if (isRelatedTransaction) { + Log.d("TransactionAdapter", "🎯 MATCH FOUND!"); + Log.d("TransactionAdapter", " Order ID: " + logOrderId); + Log.d("TransactionAdapter", " Status: " + logTransactionStatus); + Log.d("TransactionAdapter", " Reference: " + logReferenceId); + Log.d("TransactionAdapter", " Match Type: " + + (isDirectMatch ? "DIRECT" : "") + + (isRefreshMatch ? "REFRESH" : "") + + (isItemMatch ? "ITEM" : "")); + + // Priority check: settlement > capture > success > pending > init + if (logTransactionStatus.equals("settlement") || + logTransactionStatus.equals("capture") || + logTransactionStatus.equals("success")) { + realStatus = "PAID"; + foundOrderId = logOrderId; + foundTransactionStatus = logTransactionStatus; + Log.d("TransactionAdapter", "✅ PAYMENT CONFIRMED: " + logOrderId + " -> " + logTransactionStatus); + break; // Found paid status, stop searching + } else if (logTransactionStatus.equals("pending") && realStatus.equals("INIT")) { + realStatus = "PENDING"; + foundOrderId = logOrderId; + foundTransactionStatus = logTransactionStatus; + Log.d("TransactionAdapter", "⏳ PENDING found: " + logOrderId); + } + } + } + } + + Log.d("TransactionAdapter", "🔍 FINAL RESULT for " + referenceId + ":"); + Log.d("TransactionAdapter", " Status: " + realStatus); + Log.d("TransactionAdapter", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A")); + Log.d("TransactionAdapter", " Midtrans Status: " + (foundTransactionStatus != null ? foundTransactionStatus : "N/A")); + } + + // STEP 3: Update UI di main thread + final String finalStatus = realStatus; + final String finalOrderId = foundOrderId; + final String finalTransactionStatus = foundTransactionStatus; + + statusTextView.post(() -> { + statusTextView.setText(finalStatus); + setStatusColor(statusTextView, finalStatus); + + Log.d("TransactionAdapter", "🎨 UI UPDATED:"); + Log.d("TransactionAdapter", " Reference: " + referenceId); + Log.d("TransactionAdapter", " Display Status: " + finalStatus); + Log.d("TransactionAdapter", " Found Order: " + (finalOrderId != null ? finalOrderId : "N/A")); + }); + + } else { + Log.w("TransactionAdapter", "⚠️ API call failed with code: " + conn.getResponseCode()); + statusTextView.post(() -> { + statusTextView.setText("ERROR"); + setStatusColor(statusTextView, "ERROR"); + }); + } + + } catch (IOException | JSONException e) { + Log.e("TransactionAdapter", "❌ Comprehensive status check error: " + e.getMessage(), e); + statusTextView.post(() -> { + statusTextView.setText("INIT"); + setStatusColor(statusTextView, "INIT"); + }); + } + }).start(); + } + private String getPaymentMethodName(String channelCode, String channelCategory) { // Convert channel code to readable payment method name if (channelCode == null) return "Unknown";