billing: add billable nights view and improve payment ledger cash handle logic
This commit is contained in:
@@ -101,6 +101,17 @@ data class BookingExpectedDatesRequest(
|
|||||||
val expectedCheckOutAt: String? = null
|
val expectedCheckOutAt: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class BookingBillableNightsRequest(
|
||||||
|
val expectedCheckInAt: String? = null,
|
||||||
|
val expectedCheckOutAt: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookingBillableNightsResponse(
|
||||||
|
val bookingId: String? = null,
|
||||||
|
val status: String? = null,
|
||||||
|
val billableNights: Long? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class BookingDetailsResponse(
|
data class BookingDetailsResponse(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
@@ -139,6 +150,7 @@ data class BookingDetailsResponse(
|
|||||||
val expectedPay: Long? = null,
|
val expectedPay: Long? = null,
|
||||||
val amountCollected: Long? = null,
|
val amountCollected: Long? = null,
|
||||||
val pending: Long? = null,
|
val pending: Long? = null,
|
||||||
|
val billableNights: Long? = null,
|
||||||
val billingMode: String? = null,
|
val billingMode: String? = null,
|
||||||
val billingCheckinTime: String? = null,
|
val billingCheckinTime: String? = null,
|
||||||
val billingCheckoutTime: String? = null
|
val billingCheckoutTime: String? = null
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import com.android.trisolarispms.data.api.model.BookingLinkGuestRequest
|
|||||||
import com.android.trisolarispms.data.api.model.BookingNoShowRequest
|
import com.android.trisolarispms.data.api.model.BookingNoShowRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingListItem
|
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||||
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillableNightsRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillableNightsResponse
|
||||||
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||||
import com.android.trisolarispms.data.api.model.BookingBalanceResponse
|
import com.android.trisolarispms.data.api.model.BookingBalanceResponse
|
||||||
@@ -60,6 +62,13 @@ interface BookingApi {
|
|||||||
@Body body: BookingExpectedDatesRequest
|
@Body body: BookingExpectedDatesRequest
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/billable-nights")
|
||||||
|
suspend fun previewBillableNights(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Body body: BookingBillableNightsRequest
|
||||||
|
): Response<BookingBillableNightsResponse>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/billing-policy")
|
@POST("properties/{propertyId}/bookings/{bookingId}/billing-policy")
|
||||||
suspend fun updateBookingBillingPolicy(
|
suspend fun updateBookingBillingPolicy(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package com.android.trisolarispms.ui.booking
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -13,8 +15,10 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillableNightsRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
||||||
import com.android.trisolarispms.ui.common.PaddedScreenColumn
|
import com.android.trisolarispms.ui.common.PaddedScreenColumn
|
||||||
import com.android.trisolarispms.ui.common.SaveTopBarScaffold
|
import com.android.trisolarispms.ui.common.SaveTopBarScaffold
|
||||||
@@ -43,7 +47,10 @@ fun BookingExpectedDatesScreen(
|
|||||||
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
||||||
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
||||||
val today = LocalDate.now(displayZone)
|
val today = LocalDate.now(displayZone)
|
||||||
val editableCheckIn = status?.uppercase() == "OPEN"
|
val bookingStatus = status?.uppercase()
|
||||||
|
val editableCheckIn = bookingStatus == "OPEN"
|
||||||
|
val billableNights = remember { mutableStateOf<Long?>(null) }
|
||||||
|
val isBillableNightsLoading = remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(bookingId) {
|
LaunchedEffect(bookingId) {
|
||||||
@@ -70,19 +77,72 @@ fun BookingExpectedDatesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
propertyId,
|
||||||
|
bookingId,
|
||||||
|
bookingStatus,
|
||||||
|
checkInDate.value,
|
||||||
|
checkInTime.value,
|
||||||
|
checkOutDate.value,
|
||||||
|
checkOutTime.value
|
||||||
|
) {
|
||||||
|
val inAt = if (editableCheckIn) {
|
||||||
|
checkInDate.value?.let { formatBookingIso(it, checkInTime.value) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) }
|
||||||
|
val previewBody = when (bookingStatus) {
|
||||||
|
"OPEN" -> {
|
||||||
|
if (inAt.isNullOrBlank() || outAt.isNullOrBlank()) null
|
||||||
|
else BookingBillableNightsRequest(expectedCheckInAt = inAt, expectedCheckOutAt = outAt)
|
||||||
|
}
|
||||||
|
"CHECKED_IN" -> {
|
||||||
|
if (outAt.isNullOrBlank()) null
|
||||||
|
else BookingBillableNightsRequest(expectedCheckOutAt = outAt)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (previewBody == null) {
|
||||||
|
billableNights.value = null
|
||||||
|
isBillableNightsLoading.value = false
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
isBillableNightsLoading.value = true
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.previewBillableNights(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = previewBody
|
||||||
|
)
|
||||||
|
billableNights.value = if (response.isSuccessful) response.body()?.billableNights else null
|
||||||
|
} catch (_: Exception) {
|
||||||
|
billableNights.value = null
|
||||||
|
} finally {
|
||||||
|
isBillableNightsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SaveTopBarScaffold(
|
SaveTopBarScaffold(
|
||||||
title = "Update Expected Dates",
|
title = "Update Expected Dates",
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
saveEnabled = !isLoading.value,
|
saveEnabled = !isLoading.value,
|
||||||
onSave = {
|
onSave = {
|
||||||
isLoading.value = true
|
|
||||||
error.value = null
|
|
||||||
val inAt = if (editableCheckIn) {
|
val inAt = if (editableCheckIn) {
|
||||||
checkInDate.value?.let { formatBookingIso(it, checkInTime.value) }
|
checkInDate.value?.let { formatBookingIso(it, checkInTime.value) }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) }
|
val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) }
|
||||||
|
val hasCheckInChanged = editableCheckIn && !isSameBookingDateTime(inAt, expectedCheckInAt)
|
||||||
|
val hasCheckOutChanged = !isSameBookingDateTime(outAt, expectedCheckOutAt)
|
||||||
|
if (!hasCheckInChanged && !hasCheckOutChanged) {
|
||||||
|
onDone()
|
||||||
|
return@SaveTopBarScaffold
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
@@ -151,6 +211,24 @@ fun BookingExpectedDatesScreen(
|
|||||||
label = { Text("Expected Check-out") },
|
label = { Text("Expected Check-out") },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.45f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isBillableNightsLoading.value) {
|
||||||
|
"Billable Nights: Calculating..."
|
||||||
|
} else {
|
||||||
|
"Billable Nights: ${billableNights.value?.toString() ?: "-"}"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
val checkOutMinDate = maxOf(checkInDate.value ?: today, today)
|
val checkOutMinDate = maxOf(checkInDate.value ?: today, today)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BookingDateTimePickerInline(
|
BookingDateTimePickerInline(
|
||||||
@@ -174,3 +252,15 @@ fun BookingExpectedDatesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isSameBookingDateTime(current: String?, original: String?): Boolean {
|
||||||
|
if (current.isNullOrBlank() && original.isNullOrBlank()) return true
|
||||||
|
if (current.isNullOrBlank() || original.isNullOrBlank()) return false
|
||||||
|
val currentInstant = runCatching { OffsetDateTime.parse(current).toInstant() }.getOrNull()
|
||||||
|
val originalInstant = runCatching { OffsetDateTime.parse(original).toInstant() }.getOrNull()
|
||||||
|
return if (currentInstant != null && originalInstant != null) {
|
||||||
|
currentInstant == originalInstant
|
||||||
|
} else {
|
||||||
|
current.trim() == original.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,10 +56,23 @@ fun BookingPaymentsScreen(
|
|||||||
val refundTarget = remember { mutableStateOf<PaymentDto?>(null) }
|
val refundTarget = remember { mutableStateOf<PaymentDto?>(null) }
|
||||||
val refundAmount = rememberSaveable { mutableStateOf("") }
|
val refundAmount = rememberSaveable { mutableStateOf("") }
|
||||||
val refundNotes = rememberSaveable { mutableStateOf("") }
|
val refundNotes = rememberSaveable { mutableStateOf("") }
|
||||||
|
val amountValue = amountInput.value.toLongOrNull()
|
||||||
|
val pendingBalance = state.pendingBalance
|
||||||
|
val isAmountExceedingPending = pendingBalance != null && amountValue != null && amountValue > pendingBalance
|
||||||
|
val canAddCashPayment = !state.isLoading &&
|
||||||
|
amountValue != null &&
|
||||||
|
amountValue > 0L &&
|
||||||
|
(pendingBalance == null || amountValue <= pendingBalance) &&
|
||||||
|
(pendingBalance == null || pendingBalance > 0L)
|
||||||
|
|
||||||
LaunchedEffect(propertyId, bookingId) {
|
LaunchedEffect(propertyId, bookingId) {
|
||||||
viewModel.load(propertyId, bookingId)
|
viewModel.load(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(state.message) {
|
||||||
|
if (state.message == "Cash payment added") {
|
||||||
|
amountInput.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BackTopBarScaffold(
|
BackTopBarScaffold(
|
||||||
title = "Payments",
|
title = "Payments",
|
||||||
@@ -72,6 +85,12 @@ fun BookingPaymentsScreen(
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
if (canAddCash) {
|
if (canAddCash) {
|
||||||
|
Text(
|
||||||
|
text = "Pending balance: ${pendingBalance?.toString() ?: "-"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = amountInput.value,
|
value = amountInput.value,
|
||||||
onValueChange = { amountInput.value = it.filter { ch -> ch.isDigit() } },
|
onValueChange = { amountInput.value = it.filter { ch -> ch.isDigit() } },
|
||||||
@@ -79,14 +98,20 @@ fun BookingPaymentsScreen(
|
|||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
if (isAmountExceedingPending) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Amount cannot be greater than pending balance",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val amount = amountInput.value.toLongOrNull() ?: 0L
|
viewModel.addCashPayment(propertyId, bookingId, amountValue ?: 0L)
|
||||||
viewModel.addCashPayment(propertyId, bookingId, amount)
|
|
||||||
amountInput.value = ""
|
|
||||||
},
|
},
|
||||||
enabled = !state.isLoading,
|
enabled = canAddCashPayment,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text("Add cash payment")
|
Text("Add cash payment")
|
||||||
@@ -204,7 +229,7 @@ private fun PaymentCard(
|
|||||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||||
}
|
}
|
||||||
val dateText = date?.format(DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a"))
|
val dateText = date?.format(DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a"))
|
||||||
val isCash = payment.method == "CASH"
|
val isCash = payment.method.equals("CASH", ignoreCase = true)
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (isCash) {
|
containerColor = if (isCash) {
|
||||||
@@ -229,6 +254,7 @@ private fun PaymentCard(
|
|||||||
val hasRefundableAmount = (payment.amount ?: 0L) > 0L
|
val hasRefundableAmount = (payment.amount ?: 0L) > 0L
|
||||||
if (
|
if (
|
||||||
canRefund &&
|
canRefund &&
|
||||||
|
!isCash &&
|
||||||
hasRefundableAmount &&
|
hasRefundableAmount &&
|
||||||
(!payment.id.isNullOrBlank() || !payment.gatewayPaymentId.isNullOrBlank())
|
(!payment.id.isNullOrBlank() || !payment.gatewayPaymentId.isNullOrBlank())
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ data class BookingPaymentsState(
|
|||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val payments: List<PaymentDto> = emptyList()
|
val payments: List<PaymentDto> = emptyList(),
|
||||||
|
val pendingBalance: Long? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,19 +17,23 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun load(propertyId: String, bookingId: String) {
|
fun load(propertyId: String, bookingId: String) {
|
||||||
runPaymentAction(defaultError = "Load failed") { api ->
|
runPaymentAction(defaultError = "Load failed") { api ->
|
||||||
val response = api.listPayments(propertyId, bookingId)
|
val paymentsResponse = api.listPayments(propertyId, bookingId)
|
||||||
val body = response.body()
|
val payments = paymentsResponse.body()
|
||||||
if (response.isSuccessful && body != null) {
|
if (paymentsResponse.isSuccessful && payments != null) {
|
||||||
|
val pending = api.getBookingBalance(propertyId, bookingId)
|
||||||
|
.body()
|
||||||
|
?.pending
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = body,
|
payments = payments,
|
||||||
|
pendingBalance = pending,
|
||||||
error = null,
|
error = null,
|
||||||
message = null
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setActionFailure("Load", response)
|
setActionFailure("Load", paymentsResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +44,13 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
runPaymentAction(defaultError = "Create failed") { api ->
|
runPaymentAction(defaultError = "Create failed") { api ->
|
||||||
|
val latestPending = api.getBookingBalance(propertyId, bookingId).body()?.pending
|
||||||
|
?: _state.value.pendingBalance
|
||||||
|
val validationError = validateCashAmount(amount = amount, pendingBalance = latestPending)
|
||||||
|
if (validationError != null) {
|
||||||
|
_state.update { it.copy(isLoading = false, error = validationError, message = null) }
|
||||||
|
return@runPaymentAction
|
||||||
|
}
|
||||||
val response = api.createPayment(
|
val response = api.createPayment(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
bookingId = bookingId,
|
bookingId = bookingId,
|
||||||
@@ -48,9 +59,11 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
val body = response.body()
|
val body = response.body()
|
||||||
if (response.isSuccessful && body != null) {
|
if (response.isSuccessful && body != null) {
|
||||||
_state.update { current ->
|
_state.update { current ->
|
||||||
|
val nextPending = latestPending?.let { (it - amount).coerceAtLeast(0L) }
|
||||||
current.copy(
|
current.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = listOf(body) + current.payments,
|
payments = listOf(body) + current.payments,
|
||||||
|
pendingBalance = nextPending ?: current.pendingBalance,
|
||||||
error = null,
|
error = null,
|
||||||
message = "Cash payment added"
|
message = "Cash payment added"
|
||||||
)
|
)
|
||||||
@@ -63,6 +76,7 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
||||||
runPaymentAction(defaultError = "Delete failed") { api ->
|
runPaymentAction(defaultError = "Delete failed") { api ->
|
||||||
|
val payment = _state.value.payments.firstOrNull { it.id == paymentId }
|
||||||
val response = api.deletePayment(
|
val response = api.deletePayment(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
bookingId = bookingId,
|
bookingId = bookingId,
|
||||||
@@ -70,9 +84,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_state.update { current ->
|
_state.update { current ->
|
||||||
|
val restoredPending = if (payment?.method == "CASH") {
|
||||||
|
val amount = payment.amount ?: 0L
|
||||||
|
current.pendingBalance?.plus(amount)
|
||||||
|
} else {
|
||||||
|
current.pendingBalance
|
||||||
|
}
|
||||||
current.copy(
|
current.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = current.payments.filterNot { it.id == paymentId },
|
payments = current.payments.filterNot { it.id == paymentId },
|
||||||
|
pendingBalance = restoredPending,
|
||||||
error = null,
|
error = null,
|
||||||
message = "Cash payment deleted"
|
message = "Cash payment deleted"
|
||||||
)
|
)
|
||||||
@@ -112,6 +133,7 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
val body = response.body()
|
val body = response.body()
|
||||||
if (response.isSuccessful && body != null) {
|
if (response.isSuccessful && body != null) {
|
||||||
|
load(propertyId, bookingId)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -119,7 +141,6 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
message = "Refund processed"
|
message = "Refund processed"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
load(propertyId, bookingId)
|
|
||||||
} else {
|
} else {
|
||||||
setActionFailure("Refund", response)
|
setActionFailure("Refund", response)
|
||||||
}
|
}
|
||||||
@@ -155,4 +176,11 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateCashAmount(amount: Long, pendingBalance: Long?): String? {
|
||||||
|
if (pendingBalance == null) return null
|
||||||
|
if (pendingBalance <= 0L) return "No pending balance to collect"
|
||||||
|
if (amount > pendingBalance) return "Amount cannot be greater than pending balance ($pendingBalance)"
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ private fun GuestInfoTabContent(
|
|||||||
GuestDetailRow(label = "Billing Mode", value = details?.billingMode)
|
GuestDetailRow(label = "Billing Mode", value = details?.billingMode)
|
||||||
if (billingMode == BookingBillingMode.FULL_24H) {
|
if (billingMode == BookingBillingMode.FULL_24H) {
|
||||||
GuestDetailRow(label = "Billing Window", value = "24-hour block")
|
GuestDetailRow(label = "Billing Window", value = "24-hour block")
|
||||||
|
} else if (billingMode == BookingBillingMode.CUSTOM_WINDOW) {
|
||||||
|
GuestDetailRow(label = "Billing Checkout Time", value = details?.billingCheckoutTime)
|
||||||
} else {
|
} else {
|
||||||
GuestDetailRow(label = "Billing Check-in Time", value = details?.billingCheckinTime)
|
GuestDetailRow(label = "Billing Check-in Time", value = details?.billingCheckinTime)
|
||||||
GuestDetailRow(label = "Billing Checkout Time", value = details?.billingCheckoutTime)
|
GuestDetailRow(label = "Billing Checkout Time", value = details?.billingCheckoutTime)
|
||||||
@@ -304,6 +306,7 @@ private fun GuestInfoTabContent(
|
|||||||
) {
|
) {
|
||||||
GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString())
|
GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString())
|
||||||
GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString())
|
GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString())
|
||||||
|
GuestDetailRow(label = "Billable Nights", value = details?.billableNights?.toString())
|
||||||
GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString())
|
GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString())
|
||||||
GuestDetailRow(label = "Pending", value = details?.pending?.toString())
|
GuestDetailRow(label = "Pending", value = details?.pending?.toString())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user