From c604fd17b1802396ad94c211fc03caa55772e879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Fri, 15 May 2026 17:16:35 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Make=20day=E2=86=94week=20price=20conversio?= =?UTF-8?q?n=20exact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SubscriptionPeriod converted between days and weeks via a 52/365 factor. But 52 weeks is only 364 days, so a 7-day period resolved to ~0.997 weeks instead of exactly 1 — inflating a 7-day product's weekly price and skewing a week product's daily price (e.g. a $7.00/week product reported $0.99/day instead of the exact $1.00). 7 days is exactly 1 week. Use the exact ratio in both directions: - SubscriptionPeriod.pricePerDay .week: 365/52 -> 7 - SubscriptionPeriod.pricePerWeek .day: 52/365 -> 1/7 - RawStoreProduct.periodsPerUnit (trial path): the same two day↔week entries. Month↔year was already exact (12); week/month and week/year stay as conventional approximations — those aren't whole-number relationships. Mirrors the same fix in the iOS SDK and the web editor so all three agree. Adds two regression tests (a 7-day period's weekly price equals the price; a weekly product's daily price divides by exactly 7) in the active test region — the existing period-price tests are still inside the disabled "TODO: Re-enable in CI" block. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../abstractions/product/RawStoreProduct.kt | 8 +++++--- .../product/SubscriptionPeriod.kt | 9 +++++++-- .../product/SubscriptionPeriodUnitTest.kt | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt index 28a57c77e..6be6f1dbb 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt @@ -684,7 +684,8 @@ class RawStoreProduct( SubscriptionPeriod.Unit.day -> { when (trialSubscriptionPeriod?.unit) { SubscriptionPeriod.Unit.day -> BigDecimal(1) - SubscriptionPeriod.Unit.week -> BigDecimal(365).divide(BigDecimal(52), 6, RoundingMode.DOWN) + // 7 days per week exactly — not 365/52. + SubscriptionPeriod.Unit.week -> BigDecimal(7) SubscriptionPeriod.Unit.month -> BigDecimal(365).divide(BigDecimal(12), 6, RoundingMode.DOWN) SubscriptionPeriod.Unit.year -> BigDecimal(365) else -> BigDecimal.ZERO @@ -693,9 +694,10 @@ class RawStoreProduct( SubscriptionPeriod.Unit.week -> { when (trialSubscriptionPeriod?.unit) { + // A day is exactly 1/7 of a week — not 52/365. SubscriptionPeriod.Unit.day -> - BigDecimal(52).divide( - BigDecimal(365), + BigDecimal.ONE.divide( + BigDecimal(7), 6, RoundingMode.DOWN, ) diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriod.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriod.kt index ab937fd12..328dda1f3 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriod.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriod.kt @@ -106,7 +106,10 @@ data class SubscriptionPeriod( val periodsPerDay: BigDecimal = when (this.unit) { Unit.day -> BigDecimal.ONE - Unit.week -> BigDecimal(365).divide(BigDecimal(52), calculationScale, roundingMode) + // A week is exactly 7 days. Don't route through 365/52 — 52 + // weeks is only 364 days, so that approximation makes a + // weekly product's daily price a cent off. + Unit.week -> BigDecimal(7) Unit.month -> BigDecimal(365).divide(BigDecimal(12), calculationScale, roundingMode) Unit.year -> BigDecimal(365) } * BigDecimal(this.value) @@ -117,7 +120,9 @@ data class SubscriptionPeriod( fun pricePerWeek(price: BigDecimal): BigDecimal { val periodsPerWeek: BigDecimal = when (this.unit) { - Unit.day -> BigDecimal(52).divide(BigDecimal(365), calculationScale, roundingMode) + // A day is exactly 1/7 of a week. The old 52/365 made a 7-day + // product resolve to 0.997 weeks, inflating its weekly price. + Unit.day -> BigDecimal.ONE.divide(BigDecimal(7), calculationScale, roundingMode) Unit.week -> BigDecimal.ONE Unit.month -> BigDecimal(52).divide(BigDecimal(12), calculationScale, roundingMode) Unit.year -> BigDecimal(52) diff --git a/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt b/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt index b4e568bce..409b064f4 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt @@ -30,6 +30,26 @@ class SubscriptionPeriodUnitTest { println(res) assert(res == SubscriptionPeriod(31, SubscriptionPeriod.Unit.day)) } + + // A 7-day period is exactly one week, so its weekly price must equal the + // price. Before the day↔week fix this divided by (7 × 52/365) ≈ 0.997, + // inflating it (e.g. 6.99 → 7.00). + @Test + fun sevenDayPeriod_weeklyPriceEqualsPrice() { + val period = SubscriptionPeriod(7, SubscriptionPeriod.Unit.day) + val pricePerWeek = period.pricePerWeek(BigDecimal(6.99)) + assert(pricePerWeek.compareTo(truncateDecimal(BigDecimal(6.99), 2)) == 0) + } + + // A week is exactly 7 days. Before the fix, pricePerDay for a week product + // divided by 365/52 ≈ 7.019, so a $7.00/week product reported $0.99/day + // instead of the exact $1.00. + @Test + fun weeklyPeriod_dailyPriceDividesBySeven() { + val period = SubscriptionPeriod(1, SubscriptionPeriod.Unit.week) + val pricePerDay = period.pricePerDay(BigDecimal(7.00)) + assert(pricePerDay.compareTo(truncateDecimal(BigDecimal(1.00), 2)) == 0) + } /* TODO: Re-enable these in CI @Test fun singleDaily_isCorrect() { From e7afc37a1a1b6715c80553ff248993c576aac7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Fri, 15 May 2026 17:27:59 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Use=20exact=20BigDecimal=20and=20assertEqua?= =?UTF-8?q?ls=20in=20day=E2=86=94week=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues in the new regression tests: - BigDecimal(6.99) calls the double constructor, which stores the inexact IEEE-754 value rather than exactly 6.99 — the two sides of the comparison could resolve to different values. Use the String constructor for exact decimals. - Kotlin's assert() is compiled to a no-op unless the JVM runs with -ea, so the tests could silently pass even on a regression. Switch to JUnit's assertEquals, which always evaluates. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../product/SubscriptionPeriodUnitTest.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt b/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt index 409b064f4..5195123d2 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/abstractions/product/SubscriptionPeriodUnitTest.kt @@ -1,5 +1,6 @@ package com.superwall.sdk.store.abstractions.product +import org.junit.Assert.assertEquals import org.junit.Test import java.math.BigDecimal @@ -34,11 +35,13 @@ class SubscriptionPeriodUnitTest { // A 7-day period is exactly one week, so its weekly price must equal the // price. Before the day↔week fix this divided by (7 × 52/365) ≈ 0.997, // inflating it (e.g. 6.99 → 7.00). + // BigDecimal(String) — the double constructor would store an inexact + // binary value. assertEquals — Kotlin's assert() is a no-op without -ea. @Test fun sevenDayPeriod_weeklyPriceEqualsPrice() { val period = SubscriptionPeriod(7, SubscriptionPeriod.Unit.day) - val pricePerWeek = period.pricePerWeek(BigDecimal(6.99)) - assert(pricePerWeek.compareTo(truncateDecimal(BigDecimal(6.99), 2)) == 0) + val pricePerWeek = period.pricePerWeek(BigDecimal("6.99")) + assertEquals(0, pricePerWeek.compareTo(BigDecimal("6.99"))) } // A week is exactly 7 days. Before the fix, pricePerDay for a week product @@ -47,8 +50,8 @@ class SubscriptionPeriodUnitTest { @Test fun weeklyPeriod_dailyPriceDividesBySeven() { val period = SubscriptionPeriod(1, SubscriptionPeriod.Unit.week) - val pricePerDay = period.pricePerDay(BigDecimal(7.00)) - assert(pricePerDay.compareTo(truncateDecimal(BigDecimal(1.00), 2)) == 0) + val pricePerDay = period.pricePerDay(BigDecimal("7.00")) + assertEquals(0, pricePerDay.compareTo(BigDecimal("1.00"))) } /* TODO: Re-enable these in CI @Test