Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,15 @@ class TipPaymentDelegate @Inject constructor(
/**
* This amount in [target], routed through USD — the only rate every currency has. Null when
* either leg has no rate, which is the caller's cue to fall back rather than mix currencies.
*
* Rounds up because the result is a fee floor: half-up can state one below the fee, which the
* client then accepts and the server denies.
*/
private fun Fiat.inCurrency(target: CurrencyCode): Fiat? {
if (currencyCode == target) return this
val usd = exchange.rateToUsd(currencyCode)?.let { convertingTo(it) } ?: return null
if (target == CurrencyCode.USD) return usd.rounded()
return exchange.rateFor(target)?.let { usd.convertingTo(it).rounded() }
if (target == CurrencyCode.USD) return usd.roundedUp()
return exchange.rateFor(target)?.let { usd.convertingTo(it).roundedUp() }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,48 @@ class TipPaymentDelegateTest {
assertEquals(5.0, min!!.toDouble())
}

@Test
fun `minimumToOpenDmWith rounds the converted fee up so it can't be undercut`() = runTest {
every { userFlags.resolvedFlags } returns usdPresets()
every { exchange.observePreferredRate() } returns flowOf(Rate(fx = 1.0, currency = CurrencyCode.USD))
// 600 INR at 95.95 INR/USD is 6.2533... USD — half-up rounding states it as $6.25, which is
// short of the fee (6.25 * 95.95 = 599.69 < 600). Rounding up to $6.26 keeps the stated floor
// from ever understating the fee.
every { exchange.rateToUsd(CurrencyCode.INR) } returns Rate(fx = 1 / 95.95, currency = CurrencyCode.USD)

val recipient = recipient(Fiat(600.0, CurrencyCode.INR))
val min = buildDelegate().minimumToOpenDmWith(recipient).first { it != null }

assertEquals(6.26, min!!.toDouble())
assertTrue(min.toDouble() * 95.95 >= 600.0)
}

@Test
fun `minimumToOpenDmWith rounds another currency's converted fee up too`() = runTest {
every { userFlags.resolvedFlags } returns usdPresets()
every { exchange.observePreferredRate() } returns flowOf(Rate(fx = 1.0, currency = CurrencyCode.USD))
// 1500 NGN at 1326 NGN/USD is 1.1312... USD, which rounds up to $1.14 (not the half-up $1.13).
every { exchange.rateToUsd(CurrencyCode.NGN) } returns Rate(fx = 1 / 1326.0, currency = CurrencyCode.USD)

val recipient = recipient(Fiat(1500.0, CurrencyCode.NGN))
val min = buildDelegate().minimumToOpenDmWith(recipient).first { it != null }

assertEquals(1.14, min!!.toDouble())
}

@Test
fun `minimumToOpenDmWith does not bump a fee that converts exactly`() = runTest {
every { userFlags.resolvedFlags } returns usdPresets()
every { exchange.observePreferredRate() } returns flowOf(Rate(fx = 1.0, currency = CurrencyCode.USD))
// 600 INR at an even 100 INR/USD is exactly $6.00 — rounding up must not bump an exact value.
every { exchange.rateToUsd(CurrencyCode.INR) } returns Rate(fx = 1 / 100.0, currency = CurrencyCode.USD)

val recipient = recipient(Fiat(600.0, CurrencyCode.INR))
val min = buildDelegate().minimumToOpenDmWith(recipient).first { it != null }

assertEquals(6.00, min!!.toDouble())
}

@Test
fun `minimumTipFor drops to the system minimum once a chat exists`() = runTest {
every { userFlags.resolvedFlags } returns usdPresets()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ data class Fiat(
)
}

/**
* This value rounded UP (ceiling) to [decimalPlaces], instead of [rounded]'s half-up. Use this
* only where understating the value is unsafe — e.g. a converted fee floor, which must never
* round down below the fee it stands in for. A value that already lands exactly on
* [decimalPlaces] is left unchanged.
*/
fun roundedUp(decimalPlaces: Int = 2): Fiat {
return Fiat(
fiat = decimalValue.roundTo(decimalPlaces, RoundingMode.UP),
currencyCode = currencyCode
)
}

// Formatting
fun formatted(
rule: FormattingRule = FormattingRule.None,
Expand Down
Loading