remove that crap payu

This commit is contained in:
androidlover5842
2026-02-01 13:27:55 +05:30
parent 2b0f352ced
commit 43ee7311e8
25 changed files with 1046 additions and 1112 deletions

View File

@@ -36,8 +36,8 @@ import com.android.trisolarispms.ui.roomimage.ImageTagsScreen
import com.android.trisolarispms.ui.roomimage.AddImageTagScreen
import com.android.trisolarispms.ui.roomimage.EditImageTagScreen
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
import com.android.trisolarispms.ui.payu.PayuSettingsScreen
import com.android.trisolarispms.ui.payu.PayuQrScreen
import com.android.trisolarispms.ui.razorpay.RazorpaySettingsScreen
import com.android.trisolarispms.ui.razorpay.RazorpayQrScreen
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
@@ -86,7 +86,7 @@ class MainActivity : ComponentActivity() {
it == "ADMIN" || it == "MANAGER" || it == "STAFF"
} == true
}
val canManagePayuSettings: (String) -> Boolean = { propertyId ->
val canManageRazorpaySettings: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || state.propertyRoles[propertyId]?.any {
it == "ADMIN" || it == "MANAGER"
} == true
@@ -125,11 +125,11 @@ class MainActivity : ComponentActivity() {
currentRoute.propertyId,
currentRoute.roomTypeId
)
is AppRoute.PayuSettings -> route.value = AppRoute.ActiveRoomStays(
is AppRoute.RazorpaySettings -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.PayuQr -> route.value = AppRoute.BookingDetailsTabs(
is AppRoute.RazorpayQr -> route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
@@ -278,8 +278,8 @@ class MainActivity : ComponentActivity() {
onBack = { route.value = AppRoute.Home },
onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
showPayuSettings = canManagePayuSettings(currentRoute.propertyId),
onPayuSettings = { route.value = AppRoute.PayuSettings(currentRoute.propertyId) },
showRazorpaySettings = canManageRazorpaySettings(currentRoute.propertyId),
onRazorpaySettings = { route.value = AppRoute.RazorpaySettings(currentRoute.propertyId) },
onManageRoomStay = { booking ->
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
?: booking.expectedCheckInAt.orEmpty()
@@ -308,7 +308,7 @@ class MainActivity : ComponentActivity() {
)
}
)
is AppRoute.PayuSettings -> PayuSettingsScreen(
is AppRoute.RazorpaySettings -> RazorpaySettingsScreen(
propertyId = currentRoute.propertyId,
onBack = {
route.value = AppRoute.ActiveRoomStays(
@@ -317,7 +317,7 @@ class MainActivity : ComponentActivity() {
)
}
)
is AppRoute.PayuQr -> PayuQrScreen(
is AppRoute.RazorpayQr -> RazorpayQrScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
pendingAmount = currentRoute.pendingAmount,
@@ -470,8 +470,8 @@ class MainActivity : ComponentActivity() {
guestId
)
},
onOpenPayuQr = { pendingAmount, guestPhone ->
route.value = AppRoute.PayuQr(
onOpenRazorpayQr = { pendingAmount, guestPhone ->
route.value = AppRoute.RazorpayQr(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
pendingAmount = pendingAmount,
@@ -484,12 +484,12 @@ class MainActivity : ComponentActivity() {
bookingId = currentRoute.bookingId
)
},
canManageDocuments = canManagePayuSettings(currentRoute.propertyId)
canManageDocuments = canManageRazorpaySettings(currentRoute.propertyId)
)
is AppRoute.BookingPayments -> BookingPaymentsScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
canAddCash = canManagePayuSettings(currentRoute.propertyId),
canAddCash = canManageRazorpaySettings(currentRoute.propertyId),
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
onBack = {
route.value = AppRoute.BookingDetailsTabs(

View File

@@ -16,5 +16,4 @@ interface ApiService :
InboundEmailApi,
AmenityApi,
RatePlanApi,
PayuSettingsApi,
PayuPaymentLinkSettingsApi
RazorpaySettingsApi

View File

@@ -14,10 +14,12 @@ import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
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.PayuQrRequest
import com.android.trisolarispms.data.api.model.PayuQrResponse
import com.android.trisolarispms.data.api.model.PayuLinkRequest
import com.android.trisolarispms.data.api.model.PayuLinkResponse
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
import com.android.trisolarispms.data.api.model.RazorpayQrResponse
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkResponse
import com.android.trisolarispms.data.api.model.PaymentDto
import com.android.trisolarispms.data.api.model.PaymentCreateRequest
import retrofit2.Response
@@ -103,19 +105,19 @@ interface BookingApi {
@Body body: BookingRoomStayCreateRequest
): Response<RoomStayDto>
@POST("properties/{propertyId}/bookings/{bookingId}/payments/payu/qr")
suspend fun generatePayuQr(
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr")
suspend fun generateRazorpayQr(
@Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String,
@Body body: PayuQrRequest
): Response<PayuQrResponse>
@Body body: RazorpayQrRequest
): Response<RazorpayQrResponse>
@POST("properties/{propertyId}/bookings/{bookingId}/payments/payu/link")
suspend fun generatePayuLink(
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/payment-link")
suspend fun generateRazorpayPaymentLink(
@Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String,
@Body body: PayuLinkRequest
): Response<PayuLinkResponse>
@Body body: RazorpayPaymentLinkRequest
): Response<RazorpayPaymentLinkResponse>
@GET("properties/{propertyId}/bookings/{bookingId}/payments")
suspend fun listPayments(
@@ -136,4 +138,24 @@ interface BookingApi {
@Path("bookingId") bookingId: String,
@Path("paymentId") paymentId: String
): Response<Unit>
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr")
suspend fun listRazorpayQrs(
@Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String
): Response<List<RazorpayQrListItemDto>>
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/close")
suspend fun closeRazorpayQr(
@Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String,
@Path("qrId") qrId: String
): Response<Unit>
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events")
suspend fun listRazorpayQrEvents(
@Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String,
@Path("qrId") qrId: String
): Response<List<RazorpayQrEventDto>>
}

View File

@@ -1,22 +0,0 @@
package com.android.trisolarispms.data.api
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsRequest
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
interface PayuPaymentLinkSettingsApi {
@GET("properties/{propertyId}/payu-payment-link-settings")
suspend fun getPayuPaymentLinkSettings(
@Path("propertyId") propertyId: String
): Response<PayuPaymentLinkSettingsResponse>
@PUT("properties/{propertyId}/payu-payment-link-settings")
suspend fun updatePayuPaymentLinkSettings(
@Path("propertyId") propertyId: String,
@Body body: PayuPaymentLinkSettingsRequest
): Response<PayuPaymentLinkSettingsResponse>
}

View File

@@ -1,22 +0,0 @@
package com.android.trisolarispms.data.api
import com.android.trisolarispms.data.api.model.PayuSettingsRequest
import com.android.trisolarispms.data.api.model.PayuSettingsResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
interface PayuSettingsApi {
@GET("properties/{propertyId}/payu-settings")
suspend fun getPayuSettings(
@Path("propertyId") propertyId: String
): Response<PayuSettingsResponse>
@PUT("properties/{propertyId}/payu-settings")
suspend fun updatePayuSettings(
@Path("propertyId") propertyId: String,
@Body body: PayuSettingsRequest
): Response<PayuSettingsResponse>
}

View File

@@ -0,0 +1,22 @@
package com.android.trisolarispms.data.api
import com.android.trisolarispms.data.api.model.RazorpaySettingsRequest
import com.android.trisolarispms.data.api.model.RazorpaySettingsResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
interface RazorpaySettingsApi {
@GET("properties/{propertyId}/razorpay-settings")
suspend fun getRazorpaySettings(
@Path("propertyId") propertyId: String
): Response<RazorpaySettingsResponse>
@PUT("properties/{propertyId}/razorpay-settings")
suspend fun updateRazorpaySettings(
@Path("propertyId") propertyId: String,
@Body body: RazorpaySettingsRequest
): Response<RazorpaySettingsResponse>
}

View File

@@ -1,18 +0,0 @@
package com.android.trisolarispms.data.api.model
data class PayuPaymentLinkSettingsRequest(
val merchantId: String,
val clientId: String,
val clientSecret: String,
val isTest: Boolean
)
data class PayuPaymentLinkSettingsResponse(
val propertyId: String? = null,
val configured: Boolean? = null,
val merchantId: String? = null,
val isTest: Boolean? = null,
val hasClientId: Boolean? = null,
val hasClientSecret: Boolean? = null,
val hasAccessToken: Boolean? = null
)

View File

@@ -1,26 +0,0 @@
package com.android.trisolarispms.data.api.model
import com.google.gson.JsonElement
data class PayuQrRequest(
val amount: Long,
val deviceInfo: String
)
data class PayuQrResponse(
val txnid: String? = null,
val amount: Long? = null,
val currency: String? = null,
val payuResponse: JsonElement? = null
)
data class PayuLinkRequest(
val amount: Long
)
data class PayuLinkResponse(
val amount: Long? = null,
val currency: String? = null,
val paymentLink: String? = null,
val payuResponse: JsonElement? = null
)

View File

@@ -0,0 +1,42 @@
package com.android.trisolarispms.data.api.model
import com.google.gson.JsonElement
data class RazorpayQrRequest(
val amount: Long,
val deviceInfo: String
)
data class RazorpayQrResponse(
val qrId: String? = null,
val amount: Long? = null,
val currency: String? = null,
val imageUrl: String? = null
)
data class RazorpayPaymentLinkRequest(
val amount: Long
)
data class RazorpayPaymentLinkResponse(
val amount: Long? = null,
val currency: String? = null,
val paymentLink: String? = null
)
data class RazorpayQrEventDto(
val event: String? = null,
val qrId: String? = null,
val status: String? = null,
val receivedAt: String? = null
)
data class RazorpayQrListItemDto(
val qrId: String? = null,
val amount: Long? = null,
val currency: String? = null,
val status: String? = null,
val imageUrl: String? = null,
val expiryAt: String? = null,
val createdAt: String? = null
)

View File

@@ -2,20 +2,21 @@ package com.android.trisolarispms.data.api.model
import com.google.gson.annotations.SerializedName
data class PayuSettingsRequest(
val merchantKey: String,
val salt32: String? = null,
val salt256: String? = null,
data class RazorpaySettingsRequest(
val keyId: String,
val keySecret: String? = null,
val webhookSecret: String? = null,
@SerializedName("isTest") val isTest: Boolean,
val useSalt256: Boolean
)
data class PayuSettingsResponse(
data class RazorpaySettingsResponse(
val propertyId: String? = null,
val configured: Boolean? = null,
val merchantKey: String? = null,
@SerializedName("test") val isTest: Boolean? = null,
val useSalt256: Boolean? = null,
val hasSalt32: Boolean? = null,
val hasKeyId: Boolean? = null,
val hasKeySecret: Boolean? = null,
val hasWebhookSecret: Boolean? = null,
val hasSalt256: Boolean? = null
)

View File

@@ -67,8 +67,8 @@ sealed interface AppRoute {
val ratePlanId: String,
val ratePlanCode: String
) : AppRoute
data class PayuSettings(val propertyId: String) : AppRoute
data class PayuQr(
data class RazorpaySettings(val propertyId: String) : AppRoute
data class RazorpayQr(
val propertyId: String,
val bookingId: String,
val pendingAmount: Long?,

View File

@@ -1,16 +0,0 @@
package com.android.trisolarispms.ui.payu
data class PayuPaymentLinkSettingsState(
val merchantId: String = "",
val clientId: String = "",
val clientSecret: String = "",
val isTest: Boolean = false,
val configured: Boolean = false,
val hasClientId: Boolean = false,
val hasClientSecret: Boolean = false,
val hasAccessToken: Boolean = false,
val isLoading: Boolean = false,
val isSaving: Boolean = false,
val error: String? = null,
val message: String? = null
)

View File

@@ -1,120 +0,0 @@
package com.android.trisolarispms.ui.payu
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class PayuPaymentLinkSettingsViewModel : ViewModel() {
private val _state = MutableStateFlow(PayuPaymentLinkSettingsState())
val state: StateFlow<PayuPaymentLinkSettingsState> = _state
fun load(propertyId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
val response = api.getPayuPaymentLinkSettings(propertyId)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
merchantId = body.merchantId.orEmpty(),
isTest = body.isTest == true,
configured = body.configured == true,
hasClientId = body.hasClientId == true,
hasClientSecret = body.hasClientSecret == true,
hasAccessToken = body.hasAccessToken == true,
clientId = "",
clientSecret = "",
isLoading = false,
error = null
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "Load failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Load failed",
message = null
)
}
}
}
}
fun onMerchantIdChange(value: String) {
_state.update { it.copy(merchantId = value, error = null) }
}
fun onClientIdChange(value: String) {
_state.update { it.copy(clientId = value, error = null) }
}
fun onClientSecretChange(value: String) {
_state.update { it.copy(clientSecret = value, error = null) }
}
fun onIsTestChange(value: Boolean) {
_state.update { it.copy(isTest = value, error = null) }
}
fun save(propertyId: String) {
val current = state.value
val merchantId = current.merchantId.trim()
val clientId = current.clientId.trim()
val clientSecret = current.clientSecret.trim()
if (merchantId.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
_state.update { it.copy(error = "All fields are required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isSaving = true, error = null, message = null) }
try {
val api = ApiClient.create()
val response = api.updatePayuPaymentLinkSettings(
propertyId = propertyId,
body = PayuPaymentLinkSettingsRequest(
merchantId = merchantId,
clientId = clientId,
clientSecret = clientSecret,
isTest = current.isTest
)
)
if (response.isSuccessful) {
_state.update { it.copy(isSaving = false, message = "Saved", error = null) }
load(propertyId)
} else {
_state.update {
it.copy(
isSaving = false,
error = "Save failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isSaving = false,
error = e.localizedMessage ?: "Save failed",
message = null
)
}
}
}
}
}

View File

@@ -1,194 +0,0 @@
package com.android.trisolarispms.ui.payu
import android.graphics.Bitmap
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.common.BitMatrix
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun PayuQrScreen(
propertyId: String,
bookingId: String,
pendingAmount: Long?,
guestPhone: String?,
onBack: () -> Unit,
viewModel: PayuQrViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
val clipboard = LocalClipboardManager.current
val context = LocalContext.current
DisposableEffect(Unit) {
onDispose { viewModel.reset() }
}
LaunchedEffect(pendingAmount) {
viewModel.setInitialAmount(pendingAmount)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("PayU QR") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors()
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
OutlinedTextField(
value = state.amountInput,
onValueChange = viewModel::onAmountChange,
label = { Text("Amount") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = {
if (!guestPhone.isNullOrBlank()) {
viewModel.generateLink(propertyId, bookingId) { link ->
val uri = Uri.parse("smsto:${guestPhone}")
val intent = Intent(Intent.ACTION_SENDTO, uri)
.putExtra("sms_body", "Pay using this link: $link")
context.startActivity(intent)
}
}
},
enabled = !state.isLoading && !guestPhone.isNullOrBlank(),
modifier = Modifier.weight(1f)
) {
Text("Share Link")
}
Button(
onClick = { viewModel.generate(propertyId, bookingId) },
enabled = !state.isLoading,
modifier = Modifier.weight(1f)
) {
if (state.isLoading) {
CircularProgressIndicator(
modifier = Modifier
.height(16.dp)
.padding(end = 8.dp),
strokeWidth = 2.dp
)
Text("Generating...")
} else {
Text("Generate QR")
}
}
}
state.error?.let {
Spacer(modifier = Modifier.height(12.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
if (!state.qrString.isNullOrBlank()) {
Spacer(modifier = Modifier.height(16.dp))
val qrBitmap = remember(state.qrString) { state.qrString?.let { createQrBitmap(it) } }
qrBitmap?.let {
androidx.compose.foundation.Image(
bitmap = it.asImageBitmap(),
contentDescription = "PayU QR",
modifier = Modifier
.fillMaxWidth()
.height(240.dp)
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Amount: ${state.amount ?: "-"} ${state.currency ?: ""}",
style = MaterialTheme.typography.bodyMedium
)
Button(
onClick = {
clipboard.setText(AnnotatedString(state.qrString.orEmpty()))
}
) {
Text("Copy QR string")
}
}
}
}
}
}
private fun createQrBitmap(content: String, size: Int = 600): Bitmap {
val matrix: BitMatrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size)
val width = matrix.width
val height = matrix.height
val pixels = IntArray(width * height)
for (y in 0 until height) {
val offset = y * width
for (x in 0 until width) {
pixels[offset + x] = if (matrix.get(x, y)) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
}
}
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
setPixels(pixels, 0, width, 0, 0, width, height)
}
}

View File

@@ -1,14 +0,0 @@
package com.android.trisolarispms.ui.payu
data class PayuQrState(
val deviceInfo: String = "",
val amountInput: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val txnId: String? = null,
val amount: Long? = null,
val currency: String? = null,
val qrString: String? = null,
val payuResponseRaw: String? = null,
val paymentLink: String? = null
)

View File

@@ -1,163 +0,0 @@
package com.android.trisolarispms.ui.payu
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.model.PayuLinkRequest
import com.android.trisolarispms.data.api.model.PayuQrRequest
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class PayuQrViewModel : ViewModel() {
private val gson = Gson()
private val _state = MutableStateFlow(
PayuQrState(deviceInfo = buildDeviceInfo())
)
val state: StateFlow<PayuQrState> = _state
fun reset() {
_state.value = PayuQrState(deviceInfo = buildDeviceInfo())
}
fun onAmountChange(value: String) {
val digits = value.filter { it.isDigit() }
_state.update { it.copy(amountInput = digits, error = null) }
}
fun setInitialAmount(amount: Long?) {
if (amount == null || amount <= 0) return
_state.update { current ->
if (current.amountInput.isBlank()) {
current.copy(amountInput = amount.toString())
} else {
current
}
}
}
fun generate(propertyId: String, bookingId: String) {
val current = state.value
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
if (amount == null) {
_state.update { it.copy(error = "Amount is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.generatePayuQr(
propertyId = propertyId,
bookingId = bookingId,
body = PayuQrRequest(
amount = amount,
deviceInfo = current.deviceInfo
)
)
val body = response.body()
if (response.isSuccessful && body != null) {
val (qrString, rawPayload) = extractQrString(body.payuResponse)
_state.update {
it.copy(
isLoading = false,
txnId = body.txnid,
amount = body.amount,
currency = body.currency,
qrString = qrString,
payuResponseRaw = rawPayload,
error = null
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "QR request failed: ${response.code()}"
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "QR request failed"
)
}
}
}
}
fun generateLink(propertyId: String, bookingId: String, onReady: (String) -> Unit) {
val current = state.value
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
if (amount == null) {
_state.update { it.copy(error = "Amount is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.generatePayuLink(
propertyId = propertyId,
bookingId = bookingId,
body = PayuLinkRequest(amount = amount)
)
val body = response.body()
if (response.isSuccessful && body?.paymentLink != null) {
_state.update {
it.copy(
isLoading = false,
paymentLink = body.paymentLink,
amount = body.amount,
currency = body.currency,
error = null
)
}
onReady(body.paymentLink)
} else {
_state.update {
it.copy(
isLoading = false,
error = "Link request failed: ${response.code()}"
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Link request failed"
)
}
}
}
}
private fun extractQrString(payuResponse: JsonElement?): Pair<String?, String?> {
if (payuResponse == null) return null to null
val raw = gson.toJson(payuResponse)
val root = when {
payuResponse.isJsonObject -> payuResponse.asJsonObject
payuResponse.isJsonPrimitive && payuResponse.asJsonPrimitive.isString -> {
runCatching {
gson.fromJson(payuResponse.asString, JsonObject::class.java)
}.getOrNull()
}
else -> null
}
val qrString = root?.getAsJsonObject("result")?.get("qrString")?.asString
return qrString to raw
}
private fun buildDeviceInfo(): String {
val release = android.os.Build.VERSION.RELEASE
val model = android.os.Build.MODEL
return "Android $release; $model; PMS"
}
}

View File

@@ -1,437 +0,0 @@
package com.android.trisolarispms.ui.payu
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import kotlinx.coroutines.launch
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun PayuSettingsScreen(
propertyId: String,
onBack: () -> Unit,
viewModel: PayuSettingsViewModel = viewModel(),
linkViewModel: PayuPaymentLinkSettingsViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
val linkState by linkViewModel.state.collectAsState()
val clipboard = LocalClipboardManager.current
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 })
val scope = rememberCoroutineScope()
LaunchedEffect(propertyId) {
viewModel.load(propertyId)
linkViewModel.load(propertyId)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Payu Settings") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors()
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
TabRow(selectedTabIndex = pagerState.currentPage) {
Tab(
selected = pagerState.currentPage == 0,
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
text = { Text("QR") }
)
Tab(
selected = pagerState.currentPage == 1,
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
text = { Text("Payment Links") }
)
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
when (page) {
0 -> PayuQrSettingsTab(
propertyId = propertyId,
state = state,
onMerchantKeyChange = viewModel::onMerchantKeyChange,
onIsTestChange = viewModel::onIsTestChange,
onUseSalt256Change = viewModel::onUseSalt256Change,
onSalt32Change = viewModel::onSalt32Change,
onSalt256Change = viewModel::onSalt256Change,
onSave = { viewModel.save(propertyId) },
clipboard = clipboard
)
1 -> PayuPaymentLinksTab(
propertyId = propertyId,
state = linkState,
onMerchantIdChange = linkViewModel::onMerchantIdChange,
onClientIdChange = linkViewModel::onClientIdChange,
onClientSecretChange = linkViewModel::onClientSecretChange,
onIsTestChange = linkViewModel::onIsTestChange,
onSave = { linkViewModel.save(propertyId) }
)
}
}
}
}
}
@Composable
private fun PayuQrSettingsTab(
propertyId: String,
state: PayuSettingsState,
onMerchantKeyChange: (String) -> Unit,
onIsTestChange: (Boolean) -> Unit,
onUseSalt256Change: (Boolean) -> Unit,
onSalt32Change: (String) -> Unit,
onSalt256Change: (String) -> Unit,
onSave: () -> Unit,
clipboard: ClipboardManager
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
Text(
text = "Payu Settings",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "More payment settings will be added later. For now, configure PayU.",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Success/failure URLs are derived by the server using this property ID.",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(12.dp))
if (state.isLoading) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(12.dp))
}
Text(
text = if (state.configured) "Status: Configured" else "Status: Not configured",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
state.message?.let {
Text(text = it, color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.height(8.dp))
}
state.error?.let {
Text(text = it, color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(8.dp))
}
OutlinedTextField(
value = state.merchantKey,
onValueChange = onMerchantKeyChange,
label = { Text("Merchant key") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Use PayU test URL")
Switch(
checked = state.isTest,
onCheckedChange = onIsTestChange
)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = if (state.isTest) {
"Base URL: https://test.payu.in/_payment"
} else {
"Base URL: https://secure.payu.in/_payment"
},
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Use salt256")
Switch(
checked = state.useSalt256,
onCheckedChange = onUseSalt256Change
)
}
Spacer(modifier = Modifier.height(8.dp))
if (state.useSalt256) {
OutlinedTextField(
value = state.salt256,
onValueChange = onSalt256Change,
label = { Text("Salt256") },
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.salt32,
onValueChange = onSalt32Change,
label = { Text("Salt32") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
modifier = Modifier.fillMaxWidth()
)
if (state.hasSalt32) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Salt32 saved",
style = MaterialTheme.typography.bodySmall
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onSave,
enabled = !state.isSaving,
modifier = Modifier.fillMaxWidth()
) {
if (state.isSaving) {
CircularProgressIndicator(
modifier = Modifier
.height(16.dp)
.padding(end = 8.dp),
strokeWidth = 2.dp
)
Text("Saving...")
} else {
Text("Save")
}
}
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = "https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/success",
onValueChange = {},
label = { Text("Success URL") },
readOnly = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = "https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/failure",
onValueChange = {},
label = { Text("Failure URL") },
readOnly = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = {
clipboard.setText(
AnnotatedString(
"https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/success"
)
)
}
) {
Text("Copy success URL")
}
Button(
onClick = {
clipboard.setText(
AnnotatedString(
"https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/failure"
)
)
}
) {
Text("Copy failure URL")
}
}
Spacer(modifier = Modifier.height(12.dp))
}
}
@Composable
private fun PayuPaymentLinksTab(
propertyId: String,
state: PayuPaymentLinkSettingsState,
onMerchantIdChange: (String) -> Unit,
onClientIdChange: (String) -> Unit,
onClientSecretChange: (String) -> Unit,
onIsTestChange: (Boolean) -> Unit,
onSave: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
if (state.isLoading) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(12.dp))
}
Text(
text = if (state.configured) "Status: Configured" else "Status: Not configured",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
if (state.hasClientId) {
Text(text = "Client ID saved", style = MaterialTheme.typography.bodySmall)
}
if (state.hasClientSecret) {
Text(text = "Client Secret saved", style = MaterialTheme.typography.bodySmall)
}
if (state.hasAccessToken) {
Text(text = "Access token available", style = MaterialTheme.typography.bodySmall)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Payment Link Settings",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(12.dp))
state.message?.let {
Text(text = it, color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.height(8.dp))
}
state.error?.let {
Text(text = it, color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(8.dp))
}
OutlinedTextField(
value = state.merchantId,
onValueChange = onMerchantIdChange,
label = { Text("Merchant ID") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = state.clientId,
onValueChange = onClientIdChange,
label = { Text("Client ID") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = state.clientSecret,
onValueChange = onClientSecretChange,
label = { Text("Client Secret") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Use PayU test environment")
Switch(
checked = state.isTest,
onCheckedChange = onIsTestChange
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onSave,
enabled = !state.isSaving,
modifier = Modifier.fillMaxWidth()
) {
if (state.isSaving) {
CircularProgressIndicator(
modifier = Modifier
.height(16.dp)
.padding(end = 8.dp),
strokeWidth = 2.dp
)
Text("Saving...")
} else {
Text("Save")
}
}
}
}

View File

@@ -0,0 +1,244 @@
package com.android.trisolarispms.ui.razorpay
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil.compose.SubcomposeAsyncImage
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.activity.compose.BackHandler
import com.android.trisolarispms.BuildConfig
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun RazorpayQrScreen(
propertyId: String,
bookingId: String,
pendingAmount: Long?,
guestPhone: String?,
onBack: () -> Unit,
viewModel: RazorpayQrViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
DisposableEffect(Unit) {
onDispose { viewModel.reset() }
}
LaunchedEffect(pendingAmount) {
viewModel.setInitialAmount(pendingAmount)
}
LaunchedEffect(Unit) {
if (BuildConfig.DEBUG && state.amountInput.isBlank()) {
viewModel.onAmountChange("10")
}
}
LaunchedEffect(propertyId, bookingId) {
viewModel.loadQrList(propertyId, bookingId)
}
val isViewingQr = state.isClosed || !state.imageUrl.isNullOrBlank()
BackHandler(enabled = isViewingQr) {
viewModel.exitQrView()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Razorpay QR") },
navigationIcon = {
IconButton(onClick = {
if (isViewingQr) {
viewModel.exitQrView()
} else {
onBack()
}
}) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors()
)
}
) { padding ->
if (state.isClosed) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text(
text = "QR closed",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error
)
}
} else if (!state.imageUrl.isNullOrBlank()) {
LaunchedEffect(state.qrId) {
viewModel.startStreamForCurrentQr(propertyId, bookingId)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
SubcomposeAsyncImage(
model = state.imageUrl,
contentDescription = "Razorpay QR",
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize(),
loading = {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(32.dp),
strokeWidth = 3.dp
)
}
},
error = {
Text(
text = "Failed to load QR image",
color = MaterialTheme.colorScheme.error
)
}
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
OutlinedTextField(
value = state.amountInput,
onValueChange = viewModel::onAmountChange,
label = { Text("Amount") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = { viewModel.generate(propertyId, bookingId) },
enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (state.isLoading) {
Box(
modifier = Modifier
.size(18.dp)
.padding(end = 8.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(strokeWidth = 2.dp)
}
Spacer(modifier = Modifier.width(8.dp))
Text("Generating…")
} else {
Text("Generate QR")
}
}
state.error?.let {
Spacer(modifier = Modifier.height(12.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
if (state.qrList.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
Text(text = "QR List", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
state.qrList.forEachIndexed { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { viewModel.openQrFromList(item) },
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "QR ${index + 1}",
style = MaterialTheme.typography.labelMedium,
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()) {
IconButton(
onClick = {
viewModel.closeQr(propertyId, bookingId, qrId)
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Close QR",
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
package com.android.trisolarispms.ui.razorpay
data class RazorpayQrState(
val deviceInfo: String = "",
val amountInput: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val qrId: String? = null,
val amount: Long? = null,
val currency: String? = null,
val imageUrl: String? = null,
val isClosed: Boolean = false,
val paymentLink: String? = null,
val qrList: List<com.android.trisolarispms.data.api.model.RazorpayQrListItemDto> = emptyList()
)

View File

@@ -0,0 +1,327 @@
package com.android.trisolarispms.ui.razorpay
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.RazorpayQrEventDto
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
import com.google.gson.Gson
import okhttp3.Request
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class RazorpayQrViewModel : ViewModel() {
private val gson = Gson()
private val _state = MutableStateFlow(
RazorpayQrState(deviceInfo = buildDeviceInfo())
)
val state: StateFlow<RazorpayQrState> = _state
private var qrEventSource: EventSource? = null
private var lastQrId: String? = null
private var qrPollJob: Job? = null
fun reset() {
stopQrEventStream()
stopQrEventPolling()
_state.value = RazorpayQrState(deviceInfo = buildDeviceInfo())
}
fun exitQrView() {
stopQrEventPolling()
_state.update {
it.copy(
qrId = null,
imageUrl = null,
isClosed = false,
error = null
)
}
}
fun onAmountChange(value: String) {
val digits = value.filter { it.isDigit() }
_state.update { it.copy(amountInput = digits, error = null) }
}
fun setInitialAmount(amount: Long?) {
if (amount == null || amount <= 0) return
_state.update { current ->
if (current.amountInput.isBlank()) {
current.copy(amountInput = amount.toString())
} else {
current
}
}
}
fun generate(propertyId: String, bookingId: String) {
val current = state.value
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
if (amount == null) {
_state.update { it.copy(error = "Amount is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.generateRazorpayQr(
propertyId = propertyId,
bookingId = bookingId,
body = RazorpayQrRequest(
amount = amount,
deviceInfo = current.deviceInfo
)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
qrId = body.qrId,
amount = body.amount,
currency = body.currency,
imageUrl = body.imageUrl,
isClosed = false,
error = null
)
}
loadQrList(propertyId, bookingId)
startQrEventStream(propertyId, bookingId, body.qrId)
} else {
_state.update {
it.copy(
isLoading = false,
error = "QR request failed: ${response.code()}"
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "QR request failed"
)
}
}
}
}
private fun startQrEventStream(propertyId: String, bookingId: String, qrId: String?) {
if (qrId.isNullOrBlank()) return
if (lastQrId == qrId && qrEventSource != null) return
stopQrEventStream()
lastQrId = qrId
val client = ApiClient.createOkHttpClient(readTimeoutSeconds = 0)
val url = "${ApiConstants.BASE_URL}properties/$propertyId/bookings/$bookingId/payments/razorpay/qr/$qrId/events/stream"
val request = Request.Builder().url(url).get().build()
qrEventSource = EventSources.createFactory(client).newEventSource(
request,
object : EventSourceListener() {
override fun onEvent(
eventSource: EventSource,
id: String?,
type: String?,
data: String
) {
if (data.isBlank() || type == "ping") return
val event = runCatching {
gson.fromJson(data, RazorpayQrEventDto::class.java)
}.getOrNull() ?: return
if (isClosedStatus(event.status)) {
_state.update { it.copy(isClosed = true, imageUrl = null) }
stopQrEventStream()
stopQrEventPolling()
}
}
override fun onFailure(
eventSource: EventSource,
t: Throwable?,
response: okhttp3.Response?
) {
stopQrEventStream()
}
override fun onClosed(eventSource: EventSource) {
stopQrEventStream()
}
}
)
startQrEventPolling(propertyId, bookingId, qrId)
}
private fun stopQrEventStream() {
qrEventSource?.cancel()
qrEventSource = null
lastQrId = null
}
private fun startQrEventPolling(propertyId: String, bookingId: String, qrId: String) {
if (qrPollJob?.isActive == true) return
qrPollJob = viewModelScope.launch {
while (true) {
val currentQrId = state.value.qrId
if (currentQrId.isNullOrBlank() || currentQrId != qrId || state.value.isClosed) {
break
}
try {
val response = ApiClient.create().listRazorpayQrEvents(
propertyId = propertyId,
bookingId = bookingId,
qrId = qrId
)
val body = response.body()
if (response.isSuccessful && body != null) {
if (body.any { isClosedStatus(it.status) }) {
_state.update { it.copy(isClosed = true, imageUrl = null) }
stopQrEventStream()
break
}
}
} catch (_: Exception) {
// ignore polling errors
}
delay(5000)
}
}
}
private fun stopQrEventPolling() {
qrPollJob?.cancel()
qrPollJob = null
}
private fun isClosedStatus(status: String?): Boolean {
return when (status?.lowercase()) {
"credited", "closed", "expired" -> true
else -> false
}
}
fun loadQrList(propertyId: String, bookingId: String) {
viewModelScope.launch {
try {
val response = ApiClient.create().listRazorpayQrs(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) }
}
} catch (_: Exception) {
// ignore list load errors
}
}
}
fun closeQr(propertyId: String, bookingId: String, qrId: String) {
viewModelScope.launch {
try {
val response = ApiClient.create().closeRazorpayQr(
propertyId = propertyId,
bookingId = bookingId,
qrId = qrId
)
if (response.isSuccessful) {
_state.update { current ->
current.copy(qrList = current.qrList.filterNot { it.qrId == qrId })
}
loadQrList(propertyId, bookingId)
}
} catch (_: Exception) {
// ignore close errors
}
}
}
fun openQrFromList(item: RazorpayQrListItemDto) {
val status = item.status?.lowercase()
_state.update {
it.copy(
qrId = item.qrId,
amount = item.amount,
currency = item.currency,
imageUrl = item.imageUrl,
isClosed = status == "closed" || status == "expired" || status == "credited",
error = null
)
}
}
fun startStreamForCurrentQr(propertyId: String, bookingId: String) {
startQrEventStream(propertyId, bookingId, state.value.qrId)
}
fun generateLink(propertyId: String, bookingId: String, onReady: (String) -> Unit) {
val current = state.value
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
if (amount == null) {
_state.update { it.copy(error = "Amount is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.generateRazorpayPaymentLink(
propertyId = propertyId,
bookingId = bookingId,
body = RazorpayPaymentLinkRequest(amount = amount)
)
val body = response.body()
if (response.isSuccessful && body?.paymentLink != null) {
_state.update {
it.copy(
isLoading = false,
paymentLink = body.paymentLink,
amount = body.amount,
currency = body.currency,
error = null
)
}
onReady(body.paymentLink)
} else {
_state.update {
it.copy(
isLoading = false,
error = "Link request failed: ${response.code()}"
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Link request failed"
)
}
}
}
}
private fun buildDeviceInfo(): String {
val release = android.os.Build.VERSION.RELEASE
val model = android.os.Build.MODEL
return "Android $release; $model; PMS"
}
override fun onCleared() {
super.onCleared()
stopQrEventStream()
}
}

View File

@@ -0,0 +1,274 @@
package com.android.trisolarispms.ui.razorpay
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun RazorpaySettingsScreen(
propertyId: String,
onBack: () -> Unit,
viewModel: RazorpaySettingsViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
val clipboard = LocalClipboardManager.current
LaunchedEffect(propertyId) {
viewModel.load(propertyId)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Razorpay Settings") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors()
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
RazorpaySettingsTab(
propertyId = propertyId,
state = state,
onKeyIdChange = viewModel::onMerchantKeyChange,
onKeySecretChange = viewModel::onSalt32Change,
onWebhookSecretChange = viewModel::onWebhookSecretChange,
onIsTestChange = viewModel::onIsTestChange,
onUseSalt256Change = viewModel::onUseSalt256Change,
onSalt256Change = viewModel::onSalt256Change,
onSave = { viewModel.save(propertyId) },
clipboard = clipboard
)
}
}
}
@Composable
private fun RazorpaySettingsTab(
propertyId: String,
state: RazorpaySettingsState,
onKeyIdChange: (String) -> Unit,
onKeySecretChange: (String) -> Unit,
onWebhookSecretChange: (String) -> Unit,
onIsTestChange: (Boolean) -> Unit,
onUseSalt256Change: (Boolean) -> Unit,
onSalt256Change: (String) -> Unit,
onSave: () -> Unit,
clipboard: ClipboardManager
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
Text(
text = "Razorpay Settings",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "More payment settings will be added later. For now, configure Razorpay.",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Webhook URL is derived by the server using this property ID.",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(12.dp))
if (state.isLoading) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(12.dp))
}
Text(
text = if (state.configured) "Status: Configured" else "Status: Not configured",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
if (state.hasKeyId) {
Text(text = "Key ID saved", style = MaterialTheme.typography.bodySmall)
}
if (state.hasKeySecret) {
Text(text = "Key Secret saved", style = MaterialTheme.typography.bodySmall)
}
if (state.hasWebhookSecret) {
Text(text = "Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
}
Spacer(modifier = Modifier.height(12.dp))
state.message?.let {
Text(text = it, color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.height(8.dp))
}
state.error?.let {
Text(text = it, color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(8.dp))
}
OutlinedTextField(
value = state.keyId,
onValueChange = onKeyIdChange,
label = { Text("Key ID") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = state.webhookSecret,
onValueChange = onWebhookSecretChange,
label = { Text("Webhook Secret") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Use test environment")
Switch(
checked = state.isTest,
onCheckedChange = onIsTestChange
)
}
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))
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(
onClick = onSave,
enabled = !state.isSaving,
modifier = Modifier.fillMaxWidth()
) {
if (state.isSaving) {
CircularProgressIndicator(
modifier = Modifier
.height(16.dp)
.padding(end = 8.dp),
strokeWidth = 2.dp
)
Text("Saving...")
} else {
Text("Save")
}
}
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = "https://api.hoteltrisolaris.in/properties/$propertyId/razorpay/webhook",
onValueChange = {},
label = { Text("Webhook URL") },
readOnly = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
clipboard.setText(
AnnotatedString(
"https://api.hoteltrisolaris.in/properties/$propertyId/razorpay/webhook"
)
)
}
) {
Text("Copy webhook URL")
}
Spacer(modifier = Modifier.height(12.dp))
}
}

View File

@@ -1,12 +1,15 @@
package com.android.trisolarispms.ui.payu
package com.android.trisolarispms.ui.razorpay
data class PayuSettingsState(
val merchantKey: String = "",
val salt32: String = "",
data class RazorpaySettingsState(
val keyId: String = "",
val keySecret: String = "",
val webhookSecret: String = "",
val salt256: String = "",
val isTest: Boolean = false,
val useSalt256: Boolean = false,
val hasSalt32: Boolean = false,
val hasKeyId: Boolean = false,
val hasKeySecret: Boolean = false,
val hasWebhookSecret: Boolean = false,
val hasSalt256: Boolean = false,
val configured: Boolean = false,
val isLoading: Boolean = false,

View File

@@ -1,17 +1,17 @@
package com.android.trisolarispms.ui.payu
package com.android.trisolarispms.ui.razorpay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.model.PayuSettingsRequest
import com.android.trisolarispms.data.api.model.RazorpaySettingsRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class PayuSettingsViewModel : ViewModel() {
private val _state = MutableStateFlow(PayuSettingsState())
val state: StateFlow<PayuSettingsState> = _state
class RazorpaySettingsViewModel : ViewModel() {
private val _state = MutableStateFlow(RazorpaySettingsState())
val state: StateFlow<RazorpaySettingsState> = _state
fun load(propertyId: String) {
viewModelScope.launch {
@@ -20,11 +20,15 @@ class PayuSettingsViewModel : ViewModel() {
}
fun onMerchantKeyChange(value: String) {
_state.update { it.copy(merchantKey = value, error = null) }
_state.update { it.copy(keyId = value, error = null) }
}
fun onSalt32Change(value: String) {
_state.update { it.copy(salt32 = value, error = null) }
_state.update { it.copy(keySecret = value, error = null) }
}
fun onWebhookSecretChange(value: String) {
_state.update { it.copy(webhookSecret = value, error = null) }
}
fun onSalt256Change(value: String) {
@@ -41,33 +45,43 @@ class PayuSettingsViewModel : ViewModel() {
fun save(propertyId: String) {
val current = state.value
val merchantKey = current.merchantKey.trim()
val salt32 = current.salt32.trim()
val keyId = current.keyId.trim()
val keySecret = current.keySecret.trim()
val webhookSecret = current.webhookSecret.trim()
val salt256 = current.salt256.trim()
if (merchantKey.isBlank()) {
_state.update { it.copy(error = "Merchant key is required") }
if (!current.configured) {
if (keyId.isBlank()) {
_state.update { it.copy(error = "Key ID is required") }
return
}
if (current.useSalt256 && salt256.isBlank() && !current.hasSalt256) {
_state.update { it.copy(error = "Salt256 is required") }
_state.update { it.copy(error = "Secondary secret is required") }
return
}
if (!current.useSalt256 && salt32.isBlank() && !current.hasSalt32) {
_state.update { it.copy(error = "Salt32 is required") }
if (!current.useSalt256 && keySecret.isBlank() && !current.hasKeySecret) {
_state.update { it.copy(error = "Key secret is required") }
return
}
} else {
val hasKeyIdInput = keyId.isNotBlank()
val hasKeySecretInput = keySecret.isNotBlank()
if (hasKeyIdInput xor hasKeySecretInput) {
_state.update { it.copy(error = "Key ID and Key Secret must be provided together") }
return
}
}
viewModelScope.launch {
_state.update { it.copy(isSaving = true, error = null, message = null) }
try {
val api = ApiClient.create()
val response = api.updatePayuSettings(
val response = api.updateRazorpaySettings(
propertyId = propertyId,
body = PayuSettingsRequest(
merchantKey = merchantKey,
salt32 = salt32.ifBlank { null },
salt256 = salt256.ifBlank { null },
body = RazorpaySettingsRequest(
keyId = keyId,
keySecret = keySecret.ifBlank { null },
webhookSecret = webhookSecret.ifBlank { null },
isTest = current.isTest,
useSalt256 = current.useSalt256
)
@@ -103,18 +117,21 @@ class PayuSettingsViewModel : ViewModel() {
}
try {
val api = ApiClient.create()
val response = api.getPayuSettings(propertyId)
val response = api.getRazorpaySettings(propertyId)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
merchantKey = body.merchantKey.orEmpty(),
keyId = "",
isTest = body.isTest == true,
useSalt256 = body.useSalt256 == true,
hasSalt32 = body.hasSalt32 == true,
hasKeyId = body.hasKeyId == true,
hasKeySecret = body.hasKeySecret == true,
hasWebhookSecret = body.hasWebhookSecret == true,
hasSalt256 = body.hasSalt256 == true,
configured = body.configured == true,
salt32 = "",
keySecret = "",
webhookSecret = "",
salt256 = "",
isLoading = false,
isSaving = false,

View File

@@ -52,8 +52,8 @@ fun ActiveRoomStaysScreen(
onBack: () -> Unit,
onViewRooms: () -> Unit,
onCreateBooking: () -> Unit,
showPayuSettings: Boolean,
onPayuSettings: () -> Unit,
showRazorpaySettings: Boolean,
onRazorpaySettings: () -> Unit,
onManageRoomStay: (BookingListItem) -> Unit,
onViewBookingStays: (BookingListItem) -> Unit,
onOpenBookingDetails: (BookingListItem) -> Unit,
@@ -79,9 +79,9 @@ fun ActiveRoomStaysScreen(
IconButton(onClick = onViewRooms) {
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
}
if (showPayuSettings) {
IconButton(onClick = onPayuSettings) {
Icon(Icons.Default.Payment, contentDescription = "Payu Settings")
if (showRazorpaySettings) {
IconButton(onClick = onRazorpaySettings) {
Icon(Icons.Default.Payment, contentDescription = "Razorpay Settings")
}
}
},

View File

@@ -76,7 +76,7 @@ fun BookingDetailsTabsScreen(
onBack: () -> Unit,
onEditCheckout: (String?, String?) -> Unit,
onEditSignature: (String) -> Unit,
onOpenPayuQr: (Long?, String?) -> Unit,
onOpenRazorpayQr: (Long?, String?) -> Unit,
onOpenPayments: () -> Unit,
canManageDocuments: Boolean,
staysViewModel: BookingRoomStaysViewModel = viewModel(),
@@ -153,7 +153,7 @@ fun BookingDetailsTabsScreen(
error = detailsState.error,
onEditCheckout = onEditCheckout,
onEditSignature = onEditSignature,
onOpenPayuQr = onOpenPayuQr,
onOpenRazorpayQr = onOpenRazorpayQr,
onOpenPayments = onOpenPayments
)
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
@@ -190,7 +190,7 @@ private fun GuestInfoTabContent(
error: String?,
onEditCheckout: (String?, String?) -> Unit,
onEditSignature: (String) -> Unit,
onOpenPayuQr: (Long?, String?) -> Unit,
onOpenRazorpayQr: (Long?, String?) -> Unit,
onOpenPayments: () -> Unit
) {
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
@@ -280,10 +280,10 @@ private fun GuestInfoTabContent(
title = "Calculations",
headerContent = {
if (hasGuestName && hasGuestPhone && hasPending) {
IconButton(onClick = { onOpenPayuQr(details.pending, details.guestPhone) }) {
IconButton(onClick = { onOpenRazorpayQr(details.pending, details.guestPhone) }) {
Icon(
imageVector = Icons.Default.QrCode,
contentDescription = "PayU QR",
contentDescription = "Razorpay QR",
tint = MaterialTheme.colorScheme.primary
)
}