Skip to content

Commit

Permalink
Add Recurring Billing Agreement to PayPalVaultRequest (#1079)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalBillingPricing.kt

Co-authored-by: scannillo <[email protected]>

* Add JSON test

* Fix lint

---------

Co-authored-by: scannillo <[email protected]>
  • Loading branch information
sarahkoop and scannillo authored Jul 24, 2024
1 parent 99c3f5f commit d24b10b
Show file tree
Hide file tree
Showing 12 changed files with 465 additions and 10 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions PayPal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'com.android.library'
id 'kotlin-android'
id 'org.jetbrains.dokka'
id 'kotlin-parcelize'
}

android {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<PayPalBillingCycle>,
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"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
public class PayPalVaultRequest extends PayPalRequest implements Parcelable {

private boolean shouldOfferCredit;
private PayPalRecurringBillingDetails recurringBillingDetails;
private PayPalRecurringBillingPlanType recurringBillingPlanType;

private String userAuthenticationEmail;

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit d24b10b

Please sign in to comment.