payments: ability to refund
This commit is contained in:
@@ -491,6 +491,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
bookingId = currentRoute.bookingId,
|
bookingId = currentRoute.bookingId,
|
||||||
canAddCash = canManageRazorpaySettings(currentRoute.propertyId),
|
canAddCash = canManageRazorpaySettings(currentRoute.propertyId),
|
||||||
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
|
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
|
||||||
|
canRefund = canManageRazorpaySettings(currentRoute.propertyId),
|
||||||
onBack = {
|
onBack = {
|
||||||
route.value = AppRoute.BookingDetailsTabs(
|
route.value = AppRoute.BookingDetailsTabs(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
|
|||||||
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkResponse
|
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkResponse
|
||||||
import com.android.trisolarispms.data.api.model.PaymentDto
|
import com.android.trisolarispms.data.api.model.PaymentDto
|
||||||
import com.android.trisolarispms.data.api.model.PaymentCreateRequest
|
import com.android.trisolarispms.data.api.model.PaymentCreateRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayRefundResponse
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
@@ -132,6 +134,13 @@ interface BookingApi {
|
|||||||
@Body body: PaymentCreateRequest
|
@Body body: PaymentCreateRequest
|
||||||
): Response<PaymentDto>
|
): Response<PaymentDto>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/refund")
|
||||||
|
suspend fun refundRazorpayPayment(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Body body: RazorpayRefundRequest
|
||||||
|
): Response<RazorpayRefundResponse>
|
||||||
|
|
||||||
@DELETE("properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}")
|
@DELETE("properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}")
|
||||||
suspend fun deletePayment(
|
suspend fun deletePayment(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
|
|||||||
@@ -23,3 +23,17 @@ data class PaymentDto(
|
|||||||
data class PaymentCreateRequest(
|
data class PaymentCreateRequest(
|
||||||
val amount: Long
|
val amount: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class RazorpayRefundRequest(
|
||||||
|
val paymentId: String? = null,
|
||||||
|
val razorpayPaymentId: String? = null,
|
||||||
|
val amount: Long,
|
||||||
|
val notes: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RazorpayRefundResponse(
|
||||||
|
val refundId: String? = null,
|
||||||
|
val status: String? = null,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
package com.android.trisolarispms.data.api.model
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
data class RazorpaySettingsRequest(
|
data class RazorpaySettingsRequest(
|
||||||
val keyId: String,
|
val keyId: String? = null,
|
||||||
val keySecret: String? = null,
|
val keySecret: String? = null,
|
||||||
val webhookSecret: String? = null,
|
val webhookSecret: String? = null,
|
||||||
@SerializedName("isTest") val isTest: Boolean,
|
val keyIdTest: String? = null,
|
||||||
val useSalt256: Boolean
|
val keySecretTest: String? = null,
|
||||||
|
val webhookSecretTest: String? = null,
|
||||||
|
val isTest: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RazorpaySettingsResponse(
|
data class RazorpaySettingsResponse(
|
||||||
val propertyId: String? = null,
|
val propertyId: String? = null,
|
||||||
val configured: Boolean? = null,
|
val configured: Boolean? = null,
|
||||||
@SerializedName("test") val isTest: Boolean? = null,
|
@com.google.gson.annotations.SerializedName("test") val isTest: Boolean? = null,
|
||||||
val useSalt256: Boolean? = null,
|
|
||||||
val hasKeyId: Boolean? = null,
|
val hasKeyId: Boolean? = null,
|
||||||
val hasKeySecret: Boolean? = null,
|
val hasKeySecret: Boolean? = null,
|
||||||
val hasWebhookSecret: Boolean? = null,
|
val hasWebhookSecret: Boolean? = null,
|
||||||
val hasSalt256: Boolean? = null
|
val hasKeyIdTest: Boolean? = null,
|
||||||
|
val hasKeySecretTest: Boolean? = null,
|
||||||
|
val hasWebhookSecretTest: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,14 +25,17 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
@@ -49,11 +52,15 @@ fun BookingPaymentsScreen(
|
|||||||
bookingId: String,
|
bookingId: String,
|
||||||
canAddCash: Boolean,
|
canAddCash: Boolean,
|
||||||
canDeleteCash: Boolean,
|
canDeleteCash: Boolean,
|
||||||
|
canRefund: Boolean,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
viewModel: BookingPaymentsViewModel = viewModel()
|
viewModel: BookingPaymentsViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val amountInput = remember { mutableStateOf("") }
|
val amountInput = remember { mutableStateOf("") }
|
||||||
|
val refundTarget = remember { mutableStateOf<PaymentDto?>(null) }
|
||||||
|
val refundAmount = rememberSaveable { mutableStateOf("") }
|
||||||
|
val refundNotes = rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
LaunchedEffect(propertyId, bookingId) {
|
LaunchedEffect(propertyId, bookingId) {
|
||||||
viewModel.load(propertyId, bookingId)
|
viewModel.load(propertyId, bookingId)
|
||||||
@@ -108,6 +115,10 @@ fun BookingPaymentsScreen(
|
|||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
state.message?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
if (!state.isLoading && state.error == null && state.payments.isEmpty()) {
|
if (!state.isLoading && state.error == null && state.payments.isEmpty()) {
|
||||||
Text(text = "No payments yet")
|
Text(text = "No payments yet")
|
||||||
}
|
}
|
||||||
@@ -121,21 +132,87 @@ fun BookingPaymentsScreen(
|
|||||||
PaymentCard(
|
PaymentCard(
|
||||||
payment = payment,
|
payment = payment,
|
||||||
canDeleteCash = canDeleteCash,
|
canDeleteCash = canDeleteCash,
|
||||||
|
canRefund = canRefund,
|
||||||
onDelete = { paymentId ->
|
onDelete = { paymentId ->
|
||||||
viewModel.deleteCashPayment(propertyId, bookingId, paymentId)
|
viewModel.deleteCashPayment(propertyId, bookingId, paymentId)
|
||||||
|
},
|
||||||
|
onRefund = { target ->
|
||||||
|
refundTarget.value = target
|
||||||
|
refundAmount.value = target.amount?.toString().orEmpty()
|
||||||
|
refundNotes.value = ""
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refundTarget.value?.let { payment ->
|
||||||
|
val hasGateway = !payment.gatewayPaymentId.isNullOrBlank()
|
||||||
|
val hasPaymentId = !payment.id.isNullOrBlank()
|
||||||
|
val amountValue = refundAmount.value.toLongOrNull()
|
||||||
|
val canSubmit = amountValue != null && amountValue > 0 && (hasGateway || hasPaymentId)
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { refundTarget.value = null },
|
||||||
|
title = { Text("Confirm refund") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "This will initiate a Razorpay refund. Please confirm before proceeding.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = refundAmount.value,
|
||||||
|
onValueChange = { refundAmount.value = it.filter { ch -> ch.isDigit() } },
|
||||||
|
label = { Text("Refund amount") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = refundNotes.value,
|
||||||
|
onValueChange = { refundNotes.value = it },
|
||||||
|
label = { Text("Notes (optional)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.refundRazorpayPayment(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
paymentId = if (!payment.id.isNullOrBlank()) payment.id else null,
|
||||||
|
razorpayPaymentId = if (payment.id.isNullOrBlank()) payment.gatewayPaymentId else null,
|
||||||
|
amount = amountValue ?: 0L,
|
||||||
|
notes = refundNotes.value
|
||||||
|
)
|
||||||
|
refundTarget.value = null
|
||||||
|
},
|
||||||
|
enabled = canSubmit
|
||||||
|
) {
|
||||||
|
Text("Refund")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { refundTarget.value = null }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PaymentCard(
|
private fun PaymentCard(
|
||||||
payment: PaymentDto,
|
payment: PaymentDto,
|
||||||
canDeleteCash: Boolean,
|
canDeleteCash: Boolean,
|
||||||
onDelete: (String) -> Unit
|
canRefund: Boolean,
|
||||||
|
onDelete: (String) -> Unit,
|
||||||
|
onRefund: (PaymentDto) -> Unit
|
||||||
) {
|
) {
|
||||||
val date = payment.receivedAt?.let {
|
val date = payment.receivedAt?.let {
|
||||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||||
@@ -163,6 +240,11 @@ private fun PaymentCard(
|
|||||||
payment.currency?.let { append(" $it") }
|
payment.currency?.let { append(" $it") }
|
||||||
}
|
}
|
||||||
Text(text = amountText, style = MaterialTheme.typography.titleMedium)
|
Text(text = amountText, style = MaterialTheme.typography.titleMedium)
|
||||||
|
if (canRefund && (!payment.id.isNullOrBlank() || !payment.gatewayPaymentId.isNullOrBlank())) {
|
||||||
|
TextButton(onClick = { onRefund(payment) }) {
|
||||||
|
Text("Refund")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (canDeleteCash && isCash && !payment.id.isNullOrBlank()) {
|
if (canDeleteCash && isCash && !payment.id.isNullOrBlank()) {
|
||||||
IconButton(onClick = { onDelete(payment.id) }) {
|
IconButton(onClick = { onDelete(payment.id) }) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import com.android.trisolarispms.data.api.model.PaymentDto
|
|||||||
data class BookingPaymentsState(
|
data class BookingPaymentsState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
|
val message: String? = null,
|
||||||
val payments: List<PaymentDto> = emptyList()
|
val payments: List<PaymentDto> = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.android.trisolarispms.ui.payment
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.ApiClient
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@@ -14,7 +15,7 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun load(propertyId: String, bookingId: String) {
|
fun load(propertyId: String, bookingId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||||
try {
|
try {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
val response = api.listPayments(propertyId, bookingId)
|
val response = api.listPayments(propertyId, bookingId)
|
||||||
@@ -24,14 +25,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = body,
|
payments = body,
|
||||||
error = null
|
error = null,
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = "Load failed: ${response.code()}"
|
error = "Load failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,8 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = e.localizedMessage ?: "Load failed"
|
error = e.localizedMessage ?: "Load failed",
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,11 +52,11 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun addCashPayment(propertyId: String, bookingId: String, amount: Long) {
|
fun addCashPayment(propertyId: String, bookingId: String, amount: Long) {
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
_state.update { it.copy(error = "Amount must be greater than 0") }
|
_state.update { it.copy(error = "Amount must be greater than 0", message = null) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||||
try {
|
try {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
val response = api.createPayment(
|
val response = api.createPayment(
|
||||||
@@ -66,14 +70,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
current.copy(
|
current.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = listOf(body) + current.payments,
|
payments = listOf(body) + current.payments,
|
||||||
error = null
|
error = null,
|
||||||
|
message = "Cash payment added"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = "Create failed: ${response.code()}"
|
error = "Create failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +87,8 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = e.localizedMessage ?: "Create failed"
|
error = e.localizedMessage ?: "Create failed",
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +97,7 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||||
try {
|
try {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
val response = api.deletePayment(
|
val response = api.deletePayment(
|
||||||
@@ -103,14 +110,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
current.copy(
|
current.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = current.payments.filterNot { it.id == paymentId },
|
payments = current.payments.filterNot { it.id == paymentId },
|
||||||
error = null
|
error = null,
|
||||||
|
message = "Cash payment deleted"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = "Delete failed: ${response.code()}"
|
error = "Delete failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +127,69 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = e.localizedMessage ?: "Delete failed"
|
error = e.localizedMessage ?: "Delete failed",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refundRazorpayPayment(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
paymentId: String?,
|
||||||
|
razorpayPaymentId: String?,
|
||||||
|
amount: Long,
|
||||||
|
notes: String?
|
||||||
|
) {
|
||||||
|
if (amount <= 0) {
|
||||||
|
_state.update { it.copy(error = "Amount must be greater than 0", message = null) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (paymentId.isNullOrBlank() && razorpayPaymentId.isNullOrBlank()) {
|
||||||
|
_state.update { it.copy(error = "Missing payment ID", message = null) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.refundRazorpayPayment(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = RazorpayRefundRequest(
|
||||||
|
paymentId = paymentId,
|
||||||
|
razorpayPaymentId = razorpayPaymentId,
|
||||||
|
amount = amount,
|
||||||
|
notes = notes?.takeIf { it.isNotBlank() }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = null,
|
||||||
|
message = "Refund processed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
load(propertyId, bookingId)
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Refund failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Refund failed",
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,11 +74,12 @@ fun RazorpaySettingsScreen(
|
|||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
state = state,
|
state = state,
|
||||||
onKeyIdChange = viewModel::onMerchantKeyChange,
|
onKeyIdChange = viewModel::onMerchantKeyChange,
|
||||||
onKeySecretChange = viewModel::onSalt32Change,
|
onKeySecretChange = viewModel::onKeySecretChange,
|
||||||
onWebhookSecretChange = viewModel::onWebhookSecretChange,
|
onWebhookSecretChange = viewModel::onWebhookSecretChange,
|
||||||
|
onKeyIdTestChange = viewModel::onKeyIdTestChange,
|
||||||
|
onKeySecretTestChange = viewModel::onKeySecretTestChange,
|
||||||
|
onWebhookSecretTestChange = viewModel::onWebhookSecretTestChange,
|
||||||
onIsTestChange = viewModel::onIsTestChange,
|
onIsTestChange = viewModel::onIsTestChange,
|
||||||
onUseSalt256Change = viewModel::onUseSalt256Change,
|
|
||||||
onSalt256Change = viewModel::onSalt256Change,
|
|
||||||
onSave = { viewModel.save(propertyId) },
|
onSave = { viewModel.save(propertyId) },
|
||||||
clipboard = clipboard
|
clipboard = clipboard
|
||||||
)
|
)
|
||||||
@@ -93,9 +94,10 @@ private fun RazorpaySettingsTab(
|
|||||||
onKeyIdChange: (String) -> Unit,
|
onKeyIdChange: (String) -> Unit,
|
||||||
onKeySecretChange: (String) -> Unit,
|
onKeySecretChange: (String) -> Unit,
|
||||||
onWebhookSecretChange: (String) -> Unit,
|
onWebhookSecretChange: (String) -> Unit,
|
||||||
|
onKeyIdTestChange: (String) -> Unit,
|
||||||
|
onKeySecretTestChange: (String) -> Unit,
|
||||||
|
onWebhookSecretTestChange: (String) -> Unit,
|
||||||
onIsTestChange: (Boolean) -> Unit,
|
onIsTestChange: (Boolean) -> Unit,
|
||||||
onUseSalt256Change: (Boolean) -> Unit,
|
|
||||||
onSalt256Change: (String) -> Unit,
|
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
clipboard: ClipboardManager
|
clipboard: ClipboardManager
|
||||||
) {
|
) {
|
||||||
@@ -132,15 +134,12 @@ private fun RazorpaySettingsTab(
|
|||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
if (state.hasKeyId) {
|
if (state.hasKeyId) Text(text = "Live Key ID saved", style = MaterialTheme.typography.bodySmall)
|
||||||
Text(text = "Key ID saved", style = MaterialTheme.typography.bodySmall)
|
if (state.hasKeySecret) Text(text = "Live Key Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
if (state.hasWebhookSecret) Text(text = "Live Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||||
if (state.hasKeySecret) {
|
if (state.hasKeyIdTest) Text(text = "Test Key ID saved", style = MaterialTheme.typography.bodySmall)
|
||||||
Text(text = "Key Secret saved", style = MaterialTheme.typography.bodySmall)
|
if (state.hasKeySecretTest) Text(text = "Test Key Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
if (state.hasWebhookSecretTest) Text(text = "Test Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||||
if (state.hasWebhookSecret) {
|
|
||||||
Text(text = "Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
state.message?.let {
|
state.message?.let {
|
||||||
@@ -153,22 +152,59 @@ private fun RazorpaySettingsTab(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
Text(text = "Live", style = MaterialTheme.typography.titleSmall)
|
||||||
value = state.keyId,
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
onValueChange = onKeyIdChange,
|
OutlinedTextField(
|
||||||
label = { Text("Key ID") },
|
value = state.keyId,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
onValueChange = onKeyIdChange,
|
||||||
modifier = Modifier.fillMaxWidth()
|
label = { Text("Key ID (Live)") },
|
||||||
)
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
modifier = Modifier.fillMaxWidth()
|
||||||
OutlinedTextField(
|
)
|
||||||
value = state.webhookSecret,
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
onValueChange = onWebhookSecretChange,
|
OutlinedTextField(
|
||||||
label = { Text("Webhook Secret") },
|
value = state.keySecret,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
onValueChange = onKeySecretChange,
|
||||||
modifier = Modifier.fillMaxWidth()
|
label = { Text("Key Secret (Live)") },
|
||||||
)
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.webhookSecret,
|
||||||
|
onValueChange = onWebhookSecretChange,
|
||||||
|
label = { Text("Webhook Secret (Live)") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(text = "Test", style = MaterialTheme.typography.titleSmall)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.keyIdTest,
|
||||||
|
onValueChange = onKeyIdTestChange,
|
||||||
|
label = { Text("Key ID (Test)") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.keySecretTest,
|
||||||
|
onValueChange = onKeySecretTestChange,
|
||||||
|
label = { Text("Key Secret (Test)") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.webhookSecretTest,
|
||||||
|
onValueChange = onWebhookSecretTestChange,
|
||||||
|
label = { Text("Webhook Secret (Test)") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -183,53 +219,8 @@ private fun RazorpaySettingsTab(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(text = "Use secondary secret")
|
|
||||||
Switch(
|
|
||||||
checked = state.useSalt256,
|
|
||||||
onCheckedChange = onUseSalt256Change
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
if (state.useSalt256) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.salt256,
|
|
||||||
onValueChange = onSalt256Change,
|
|
||||||
label = { Text("Secret (secondary)") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
if (state.hasSalt256) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Salt256 saved",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.keySecret,
|
|
||||||
onValueChange = onKeySecretChange,
|
|
||||||
label = { Text("Key Secret") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
if (state.hasKeySecret) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Key Secret saved",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onSave,
|
onClick = onSave,
|
||||||
enabled = !state.isSaving,
|
enabled = !state.isSaving,
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ data class RazorpaySettingsState(
|
|||||||
val keyId: String = "",
|
val keyId: String = "",
|
||||||
val keySecret: String = "",
|
val keySecret: String = "",
|
||||||
val webhookSecret: String = "",
|
val webhookSecret: String = "",
|
||||||
val salt256: String = "",
|
val keyIdTest: String = "",
|
||||||
|
val keySecretTest: String = "",
|
||||||
|
val webhookSecretTest: String = "",
|
||||||
val isTest: Boolean = false,
|
val isTest: Boolean = false,
|
||||||
val useSalt256: Boolean = false,
|
|
||||||
val hasKeyId: Boolean = false,
|
val hasKeyId: Boolean = false,
|
||||||
val hasKeySecret: Boolean = false,
|
val hasKeySecret: Boolean = false,
|
||||||
val hasWebhookSecret: Boolean = false,
|
val hasWebhookSecret: Boolean = false,
|
||||||
val hasSalt256: Boolean = false,
|
val hasKeyIdTest: Boolean = false,
|
||||||
|
val hasKeySecretTest: Boolean = false,
|
||||||
|
val hasWebhookSecretTest: Boolean = false,
|
||||||
val configured: Boolean = false,
|
val configured: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val isSaving: Boolean = false,
|
val isSaving: Boolean = false,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class RazorpaySettingsViewModel : ViewModel() {
|
|||||||
_state.update { it.copy(keyId = value, error = null) }
|
_state.update { it.copy(keyId = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSalt32Change(value: String) {
|
fun onKeySecretChange(value: String) {
|
||||||
_state.update { it.copy(keySecret = value, error = null) }
|
_state.update { it.copy(keySecret = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,37 +31,51 @@ class RazorpaySettingsViewModel : ViewModel() {
|
|||||||
_state.update { it.copy(webhookSecret = value, error = null) }
|
_state.update { it.copy(webhookSecret = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSalt256Change(value: String) {
|
fun onKeyIdTestChange(value: String) {
|
||||||
_state.update { it.copy(salt256 = value, error = null) }
|
_state.update { it.copy(keyIdTest = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKeySecretTestChange(value: String) {
|
||||||
|
_state.update { it.copy(keySecretTest = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onWebhookSecretTestChange(value: String) {
|
||||||
|
_state.update { it.copy(webhookSecretTest = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onIsTestChange(value: Boolean) {
|
fun onIsTestChange(value: Boolean) {
|
||||||
_state.update { it.copy(isTest = value, error = null) }
|
_state.update { it.copy(isTest = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUseSalt256Change(value: Boolean) {
|
|
||||||
_state.update { it.copy(useSalt256 = value, error = null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save(propertyId: String) {
|
fun save(propertyId: String) {
|
||||||
val current = state.value
|
val current = state.value
|
||||||
val keyId = current.keyId.trim()
|
val keyId = current.keyId.trim()
|
||||||
val keySecret = current.keySecret.trim()
|
val keySecret = current.keySecret.trim()
|
||||||
val webhookSecret = current.webhookSecret.trim()
|
val webhookSecret = current.webhookSecret.trim()
|
||||||
val salt256 = current.salt256.trim()
|
val keyIdTest = current.keyIdTest.trim()
|
||||||
|
val keySecretTest = current.keySecretTest.trim()
|
||||||
|
val webhookSecretTest = current.webhookSecretTest.trim()
|
||||||
|
|
||||||
if (!current.configured) {
|
if (!current.configured) {
|
||||||
if (keyId.isBlank()) {
|
val needsTest = current.isTest
|
||||||
_state.update { it.copy(error = "Key ID is required") }
|
if (needsTest) {
|
||||||
return
|
if (keyIdTest.isBlank() && !current.hasKeyIdTest) {
|
||||||
}
|
_state.update { it.copy(error = "Test Key ID is required") }
|
||||||
if (current.useSalt256 && salt256.isBlank() && !current.hasSalt256) {
|
return
|
||||||
_state.update { it.copy(error = "Secondary secret is required") }
|
}
|
||||||
return
|
if (keySecretTest.isBlank() && !current.hasKeySecretTest) {
|
||||||
}
|
_state.update { it.copy(error = "Test Key Secret is required") }
|
||||||
if (!current.useSalt256 && keySecret.isBlank() && !current.hasKeySecret) {
|
return
|
||||||
_state.update { it.copy(error = "Key secret is required") }
|
}
|
||||||
return
|
} else {
|
||||||
|
if (keyId.isBlank() && !current.hasKeyId) {
|
||||||
|
_state.update { it.copy(error = "Key ID is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (keySecret.isBlank() && !current.hasKeySecret) {
|
||||||
|
_state.update { it.copy(error = "Key Secret is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val hasKeyIdInput = keyId.isNotBlank()
|
val hasKeyIdInput = keyId.isNotBlank()
|
||||||
@@ -70,6 +84,12 @@ class RazorpaySettingsViewModel : ViewModel() {
|
|||||||
_state.update { it.copy(error = "Key ID and Key Secret must be provided together") }
|
_state.update { it.copy(error = "Key ID and Key Secret must be provided together") }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val hasKeyIdTestInput = keyIdTest.isNotBlank()
|
||||||
|
val hasKeySecretTestInput = keySecretTest.isNotBlank()
|
||||||
|
if (hasKeyIdTestInput xor hasKeySecretTestInput) {
|
||||||
|
_state.update { it.copy(error = "Test Key ID and Test Key Secret must be provided together") }
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -79,11 +99,13 @@ class RazorpaySettingsViewModel : ViewModel() {
|
|||||||
val response = api.updateRazorpaySettings(
|
val response = api.updateRazorpaySettings(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
body = RazorpaySettingsRequest(
|
body = RazorpaySettingsRequest(
|
||||||
keyId = keyId,
|
keyId = keyId.ifBlank { null },
|
||||||
keySecret = keySecret.ifBlank { null },
|
keySecret = keySecret.ifBlank { null },
|
||||||
webhookSecret = webhookSecret.ifBlank { null },
|
webhookSecret = webhookSecret.ifBlank { null },
|
||||||
isTest = current.isTest,
|
keyIdTest = keyIdTest.ifBlank { null },
|
||||||
useSalt256 = current.useSalt256
|
keySecretTest = keySecretTest.ifBlank { null },
|
||||||
|
webhookSecretTest = webhookSecretTest.ifBlank { null },
|
||||||
|
isTest = current.isTest
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
@@ -124,15 +146,18 @@ class RazorpaySettingsViewModel : ViewModel() {
|
|||||||
it.copy(
|
it.copy(
|
||||||
keyId = "",
|
keyId = "",
|
||||||
isTest = body.isTest == true,
|
isTest = body.isTest == true,
|
||||||
useSalt256 = body.useSalt256 == true,
|
|
||||||
hasKeyId = body.hasKeyId == true,
|
hasKeyId = body.hasKeyId == true,
|
||||||
hasKeySecret = body.hasKeySecret == true,
|
hasKeySecret = body.hasKeySecret == true,
|
||||||
hasWebhookSecret = body.hasWebhookSecret == true,
|
hasWebhookSecret = body.hasWebhookSecret == true,
|
||||||
hasSalt256 = body.hasSalt256 == true,
|
hasKeyIdTest = body.hasKeyIdTest == true,
|
||||||
|
hasKeySecretTest = body.hasKeySecretTest == true,
|
||||||
|
hasWebhookSecretTest = body.hasWebhookSecretTest == true,
|
||||||
configured = body.configured == true,
|
configured = body.configured == true,
|
||||||
keySecret = "",
|
keySecret = "",
|
||||||
webhookSecret = "",
|
webhookSecret = "",
|
||||||
salt256 = "",
|
keyIdTest = "",
|
||||||
|
keySecretTest = "",
|
||||||
|
webhookSecretTest = "",
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
error = null
|
error = null
|
||||||
|
|||||||
Reference in New Issue
Block a user