ai added more room db stuff
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
package com.android.trisolarispms.data.local.bookinglist
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface BookingListCacheDao {
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM booking_list_cache
|
||||||
|
WHERE propertyId = :propertyId AND status = :status
|
||||||
|
ORDER BY sortOrder ASC, bookingId ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun observeByStatus(propertyId: String, status: String): Flow<List<BookingListCacheEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsertAll(items: List<BookingListCacheEntity>)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM booking_list_cache
|
||||||
|
WHERE propertyId = :propertyId AND status = :status
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteByPropertyAndStatus(propertyId: String, status: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun replaceForStatus(
|
||||||
|
propertyId: String,
|
||||||
|
status: String,
|
||||||
|
items: List<BookingListCacheEntity>
|
||||||
|
) {
|
||||||
|
deleteByPropertyAndStatus(propertyId = propertyId, status = status)
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
upsertAll(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.android.trisolarispms.data.local.bookinglist
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "booking_list_cache",
|
||||||
|
primaryKeys = ["propertyId", "bookingId"],
|
||||||
|
indices = [
|
||||||
|
Index(value = ["propertyId"]),
|
||||||
|
Index(value = ["propertyId", "status"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class BookingListCacheEntity(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String,
|
||||||
|
val status: String? = null,
|
||||||
|
val guestId: String? = null,
|
||||||
|
val guestName: String? = null,
|
||||||
|
val guestPhone: String? = null,
|
||||||
|
val vehicleNumbers: List<String> = emptyList(),
|
||||||
|
val roomNumbers: List<Int> = emptyList(),
|
||||||
|
val source: String? = null,
|
||||||
|
val fromCity: String? = null,
|
||||||
|
val toCity: String? = null,
|
||||||
|
val memberRelation: String? = null,
|
||||||
|
val transportMode: String? = null,
|
||||||
|
val checkInAt: String? = null,
|
||||||
|
val checkOutAt: String? = null,
|
||||||
|
val expectedCheckInAt: String? = null,
|
||||||
|
val expectedCheckOutAt: String? = null,
|
||||||
|
val adultCount: Int? = null,
|
||||||
|
val childCount: Int? = null,
|
||||||
|
val maleCount: Int? = null,
|
||||||
|
val femaleCount: Int? = null,
|
||||||
|
val totalGuestCount: Int? = null,
|
||||||
|
val expectedGuestCount: Int? = null,
|
||||||
|
val notes: String? = null,
|
||||||
|
val pending: Long? = null,
|
||||||
|
val billingMode: String? = null,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null,
|
||||||
|
val sortOrder: Int = 0,
|
||||||
|
val updatedAtEpochMs: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun BookingListItem.toCacheEntity(
|
||||||
|
propertyId: String,
|
||||||
|
sortOrder: Int
|
||||||
|
): BookingListCacheEntity? {
|
||||||
|
val safeBookingId = id?.trim()?.ifBlank { null } ?: return null
|
||||||
|
return BookingListCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = safeBookingId,
|
||||||
|
status = status,
|
||||||
|
guestId = guestId,
|
||||||
|
guestName = guestName,
|
||||||
|
guestPhone = guestPhone,
|
||||||
|
vehicleNumbers = vehicleNumbers,
|
||||||
|
roomNumbers = roomNumbers,
|
||||||
|
source = source,
|
||||||
|
fromCity = fromCity,
|
||||||
|
toCity = toCity,
|
||||||
|
memberRelation = memberRelation,
|
||||||
|
transportMode = transportMode,
|
||||||
|
checkInAt = checkInAt,
|
||||||
|
checkOutAt = checkOutAt,
|
||||||
|
expectedCheckInAt = expectedCheckInAt,
|
||||||
|
expectedCheckOutAt = expectedCheckOutAt,
|
||||||
|
adultCount = adultCount,
|
||||||
|
childCount = childCount,
|
||||||
|
maleCount = maleCount,
|
||||||
|
femaleCount = femaleCount,
|
||||||
|
totalGuestCount = totalGuestCount,
|
||||||
|
expectedGuestCount = expectedGuestCount,
|
||||||
|
notes = notes,
|
||||||
|
pending = pending,
|
||||||
|
billingMode = billingMode,
|
||||||
|
billingCheckinTime = billingCheckinTime,
|
||||||
|
billingCheckoutTime = billingCheckoutTime,
|
||||||
|
sortOrder = sortOrder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BookingListCacheEntity.toApiModel(): BookingListItem = BookingListItem(
|
||||||
|
id = bookingId,
|
||||||
|
status = status,
|
||||||
|
guestId = guestId,
|
||||||
|
guestName = guestName,
|
||||||
|
guestPhone = guestPhone,
|
||||||
|
vehicleNumbers = vehicleNumbers,
|
||||||
|
roomNumbers = roomNumbers,
|
||||||
|
source = source,
|
||||||
|
fromCity = fromCity,
|
||||||
|
toCity = toCity,
|
||||||
|
memberRelation = memberRelation,
|
||||||
|
transportMode = transportMode,
|
||||||
|
checkInAt = checkInAt,
|
||||||
|
checkOutAt = checkOutAt,
|
||||||
|
expectedCheckInAt = expectedCheckInAt,
|
||||||
|
expectedCheckOutAt = expectedCheckOutAt,
|
||||||
|
adultCount = adultCount,
|
||||||
|
childCount = childCount,
|
||||||
|
maleCount = maleCount,
|
||||||
|
femaleCount = femaleCount,
|
||||||
|
totalGuestCount = totalGuestCount,
|
||||||
|
expectedGuestCount = expectedGuestCount,
|
||||||
|
notes = notes,
|
||||||
|
pending = pending,
|
||||||
|
billingMode = billingMode,
|
||||||
|
billingCheckinTime = billingCheckinTime,
|
||||||
|
billingCheckoutTime = billingCheckoutTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.android.trisolarispms.data.local.bookinglist
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiService
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class BookingListRepository(
|
||||||
|
private val dao: BookingListCacheDao,
|
||||||
|
private val createApi: () -> ApiService = { ApiClient.create() }
|
||||||
|
) {
|
||||||
|
fun observeByStatus(propertyId: String, status: String): Flow<List<BookingListItem>> =
|
||||||
|
dao.observeByStatus(propertyId = propertyId, status = status).map { rows ->
|
||||||
|
rows.map { it.toApiModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshByStatus(propertyId: String, status: String): Result<Unit> = runCatching {
|
||||||
|
val response = createApi().listBookings(propertyId = propertyId, status = status)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IllegalStateException("Load failed: ${response.code()}")
|
||||||
|
}
|
||||||
|
val rows = response.body().orEmpty().mapIndexedNotNull { index, booking ->
|
||||||
|
booking.toCacheEntity(propertyId = propertyId, sortOrder = index)
|
||||||
|
}
|
||||||
|
dao.replaceForStatus(propertyId = propertyId, status = status, items = rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,15 @@ import androidx.room.TypeConverters
|
|||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.android.trisolarispms.data.local.booking.BookingDetailsCacheDao
|
import com.android.trisolarispms.data.local.booking.BookingDetailsCacheDao
|
||||||
import com.android.trisolarispms.data.local.booking.BookingDetailsCacheEntity
|
import com.android.trisolarispms.data.local.booking.BookingDetailsCacheEntity
|
||||||
|
import com.android.trisolarispms.data.local.bookinglist.BookingListCacheDao
|
||||||
|
import com.android.trisolarispms.data.local.bookinglist.BookingListCacheEntity
|
||||||
|
import com.android.trisolarispms.data.local.guestdoc.GuestDocumentCacheDao
|
||||||
|
import com.android.trisolarispms.data.local.guestdoc.GuestDocumentCacheEntity
|
||||||
|
import com.android.trisolarispms.data.local.payment.PaymentCacheDao
|
||||||
|
import com.android.trisolarispms.data.local.payment.PaymentCacheEntity
|
||||||
|
import com.android.trisolarispms.data.local.razorpay.RazorpayCacheDao
|
||||||
|
import com.android.trisolarispms.data.local.razorpay.RazorpayQrEventCacheEntity
|
||||||
|
import com.android.trisolarispms.data.local.razorpay.RazorpayRequestCacheEntity
|
||||||
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayCacheDao
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayCacheDao
|
||||||
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayCacheEntity
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayCacheEntity
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
@@ -13,15 +22,24 @@ import androidx.room.migration.Migration
|
|||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
BookingDetailsCacheEntity::class,
|
BookingDetailsCacheEntity::class,
|
||||||
ActiveRoomStayCacheEntity::class
|
ActiveRoomStayCacheEntity::class,
|
||||||
|
BookingListCacheEntity::class,
|
||||||
|
PaymentCacheEntity::class,
|
||||||
|
RazorpayRequestCacheEntity::class,
|
||||||
|
RazorpayQrEventCacheEntity::class,
|
||||||
|
GuestDocumentCacheEntity::class
|
||||||
],
|
],
|
||||||
version = 2,
|
version = 5,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(RoomConverters::class)
|
@TypeConverters(RoomConverters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun bookingDetailsCacheDao(): BookingDetailsCacheDao
|
abstract fun bookingDetailsCacheDao(): BookingDetailsCacheDao
|
||||||
abstract fun activeRoomStayCacheDao(): ActiveRoomStayCacheDao
|
abstract fun activeRoomStayCacheDao(): ActiveRoomStayCacheDao
|
||||||
|
abstract fun bookingListCacheDao(): BookingListCacheDao
|
||||||
|
abstract fun paymentCacheDao(): PaymentCacheDao
|
||||||
|
abstract fun razorpayCacheDao(): RazorpayCacheDao
|
||||||
|
abstract fun guestDocumentCacheDao(): GuestDocumentCacheDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||||
@@ -63,5 +81,180 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `booking_list_cache` (
|
||||||
|
`propertyId` TEXT NOT NULL,
|
||||||
|
`bookingId` TEXT NOT NULL,
|
||||||
|
`status` TEXT,
|
||||||
|
`guestId` TEXT,
|
||||||
|
`guestName` TEXT,
|
||||||
|
`guestPhone` TEXT,
|
||||||
|
`vehicleNumbers` TEXT NOT NULL,
|
||||||
|
`roomNumbers` TEXT NOT NULL,
|
||||||
|
`source` TEXT,
|
||||||
|
`fromCity` TEXT,
|
||||||
|
`toCity` TEXT,
|
||||||
|
`memberRelation` TEXT,
|
||||||
|
`transportMode` TEXT,
|
||||||
|
`checkInAt` TEXT,
|
||||||
|
`checkOutAt` TEXT,
|
||||||
|
`expectedCheckInAt` TEXT,
|
||||||
|
`expectedCheckOutAt` TEXT,
|
||||||
|
`adultCount` INTEGER,
|
||||||
|
`childCount` INTEGER,
|
||||||
|
`maleCount` INTEGER,
|
||||||
|
`femaleCount` INTEGER,
|
||||||
|
`totalGuestCount` INTEGER,
|
||||||
|
`expectedGuestCount` INTEGER,
|
||||||
|
`notes` TEXT,
|
||||||
|
`pending` INTEGER,
|
||||||
|
`billingMode` TEXT,
|
||||||
|
`billingCheckinTime` TEXT,
|
||||||
|
`billingCheckoutTime` TEXT,
|
||||||
|
`sortOrder` INTEGER NOT NULL,
|
||||||
|
`updatedAtEpochMs` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`propertyId`, `bookingId`)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS `index_booking_list_cache_propertyId`
|
||||||
|
ON `booking_list_cache` (`propertyId`)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS `index_booking_list_cache_propertyId_status`
|
||||||
|
ON `booking_list_cache` (`propertyId`, `status`)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `payment_cache` (
|
||||||
|
`propertyId` TEXT NOT NULL,
|
||||||
|
`bookingId` TEXT NOT NULL,
|
||||||
|
`paymentId` TEXT NOT NULL,
|
||||||
|
`amount` INTEGER,
|
||||||
|
`currency` TEXT,
|
||||||
|
`method` TEXT,
|
||||||
|
`gatewayPaymentId` TEXT,
|
||||||
|
`gatewayTxnId` TEXT,
|
||||||
|
`bankRefNum` TEXT,
|
||||||
|
`mode` TEXT,
|
||||||
|
`pgType` TEXT,
|
||||||
|
`payerVpa` TEXT,
|
||||||
|
`payerName` TEXT,
|
||||||
|
`paymentSource` TEXT,
|
||||||
|
`reference` TEXT,
|
||||||
|
`notes` TEXT,
|
||||||
|
`receivedAt` TEXT,
|
||||||
|
`receivedByUserId` TEXT,
|
||||||
|
`sortOrder` INTEGER NOT NULL,
|
||||||
|
`updatedAtEpochMs` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`propertyId`, `bookingId`, `paymentId`)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS `index_payment_cache_propertyId_bookingId`
|
||||||
|
ON `payment_cache` (`propertyId`, `bookingId`)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `razorpay_request_cache` (
|
||||||
|
`propertyId` TEXT NOT NULL,
|
||||||
|
`bookingId` TEXT NOT NULL,
|
||||||
|
`requestKey` TEXT NOT NULL,
|
||||||
|
`type` TEXT,
|
||||||
|
`requestId` TEXT,
|
||||||
|
`amount` INTEGER,
|
||||||
|
`currency` TEXT,
|
||||||
|
`status` TEXT,
|
||||||
|
`createdAt` TEXT,
|
||||||
|
`qrId` TEXT,
|
||||||
|
`imageUrl` TEXT,
|
||||||
|
`expiryAt` TEXT,
|
||||||
|
`paymentLinkId` TEXT,
|
||||||
|
`paymentLink` TEXT,
|
||||||
|
`sortOrder` INTEGER NOT NULL,
|
||||||
|
`updatedAtEpochMs` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`propertyId`, `bookingId`, `requestKey`)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS `index_razorpay_request_cache_propertyId_bookingId`
|
||||||
|
ON `razorpay_request_cache` (`propertyId`, `bookingId`)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `razorpay_qr_event_cache` (
|
||||||
|
`propertyId` TEXT NOT NULL,
|
||||||
|
`bookingId` TEXT NOT NULL,
|
||||||
|
`qrId` TEXT NOT NULL,
|
||||||
|
`eventKey` TEXT NOT NULL,
|
||||||
|
`event` TEXT,
|
||||||
|
`status` TEXT,
|
||||||
|
`receivedAt` TEXT,
|
||||||
|
`sortOrder` INTEGER NOT NULL,
|
||||||
|
`updatedAtEpochMs` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`propertyId`, `bookingId`, `qrId`, `eventKey`)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS `index_razorpay_qr_event_cache_propertyId_bookingId_qrId`
|
||||||
|
ON `razorpay_qr_event_cache` (`propertyId`, `bookingId`, `qrId`)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `guest_document_cache` (
|
||||||
|
`propertyId` TEXT NOT NULL,
|
||||||
|
`guestId` TEXT NOT NULL,
|
||||||
|
`documentId` TEXT NOT NULL,
|
||||||
|
`bookingId` TEXT,
|
||||||
|
`uploadedByUserId` TEXT,
|
||||||
|
`uploadedAt` TEXT,
|
||||||
|
`originalFilename` TEXT,
|
||||||
|
`contentType` TEXT,
|
||||||
|
`sizeBytes` INTEGER,
|
||||||
|
`extractedDataJson` TEXT,
|
||||||
|
`extractedAt` TEXT,
|
||||||
|
`sortOrder` INTEGER NOT NULL,
|
||||||
|
`updatedAtEpochMs` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`propertyId`, `guestId`, `documentId`)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS `index_guest_document_cache_propertyId_guestId`
|
||||||
|
ON `guest_document_cache` (`propertyId`, `guestId`)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ object LocalDatabaseProvider {
|
|||||||
"trisolaris_pms_local.db"
|
"trisolaris_pms_local.db"
|
||||||
)
|
)
|
||||||
.addMigrations(AppDatabase.MIGRATION_1_2)
|
.addMigrations(AppDatabase.MIGRATION_1_2)
|
||||||
|
.addMigrations(AppDatabase.MIGRATION_2_3)
|
||||||
|
.addMigrations(AppDatabase.MIGRATION_3_4)
|
||||||
|
.addMigrations(AppDatabase.MIGRATION_4_5)
|
||||||
.build()
|
.build()
|
||||||
.also { built ->
|
.also { built ->
|
||||||
instance = built
|
instance = built
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.android.trisolarispms.data.local.guestdoc
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface GuestDocumentCacheDao {
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM guest_document_cache
|
||||||
|
WHERE propertyId = :propertyId AND guestId = :guestId
|
||||||
|
ORDER BY sortOrder ASC, documentId ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun observeByGuest(propertyId: String, guestId: String): Flow<List<GuestDocumentCacheEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsertAll(items: List<GuestDocumentCacheEntity>)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM guest_document_cache
|
||||||
|
WHERE propertyId = :propertyId AND guestId = :guestId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteByGuest(propertyId: String, guestId: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun replaceForGuest(
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
items: List<GuestDocumentCacheEntity>
|
||||||
|
) {
|
||||||
|
deleteByGuest(propertyId = propertyId, guestId = guestId)
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
upsertAll(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.android.trisolarispms.data.local.guestdoc
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.android.trisolarispms.data.api.model.GuestDocumentDto
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "guest_document_cache",
|
||||||
|
primaryKeys = ["propertyId", "guestId", "documentId"],
|
||||||
|
indices = [
|
||||||
|
Index(value = ["propertyId", "guestId"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class GuestDocumentCacheEntity(
|
||||||
|
val propertyId: String,
|
||||||
|
val guestId: String,
|
||||||
|
val documentId: String,
|
||||||
|
val bookingId: String? = null,
|
||||||
|
val uploadedByUserId: String? = null,
|
||||||
|
val uploadedAt: String? = null,
|
||||||
|
val originalFilename: String? = null,
|
||||||
|
val contentType: String? = null,
|
||||||
|
val sizeBytes: Long? = null,
|
||||||
|
val extractedDataJson: String? = null,
|
||||||
|
val extractedAt: String? = null,
|
||||||
|
val sortOrder: Int = 0,
|
||||||
|
val updatedAtEpochMs: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
private val extractedDataType = object : TypeToken<Map<String, String>>() {}.type
|
||||||
|
|
||||||
|
internal fun GuestDocumentDto.toCacheEntity(
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
sortOrder: Int
|
||||||
|
): GuestDocumentCacheEntity? {
|
||||||
|
val safeDocumentId = id?.trim()?.ifBlank { null } ?: return null
|
||||||
|
val extractedJson = runCatching { Gson().toJson(extractedData ?: emptyMap<String, String>()) }
|
||||||
|
.getOrNull()
|
||||||
|
return GuestDocumentCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
documentId = safeDocumentId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
uploadedByUserId = uploadedByUserId,
|
||||||
|
uploadedAt = uploadedAt,
|
||||||
|
originalFilename = originalFilename,
|
||||||
|
contentType = contentType,
|
||||||
|
sizeBytes = sizeBytes,
|
||||||
|
extractedDataJson = extractedJson,
|
||||||
|
extractedAt = extractedAt,
|
||||||
|
sortOrder = sortOrder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun GuestDocumentCacheEntity.toApiModel(): GuestDocumentDto {
|
||||||
|
val extractedMap = runCatching {
|
||||||
|
if (extractedDataJson.isNullOrBlank()) emptyMap<String, String>()
|
||||||
|
else Gson().fromJson<Map<String, String>>(extractedDataJson, extractedDataType)
|
||||||
|
}.getOrElse { emptyMap() }
|
||||||
|
return GuestDocumentDto(
|
||||||
|
id = documentId,
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
uploadedByUserId = uploadedByUserId,
|
||||||
|
uploadedAt = uploadedAt,
|
||||||
|
originalFilename = originalFilename,
|
||||||
|
contentType = contentType,
|
||||||
|
sizeBytes = sizeBytes,
|
||||||
|
extractedData = extractedMap,
|
||||||
|
extractedAt = extractedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.android.trisolarispms.data.local.guestdoc
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiService
|
||||||
|
import com.android.trisolarispms.data.api.model.GuestDocumentDto
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class GuestDocumentRepository(
|
||||||
|
private val dao: GuestDocumentCacheDao,
|
||||||
|
private val createApi: () -> ApiService = { ApiClient.create() }
|
||||||
|
) {
|
||||||
|
fun observeByGuest(propertyId: String, guestId: String): Flow<List<GuestDocumentDto>> =
|
||||||
|
dao.observeByGuest(propertyId = propertyId, guestId = guestId).map { rows ->
|
||||||
|
rows.map { it.toApiModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refresh(propertyId: String, guestId: String): Result<Unit> = runCatching {
|
||||||
|
val response = createApi().listGuestDocuments(propertyId = propertyId, guestId = guestId)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IllegalStateException("Load failed: ${response.code()}")
|
||||||
|
}
|
||||||
|
val rows = response.body().orEmpty().mapIndexedNotNull { index, doc ->
|
||||||
|
doc.toCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
sortOrder = index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dao.replaceForGuest(propertyId = propertyId, guestId = guestId, items = rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun storeSnapshot(
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
documents: List<GuestDocumentDto>
|
||||||
|
) {
|
||||||
|
val rows = documents.mapIndexedNotNull { index, doc ->
|
||||||
|
doc.toCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
sortOrder = index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dao.replaceForGuest(propertyId = propertyId, guestId = guestId, items = rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.android.trisolarispms.data.local.payment
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PaymentCacheDao {
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM payment_cache
|
||||||
|
WHERE propertyId = :propertyId AND bookingId = :bookingId
|
||||||
|
ORDER BY sortOrder ASC, paymentId ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun observeByBooking(propertyId: String, bookingId: String): Flow<List<PaymentCacheEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsertAll(items: List<PaymentCacheEntity>)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM payment_cache
|
||||||
|
WHERE propertyId = :propertyId AND bookingId = :bookingId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteByBooking(propertyId: String, bookingId: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun replaceForBooking(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
items: List<PaymentCacheEntity>
|
||||||
|
) {
|
||||||
|
deleteByBooking(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
upsertAll(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.android.trisolarispms.data.local.payment
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.android.trisolarispms.data.api.model.PaymentDto
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "payment_cache",
|
||||||
|
primaryKeys = ["propertyId", "bookingId", "paymentId"],
|
||||||
|
indices = [
|
||||||
|
Index(value = ["propertyId", "bookingId"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class PaymentCacheEntity(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String,
|
||||||
|
val paymentId: String,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val method: String? = null,
|
||||||
|
val gatewayPaymentId: String? = null,
|
||||||
|
val gatewayTxnId: String? = null,
|
||||||
|
val bankRefNum: String? = null,
|
||||||
|
val mode: String? = null,
|
||||||
|
val pgType: String? = null,
|
||||||
|
val payerVpa: String? = null,
|
||||||
|
val payerName: String? = null,
|
||||||
|
val paymentSource: String? = null,
|
||||||
|
val reference: String? = null,
|
||||||
|
val notes: String? = null,
|
||||||
|
val receivedAt: String? = null,
|
||||||
|
val receivedByUserId: String? = null,
|
||||||
|
val sortOrder: Int = 0,
|
||||||
|
val updatedAtEpochMs: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun PaymentDto.toCacheEntity(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
sortOrder: Int
|
||||||
|
): PaymentCacheEntity? {
|
||||||
|
val safePaymentId = id?.trim()?.ifBlank { null } ?: return null
|
||||||
|
return PaymentCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
paymentId = safePaymentId,
|
||||||
|
amount = amount,
|
||||||
|
currency = currency,
|
||||||
|
method = method,
|
||||||
|
gatewayPaymentId = gatewayPaymentId,
|
||||||
|
gatewayTxnId = gatewayTxnId,
|
||||||
|
bankRefNum = bankRefNum,
|
||||||
|
mode = mode,
|
||||||
|
pgType = pgType,
|
||||||
|
payerVpa = payerVpa,
|
||||||
|
payerName = payerName,
|
||||||
|
paymentSource = paymentSource,
|
||||||
|
reference = reference,
|
||||||
|
notes = notes,
|
||||||
|
receivedAt = receivedAt,
|
||||||
|
receivedByUserId = receivedByUserId,
|
||||||
|
sortOrder = sortOrder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun PaymentCacheEntity.toApiModel(): PaymentDto = PaymentDto(
|
||||||
|
id = paymentId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
amount = amount,
|
||||||
|
currency = currency,
|
||||||
|
method = method,
|
||||||
|
gatewayPaymentId = gatewayPaymentId,
|
||||||
|
gatewayTxnId = gatewayTxnId,
|
||||||
|
bankRefNum = bankRefNum,
|
||||||
|
mode = mode,
|
||||||
|
pgType = pgType,
|
||||||
|
payerVpa = payerVpa,
|
||||||
|
payerName = payerName,
|
||||||
|
paymentSource = paymentSource,
|
||||||
|
reference = reference,
|
||||||
|
notes = notes,
|
||||||
|
receivedAt = receivedAt,
|
||||||
|
receivedByUserId = receivedByUserId
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.android.trisolarispms.data.local.payment
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiService
|
||||||
|
import com.android.trisolarispms.data.api.model.PaymentDto
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class PaymentRepository(
|
||||||
|
private val dao: PaymentCacheDao,
|
||||||
|
private val createApi: () -> ApiService = { ApiClient.create() }
|
||||||
|
) {
|
||||||
|
fun observeByBooking(propertyId: String, bookingId: String): Flow<List<PaymentDto>> =
|
||||||
|
dao.observeByBooking(propertyId = propertyId, bookingId = bookingId).map { rows ->
|
||||||
|
rows.map { it.toApiModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refresh(propertyId: String, bookingId: String): Result<Unit> = runCatching {
|
||||||
|
val response = createApi().listPayments(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IllegalStateException("Load failed: ${response.code()}")
|
||||||
|
}
|
||||||
|
val rows = response.body().orEmpty().mapIndexedNotNull { index, payment ->
|
||||||
|
payment.toCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
sortOrder = index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dao.replaceForBooking(propertyId = propertyId, bookingId = bookingId, items = rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.android.trisolarispms.data.local.razorpay
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RazorpayCacheDao {
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM razorpay_request_cache
|
||||||
|
WHERE propertyId = :propertyId AND bookingId = :bookingId
|
||||||
|
ORDER BY sortOrder ASC, requestKey ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun observeRequests(propertyId: String, bookingId: String): Flow<List<RazorpayRequestCacheEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsertRequests(items: List<RazorpayRequestCacheEntity>)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM razorpay_request_cache
|
||||||
|
WHERE propertyId = :propertyId AND bookingId = :bookingId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteRequests(propertyId: String, bookingId: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun replaceRequests(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
items: List<RazorpayRequestCacheEntity>
|
||||||
|
) {
|
||||||
|
deleteRequests(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
upsertRequests(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM razorpay_qr_event_cache
|
||||||
|
WHERE propertyId = :propertyId AND bookingId = :bookingId AND qrId = :qrId
|
||||||
|
ORDER BY sortOrder ASC, eventKey ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun observeQrEvents(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
qrId: String
|
||||||
|
): Flow<List<RazorpayQrEventCacheEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsertQrEvents(items: List<RazorpayQrEventCacheEntity>)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM razorpay_qr_event_cache
|
||||||
|
WHERE propertyId = :propertyId AND bookingId = :bookingId AND qrId = :qrId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteQrEvents(propertyId: String, bookingId: String, qrId: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun replaceQrEvents(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
qrId: String,
|
||||||
|
items: List<RazorpayQrEventCacheEntity>
|
||||||
|
) {
|
||||||
|
deleteQrEvents(propertyId = propertyId, bookingId = bookingId, qrId = qrId)
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
upsertQrEvents(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.android.trisolarispms.data.local.razorpay
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiService
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class RazorpayCacheRepository(
|
||||||
|
private val dao: RazorpayCacheDao,
|
||||||
|
private val createApi: () -> ApiService = { ApiClient.create() }
|
||||||
|
) {
|
||||||
|
fun observeRequests(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String
|
||||||
|
): Flow<List<RazorpayRequestListItemDto>> =
|
||||||
|
dao.observeRequests(propertyId = propertyId, bookingId = bookingId).map { rows ->
|
||||||
|
rows.map { it.toApiModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeQrEvents(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
qrId: String
|
||||||
|
): Flow<List<RazorpayQrEventDto>> =
|
||||||
|
dao.observeQrEvents(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId
|
||||||
|
).map { rows ->
|
||||||
|
rows.map { it.toApiModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshRequests(propertyId: String, bookingId: String): Result<Unit> = runCatching {
|
||||||
|
val response = createApi().listRazorpayRequests(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IllegalStateException("Load failed: ${response.code()}")
|
||||||
|
}
|
||||||
|
val rows = response.body().orEmpty().mapIndexed { index, item ->
|
||||||
|
item.toCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
sortOrder = index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dao.replaceRequests(propertyId = propertyId, bookingId = bookingId, items = rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshQrEvents(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
qrId: String
|
||||||
|
): Result<List<RazorpayQrEventDto>> = runCatching {
|
||||||
|
val response = createApi().listRazorpayQrEvents(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId
|
||||||
|
)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IllegalStateException("Load failed: ${response.code()}")
|
||||||
|
}
|
||||||
|
val body = response.body().orEmpty()
|
||||||
|
val rows = body.mapIndexed { index, item ->
|
||||||
|
item.toCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId,
|
||||||
|
sortOrder = index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dao.replaceQrEvents(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId,
|
||||||
|
items = rows
|
||||||
|
)
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.android.trisolarispms.data.local.razorpay
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "razorpay_qr_event_cache",
|
||||||
|
primaryKeys = ["propertyId", "bookingId", "qrId", "eventKey"],
|
||||||
|
indices = [
|
||||||
|
Index(value = ["propertyId", "bookingId", "qrId"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class RazorpayQrEventCacheEntity(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String,
|
||||||
|
val qrId: String,
|
||||||
|
val eventKey: String,
|
||||||
|
val event: String? = null,
|
||||||
|
val status: String? = null,
|
||||||
|
val receivedAt: String? = null,
|
||||||
|
val sortOrder: Int = 0,
|
||||||
|
val updatedAtEpochMs: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun RazorpayQrEventDto.toCacheEntity(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
qrId: String,
|
||||||
|
sortOrder: Int
|
||||||
|
): RazorpayQrEventCacheEntity {
|
||||||
|
val key = "${receivedAt.orEmpty()}:${event.orEmpty()}:${status.orEmpty()}:$sortOrder"
|
||||||
|
return RazorpayQrEventCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId,
|
||||||
|
eventKey = key,
|
||||||
|
event = event,
|
||||||
|
status = status,
|
||||||
|
receivedAt = receivedAt,
|
||||||
|
sortOrder = sortOrder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun RazorpayQrEventCacheEntity.toApiModel(): RazorpayQrEventDto = RazorpayQrEventDto(
|
||||||
|
event = event,
|
||||||
|
qrId = qrId,
|
||||||
|
status = status,
|
||||||
|
receivedAt = receivedAt
|
||||||
|
)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.android.trisolarispms.data.local.razorpay
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "razorpay_request_cache",
|
||||||
|
primaryKeys = ["propertyId", "bookingId", "requestKey"],
|
||||||
|
indices = [
|
||||||
|
Index(value = ["propertyId", "bookingId"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class RazorpayRequestCacheEntity(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String,
|
||||||
|
val requestKey: String,
|
||||||
|
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 paymentLinkId: String? = null,
|
||||||
|
val paymentLink: String? = null,
|
||||||
|
val sortOrder: Int = 0,
|
||||||
|
val updatedAtEpochMs: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun RazorpayRequestListItemDto.toCacheEntity(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
sortOrder: Int
|
||||||
|
): RazorpayRequestCacheEntity {
|
||||||
|
val key = requestId?.trim()?.ifBlank { null }
|
||||||
|
?: qrId?.trim()?.ifBlank { null }?.let { "qr:$it" }
|
||||||
|
?: paymentLinkId?.trim()?.ifBlank { null }?.let { "plink:$it" }
|
||||||
|
?: "idx:$sortOrder:${createdAt.orEmpty()}:${type.orEmpty()}"
|
||||||
|
return RazorpayRequestCacheEntity(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
requestKey = key,
|
||||||
|
type = type,
|
||||||
|
requestId = requestId,
|
||||||
|
amount = amount,
|
||||||
|
currency = currency,
|
||||||
|
status = status,
|
||||||
|
createdAt = createdAt,
|
||||||
|
qrId = qrId,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
expiryAt = expiryAt,
|
||||||
|
paymentLinkId = paymentLinkId,
|
||||||
|
paymentLink = paymentLink,
|
||||||
|
sortOrder = sortOrder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun RazorpayRequestCacheEntity.toApiModel(): RazorpayRequestListItemDto = RazorpayRequestListItemDto(
|
||||||
|
type = type,
|
||||||
|
requestId = requestId,
|
||||||
|
amount = amount,
|
||||||
|
currency = currency,
|
||||||
|
status = status,
|
||||||
|
createdAt = createdAt,
|
||||||
|
qrId = qrId,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
expiryAt = expiryAt,
|
||||||
|
paymentLinkId = paymentLinkId,
|
||||||
|
paymentLink = paymentLink
|
||||||
|
)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.android.trisolarispms.ui.booking
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.core.booking.isFutureBookingCheckIn
|
import com.android.trisolarispms.core.booking.isFutureBookingCheckIn
|
||||||
import com.android.trisolarispms.core.viewmodel.CitySearchController
|
import com.android.trisolarispms.core.viewmodel.CitySearchController
|
||||||
@@ -11,19 +12,34 @@ import com.android.trisolarispms.data.api.model.BookingCreateRequest
|
|||||||
import com.android.trisolarispms.data.api.model.BookingCreateResponse
|
import com.android.trisolarispms.data.api.model.BookingCreateResponse
|
||||||
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewRequest
|
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewRequest
|
||||||
import com.android.trisolarispms.data.api.model.GuestDto
|
import com.android.trisolarispms.data.api.model.GuestDto
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
|
import com.android.trisolarispms.data.local.bookinglist.BookingListRepository
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
||||||
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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class BookingCreateViewModel : ViewModel() {
|
class BookingCreateViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private companion object {
|
private companion object {
|
||||||
const val DEFAULT_PREVIEW_BILLABLE_NIGHTS = 1
|
const val DEFAULT_PREVIEW_BILLABLE_NIGHTS = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _state = MutableStateFlow(BookingCreateState())
|
private val _state = MutableStateFlow(BookingCreateState())
|
||||||
val state: StateFlow<BookingCreateState> = _state
|
val state: StateFlow<BookingCreateState> = _state
|
||||||
|
private val activeRoomStayRepository = ActiveRoomStayRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
||||||
|
)
|
||||||
|
private val bookingListRepository = BookingListRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingListCacheDao()
|
||||||
|
)
|
||||||
|
private val bookingDetailsRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
private var expectedCheckoutPreviewRequestId: Long = 0
|
private var expectedCheckoutPreviewRequestId: Long = 0
|
||||||
private val fromCitySearch = CitySearchController(
|
private val fromCitySearch = CitySearchController(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
@@ -358,6 +374,10 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
val body = response.body()
|
val body = response.body()
|
||||||
if (response.isSuccessful && body != null) {
|
if (response.isSuccessful && body != null) {
|
||||||
|
syncCreateBookingCaches(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = body.id
|
||||||
|
)
|
||||||
_state.update { it.copy(isLoading = false, error = null) }
|
_state.update { it.copy(isLoading = false, error = null) }
|
||||||
onDone(body, null, phone)
|
onDone(body, null, phone)
|
||||||
} else {
|
} else {
|
||||||
@@ -368,6 +388,19 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun syncCreateBookingCaches(propertyId: String, bookingId: String?) {
|
||||||
|
activeRoomStayRepository.refresh(propertyId = propertyId)
|
||||||
|
bookingListRepository.refreshByStatus(propertyId = propertyId, status = "OPEN")
|
||||||
|
bookingListRepository.refreshByStatus(propertyId = propertyId, status = "CHECKED_IN")
|
||||||
|
val safeBookingId = bookingId?.trim().orEmpty()
|
||||||
|
if (safeBookingId.isNotBlank()) {
|
||||||
|
bookingDetailsRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = safeBookingId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BookingCreateState.withDefaultTransportModeForCheckIn(expectedCheckInAt: String): BookingCreateState {
|
private fun BookingCreateState.withDefaultTransportModeForCheckIn(expectedCheckInAt: String): BookingCreateState {
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package com.android.trisolarispms.ui.booking
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.model.BookingRoomRequestCreateRequest
|
import com.android.trisolarispms.data.api.model.BookingRoomRequestCreateRequest
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
|
import com.android.trisolarispms.data.local.bookinglist.BookingListRepository
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
||||||
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
|
||||||
@@ -11,9 +16,20 @@ import kotlinx.coroutines.launch
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
class BookingRoomRequestViewModel : ViewModel() {
|
class BookingRoomRequestViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val _state = MutableStateFlow(BookingRoomRequestState())
|
private val _state = MutableStateFlow(BookingRoomRequestState())
|
||||||
val state: StateFlow<BookingRoomRequestState> = _state
|
val state: StateFlow<BookingRoomRequestState> = _state
|
||||||
|
private val activeRoomStayRepository = ActiveRoomStayRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
||||||
|
)
|
||||||
|
private val bookingListRepository = BookingListRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingListCacheDao()
|
||||||
|
)
|
||||||
|
private val bookingDetailsRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
|
|
||||||
fun load(propertyId: String, fromAt: String, toAt: String) {
|
fun load(propertyId: String, fromAt: String, toAt: String) {
|
||||||
if (propertyId.isBlank()) return
|
if (propertyId.isBlank()) return
|
||||||
@@ -125,6 +141,7 @@ class BookingRoomRequestViewModel : ViewModel() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
syncRoomRequestCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
_state.update { it.copy(isSubmitting = false, error = null) }
|
_state.update { it.copy(isSubmitting = false, error = null) }
|
||||||
onDone()
|
onDone()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -148,6 +165,16 @@ class BookingRoomRequestViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun syncRoomRequestCaches(propertyId: String, bookingId: String) {
|
||||||
|
activeRoomStayRepository.refresh(propertyId = propertyId)
|
||||||
|
bookingListRepository.refreshByStatus(propertyId = propertyId, status = "OPEN")
|
||||||
|
bookingListRepository.refreshByStatus(propertyId = propertyId, status = "CHECKED_IN")
|
||||||
|
bookingDetailsRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toDateOnly(): String? =
|
private fun String.toDateOnly(): String? =
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.android.trisolarispms.ui.guest
|
package com.android.trisolarispms.ui.guest
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.core.viewmodel.CitySearchController
|
import com.android.trisolarispms.core.viewmodel.CitySearchController
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
@@ -8,6 +9,9 @@ import com.android.trisolarispms.data.api.core.GeoSearchRepository
|
|||||||
import com.android.trisolarispms.data.api.model.BookingLinkGuestRequest
|
import com.android.trisolarispms.data.api.model.BookingLinkGuestRequest
|
||||||
import com.android.trisolarispms.data.api.model.GuestDto
|
import com.android.trisolarispms.data.api.model.GuestDto
|
||||||
import com.android.trisolarispms.data.api.model.GuestUpdateRequest
|
import com.android.trisolarispms.data.api.model.GuestUpdateRequest
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
||||||
import com.android.trisolarispms.ui.booking.findPhoneCountryOption
|
import com.android.trisolarispms.ui.booking.findPhoneCountryOption
|
||||||
import com.google.gson.JsonNull
|
import com.google.gson.JsonNull
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
@@ -20,9 +24,17 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class GuestInfoViewModel : ViewModel() {
|
class GuestInfoViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val _state = MutableStateFlow(GuestInfoState())
|
private val _state = MutableStateFlow(GuestInfoState())
|
||||||
val state: StateFlow<GuestInfoState> = _state
|
val state: StateFlow<GuestInfoState> = _state
|
||||||
|
private val bookingRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
|
private val roomStayRepository = ActiveRoomStayRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
||||||
|
)
|
||||||
private var nationalitySearchJob: Job? = null
|
private var nationalitySearchJob: Job? = null
|
||||||
private var phoneAutofillJob: Job? = null
|
private var phoneAutofillJob: Job? = null
|
||||||
private var lastAutofilledPhoneE164: String? = null
|
private var lastAutofilledPhoneE164: String? = null
|
||||||
@@ -350,6 +362,11 @@ class GuestInfoViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (submitError == null) {
|
if (submitError == null) {
|
||||||
|
roomStayRepository.refresh(propertyId = propertyId)
|
||||||
|
bookingRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
)
|
||||||
_state.update { it.copy(isLoading = false, error = null) }
|
_state.update { it.copy(isLoading = false, error = null) }
|
||||||
onDone()
|
onDone()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import com.android.trisolarispms.ui.common.PaddedScreenColumn
|
|||||||
@Composable
|
@Composable
|
||||||
fun GuestSignatureScreen(
|
fun GuestSignatureScreen(
|
||||||
propertyId: String,
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
guestId: String,
|
guestId: String,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
@@ -68,7 +69,7 @@ fun GuestSignatureScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val svg = buildSignatureSvg(strokes, canvasSize.value)
|
val svg = buildSignatureSvg(strokes, canvasSize.value)
|
||||||
if (!svg.isNullOrBlank()) {
|
if (!svg.isNullOrBlank()) {
|
||||||
viewModel.uploadSignature(propertyId, guestId, svg, onDone)
|
viewModel.uploadSignature(propertyId, bookingId, guestId, svg, onDone)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = strokes.isNotEmpty() && !state.isLoading
|
enabled = strokes.isNotEmpty() && !state.isLoading
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.android.trisolarispms.ui.guest
|
package com.android.trisolarispms.ui.guest
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.core.viewmodel.launchRequest
|
import com.android.trisolarispms.core.viewmodel.launchRequest
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
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
|
||||||
@@ -10,15 +13,26 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
class GuestSignatureViewModel : ViewModel() {
|
class GuestSignatureViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val _state = MutableStateFlow(GuestSignatureState())
|
private val _state = MutableStateFlow(GuestSignatureState())
|
||||||
val state: StateFlow<GuestSignatureState> = _state
|
val state: StateFlow<GuestSignatureState> = _state
|
||||||
|
private val bookingRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
_state.value = GuestSignatureState()
|
_state.value = GuestSignatureState()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uploadSignature(propertyId: String, guestId: String, svg: String, onDone: () -> Unit) {
|
fun uploadSignature(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
guestId: String,
|
||||||
|
svg: String,
|
||||||
|
onDone: () -> Unit
|
||||||
|
) {
|
||||||
if (propertyId.isBlank() || guestId.isBlank()) return
|
if (propertyId.isBlank() || guestId.isBlank()) return
|
||||||
launchRequest(
|
launchRequest(
|
||||||
state = _state,
|
state = _state,
|
||||||
@@ -35,6 +49,12 @@ class GuestSignatureViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
val response = api.uploadSignature(propertyId = propertyId, guestId = guestId, file = part)
|
val response = api.uploadSignature(propertyId = propertyId, guestId = guestId, file = part)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
|
if (bookingId.isNotBlank()) {
|
||||||
|
bookingRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
)
|
||||||
|
}
|
||||||
_state.update { it.copy(isLoading = false, error = null) }
|
_state.update { it.copy(isLoading = false, error = null) }
|
||||||
onDone()
|
onDone()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
package com.android.trisolarispms.ui.guestdocs
|
package com.android.trisolarispms.ui.guestdocs
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.core.ApiConstants
|
import com.android.trisolarispms.data.api.core.ApiConstants
|
||||||
import com.android.trisolarispms.data.api.model.GuestDocumentDto
|
import com.android.trisolarispms.data.api.model.GuestDocumentDto
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
|
import com.android.trisolarispms.data.local.guestdoc.GuestDocumentRepository
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
@@ -24,53 +29,47 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class GuestDocumentsViewModel : ViewModel() {
|
class GuestDocumentsViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val _state = MutableStateFlow(GuestDocumentsState())
|
private val _state = MutableStateFlow(GuestDocumentsState())
|
||||||
val state: StateFlow<GuestDocumentsState> = _state
|
val state: StateFlow<GuestDocumentsState> = _state
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
private val repository = GuestDocumentRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).guestDocumentCacheDao()
|
||||||
|
)
|
||||||
private var eventSource: EventSource? = null
|
private var eventSource: EventSource? = null
|
||||||
private var streamKey: String? = null
|
private var streamKey: String? = null
|
||||||
|
private var observeKey: String? = null
|
||||||
|
private var observeJob: Job? = null
|
||||||
|
|
||||||
fun load(propertyId: String, guestId: String) {
|
fun load(propertyId: String, guestId: String) {
|
||||||
|
if (propertyId.isBlank() || guestId.isBlank()) return
|
||||||
|
observeCache(propertyId = propertyId, guestId = guestId)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
try {
|
val result = repository.refresh(propertyId = propertyId, guestId = guestId)
|
||||||
val api = ApiClient.create()
|
|
||||||
val response = api.listGuestDocuments(propertyId, guestId)
|
|
||||||
val body = response.body()
|
|
||||||
if (response.isSuccessful && body != null) {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
documents = body,
|
error = result.exceptionOrNull()?.localizedMessage
|
||||||
error = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = "Load failed: ${response.code()}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = e.localizedMessage ?: "Load failed"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startStream(propertyId: String, guestId: String) {
|
fun startStream(propertyId: String, guestId: String) {
|
||||||
|
if (propertyId.isBlank() || guestId.isBlank()) return
|
||||||
|
observeCache(propertyId = propertyId, guestId = guestId)
|
||||||
val key = "$propertyId:$guestId"
|
val key = "$propertyId:$guestId"
|
||||||
if (streamKey == key && eventSource != null) return
|
if (streamKey == key && eventSource != null) return
|
||||||
stopStream()
|
stopStream()
|
||||||
streamKey = key
|
streamKey = key
|
||||||
_state.update { it.copy(isLoading = true, error = null, documents = emptyList()) }
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.refresh(propertyId = propertyId, guestId = guestId)
|
||||||
|
_state.update { it.copy(isLoading = false) }
|
||||||
|
}
|
||||||
val client = ApiClient.createOkHttpClient(readTimeoutSeconds = 0)
|
val client = ApiClient.createOkHttpClient(readTimeoutSeconds = 0)
|
||||||
val url = "${ApiConstants.BASE_URL}properties/$propertyId/guests/$guestId/documents/stream"
|
val url = "${ApiConstants.BASE_URL}properties/$propertyId/guests/$guestId/documents/stream"
|
||||||
val request = Request.Builder().url(url).get().build()
|
val request = Request.Builder().url(url).get().build()
|
||||||
@@ -91,19 +90,16 @@ class GuestDocumentsViewModel : ViewModel() {
|
|||||||
data: String
|
data: String
|
||||||
) {
|
) {
|
||||||
if (data.isBlank() || type == "ping") return
|
if (data.isBlank() || type == "ping") return
|
||||||
val docs = try {
|
val docs = runCatching {
|
||||||
gson.fromJson(data, Array<GuestDocumentDto>::class.java)?.toList()
|
gson.fromJson(data, Array<GuestDocumentDto>::class.java)?.toList()
|
||||||
} catch (_: Exception) {
|
}.getOrNull() ?: return
|
||||||
null
|
viewModelScope.launch {
|
||||||
}
|
repository.storeSnapshot(
|
||||||
if (docs != null) {
|
propertyId = propertyId,
|
||||||
_state.update {
|
guestId = guestId,
|
||||||
it.copy(
|
documents = docs
|
||||||
isLoading = false,
|
|
||||||
documents = docs,
|
|
||||||
error = null
|
|
||||||
)
|
)
|
||||||
}
|
_state.update { current -> current.copy(isLoading = false, error = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +122,6 @@ class GuestDocumentsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
_state.update { it.copy(isLoading = false) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopStream() {
|
fun stopStream() {
|
||||||
@@ -166,49 +161,43 @@ class GuestDocumentsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
val filename = resolveFileName(resolver, uri) ?: "document"
|
val filename = resolveFileName(resolver, uri) ?: "document"
|
||||||
val file = copyToCache(resolver, uri, context.cacheDir, filename)
|
val file = copyToCache(resolver, uri, context.cacheDir, filename)
|
||||||
uploadFile(propertyId, guestId, bookingId, file, mime)
|
val uploadError = uploadFile(propertyId, guestId, bookingId, file, mime)
|
||||||
|
if (uploadError != null) {
|
||||||
|
errorMessage = uploadError
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = e.localizedMessage ?: "Upload failed"
|
errorMessage = e.localizedMessage ?: "Upload failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_state.update { current ->
|
_state.update {
|
||||||
current.copy(
|
it.copy(
|
||||||
isUploading = false,
|
isUploading = false,
|
||||||
error = errorMessage ?: current.error
|
error = errorMessage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDocument(propertyId: String, guestId: String, documentId: String) {
|
fun deleteDocument(propertyId: String, guestId: String, documentId: String) {
|
||||||
|
if (propertyId.isBlank() || guestId.isBlank() || documentId.isBlank()) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
try {
|
try {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
val response = api.deleteGuestDocument(propertyId, guestId, documentId)
|
val response = api.deleteGuestDocument(propertyId, guestId, documentId)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_state.update { current ->
|
val refreshResult = repository.refresh(propertyId = propertyId, guestId = guestId)
|
||||||
current.copy(
|
_state.update {
|
||||||
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = null,
|
error = refreshResult.exceptionOrNull()?.localizedMessage
|
||||||
documents = current.documents.filterNot { it.id == documentId }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_state.update {
|
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = "Delete failed: ${response.code()}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_state.update {
|
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") }
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = e.localizedMessage ?: "Delete failed"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,18 +211,15 @@ class GuestDocumentsViewModel : ViewModel() {
|
|||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isUploading = true, error = null) }
|
_state.update { it.copy(isUploading = true, error = null) }
|
||||||
try {
|
val uploadError = uploadFile(propertyId, guestId, bookingId, file, mimeType)
|
||||||
uploadFile(propertyId, guestId, bookingId, file, mimeType)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isUploading = false,
|
isUploading = false,
|
||||||
error = e.localizedMessage ?: "Upload failed"
|
error = uploadError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun uploadFile(
|
private suspend fun uploadFile(
|
||||||
propertyId: String,
|
propertyId: String,
|
||||||
@@ -241,7 +227,7 @@ class GuestDocumentsViewModel : ViewModel() {
|
|||||||
bookingId: String,
|
bookingId: String,
|
||||||
file: File,
|
file: File,
|
||||||
mimeType: String
|
mimeType: String
|
||||||
) {
|
): String? {
|
||||||
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
||||||
val part = MultipartBody.Part.createFormData("file", file.name, requestBody)
|
val part = MultipartBody.Part.createFormData("file", file.name, requestBody)
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
@@ -251,22 +237,29 @@ class GuestDocumentsViewModel : ViewModel() {
|
|||||||
bookingId = bookingId,
|
bookingId = bookingId,
|
||||||
file = part
|
file = part
|
||||||
)
|
)
|
||||||
val body = response.body()
|
if (response.isSuccessful && response.body() != null) {
|
||||||
if (response.isSuccessful && body != null) {
|
val refreshResult = repository.refresh(propertyId = propertyId, guestId = guestId)
|
||||||
_state.update { it.copy(isUploading = false, error = null) }
|
return refreshResult.exceptionOrNull()?.localizedMessage
|
||||||
} else if (response.code() == 409) {
|
}
|
||||||
_state.update {
|
if (response.code() == 409) {
|
||||||
it.copy(
|
return "Duplicate document"
|
||||||
isUploading = false,
|
}
|
||||||
error = "Duplicate document"
|
return "Upload failed: ${response.code()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeCache(propertyId: String, guestId: String) {
|
||||||
|
val key = "$propertyId:$guestId"
|
||||||
|
if (observeKey == key && observeJob?.isActive == true) return
|
||||||
|
observeJob?.cancel()
|
||||||
|
observeKey = key
|
||||||
|
observeJob = viewModelScope.launch {
|
||||||
|
repository.observeByGuest(propertyId = propertyId, guestId = guestId).collect { docs ->
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(
|
||||||
|
documents = docs,
|
||||||
|
isLoading = if (docs.isNotEmpty()) false else current.isLoading
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isUploading = false,
|
|
||||||
error = "Upload failed: ${response.code()}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,6 +289,7 @@ class GuestDocumentsViewModel : ViewModel() {
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
observeJob?.cancel()
|
||||||
stopStream()
|
stopStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ internal fun renderHomeGuestRoutes(
|
|||||||
|
|
||||||
is AppRoute.GuestSignature -> GuestSignatureScreen(
|
is AppRoute.GuestSignature -> GuestSignatureScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId,
|
||||||
guestId = currentRoute.guestId,
|
guestId = currentRoute.guestId,
|
||||||
onBack = {
|
onBack = {
|
||||||
refs.route.value = AppRoute.GuestInfo(
|
refs.route.value = AppRoute.GuestInfo(
|
||||||
|
|||||||
@@ -1,40 +1,43 @@
|
|||||||
package com.android.trisolarispms.ui.payment
|
package com.android.trisolarispms.ui.payment
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.core.ApiService
|
import com.android.trisolarispms.data.api.core.ApiService
|
||||||
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
|
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
|
import com.android.trisolarispms.data.local.payment.PaymentRepository
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
class BookingPaymentsViewModel : ViewModel() {
|
class BookingPaymentsViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val _state = MutableStateFlow(BookingPaymentsState())
|
private val _state = MutableStateFlow(BookingPaymentsState())
|
||||||
val state: StateFlow<BookingPaymentsState> = _state
|
val state: StateFlow<BookingPaymentsState> = _state
|
||||||
|
private val bookingRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
|
private val paymentRepository = PaymentRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).paymentCacheDao()
|
||||||
|
)
|
||||||
|
private var observeKey: String? = null
|
||||||
|
private var observePaymentsJob: Job? = null
|
||||||
|
private var observeBookingJob: Job? = null
|
||||||
|
|
||||||
fun load(propertyId: String, bookingId: String) {
|
fun load(propertyId: String, bookingId: String) {
|
||||||
runPaymentAction(defaultError = "Load failed") { api ->
|
if (propertyId.isBlank() || bookingId.isBlank()) return
|
||||||
val paymentsResponse = api.listPayments(propertyId, bookingId)
|
observeCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
val payments = paymentsResponse.body()
|
runPaymentAction(defaultError = "Load failed") {
|
||||||
if (paymentsResponse.isSuccessful && payments != null) {
|
refreshPaymentCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
val pending = api.getBookingBalance(propertyId, bookingId)
|
_state.update { it.copy(isLoading = false, error = null, message = null) }
|
||||||
.body()
|
|
||||||
?.pending
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
payments = payments,
|
|
||||||
pendingBalance = pending,
|
|
||||||
error = null,
|
|
||||||
message = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setActionFailure("Load", paymentsResponse)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +61,10 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
val body = response.body()
|
val body = response.body()
|
||||||
if (response.isSuccessful && body != null) {
|
if (response.isSuccessful && body != null) {
|
||||||
_state.update { current ->
|
refreshPaymentCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
val nextPending = latestPending?.let { (it - amount).coerceAtLeast(0L) }
|
_state.update {
|
||||||
current.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = listOf(body) + current.payments,
|
|
||||||
pendingBalance = nextPending ?: current.pendingBalance,
|
|
||||||
error = null,
|
error = null,
|
||||||
message = "Cash payment added"
|
message = "Cash payment added"
|
||||||
)
|
)
|
||||||
@@ -76,24 +77,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
||||||
runPaymentAction(defaultError = "Delete failed") { api ->
|
runPaymentAction(defaultError = "Delete failed") { api ->
|
||||||
val payment = _state.value.payments.firstOrNull { it.id == paymentId }
|
|
||||||
val response = api.deletePayment(
|
val response = api.deletePayment(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
bookingId = bookingId,
|
bookingId = bookingId,
|
||||||
paymentId = paymentId
|
paymentId = paymentId
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_state.update { current ->
|
refreshPaymentCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
val restoredPending = if (payment?.method == "CASH") {
|
_state.update {
|
||||||
val amount = payment.amount ?: 0L
|
it.copy(
|
||||||
current.pendingBalance?.plus(amount)
|
|
||||||
} else {
|
|
||||||
current.pendingBalance
|
|
||||||
}
|
|
||||||
current.copy(
|
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
payments = current.payments.filterNot { it.id == paymentId },
|
|
||||||
pendingBalance = restoredPending,
|
|
||||||
error = null,
|
error = null,
|
||||||
message = "Cash payment deleted"
|
message = "Cash payment deleted"
|
||||||
)
|
)
|
||||||
@@ -133,7 +126,7 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
val body = response.body()
|
val body = response.body()
|
||||||
if (response.isSuccessful && body != null) {
|
if (response.isSuccessful && body != null) {
|
||||||
load(propertyId, bookingId)
|
refreshPaymentCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -183,4 +176,46 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
if (amount > pendingBalance) return "Amount cannot be greater than pending balance ($pendingBalance)"
|
if (amount > pendingBalance) return "Amount cannot be greater than pending balance ($pendingBalance)"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
observePaymentsJob?.cancel()
|
||||||
|
observeBookingJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeCaches(propertyId: String, bookingId: String) {
|
||||||
|
val key = "$propertyId:$bookingId"
|
||||||
|
if (observeKey == key &&
|
||||||
|
observePaymentsJob?.isActive == true &&
|
||||||
|
observeBookingJob?.isActive == true
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
observePaymentsJob?.cancel()
|
||||||
|
observeBookingJob?.cancel()
|
||||||
|
observeKey = key
|
||||||
|
observePaymentsJob = viewModelScope.launch {
|
||||||
|
paymentRepository.observeByBooking(propertyId = propertyId, bookingId = bookingId).collect { items ->
|
||||||
|
_state.update { current -> current.copy(payments = items) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
observeBookingJob = viewModelScope.launch {
|
||||||
|
bookingRepository.observeBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
).collect { details ->
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(pendingBalance = details?.pending ?: current.pendingBalance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshPaymentCaches(propertyId: String, bookingId: String) {
|
||||||
|
paymentRepository.refresh(propertyId = propertyId, bookingId = bookingId).getOrThrow()
|
||||||
|
bookingRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
).getOrThrow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.android.trisolarispms.ui.razorpay
|
package com.android.trisolarispms.ui.razorpay
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.core.ApiConstants
|
import com.android.trisolarispms.data.api.core.ApiConstants
|
||||||
@@ -9,37 +10,60 @@ import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
|
|||||||
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
|
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
|
||||||
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
|
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
|
||||||
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
|
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
|
import com.android.trisolarispms.data.local.payment.PaymentRepository
|
||||||
|
import com.android.trisolarispms.data.local.razorpay.RazorpayCacheRepository
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.sse.EventSource
|
import okhttp3.sse.EventSource
|
||||||
import okhttp3.sse.EventSourceListener
|
import okhttp3.sse.EventSourceListener
|
||||||
import okhttp3.sse.EventSources
|
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() {
|
class RazorpayQrViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val _state = MutableStateFlow(
|
private val _state = MutableStateFlow(
|
||||||
RazorpayQrState(deviceInfo = buildDeviceInfo())
|
RazorpayQrState(deviceInfo = buildDeviceInfo())
|
||||||
)
|
)
|
||||||
val state: StateFlow<RazorpayQrState> = _state
|
val state: StateFlow<RazorpayQrState> = _state
|
||||||
|
private val razorpayRepository = RazorpayCacheRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).razorpayCacheDao()
|
||||||
|
)
|
||||||
|
private val paymentRepository = PaymentRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).paymentCacheDao()
|
||||||
|
)
|
||||||
|
private val bookingRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
private var qrEventSource: EventSource? = null
|
private var qrEventSource: EventSource? = null
|
||||||
private var lastQrId: String? = null
|
private var lastQrId: String? = null
|
||||||
private var qrPollJob: Job? = null
|
private var qrPollJob: Job? = null
|
||||||
|
private var requestObserveKey: String? = null
|
||||||
|
private var requestObserveJob: Job? = null
|
||||||
|
private var eventObserveKey: String? = null
|
||||||
|
private var eventObserveJob: Job? = null
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
stopQrEventStream()
|
stopQrEventStream()
|
||||||
stopQrEventPolling()
|
stopQrEventPolling()
|
||||||
|
stopRequestObservation()
|
||||||
|
stopQrEventObservation()
|
||||||
_state.value = RazorpayQrState(deviceInfo = buildDeviceInfo())
|
_state.value = RazorpayQrState(deviceInfo = buildDeviceInfo())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exitQrView() {
|
fun exitQrView() {
|
||||||
stopQrEventStream()
|
stopQrEventStream()
|
||||||
stopQrEventPolling()
|
stopQrEventPolling()
|
||||||
|
stopQrEventObservation()
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
qrId = null,
|
qrId = null,
|
||||||
@@ -96,10 +120,11 @@ class RazorpayQrViewModel : ViewModel() {
|
|||||||
currency = body.currency,
|
currency = body.currency,
|
||||||
imageUrl = body.imageUrl,
|
imageUrl = body.imageUrl,
|
||||||
isClosed = false,
|
isClosed = false,
|
||||||
|
isCredited = false,
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadQrList(propertyId, bookingId)
|
refreshRequestCache(propertyId = propertyId, bookingId = bookingId)
|
||||||
startQrEventStream(propertyId, bookingId, body.qrId)
|
startQrEventStream(propertyId, bookingId, body.qrId)
|
||||||
} else {
|
} else {
|
||||||
_state.update {
|
_state.update {
|
||||||
@@ -120,127 +145,11 @@ class RazorpayQrViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startQrEventStream(propertyId: String, bookingId: String, qrId: String?) {
|
|
||||||
if (qrId.isNullOrBlank()) return
|
|
||||||
if (lastQrId == qrId && qrEventSource != null) return
|
|
||||||
stopQrEventStream()
|
|
||||||
stopQrEventPolling()
|
|
||||||
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
|
|
||||||
val status = event.status?.lowercase()
|
|
||||||
val eventName = event.event?.lowercase()
|
|
||||||
if (eventName == "qr_code.credited" || status == "credited") {
|
|
||||||
_state.update { it.copy(isCredited = true, imageUrl = null) }
|
|
||||||
stopQrEventStream()
|
|
||||||
stopQrEventPolling()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isClosedStatus(status)) {
|
|
||||||
_state.update { it.copy(isClosed = true, imageUrl = null) }
|
|
||||||
stopQrEventStream()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(
|
|
||||||
eventSource: EventSource,
|
|
||||||
t: Throwable?,
|
|
||||||
response: okhttp3.Response?
|
|
||||||
) {
|
|
||||||
stopQrEventStream()
|
|
||||||
startQrEventPolling(propertyId, bookingId, qrId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosed(eventSource: EventSource) {
|
|
||||||
stopQrEventStream()
|
|
||||||
startQrEventPolling(propertyId, bookingId, qrId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Keep polling as a fallback in case SSE is buffered or never delivers events.
|
|
||||||
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 {
|
|
||||||
var delayMs = 4000L
|
|
||||||
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 { it.event?.lowercase() == "qr_code.credited" || it.status?.lowercase() == "credited" }) {
|
|
||||||
_state.update { it.copy(isCredited = true, imageUrl = null) }
|
|
||||||
stopQrEventStream()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (body.any { isClosedStatus(it.status) }) {
|
|
||||||
_state.update { it.copy(isClosed = true, imageUrl = null) }
|
|
||||||
stopQrEventStream()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delayMs = 4000L
|
|
||||||
} catch (e: Exception) {
|
|
||||||
delayMs = (delayMs * 1.5).toLong().coerceAtMost(15000L)
|
|
||||||
}
|
|
||||||
delay(delayMs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
fun loadQrList(propertyId: String, bookingId: String) {
|
||||||
|
if (propertyId.isBlank() || bookingId.isBlank()) return
|
||||||
|
observeRequestCache(propertyId = propertyId, bookingId = bookingId)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
refreshRequestCache(propertyId = propertyId, bookingId = bookingId)
|
||||||
val response = ApiClient.create().listRazorpayRequests(propertyId, bookingId)
|
|
||||||
val body = response.body()
|
|
||||||
if (response.isSuccessful && body != null) {
|
|
||||||
_state.update { it.copy(qrList = body) }
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
// ignore list load errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,10 +165,7 @@ class RazorpayQrViewModel : ViewModel() {
|
|||||||
body = RazorpayCloseRequest(qrId = qrId, paymentLinkId = paymentLinkId)
|
body = RazorpayCloseRequest(qrId = qrId, paymentLinkId = paymentLinkId)
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_state.update { current ->
|
refreshRequestCache(propertyId = propertyId, bookingId = bookingId)
|
||||||
current.copy(qrList = current.qrList.filterNot { it.requestId == item.requestId })
|
|
||||||
}
|
|
||||||
loadQrList(propertyId, bookingId)
|
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// ignore close errors
|
// ignore close errors
|
||||||
@@ -313,7 +219,7 @@ class RazorpayQrViewModel : ViewModel() {
|
|||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadQrList(propertyId, bookingId)
|
refreshRequestCache(propertyId = propertyId, bookingId = bookingId)
|
||||||
} else {
|
} else {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -333,6 +239,200 @@ class RazorpayQrViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startQrEventStream(propertyId: String, bookingId: String, qrId: String?) {
|
||||||
|
if (qrId.isNullOrBlank()) return
|
||||||
|
observeQrEventCache(propertyId = propertyId, bookingId = bookingId, qrId = qrId)
|
||||||
|
if (lastQrId == qrId && qrEventSource != null) return
|
||||||
|
stopQrEventStream()
|
||||||
|
stopQrEventPolling()
|
||||||
|
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
|
||||||
|
val eventQrId = event.qrId?.takeIf { it.isNotBlank() } ?: qrId
|
||||||
|
viewModelScope.launch {
|
||||||
|
refreshQrEventCache(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = eventQrId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
eventSource: EventSource,
|
||||||
|
t: Throwable?,
|
||||||
|
response: okhttp3.Response?
|
||||||
|
) {
|
||||||
|
stopQrEventStream()
|
||||||
|
startQrEventPolling(propertyId, bookingId, qrId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(eventSource: EventSource) {
|
||||||
|
stopQrEventStream()
|
||||||
|
startQrEventPolling(propertyId, bookingId, qrId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Keep polling as a fallback in case SSE is buffered or never delivers events.
|
||||||
|
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 {
|
||||||
|
var delayMs = 4000L
|
||||||
|
while (true) {
|
||||||
|
val currentQrId = state.value.qrId
|
||||||
|
if (currentQrId.isNullOrBlank() || currentQrId != qrId || state.value.isClosed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
refreshQrEventCache(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId
|
||||||
|
)
|
||||||
|
delayMs = 4000L
|
||||||
|
} catch (_: Exception) {
|
||||||
|
delayMs = (delayMs * 1.5).toLong().coerceAtMost(15000L)
|
||||||
|
}
|
||||||
|
delay(delayMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopQrEventPolling() {
|
||||||
|
qrPollJob?.cancel()
|
||||||
|
qrPollJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeRequestCache(propertyId: String, bookingId: String) {
|
||||||
|
val key = "$propertyId:$bookingId"
|
||||||
|
if (requestObserveKey == key && requestObserveJob?.isActive == true) return
|
||||||
|
stopRequestObservation()
|
||||||
|
requestObserveKey = key
|
||||||
|
requestObserveJob = viewModelScope.launch {
|
||||||
|
razorpayRepository.observeRequests(propertyId = propertyId, bookingId = bookingId).collect { items ->
|
||||||
|
_state.update { current -> current.copy(qrList = items) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeQrEventCache(propertyId: String, bookingId: String, qrId: String) {
|
||||||
|
val key = "$propertyId:$bookingId:$qrId"
|
||||||
|
if (eventObserveKey == key && eventObserveJob?.isActive == true) return
|
||||||
|
stopQrEventObservation()
|
||||||
|
eventObserveKey = key
|
||||||
|
eventObserveJob = viewModelScope.launch {
|
||||||
|
razorpayRepository.observeQrEvents(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId
|
||||||
|
).collect { events ->
|
||||||
|
applyQrEventState(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
events = events
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopRequestObservation() {
|
||||||
|
requestObserveJob?.cancel()
|
||||||
|
requestObserveJob = null
|
||||||
|
requestObserveKey = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopQrEventObservation() {
|
||||||
|
eventObserveJob?.cancel()
|
||||||
|
eventObserveJob = null
|
||||||
|
eventObserveKey = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshRequestCache(propertyId: String, bookingId: String) {
|
||||||
|
val result = razorpayRepository.refreshRequests(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
result.exceptionOrNull()?.let { throwable ->
|
||||||
|
_state.update { it.copy(error = throwable.localizedMessage ?: "Load failed") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshQrEventCache(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
qrId: String
|
||||||
|
): Result<List<RazorpayQrEventDto>> =
|
||||||
|
razorpayRepository.refreshQrEvents(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun applyQrEventState(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
events: List<RazorpayQrEventDto>
|
||||||
|
) {
|
||||||
|
if (events.isEmpty()) return
|
||||||
|
val isCredited = events.any {
|
||||||
|
it.event?.lowercase() == "qr_code.credited" || it.status?.lowercase() == "credited"
|
||||||
|
}
|
||||||
|
val isClosed = events.any { event ->
|
||||||
|
when (event.status?.lowercase()) {
|
||||||
|
"credited", "closed", "expired" -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var becameCredited = false
|
||||||
|
_state.update { current ->
|
||||||
|
val nextCredited = current.isCredited || isCredited
|
||||||
|
val nextClosed = current.isClosed || isClosed || nextCredited
|
||||||
|
becameCredited = !current.isCredited && nextCredited
|
||||||
|
if (nextCredited == current.isCredited && nextClosed == current.isClosed) {
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
current.copy(
|
||||||
|
isCredited = nextCredited,
|
||||||
|
isClosed = nextClosed,
|
||||||
|
imageUrl = if (nextClosed) null else current.imageUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (becameCredited) {
|
||||||
|
stopQrEventStream()
|
||||||
|
stopQrEventPolling()
|
||||||
|
viewModelScope.launch {
|
||||||
|
syncPaymentCachesAfterCredit(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
}
|
||||||
|
} else if (isClosed) {
|
||||||
|
stopQrEventStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun syncPaymentCachesAfterCredit(propertyId: String, bookingId: String) {
|
||||||
|
paymentRepository.refresh(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
bookingRepository.refreshBookingDetails(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildDeviceInfo(): String {
|
private fun buildDeviceInfo(): String {
|
||||||
val release = android.os.Build.VERSION.RELEASE
|
val release = android.os.Build.VERSION.RELEASE
|
||||||
val model = android.os.Build.MODEL
|
val model = android.os.Build.MODEL
|
||||||
@@ -342,5 +442,8 @@ class RazorpayQrViewModel : ViewModel() {
|
|||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
stopQrEventStream()
|
stopQrEventStream()
|
||||||
|
stopQrEventPolling()
|
||||||
|
stopRequestObservation()
|
||||||
|
stopQrEventObservation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.android.trisolarispms.ui.roomstay
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.local.bookinglist.BookingListRepository
|
||||||
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -21,8 +21,14 @@ class ActiveRoomStaysViewModel(
|
|||||||
private val repository = ActiveRoomStayRepository(
|
private val repository = ActiveRoomStayRepository(
|
||||||
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
||||||
)
|
)
|
||||||
|
private val bookingListRepository = BookingListRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingListCacheDao()
|
||||||
|
)
|
||||||
private var observeJob: Job? = null
|
private var observeJob: Job? = null
|
||||||
private var observePropertyId: String? = null
|
private var observePropertyId: String? = null
|
||||||
|
private var observeBookingListPropertyId: String? = null
|
||||||
|
private var observeCheckedInJob: Job? = null
|
||||||
|
private var observeOpenJob: Job? = null
|
||||||
|
|
||||||
fun toggleShowOpenBookings() {
|
fun toggleShowOpenBookings() {
|
||||||
_state.update { it.copy(showOpenBookings = !it.showOpenBookings) }
|
_state.update { it.copy(showOpenBookings = !it.showOpenBookings) }
|
||||||
@@ -35,41 +41,24 @@ class ActiveRoomStaysViewModel(
|
|||||||
fun load(propertyId: String) {
|
fun load(propertyId: String) {
|
||||||
if (propertyId.isBlank()) return
|
if (propertyId.isBlank()) return
|
||||||
observeCache(propertyId = propertyId)
|
observeCache(propertyId = propertyId)
|
||||||
|
observeBookingLists(propertyId = propertyId)
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val activeResult = repository.refresh(propertyId = propertyId)
|
val activeResult = repository.refresh(propertyId = propertyId)
|
||||||
val api = ApiClient.create()
|
val checkedInResult = bookingListRepository.refreshByStatus(
|
||||||
val checkedInBookingsResponse = runCatching {
|
propertyId = propertyId,
|
||||||
api.listBookings(propertyId = propertyId, status = "CHECKED_IN")
|
status = "CHECKED_IN"
|
||||||
}.getOrNull()
|
)
|
||||||
val openBookingsResponse = runCatching {
|
val openResult = bookingListRepository.refreshByStatus(
|
||||||
api.listBookings(propertyId = propertyId, status = "OPEN")
|
propertyId = propertyId,
|
||||||
}.getOrNull()
|
status = "OPEN"
|
||||||
val checkedInBookings = checkedInBookingsResponse
|
)
|
||||||
?.body()
|
|
||||||
.orEmpty()
|
|
||||||
.takeIf { checkedInBookingsResponse?.isSuccessful == true }
|
|
||||||
.orEmpty()
|
|
||||||
val openBookings = openBookingsResponse
|
|
||||||
?.body()
|
|
||||||
.orEmpty()
|
|
||||||
.takeIf { openBookingsResponse?.isSuccessful == true }
|
|
||||||
.orEmpty()
|
|
||||||
val errorMessage = activeResult.exceptionOrNull()?.localizedMessage
|
val errorMessage = activeResult.exceptionOrNull()?.localizedMessage
|
||||||
?: when {
|
?: checkedInResult.exceptionOrNull()?.localizedMessage
|
||||||
checkedInBookingsResponse != null && !checkedInBookingsResponse.isSuccessful ->
|
?: openResult.exceptionOrNull()?.localizedMessage
|
||||||
"Load failed: ${checkedInBookingsResponse.code()}"
|
|
||||||
|
|
||||||
openBookingsResponse != null && !openBookingsResponse.isSuccessful ->
|
|
||||||
"Load failed: ${openBookingsResponse.code()}"
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
checkedInBookings = checkedInBookings,
|
|
||||||
openBookings = openBookings,
|
|
||||||
error = errorMessage
|
error = errorMessage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -79,6 +68,8 @@ class ActiveRoomStaysViewModel(
|
|||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
observeJob?.cancel()
|
observeJob?.cancel()
|
||||||
|
observeCheckedInJob?.cancel()
|
||||||
|
observeOpenJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeCache(propertyId: String) {
|
private fun observeCache(propertyId: String) {
|
||||||
@@ -96,4 +87,32 @@ class ActiveRoomStaysViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeBookingLists(propertyId: String) {
|
||||||
|
if (observeBookingListPropertyId == propertyId &&
|
||||||
|
observeCheckedInJob?.isActive == true &&
|
||||||
|
observeOpenJob?.isActive == true
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
observeCheckedInJob?.cancel()
|
||||||
|
observeOpenJob?.cancel()
|
||||||
|
observeBookingListPropertyId = propertyId
|
||||||
|
observeCheckedInJob = viewModelScope.launch {
|
||||||
|
bookingListRepository.observeByStatus(
|
||||||
|
propertyId = propertyId,
|
||||||
|
status = "CHECKED_IN"
|
||||||
|
).collect { bookings ->
|
||||||
|
_state.update { current -> current.copy(checkedInBookings = bookings) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
observeOpenJob = viewModelScope.launch {
|
||||||
|
bookingListRepository.observeByStatus(
|
||||||
|
propertyId = propertyId,
|
||||||
|
status = "OPEN"
|
||||||
|
).collect { bookings ->
|
||||||
|
_state.update { current -> current.copy(openBookings = bookings) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,12 +56,9 @@ import coil.ImageLoader
|
|||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.decode.SvgDecoder
|
import coil.decode.SvgDecoder
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
|
||||||
import com.android.trisolarispms.data.api.core.ApiConstants
|
import com.android.trisolarispms.data.api.core.ApiConstants
|
||||||
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
|
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
|
||||||
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
||||||
import com.android.trisolarispms.data.api.model.BookingCancelRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.BookingCheckOutRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||||
import com.android.trisolarispms.ui.booking.BookingDatePickerDialog
|
import com.android.trisolarispms.ui.booking.BookingDatePickerDialog
|
||||||
import com.android.trisolarispms.ui.booking.BookingDateTimeQuickEditorCard
|
import com.android.trisolarispms.ui.booking.BookingDateTimeQuickEditorCard
|
||||||
@@ -293,19 +290,16 @@ fun BookingDetailsTabsScreen(
|
|||||||
checkoutLoading.value = true
|
checkoutLoading.value = true
|
||||||
checkoutError.value = null
|
checkoutError.value = null
|
||||||
try {
|
try {
|
||||||
val response = ApiClient.create().checkOut(
|
val result = detailsViewModel.checkoutBooking(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
bookingId = bookingId,
|
bookingId = bookingId
|
||||||
body = BookingCheckOutRequest()
|
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (result.isSuccess) {
|
||||||
showCheckoutConfirm.value = false
|
showCheckoutConfirm.value = false
|
||||||
onBack()
|
onBack()
|
||||||
} else if (response.code() == 409) {
|
|
||||||
checkoutError.value =
|
|
||||||
extractApiErrorMessage(response) ?: "Checkout conflict"
|
|
||||||
} else {
|
} else {
|
||||||
checkoutError.value = "Checkout failed: ${response.code()}"
|
val message = result.exceptionOrNull()?.localizedMessage
|
||||||
|
checkoutError.value = message ?: "Checkout failed"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
checkoutError.value = e.localizedMessage ?: "Checkout failed"
|
checkoutError.value = e.localizedMessage ?: "Checkout failed"
|
||||||
@@ -364,16 +358,16 @@ fun BookingDetailsTabsScreen(
|
|||||||
cancelLoading.value = true
|
cancelLoading.value = true
|
||||||
cancelError.value = null
|
cancelError.value = null
|
||||||
try {
|
try {
|
||||||
val response = ApiClient.create().cancelBooking(
|
val result = detailsViewModel.cancelBooking(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
bookingId = bookingId,
|
bookingId = bookingId
|
||||||
body = BookingCancelRequest()
|
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (result.isSuccess) {
|
||||||
showCancelConfirm.value = false
|
showCancelConfirm.value = false
|
||||||
onBack()
|
onBack()
|
||||||
} else {
|
} else {
|
||||||
cancelError.value = "Cancel failed: ${response.code()}"
|
val message = result.exceptionOrNull()?.localizedMessage
|
||||||
|
cancelError.value = message ?: "Cancel failed"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
cancelError.value = e.localizedMessage ?: "Cancel failed"
|
cancelError.value = e.localizedMessage ?: "Cancel failed"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import androidx.lifecycle.AndroidViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.core.ApiConstants
|
import com.android.trisolarispms.data.api.core.ApiConstants
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingCancelRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingCheckOutRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||||
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
||||||
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
@@ -163,6 +165,41 @@ class BookingDetailsViewModel(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun checkoutBooking(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String
|
||||||
|
): Result<Unit> = runCatching {
|
||||||
|
val response = ApiClient.create().checkOut(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = BookingCheckOutRequest()
|
||||||
|
)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
val message = if (response.code() == 409) {
|
||||||
|
extractApiErrorMessage(response) ?: "Checkout conflict"
|
||||||
|
} else {
|
||||||
|
"Checkout failed: ${response.code()}"
|
||||||
|
}
|
||||||
|
throw IllegalStateException(message)
|
||||||
|
}
|
||||||
|
syncBookingCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun cancelBooking(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String
|
||||||
|
): Result<Unit> = runCatching {
|
||||||
|
val response = ApiClient.create().cancelBooking(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = BookingCancelRequest()
|
||||||
|
)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IllegalStateException("Cancel failed: ${response.code()}")
|
||||||
|
}
|
||||||
|
syncBookingCaches(propertyId = propertyId, bookingId = bookingId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
observeJob?.cancel()
|
observeJob?.cancel()
|
||||||
@@ -196,4 +233,12 @@ class BookingDetailsViewModel(
|
|||||||
startStream(propertyId, bookingId)
|
startStream(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun syncBookingCaches(propertyId: String, bookingId: String) {
|
||||||
|
roomStayRepository.refresh(propertyId = propertyId).getOrThrow()
|
||||||
|
bookingRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
).getOrThrow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.model.BookingRoomStayCheckOutRequest
|
import com.android.trisolarispms.data.api.model.BookingRoomStayCheckOutRequest
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -22,6 +23,9 @@ class BookingRoomStaysViewModel(
|
|||||||
private val repository = ActiveRoomStayRepository(
|
private val repository = ActiveRoomStayRepository(
|
||||||
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
||||||
)
|
)
|
||||||
|
private val bookingRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
private var observeJob: Job? = null
|
private var observeJob: Job? = null
|
||||||
private var observeKey: String? = null
|
private var observeKey: String? = null
|
||||||
|
|
||||||
@@ -62,7 +66,14 @@ class BookingRoomStaysViewModel(
|
|||||||
)
|
)
|
||||||
when {
|
when {
|
||||||
response.isSuccessful -> {
|
response.isSuccessful -> {
|
||||||
|
val refreshResult = repository.refresh(propertyId = propertyId)
|
||||||
|
if (refreshResult.isFailure) {
|
||||||
repository.removeFromCache(propertyId = propertyId, roomStayId = roomStayId)
|
repository.removeFromCache(propertyId = propertyId, roomStayId = roomStayId)
|
||||||
|
}
|
||||||
|
bookingRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
)
|
||||||
_state.update { current ->
|
_state.update { current ->
|
||||||
current.copy(
|
current.copy(
|
||||||
checkingOutRoomStayId = null,
|
checkingOutRoomStayId = null,
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
package com.android.trisolarispms.ui.roomstay
|
package com.android.trisolarispms.ui.roomstay
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingBulkCheckInStayRequest
|
import com.android.trisolarispms.data.api.model.BookingBulkCheckInStayRequest
|
||||||
import com.android.trisolarispms.core.viewmodel.launchRequest
|
import com.android.trisolarispms.core.viewmodel.launchRequest
|
||||||
|
import com.android.trisolarispms.data.local.booking.BookingDetailsRepository
|
||||||
|
import com.android.trisolarispms.data.local.core.LocalDatabaseProvider
|
||||||
|
import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository
|
||||||
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
|
||||||
|
|
||||||
class ManageRoomStayRatesViewModel : ViewModel() {
|
class ManageRoomStayRatesViewModel(
|
||||||
|
application: Application
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
private val _state = MutableStateFlow(ManageRoomStayRatesState())
|
private val _state = MutableStateFlow(ManageRoomStayRatesState())
|
||||||
val state: StateFlow<ManageRoomStayRatesState> = _state
|
val state: StateFlow<ManageRoomStayRatesState> = _state
|
||||||
|
private val roomStayRepository = ActiveRoomStayRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao()
|
||||||
|
)
|
||||||
|
private val bookingRepository = BookingDetailsRepository(
|
||||||
|
dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao()
|
||||||
|
)
|
||||||
|
|
||||||
fun setItems(items: List<ManageRoomStaySelection>) {
|
fun setItems(items: List<ManageRoomStaySelection>) {
|
||||||
val mapped = items.map { item ->
|
val mapped = items.map { item ->
|
||||||
@@ -102,6 +114,11 @@ class ManageRoomStayRatesViewModel : ViewModel() {
|
|||||||
body = BookingBulkCheckInRequest(stays = stays)
|
body = BookingBulkCheckInRequest(stays = stays)
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
|
roomStayRepository.refresh(propertyId = propertyId)
|
||||||
|
bookingRepository.refreshBookingDetails(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId
|
||||||
|
)
|
||||||
_state.update { it.copy(isLoading = false, error = null) }
|
_state.update { it.copy(isLoading = false, error = null) }
|
||||||
onDone()
|
onDone()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user