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"
|
private const val ID = "_id"
|
||||||
|
|
||||||
/** The serialized subscriber 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 */
|
/** 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 */
|
/** The type of subscription used by this subscriber id */
|
||||||
private const val TYPE = "type"
|
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.V233_FixInAppPaymentTableDefaultNotifiedValue
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V234_ThumbnailRestoreStateColumn
|
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.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.
|
* 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,
|
232 to V232_CreateInAppPaymentTable,
|
||||||
233 to V233_FixInAppPaymentTableDefaultNotifiedValue,
|
233 to V233_FixInAppPaymentTableDefaultNotifiedValue,
|
||||||
234 to V234_ThumbnailRestoreStateColumn,
|
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
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
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