Skip to content

Commit 4941827

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> 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 3330db5 commit 4941827

8 files changed

Lines changed: 165 additions & 93 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: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -75,76 +75,6 @@ internal class CardBillingAddressElementTest {
7575
}
7676
}
7777

78-
@Test
79-
fun `Verify that automatic tax fields are unioned with AVS defaults for IN`() = runTest {
80-
val element = createCardBillingAddressElement(requiresBillingAddressForAutomaticTax = true)
81-
82-
element.hiddenIdentifiers.test {
83-
// IN has no AVS default fields, but requires a postal code for automatic tax.
84-
dropdownFieldController.onRawValueChange("IN")
85-
expectMostRecentItem().verifyFieldsShown(IdentifierSpec.PostalCode)
86-
}
87-
}
88-
89-
@Test
90-
fun `Verify that automatic tax fields for PR do not require state`() = runTest {
91-
val element = createCardBillingAddressElement(requiresBillingAddressForAutomaticTax = true)
92-
93-
element.hiddenIdentifiers.test {
94-
dropdownFieldController.onRawValueChange("PR")
95-
expectMostRecentItem().verifyFieldsShown(
96-
IdentifierSpec.Line1,
97-
IdentifierSpec.City,
98-
IdentifierSpec.PostalCode,
99-
)
100-
}
101-
}
102-
103-
@Test
104-
fun `Verify that automatic tax fields are shown for US`() = runTest {
105-
val element = createCardBillingAddressElement(requiresBillingAddressForAutomaticTax = true)
106-
107-
element.hiddenIdentifiers.test {
108-
dropdownFieldController.onRawValueChange("US")
109-
expectMostRecentItem().verifyFieldsShown(
110-
IdentifierSpec.Line1,
111-
IdentifierSpec.City,
112-
IdentifierSpec.State,
113-
IdentifierSpec.PostalCode,
114-
)
115-
}
116-
}
117-
118-
@Test
119-
fun `Verify that automatic tax fields have no effect when address collection is Never`() = runTest {
120-
val element = createCardBillingAddressElement(
121-
requiresBillingAddressForAutomaticTax = true,
122-
collectionConfiguration = BillingDetailsCollectionConfiguration(
123-
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Never,
124-
),
125-
)
126-
127-
element.hiddenIdentifiers.test {
128-
dropdownFieldController.onRawValueChange("US")
129-
expectMostRecentItem().verifyFieldsShown()
130-
}
131-
}
132-
133-
@Test
134-
fun `Verify that automatic tax fields have no effect when address collection is Full`() = runTest {
135-
val element = createCardBillingAddressElement(
136-
requiresBillingAddressForAutomaticTax = true,
137-
collectionConfiguration = BillingDetailsCollectionConfiguration(
138-
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Full,
139-
),
140-
)
141-
142-
element.hiddenIdentifiers.test {
143-
dropdownFieldController.onRawValueChange("US")
144-
assertThat(expectMostRecentItem()).isEmpty()
145-
}
146-
}
147-
14878
@Test
14979
fun `Verify that AutocompleteAddressElement is used when billing details collection is Full`() =
15080
autocompleteTest(
@@ -291,14 +221,12 @@ internal class CardBillingAddressElementTest {
291221
}
292222

293223
private fun createCardBillingAddressElement(
294-
requiresBillingAddressForAutomaticTax: Boolean = false,
295224
collectionConfiguration: BillingDetailsCollectionConfiguration = BillingDetailsCollectionConfiguration(),
296225
): BillingAddressElement {
297226
return createBillingAddressElement(
298227
collectionConfiguration = collectionConfiguration,
299228
addressCollectionMode = cardBillingAddressCollectionMode(
300229
addressCollectionMode = collectionConfiguration.address,
301-
requiresBillingAddressForAutomaticTax = requiresBillingAddressForAutomaticTax,
302230
),
303231
)
304232
}
@@ -405,7 +333,6 @@ internal class CardBillingAddressElementTest {
405333
shippingValues = null,
406334
addressCollectionMode = cardBillingAddressCollectionMode(
407335
addressCollectionMode = configuration.address,
408-
requiresBillingAddressForAutomaticTax = false,
409336
),
410337
collectionConfiguration = configuration,
411338
shouldHideCountryOnNoAddressCollection = true,

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

Lines changed: 6 additions & 3 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
}
@@ -118,7 +122,6 @@ private fun createTaxBillingAddressElement(
118122
identifier = IdentifierSpec.Generic("billing_details[address]"),
119123
initialValues = arguments.initialValues,
120124
countryCodes = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
121-
countryElementIdentifier = IdentifierSpec.Country,
122125
autocompleteAddressInteractorFactory = null,
123126
shippingValues = arguments.shippingValues,
124127
addressCollectionMode = BillingAddressCollectionMode.Country(

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)
@@ -298,7 +296,6 @@ private fun cardBillingElements(
298296
shippingValues = shippingValues,
299297
addressCollectionMode = cardBillingAddressCollectionMode(
300298
addressCollectionMode = collectionConfiguration.address,
301-
requiresBillingAddressForAutomaticTax = requiresBillingAddressForAutomaticTax,
302299
),
303300
collectionConfiguration = collectionConfiguration,
304301
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
@@ -57,7 +57,6 @@ internal class BillingDetailsForm(
5757
AddressCollectionMode.Never -> BillingDetailsCollectionConfiguration.AddressCollectionMode.Never
5858
AddressCollectionMode.Full -> BillingDetailsCollectionConfiguration.AddressCollectionMode.Full
5959
},
60-
requiresBillingAddressForAutomaticTax = false,
6160
),
6261
collectionConfiguration = BillingDetailsCollectionConfiguration(
6362
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)