A customer portal allows your customers to update their information such as their card and billing information. In addition they can view and download their invoices. You could build your own full fledged customer portal using Chargebee's extensive api.
Alternative options
Instead of building your own customer portal you could instead use the hosted customer portal provided by Chargebee.
Features in the sample Self Service Portal
The following features are supported.
- Viewing the current subscription details such as subscribed items,billing address, shipping address
- Editing billing and shipping addresses.
- Adding/Updating the card using the hosted updated card page.
- Listing the invoices and support for downloading the invoices.
- Canceling the subscription.
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.
/**
* 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"));
For the tutorial, we have configured the site credentials in config/environments/development.rb
ENV["CHARGEBEE_SITE"]="honeycomics-test"
ENV["CHARGEBEE_API_KEY"]="test_5LjFA6K6doB2EKRP7cufTd5TvT32a5BrT"
We setup the client library in config/initializers/chargebee.rb
ChargeBee.configure(:site => ENV["CHARGEBEE_SITE"], :api_key => ENV["CHARGEBEE_API_KEY"])
For the tutorial, we have configured the site credentials in Config.php
require_once(dirname(__FILE__) . "/lib/ChargeBee.php");
/*
* Sets the environment for calling the Chargebee API.
* You need to sign up at ChargeBee app to get this credential.
* It is better if you fetch configuration from the environment
* properties instead of hard coding it in code.
*/
ChargeBee_Environment::configure("honeycomics-test", "test_5LjFA6K6doB2EKRP7cufTd5TvT32a5BrT");
Authentication
The sample does not do "real" authentication. But rather just checks if the subscription id is present in your Chargebee site and subscription id is set in the session. The assumption is once you integrate the ssp into your app/website, the customer would login into your app/website you just need to integrate your authentication into sample ssp.
/*
* Verifying subscription id is present in ChargeBee.
*/
private boolean fetchSubscription(HttpServletRequest request) throws IOException, Exception {
try {
String username = request.getParameter("subscription_id");
if(username == null || username.isEmpty()) {
return false;
}
Result result = Subscription.retrieve(username).request();
HttpSession session = request.getSession();
session.setAttribute("subscription_id",
result.subscription().id());
session.setAttribute("customer_id",
result.customer().id());
return true;
} catch (InvalidRequestException ex) {
if ("resource_not_found".equals(ex.apiErrorCode)) {
return false;
}
throw ex;
}
}
# Verify Subscription Id is present in ChargeBee.
def fetch_subscription(_params)
subscription_id = _params['subscription_id']
if subscription_id.blank? == true
return false
end
begin
result = ChargeBee::Subscription.retrieve(subscription_id)
session[:subscription_id] = result.subscription.id
session[:customer_id] = result.customer.id
return true
rescue ChargeBee::APIError => e
if e.api_error_code == "resource_not_found"
return false
end
throw e
end
end
/*
* Verifying subscription id is present in ChargeBee.
*/
function fetchSubscription() {
$subscriptionId = $_POST['subscription_id'];
if( empty($subscriptionId) ) {
return false;
}
try {
$result = ChargeBee_Subscription::retrieve($subscriptionId);
session_start();
$_SESSION['subscription_id'] = $result->subscription()->id;
$_SESSION['customer_id'] = $result->customer()->id;
return true;
} catch ( ChargeBee_APIError $e ) {
if( $e->getApiErrorCode() == "resource_not_found" ) {
return false;
}
throw $e;
}
}
Building the Profile page.
The profile page allows the customer to see his information like subcribed items, his billing and shipping addresses etc. We use the retrieve subscription api that returns the customer,subscription and card details.
<%
Result result = Subscription.retrieve(subscriptionId).request();
%>
# Retrieves the Subscription details from ChargeBee
def subscription
@result = ChargeBee::Subscription.retrieve(@subscription_id)
@billing_address = @result.customer.billing_address
@country_codes = get_country_codes
@shipping_address = retrieve_shipping_address(@subscription_id)
@invoice_estimate = nil
if @result.subscription.status != "cancelled" && @result.subscription.status != "non_renewing"
@invoice_estimate = ChargeBee::Estimate.renewal_estimate(@subscription_id, {"use_existing_balances" => "true"}).estimate.invoice_estimate
end
@subscription_status = subscription_status()[@result.subscription.status]
end
<?php
$result = ChargeBee_Subscription::retrieve($subscriptionId);
?>
The result is used to display the information.
<div className="row">
<label className="col-sm-5 control-label">Name</label>
<div className="col-sm-7 form-control-static">
<%= Utils.esc(result.customer().firstName()) + " " + Utils.esc(result.customer().lastName())%>
</div>
</div>
<div className="row">
<label className="col-sm-5 control-label">Name</label>
<div className="col-sm-7 form-control-static">
<%= @result.customer.first_name %> <%= @result.customer.last_name %>
</div>
</div>
<div className="row">
<label className="col-sm-5 control-label">Name</label>
<div className="col-sm-7 form-control-static">
<?php echo esc($result->customer()->firstName . " " . $result->customer()->lastName) ?>
</div>
</div>
Editing account,billing and shipping addresses
The following api are used to allow the customers to add/update their information.
- The update customer api to update the account information such as name.
- The update billing api for adding/updating the customer's billing information.
- The update subscription api for adding/updating a customer's shipping information.
We have created specific forms for each of the above information as shown below.
<div className="form-group">
<label for="billing_address[line1]" >Address Line 1</label>
<small for="billing_address[line1]" className="pull-right text-danger"> </small>
<input type="text" name="billing_address[line1]" className="form-control"
placeholder="Enter your address line 1"
value="<%= Utils.esc(billingAddress != null ? billingAddress.line1() : "")%>"
maxlength="50" required data-msg-required="cannot be blank" />
</div>
<div className="row">
<label className="col-sm-5 control-label">Name</label>
<div className="col-sm-7 form-control-static">
<%= @result.customer.first_name %> <%= @result.customer.last_name %>
</div>
</div>
<div className="row">
<label className="col-sm-5 control-label">Name</label>
<div className="col-sm-7 form-control-static">
<?php echo esc($result->customer()->firstName . " " . $result->customer()->lastName) ?>
</div>
</div>
When the customer submits the form, we call the appropriate api
/*
* Update Billing info of customer in ChargeBee.
*/
private void updateBillingInfo(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
validateParameters(req);
try {
Customer.updateBillingInfo(getCustomerId(req))
.billingAddressFirstName(req.getParameter("billing_address[first_name]"))
.billingAddressLastName(req.getParameter("billing_address[last_name]"))
.billingAddressLine1(req.getParameter("billing_address[line1]"))
.billingAddressLine2(req.getParameter("billing_address[line2]"))
.billingAddressCity(req.getParameter("billing_address[city]"))
.billingAddressState(req.getParameter("billing_address[state]"))
.billingAddressCountry(req.getParameter("billing_address[country]"))
.billingAddressZip(req.getParameter("billing_address[zip]"))
.request();
out.write("{\"forward\" : \"/ssp/subscription.jsp\"}");
} catch (InvalidRequestException e) {
handleInvalidRequestErrors(e, resp, out, null);
} catch (Exception e) {
handleGeneralErrors(e, resp, out);
} finally {
out.flush();
}
}
# Update Billing info of customer in ChargeBee.
def update_billing_info
Validation.validateParameters(params)
billing_address = params['billing_address']
begin
ChargeBee::Customer.update_billing_info(@customer_id,
:billing_address => billing_address)
render json: {
:forward => "/ssp/subscription"
}
rescue ChargeBee::InvalidRequestError => e
ErrorHandler.handle_invalid_request_errors(e, self)
rescue Exception => e
ErrorHandler.handle_general_errors(e, self)
end
end
/*
* Update Billing info of customer in ChargeBee.
*/
function updateBillingInfo() {
header('Content-Type: application/json');
validateParameters($_POST);
$customerId = getCustomerId();
$billingAddrParams = $_POST['billing_address'];
try {
$result = ChargeBee_Customer::updateBillingInfo($customerId,
array("billing_address" => $billingAddrParams));
$jsonResponse = array("forward" => "/ssp-php/subscription");
print json_encode($jsonResponse, true);
} catch(ChargeBee_InvalidRequestException $e) {
handleInvalidRequestErrors($e);
} catch (Exception $e) {
handleGeneralErrors($e);
}
}
We have kept the form input names to follow the chargebee's curl parameter format (example billing_address[first_name] ). Whenever error is thrown by Chargebee API due to invalid data in a parameter, the error response contains the parameter that is invalid in the error_param attribute. The parameter name sent back follows the curl parameter format. We send the error response received from back to browser client where we are able to display the error at the appropriate position near the field.
error: function(jqXHR, textStatus, errorThrown) {
try {
var resp = JSON.parse(jqXHR.responseText);
if ('error_param' in resp) {
var errorMap = {};
var errParam = resp.error_param;
var errMsg = resp.error_msg;
errorMap[errParam] = errMsg;
formElement.validate().showErrors(errorMap);
} else {
var errMsg = resp.error_msg;
$(".alert-danger").show().text(errMsg);
}
} catch (err) {
$(".alert-danger").show().text("Error while processing your request");
}
}
Adding/Updating customers card information via hosted page
We use the Chargebee's hosted page api to let the customers update their card information.
Listing invoices
The list invoices for a subscription api is used to fetch the invoices for the subscription.
<%
ListResult result = Invoice.invoicesForSubscription(subscriptionId).limit(20).request();
%>
# list last 20 invoices of the subscription
def invoice_list
@list_result=ChargeBee::Invoice.invoices_for_subscription(@subscription_id, { :limit => 20 })
end
<?php
$listResult = ChargeBee_Invoice::invoicesForSubscription($subscriptionId, array("limit" => 20));
?>
We iterate through the list, skipping the pending invoices (used when you have metered billing).
<%
int i = 0;
for (ListResult.Entry entry : result) {
Invoice invoice = entry.invoice();
if (invoice.status().equals(Invoice.Status.PENDING)) {
continue;
}
i++;
}
%>
<%
@list_result.each do |entry|
invoice = entry.invoice
if invoice.status == "pending"
continue
end
i = i + 1
%>
<?php
$i = 0;
foreach($listResult as $entry ) {
$invoice = $entry->invoice();
if ( $invoice->status == "pending" ) {
continue;
}
$i++;
?>
and display the important details of each invoice.
<td>
<%= Utils.getHumanReadableDate(invoice.date())%>
</td>
<td>
<%=invoice.id()%>
</td>
<td >
$ <%= String.format("%d.%02d", invoice.total()/ 100, invoice.total() % 100)%>
</td>
<td>
<%= Time.at(invoice.date).strftime("%d-%b-%y") %>
</td>
<td>
<%= invoice.id %>
</td>
<td >
$ <%= formatter[invoice.total] %>
</td>
<td>
<?php echo date('m/d/y', $invoice->date) ?>
</td>
<td>
<?php echo $invoice->id ?>
</td>
<td >
$ <?php echo number_format($invoice->total / 100, 2, '.', '') ?>
</td>
Downloading invoices as PDF
We allow users to download the invoices using download invoice as pdf api. The api returns a secure temporary url that we can forward it to the browser to trigger automatic pdf download.
/*
* Returns pdf download url for the requested invoice
*/
private void invoiceAsPdf(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException, Exception {
//response.setHeader("Content-Type", "application/json;charset=utf-8");
String invoiceId = request.getParameter("invoice_id");
Invoice invoice = Invoice.retrieve(invoiceId).request().invoice();
if( !getSubscriptionId(request).equals(invoice.subscriptionId()) ) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
Result result = Invoice.pdf(invoiceId).request();
response.sendRedirect(result.download().downloadUrl());
}
# Retrieves the pdf download url for the requested invoice
def invoice_as_pdf
invoice_id = params['invoice_id']
invoice = ChargeBee::Invoice.retrieve(invoice_id).invoice
if invoice.subscription_id != @subscription_id
redirect_to "/400"
return
end
result = ChargeBee::Invoice.pdf(invoice_id)
redirect_to result.download.download_url
end
/*
* Returns pdf download url for the requested invoice
*/
function invoiceAsPdf() {
$invoiceId = $_GET['invoice_id'];
$invoice = ChargeBee_Invoice::retrieve($invoiceId)->invoice();
if( $invoice->subscriptionId != getSubscriptionId() ) {
header("HTTP/1.0 400 Error");
include($_SERVER["DOCUMENT_ROOT"]."/error_pages/400.html");
return;
}
$result = ChargeBee_Invoice::pdf($invoiceId);
header("Location: " . $result->download()->downloadUrl);
}
Canceling subscription.
The customers also have the option of canceling the subscription. They can choose to cancel immediately or can cancel at the end of term. We use the cancel subscription api to cancel the subscription.
/*
* Cancels the Subscription.
*/
private void subscriptionCancel(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException, Exception {
String cancelStatus = request.getParameter("cancel_status");
Subscription.CancelRequest subscriptionCancelParam = Subscription
.cancel(getSubscriptionId(request));
if ("cancel_on_next_renewal".equals(cancelStatus)) {
subscriptionCancelParam.endOfTerm(Boolean.TRUE);
}
subscriptionCancelParam.request();
response.sendRedirect("/ssp/subscription.jsp");
}
def sub_cancel
cancel_status = params['cancel_status']
params = {}
if cancel_status == "cancel_on_next_renewal"
puts "Subscription cancel on end of term"
params[:end_of_term] = "true"
end
result = ChargeBee::Subscription.cancel(@subscription_id, params)
redirect_to "/ssp/subscription"
end
/*
* This method will be executed for cancel subscription request.
*/
function subscriptionCancel() {
$subscriptionId = getSubscriptionId();
$cancelStatus = $_POST['cancel_status'];
$params = array();
if( $cancelStatus == "cancel_on_next_renewal" ) {
$params['end_of_term'] = 'true';
}
$result = ChargeBee_Subscription::cancel($subscriptionId,$params);
header("Location: subscription");
}
Re-activating subscription
Incase the subscription has been canceled we allow users to re-activate the subscription using the reactivate a subscription api.
/*
* Reactivate the subscription from cancel/non-renewing state to active state.
*/
private void subscriptionReactivate(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter out = response.getWriter();
try{
Subscription.reactivate(getSubscriptionId(request))
.request();
out.write("{ \"forward\" : \"/ssp/subscription.jsp\" }");
} catch (InvalidRequestException e ) {
handleInvalidErrors(e, response, out);
} catch ( Exception e) {
handleGeneralErrors(e, response, out);
} finally {
out.flush();
}
}
# Reactivate the subscription from cancel/non-renewing state to active state.
def sub_reactivate
begin
ChargeBee::Subscription.reactivate(@subscription_id)
render json: {
:forward => "/ssp/subscription"
}
rescue ChargeBee::PaymentError =>e
ErrorHandler.handle_charge_attempt_failure_errors(e, self)
rescue ChargeBee::InvalidRequestError => e
ErrorHandler.handle_invalid_errors(e, self)
rescue Exception => e
ErrorHandler.handle_general_errors(e, self)
end
end
/*
* Reactivate the subscription from cancel/non-renewing state to active state.
*/
function subscriptionReactivate() {
header('Content-Type: application/json');
try {
$subscriptionId = getSubscriptionId();
$result = ChargeBee_Subscription::reactivate($subscriptionId);
$jsonResponse = array("forward" => "/ssp-php/subscription");
print json_encode($jsonResponse, true);
} catch(ChargeBee_PaymentException $e) {
handleChargeAttemptFailureErrors($e);
} catch(ChargeBee_InvalidRequestException $e) {
handleInvalidErrors($e);
} catch(Exception $e) {
handleGeneralErrors($e);
}
}
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.
- 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.
- 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.
Reference Links
We're always happy to help you with any questions you might have!
support@chargebee.com