Overview

Starting with Kill Bill 0.18.x, we have introduced support of HA (Hierarchical Accounts), and so in this tutorial we will review first what the feature is about, and then explore how it works and which apis to use.

Let’s review first what we have prior to introducing the HA feature: In the Kill Bill terminology, a customer will be represented as a Kill Bill Account, and such Account will then be invoiced based on its current subscriptions and one-off charges. Payments will also be made by using the default payment method associated with the Account.

The idea with the HA feature is to delegate some of the payment operations associated to an Account to a parent Account. Some typical use case for the feature are:

  • Affiliate model: In the affiliate model, payments associated to some customers are made through the parent Account but each individual Account will manage its own subscriptions and will have access to the associated invoices.

  • Sub-Organization: In large organizations, it is common to see sub organizations that work independently, and yet parent organization is responsible for the payments.

The new HA feature allows for the following:

  • Ability to set an Account as the child of another Account

  • Ability to specify on an Account that it will be responsible for paying its own invoice (current behaviour)

  • Ability to specify on an Account that the parent will be responsible for the payments

  • Ability to transfer credit from child 'Account' to parent 'Account'

  • Ability to list all children Account

How does it work?

The Kill Bill Account abstraction has been enhanced to allow specifying a parent Account and whether or not that parent Account is responsible to pay children’s invoices. A parent Account is an Account that contains one or more children Account. When such a parent Account exists and when its children have been configured to delegate their payments, the following happens:

  1. Every time the system computes a new invoice (or adjusts an existing one) for a given child Account, the parent gets notified and also computes a special summary invoice that will include all the items for all the children on a given day.

  2. The payment system will ignore the child invoice as it knows this should be paid by the parent.

  3. At the end of each day (UTC time), the summary invoice will transition from DRAFT to COMMITTED

  4. As a result, the payment system will attempt to make a payment for the parent summary invoice using the default payment method associated to the parent Account.

As we can see, each child Account can still have its own subscriptions and matching invoices, but associated payments are delegated to the parent through the daily computation of a summary invoice.

The balance associated with both the child invoice and parent summary invoice will be zero until the end of the day when the transition from DRAFT to COMMITTED occurs. At this point, if the payment succeeds, then such invoice balances remain to zero. If not, then each invoice balance (child and parent) becomes equal to its invoice amount.

In terms of dunning (overdue), since parent Account are the one making the payments, then overdue system will compute dunning state based on the per-tenant overdue configuration and parent Account 's payment state. The children Account will inherit the same dunning state as their parent. As a result one unpaid parent invoice that did not contain any invoice item for a given child would still put this child in an overdue state.

Tutorial

Let’s create a child Account associated to one parent Account to see what happens (we assume the defauly demo bob/lazar tenant already exists):

Let’s create first the parent Account (we can see its ID in the Location header that is being returned) and also add a default payment method:

curl -v \
     -u admin:password \
     -H "X-Killbill-ApiKey: bob" \
     -H "X-Killbill-ApiSecret: lazar" \
     -H "Content-Type: application/json" \
     -H "X-Killbill-CreatedBy: demo" \
     -X POST \
     --data-binary '{"name":"Parent","email":"[email protected]","currency":"USD"}' \
     "http://127.0.0.1:8080/1.0/kb/accounts"

< Location: http://127.0.0.1:8080/1.0/kb/accounts/09d5dfdf-eff2-4b45-96d8-535ea269178e

curl -v \
     -u admin:password \
     -H "X-Killbill-ApiKey: bob" \
     -H "X-Killbill-ApiSecret: lazar" \
     -H "Content-Type: application/json" \
     -H "X-Killbill-CreatedBy: demo" \
     -X POST \
     --data-binary '{"pluginName":"__EXTERNAL_PAYMENT__","pluginInfo":{}}' \
     "http://127.0.0.1:8080/1.0/kb/accounts/09d5dfdf-eff2-4b45-96d8-535ea269178e/paymentMethods?isDefault=true"

Let’s now create the child Account (notice the new fields parentAccountId and isPaymentDelegatedToParent in the json):

curl -v \
     -u admin:password \
     -H "X-Killbill-ApiKey: bob" \
     -H "X-Killbill-ApiSecret: lazar" \
     -H "Content-Type: application/json" \
     -H "X-Killbill-CreatedBy: demo" \
     -X POST \
     --data-binary '{"name":"C1","email":"[email protected]","currency":"USD","parentAccountId":"09d5dfdf-eff2-4b45-96d8-535ea269178e", "isPaymentDelegatedToParent":true}' \
     "http://127.0.0.1:8080/1.0/kb/accounts"

< Location: http://127.0.0.1:8080/1.0/kb/accounts/ea58b7dd-eb23-4065-a3e5-980291d45ab8

Let’s now create a subscription for the child Account (we’ll use a simple monthly plan with no trial called zoo-monthly):

curl -v \
     -u admin:password \
     -H "X-Killbill-ApiKey: bob" \
     -H "X-Killbill-ApiSecret: lazar" \
     -H "Content-Type: application/json" \
     -H "X-Killbill-CreatedBy: demo" \
     -X POST \
     --data-binary '{"accountId":"ea58b7dd-eb23-4065-a3e5-980291d45ab8","externalKey":"s1","planName":"zoo-monthly"}' \
     "http://127.0.0.1:8080/1.0/kb/subscriptions"

