solved duplicate data
This commit is contained in:
parent
99fab68e71
commit
4aaa9957e7
@ -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
|
||||
@ -104,6 +118,144 @@ public class QrisActivity extends AppCompatActivity {
|
||||
// Initially disable the button
|
||||
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 -> {
|
||||
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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:
|
||||
break;
|
||||
return code; // Return the code as-is if not mapped
|
||||
}
|
||||
}
|
||||
|
||||
// Determine from channel category
|
||||
if (channelCategory != null && !channelCategory.isEmpty()) {
|
||||
return channelCategory.toUpperCase();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Default
|
||||
return "QRIS";
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
if (!statusMatches) continue; // Skip if status doesn't match
|
||||
}
|
||||
|
||||
// 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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXED: Extract raw amount from formatted string properly
|
||||
* Handles various amount formats from backend
|
||||
* ✅ 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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user