feat: parse and store createdAt from time in PaymentRecord
This commit is contained in:
@@ -166,7 +166,9 @@ data class PaymentRecord(
|
|||||||
@SerializedName("bolt11") val bolt11: String?,
|
@SerializedName("bolt11") val bolt11: String?,
|
||||||
@SerializedName("pending") val pending: Boolean,
|
@SerializedName("pending") val pending: Boolean,
|
||||||
@SerializedName("preimage") val preimage: String?,
|
@SerializedName("preimage") val preimage: String?,
|
||||||
@SerializedName("extra") val extra: PaymentExtra?
|
@SerializedName("extra") val extra: PaymentExtra?,
|
||||||
|
// Not from the API — populated only when loaded from Room via toDomain()
|
||||||
|
val createdAt: Long? = null
|
||||||
) {
|
) {
|
||||||
val amountSat: Long get() = kotlin.math.abs(amountMsat) / 1000L
|
val amountSat: Long get() = kotlin.math.abs(amountMsat) / 1000L
|
||||||
val feeSat: Long get() = kotlin.math.abs(feeMsat) / 1000L
|
val feeSat: Long get() = kotlin.math.abs(feeMsat) / 1000L
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
entities = [
|
entities = [
|
||||||
PaymentRecordEntity::class,
|
PaymentRecordEntity::class,
|
||||||
LnurlCacheEntity::class,
|
LnurlCacheEntity::class,
|
||||||
NodeAliasCacheEntity::class, // ← added
|
NodeAliasCacheEntity::class,
|
||||||
],
|
],
|
||||||
version = 3, // ← bumped
|
version = 4, // ← bumped
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(PaymentConverters::class)
|
@TypeConverters(PaymentConverters::class)
|
||||||
@@ -22,7 +22,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
abstract fun paymentDao(): PaymentDao
|
abstract fun paymentDao(): PaymentDao
|
||||||
abstract fun lnurlCacheDao(): LnurlCacheDao
|
abstract fun lnurlCacheDao(): LnurlCacheDao
|
||||||
abstract fun nodeAliasCacheDao(): NodeAliasCacheDao // ← added
|
abstract fun nodeAliasCacheDao(): NodeAliasCacheDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile private var INSTANCE: AppDatabase? = null
|
@Volatile private var INSTANCE: AppDatabase? = null
|
||||||
@@ -42,7 +42,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val MIGRATION_2_3 = object : Migration(2, 3) { // ← added
|
private val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
@@ -57,6 +57,19 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("ALTER TABLE payment_records ADD COLUMN createdAt INTEGER")
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
UPDATE payment_records
|
||||||
|
SET createdAt = CAST(time AS INTEGER)
|
||||||
|
WHERE CAST(time AS INTEGER) > 1000000000
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getInstance(context: Context): AppDatabase =
|
fun getInstance(context: Context): AppDatabase =
|
||||||
INSTANCE ?: synchronized(this) {
|
INSTANCE ?: synchronized(this) {
|
||||||
INSTANCE ?: Room.databaseBuilder(
|
INSTANCE ?: Room.databaseBuilder(
|
||||||
@@ -64,7 +77,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"gudari_wallet.db"
|
"gudari_wallet.db"
|
||||||
)
|
)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) // ← added
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) // ← added
|
||||||
.build()
|
.build()
|
||||||
.also { INSTANCE = it }
|
.also { INSTANCE = it }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ data class PaymentRecordEntity(
|
|||||||
val feeMsat: Long,
|
val feeMsat: Long,
|
||||||
val memo: String?,
|
val memo: String?,
|
||||||
val time: String,
|
val time: String,
|
||||||
|
val createdAt: Long? = null,
|
||||||
val status: String,
|
val status: String,
|
||||||
val bolt11: String?,
|
val bolt11: String?,
|
||||||
val pending: Boolean,
|
val pending: Boolean,
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
package com.bitcointxoko.gudariwallet.data.db
|
package com.bitcointxoko.gudariwallet.data.db
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||||
import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity
|
import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity
|
||||||
|
|
||||||
|
private const val TAG = "PaymentMappers"
|
||||||
|
|
||||||
|
|
||||||
|
// ── Parse helper — runs once at write time, never again ──────────────────────
|
||||||
|
|
||||||
|
private fun parseCreatedAt(time: String): Long? {
|
||||||
|
val result = time.toLongOrNull()
|
||||||
|
?: runCatching {
|
||||||
|
java.time.OffsetDateTime.parse(time).toEpochSecond()
|
||||||
|
}.getOrNull()
|
||||||
|
if (result == null) {
|
||||||
|
Log.w(TAG, "parseCreatedAt: could not parse time string \"$time\" — createdAt will be null")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PaymentRecord → PaymentRecordEntity ──────────────────────────────────────
|
||||||
|
|
||||||
fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity =
|
fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity =
|
||||||
PaymentRecordEntity(
|
PaymentRecordEntity(
|
||||||
checkingId = checkingId,
|
checkingId = checkingId,
|
||||||
@@ -11,6 +30,7 @@ fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity =
|
|||||||
feeMsat = feeMsat,
|
feeMsat = feeMsat,
|
||||||
memo = memo,
|
memo = memo,
|
||||||
time = time,
|
time = time,
|
||||||
|
createdAt = parseCreatedAt(time), // ← NEW
|
||||||
status = status,
|
status = status,
|
||||||
bolt11 = bolt11,
|
bolt11 = bolt11,
|
||||||
pending = pending,
|
pending = pending,
|
||||||
@@ -19,6 +39,8 @@ fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity =
|
|||||||
savedAt = savedAt
|
savedAt = savedAt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── PaymentRecordEntity → PaymentRecord ──────────────────────────────────────
|
||||||
|
|
||||||
fun PaymentRecordEntity.toDomain(): PaymentRecord =
|
fun PaymentRecordEntity.toDomain(): PaymentRecord =
|
||||||
PaymentRecord(
|
PaymentRecord(
|
||||||
checkingId = checkingId,
|
checkingId = checkingId,
|
||||||
@@ -27,6 +49,7 @@ fun PaymentRecordEntity.toDomain(): PaymentRecord =
|
|||||||
feeMsat = feeMsat,
|
feeMsat = feeMsat,
|
||||||
memo = memo,
|
memo = memo,
|
||||||
time = time,
|
time = time,
|
||||||
|
createdAt = createdAt, // ← NEW
|
||||||
status = status,
|
status = status,
|
||||||
bolt11 = bolt11,
|
bolt11 = bolt11,
|
||||||
pending = pending,
|
pending = pending,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bitcointxoko.gudariwallet.ui
|
|||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -493,8 +494,11 @@ private fun PaymentRow(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
|
val formattedTime = remember(payment.createdAt ?: payment.time) {
|
||||||
|
formatTimestamp(payment.createdAt, payment.time)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = formatTimestamp(payment.time),
|
text = formattedTime,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -702,7 +706,7 @@ private fun PaymentDetailContent(
|
|||||||
// ── Basic info ───────────────────────────────────────────────────────
|
// ── Basic info ───────────────────────────────────────────────────────
|
||||||
item {
|
item {
|
||||||
DetailSection(title = "Details") {
|
DetailSection(title = "Details") {
|
||||||
DetailRow("Date", formatTimestamp(payment.time))
|
DetailRow("Date", formatTimestamp(payment.createdAt, payment.time))
|
||||||
DetailRow("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "—")
|
DetailRow("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "—")
|
||||||
if (payment.extra?.tag != null) {
|
if (payment.extra?.tag != null) {
|
||||||
DetailRow("Type", payment.extra.tag)
|
DetailRow("Type", payment.extra.tag)
|
||||||
@@ -897,22 +901,23 @@ private fun StatusChip(status: String) {
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun formatTimestamp(time: String): String {
|
private fun formatTimestamp(createdAt: Long?, rawTime: String): String {
|
||||||
|
Log.d("TimestampDebug", "createdAt=$createdAt rawTime=$rawTime")
|
||||||
|
val formatter = java.time.format.DateTimeFormatter
|
||||||
|
.ofPattern("dd MMM yyyy, HH:mm")
|
||||||
|
.withZone(java.time.ZoneId.systemDefault())
|
||||||
|
if (createdAt != null && createdAt > 1_000_000_000L) { // sanity check
|
||||||
|
return formatter.format(java.time.Instant.ofEpochSecond(createdAt))
|
||||||
|
}
|
||||||
|
// Fallback: only reached for payments not yet written through Room
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val epochSeconds = time.toLong()
|
formatter.format(java.time.Instant.ofEpochSecond(rawTime.toLong()))
|
||||||
java.time.format.DateTimeFormatter
|
|
||||||
.ofPattern("dd MMM yyyy, HH:mm")
|
|
||||||
.withZone(java.time.ZoneId.systemDefault())
|
|
||||||
.format(java.time.Instant.ofEpochSecond(epochSeconds))
|
|
||||||
}.recoverCatching {
|
}.recoverCatching {
|
||||||
val odt = java.time.OffsetDateTime.parse(time)
|
formatter.format(java.time.OffsetDateTime.parse(rawTime))
|
||||||
java.time.format.DateTimeFormatter
|
}.getOrDefault(rawTime)
|
||||||
.ofPattern("dd MMM yyyy, HH:mm")
|
|
||||||
.withZone(java.time.ZoneId.systemDefault())
|
|
||||||
.format(odt)
|
|
||||||
}.getOrDefault(time)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AmountWithFiatColumn(
|
private fun AmountWithFiatColumn(
|
||||||
amountSat : Long,
|
amountSat : Long,
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ class HistoryViewModel(
|
|||||||
if (toMerge.isNotEmpty()) {
|
if (toMerge.isNotEmpty()) {
|
||||||
paymentCache.mergePayments(toMerge)
|
paymentCache.mergePayments(toMerge)
|
||||||
totalNew += toMerge.size
|
totalNew += toMerge.size
|
||||||
Log.d(TAG, "PAYMENTS [SYNC PAGE ] page $pagesChecked — ${toMerge.size} new payments merged")
|
val withTimestamp = toMerge.count { it.time.toLongOrNull() != null }
|
||||||
|
Log.d(TAG, "PAYMENTS [SYNC PAGE ] page $pagesChecked — ${toMerge.size} new payments merged ($withTimestamp with numeric timestamp)")
|
||||||
}
|
}
|
||||||
|
|
||||||
val reachedOverlap = overlapIndex != -1
|
val reachedOverlap = overlapIndex != -1
|
||||||
|
|||||||
Reference in New Issue
Block a user