mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +00:00
Heal InAppPaymentSubscriber currency if we have a payment with a matching subscriber id.
This commit is contained in:
committed by
Greyson Parrelli
parent
7a696f9a62
commit
ea87108def
@@ -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)!!
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user