Skip to content

Commit 16539a1

Browse files
cttsai-stripecodex
andcommitted
Centralize automatic-tax form finalization
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
1 parent 65a1fb8 commit 16539a1

3 files changed

Lines changed: 150 additions & 84 deletions

File tree

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

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,54 +18,56 @@ import com.stripe.android.uicore.elements.SameAsShippingElement
1818
import com.stripe.android.uicore.elements.SectionController
1919
import com.stripe.android.uicore.elements.SectionElement
2020

21-
/**
22-
* Widens an existing shared address for automatic tax, or appends a country-first billing
23-
* address when the payment method does not already collect one.
24-
*/
25-
internal fun List<FormElement>.withAutomaticTaxBillingAddressIfNecessary(
26-
paymentMethodCode: String,
27-
arguments: UiDefinitionFactory.Arguments,
28-
): List<FormElement> {
29-
val shouldCollectTaxAddress = arguments.requiresBillingAddressForAutomaticTax &&
30-
arguments.billingDetailsCollectionConfiguration.address ==
31-
PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic
32-
if (!shouldCollectTaxAddress || paymentMethodCode in separatelyRenderedPaymentMethodCodes) {
33-
return this
34-
}
21+
/** Finalizes a base payment method form with any billing fields required for automatic tax. */
22+
internal object AutomaticTaxFormFinalizer {
23+
fun finalize(
24+
formElements: List<FormElement>,
25+
paymentMethodCode: String,
26+
arguments: UiDefinitionFactory.Arguments,
27+
): List<FormElement> {
28+
val shouldCollectTaxAddress = arguments.requiresBillingAddressForAutomaticTax &&
29+
arguments.billingDetailsCollectionConfiguration.address ==
30+
PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic
31+
if (!shouldCollectTaxAddress || paymentMethodCode in separatelyRenderedPaymentMethodCodes) {
32+
return formElements
33+
}
3534

36-
val addressField = filterIsInstance<SectionElement>()
37-
.flatMap { it.fields }
38-
.filterIsInstance<AddressFieldsElement>()
39-
.firstOrNull()
40-
if (addressField != null && addressField !is BillingAddressElement) {
41-
return this
42-
}
35+
val addressFields = formElements.filterIsInstance<SectionElement>()
36+
.flatMap { it.fields }
37+
.filterIsInstance<AddressFieldsElement>()
38+
assert(addressFields.size <= 1) { "A payment method form must not contain multiple billing addresses." }
4339

44-
val existingSameAsShippingElement = filterIsInstance<SameAsShippingElement>().firstOrNull()
45-
val sameAsShippingElement = existingSameAsShippingElement ?: createSameAsShippingElement(arguments)
40+
val addressField = addressFields.firstOrNull()
41+
if (addressField != null && addressField !is BillingAddressElement) {
42+
return formElements
43+
}
44+
45+
val existingSameAsShippingElement = formElements.filterIsInstance<SameAsShippingElement>().firstOrNull()
46+
val sameAsShippingElement = existingSameAsShippingElement ?: createSameAsShippingElement(arguments)
4647

47-
if (addressField is BillingAddressElement) {
48-
return widenBillingAddress(
49-
addressField = addressField,
48+
if (addressField is BillingAddressElement) {
49+
return formElements.widenBillingAddress(
50+
addressField = addressField,
51+
arguments = arguments,
52+
sameAsShippingElement = sameAsShippingElement,
53+
appendSameAsShippingElement = existingSameAsShippingElement == null,
54+
)
55+
}
56+
57+
val addressElement = createTaxBillingAddressElement(
5058
arguments = arguments,
59+
countryDropdownFieldController = DropdownFieldController(
60+
config = CountryConfig(arguments.billingDetailsCollectionConfiguration.allowedBillingCountries),
61+
initialValue = arguments.initialValues[IdentifierSpec.Country],
62+
),
5163
sameAsShippingElement = sameAsShippingElement,
52-
appendSameAsShippingElement = existingSameAsShippingElement == null,
5364
)
54-
}
5565

56-
val addressElement = createTaxBillingAddressElement(
57-
arguments = arguments,
58-
countryDropdownFieldController = DropdownFieldController(
59-
config = CountryConfig(arguments.billingDetailsCollectionConfiguration.allowedBillingCountries),
60-
initialValue = arguments.initialValues[IdentifierSpec.Country],
61-
),
62-
sameAsShippingElement = sameAsShippingElement,
63-
)
64-
65-
return this + listOfNotNull(
66-
SectionElement.wrap(addressElement, R.string.stripe_billing_details.resolvableString),
67-
sameAsShippingElement,
68-
)
66+
return formElements + listOfNotNull(
67+
SectionElement.wrap(addressElement, R.string.stripe_billing_details.resolvableString),
68+
sameAsShippingElement,
69+
)
70+
}
6971
}
7072

7173
private fun List<FormElement>.widenBillingAddress(

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

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,7 @@ internal sealed interface UiDefinitionFactory {
185185

186186
buildFormElements(metadata, arguments, builder)
187187

188-
return builder.build().withAutomaticTaxBillingAddressIfNecessary(
189-
paymentMethodCode = createSupportedPaymentMethod(metadata).code,
190-
arguments = arguments,
191-
)
188+
return builder.build()
192189
}
193190

194191
protected open fun buildFormElements(
@@ -293,39 +290,39 @@ internal sealed interface UiDefinitionFactory {
293290
metadata: PaymentMethodMetadata,
294291
sharedDataSpecs: List<SharedDataSpec>,
295292
arguments: Arguments,
296-
): List<FormElement>? = when (this) {
297-
is Simple -> {
298-
createBaseFormElements(
293+
): List<FormElement>? {
294+
val baseFormElements = when (this) {
295+
is Simple -> createBaseFormElements(
299296
metadata = metadata,
300297
arguments = arguments,
301298
)
302-
}
303299

304-
is Custom -> {
305-
createBaseFormElements(
300+
is Custom -> createBaseFormElements(
306301
metadata = metadata,
307302
arguments = arguments,
308-
).withAutomaticTaxBillingAddressIfNecessary(
309-
paymentMethodCode = definition.type.code,
310-
arguments = arguments,
311303
)
312-
}
313304

314-
is RequiresSharedDataSpec -> {
315-
val sharedDataSpec = sharedDataSpecs.firstOrNull { it.type == paymentMethodCode }
316-
if (sharedDataSpec != null) {
317-
createBaseFormElements(
318-
metadata = metadata,
319-
sharedDataSpec = sharedDataSpec,
320-
transformSpecToElements = TransformSpecToElements(arguments),
321-
arguments = arguments,
322-
).withAutomaticTaxBillingAddressIfNecessary(
323-
paymentMethodCode = definition.type.code,
324-
arguments = arguments,
325-
)
326-
} else {
327-
null
305+
is RequiresSharedDataSpec -> {
306+
val sharedDataSpec = sharedDataSpecs.firstOrNull { it.type == paymentMethodCode }
307+
if (sharedDataSpec != null) {
308+
createBaseFormElements(
309+
metadata = metadata,
310+
sharedDataSpec = sharedDataSpec,
311+
transformSpecToElements = TransformSpecToElements(arguments),
312+
arguments = arguments,
313+
)
314+
} else {
315+
null
316+
}
328317
}
329318
}
319+
320+
return baseFormElements?.let { formElements ->
321+
AutomaticTaxFormFinalizer.finalize(
322+
formElements = formElements,
323+
paymentMethodCode = paymentMethodCode,
324+
arguments = arguments,
325+
)
326+
}
330327
}
331328
}

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

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.stripe.android.lpmfoundations.paymentmethod.definitions.BlikDefinitio
77
import com.stripe.android.lpmfoundations.paymentmethod.definitions.KlarnaDefinition
88
import com.stripe.android.lpmfoundations.paymentmethod.definitions.WeroDefinition
99
import com.stripe.android.model.PaymentMethod
10+
import com.stripe.android.model.PaymentMethodCreateParams
1011
import com.stripe.android.model.PaymentMethodFixtures
1112
import com.stripe.android.paymentsheet.PaymentSheet
1213
import com.stripe.android.paymentsheet.addresselement.AddressDetails
@@ -18,6 +19,7 @@ import com.stripe.android.testing.FeatureFlagTestRule
1819
import com.stripe.android.testing.PaymentIntentFactory
1920
import com.stripe.android.ui.core.FieldValuesToParamsMapConverter
2021
import com.stripe.android.ui.core.elements.BillingAddressElement
22+
import com.stripe.android.ui.core.elements.additionalAutomaticTaxFieldsByCountry
2123
import com.stripe.android.uicore.elements.AddressElement
2224
import com.stripe.android.uicore.elements.AddressFieldsElement
2325
import com.stripe.android.uicore.elements.FormElement
@@ -92,21 +94,41 @@ class AutomaticTaxBillingAddressTest {
9294
.sectionFields()
9395
.filterIsInstance<BillingAddressElement>()
9496
.single()
95-
val formValues = requireNotNull(
96-
CompleteFormFieldValueFilter(
97-
currentFieldValueMap = addressElement.getFormFieldValueFlow().map { it.toMap() },
98-
hiddenIdentifiers = addressElement.hiddenIdentifiers,
99-
userRequestedReuse = flowOf(PaymentSelection.CustomerRequestedSave.NoRequest),
100-
defaultValues = emptyMap(),
101-
).filterFlow().first()
97+
val createParams = addressElement.toCreateParams(metadata, PaymentMethod.Type.Blik.code)
98+
val billingDetails = createParams.toParamMap()["billing_details"] as Map<*, *>
99+
100+
assertThat(billingDetails["address"]).isEqualTo(
101+
mapOf(
102+
"line1" to "510 Townsend Street",
103+
"city" to "San Francisco",
104+
"state" to "CA",
105+
"postal_code" to "94103",
106+
"country" to "US",
107+
)
102108
)
109+
}
103110

104-
val createParams = FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams(
105-
fieldValuePairs = formValues.fieldValuePairs,
106-
code = PaymentMethod.Type.Blik.code,
107-
requiresMandate = false,
108-
clientAttributionMetadata = metadata.clientAttributionMetadata,
111+
@Test
112+
fun `widened Klarna address is included in create params`() = runTest {
113+
val metadata = createMetadata(
114+
paymentMethodCode = PaymentMethod.Type.Klarna.code,
115+
checkoutSessionResponse = automaticTaxCheckoutSessionResponse,
116+
defaultBillingDetails = PaymentSheet.BillingDetails(
117+
address = PaymentSheet.Address(
118+
line1 = "510 Townsend Street",
119+
city = "San Francisco",
120+
state = "CA",
121+
postalCode = "94103",
122+
country = "US",
123+
),
124+
),
109125
)
126+
val addressElement = KlarnaDefinition.formElements(metadata)
127+
.sectionFields()
128+
.filterIsInstance<BillingAddressElement>()
129+
.single()
130+
131+
val createParams = addressElement.toCreateParams(metadata, PaymentMethod.Type.Klarna.code)
110132
val billingDetails = createParams.toParamMap()["billing_details"] as Map<*, *>
111133

112134
assertThat(billingDetails["address"]).isEqualTo(
@@ -278,9 +300,33 @@ class AutomaticTaxBillingAddressTest {
278300
uiDefinitionFactoryArgumentsFactory = TestUiDefinitionFactoryArgumentsFactory.create(),
279301
)
280302
)
281-
assertWithMessage("$code should contain exactly one address")
282-
.that(formElements.sectionFields().filterIsInstance<AddressFieldsElement>())
283-
.hasSize(1)
303+
val addressFields = formElements.sectionFields().filterIsInstance<AddressFieldsElement>()
304+
assertWithMessage("$code should contain exactly one address").that(addressFields).hasSize(1)
305+
306+
val addressElement = addressFields.single()
307+
if (addressElement !is BillingAddressElement) {
308+
assertWithMessage("$code should already contain a full address")
309+
.that(addressElement)
310+
.isInstanceOf(AddressElement::class.java)
311+
return@forEach
312+
}
313+
314+
if (code == PaymentMethod.Type.Wero.code) {
315+
val controller = addressElement.countryElement.controller
316+
listOf("DE", "BE", "FR").forEach { allowedCountry ->
317+
controller.onRawValueChange(allowedCountry)
318+
assertThat(controller.rawFieldValue.value).isEqualTo(allowedCountry)
319+
}
320+
controller.onRawValueChange("US")
321+
assertThat(controller.rawFieldValue.value).isNotEqualTo("US")
322+
}
323+
324+
val selectedCountry = requireNotNull(addressElement.countryElement.controller.rawFieldValue.value)
325+
additionalAutomaticTaxFieldsByCountry[selectedCountry].orEmpty().forEach { requiredField ->
326+
assertWithMessage("$code should show $requiredField for $selectedCountry")
327+
.that(addressElement.hiddenIdentifiers.value)
328+
.doesNotContain(requiredField)
329+
}
284330
}
285331
}
286332

@@ -296,6 +342,27 @@ class AutomaticTaxBillingAddressTest {
296342
return addressElement
297343
}
298344

345+
private suspend fun BillingAddressElement.toCreateParams(
346+
metadata: PaymentMethodMetadata,
347+
paymentMethodCode: String,
348+
): PaymentMethodCreateParams {
349+
val formValues = requireNotNull(
350+
CompleteFormFieldValueFilter(
351+
currentFieldValueMap = getFormFieldValueFlow().map { it.toMap() },
352+
hiddenIdentifiers = hiddenIdentifiers,
353+
userRequestedReuse = flowOf(PaymentSelection.CustomerRequestedSave.NoRequest),
354+
defaultValues = emptyMap(),
355+
).filterFlow().first()
356+
)
357+
358+
return FieldValuesToParamsMapConverter.transformToPaymentMethodCreateParams(
359+
fieldValuePairs = formValues.fieldValuePairs,
360+
code = paymentMethodCode,
361+
requiresMandate = false,
362+
clientAttributionMetadata = metadata.clientAttributionMetadata,
363+
)
364+
}
365+
299366
private fun createMetadata(
300367
paymentMethodCode: String,
301368
checkoutSessionResponse: CheckoutSessionResponse?,

0 commit comments

Comments
 (0)