razorpay: add payment link support

This commit is contained in:
androidlover5842
2026-02-01 14:37:05 +05:30
parent 8f62459d5e
commit 4c31a20af4
5 changed files with 103 additions and 38 deletions

View File

@@ -15,7 +15,7 @@ import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.android.trisolarispms.data.api.model.RoomStayDto
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
import com.android.trisolarispms.data.api.model.RazorpayQrResponse
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
@@ -139,17 +139,17 @@ interface BookingApi {
@Path("paymentId") paymentId: String
): Response<Unit>
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr")
suspend fun listRazorpayQrs(
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/requests")
suspend fun listRazorpayRequests(
@Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String
): Response<List<RazorpayQrListItemDto>>
): Response<List<RazorpayRequestListItemDto>>
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/close")
suspend fun closeRazorpayQr(
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/close")
suspend fun closeRazorpayRequest(
@Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String,
@Path("qrId") qrId: String
@Body body: com.android.trisolarispms.data.api.model.RazorpayCloseRequest
): Response<Unit>
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events")

View File

@@ -24,6 +24,11 @@ data class RazorpayPaymentLinkResponse(
val paymentLink: String? = null
)
data class RazorpayCloseRequest(
val qrId: String? = null,
val paymentLinkId: String? = null
)
data class RazorpayQrEventDto(
val event: String? = null,
val qrId: String? = null,
@@ -31,12 +36,16 @@ data class RazorpayQrEventDto(
val receivedAt: String? = null
)
data class RazorpayQrListItemDto(
val qrId: String? = null,
data class RazorpayRequestListItemDto(
val type: String? = null,
val requestId: String? = null,
val amount: Long? = null,
val currency: String? = null,
val status: String? = null,
val createdAt: String? = null,
val qrId: String? = null,
val imageUrl: String? = null,
val expiryAt: String? = null,
val createdAt: String? = null
val paymentLinkId: String? = null,
val paymentLink: String? = null
)

View File

@@ -29,6 +29,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -45,6 +46,9 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.activity.compose.BackHandler
import com.android.trisolarispms.BuildConfig
import androidx.compose.ui.platform.LocalContext
import android.content.Intent
import android.net.Uri
@Composable
@OptIn(ExperimentalMaterial3Api::class)
@@ -57,6 +61,7 @@ fun RazorpayQrScreen(
viewModel: RazorpayQrViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current
DisposableEffect(Unit) {
onDispose { viewModel.reset() }
@@ -205,6 +210,18 @@ fun RazorpayQrScreen(
Text("Generate QR")
}
}
Spacer(modifier = Modifier.height(8.dp))
TextButton(
onClick = { viewModel.generateLink(propertyId, bookingId) },
enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (state.isLoading) {
Text("Generating link…")
} else {
Text("Generate payment link")
}
}
state.error?.let {
Spacer(modifier = Modifier.height(12.dp))
@@ -213,13 +230,19 @@ fun RazorpayQrScreen(
if (state.qrList.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
Text(text = "QR List", style = MaterialTheme.typography.titleMedium)
Text(text = "Requests", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
state.qrList.forEachIndexed { index, item ->
val status = item.status?.lowercase().orEmpty()
val isInactive = status == "closed" || status == "expired" || status == "credited" || status == "cancelled" || status == "canceled"
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { viewModel.openQrFromList(item) },
.clickable(
enabled = !isInactive &&
item.type?.equals("QR", ignoreCase = true) == true &&
!item.imageUrl.isNullOrBlank()
) { viewModel.openQrFromList(item) },
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
@@ -230,23 +253,58 @@ fun RazorpayQrScreen(
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "QR ${index + 1}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Column {
Text(
text = when (item.type?.uppercase()) {
"PAYMENT_LINK" -> "Payment Link"
else -> "QR"
} + " ${index + 1}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
item.status?.let { status ->
Text(
text = status.replaceFirstChar { it.uppercase() },
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = "${item.amount ?: "-"} ${item.currency.orEmpty()}",
style = MaterialTheme.typography.titleMedium
)
val qrId = item.qrId
if (!qrId.isNullOrBlank()) {
if (item.type?.equals("PAYMENT_LINK", ignoreCase = true) == true &&
!item.paymentLink.isNullOrBlank() &&
!isInactive
) {
Spacer(modifier = Modifier.width(8.dp))
TextButton(
onClick = {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, item.paymentLink)
if (!guestPhone.isNullOrBlank()) {
putExtra("address", guestPhone)
putExtra(Intent.EXTRA_PHONE_NUMBER, guestPhone)
}
}
context.startActivity(
Intent.createChooser(intent, "Share payment link")
)
}
) {
Text("Share")
}
}
if (!item.qrId.isNullOrBlank() || !item.paymentLinkId.isNullOrBlank()) {
IconButton(
onClick = {
viewModel.closeQr(propertyId, bookingId, qrId)
viewModel.closeRequest(propertyId, bookingId, item)
},
modifier = Modifier.size(32.dp)
modifier = Modifier.size(32.dp),
enabled = !isInactive
) {
Icon(
imageVector = Icons.Default.Delete,

View File

@@ -12,5 +12,5 @@ data class RazorpayQrState(
val isClosed: Boolean = false,
val isCredited: Boolean = false,
val paymentLink: String? = null,
val qrList: List<com.android.trisolarispms.data.api.model.RazorpayQrListItemDto> = emptyList()
val qrList: List<com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto> = emptyList()
)

View File

@@ -4,8 +4,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.model.RazorpayCloseRequest
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
import com.google.gson.Gson
@@ -232,16 +233,10 @@ class RazorpayQrViewModel : ViewModel() {
fun loadQrList(propertyId: String, bookingId: String) {
viewModelScope.launch {
try {
val response = ApiClient.create().listRazorpayQrs(propertyId, bookingId)
val response = ApiClient.create().listRazorpayRequests(propertyId, bookingId)
val body = response.body()
if (response.isSuccessful && body != null) {
val filtered = body.filterNot { item ->
when (item.status?.lowercase()) {
"closed", "expired", "credited" -> true
else -> false
}
}
_state.update { it.copy(qrList = filtered) }
_state.update { it.copy(qrList = body) }
}
} catch (_: Exception) {
// ignore list load errors
@@ -249,17 +244,20 @@ class RazorpayQrViewModel : ViewModel() {
}
}
fun closeQr(propertyId: String, bookingId: String, qrId: String) {
fun closeRequest(propertyId: String, bookingId: String, item: RazorpayRequestListItemDto) {
val qrId = item.qrId
val paymentLinkId = item.paymentLinkId
if (qrId.isNullOrBlank() && paymentLinkId.isNullOrBlank()) return
viewModelScope.launch {
try {
val response = ApiClient.create().closeRazorpayQr(
val response = ApiClient.create().closeRazorpayRequest(
propertyId = propertyId,
bookingId = bookingId,
qrId = qrId
body = RazorpayCloseRequest(qrId = qrId, paymentLinkId = paymentLinkId)
)
if (response.isSuccessful) {
_state.update { current ->
current.copy(qrList = current.qrList.filterNot { it.qrId == qrId })
current.copy(qrList = current.qrList.filterNot { it.requestId == item.requestId })
}
loadQrList(propertyId, bookingId)
}
@@ -269,7 +267,7 @@ class RazorpayQrViewModel : ViewModel() {
}
}
fun openQrFromList(item: RazorpayQrListItemDto) {
fun openQrFromList(item: RazorpayRequestListItemDto) {
val status = item.status?.lowercase()
_state.update {
it.copy(
@@ -288,7 +286,7 @@ class RazorpayQrViewModel : ViewModel() {
startQrEventStream(propertyId, bookingId, state.value.qrId)
}
fun generateLink(propertyId: String, bookingId: String, onReady: (String) -> Unit) {
fun generateLink(propertyId: String, bookingId: String) {
val current = state.value
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
if (amount == null) {
@@ -315,7 +313,7 @@ class RazorpayQrViewModel : ViewModel() {
error = null
)
}
onReady(body.paymentLink)
loadQrList(propertyId, bookingId)
} else {
_state.update {
it.copy(