Skip to content

Commit e2a12d4

Browse files
cttsai-stripeclaudecodex
committed
Address review: document all-types tax filtering; add non-card, per-country, and wiring tests (MOBILESDK-4697)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
1 parent bb3acfa commit e2a12d4

4 files changed

Lines changed: 88 additions & 1 deletion

File tree

paymentsheet/src/main/java/com/stripe/android/common/validation/BillingDetailsValidation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import com.stripe.android.uicore.elements.IdentifierSpec
1111
internal fun PaymentMethod.hasSufficientBillingDetailsForAutomaticTax(): Boolean {
1212
val address = billingDetails?.address ?: return false
1313
// Saved-PM billing details come from the API and are not guaranteed uppercase, unlike the
14-
// CountryConfig-sourced value the CardBillingAddressElement form passes. Uppercase before the
14+
// CountryConfig-sourced value the billing-address form passes. Uppercase before the
1515
// map lookup; do not "simplify" this away.
1616
val country = address.country?.uppercase()
1717
if (country.isNullOrBlank()) return false

paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentMethodFilter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ internal class DefaultPaymentMethodFilter @Inject constructor() : PaymentMethodF
4848
params.cardBrandFilter.isAccepted(paymentMethod) &&
4949
fundingAccepted &&
5050
paymentMethod.isSupportedWithBillingConfig(params.billingDetailsCollectionConfiguration) &&
51+
// Intentionally applies to every saved PM type, not just cards: pay-server's
52+
// confirm-time tax guard (finalize_payment_taxes) is not payment-method-type gated,
53+
// so any SPM whose billing address can't resolve a tax location 400s at confirm.
5154
(
5255
!params.requiresBillingAddressForAutomaticTax ||
5356
paymentMethod.hasSufficientBillingDetailsForAutomaticTax()

paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/CreateCustomerStateTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,34 @@ internal class CreateCustomerStateTest {
192192

193193
val filterCall = filterCalls.awaitItem()
194194
assertThat(filterCall.paymentMethods).isEqualTo(cards)
195+
// Default metadata does not require a billing address for automatic tax.
196+
assertThat(filterCall.params.requiresBillingAddressForAutomaticTax).isFalse()
197+
}
198+
}
199+
200+
@Test
201+
fun `Threads requiresBillingAddressForAutomaticTax into the filter params`() = runScenario {
202+
val cards = PaymentMethodFactory.cards(3)
203+
204+
FakePaymentMethodFilter.test(filteredPaymentMethods = cards) {
205+
createCustomerState(
206+
initializationMode = DEFAULT_INITIALIZATION_MODE,
207+
elementsSession = DEFAULT_ELEMENTS_SESSION.copy(
208+
customer = createElementsSessionCustomer(paymentMethods = cards),
209+
),
210+
metadata = PaymentMethodMetadataFactory.create(
211+
checkoutSessionResponse = CheckoutSessionResponseFactory.create(
212+
automaticTaxEnabled = true,
213+
taxAddressSource = CheckoutSessionResponse.TaxAddressSource.BILLING,
214+
),
215+
)
216+
.copy(customerMetadata = CUSTOMER_SESSION_METADATA),
217+
savedSelection = CompletableDeferred(SavedSelection.None),
218+
paymentMethodFilter = paymentMethodFilter,
219+
)
220+
221+
val filterCall = filterCalls.awaitItem()
222+
assertThat(filterCall.params.requiresBillingAddressForAutomaticTax).isTrue()
195223
}
196224
}
197225

paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentMethodFilterTest.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,62 @@ class DefaultPaymentMethodFilterTest {
270270
assertThat(observed).isEmpty()
271271
}
272272

273+
@Test
274+
fun `Applies automatic-tax filter to non-card SPMs too`() = runTest {
275+
// pay-server's confirm-time tax guard is not payment-method-type gated, so a bank-account
276+
// SPM with an insufficient tax address 400s at confirm just like a card; the filter must
277+
// drop it as well. Guards against a future card-only regression.
278+
val completeBank = PaymentMethodFactory.usBankAccount().copy(
279+
id = "pm_bank_complete",
280+
billingDetails = PaymentMethod.BillingDetails(
281+
address = Address(
282+
line1 = "1 Market St",
283+
city = "San Francisco",
284+
state = "CA",
285+
postalCode = "94111",
286+
country = "US",
287+
),
288+
),
289+
)
290+
val incompleteBank = PaymentMethodFactory.usBankAccount().copy(
291+
id = "pm_bank_incomplete",
292+
billingDetails = PaymentMethod.BillingDetails(
293+
// Missing line1 and state for a US address.
294+
address = Address(city = "San Francisco", postalCode = "94111", country = "US"),
295+
),
296+
)
297+
298+
val observed = filter(
299+
paymentMethods = listOf(completeBank, incompleteBank),
300+
requiresBillingAddressForAutomaticTax = true,
301+
)
302+
303+
assertThat(observed).containsExactly(completeBank)
304+
}
305+
306+
@Test
307+
fun `Applies per-country automatic-tax fields at the filter boundary`() = runTest {
308+
// CA requires only a postal code (not the US line1/city/state set), proving the per-country
309+
// lookup is consulted at the filter boundary rather than hardcoding US behavior.
310+
val caComplete = PaymentMethodFactory.card(id = "pm_ca_complete").update(
311+
last4 = "1000",
312+
addCbcNetworks = false,
313+
brand = CardBrand.Visa,
314+
).copy(billingDetails = PaymentMethod.BillingDetails(address = Address(postalCode = "K1A0B1", country = "CA")))
315+
val caMissingPostal = PaymentMethodFactory.card(id = "pm_ca_missing").update(
316+
last4 = "1000",
317+
addCbcNetworks = false,
318+
brand = CardBrand.Visa,
319+
).copy(billingDetails = PaymentMethod.BillingDetails(address = Address(country = "CA")))
320+
321+
val observed = filter(
322+
paymentMethods = listOf(caComplete, caMissingPostal),
323+
requiresBillingAddressForAutomaticTax = true,
324+
)
325+
326+
assertThat(observed).containsExactly(caComplete)
327+
}
328+
273329
private suspend fun filter(
274330
paymentMethods: List<PaymentMethod>,
275331
billingDetailsCollectionConfiguration: PaymentSheet.BillingDetailsCollectionConfiguration =

0 commit comments

Comments
 (0)