From 20172fa9d6e8be97f34cbcbb150b4523b817626a Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 3 Sep 2026 14:40:25 -0400 Subject: [PATCH] fix(flipcash): treat an unset bill_exchange_data_timeout as absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bill_exchange_data_timeout is a google.protobuf.Duration, so an unset field still yields the default instance and the mapper turned that into 0.seconds rather than null. Guard on hasBillExchangeDataTimeout(), which is how every other optional message-typed field here is read — see UserProfileMapper's profilePicture and joinTs, and ChatMetadataMapper's lastMessage. The two values are not interchangeable downstream. VerifiedState.exchangeDataFor substitutes a 15 minute default for null but returns null outright for a timeout of zero, so every user whose flags omitted the field was treated as having verified exchange data permanently expired: GiveBillTransactor.start refetched the rate on every bill instead of using the state it already held. No consumer needed adjusting. Field.BillExchangeDataTimeout already encodes null as -1 and formats it as "None", and CachedFlags round-trips it as a nullable Long. --- .../flipcash/services/internal/domain/UserFlagsMapper.kt | 4 +++- .../kotlin/com/flipcash/services/models/UserFlags.kt | 3 +++ .../services/internal/domain/UserFlagsMapperTest.kt | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt index 0156fce551..c55b96d1d4 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt @@ -22,7 +22,9 @@ internal class UserFlagsMapper @Inject constructor(): preferredOnRampProvider = from.preferredOnRampProvider.toDomain().takeIf { it != OnRampProvider.Unknown }, supportedOnRampProviders = from.supportedOnRampProvidersList.map { it.toDomain() }, minimumVersion = from.minBuildNumber, - billExchangeDataTimeout = from.billExchangeDataTimeout.seconds.toDuration(DurationUnit.SECONDS), + billExchangeDataTimeout = if (from.hasBillExchangeDataTimeout()) { + from.billExchangeDataTimeout.seconds.toDuration(DurationUnit.SECONDS) + } else null, newCurrencyPurchaseAmount = Fiat(quarks = from.newCurrencyPurchaseAmount), newCurrencyFeeAmount = Fiat(quarks = from.newCurrencyFeeAmount), withdrawalFeeAmount = Fiat(quarks = from.withdrawalFeeAmount), diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserFlags.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserFlags.kt index 6b4f46e725..fcdac22492 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserFlags.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserFlags.kt @@ -12,6 +12,9 @@ data class UserFlags( val preferredOnRampProvider: OnRampProvider?, val supportedOnRampProviders: List, val minimumVersion: Int?, + // How long a verified exchange rate stays usable for a bill. Absent when the + // server hasn't set a timeout (not the same as a zero-length one, which + // disables verified exchange data entirely). val billExchangeDataTimeout: Duration?, val newCurrencyPurchaseAmount: Fiat, val newCurrencyFeeAmount: Fiat, diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt index 12f85c3ed2..b48e78de53 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt @@ -116,6 +116,15 @@ class UserFlagsMapperTest { assertEquals(30.seconds, result.billExchangeDataTimeout) } + @Test + fun `bill exchange data timeout is null when unset, not zero`() { + val proto = userFlags { } + + val result = mapper.map(proto) + + assertNull(result.billExchangeDataTimeout) + } + @Test fun `maps new currency purchase amount`() { val proto = userFlags {