More Tutorials

Build Your Custom Checkout Form with Dynamically Changing Order Summary

Checkout
Product Catalog 1.0

In-app checkout page provides you with the flexibility to be able to provide a unique checkout experience for your users. The drawbacks are you need to ensure security. Also if you want to show a order summary , then you would have to do the various calculations (like tax , coupon support) etc. Chargebee's Estimate API allows you to dynamically update the order summary based on user selections without having to hard-code the prices or validate coupons.

Alternative Options

The simplest way to setup your checkout process with Chargebee is by using the hosted payment pages or the Hosted Pages + API integration method. Chargebee’s hosted pages are built using the Bootstrap themes.

Overview

What is Estimate API?

Estimate api allows you to calculate the 'invoice' that would be raised on completion of a operation such as creating a subscription or updating a subscription. Using the returned estimate you could show a order summary. This allows you to dynamically update the order summary based on user selection. You can avoid hard-coding the prices or validating the coupon codes for updating it.

How do I use it?

Whenever a user selects an addon or applies a coupon you just need to call the estimate api to get the line items for the order summary. Additionally if you want allow users to apply coupon codes, you should handle the coupon errors such as wrong code and show the message appropriately.

Honey Comics - Demo Application

'Honey Comics', our demo application, is a fictitious online comic book store providing a subscription service for comics. We send physical comic books every month. In addition to comic books we sell wallpapers of the famous heros and also provide a option for users to opt for a e-book version of the comic to be sent along with comics. The wallpapers and e-book are modeled as add-ons in Chargebee.


Prerequisites

To try out the tutorial yourself, you'll need the following:

  • A Chargebee account. Signup for a free trial if you don't have one.
  • Create the following configuration in your Chargebee site. (Note: You can setup the plan and addons for the demo using the "Setup Configuration" option in the index page if you have downloaded code and started the tutorials locally).
    1. A plan with id 'monthly' in your Chargebee Site.
    2. A 'On Off' addon with id 'e-book' in Chargebee.
    3. A 'Quantity' type addon with id 'wall-poster' in Chargebee.
  • Your Chargebee API key for your test site.

Setup the Chargebee client library

You have to download and import the client library of our choice. Then, configure the client library with your test site and its api key.

For the tutorial, we have configured the site and the credentials in a separate properties file. When the webapp is initialized, the client library gets configured.

EnvInitializer.javaView full code
/**
  * The credentials are stored in a properties file under WEB-INF
  * The live site api keys should be stored securely. It should preferably
  * be stored only in the production machine(s) and not hard coded
  * in code or checked into a version control system by mistake.
  */
Properties credentials = read("WEB-INF/ChargeBeeCredentials.properties");
Environment.configure(credentials.getProperty("site"), credentials.getProperty("api_key"));

Implementing the dynamic order summary using Estimate api


Requesting updated order summary in client

We will start with the client side implementation. Whenever the user changes e-book option (or updates wallpaper quantity or applies a coupon) we will request the server for the updated order summary. We do this via javascript as shown below

estimate.jsView full code
$('#order_summary').on('click', '#apply-coupon', function(e) {
    if ($('#coupon').val().trim() == '') {
        $('.error_msg').text("invalid coupon code");
        $('.error_msg').show();
    } else {
        sendAjaxRequest();
    }
})

$('#order_summary').on('click', '#remove-coupon', function(e) {
    $('#coupon').removeAttr("value");
    sendAjaxRequest();
})

$('.addons').on('change', '.wallposters-quantity', function(e) {
    sendAjaxRequest();
})

$('.addons').on('change', '.ebook', function(e) {
    sendAjaxRequest();
})

function sendAjaxRequest() {
    var wallpostersQuantity, ebook, coupon;
    if ($('.wallposters').is(":checked")) {
        wallpostersQuantity = $('.wallposters-quantity').val();
    }
    if ($('.ebook').is(':checked')) {
        ebook = "true";
    }
    if ($('#coupon').val().trim() != '') {
        coupon = $('#coupon').val().trim();
    }
    parameters = {"wallposters-quantity": wallpostersQuantity,
        "ebook": ebook,
        "coupon": coupon
    }
    orderSummaryAjaxHandler(parameters)
}

