Skip to content

Commit 6a2ae10

Browse files
cttsai-stripecodex
andcommitted
Centralize automatic tax form policy
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
1 parent 765b707 commit 6a2ae10

8 files changed

Lines changed: 180 additions & 99 deletions

File tree

payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/BillingAddressElement.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ sealed interface BillingAddressCollectionMode {
4545
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
4646
fun cardBillingAddressCollectionMode(
4747
addressCollectionMode: BillingDetailsCollectionConfiguration.AddressCollectionMode,
48-
requiresBillingAddressForAutomaticTax: Boolean,
4948
): BillingAddressCollectionMode {
5049
return when (addressCollectionMode) {
5150
BillingDetailsCollectionConfiguration.AddressCollectionMode.Never -> BillingAddressCollectionMode.Never
@@ -55,11 +54,6 @@ fun cardBillingAddressCollectionMode(
5554
listOf("US", "GB", "CA").forEach { countryCode ->
5655
put(countryCode, setOf(IdentifierSpec.PostalCode))
5756
}
58-
if (requiresBillingAddressForAutomaticTax) {
59-
additionalAutomaticTaxFieldsByCountry.forEach { (countryCode, fields) ->
60-
put(countryCode, get(countryCode).orEmpty() + fields)
61-
}
62-
}
6357
}
6458
BillingAddressCollectionMode.Country(additionalFieldsByCountry)
6559
}

payments-ui-core/src/test/java/com/stripe/android/ui/core/elements/CardBillingAddressElementTest.kt

Lines changed: 15 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,22 @@ internal class CardBillingAddressElementTest {
8080
fun `Country-only collection preserves a configured country identifier`() = runTest {
8181
val countryIdentifier = IdentifierSpec.Generic("payment_method_data[future_lpm][country]")
8282
val element = BillingAddressElement(
83-
identifier = IdentifierSpec.Generic("billing_element"),
84-
rawValuesMap = mapOf(countryIdentifier to "DE"),
85-
countryCodes = setOf("DE"),
86-
countryElementIdentifier = countryIdentifier,
87-
autocompleteAddressInteractorFactory = null,
83+
configuration = BillingAddressElement.Configuration(
84+
identifier = IdentifierSpec.Generic("billing_element"),
85+
initialValues = mapOf(countryIdentifier to "DE"),
86+
countryCodes = setOf("DE"),
87+
countryElementIdentifier = countryIdentifier,
88+
autocompleteAddressInteractorFactory = null,
89+
shippingValues = null,
90+
addressCollectionMode = BillingAddressCollectionMode.Country(emptyMap()),
91+
collectionConfiguration = BillingDetailsCollectionConfiguration(),
92+
shouldHideCountryOnNoAddressCollection = true,
93+
),
94+
countryDropdownFieldController = DropdownFieldController(
95+
CountryConfig(setOf("DE")),
96+
initialValue = "DE",
97+
),
8898
sameAsShippingElement = null,
89-
shippingValuesMap = null,
90-
addressCollectionMode = BillingAddressCollectionMode.Country(emptyMap()),
9199
)
92100

93101
assertThat(element.countryElement.identifier).isEqualTo(countryIdentifier)
@@ -97,76 +105,6 @@ internal class CardBillingAddressElementTest {
97105
assertThat(formFieldIdentifiers).doesNotContain(IdentifierSpec.Country)
98106
}
99107

100-
@Test
101-
fun `Verify that automatic tax fields are unioned with AVS defaults for IN`() = runTest {
102-
val element = createCardBillingAddressElement(requiresBillingAddressForAutomaticTax = true)
103-
104-
element.hiddenIdentifiers.test {
105-
// IN has no AVS default fields, but requires a postal code for automatic tax.
106-
dropdownFieldController.onRawValueChange("IN")
107-
expectMostRecentItem().verifyFieldsShown(IdentifierSpec.PostalCode)
108-
}
109-
}
110-
111-
@Test
112-
fun `Verify that automatic tax fields for PR do not require state`() = runTest {
113-
val element = createCardBillingAddressElement(requiresBillingAddressForAutomaticTax = true)
114-
115-
element.hiddenIdentifiers.test {
116-
dropdownFieldController.onRawValueChange("PR")
117-
expectMostRecentItem().verifyFieldsShown(
118-
IdentifierSpec.Line1,
119-
IdentifierSpec.City,
120-
IdentifierSpec.PostalCode,
121-
)
122-
}
123-
}
124-
125-
@Test
126-
fun `Verify that automatic tax fields are shown for US`() = runTest {
127-
val element = createCardBillingAddressElement(requiresBillingAddressForAutomaticTax = true)
128-
129-
element.hiddenIdentifiers.test {
130-
dropdownFieldController.onRawValueChange("US")
131-
expectMostRecentItem().verifyFieldsShown(
132-
IdentifierSpec.Line1,
133-
IdentifierSpec.City,
134-
IdentifierSpec.State,
135-
IdentifierSpec.PostalCode,
136-
)
137-
}
138-
}
139-
140-
@Test
141-
fun `Verify that automatic tax fields have no effect when address collection is Never`() = runTest {
142-
val element = createCardBillingAddressElement(
143-
requiresBillingAddressForAutomaticTax = true,
144-
collectionConfiguration = BillingDetailsCollectionConfiguration(
145-
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Never,
146-
),
147-
)
148-
149-
element.hiddenIdentifiers.test {
150-
dropdownFieldController.onRawValueChange("US")
151-
expectMostRecentItem().verifyFieldsShown()
152-
}
153-
}
154-
155-
@Test
156-
fun `Verify that automatic tax fields have no effect when address collection is Full`() = runTest {
157-
val element = createCardBillingAddressElement(
158-
requiresBillingAddressForAutomaticTax = true,
159-
collectionConfiguration = BillingDetailsCollectionConfiguration(
160-
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Full,
161-
),
162-
)
163-
164-
element.hiddenIdentifiers.test {
165-
dropdownFieldController.onRawValueChange("US")
166-
assertThat(expectMostRecentItem()).isEmpty()
167-
}
168-
}
169-
170108
@Test
171109
fun `Verify that AutocompleteAddressElement is used when billing details collection is Full`() =
172110
autocompleteTest(
@@ -313,14 +251,12 @@ internal class CardBillingAddressElementTest {
313251
}
314252

315253
private fun createCardBillingAddressElement(
316-
requiresBillingAddressForAutomaticTax: Boolean = false,
317254
collectionConfiguration: BillingDetailsCollectionConfiguration = BillingDetailsCollectionConfiguration(),
318255
): BillingAddressElement {
319256
return createBillingAddressElement(
320257
collectionConfiguration = collectionConfiguration,
321258
addressCollectionMode = cardBillingAddressCollectionMode(
322259
addressCollectionMode = collectionConfiguration.address,
323-
requiresBillingAddressForAutomaticTax = requiresBillingAddressForAutomaticTax,
324260
),
325261
)
326262
}
@@ -429,7 +365,6 @@ internal class CardBillingAddressElementTest {
429365
shippingValues = null,
430366
addressCollectionMode = cardBillingAddressCollectionMode(
431367
addressCollectionMode = configuration.address,
432-
requiresBillingAddressForAutomaticTax = false,
433368
),
434369
collectionConfiguration = configuration,
435370
shouldHideCountryOnNoAddressCollection = true,

paymentsheet/src/main/java/com/stripe/android/lpmfoundations/paymentmethod/AutomaticTaxFormFinalizer.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.stripe.android.ui.core.R
88
import com.stripe.android.ui.core.elements.BillingAddressCollectionMode
99
import com.stripe.android.ui.core.elements.BillingAddressElement
1010
import com.stripe.android.ui.core.elements.additionalAutomaticTaxFieldsByCountry
11+
import com.stripe.android.uicore.elements.AddressElement
1112
import com.stripe.android.uicore.elements.AddressFieldsElement
1213
import com.stripe.android.uicore.elements.CountryConfig
1314
import com.stripe.android.uicore.elements.DropdownFieldController
@@ -35,10 +36,13 @@ internal object AutomaticTaxFormFinalizer {
3536
val addressFields = formElements.filterIsInstance<SectionElement>()
3637
.flatMap { it.fields }
3738
.filterIsInstance<AddressFieldsElement>()
38-
assert(addressFields.size <= 1) { "A payment method form must not contain multiple billing addresses." }
39+
check(addressFields.size <= 1) { "A payment method form must not contain multiple billing addresses." }
3940

4041
val addressField = addressFields.firstOrNull()
4142
if (addressField != null && addressField !is BillingAddressElement) {
43+
check(addressField is AddressElement) {
44+
"A specialized automatic-tax address must use AddressElement."
45+
}
4246
return formElements
4347
}
4448

@@ -64,7 +68,7 @@ internal object AutomaticTaxFormFinalizer {
6468

6569
return formElements + listOfNotNull(
6670
SectionElement.wrap(addressElement, R.string.stripe_billing_details.resolvableString),
67-
sameAsShippingElement,
71+
sameAsShippingElement.takeIf { existingSameAsShippingElement == null },
6872
)
6973
}
7074
}

paymentsheet/src/main/java/com/stripe/android/lpmfoundations/paymentmethod/definitions/CardDefinition.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,6 @@ private object CardUiDefinitionFactory : UiDefinitionFactory.Custom {
245245
arguments.autocompleteAddressInteractorFactory,
246246
arguments.initialValues,
247247
arguments.shippingValues,
248-
arguments.requiresBillingAddressForAutomaticTax,
249248
)
250249
)
251250
}
@@ -278,7 +277,6 @@ private fun cardBillingElements(
278277
autocompleteAddressInteractorFactory: AutocompleteAddressInteractor.Factory?,
279278
initialValues: Map<IdentifierSpec, String?>,
280279
shippingValues: Map<IdentifierSpec, String?>?,
281-
requiresBillingAddressForAutomaticTax: Boolean,
282280
): List<FormElement> {
283281
val sameAsShippingElement =
284282
shippingValues?.get(IdentifierSpec.SameAsShipping)
@@ -299,7 +297,6 @@ private fun cardBillingElements(
299297
shippingValues = shippingValues,
300298
addressCollectionMode = cardBillingAddressCollectionMode(
301299
addressCollectionMode = collectionConfiguration.address,
302-
requiresBillingAddressForAutomaticTax = requiresBillingAddressForAutomaticTax,
303300
),
304301
collectionConfiguration = collectionConfiguration,
305302
shouldHideCountryOnNoAddressCollection = true,

paymentsheet/src/main/java/com/stripe/android/paymentsheet/forms/FormViewModel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ internal class FormViewModel(
5858
}
5959
}
6060

61-
private val billingAddressElement = elements.filterIsInstance<SectionElement>()
61+
private val cardBillingElement = elements.filterIsInstance<SectionElement>()
6262
.flatMap { it.fields }
6363
.filterIsInstance<BillingAddressElement>()
6464
.firstOrNull()
@@ -77,10 +77,10 @@ internal class FormViewModel(
7777
}
7878

7979
internal val hiddenIdentifiers = combineAsStateFlow(
80-
billingAddressElement?.hiddenIdentifiers ?: stateFlowOf(emptySet()),
80+
cardBillingElement?.hiddenIdentifiers ?: stateFlowOf(emptySet()),
8181
externalHiddenIdentifiers
82-
) { billingAddressIdentifiers, externalHiddenIdentifiers ->
83-
externalHiddenIdentifiers.plus(billingAddressIdentifiers)
82+
) { cardBillingIdentifiers, externalHiddenIdentifiers ->
83+
externalHiddenIdentifiers.plus(cardBillingIdentifiers)
8484
}
8585

8686
// This will convert the save for future use value into a CustomerRequestedSave operation

paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/BillingDetailsForm.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ internal class BillingDetailsForm(
5858
AddressCollectionMode.Never -> BillingDetailsCollectionConfiguration.AddressCollectionMode.Never
5959
AddressCollectionMode.Full -> BillingDetailsCollectionConfiguration.AddressCollectionMode.Full
6060
},
61-
requiresBillingAddressForAutomaticTax = false,
6261
),
6362
collectionConfiguration = BillingDetailsCollectionConfiguration(
6463
address = when (addressCollectionMode) {

paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/AutomaticTaxBillingAddressTest.kt

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.stripe.android.lpmfoundations.paymentmethod
33
import com.google.common.truth.Truth.assertThat
44
import com.google.common.truth.Truth.assertWithMessage
55
import com.stripe.android.core.utils.FeatureFlags
6+
import com.stripe.android.lpmfoundations.paymentmethod.definitions.BacsDebitDefinition
67
import com.stripe.android.lpmfoundations.paymentmethod.definitions.BlikDefinition
78
import com.stripe.android.lpmfoundations.paymentmethod.definitions.KlarnaDefinition
89
import com.stripe.android.lpmfoundations.paymentmethod.definitions.WeroDefinition
@@ -24,12 +25,14 @@ import com.stripe.android.uicore.elements.AddressElement
2425
import com.stripe.android.uicore.elements.AddressFieldsElement
2526
import com.stripe.android.uicore.elements.FormElement
2627
import com.stripe.android.uicore.elements.IdentifierSpec
28+
import com.stripe.android.uicore.elements.SameAsShippingController
2729
import com.stripe.android.uicore.elements.SameAsShippingElement
2830
import com.stripe.android.uicore.elements.SectionElement
2931
import kotlinx.coroutines.flow.first
3032
import kotlinx.coroutines.flow.flowOf
3133
import kotlinx.coroutines.flow.map
3234
import kotlinx.coroutines.test.runTest
35+
import org.junit.Assert.assertThrows
3336
import org.junit.Rule
3437
import org.junit.Test
3538
import org.junit.runner.RunWith
@@ -157,6 +160,52 @@ class AutomaticTaxBillingAddressTest {
157160
assertThat(formElements.sectionFields().filterIsInstance<AddressFieldsElement>()).isEmpty()
158161
}
159162

163+
@Test
164+
fun `appended tax address retains an existing same as shipping element once`() {
165+
val metadata = createMetadata(
166+
paymentMethodCode = PaymentMethod.Type.Blik.code,
167+
checkoutSessionResponse = automaticTaxCheckoutSessionResponse,
168+
)
169+
val sameAsShippingElement = SameAsShippingElement(
170+
identifier = IdentifierSpec.SameAsShipping,
171+
controller = SameAsShippingController(true),
172+
)
173+
174+
val formElements = AutomaticTaxFormFinalizer.finalize(
175+
formElements = listOf(sameAsShippingElement),
176+
paymentMethodCode = PaymentMethod.Type.Blik.code,
177+
arguments = TestUiDefinitionFactoryArgumentsFactory.create().create(
178+
metadata = metadata,
179+
requiresMandate = false,
180+
),
181+
)
182+
183+
assertThat(formElements.filterIsInstance<SameAsShippingElement>()).containsExactly(sameAsShippingElement)
184+
}
185+
186+
@Test
187+
fun `multiple billing addresses cause automatic tax finalization to fail`() {
188+
val metadata = createMetadata(
189+
paymentMethodCode = PaymentMethod.Type.Blik.code,
190+
checkoutSessionResponse = automaticTaxCheckoutSessionResponse,
191+
)
192+
val formElements = BlikDefinition.formElements(metadata)
193+
194+
val exception = assertThrows(IllegalStateException::class.java) {
195+
AutomaticTaxFormFinalizer.finalize(
196+
formElements = formElements + formElements,
197+
paymentMethodCode = PaymentMethod.Type.Blik.code,
198+
arguments = TestUiDefinitionFactoryArgumentsFactory.create().create(
199+
metadata = metadata,
200+
requiresMandate = false,
201+
),
202+
)
203+
}
204+
205+
assertThat(exception).hasMessageThat()
206+
.contains("A payment method form must not contain multiple billing addresses.")
207+
}
208+
160209
@Test
161210
fun `existing country section is widened without duplication`() {
162211
val formElements = WeroDefinition.formElements(
@@ -212,6 +261,37 @@ class AutomaticTaxBillingAddressTest {
212261
assertThat(addressElement.addressElement).isInstanceOf(AddressElement::class.java)
213262
}
214263

264+
@Test
265+
fun `specialized full address is preserved by automatic tax finalization`() {
266+
val baseMetadata = createMetadata(
267+
paymentMethodCode = PaymentMethod.Type.BacsDebit.code,
268+
checkoutSessionResponse = null,
269+
)
270+
val baseFormElements = requireNotNull(
271+
baseMetadata.formElementsForCode(
272+
code = PaymentMethod.Type.BacsDebit.code,
273+
uiDefinitionFactoryArgumentsFactory = TestUiDefinitionFactoryArgumentsFactory.create(),
274+
)
275+
)
276+
val specializedAddress = baseFormElements.sectionFields().filterIsInstance<AddressElement>().single()
277+
val taxMetadata = createMetadata(
278+
paymentMethodCode = PaymentMethod.Type.BacsDebit.code,
279+
checkoutSessionResponse = automaticTaxCheckoutSessionResponse,
280+
)
281+
282+
val finalizedFormElements = AutomaticTaxFormFinalizer.finalize(
283+
formElements = baseFormElements,
284+
paymentMethodCode = PaymentMethod.Type.BacsDebit.code,
285+
arguments = TestUiDefinitionFactoryArgumentsFactory.create().create(
286+
metadata = taxMetadata,
287+
requiresMandate = BacsDebitDefinition.requiresMandate(taxMetadata),
288+
),
289+
)
290+
291+
assertThat(finalizedFormElements.sectionFields().filterIsInstance<AddressElement>())
292+
.containsExactly(specializedAddress)
293+
}
294+
215295
@Test
216296
fun `Klarna country section is widened without duplication`() {
217297
val formElements = KlarnaDefinition.formElements(

0 commit comments

Comments
 (0)