From d24b10b82e032371ec10ccf54a65bf4eac4c0e96 Mon Sep 17 00:00:00 2001 From: Sarah Koop Date: Wed, 24 Jul 2024 14:07:11 -0500 Subject: [PATCH] Add Recurring Billing Agreement to PayPalVaultRequest (#1079) * Add new request objects * Add doc strings * Move files * Add JSON * Add parcelize * Update constructors for Java * Update PayPalVaultRequest unit tests * Update unit tests * Fix required fields and doc strings * revert unrelated changes * Add CHANGELOG * Fix CHANGELOG * merge main * Fix lint * Consolidate constructors * Move toJson methods out of companion objectz' * Fix spacing * Update CHANGELOG.md Co-authored-by: scannillo <35243507+scannillo@users.noreply.github.com> * Update PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingPricing.kt Co-authored-by: scannillo <35243507+scannillo@users.noreply.github.com> * Add JSON test * Fix lint --------- Co-authored-by: scannillo <35243507+scannillo@users.noreply.github.com> --- CHANGELOG.md | 5 + PayPal/build.gradle | 1 + .../api/paypal/PayPalBillingCycle.kt | 67 ++++++++ .../api/paypal/PayPalBillingInterval.kt | 11 ++ .../api/paypal/PayPalBillingPricing.kt | 35 ++++ .../api/paypal/PayPalPricingModel.kt | 10 ++ .../paypal/PayPalRecurringBillingDetails.kt | 71 ++++++++ .../paypal/PayPalRecurringBillingPlanType.kt | 28 ++++ .../api/paypal/PayPalRequest.java | 4 + .../api/paypal/PayPalVaultRequest.java | 42 ++++- .../paypal/PayPalVaultRequestUnitTest.java | 157 +++++++++++++++++- .../api/testutils/Fixtures.kt | 44 +++++ 12 files changed, 465 insertions(+), 10 deletions(-) create mode 100644 PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingCycle.kt create mode 100644 PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingInterval.kt create mode 100644 PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingPricing.kt create mode 100644 PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalPricingModel.kt create mode 100644 PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingDetails.kt create mode 100644 PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingPlanType.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0b24f825..068212c04f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Braintree Android SDK Release Notes +## unreleased + +* PayPal + * Add `PayPalRecurringBillingDetails` and `PayPalRecurringBillingPlanType` opt-in request objects. Including these details will provide transparency to users on their billing schedule, dates, and amounts, as well as launch a modernized checkout UI. + ## 5.0.0-beta1 (2024-07-23) * Breaking Changes diff --git a/PayPal/build.gradle b/PayPal/build.gradle index aca341a451..f022bc7ae5 100644 --- a/PayPal/build.gradle +++ b/PayPal/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.library' id 'kotlin-android' id 'org.jetbrains.dokka' + id 'kotlin-parcelize' } android { diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingCycle.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingCycle.kt new file mode 100644 index 0000000000..9bc003231e --- /dev/null +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingCycle.kt @@ -0,0 +1,67 @@ +package com.braintreepayments.api.paypal + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.json.JSONObject + +/** + * PayPal recurring billing cycle details. + * + * @property interval The number of intervals after which a subscriber is charged or billed. + * @property intervalCount The number of times this billing cycle gets executed. For example, if + * the [intervalCount] is [PayPalBillingInterval.DAY] with an [intervalCount] of 2, the subscription + * is billed once every two days. Maximum values [PayPalBillingInterval.DAY] -> 365, + * [PayPalBillingInterval.WEEK] -> 52, [PayPalBillingInterval.MONTH] -> 12, + * [PayPalBillingInterval.YEAR] -> 1. + * @property numberOfExecutions The number of times this billing cycle gets executed. Trial billing + * cycles can only be executed a finite number of times (value between 1 and 999). Regular billing + * cycles can be executed infinite times (value of 0) or a finite number of times (value between 1 + * and 999). + * @property sequence The sequence of the billing cycle. Used to identify unique billing cycles. For + * example, sequence 1 could be a 3 month trial period, and sequence 2 could be a longer term full + * rater cycle. Max value 100. All billing cycles should have unique sequence values. + * @property startDate The date and time when the billing cycle starts, in Internet date and time + * format `YYYY-MM-DDT00:00:00Z`. If not provided the billing cycle starts at the time of checkout. + * If provided and the merchant wants the billing cycle to start at the time of checkout, provide + * the current time. Otherwise the [startDate] can be in future. + * @property isTrial The tenure type of the billing cycle. In case of a plan having trial cycle, + * only 2 trial cycles are allowed per plan. + * @property pricing The active pricing scheme for this billing cycle. Required if [isTrial] is + * false. Optional if [isTrial] is true. + */ +@Parcelize +data class PayPalBillingCycle @JvmOverloads constructor( + val interval: PayPalBillingInterval, + val intervalCount: Int, + val numberOfExecutions: Int, + var sequence: Int? = null, + var startDate: String? = null, + var isTrial: Boolean = false, + var pricing: PayPalBillingPricing? = null +) : Parcelable { + + fun toJson(): JSONObject { + return JSONObject().apply { + put(KEY_INTERVAL, interval) + put(KEY_INTERVAL_COUNT, intervalCount) + put(KEY_NUMBER_OF_EXECUTIONS, numberOfExecutions) + putOpt(KEY_SEQUENCE, sequence) + putOpt(KEY_START_DATE, startDate) + put(KEY_TRIAL, isTrial) + pricing?.let { + put(KEY_PRICING, it.toJson()) + } + } + } + + companion object { + + private const val KEY_INTERVAL = "billing_frequency_unit" + private const val KEY_INTERVAL_COUNT = "billing_frequency" + private const val KEY_NUMBER_OF_EXECUTIONS = "number_of_executions" + private const val KEY_SEQUENCE = "sequence" + private const val KEY_START_DATE = "start_date" + private const val KEY_TRIAL = "trial" + private const val KEY_PRICING = "pricing_scheme" + } +} diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingInterval.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingInterval.kt new file mode 100644 index 0000000000..a32f6237c2 --- /dev/null +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingInterval.kt @@ -0,0 +1,11 @@ +package com.braintreepayments.api.paypal + +/** + * The interval at which the payment is charged or billed. + */ +enum class PayPalBillingInterval { + DAY, + WEEK, + MONTH, + YEAR +} diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingPricing.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingPricing.kt new file mode 100644 index 0000000000..7e4c1fc054 --- /dev/null +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingPricing.kt @@ -0,0 +1,35 @@ +package com.braintreepayments.api.paypal + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.json.JSONObject + +/** + * PayPal Recurring Billing Agreement pricing details. + * + * @property pricingModel The pricing model associated with the billing agreement. + * @property amount Price. The amount to charge for the subscription, recurring, UCOF or installments. + * @property reloadThresholdAmount The reload trigger threshold condition amount when the customer is charged. + */ +@Parcelize +data class PayPalBillingPricing @JvmOverloads constructor( + val pricingModel: PayPalPricingModel, + val amount: String, + var reloadThresholdAmount: String? = null +) : Parcelable { + + fun toJson(): JSONObject { + return JSONObject().apply { + put(KEY_PRICING_MODEL, pricingModel.name) + put(KEY_AMOUNT, amount) + putOpt(KEY_RELOAD_THRESHOLD_AMOUNT, reloadThresholdAmount) + } + } + + companion object { + + private const val KEY_PRICING_MODEL = "pricing_model" + private const val KEY_AMOUNT = "price" + private const val KEY_RELOAD_THRESHOLD_AMOUNT = "reload_threshold_amount" + } +} diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalPricingModel.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalPricingModel.kt new file mode 100644 index 0000000000..6d2ebda3be --- /dev/null +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalPricingModel.kt @@ -0,0 +1,10 @@ +package com.braintreepayments.api.paypal + +/** + * The interval at which the payment is charged or billed. + */ +enum class PayPalPricingModel { + FIXED, + VARIABLE, + AUTO_RELOAD +} diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingDetails.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingDetails.kt new file mode 100644 index 0000000000..23635e7a35 --- /dev/null +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingDetails.kt @@ -0,0 +1,71 @@ +package com.braintreepayments.api.paypal + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.json.JSONArray +import org.json.JSONObject + +/** + * PayPal recurring billing product details + * + * @property totalAmount + * @property billingCycles A list of billing cycles for trial billing and regular billing. A plan + * can have at most two trial cycles and only one regular cycle. + * @property currencyISOCode The three-character ISO-4217 currency code that identifies the + * currency. + * @property productName The name of the plan to display at checkout. + * @property oneTimeFeeAmount Price and currency for any one-time charges due at plan signup. + * @property productDescription Product description to display at the checkout. + * @property productAmount The item price for the product associated with the billing cycle at the + * time of checkout. + * @property productQuantity Quantity associated with the product. + * @property shippingAmount The shipping amount for the billing cycle at the time of checkout. + * @property taxAmount The taxes for the billing cycle at the time of checkout. + */ +@Parcelize +data class PayPalRecurringBillingDetails @JvmOverloads constructor( + val billingCycles: List, + val totalAmount: String, + val currencyISOCode: String, + var productName: String? = null, + var oneTimeFeeAmount: String? = null, + var productDescription: String? = null, + var productAmount: String? = null, + var productQuantity: Int? = null, + var shippingAmount: String? = null, + var taxAmount: String? = null, +) : Parcelable { + + fun toJson(): JSONObject { + return JSONObject().apply { + put(KEY_BILLING_CYCLES, JSONArray().apply { + for (billingCycle in billingCycles) { + put(billingCycle.toJson()) + } + }) + put(KEY_TOTAL_AMOUNT, totalAmount) + put(KEY_CURRENCY_ISO_CODE, currencyISOCode) + putOpt(KEY_PRODUCT_NAME, productName) + putOpt(KEY_ONE_TIME_FEE_AMOUNT, oneTimeFeeAmount) + putOpt(KEY_PRODUCT_DESCRIPTION, productDescription) + putOpt(KEY_PRODUCT_PRICE, productAmount) + putOpt(KEY_PRODUCT_QUANTITY, productQuantity) + putOpt(KEY_SHIPPING_AMOUNT, shippingAmount) + putOpt(KEY_TAX_AMOUNT, taxAmount) + } + } + + companion object { + + private const val KEY_BILLING_CYCLES = "billing_cycles" + private const val KEY_CURRENCY_ISO_CODE = "currency_iso_code" + private const val KEY_PRODUCT_NAME = "name" + private const val KEY_ONE_TIME_FEE_AMOUNT = "one_time_fee_amount" + private const val KEY_PRODUCT_DESCRIPTION = "product_description" + private const val KEY_PRODUCT_PRICE = "product_price" + private const val KEY_PRODUCT_QUANTITY = "product_quantity" + private const val KEY_SHIPPING_AMOUNT = "shipping_amount" + private const val KEY_TAX_AMOUNT = "tax_amount" + private const val KEY_TOTAL_AMOUNT = "total_amount" + } +} diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingPlanType.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingPlanType.kt new file mode 100644 index 0000000000..3505a62045 --- /dev/null +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRecurringBillingPlanType.kt @@ -0,0 +1,28 @@ +package com.braintreepayments.api.paypal + +/** + * PayPal recurring billing plan type, or charge pattern. + */ +enum class PayPalRecurringBillingPlanType { + /** + * Variable amount, fixed frequency, no defined duration. (E.g., utility bills, insurance). + */ + RECURRING, + + /** + * Fixed amount, fixed frequency, defined duration. (E.g., pay for furniture using monthly + * payments). + */ + INSTALLMENT, + + /** + * Fixed or variable amount, variable freq, no defined duration. (E.g., Coffee shop card reload, + * prepaid road tolling). + */ + UNSCHEDULED, + + /** + * Fixed amount, fixed frequency, no defined duration. (E.g., Streaming service). + */ + SUBSCRIPTION +} diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.java b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.java index 45c1d15dbd..946bff62ae 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.java +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.java @@ -49,6 +49,10 @@ public abstract class PayPalRequest implements Parcelable { static final String LINE_ITEMS_KEY = "line_items"; static final String USER_ACTION_KEY = "user_action"; + static final String PLAN_TYPE_KEY = "plan_type"; + + static final String PLAN_METADATA_KEY = "plan_metadata"; + @Retention(RetentionPolicy.SOURCE) @StringDef({PayPalRequest.LANDING_PAGE_TYPE_BILLING, PayPalRequest.LANDING_PAGE_TYPE_LOGIN}) @interface PayPalLandingPageType { diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.java b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.java index fa0665d5e3..8d20367ec8 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.java +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.java @@ -21,6 +21,8 @@ public class PayPalVaultRequest extends PayPalRequest implements Parcelable { private boolean shouldOfferCredit; + private PayPalRecurringBillingDetails recurringBillingDetails; + private PayPalRecurringBillingPlanType recurringBillingPlanType; private String userAuthenticationEmail; @@ -64,10 +66,35 @@ public void setUserAuthenticationEmail(@Nullable String userAuthenticationEmail) public String getUserAuthenticationEmail() { return this.userAuthenticationEmail; } + + /** + * Optional: Recurring billing product details. + * + * @param recurringBillingDetails {@link PayPalRecurringBillingDetails} + */ + public void setRecurringBillingDetails(PayPalRecurringBillingDetails recurringBillingDetails) { + this.recurringBillingDetails = recurringBillingDetails; + } + + public PayPalRecurringBillingDetails getRecurringBillingDetails() { + return recurringBillingDetails; + } + + /** + * Optional: Recurring billing plan type, or charge pattern. + * + * @param recurringBillingPlanType {@link PayPalRecurringBillingPlanType} + */ + public void setRecurringBillingPlanType(PayPalRecurringBillingPlanType recurringBillingPlanType) { + this.recurringBillingPlanType = recurringBillingPlanType; + } + + public PayPalRecurringBillingPlanType getRecurringBillingPlanType() { + return recurringBillingPlanType; + } String createRequestBody(Configuration configuration, Authorization authorization, String successUrl, String cancelUrl) throws JSONException { - JSONObject parameters = new JSONObject() .put(RETURN_URL_KEY, successUrl) .put(CANCEL_URL_KEY, cancelUrl) @@ -132,18 +159,31 @@ String createRequestBody(Configuration configuration, Authorization authorizatio } parameters.put(EXPERIENCE_PROFILE_KEY, experienceProfile); + + if (getRecurringBillingPlanType() != null) { + parameters.put(PLAN_TYPE_KEY, recurringBillingPlanType); + } + + if (getRecurringBillingDetails() != null) { + parameters.put(PLAN_METADATA_KEY, recurringBillingDetails.toJson()); + } + return parameters.toString(); } PayPalVaultRequest(Parcel in) { super(in); shouldOfferCredit = in.readByte() != 0; + recurringBillingDetails = in.readParcelable(PayPalRecurringBillingDetails.class.getClassLoader()); + recurringBillingPlanType = (PayPalRecurringBillingPlanType) in.readSerializable(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeByte((byte) (shouldOfferCredit ? 1 : 0)); + dest.writeParcelable(recurringBillingDetails, flags); + dest.writeSerializable(recurringBillingPlanType); } @Override diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalVaultRequestUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalVaultRequestUnitTest.java index 58671e8647..62412b5dfa 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalVaultRequestUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalVaultRequestUnitTest.java @@ -1,26 +1,28 @@ package com.braintreepayments.api.paypal; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + import android.os.Parcel; import com.braintreepayments.api.core.Authorization; import com.braintreepayments.api.core.Configuration; import com.braintreepayments.api.core.PostalAddress; -import com.braintreepayments.api.paypal.PayPalLineItem; -import com.braintreepayments.api.paypal.PayPalRequest; -import com.braintreepayments.api.paypal.PayPalVaultRequest; +import com.braintreepayments.api.testutils.Fixtures; import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.skyscreamer.jsonassert.JSONAssert; import java.util.ArrayList; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; -import static org.mockito.Mockito.mock; +import java.util.List; +import java.util.Objects; @RunWith(RobolectricTestRunner.class) public class PayPalVaultRequestUnitTest { @@ -52,6 +54,27 @@ public void setsValuesCorrectly() { request.setLandingPageType(PayPalRequest.LANDING_PAGE_TYPE_LOGIN); request.setShouldOfferCredit(true); request.setAppLinkEnabled(true); + PayPalBillingInterval billingInterval = PayPalBillingInterval.MONTH; + PayPalPricingModel pricingModel = PayPalPricingModel.FIXED; + PayPalBillingPricing billingPricing = + new PayPalBillingPricing(pricingModel, "1.00"); + billingPricing.setReloadThresholdAmount("6.00"); + PayPalBillingCycle billingCycle = + new PayPalBillingCycle(billingInterval, 1, 2); + billingCycle.setSequence(1); + billingCycle.setStartDate("2024-04-06T00:00:00Z"); + billingCycle.setTrial(true); + billingCycle.setPricing(billingPricing); + PayPalRecurringBillingDetails billingDetails = + new PayPalRecurringBillingDetails(List.of(billingCycle), "11.00", "USD"); + billingDetails.setOneTimeFeeAmount("2.00"); + billingDetails.setProductName("A Product"); + billingDetails.setProductDescription("A Description"); + billingDetails.setProductQuantity(1); + billingDetails.setShippingAmount("5.00"); + billingDetails.setTaxAmount("3.00"); + request.setRecurringBillingDetails(billingDetails); + request.setRecurringBillingPlanType(PayPalRecurringBillingPlanType.RECURRING); assertEquals("US", request.getLocaleCode()); assertEquals("Billing Agreement Description", request.getBillingAgreementDescription()); @@ -63,6 +86,27 @@ public void setsValuesCorrectly() { assertTrue(request.getShouldOfferCredit()); assertTrue(request.hasUserLocationConsent()); assertTrue(request.isAppLinkEnabled()); + assertEquals(PayPalRecurringBillingPlanType.RECURRING, request.getRecurringBillingPlanType()); + assertEquals("USD", request.getRecurringBillingDetails().getCurrencyISOCode()); + assertEquals("2.00", request.getRecurringBillingDetails().getOneTimeFeeAmount()); + assertEquals("A Product", request.getRecurringBillingDetails().getProductName()); + assertEquals("A Description", request.getRecurringBillingDetails().getProductDescription()); + assertSame(1, + Objects.requireNonNull(request.getRecurringBillingDetails().getProductQuantity())); + assertEquals("5.00", request.getRecurringBillingDetails().getShippingAmount()); + assertEquals("3.00", request.getRecurringBillingDetails().getTaxAmount()); + assertEquals("11.00", request.getRecurringBillingDetails().getTotalAmount()); + PayPalBillingCycle requestBillingCycle = request.getRecurringBillingDetails().getBillingCycles().get(0); + assertEquals(PayPalBillingInterval.MONTH, requestBillingCycle.getInterval()); + assertEquals(1, requestBillingCycle.getIntervalCount()); + assertEquals(2, requestBillingCycle.getNumberOfExecutions()); + assertEquals("2024-04-06T00:00:00Z", requestBillingCycle.getStartDate()); + assertSame(1, requestBillingCycle.getSequence()); + assertTrue(requestBillingCycle.isTrial()); + PayPalBillingPricing requestBillingPricing = requestBillingCycle.getPricing(); + assertEquals("6.00", requestBillingPricing.getReloadThresholdAmount()); + assertEquals("1.00", requestBillingPricing.getAmount()); + assertEquals(PayPalPricingModel.FIXED, requestBillingPricing.getPricingModel()); } @Test @@ -84,6 +128,28 @@ public void parcelsCorrectly() { request.setRiskCorrelationId("123-correlation"); request.setMerchantAccountId("merchant_account_id"); + PayPalBillingInterval billingInterval = PayPalBillingInterval.MONTH; + PayPalPricingModel pricingModel = PayPalPricingModel.FIXED; + PayPalBillingPricing billingPricing = + new PayPalBillingPricing(pricingModel, "1.00"); + billingPricing.setReloadThresholdAmount("6.00"); + PayPalBillingCycle billingCycle = + new PayPalBillingCycle(billingInterval, 1, 2); + billingCycle.setSequence(1); + billingCycle.setStartDate("2024-04-06T00:00:00Z"); + billingCycle.setTrial(true); + billingCycle.setPricing(billingPricing); + PayPalRecurringBillingDetails billingDetails = + new PayPalRecurringBillingDetails(List.of(billingCycle), "11.00", "USD"); + billingDetails.setOneTimeFeeAmount("2.00"); + billingDetails.setProductName("A Product"); + billingDetails.setProductDescription("A Description"); + billingDetails.setProductQuantity(1); + billingDetails.setShippingAmount("5.00"); + billingDetails.setTaxAmount("3.00"); + request.setRecurringBillingDetails(billingDetails); + request.setRecurringBillingPlanType(PayPalRecurringBillingPlanType.RECURRING); + ArrayList lineItems = new ArrayList<>(); lineItems.add(new PayPalLineItem(PayPalLineItem.KIND_DEBIT, "An Item", "1", "1")); request.setLineItems(lineItems); @@ -109,6 +175,27 @@ public void parcelsCorrectly() { assertEquals("An Item", result.getLineItems().get(0).getName()); assertTrue(result.hasUserLocationConsent()); assertTrue(result.isAppLinkEnabled()); + assertEquals(PayPalRecurringBillingPlanType.RECURRING, result.getRecurringBillingPlanType()); + assertEquals("USD", result.getRecurringBillingDetails().getCurrencyISOCode()); + assertEquals("2.00", result.getRecurringBillingDetails().getOneTimeFeeAmount()); + assertEquals("A Product", result.getRecurringBillingDetails().getProductName()); + assertEquals("A Description", result.getRecurringBillingDetails().getProductDescription()); + assertSame(1, + Objects.requireNonNull(result.getRecurringBillingDetails().getProductQuantity())); + assertEquals("5.00", result.getRecurringBillingDetails().getShippingAmount()); + assertEquals("3.00", result.getRecurringBillingDetails().getTaxAmount()); + assertEquals("11.00", result.getRecurringBillingDetails().getTotalAmount()); + PayPalBillingCycle resultBillingCycle = result.getRecurringBillingDetails().getBillingCycles().get(0); + assertEquals(PayPalBillingInterval.MONTH, resultBillingCycle.getInterval()); + assertEquals(1, resultBillingCycle.getIntervalCount()); + assertEquals(2, resultBillingCycle.getNumberOfExecutions()); + assertEquals("2024-04-06T00:00:00Z", resultBillingCycle.getStartDate()); + assertSame(1, resultBillingCycle.getSequence()); + assertTrue(resultBillingCycle.isTrial()); + PayPalBillingPricing resultBillingPricing = resultBillingCycle.getPricing(); + assertEquals("6.00", resultBillingPricing.getReloadThresholdAmount()); + assertEquals("1.00", resultBillingPricing.getAmount()); + assertEquals(PayPalPricingModel.FIXED, resultBillingPricing.getPricingModel()); } @Test @@ -126,4 +213,56 @@ public void createRequestBody_sets_userAuthenticationEmail_when_not_null() throw assertTrue(requestBody.contains("\"payer_email\":" + "\"" + payerEmail + "\"")); } + + @Test + public void createRequestBody_correctlyFormatsJSON() throws JSONException { + PayPalVaultRequest request = new PayPalVaultRequest(true); + request.setLocaleCode("en-US"); + request.setBillingAgreementDescription("Billing Agreement Description"); + request.setShippingAddressRequired(true); + request.setShippingAddressEditable(true); + request.setShouldOfferCredit(true); + request.setAppLinkEnabled(true); + request.setUserAuthenticationEmail("email"); + + PostalAddress postalAddress = new PostalAddress(); + postalAddress.setRecipientName("Postal Address"); + request.setShippingAddressOverride(postalAddress); + + request.setLandingPageType(PayPalRequest.LANDING_PAGE_TYPE_LOGIN); + request.setDisplayName("Display Name"); + request.setRiskCorrelationId("123-correlation"); + request.setMerchantAccountId("merchant_account_id"); + + PayPalBillingInterval billingInterval = PayPalBillingInterval.MONTH; + PayPalPricingModel pricingModel = PayPalPricingModel.VARIABLE; + PayPalBillingPricing billingPricing = + new PayPalBillingPricing(pricingModel, "1.00"); + billingPricing.setReloadThresholdAmount("6.00"); + PayPalBillingCycle billingCycle = + new PayPalBillingCycle(billingInterval, 1, 2); + billingCycle.setSequence(1); + billingCycle.setStartDate("2024-04-06T00:00:00Z"); + billingCycle.setTrial(true); + billingCycle.setPricing(billingPricing); + PayPalRecurringBillingDetails billingDetails = + new PayPalRecurringBillingDetails(List.of(billingCycle), "11.00", "USD"); + billingDetails.setOneTimeFeeAmount("2.00"); + billingDetails.setProductName("A Product"); + billingDetails.setProductDescription("A Description"); + billingDetails.setProductQuantity(1); + billingDetails.setShippingAmount("5.00"); + billingDetails.setTaxAmount("3.00"); + request.setRecurringBillingDetails(billingDetails); + request.setRecurringBillingPlanType(PayPalRecurringBillingPlanType.RECURRING); + + String requestBody = request.createRequestBody( + mock(Configuration.class), + mock(Authorization.class), + "success_url", + "cancel_url" + ); + + JSONAssert.assertEquals(Fixtures.PAYPAL_REQUEST_JSON, requestBody, false); + } } diff --git a/TestUtils/src/main/java/com/braintreepayments/api/testutils/Fixtures.kt b/TestUtils/src/main/java/com/braintreepayments/api/testutils/Fixtures.kt index 45c2f3e756..74746292ab 100644 --- a/TestUtils/src/main/java/com/braintreepayments/api/testutils/Fixtures.kt +++ b/TestUtils/src/main/java/com/braintreepayments/api/testutils/Fixtures.kt @@ -2133,6 +2133,50 @@ object Fixtures { // endregion // region PayPal + + // language=JSON + const val PAYPAL_REQUEST_JSON = """ + { + "return_url": "success_url", + "cancel_url": "cancel_url", + "offer_paypal_credit": true, + "description": "Billing Agreement Description", + "shipping_address": { + "recipient_name": "Postal Address"}, + "merchant_account_id": "merchant_account_id", + "correlation_id": "123-correlation", + "experience_profile": { + "no_shipping": false, + "landing_page_type": "login", + "brand_name": "Display Name", + "locale_code": "en-US", + "address_override": false}, + "payer_email" : "email", + "plan_type": "RECURRING", + "plan_metadata": { + "billing_cycles": [{ + "billing_frequency_unit": "MONTH", + "billing_frequency": 1, + "number_of_executions": 2, + "sequence": 1, + "start_date": "2024-04-06T00:00:00Z", + "trial": true, + "pricing_scheme": { + "pricing_model": "VARIABLE", + "price": "1.00", + "reload_threshold_amount": "6.00"}}], + "total_amount": "11.00", + "currency_iso_code": "USD", + "name": "A Product", + "one_time_fee_amount": "2.00", + "product_description": "A Description", + "product_quantity": 1, + "shipping_amount": "5.00", + "tax_amount": "3.00" + } + } + """ + // language=JSON const val PAYPAL_HERMES_BILLING_AGREEMENT_RESPONSE = """ {