If we inspect our DB entries, we see that there is a COMMITTED invoice for the child and a DRAFT invoice for the parent:

mysql> select * from invoices where account_id = 'ea58b7dd-eb23-4065-a3e5-980291d45ab8'\G
*************************** 1. row ***************************
        record_id: 45545
               id: 742c700d-e957-4948-a0bb-b16c0e4a4ecb
       account_id: ea58b7dd-eb23-4065-a3e5-980291d45ab8
     invoice_date: 2016-12-09
      target_date: 2016-12-09
         currency: USD
           status: COMMITTED
         migrated: 0
   parent_invoice: 0
       created_by: SubscriptionBaseTransition
     created_date: 2016-12-09 21:11:12
account_record_id: 6750
 tenant_record_id: 338
1 row in set (0.00 sec)

mysql> select * from invoices where account_id = '09d5dfdf-eff2-4b45-96d8-535ea269178e'\G
*************************** 1. row ***************************
        record_id: 45546
               id: 5a056e57-1089-4d15-a2b2-27df996dfbb1
       account_id: 09d5dfdf-eff2-4b45-96d8-535ea269178e
     invoice_date: 2016-12-09
      target_date: NULL
         currency: USD
           status: DRAFT
         migrated: 0
   parent_invoice: 1
       created_by: CreateParentInvoice
     created_date: 2016-12-09 21:11:13
account_record_id: 6749
 tenant_record_id: 338
1 row in set (0.00 sec)

Let’s now move the clock to the end of the day to trigger the transition from DRAFT to COMMITTED:

curl -v \
    -X POST \
     -u admin:password \
     -H "X-Killbill-ApiKey: bob" \
     -H "X-Killbill-ApiSecret: lazar" \
     -H "Content-Type: application/json" \
     -H "X-Killbill-CreatedBy: demo" \
    'http://127.0.0.1:8080/1.0/kb/test/clock?requestedDate=2016-12-10'

Let’s look again at the parent invoice (and also the item it contains):

mysql>  select * from invoices where account_id = '09d5dfdf-eff2-4b45-96d8-535ea269178e'\G
*************************** 1. row ***************************
        record_id: 45546
               id: 5a056e57-1089-4d15-a2b2-27df996dfbb1
       account_id: 09d5dfdf-eff2-4b45-96d8-535ea269178e
     invoice_date: 2016-12-09
      target_date: NULL
         currency: USD
           status: COMMITTED
         migrated: 0
   parent_invoice: 1
       created_by: CreateParentInvoice
     created_date: 2016-12-09 21:11:13
account_record_id: 6749
 tenant_record_id: 338
1 row in set (0.00 sec)

> select * from invoice_items  where invoice_id = '5a056e57-1089-4d15-a2b2-27df996dfbb1'\G
*************************** 1. row ***************************
        record_id: 59901
               id: bed7bd0d-4557-435c-9208-f09ef08d36c3
             type: PARENT_SUMMARY
       invoice_id: 5a056e57-1089-4d15-a2b2-27df996dfbb1
       account_id: 09d5dfdf-eff2-4b45-96d8-535ea269178e
 child_account_id: ea58b7dd-eb23-4065-a3e5-980291d45ab8
        bundle_id: NULL
  subscription_id: NULL
      description: ea58b7dd-eb23-4065-a3e5-980291d45ab8 summary
        plan_name: NULL
       phase_name: NULL
       usage_name: NULL
       start_date: NULL
         end_date: NULL
           amount: 34.000000000
             rate: NULL
         currency: USD
   linked_item_id: NULL
       created_by: CreateParentInvoice
     created_date: 2016-12-09 21:11:13
account_record_id: 6749
 tenant_record_id: 338

We can see that the parent invoice contains only one PARENT_SUMMARY item and that its state is now COMMITTED as expected.

Let’s now verify what happens on the payment side:

 mysql> select * from payments where account_id = '09d5dfdf-eff2-4b45-96d8-535ea269178e'\G
 *************************** 1. row ***************************
               record_id: 17634
                      id: b75a7646-091d-471c-824c-4cef375de714
              account_id: 09d5dfdf-eff2-4b45-96d8-535ea269178e
       payment_method_id: 857aea5d-9c55-475b-8094-7746e96448de
            external_key: e9f07f58-4332-44ee-8c4a-05c89395a308
              state_name: PURCHASE_SUCCESS
 last_success_state_name: PURCHASE_SUCCESS
              created_by: PaymentRequestProcessor
            created_date: 2016-12-10 00:00:00
              updated_by: PaymentRequestProcessor
            updated_date: 2016-12-10 00:00:00
       account_record_id: 6749
        tenant_record_id: 338
 1 row in set (0.00 sec)

 mysql>
 mysql> select * from payments where account_id = 'ea58b7dd-eb23-4065-a3e5-980291d45ab8'\G
 Empty set (0.01 sec)

As expected we see one payment for the parent invoice and no payment for the child.

Conclusion

There is a lot more to demo (regarding dunning, invoice adjustment, …​), but this should provide a highlight of what the feature is about. Note that this is a new feature in 0.18 and as such it should be seen as Beta (you are responsible to verify it works accordingly to your use case, load, …​).

In the future, we may want to add the ability to unparent children (feature is not supported yet).