function orderSummaryAjaxHandler(dataContent) {
    $.ajax({
        url: "order_summary",
        data: dataContent,
        beforeSend: function(data, textstatus, jqXHR) {
            $('.text-danger').text('');
            $('.ajax-loader').show();
        },
        success: function(data, textstatus, jqXHR) {
            $('#order_summary').html(data);
        },
        error: function(data, textstatus, jqXHR) {
            try {
                var error = JSON.parse(data.responseText);
                $('.error_msg').text(error.error_msg);
            } catch (e) {
                $('.error_msg').text("Internal Server Error");
            }
            $('.error_msg').show();
        },
        complete: function() {
            $('.ajax-loader').hide();
        }

    });
}

Handling order summary requests in server

We will invoke the invoke the estimate api with the various options selected by the user.

EstimateCheckout.javaView full code
/*
* Returns estimate object by applying the addons and coupons set by user.
*/
public static Estimate getOrderSummary(HttpServletRequest request) 
        throws IOException, Exception {
    /* 
    * Forming create subscription estimate parameters to ChargeBee.
    */
    Estimate.CreateSubscriptionRequest estimateReq = Estimate.
                createSubscription().subscriptionPlanId("monthly");

    /*
    * Adding addon1 to the create subscription estimate request,
    * if it is set by user.
    */
    if (request.getParameter("wallposters-quantity") != null &&
              !"".equals(request.getParameter("wallposters-quantity"))){
        String quantityParam = request.getParameter("wallposters-quantity");
        Integer quantity = Integer.parseInt(quantityParam);
        estimateReq.addonId(0, "wall-posters")
                    .addonQuantity(0, quantity);
    }

    /*
    * Adding addon2 to the create subscription estimate request,
    * if it is set by user.
    */
    if (request.getParameter("ebook") != null &&
              "true".equals(request.getParameter("ebook"))){
        estimateReq.addonId(1, "e-book");
    }

    /*
    * Adding coupon to the create subscription estimate request,
    * if it is set by user.
    */
    if (request.getParameter("coupon") != null &&
              !"".equals(request.getParameter("coupon"))){
        estimateReq.subscriptionCoupon(request.getParameter("coupon"));
    }

    /*
    * Sending request to the ChargeBee.
    */
    Result result = estimateReq.request();
    Utils.log("estimate.json", "estimate_response", result);
    return result.estimate();
}

The response(as shown below) contains the estimate based on the options. We use it to create our order summary snippet which we send back to client.

