Payment plugins integrate Kill Bill with a specific gateway (i.e. payment processor), such as Stripe or Braintree. We already have many open-source payment plugins available on GitHub as follows:
killbill-stripe-plugin - Plugin to use Stripe as the payment gateway
killbill-adyen-plugin - Plugin to use Adyen as the payment gateway
killbill-qualpay-plugin - Plugin to use QualPay as the payment gateway
This guide will give you pointers in case you need to develop your own payment plugin.
Before reading this guide, make sure to familiarize yourself with payment components and the payment userguide.
Developing a Payment Plugin
We provide an interface called PaymentPluginApi. This is the interface between Kill Bill and payment plugins. In order to create your own payment plugin, you need to create a class that implements this interface and implement the methods in this interface.
We provide a plugin framework which has many classes that can be used out of the box for plugin development. We strongly encourage you to use this library.
Getting Started
We provide a simple Hello World Plugin that can be used as the starting point to develop your payment plugin. The Hello World Plugin already includes the plugin framework.
In order to get started with your own payment plugin, you need to do the following:
Set up the Hello World Plugin as per our Plugin Development document.
Create a new Maven project similar to Hello World Plugin.
Create a class similar to HelloWorldPaymentPluginApi.java. This is a reference class that is included in the Hello World Plugin. It implements the
PaymentPluginApi
interface.Implement the desired methods from the
PaymentPluginApi
interface within your class. These methods are explained in the "PaymentPluginApi Methods" table below.Create a class similar to HelloWorldActivator. This class will be responsible for starting the plugin.
PaymentPluginApi Methods
The following table lists the methods in PaymentPluginApi interface. You can implement the desired methods in your plugin class.
Method Name | Method Description |
---|---|
authorizePayment/ capturePayment | Should trigger an authorization and capture respectfully (applies to credit cards only) |
purchasePayment | Should trigger a generic payment (authorization with auto-capture for credit cards, ACH/SEPA, etc.) |
voidPayment | Should void an authorization (credit cards only) |
creditPayment | Should fund the payment method from your merchant account (similar to a refund, but no prior payment is required: this can be used to initiate an ACH for disbursement for instance) |
refundPayment | Should reverse an existing (settled) charge |
getPaymentInfo | Should return detailed information on the transactions for that payment (arbitrary properties can be populated in PaymentTransactionInfoPlugin#getProperties) |
searchPayments | Should return payment transactions matching the specified searchKey (the implementation is up to the plugin) |
addPaymentMethod | Should create the payment method in the gateway (e.g. store a token) |
deletePaymentMethod | Should delete the payment method in the gateway (e.g. revoke a token) |
getPaymentMethodDetail | Should return detailed information about the payment method (e.g. billing address) |
setDefaultPaymentMethod | Should mark the payment method as the default one in the gateway (if the associated account in the gateway has several payment methods) |
getPaymentMethods | Should list the payment methods in the gateway for that account (used by the refresh endpoint, when payment methods are added directly in the gateway, bypassing addPaymentMethod) |
searchPaymentMethods | Should return payment methods matching the specified searchKey (the implementation is up to the plugin) |
resetPaymentMethods | Called at the end of a refresh call, should associate to that account the payment methods specified (this is useful if Kill Bill knows payment methods that are not yet in the gateway) |
buildFormDescriptor | Should return enough metadata for the front-end to build a form or a redirect to a hosted payment page |
processNotification | Should process payloads from the gateway to transition payments states |
A few pointers for implementing these methods:
Almost all the
PaymentPluginApi
methods accept parameters like Kill Bill account Id, Kill Bill payment id, Kill Bill transaction id, etc. which are self-explanatory. These may or may not be used by your plugin code. But you may need some of these values to populate the PaymentTransactionInfoPlugin object which is returned by most of the methods.All the methods listed in the
PaymentPluginApi
also acceptIterable<PluginProperty> properties
as a parameter. A PluginProperty consists of a key-value pair. It can be used to pass plugin specific properties to the plugin. For example, if your plugin requires properties like city=San Francisco and billing_address=abc, a client application can create twoPluginProperty
objects and pass aList
of these objects.All the methods listed in the
PaymentPluginApi
also accept as parameter either aTenantContext
or aCallContext
. Read only operations (operations which retrieve some data from Kill Bill) like thegetPaymentInfo
accept aTenantContext
. Read/write operations likeauthorizePayment
accept aCallContext
. Both these values can be used for auditing purposes.It is quite possible that your plugin might not support some of the operations listed above. In such a case, the plugin can do one of the following:
Return an empty list when the return type is a list
Return a transaction with status CANCELED if the return type is a
PaymentTransactionInfoPlugin
Return empty objects otherwise
PaymentTransactionInfoPlugin
Most of the methods in the PaymentPluginApi
return a PaymentTransactionInfoPlugin object. Again, the Kill Bill Plugin framework includes a class called PluginPaymentTransactionInfoPlugin which implements the PaymentTransactionInfoPlugin
. You can create a class that extends this class and have the plugin methods return this object.
The PaymentTransactionInfoPlugin
has the following methods (Your plugin code needs to populate this information in the PaymentTransactionInfoPlugin
object and a client application can then invoke these methods to retrieve this information):
Method Name | Method Description |
getKbPaymentId | Returns Payment Id in Kill Bill |
getKbTransactionPaymentId | Returns Transaction Payment Id in Kill Bill |
getTransactionType | Returns the transaction type (TransactionType object) |
getAmount | Returns the processed amount |
getCurrency | Returns the processed currency |
getCreatedDate | Returns the date when the payment was created |
getEffectiveDate | Returns the date when the payment is effective |
getStatus | Returns the payment status (PaymentPluginStatus object) |
getGatewayError | Returns the gateway error if any |
getGatewayErrorCode | Returns the gateway error code if any |
getFirstPaymentReferenceId | Returns gateway specific first payment id if any |
getSecondPaymentReferenceId | Returns gateway specific second payment id if any |
getProperties | Returns a |
It is very important to correctly populate the PaymentPluginStatus in the PaymentTransactionInfoPlugin
object. The following table elaborates how the status should be populated:
Status | Status Description |
---|---|
PROCESSED | Indicates that the payment is successful |
ERROR | Indicates that the payment is rejected by the gateway (insufficient funds, fails AVS check, fraud detected, etc.) |
PENDING | Indicates that the payment requires a completion step (3D-S verification, HPP, etc.) |
CANCELED | Indicates that the gateway wasn’t contacted (DNS error, SSL handshake error, socket connect timeout, etc.) |
UNDEFINED | Should be used for all other cases (socket read timeout, 500 returned, etc.) |
Kill Bill has a https://docs.killbill.io/latest/userguide_payment.html#janitor[_Janitor] system in place that attempts to fix PENDING and UNKNOWN states . It polls the plugin via PaymentPluginApi#getPaymentInfo
method. If the plugin subsequently returns PROCESSED, the Janitor updates the internal payment state as well as invoice balance, etc.) accordingly.
The Janitor matches the internal transactions against plugin transactions via the transaction id, so make sure PaymentTransactionInfoPlugin#getKbTransactionPaymentId
is correctly implemented.
You should try to avoid UNDEFINED as much as possible, because it is the only case where Kill Bill cannot retry payments (since the payment may or may not have happened).
GoCardless Plugin Tutorial
In order to demonstrate creating a payment plugin, we will be creating a Kill Bill payment plugin for GoCardless. GoCardless allows direct debit from customer’s bank accounts. It requires a customer to set up a mandate the first time. A mandate is an authorisation from a customer to take payments from their bank account. Once a mandate is set up, it directly collects payments against the mandate.
GoCardless provides a client library. We will be using this library to integrate GoCardless with Kill Bill. For the sake of simplicity, we will be creating a very basic plugin that can only process payments. Refunds, credits and other plugin functionality will currently not be implemented.
The complete code for this tutorial is available on Github.
How GoCardless Works
The first step in GoCardless would be adding a customer and setting up a payment mandate. GoCardless supports three ways to do this as explained here. In this tutorial, we will be using the hosted payment page approach which is explained here.
The diagram below explains the steps involved. We consider the following actors:
Browser: user sitting behind a browser and initiating the payment flow
Merchant Site: customer facing web site which receives the order
GoCardless: The GoCardless payment system
Bank - Customer’s bank which processes the payments
A user enters his/her payment details on a merchant site.
The merchant site initiates the GoCardless Redirect flow with the customer details (optional) and a success page URL.
GoCardless returns a redirect URL.
The merchant site redirects the user to this URL.
The user manually enters bank details at this page.
If successful, GoCardless redirects the user to the success page URL sent to it in step 1.
The merchant site completes the GoCardless Redirect flow .
GoCardless then actually sets up the mandate with the customer’s bank.
If successful, it returns a mandate Id to the merchant site.
The merchant site then charges the customer against the mandate Id as required.
Using GoCardless from Kill Bill
In order to use GoCardless from Kill Bill, we will need to create a payment plugin corresponding to GoCardless. Since we are developing a very basic plugin that can only process payments, we only need to do the following:
Set up the mandate. This is a two step process as explained above where:
Step 1 involves redirecting the user to a page to manually confirm setting up the mandate. This can be implemented via the
PaymentPluginApi#buildFormDescriptor
methodStep 2 involves completing the GoCardless flow and retrieving the mandate Id. This can be implemented via the
PaymentPluginApi#addPaymentMethod
method
Implement the
PaymentPluginApi#purchasePayment
method to charge the customer
The diagram below explains the end-to-end flow. We consider the following actors:
Browser: user sitting behind a browser and initiating the payment flow
Merchant Site: customer facing web site which receives the order
Kill Bill - The Kill Bill system
Checkout Servlet - Servlet that initiates setting up the payment method. This is explained further in detail
GoCardless Plugin: Payment plugin corresponding to GoCardless that can process payments using the GoCardless system
GoCardless: The GoCardless payment system
A user enters his/her payment details on a merchant site.
The merchant site invokes the
Checkout Servlet
.The
Checkout Servlet
invokes theGoCarldessPlugin#buildFormDescriptor
.The
GoCarldessPlugin#buildFormDescriptor
method invokes theredirectFlows().create()
. This initiates the GoCardless redirect flow and returns the redirect URL .The merchant site redirects the user to this URL.
The user manually enters bank details on this page.
GoCardless redirects the user to the success page.
The merchant site invokes the
KillBill#addPaymentMethod
which in turn invokesGoCardlessPlugin#addPaymentMethod
.The
GoCarldessPlugin#addPaymentMethod
invokesredirectFlows().complete
. This completes the redirect flow and returns the mandate id which is saved in the Kill Bill database.The merchant site can then invoke
KillBill#purchasePayment
as required. This in turn invokesGoCardlessPlugin#purchasePayment
.The
GoCardlessPlugin#purchasePayment
invokes thepayments().create()
to charge the customer against the saved mandate id as explained here.
Creating the GoCardless Plugin
Let us now understand how we can create a payment plugin for GoCardless.
Step 1 - Initial Setup
The first step in creating a payment plugin would be to create a new Maven project. For this we need to do the following:
Create a new Maven project.
Copy the Hello World Plugin pom.xml.
Generate a GoCardless access token (https://manage-sandbox.gocardless.com/developers) and create an environment variable GC_ACCESS_TOKEN with this token.
Step 2 - Creating GoCardlessPluginApi
We will first need to create a class similar to HelloWorldPaymentPluginApi
that implements the PaymentPluginApi
interface. We can create this class as follows (See GoCardlessPaymentPluginApi.java):
1
2
3
4
5
6
7
8
9
10
11
12
13
public class GoCardlessPaymentPluginApi implements PaymentPluginApi {
private static final Logger logger = LoggerFactory.getLogger(GoCardlessPaymentPluginApi.class);
private OSGIKillbillAPI killbillAPI;
private Clock clock;
private static String GC_ACCESS_TOKEN_PROPERTY = "GC_ACCESS_TOKEN";
private GoCardlessClient client;
public GoCardlessPaymentPluginApi(final OSGIKillbillAPI killbillAPI,final Clock clock) {
this.killbillAPI = killbillAPI;
this.clock = clock;
client = GoCardlessClient.newBuilder(System.getenv(GC_ACCESS_TOKEN_PROPERTY)).withEnvironment(GoCardlessClient.Environment.SANDBOX).build();
}
//other methods
}
The
GoCardlessPaymentPluginApi
implements thePaymentPluginApi
interface.It declares the following fields:
killbillAPI
- This is of type OSGIKillbillAPI. TheOSGIKillBillAPI
is a Kill Bill class which exposes all of Kill Bill’s internal APIs.GC_ACCESS_TOKEN_PROPERTY
- This is a String field that is required for accessing the GoCardless access tokenclock
- This is of type Clock. This is part of Kill Bill’s clock library.client
This is of type GoCardlessClient. This is a GoCardless specific class that can be used to access the GoCardless API
The constructor simply initializes the fields with the values passed in. In addition, it also creates a GoCardless client and initializes the
client
field
Within this class, we need to implement the buildFormDescriptor
, addPaymentMethod
and purchasePayment
as explained in the "Using GoCardless from Kill Bill" section above.
Step 3 - Creating GoCardlessPaymentTransactionInfoPlugin
As explained earlier, most of the PaymentPluginApi
methods return a PaymentTransactionInfoPlugin object. Thus, we need to create a class corresponding to this as follows (See GoCardlessPaymentTransactionInfoPlugin.java):
1
2
3
4
5
6
7
8
9
10
public class GoCardlessPaymentTransactionInfoPlugin extends PluginPaymentTransactionInfoPlugin{
public GoCardlessPaymentTransactionInfoPlugin(UUID kbPaymentId, UUID kbTransactionPaymentPaymentId,
TransactionType transactionType, BigDecimal amount, Currency currency, PaymentPluginStatus pluginStatus,
String gatewayError, String gatewayErrorCode, String firstPaymentReferenceId,
String secondPaymentReferenceId, DateTime createdDate, DateTime effectiveDate,
List<PluginProperty> properties) {
super(kbPaymentId, kbTransactionPaymentPaymentId, transactionType, amount, currency, pluginStatus, gatewayError,
gatewayErrorCode, firstPaymentReferenceId, secondPaymentReferenceId, createdDate, effectiveDate, properties);
}
}
The
GoCardlessPaymentTransactionInfoPlugin
extends the PluginPaymentTransactionInfoPlugin from the Kill Bill plugin framework. This class in turn implements the PaymentTransactionInfoPlugin interfaceThe
GoCardlessPaymentTransactionInfoPlugin
constructor accepts parameters corresponding to the data to be returned by PaymentTransactionInfoPlugin. It simply invokes the superclass constructor with these parameters
Step 4 - Implementing GoCardlessPaymentPluginApi#buildFormDescriptor
The buildFormDesciptor
is typically used for https://docs.killbill.io/latest/userguide_payment.html#hosted_payment_page_flow[hosted payment flows] to display a form where a user can enter his/her payment details. In the case of GoCardless, we will be using it to initiate the Gocardless redirect flow in GoCardless and obtain the redirect URL. This method can be implemented as follows (See _GoCardlessPaymentPluginApi.buildFormDescriptor):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public HostedPaymentPageFormDescriptor buildFormDescriptor(UUID kbAccountId, Iterable<PluginProperty> customFields,
Iterable<PluginProperty> properties, CallContext context) throws PaymentPluginApiException {
logger.info("buildFormDescriptor, kbAccountId=" + kbAccountId);
// retrieve properties
String successRedirectUrl = PluginProperties.findPluginPropertyValue("success_redirect_url", properties); // "https://developer.gocardless.com/example-redirect-uri/"; - this is the URL to which GoCardless will redirect after users set up the mandate
String redirectFlowDescription = PluginProperties.findPluginPropertyValue("redirect_flow_description",properties);
String sessionToken = PluginProperties.findPluginPropertyValue("session_token", properties); PrefilledCustomer customer = buildCustomer(customFields);// build a PrefilledCuctomer object from custom fields if present
RedirectFlow redirectFlow = client.redirectFlows().create().withDescription(redirectFlowDescription)
.withSessionToken(sessionToken)
.withSuccessRedirectUrl(successRedirectUrl).withPrefilledCustomer(customer).execute();
logger.info("RedirectFlow Id", redirectFlow.getId());
logger.info("RedirectFlow URL", redirectFlow.getRedirectUrl());
PluginHostedPaymentPageFormDescriptor pluginHostedPaymentPageFormDescriptor = new PluginHostedPaymentPageFormDescriptor(
kbAccountId, redirectFlow.getRedirectUrl());
return pluginHostedPaymentPageFormDescriptor;
}
The
buildFormDescriptor
method acceptsIterable<PluginProperty> properties
as a parameter. As explained above, aPluginProperty
can be used to pass a plugin specific key-value pair to a plugin.The code first retrieves the
successRedirectUrl
,redirectFlowDescription
andsessionToken
sent by the client application from theproperties
passed in. These are required by GoCardless and are as explained below:successRedirectUrl
- Indicates the page to which the user should be redirected after setting up the mandate successfully.redirectFlowDescription
- is a description that is displayed on the payment page (page where the user is redirected to set up the mandate)sessionToken
- is something that identifies the user’s session on the client application. GoCardless requires this to be supplied while creating the redirect flow (now, while invoking thebuildFormDescriptor
method), and while completing the redirect flow (when theaddPaymentMethod
is invoked) it at the end. Supplying this token twice makes sure that the person who completed the redirect flow is the person who initiated it.
Next, the
client.redirectFlows().create()
is invoked with thesuccessRedirectUrl
,redirectFlowDescription
andsessionToken
. This returns aRedirectFlow
object. TheRedirectFlow
object contains the redirect URL.Finally, a
HostedPaymentPageFormDescriptor
object is created using the redirect URL and the Kill Bill Account Id. This is then returned to the client application.
Step 5 - Implementing GoCardlessPaymentPluginApi#addPaymentMethod
The addPaymentMethod
is typically used to add a payment method in Kill Bill corresponding to a customer/account. In the case of GoCardless, we will be using it to complete the redirect flow. So, this method can be implemented as follows (See GoCardlessPaymentPluginApi#addPaymentMethod):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void addPaymentMethod(UUID kbAccountId, UUID kbPaymentMethodId, PaymentMethodPlugin paymentMethodProps,
boolean setDefault, Iterable<PluginProperty> properties, CallContext context)
throws PaymentPluginApiException {
logger.info("addPaymentMethod, kbAccountId=" + kbAccountId);
final Iterable<PluginProperty> allProperties = PluginProperties.merge(paymentMethodProps.getProperties(),
properties);
String redirectFlowId = PluginProperties.findPluginPropertyValue("redirect_flow_id", allProperties); //retrieve the redirect flow id
String sessionToken = PluginProperties.findPluginPropertyValue("session_token", allProperties);
try {
//Use the redirect flow id to "complete" the GoCardless flow
RedirectFlow redirectFlow = client.redirectFlows().complete(redirectFlowId).withSessionToken(sessionToken).execute();
String mandateId = redirectFlow.getLinks().getMandate(); //obtain mandate id from the redirect flow
logger.info("MandateId:", mandateId);
try {
//save Mandate id in the Kill Bill database
killbillAPI.getCustomFieldUserApi().addCustomFields(ImmutableList.of(new PluginCustomField(kbAccountId,
ObjectType.ACCOUNT, "GOCARDLESS_MANDATE_ID", mandateId, clock.getUTCNow())), context);
} catch (CustomFieldApiException e) {
logger.warn("Error occured while saving mandate id", e);
throw new PaymentPluginApiException("Error occured while saving mandate id", e);
}
} catch (GoCardlessApiException e) {
logger.warn("Error occured while completing the GoCardless flow", e.getType(), e);
throw new PaymentPluginApiException("Error occured while completing the GoCardless flow", e);
}
}
Like other methods, the
addPaymentMethod
method also acceptsIterable<PluginProperty> properties
as a parameter.In addition, it also accepts
PaymentMethodPlugin paymentMethodProps
as a parameter.PaymentMethodPlugin
is a generic object that represents a payment method (creditcard, bank account, etc.). It has agetProperties
method that returns aList<PluginProperty>
.The
properties
parameter is typically used to pass properties which are related to the specific method call (addPaymentMethod
in this case) while thePaymentMethodPlugin#getProperties
typically refers to non-standard generic information about the payment method itself.A client application can use either of these to pass in the GoCardless properties. The code above (like other plugins) is lenient and accepts both ways. So, it first invokes
PluginProperties.merge
to merge both properties and stores them into a mergedallProperties
listIt then retrieves the
redirectFlowId
andsessionToken
fromallProperties
. These are required by GoCardless and are as explained below:redirectFlowId
- If you recall, theredirectFlowId
is sent to a client application after thebuildFormDescriptor
method call. A client application needs to send this back.sessionToken
- As explained earlier, a client application needs to send the samesessionToken
that was sent at the time of creating the redirect flow (when thebuildFormDescriptor
method was invoked) to ensure that the person who completes the redirect flow is the person who initiated it.
Next, the
client.redirectFlows().complete
is invoked with theredirectFlowId
and thesessionToken
. This returns aRedirectFlow
object which contains the mandate Id.Finally, the mandateId is stored in the Kill Bill database. Normally, each plugin has its own plugin specific tables. However, since we are not creating a full-fledged GoCardless plugin, we are storing the mandateId in the custom_fields table. The custom_fields table can be used to store arbitrary key/value pairs in the Kill Bill database.
In case an error occurs in any of the steps, the code throws a PaymentPluginApiException
Step 6 - Implementing GoCardlessPaymentPluginApi#purchasePayment
The purchasePayment
is used to charge a customer against a payment method. So, once a user sets up a payment method as done above, the purchasePayment
method can be used by a client application to trigger payments against a mandateId. So, this method can be implemented as follows (See GoCardlessPaymentPluginApi#purchasePayment):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public PaymentTransactionInfoPlugin purchasePayment(UUID kbAccountId, UUID kbPaymentId, UUID kbTransactionId,
UUID kbPaymentMethodId, BigDecimal amount, Currency currency, Iterable<PluginProperty> properties,
CallContext context) throws PaymentPluginApiException {
logger.info("purchasePayment, kbAccountId=" + kbAccountId);
PaymentTransactionInfoPlugin paymentTransactionInfoPlugin;
String mandate = getMandateId(kbAccountId, context); // retrieve mandateId from Kill Bill tables
logger.info("MandateId="+mandate);
if (mandate != null) {
logger.info("Processing payment");
try {
String idempotencyKey = PluginProperties.findPluginPropertyValue("idempotencykey", properties);
com.gocardless.services.PaymentService.PaymentCreateRequest.Currency goCardlessCurrency = convertKillBillCurrencyToGoCardlessCurrency(
currency);
Payment payment = client.payments().create()
.withAmount(Math.toIntExact(KillBillMoney.toMinorUnits(currency.toString(), amount)))
.withCurrency(goCardlessCurrency).withLinksMandate(mandate).withIdempotencyKey(idempotencyKey)
.withMetadata("kbPaymentId", kbPaymentId.toString()).withMetadata("kbTransactionId", kbTransactionId.toString()) //added for getPaymentInfo
.execute();
List<PluginProperty> outputProperties = new ArrayList<PluginProperty>();
outputProperties.add(new PluginProperty("paymentId", payment.getId(), false));
paymentTransactionInfoPlugin = new GoCardlessPaymentTransactionInfoPlugin(kbPaymentId, kbTransactionId,
TransactionType.PURCHASE, amount, currency, PaymentPluginStatus.PROCESSED, null, null,
String.valueOf(payment.getId()), null, new DateTime(), new DateTime(payment.getCreatedAt()),
outputProperties);
logger.info("Payment processed, PaymentId="+payment.getId());
} catch (GoCardlessApiException e) {
paymentTransactionInfoPlugin = new GoCardlessPaymentTransactionInfoPlugin(kbPaymentId, kbTransactionId,
TransactionType.PURCHASE, amount, currency, PaymentPluginStatus.ERROR, e.getErrorMessage(),
String.valueOf(e.getCode()), null, null, new DateTime(), null, null);
logger.warn("Error occured in purchasePayment", e.getType(), e);
}
} else {
logger.warn("Unable to fetch mandate, so cannot process payment");
paymentTransactionInfoPlugin = new GoCardlessPaymentTransactionInfoPlugin(kbPaymentId, kbTransactionId,
TransactionType.PURCHASE, amount, currency, PaymentPluginStatus.CANCELED, null,
null, null, null, new DateTime(), null, null);
}
return paymentTransactionInfoPlugin;
}
Like the other methods, the
purchasePayment
method acceptsIterable<PluginProperty> properties
as a parameter.In addition, it also accepts parameters corresponding to
amount
(specifies the amount to charge the customer) andcurrency
(specifies the currency to be used)If you recall, the
addPaymentMethod
stores the mandate id in the Kill Bill database. This is first retrieved and assigned tomandate
Next, the
idempotencyKey
is retrieved from theproperties
passed in. TheidempotencyKey
is a GoCardless specific value. As per the GoCardless documentation, their API will ensure this payment is only ever created once peridempotencyKey
. So a client application could specifykbPaymentId
as theidempotencyKey
to ensure at most a single payment is created perkbPaymentId
.The
currency
object passed in is of typeorg.killbill.billing.catalog.api.Currency
. This is then converted to a GoCardless Currency object (of typecom.gocardless.services.PaymentService.PaymentCreateRequest.Currency
). Most payment plugins have code similar to this to convert Kill Bill objects to compatible objects in the plugin’s client library.Finally, the
client.payments().create()
is invoked with theidempotencyKey
,amount
andcurrency
values. This returns aPayment
object which contains apaymentId
. Additionally, thekbPaymentId
andkbTransactionId
are sent as metadata to GoCardless in this call. GoCardless metadata allows an application to send custom key value pairs to GoCardless. These can then be retrieved later on as required. In our case, thekbPaymentId
andkbTransactionId
are required for thegetPaymentInfo
call which will be explained further.The
purchasePayment
method returns aPaymentTransactionInfoPlugin
object. The PaymentTransactionInfoPlugin section above explains this interface in detail. We have already created a GoCardlessPaymentTransactionInfoPlugin class above.If the payment is successful, the
GoCardlessPaymentTransactionInfoPlugin
object is created with the following values:kbPaymentId - Set to
kbPaymentId
. It corresponds to the Kill Bill payment id.kbTransactionId - Set to
kbTransactionId
. It corresponds to the Kill Bill transaction id.TransactionType - Set to
TransactionType.PURCHASE
since this is a purchase transactionamount - Set to
amount
. It corresponds to the amount with which the customer is chargedcurrency - Set to
currency
. It corresponds to the currency in which the customer is chargedPaymentPluginStatus - Set to
PaymentPluginStatus.PROCESSED
since the payment is processed successfullygatewayError - Set to
null
since there is no errorgatewayErrorCode - set to
null
since there is no errorfirstPaymentReferenceId - Set to the payment Id returned by GoCardless
secondPaymentReferenceId - set to
null
since GoCardless does not have a secondPaymentReferenceId. Other payment plugins might use this parameter if required.createdDate - Set to the current date
effectiveDate - Set to the date when the payment was created. This is retrieved from the
payment
object returned by GoCardlessproperties - Set to a
List<PluginProperty>
calledoutputProperties
which contains the GoCardless payment Id. The client API can use theproperties
as desired.
If there is an exception while processing the payment, the
GoCardlessPaymentTransactionInfoPlugin
object is created with the following values :PaymentPluginStatus - Set to
PaymentPluginStatus.ERROR
since there is an error in the paymentgatewayError - Set to the error message from the exception
gatewayErrorCode - Set to the error code from the exception
firstPaymentReferenceId - Set to
null
since the payment failedeffectiveDate - Set to
null
since the payment failedproperties - Set to
null
since the payment failed
If the code is unable to retrieve the
mandateId
from the Kill Bill database, theGoCardlessPaymentTransactionInfoPlugin
object is created with the following values:PaymentPluginStatus - Set to
PaymentPluginStatus.CANCELED
since the gateway was not contacted as the plugin was unable to retrieve themandateId
gatewayError - Set to
null
since there is no errorgatewayErrorCode - set to
null
since there is no errorfirstPaymentReferenceId - Set to
null
since the payment was not processedeffectiveDate - Set to
null
since the payment was not processedproperties - Set to
null
since the payment was not processed
Step 7 - Implementing GoCardlessPaymentPluginApi#getPaymentInfo
As explained earlier, the Kill Bill Janitor system attempts to fix PENDING and UNKNOWN states by polling the plugin via PaymentPluginApi#getPaymentInfo
method. So, this method can be implemented as follows (See GoCardlessPaymentPluginApi#getPaymentInfo):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public List<PaymentTransactionInfoPlugin> getPaymentInfo(UUID kbAccountId, UUID kbPaymentId,
Iterable<PluginProperty> properties, TenantContext context) throws PaymentPluginApiException {
logger.info("getPaymentInfo");
List<PaymentTransactionInfoPlugin> paymentTransactionInfoPluginList = new ArrayList<>();
String mandateId = getMandateId(kbAccountId, context) ;
logger.info("Mandate="+mandateId);
Mandate mandate = client.mandates().get(mandateId).execute(); //get GoCardless Mandate object
String customerId = mandate.getLinks().getCustomer(); //retrieve customer id from mandate
logger.info("CustomerId="+customerId);
Iterable<Payment> payments = client.payments().all().withCustomer(customerId).execute(); //get all payments related to customer
for (Payment payment : payments) {
String kbPaymentIdFromPayment = payment.getMetadata().get("kbPaymentId"); //get kbPaymentId from metadata in payment
logger.info("kbPaymentIdFromPayment="+kbPaymentIdFromPayment);
if(kbPaymentIdFromPayment != null && kbPaymentId.toString().equals(kbPaymentIdFromPayment)) {
logger.info("Found matching payment");
Currency killBillCurrency = convertGoCardlessCurrencyToKillBillCurrency(payment.getCurrency());
logger.info("payment_status="+payment.getStatus());
PaymentPluginStatus status = convertGoCardlessToKillBillStatus(payment.getStatus());
String kbTransactionPaymentIdStr = payment.getMetadata().get("kbTransactionId");
UUID kbTransactionPaymentId = kbTransactionPaymentIdStr !=null?UUID.fromString(kbTransactionPaymentIdStr):null;
logger.info("kbTransactionPaymentId="+kbTransactionPaymentId);
List<PluginProperty> outputProperties = new ArrayList<PluginProperty>();
outputProperties.add(new PluginProperty("mandateId",mandateId,false)); //arbitrary data to be returned to the caller
outputProperties.add(new PluginProperty("customerId",customerId,false)); //arbitrary data to be returned to the caller
GoCardlessPaymentTransactionInfoPlugin paymentTransactionInfoPlugin = new GoCardlessPaymentTransactionInfoPlugin(
kbPaymentId, kbTransactionPaymentId, TransactionType.PURCHASE, new BigDecimal(payment.getAmount()), killBillCurrency,
status, null, null, String.valueOf(payment.getId()), null, new DateTime(),
new DateTime(payment.getCreatedAt()), outputProperties);
paymentTransactionInfoPluginList.add(paymentTransactionInfoPlugin);
}
}
return paymentTransactionInfoPluginList;
}
If you recall, the
addPaymentMethod
stores the mandate id in the Kill Bill database in the custom_fields table. This is first retrieved and assigned tomandateId
.Next, the GoCardless Mandate object is retrieved via
client.mandates().get(mandateId)
as explained here.Next, the
customerId
associated with the mandate is retrieved and all thepayments
associated with the customer are retrieved viaclient.payments().all().withCustomer(customerId)
as explained here.The code then iterates through the
payment
objects and obtains thePayment
object corresponding to thekbPaymentId
passed in. If you recall, thepurchasePayment
method sendskbPaymentId
andkbTransactionId
to GoCardless as metadata fields. Thus, thekbPaymentId
is retrieved from the metadata of eachPayment
object and compared with thekbPaymentId
passed in.The code then creates a
GoCardlessPaymentTransactionInfoPlugin
corresponding to a matching `Payment`using the following:killBillCurrency - The GoCardless currency (
com.gocardless.resources.Payment.Currency
) is retrieved from thepayment
object and converted to Kill Bill currency (org.killbill.billing.catalog.api.Currency
).status - The GoCardless status (
com.gocardless.resources.Payment.Status
) is retrieved from thepayment
object and converted to Kill Bill status (org.killbill.billing.payment.plugin.api.PaymentPluginStatus
).kbTransactionId - This is retrieved from the payment metadata. This step is very important because, the Janitor uses the transaction id to match the internal transactions with the plugin transactions as explained earlier.
outputProperties - This is a List of
PluginProperty
objects. SomePluginProperty
objects are created and added to theoutputProperties
List. Any arbitrary key-value pairs which need to be returned to the caller can be specified here.
The List of
GoCardlessPaymentTransactionInfoPlugin
object is returned back.The Janitor then updates the internal payment state as well as invoice balance, etc. based on the information in the
GoCardlessPaymentTransactionInfoPlugin
object.In addition to the Janitor, the
getPaymentInfo
can also be used used on the merchant website (or in Kaui) to display the payment details to the customer. In such cases, it is invoked via an API call as specified here. An important point to note is that the actual plugin method (GoCardlessPluginApi#getPaymentInfo
) is invoked only when thewithPluginInfo=true
is specified in the API call. IfwithPluginInfo=false
is specified, the plugin method is not invoked and Kill Bill just returns the payment information from the Kill Bill database.
Step 8 - Creating GoCardlessCheckoutServlet
In the case of GoCardless, we need to create an additional servlet that invokes the GoCardlessPluginApi#buildFormDescriptor
method. Normally, a client API invokes buildFormDescriptor
via the
PaymentGatewayApi interface. If you take a look at the PaymentGatewayApi#buildFormDescriptor method, you will notice that it accepts a UUID paymentMethodId
as a parameter. Thus, this method assumes that a payment method already exists. (You can read the payment user guide to know more about payment methods). However, in the case of GoCardless, we are using GoCardlessPluginApi#buildFormDescriptor
to create a form where a user sets up a mandate. Thus, the payment method will not exist in Kill Bill at the time of invoking the PaymentGatewayApi#buildFormDescriptor
method. So, this method cannot directly invoke the GoCardlessPlugin#buildFormDescriptor
method. To work around this, we need to create a servlet and invoke the GoCardlessPlugin#buildFormDescriptor
method from this servlet. The client application can invoke this servlet and not the PaymentGatewayApi#buildFormDescriptor
method
This servlet can be created as follows (See GoCardlessCheckoutServlet):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Singleton
// Handle /plugins/killbill-gocardless/checkout
@Path("/checkout")
public class GoCardlessCheckoutServlet {
private final OSGIKillbillClock clock;
private final GoCardlessPaymentPluginApi goCardlessPaymentPluginApi;
private static final Logger logger = LoggerFactory.getLogger(GoCardlessCheckoutServlet.class);
@Inject
public GoCardlessCheckoutServlet(final OSGIKillbillClock clock,
final GoCardlessPaymentPluginApi goCardlessPaymentPluginApi) {
this.clock = clock;
this.goCardlessPaymentPluginApi = goCardlessPaymentPluginApi;
}
// Setting up Direct Debit mandates using Hosted Payment Pages, before a payment method has been added to the account
@POST
public Result createSession(@Named("kbAccountId") final UUID kbAccountId,
@Named("success_redirect_url") final Optional<String> successUrl,
@Named("redirect_flow_description") final Optional<String> description,
@Named("lineItemName") final Optional<String> token,
@Local @Named("killbill_tenant") final Tenant tenant) throws PaymentPluginApiException {
logger.info("Inside createSession");
final CallContext context = new PluginCallContext(GoCardlessActivator.PLUGIN_NAME, clock.getClock().getUTCNow(), kbAccountId, tenant.getId());
final ImmutableList<PluginProperty> properties = ImmutableList.of(
new PluginProperty("success_redirect_url", successUrl.orElse("https://developer.gocardless.com/example-redirect-uri/"), false),
new PluginProperty("redirect_flow_description", description.orElse("Kill Bill payment"), false),
new PluginProperty("session_token", token.orElse("killbill_token"), false));
final HostedPaymentPageFormDescriptor hostedPaymentPageFormDescriptor = goCardlessPaymentPluginApi.buildFormDescriptor(kbAccountId,
ImmutableList.of(),
properties,
context);
return Results.with(hostedPaymentPageFormDescriptor, Status.CREATED)
.type(MediaType.json);
}
}
The servlet is mapped to the
/checkout
path. Thus, a client application needs to make a request to this path to invoke the servletThe
createSession
accepts properties corresponding toclock
andgoCardlessPaymentPluginApi
. These are injected via theGoCardlessActivator
class as explained belowIt then creates
PluginProperty
objects corresponding to the values passed in as parametersFinally, it invokes the
GoCardlessPlugin#buildFormDescriptor
method
Step 9 - Creating GoCardlessActivator
We also need to create a class similar to HelloWorldActivator
that starts the plugin. We can create this class as follows (See GoCardlessActivator):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class GoCardlessActivator extends KillbillActivatorBase{
//This is the plugin name and is used by Kill Bill to route payment to the appropriate payment plugin
public static final String PLUGIN_NAME = "killbill-gocardless";
@Override
public void start(final BundleContext context) throws Exception {
super.start(context);
final GoCardlessPaymentPluginApi pluginApi = new GoCardlessPaymentPluginApi(killbillAPI,clock.getClock());
registerPaymentPluginApi(context, pluginApi);
// Register the servlet, which is used as the entry point to generate the Hosted Payment Pages redirect url
final PluginApp pluginApp = new PluginAppBuilder(PLUGIN_NAME, killbillAPI, dataSource, super.clock, configProperties)
.withRouteClass(GoCardlessCheckoutServlet.class)
.withService(pluginApi)
.withService(clock)
.build();
final HttpServlet goCardlessServlet = PluginApp.createServlet(pluginApp);
registerServlet(context, goCardlessServlet);
}
private void registerPaymentPluginApi(final BundleContext context, final PaymentPluginApi api) {
final Hashtable<String, String> props = new Hashtable<String, String>();
props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, PLUGIN_NAME);
registrar.registerService(context, PaymentPluginApi.class, api, props);
}
private void registerServlet(final BundleContext context, final HttpServlet servlet) {
final Hashtable<String, String> props = new Hashtable<String, String>();
props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, PLUGIN_NAME);
registrar.registerService(context, Servlet.class, servlet, props);
}
}
The
GoCardlessActivator
class defines a static field calledPLUGIN_NAME
with the valuekillbill-gocardless
. This is the name of the plugin and will be used by Kill Bill to route payment to the appropriate plugin as explained in our Payment User GuideThe
start
method creates a newGoCardlessPaymentPluginApi
objectIt then invokes the
registerPaymentPluginApi
method which registers the plugin with thePLUGIN_NAME
. This code is pretty standard across all plugins and can be used as it isIn the case of GoCardless, we need to create a checkout servlet as explained above. The
start
method creates this servlet viaPluginAppBuilder
as follows:withRouteClass
specifies the name of the servlet, in this caseGoCardlessCheckoutServlet
withService
specifiespluginApi
. Since theGoCardlessCheckoutServlet
accepts a parameter corresponding toGoCardlessPaymentPluginApi
, this is injected viawithService
Similarly, since
GoCardlessPaymentPluginApi
accepts a parameter corresponding toOSGIKillbillClock
, aclock
object is injected viawithService
Any other values that need to be passed to the servlet can be injected similarly
The
build
method is invoked which createspluginApp
Finally, the servlet is created via
PluginApp.createServlet
The
registerServlet
method is then invoked which registers the servlet
Step 10 - Build and Deployment
The GoCardless plugin can be built and deployed as per the build and deployment instructions specified in our Plugin Development Document.
Step 11 - Testing
Once the plugin is deployed successfully, it can be tested using curl
commands as specified here.