From 77558ab0ee4d92a036fcdbd31416d7d0847c1b78 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sat, 26 Jul 2025 03:14:44 +0500 Subject: [PATCH] Pay any bill --- app/src/main/AndroidManifest.xml | 5 + .../java/sh/sar/gridflow/LoginActivity.kt | 5 +- .../main/java/sh/sar/gridflow/MainActivity.kt | 8 +- .../sar/gridflow/PayWithoutAccountActivity.kt | 258 ++++++++++ .../main/java/sh/sar/gridflow/data/Models.kt | 166 ++++++ .../sar/gridflow/network/FenakaApiService.kt | 54 ++ app/src/main/res/layout/activity_login.xml | 12 +- .../layout/activity_pay_without_account.xml | 477 ++++++++++++++++++ 8 files changed, 976 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/sh/sar/gridflow/PayWithoutAccountActivity.kt create mode 100644 app/src/main/res/layout/activity_pay_without_account.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c6843f..73c6e5a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -67,6 +67,11 @@ android:exported="false" android:label="Bill Details" android:theme="@style/Theme.GridFlow.NoActionBar" /> + when (menuItem.itemId) { R.id.nav_terms_of_service -> { @@ -88,6 +88,12 @@ class MainActivity : AppCompatActivity() { drawerLayout.closeDrawers() true } + R.id.nav_pay_any_bill -> { + val intent = Intent(this, PayWithoutAccountActivity::class.java) + startActivity(intent) + drawerLayout.closeDrawers() + true + } else -> { // Let the default navigation handle other items try { diff --git a/app/src/main/java/sh/sar/gridflow/PayWithoutAccountActivity.kt b/app/src/main/java/sh/sar/gridflow/PayWithoutAccountActivity.kt new file mode 100644 index 0000000..7b631e5 --- /dev/null +++ b/app/src/main/java/sh/sar/gridflow/PayWithoutAccountActivity.kt @@ -0,0 +1,258 @@ +package sh.sar.gridflow + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import sh.sar.gridflow.data.BillLookupResponse +import sh.sar.gridflow.databinding.ActivityPayWithoutAccountBinding +import sh.sar.gridflow.network.ApiResult +import sh.sar.gridflow.network.FenakaApiService +import java.text.SimpleDateFormat +import java.util.* + +class PayWithoutAccountActivity : AppCompatActivity() { + + private lateinit var binding: ActivityPayWithoutAccountBinding + private lateinit var apiService: FenakaApiService + private var currentBill: BillLookupResponse? = null + + companion object { + private const val TAG = "PayWithoutAccountActivity" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Force system theme (follows device dark mode setting) + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + Log.d(TAG, "PayWithoutAccountActivity onCreate") + + binding = ActivityPayWithoutAccountBinding.inflate(layoutInflater) + setContentView(binding.root) + + apiService = FenakaApiService() + setupClickListeners() + } + + private fun setupClickListeners() { + binding.btnContinue.setOnClickListener { + handleContinue() + } + + binding.btnPay.setOnClickListener { + handlePayment() + } + } + + private fun handleContinue() { + // Check if we're showing the card - if so, hide it and show input fields + if (binding.billDetailsCard.visibility == View.VISIBLE) { + Log.d(TAG, "Hiding bill details card and showing input fields") + showInputFields() + return + } + + // Otherwise, perform bill search + val billNumber = binding.etBillNumber.text.toString().trim() + val subscriptionNumber = binding.etSubscriptionNumber.text.toString().trim() + + Log.d(TAG, "handleContinue called with bill: $billNumber, subscription: $subscriptionNumber") + + if (validateInput(billNumber, subscriptionNumber)) { + Log.d(TAG, "Input validation passed, fetching bill details") + fetchBillDetails(billNumber, subscriptionNumber) + } else { + Log.d(TAG, "Input validation failed") + } + } + + private fun showInputFields() { + binding.inputFieldsLayout.visibility = View.VISIBLE + binding.billDetailsCard.visibility = View.GONE + currentBill = null + // Update button text to Continue when showing input fields + binding.btnContinue.text = "Continue" + } + + private fun fetchBillDetails(billNumber: String, subscriptionNumber: String) { + setLoading(true) + + lifecycleScope.launch { + try { + when (val result = apiService.findBill(billNumber, subscriptionNumber)) { + is ApiResult.Success -> { + Log.d(TAG, "Bill fetch successful: ${result.data.billNumber}") + currentBill = result.data + setLoading(false) + showBillDetails(result.data) + } + is ApiResult.Error -> { + if (result.code == 404) { + Log.d(TAG, "404 received, trying with swapped parameters") + // Try again with swapped parameters + when (val retryResult = apiService.findBill(subscriptionNumber, billNumber)) { + is ApiResult.Success -> { + Log.d(TAG, "Retry successful with swapped parameters: ${retryResult.data.billNumber}") + currentBill = retryResult.data + setLoading(false) + showBillDetails(retryResult.data) + } + is ApiResult.Error -> { + Log.d(TAG, "Retry also failed: ${retryResult.message} (code: ${retryResult.code})") + setLoading(false) + showError(retryResult.message) + } + } + } else { + Log.d(TAG, "Bill fetch failed: ${result.message} (code: ${result.code})") + setLoading(false) + showError(result.message) + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Exception in fetchBillDetails", e) + setLoading(false) + showError("Failed to fetch bill details: ${e.message}") + } + } + } + + private fun handlePayment() { + currentBill?.let { bill -> + if (bill.status == "paid") { + Toast.makeText(this, "This bill has already been paid", Toast.LENGTH_SHORT).show() + } else { + // TODO: Navigate to payment flow + Toast.makeText(this, "Payment flow coming soon", Toast.LENGTH_SHORT).show() + } + } + } + + private fun showError(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + // Show input fields and hide bill details + showInputFields() + // Don't clear input fields - user might have made a small mistake + } + + private fun showBillDetails(bill: BillLookupResponse) { + // Hide input fields and show bill details + binding.inputFieldsLayout.visibility = View.GONE + binding.billDetailsCard.visibility = View.VISIBLE + // Update button text to Search Another Bill when showing card + binding.btnContinue.text = "Search Another Bill" + + // Set bill information + binding.tvBillNumber.text = bill.billNumber + binding.tvBillAmount.text = "MVR ${bill.billAmount}" + binding.tvBillStatus.text = bill.status.uppercase() + + // Set customer information + binding.tvCustomerName.text = bill.customer.name + binding.tvAccountNumber.text = bill.customer.accountNumber + binding.tvPhoneNumber.text = bill.customer.phone + + // Set address information + val address = "${bill.subscriptionAddress.property.name}, ${bill.subscriptionAddress.property.street.name}" + binding.tvAddress.text = address + + // Set subscription information + binding.tvSubscriptionNumber.text = bill.subscription.subscriptionNumber + binding.tvServiceType.text = if (bill.subscription.serviceId == 1) "Electricity" else "Water" + + // Format and set dates + val dueDateFormatted = formatDate(bill.dueDate) + val billDateFormatted = formatDate(bill.billDate) + binding.tvDueDate.text = dueDateFormatted + binding.tvBillDate.text = billDateFormatted + + // Set payment status and button + val isPaid = bill.status == "paid" + binding.btnPay.isEnabled = !isPaid + binding.btnPay.alpha = if (isPaid) 0.5f else 1.0f + binding.btnPay.text = if (isPaid) "Already Paid" else "Pay MVR ${bill.billAmount}" + + // Set status color + when (bill.status.lowercase()) { + "paid" -> { + binding.tvBillStatus.setTextColor(getColor(android.R.color.holo_green_dark)) + } + "unpaid" -> { + binding.tvBillStatus.setTextColor(getColor(android.R.color.holo_red_dark)) + } + else -> { + binding.tvBillStatus.setTextColor(getColor(android.R.color.holo_orange_dark)) + } + } + + // Show additional payment details if available + bill.billPaymentDetails?.let { paymentDetails -> + if (paymentDetails.paidAmount.toDoubleOrNull() ?: 0.0 > 0.0) { + binding.tvPaidAmount.visibility = View.VISIBLE + binding.tvPaidAmount.text = "Paid: MVR ${paymentDetails.paidAmount}" + + if (paymentDetails.pendingAmount.toDoubleOrNull() ?: 0.0 > 0.0) { + binding.tvPendingAmount.visibility = View.VISIBLE + binding.tvPendingAmount.text = "Pending: MVR ${paymentDetails.pendingAmount}" + } + } + } + } + + private fun formatDate(dateString: String): String { + return try { + val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) + val outputFormat = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()) + val date = inputFormat.parse(dateString) + outputFormat.format(date ?: Date()) + } catch (e: Exception) { + Log.e(TAG, "Error formatting date: $dateString", e) + dateString.substringBefore("T") + } + } + + private fun validateInput(billNumber: String, subscriptionNumber: String): Boolean { + if (billNumber.isEmpty()) { + binding.etBillNumber.error = "Bill number is required" + return false + } + + if (subscriptionNumber.isEmpty()) { + binding.etSubscriptionNumber.error = "Subscription number is required" + return false + } + + // Basic validation - you can add more specific validation rules here + if (billNumber.length < 3) { + binding.etBillNumber.error = "Bill number must be at least 3 characters" + return false + } + + if (subscriptionNumber.length < 3) { + binding.etSubscriptionNumber.error = "Subscription number must be at least 3 characters" + return false + } + + return true + } + + private fun setLoading(isLoading: Boolean) { + binding.btnContinue.isEnabled = !isLoading + binding.btnContinue.text = if (isLoading) "Searching Bill..." else "Continue" + + binding.etBillNumber.isEnabled = !isLoading + binding.etSubscriptionNumber.isEnabled = !isLoading + + if (isLoading) { + binding.billDetailsCard.visibility = View.GONE + binding.inputFieldsLayout.visibility = View.VISIBLE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sh/sar/gridflow/data/Models.kt b/app/src/main/java/sh/sar/gridflow/data/Models.kt index b943047..8cbc8b3 100644 --- a/app/src/main/java/sh/sar/gridflow/data/Models.kt +++ b/app/src/main/java/sh/sar/gridflow/data/Models.kt @@ -413,3 +413,169 @@ data class BillPaymentDetails( val miscFine: String, val pendingAmount: String ) : Serializable + +// Bill lookup response for guest payment +data class BillLookupResponse( + val id: Long, + val billNumber: String, + val billAmount: String, + val carryForwardBalance: String, + val discount: String?, + val tax: String?, + val billedUnits: String, + val dueDate: String, + val billDate: String, + val billYear: Int, + val billMonth: Int, + val deliveredDate: String?, + val warnDate: String?, + val warnDueDate: String?, + val status: String, + val type: String, + val isDelivered: Boolean, + val oldId: String?, + val oldBranchId: String?, + val billDuration: Int, + val fineStatus: String, + val referenceNo: Int, + val details: String?, + val groupUuid: String, + val publicId: String, + val createdAt: String, + val updatedAt: String, + val customerId: Long, + val categoryId: Int, + val subscriptionAddressId: Long, + val billingAddressId: Long, + val subscriptionId: Long, + val consumerId: Long, + val readingEventId: Long?, + val branchId: Int, + val subscriptionAddress: BillLookupSubscriptionAddress, + val subscription: BillLookupSubscription, + val customer: BillLookupCustomer, + val billPayments: List?, + val billPaymentDetails: BillLookupPaymentDetails? +) : Serializable + +data class BillLookupSubscriptionAddress( + val id: Long, + val type: String, + val startAt: String, + val endAt: String?, + val oldId: String?, + val oldServiceId: String?, + val oldBranchId: String?, + val createdAt: String, + val updatedAt: String, + val propertyId: Long, + val subscriptionId: Long, + val apartmentId: String?, + val property: BillLookupProperty +) : Serializable + +data class BillLookupProperty( + val id: Long, + val name: String, + val oldId: String?, + val oldServiceId: String?, + val oldBranchId: String?, + val createdAt: String, + val updatedAt: String, + val areaId: Int, + val geographyId: Int, + val streetId: Int, + val street: BillLookupStreet +) : Serializable + +data class BillLookupStreet( + val id: Int, + val name: String, + val oldId: String?, + val oldServiceId: String?, + val oldBranchId: String?, + val createdAt: String, + val updatedAt: String, + val geographyId: Int +) : Serializable + +data class BillLookupSubscription( + val id: Long, + val subscriptionNumber: String, + val isTemporary: Boolean, + val readingOrder: Int, + val deliveryOrder: Int, + val groupUuid: String, + val email: String?, + val status: String, + val oldId: String?, + val oldServiceId: String?, + val oldBranchId: String?, + val carryForwardBalance: String?, + val depositBalance: String?, + val hasSmartMeter: Boolean, + val subscribedAt: String, + val createdAt: String, + val updatedAt: String, + val areaId: Int, + val branchId: Int, + val categoryId: Int, + val customerId: Long, + val requestId: Long?, + val serviceId: Int +) : Serializable + +data class BillLookupCustomer( + val id: Long, + val type: String, + val name: String, + val accountNumber: String, + val phone: String, + val oldAccountNo: String?, + val oldId: String?, + val oldServiceId: String?, + val oldBranchId: String?, + val registrationNumber: String?, + val agreementNumber: String?, + val discount: String, + val status: String, + val createdAt: String, + val updatedAt: String, + val personId: Long? +) : Serializable + +data class BillLookupPayment( + val id: Long, + val amount: String, + val type: String, + val createdAt: String, + val updatedAt: String, + val deletedAt: String?, + val billId: Long, + val paymentId: Long, + val payment: BillLookupPaymentDetail +) : Serializable + +data class BillLookupPaymentDetail( + val id: Long, + val tenderedAmount: String, + val balanceAmount: String, + val carryForwardAmount: String, + val status: String, + val date: String, + val mode: String, + val createdAt: String, + val updatedAt: String, + val deletedAt: String?, + val mainJournalId: String?, + val branchId: String?, + val userId: Long? +) : Serializable + +data class BillLookupPaymentDetails( + val billId: Long, + val advancePaid: String, + val paidAmount: String, + val miscFine: String, + val pendingAmount: String +) : Serializable diff --git a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt index 437e9e6..2822cf8 100644 --- a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt +++ b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt @@ -10,6 +10,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor import sh.sar.gridflow.data.BandRatesResponse import sh.sar.gridflow.data.Bill +import sh.sar.gridflow.data.BillLookupResponse import sh.sar.gridflow.data.CustomerSubscription import sh.sar.gridflow.data.ErrorResponse import sh.sar.gridflow.data.ForgotPasswordRequest @@ -453,6 +454,59 @@ class FenakaApiService { ApiResult.Error("Unexpected error: ${e.message}", -1) } } + + suspend fun findBill(billNumber: String, subscriptionNumber: String): ApiResult = withContext(Dispatchers.IO) { + Log.d(TAG, "Attempting to find bill: $billNumber, subscription: $subscriptionNumber") + + val filterQuery = "billNumber+eq+$billNumber|subscription.subscription_number+eq+$subscriptionNumber" + val includeQuery = "subscriptionAddress.property.street,subscription,customer" + + val url = "$BASE_URL/saiph/find-bill/?filter=$filterQuery&include=$includeQuery" + + val request = Request.Builder() + .url(url) + .get() + .header("Authorization", "Bearer $BEARER_TOKEN") + .header("Host", "api.fenaka.mv") + .build() + + Log.d(TAG, "Making bill lookup request to: ${request.url}") + + try { + val response = client.newCall(request).execute() + Log.d(TAG, "Bill lookup response code: ${response.code}") + Log.d(TAG, "Bill lookup response headers: ${response.headers}") + + val responseBody = response.body?.string() + Log.d(TAG, "Bill lookup response body: $responseBody") + + when (response.code) { + 200 -> { + val billResponse = gson.fromJson(responseBody, BillLookupResponse::class.java) + Log.d(TAG, "Bill lookup successful for bill: ${billResponse.billNumber}") + ApiResult.Success(billResponse, null) + } + 404 -> { + Log.d(TAG, "Bill lookup failed: 404 Bill not found") + ApiResult.Error("Bill not found with provided details", 404) + } + 400 -> { + Log.d(TAG, "Bill lookup failed: 400 Bad request") + ApiResult.Error("Invalid bill number or subscription number", 400) + } + else -> { + Log.d(TAG, "Bill lookup failed: Unknown error ${response.code}") + ApiResult.Error("Unknown error occurred", response.code) + } + } + } catch (e: IOException) { + Log.e(TAG, "Network error during bill lookup", e) + ApiResult.Error("Network error: ${e.message}", -1) + } catch (e: Exception) { + Log.e(TAG, "Unexpected error during bill lookup", e) + ApiResult.Error("Unexpected error: ${e.message}", -1) + } + } } sealed class ApiResult { diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 6995e26..1eb85e3 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - android:background="@android:color/white"> + android:background="?android:attr/colorBackground"> @@ -150,7 +150,7 @@ android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" - android:background="#CCCCCC" + android:background="?android:attr/textColorSecondary" android:alpha="0.3" /> @@ -167,7 +167,7 @@ android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" - android:background="#CCCCCC" + android:background="?android:attr/textColorSecondary" android:alpha="0.3" /> diff --git a/app/src/main/res/layout/activity_pay_without_account.xml b/app/src/main/res/layout/activity_pay_without_account.xml new file mode 100644 index 0000000..fb41d46 --- /dev/null +++ b/app/src/main/res/layout/activity_pay_without_account.xml @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file