{
  "estimate": {
    "created_at": 1464359301,
    "invoice_estimate": {
      "amount_due": 15300,
      "amount_paid": 0,
      "credits_applied": 0,
      "discounts": [
        {
          "amount": 1700,
          "description": "SPIDERMAN25",
          "entity_id": "SPIDERMAN25",
          "entity_type": "document_level_coupon",
          "object": "discount"
        }
      ],
      "line_item_taxes": [],
      "line_items": [
        {
          "amount": 1000,
          "date_from": 1464359301,
          "date_to": 1467037701,
          "description": "monthly",
          "discount_amount": 100,
          "entity_id": "monthly",
          "entity_type": "plan",
          "id": "li_HtZEwLjPmPeOXR1MDc",
          "is_taxed": false,
          "item_level_discount_amount": 0,
          "object": "line_item",
          "quantity": 1,
          "tax_amount": 0,
          "unit_amount": 1000
        },
        {
          "amount": 15000,
          "date_from": 1464359301,
          "date_to": 1467037701,
          "description": "wall-posters",
          "discount_amount": 1500,
          "entity_id": "wall-posters",
          "entity_type": "addon",
          "id": "li_HtZEwLjPmPeOXR1MDd",
          "is_taxed": false,
          "item_level_discount_amount": 0,
          "object": "line_item",
          "quantity": 3,
          "tax_amount": 0,
          "unit_amount": 5000
        },
        {
          "amount": 1000,
          "date_from": 1464359301,
          "date_to": 1467037701,
          "description": "e-book",
          "discount_amount": 100,
          "entity_id": "e-book",
          "entity_type": "addon",
          "id": "li_HtZEwLjPmPeOXR1MDe",
          "is_taxed": false,
          "item_level_discount_amount": 0,
          "object": "line_item",
          "quantity": 1,
          "tax_amount": 0,
          "unit_amount": 1000
        }
      ],
      "object": "invoice_estimate",
      "price_type": "tax_exclusive",
      "recurring": true,
      "sub_total": 17000,
      "taxes": [],
      "total": 15300
    },
    "object": "estimate",
    "subscription_estimate": {
      "next_billing_at": 1467037701,
      "object": "subscription_estimate",
      "status": "active"
    }
  }
}
EstimateCheckout.javaView full code
<% List<InvoiceEstimate.LineItem> lineItems = invoiceEstimate.lineItems(); %>
<div class="row">
  <div class="col-xs-12">
    <div class="page-header">
      <h3>Your Order Summary</h3>
    </div>
      <ul class="text-right list-unstyled">
        <% for (LineItem li : lineItems) { %>
          <li class="row">
            <span class="col-xs-8"> 
              <%=Utils.esc(li.description())%> 
              <%=" &times; " + Utils.esc(li.quantity() + " item(s)")%>
            </span>
            <span class="col-xs-4">$ 
              <label> 
                <%= Utils.esc(String.format("%d.%02d", li.amount() / 100, li.amount() % 100))%>
              </label>
            </span>
          </li>
        <% } %>

Implementing checkout.

When user subscribes by providing card and other details we first validate the inputs in client for immediate feedback incase of error. We are using Stripe's client validation library for the card related validations.

estimate.jsView full code
var validatePaymentDetails = function(form) {
    var errorMap = {};
    if (!$.payment.validateCardNumber($('#card_no').val())) {
        errorMap[$('#card_no').attr('name')] = 'invalid card number';
    }
    if (!$.payment.validateCardExpiry($('#expiry_month').val(),
            $('#expiry_year').val())) {
        errorMap[$('#expiry_month').attr('name')] = 'invalid expiry date';
    }
    if (!$.payment.validateCardCVC($('#cvc').val(),
               $.payment.cardType($('#card_no').val()) ) ) {
        errorMap[$('#cvc').attr('name')] = 'invalid cvc number';
    }
    if(jQuery.isEmptyObject(errorMap)){
        return true;
    }else{
        $(form).validate().showErrors(errorMap);
        return false;
    }
};

On the server side we fill the parameters required for create subscription api.

EstimateCheckout.javaView full code
/*
* Forming create subscription request parameters to ChargeBee.
*/
Subscription.CreateRequest createSubcriptionRequest = Subscription.create()
    .planId("monthly")
    .customerFirstName(request.getParameter("customer[first_name]"))
    .customerLastName(request.getParameter("customer[last_name]"))
    .customerEmail(request.getParameter("customer[email]"))
    .customerPhone(request.getParameter("customer[phone]"))
    .cardNumber(request.getParameter("card_no"))
    .cardExpiryMonth(Integer.parseInt(request.getParameter("expiry_month")))
    .cardExpiryYear(Integer.parseInt(request.getParameter("expiry_year")))
    .cardCvv(request.getParameter("cvc"));

We also add the addons and coupons based on the customer's input

EstimateCheckout.javaView full code
/*
* Adding addon1 to the create subscription request, if it is set by user.
*/
if(request.getParameter("wallposters-quantity") != null && 
        !"".equals(request.getParameter("wallposters-quantity"))) {
    String quantityParam = request.getParameter("wallposters-quantity");
    Integer quantity = Integer.parseInt(quantityParam);
    createSubcriptionRequest.addonId(0, "wall-posters")
              .addonQuantity(0, quantity);
}

