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