Heal InAppPaymentSubscriber currency if we have a payment with a matching subscriber id.

This commit is contained in:
Alex Hart
2024-06-18 09:19:25 -03:00
committed by Greyson Parrelli
parent 7a696f9a62
commit ea87108def
4 changed files with 240 additions and 4 deletions

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.testing.assertIs
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.math.BigDecimal
import java.util.Currency
@RunWith(AndroidJUnit4::class)
class FixInAppCurrencyIfAbleTest {
@get:Rule
val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
@Test
fun givenNoSubscribers_whenIMigrate_thenIDoNothing() {
migrate()
}
@Test
fun givenASubscriberButNoPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMismatchedPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
val otherSubscriber = insertSubscriber("EUR")
insertPayment(otherSubscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndPaymentWithNoSubscriber_whenIMigrate_thenDoNothing() {
val subscriber = insertSubscriber("USD")
insertPayment(null)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMatchingPayment_whenIMigrate_thenUpdateCurrencyCode() {
val subscriber = insertSubscriber("USD")
insertPayment(subscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs "USD"
}
@Test
fun givenASupercededSubscriber_whenIMigrate_thenIDoNothing() {
val oldSubscriber = insertSubscriber("USD")
insertPayment(oldSubscriber)
clearCurrencyCode(oldSubscriber)
insertSubscriber("USD")
migrate()
}
private fun migrate() {
V236_FixInAppSubscriberCurrencyIfAble.migrate(
context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
db = SignalDatabase.rawDatabase,
oldVersion = 0,
newVersion = 0
)
}
private fun insertSubscriber(currencyCode: String): InAppPaymentSubscriberRecord {
val record = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = Currency.getInstance(currencyCode),
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)
return record
}
private fun clearCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord) {
SignalDatabase.rawDatabase.update(InAppPaymentSubscriberTable.TABLE_NAME)
.values(InAppPaymentSubscriberTable.CURRENCY_CODE to "")
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
}
private fun getCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord): String {
return SignalDatabase.rawDatabase.select(InAppPaymentSubscriberTable.CURRENCY_CODE)
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
.readToSingleObject { it.requireNonNullString(InAppPaymentSubscriberTable.CURRENCY_CODE) }!!
}
private fun insertPayment(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord?): InAppPaymentTable.InAppPayment {
val id = SignalDatabase.inAppPayments.insert(
type = InAppPaymentType.RECURRING_DONATION,
state = InAppPaymentTable.State.END,
subscriberId = inAppPaymentSubscriberRecord?.subscriberId,
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
amount = FiatValue(
currencyCode = inAppPaymentSubscriberRecord?.currency?.currencyCode ?: "USD",
amount = BigDecimal.ONE.toDecimalValue()
),
level = 200,
paymentMethodType = inAppPaymentSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
)
)
return SignalDatabase.inAppPayments.getById(id)!!
}
}

View File

@@ -45,10 +45,12 @@ class InAppPaymentSubscriberTable(
private const val ID = "_id"
/** The serialized subscriber id */
private const val SUBSCRIBER_ID = "subscriber_id"
@VisibleForTesting
const val SUBSCRIBER_ID = "subscriber_id"
/** The currency code for this subscriber id */
private const val CURRENCY_CODE = "currency_code"
@VisibleForTesting
const val CURRENCY_CODE = "currency_code"
/** The type of subscription used by this subscriber id */
private const val TYPE = "type"

View File

@@ -93,6 +93,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V232_CreateInAppPay
import org.thoughtcrime.securesms.database.helpers.migration.V233_FixInAppPaymentTableDefaultNotifiedValue
import org.thoughtcrime.securesms.database.helpers.migration.V234_ThumbnailRestoreStateColumn
import org.thoughtcrime.securesms.database.helpers.migration.V235_AttachmentUuidColumn
import org.thoughtcrime.securesms.database.helpers.migration.V236_FixInAppSubscriberCurrencyIfAble
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@@ -188,10 +189,11 @@ object SignalDatabaseMigrations {
232 to V232_CreateInAppPaymentTable,
233 to V233_FixInAppPaymentTableDefaultNotifiedValue,
234 to V234_ThumbnailRestoreStateColumn,
235 to V235_AttachmentUuidColumn
235 to V235_AttachmentUuidColumn,
236 to V236_FixInAppSubscriberCurrencyIfAble
)
const val DATABASE_VERSION = 235
const val DATABASE_VERSION = 236
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import androidx.core.content.contentValuesOf
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.signal.core.util.logging.Log
import org.signal.core.util.update
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
/**
* Fixes a bug in storage sync where we could end up overwriting a subscriber's currency code with
* an empty string. This can't happen anymore, as we now require a Currency on the InAppPaymentSubscriberRecord
* instead of just a currency code string.
*
* If a subscriber has a null or empty currency code, we try to load the code from the
* in app payments table. We utilize CONFLICT_IGNORE because if there's already a new subscriber id
* created, we don't want to impact it.
*
* Because the data column is a protobuf encoded blob, we cannot do a raw query here.
*/
@Suppress("ClassName")
object V236_FixInAppSubscriberCurrencyIfAble : SignalDatabaseMigration {
private val TAG = Log.tag(V236_FixInAppSubscriberCurrencyIfAble::class)
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
val subscriberIds: List<String> = db.query(
"in_app_payment_subscriber",
arrayOf("subscriber_id"),
"currency_code = ?",
arrayOf(""),
null,
null,
"_id DESC"
).use { cursor ->
val ids = mutableListOf<String>()
val columnIndex = cursor.getColumnIndexOrThrow("subscriber_id")
while (cursor.moveToNext()) {
ids.add(cursor.getString(columnIndex))
}
ids
}
for (id in subscriberIds) {
val currencyCode: String? = db.query(
"in_app_payment",
arrayOf("data"),
"subscriber_id = ?",
arrayOf(id),
null,
null,
"inserted_at DESC",
"1"
).use { cursor ->
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndexOrThrow("data")
val rawData = cursor.getBlob(columnIndex)
val data = InAppPaymentData.ADAPTER.decode(rawData)
data.amount?.currencyCode
} else {
null
}
}
if (currencyCode != null) {
Log.d(TAG, "Found and attempting to heal subscriber of currency $currencyCode")
db.update(
"in_app_payment_subscriber",
SQLiteDatabase.CONFLICT_IGNORE,
contentValuesOf("currency_code" to currencyCode),
"subscriber_id = ?",
arrayOf(id)
)
}
}
}
}