We then invoke the api. The response(as shown below) contains the subscription details which we could store in our db. We also update the shipping address via a separate api call. At the end we forward the user to the 'thank you' page.

{
   "card": {
      "card_type": "visa",
      "customer_id": "1sjs9ilPmPes9A19xq",
      "expiry_month": 1,
      "expiry_year": 2020,
      "gateway": "chargebee",
      "iin": "411111",
      "last4": "1111",
      "masked_number": "************1111",
      "object": "card",
      "status": "valid"
   },
   "customer": {
      "allow_direct_debit": false,
      "auto_collection": "on",
      "card_status": "valid",
      "created_at": 1464359415,
      "email": "john@acmeinc.com",
      "excess_payments": 0,
      "first_name": "John",
      "id": "1sjs9ilPmPes9A19xq",
      "last_name": "Doe",
      "object": "customer",
      "payment_method": {
         "gateway": "chargebee",
         "object": "payment_method",
         "reference_id": "tok_1sjs9ilPmPes9f19xr",
         "status": "valid",
         "type": "card"
      },
      "phone": "1956781276",
      "promotional_credits": 0,
      "refundable_credits": 0,
      "taxability": "taxable"
   },
   "invoice": {
      "adjustment_credit_notes": [],
      "amount_adjusted": 0,
      "amount_due": 0,
      "amount_paid": 15300,
      "applied_credits": [],
      "billing_address": {
         "first_name": "John",
         "last_name": "Doe",
         "object": "billing_address"
      },
      "credits_applied": 0,
      "currency_code": "USD",
      "customer_id": "1sjs9ilPmPes9A19xq",
      "date": 1464359415,
      "discounts": [{
         "amount": 1700,
         "description": "SPIDERMAN25",
         "entity_id": "SPIDERMAN25",
         "entity_type": "document_level_coupon",
         "object": "discount"
      }],
      "first_invoice": true,
      "id": "850",
      "issued_credit_notes": [],
      "line_items": [
         {
            "amount": 1000,
            "date_from": 1464359415,
            "date_to": 1467037815,
            "description": "monthly",
            "discount_amount": 100,
            "entity_id": "monthly",
            "entity_type": "plan",
            "id": "li_1sjs9ilPmPesAa19xt",
            "is_taxed": false,
            "item_level_discount_amount": 0,
            "object": "line_item",
            "quantity": 1,
            "tax_amount": 0,
            "unit_amount": 1000
         },
         {
            "amount": 15000,
            "date_from": 1464359415,
            "date_to": 1467037815,
            "description": "wall-posters",
            "discount_amount": 1500,
            "entity_id": "wall-posters",
            "entity_type": "addon",
            "id": "li_1sjs9ilPmPesAc19xu",
            "is_taxed": false,
            "item_level_discount_amount": 0,
            "object": "line_item",
            "quantity": 3,
            "tax_amount": 0,
            "unit_amount": 5000
         },
         {
            "amount": 1000,
            "date_from": 1464359415,
            "date_to": 1467037815,
            "description": "e-book",
            "discount_amount": 100,
            "entity_id": "e-book",
            "entity_type": "addon",
            "id": "li_1sjs9ilPmPesAc19xv",
            "is_taxed": false,
            "item_level_discount_amount": 0,
            "object": "line_item",
            "quantity": 1,
            "tax_amount": 0,
            "unit_amount": 1000
         }
      ],
      "linked_orders": [],
      "linked_payments": [{
         "applied_amount": 15300,
         "applied_at": 1464359415,
         "txn_amount": 15300,
         "txn_date": 1464359415,
         "txn_id": "txn_1sjs9ilPmPesBh19xx",
         "txn_status": "success"
      }],
      "object": "invoice",
      "paid_at": 1464359415,
      "price_type": "tax_exclusive",
      "recurring": true,
      "status": "paid",
      "sub_total": 17000,
      "subscription_id": "1sjs9ilPmPes9A19xq",
      "tax": 0,
      "total": 15300,
      "write_off_amount": 0
   },
   "subscription": {
      "activated_at": 1464359415,
      "addons": [
         {
            "id": "wall-posters",
            "object": "addon",
            "quantity": 3
         },
         {
            "id": "e-book",
            "object": "addon",
            "quantity": 1
         }
      ],
      "coupon": "SPIDERMAN25",
      "coupons": [{
         "applied_count": 1,
         "coupon_id": "SPIDERMAN25",
         "object": "coupon"
      }],
      "created_at": 1464359415,
      "current_term_end": 1467037815,
      "current_term_start": 1464359415,
      "customer_id": "1sjs9ilPmPes9A19xq",
      "due_invoices_count": 0,
      "has_scheduled_changes": false,
      "id": "1sjs9ilPmPes9A19xq",
      "object": "subscription",
      "plan_id": "monthly",
      "plan_quantity": 1,
      "started_at": 1464359415,
      "status": "active"
   }
}
EstimateCheckout.javaView full code
/*
* Sending request to the ChargeBee.
*/
Result result = createSubcriptionRequest.request();
/*
* Adds shipping address to the subscription using the subscription Id 
* returned during create subscription response.
*/
addShippingAddress(result.subscription().id(), request);
/*
* Forwarding to thank you page.
*/
respJson.put("forward", "thankyou.html");
out.write(respJson.toString());

Validation and Error Handling

Here's how we validate user inputs and handle API call errors in this demo:

  • Client Side Validation: Chargebee uses jQuery form validation plugin to check whether the user’s field inputs(email, zip code and phone number) are valid or not.
  • Credit Card Validation: We use Stripe's client validation library to validate credit card details.

  • Server Side Validation: As this is a demo application we have skipped the server side validation of all input parameters. But we recommend you to perform the validation at your end.

  • Coupon Errors: If a user enters a coupon that is invalid, expired, exhausted or not applicable to a given plan, Chargebee returns an error response(as shown below) that contains the following attributes: api_error_code, param. Using these attributes you will be able to identify different coupon errors and display custom error messages.

    {
      "api_error_code": "resource_not_found",
      "error_code": "referenced_resource_not_found",
      "error_msg": "The SPIDERMAN25 referenced in parameter subscription[coupon] is not present ",
      "error_param": "subscription[coupon]",
      "http_status_code": 404,
      "message": "subscription[coupon] : The SPIDERMAN25 referenced in parameter subscription[coupon] is not present ",
      "param": "subscription[coupon]",
      "type": "invalid_request"
    }
    
  • Payment Errors: If a payment fails due to card verification or processing errors, Chargebee returns an error response(as shown below) which is thrown as a payment exception by the client library. We handle the exceptions in the demo application with appropriate error messages.

    {
      "api_error_code": "payment_method_verification_failed",
      "error_code": "add_card_error",
      "error_msg": "Problem while adding the card. Error message : (3009) Do not honour.",
      "http_status_code": 400,
      "message": "Problem while adding the card. Error message : (3009) Do not honour.",
      "type": "payment"
    }
    
  • General API Errors: Chargebee might return error responses due to various reasons such as invalid configuration, bad request etc. To identify specific reasons for all error responses you can check the API documentation. Also take a look at the error handler file to check how these errors can be handled.

Test cards

Now that you're all set, why don't you test your integration with some test transactions. Here are some credit card numbers that you can use to test your application.

Valid Card 4111 1111 1111 1111
Verification Error Card 4119 8627 6033 8320
Transaction Error Card 4005 5192 0000 0004
Was this tutorial helpful ?
Need more help?

We're always happy to help you with any questions you might have!

support@chargebee.com