Driven by the entitlement and subscription modules, along with the catalog configuration, the invoice subsystem is core to Kill Bill. This manual will walk you through a couple of use-cases, to help you understand how it works under the covers and how to make sense of the data on disk.
Overview
Before we dig into examples, let’s review some of the characteristics of the invoicing algorithm. The first thing to recall is that there are different ways to invoice customers and this is specified inside the catalog configuration. For the sake of simplicity we will discard usage-billing altogether here and focus on recurring subscriptions (along with fixed-term charges).
Invoicing for recurring subscriptions also comes in two different fashions, or so called billing modes
. Such a mode is defined at the Plan
level-- with a possible default at the catalog level — and there are 2 options:
-
IN_ADVANCE
: In such a configuration, each recurring subscription is charged at the begining of the period (in advance), and as a consequence, this can lead to pro-ration credits upon certain cancellation or change ofPlan
(special policies can be configured to make the system generate or not such pro-ration credits). -
IN_ARREAR
: In such a configuration, each recurring subscription is charged at the end of the period (in arrear), and as a consequence there is never any pro-ration credit generated because the system always know what to bill for (things can’t change back in time).
In the rest of this documentation, we will assume an IN_ADVANCE
billing mode because this is the most common and most complex to deal with.
Let’s revisit some of the terminology:
-
chargedThroughDate
orCTD
: We callchargedThroughDate
the last day of the recurring period associated with a given subscription that has been charged for; there is one such date for each subscription in the system. -
targetDate
: We calltargetDate
the date up to which we want the invoiving system to bill for. Let’s assume a monthly subscription and a current date of2012-05-01
(which corresponds to ourPHASE
event in our example below), and let’s look at the impact of thetargetDate
:-
targetDate
<2012-05-01
: Nothing happens -
2012-05-01
⇐targetDate
<2012-06-01
: System would bill for period [2012-05-01, 2012-06-01), and CTD =2012-06-01
-
2012-06-01
⇐targetDate
<2012-07-01
: System would bill for period [2012-05-01, 2012-06-01) and [2012-06-01, 2012-07-01) and CTD =2012-07-01
-
Kill Bill always invoice at the account level, meaning each time an invoice is being generated, this is for a given Kill Bill Account
(matching a given customer) and so the resulting invoice will include potentially different items matching different subscriptions associated with this particular account. In the most normal case, invoices are generated by reacting to specific events and we 'll see examples below of the system reacting to specific bus events or (future) notifications.
Initial subscription creation
Let’s assume a new user subscribes to the shotgun-monthly plan on 2012-04-01.
The plan is defined as follows:
<plan name="shotgun-monthly">
<product>Shotgun</product>
<initialPhases>
<phase type="TRIAL">
<duration>
<unit>DAYS</unit>
<number>30</number>
</duration>
</phase>
</initialPhases>
<finalPhase type="EVERGREEN">
<duration>
<unit>UNLIMITED</unit>
<number>-1</number>
</duration>
<recurring>
<billingPeriod>MONTHLY</billingPeriod>
<recurringPrice>
<price>
<currency>USD</currency>
<value>249.95</value>
</price>
</recurringPrice>
</recurring>
</finalPhase>
</plan>
Upon subscription creation, a new entry is added to the bundles
and subscriptions
tables, which look like this:
MySQL [killbill]> select * from bundles\G
*************************** 1. row ***************************
record_id: 1
id: a7a1370e-1fa4-4c32-abd5-223e1da97339
external_key: externalKey
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
last_sys_update_date: 2012-04-01 00:01:15
original_created_date: 2012-04-01 00:01:14
created_by: website-through-http-client
created_date: 2012-04-01 00:01:14
updated_by: SubscriptionBaseTransition
updated_date: 2012-04-01 00:01:15
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from subscriptions\G
*************************** 1. row ***************************
record_id: 1
id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
bundle_id: a7a1370e-1fa4-4c32-abd5-223e1da97339
category: BASE
start_date: 2012-04-01 00:01:14
bundle_start_date: 2012-04-01 00:01:14
charged_through_date: 2012-04-01 00:00:00
migrated: 0
created_by: website-through-http-client
created_date: 2012-04-01 00:01:15
updated_by: SubscriptionBaseTransition
updated_date: 2012-04-01 00:01:15
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
Associated with the subscription are a couple of subscription events, one CREATE
marking the beginning of the subscription at the effective date 2012-04-01 00:01:14
and one PHASE
event marking the date at which the EVERGREEN
phase starts:
MySQL [killbill]> select * from subscription_events\G
*************************** 1. row ***************************
record_id: 1
id: 55d5cc5e-2c58-4560-9042-f3c7115d6ccd
event_type: API_USER
user_type: CREATE
effective_date: 2012-04-01 00:01:14
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
plan_name: shotgun-monthly
phase_name: shotgun-monthly-trial
price_list_name: DEFAULT
billing_cycle_day_local: 0
is_active: 1
created_by: website-through-http-client
created_date: 2012-04-01 00:01:15
updated_by: website-through-http-client
updated_date: 2012-04-01 00:01:15
account_record_id: 1
tenant_record_id: 0
*************************** 2. row ***************************
record_id: 2
id: 8751c48e-686b-4eea-b959-52676e1bb9da
event_type: PHASE
user_type: NULL
effective_date: 2012-05-01 00:01:14
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
plan_name: NULL
phase_name: shotgun-monthly-evergreen
price_list_name: NULL
billing_cycle_day_local: 0
is_active: 1
created_by: website-through-http-client
created_date: 2012-04-01 00:01:15
updated_by: website-through-http-client
updated_date: 2012-04-01 00:01:15
account_record_id: 1
tenant_record_id: 0
2 rows in set (0.00 sec)
The subscription service has also recorded a future notification effective when the EVERGREEN
phase starts:
MySQL [killbill]> select * from notifications\G
*************************** 1. row ***************************
record_id: 1
class_name: org.killbill.billing.subscription.engine.core.SubscriptionNotificationKey
event_json: {"eventId":"8751c48e-686b-4eea-b959-52676e1bb9da","seqId":0}
user_token: f291917d-ce03-428f-9e58-e538db057d37
created_date: 2012-04-01 00:01:15
creating_owner: 127.0.0.1
processing_owner: NULL
processing_available_date: NULL
processing_state: AVAILABLE
error_count: 0
search_key1: 1
search_key2: 0
queue_name: subscription-service:subscription-events
effective_date: 2012-05-01 00:01:14
future_user_token: 892a1fdf-45b5-404d-a492-99f612ba8b55
1 row in set (0.00 sec)
The entitlement subsystem has also a record of the start of the entitlement, in the blocking_states
table (on older Kill Bill versions, this was not present so you could still see some data were this is missing and this is fine, the system knows how to handle this case):
MySQL [killbill]> select * from blocking_states\G
*************************** 1. row ***************************
record_id: 1
id: 18696a69-bcb0-40c4-98b5-9c13bc00307e
blockable_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
type: SUBSCRIPTION
state: ENT_STARTED
service: entitlement-service
block_change: 0
block_entitlement: 0
block_billing: 0
effective_date: 2012-04-01 00:01:15
is_active: 1
created_date: 2012-04-01 00:01:14
created_by: website-through-http-client
updated_date: 2012-04-01 00:01:14
updated_by: website-through-http-client
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
Upon subscription creation, a bus event is triggered and caught by the invoicing subsystem, which invoices the account with a target date of 2012-04-01. To do so, it computes the billing events from these subscription events and blocking states (they are effectively markers between billable periods). In our case, these billing events are:
DefaultBillingEvent{type=CREATE, effectiveDate=2012-04-01T00:01:14.000Z, planPhaseName=shotgun-monthly-trial, subscriptionId=d9c7bb57-675e-4419-a340-5f6b4fd612f4, totalOrdering=1}
DefaultBillingEvent{type=PHASE, effectiveDate=2012-05-01T00:01:14.000Z, planPhaseName=shotgun-monthly-evergreen, subscriptionId=d9c7bb57-675e-4419-a340-5f6b4fd612f4, totalOrdering=2}
The target date being 2012-04-01, only the first one matters. Based on the catalog configuration, the following invoice and invoice item are generated (an invoice has always 1 or more invoice items associated with it):
MySQL [killbill]> select * from invoices\G
*************************** 1. row ***************************
record_id: 1
id: 5c6369d2-cd18-489f-9fe5-748e72f9938e
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
invoice_date: 2012-04-01
target_date: 2012-04-01
currency: USD
status: COMMITTED
migrated: 0
parent_invoice: 0
created_by: SubscriptionBaseTransition
created_date: 2012-04-01 00:01:15
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from invoice_items\G
*************************** 1. row ***************************
record_id: 1
id: 19667140-fa16-48e0-b04e-579b9972f612
type: FIXED
invoice_id: 5c6369d2-cd18-489f-9fe5-748e72f9938e
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: a7a1370e-1fa4-4c32-abd5-223e1da97339
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
description: shotgun-monthly-trial
plan_name: shotgun-monthly
phase_name: shotgun-monthly-trial
usage_name: NULL
start_date: 2012-04-01
end_date: NULL
amount: 0.000000000
rate: NULL
currency: USD
linked_item_id: NULL
created_by: SubscriptionBaseTransition
created_date: 2012-04-01 00:01:15
account_record_id: 1
tenant_record_id: 0
There is only a single FIXED
item with a start date of 2012-04-01.
Upon invoice generation, an event is triggered and caught by the payment subsystem, which triggers a payment for that invoice (using the default payment method on the account).
What happens is that the payment subsystem calls the createPurchaseWithPaymentControl
API and specifies INVOICE_PAYMENT_CONTROL_PLUGIN
as the control plugin to use (you can add your own via the system property org.killbill.payment.invoice.plugin
). This plugin is responsible to compute the payment amount (typically the invoice balance) and to insert a row in the invoice_payments
table (success
is first set to false, to implement a two-phase commit strategy). The payment is then delegated to the payment system (a payment and/or a transaction are recorded if necessary). Upon success, the entry is updated with the metadata from the payment (the plugin could have decided to pay less than the requested amount for example) and the success flag is set to true
. In case of failure, success
would remain set to false and the next payment retry date would be computed, based on the system configuration.
While the link between invoice and payments is encapsulated in the invoice_payments
table, there is one level of indirection with the payments
table through the payment_attempts
table, to manage aborted payments and retries:
-
Aborted payments: In a situation where the invoice was already paid (or there is a $0 balance), the invoice control plugin would abort the payment. In such situations, we would end up with a row in the
payment_attempts
table with anABORTED
state and no row in theinvoice_payments
,payments
andpayment_transactions
tables. -
Payment Retries: In a situation where we see a payment failure (e.g. insufficient funds), a payment will be associated with multiple transactions (all sharing the same transaction external key and typically in a
PAYMENT_FAILURE
status). Each of these transactions will be associated with an attempt in aRETRIED
state.
Note also that the payment_attempts
entry is linked to the invoice via the plugin property IPCD_INVOICE_ID
(which points to the invoice id).
In our scenario, no payment was actually processed, since the invoice amount is zero (trial). Hence the ABORTED
state:
MySQL [killbill]> select * from payment_attempts\G
*************************** 1. row ***************************
record_id: 1
id: 16f869b1-c5c9-41ed-a776-87f3ce4e5bb5
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
payment_external_key: 6bd135f7-8a7d-4448-9ce9-3889055af9e3
transaction_id: NULL
transaction_external_key: ae53501e-c9dd-45e3-8ec6-78da4e9f8d99
transaction_type: PURCHASE
state_name: ABORTED
amount: NULL
currency: USD
plugin_name: __INVOICE_PAYMENT_CONTROL_PLUGIN__
plugin_properties: ZV <[{"IPCD_INVOICE_ID":"5c6369d2-cd18-489f-9fe5-748e72f9938e"}]
created_by: PaymentRequestProcessor
created_date: 2012-04-01 00:01:15
updated_by: PaymentRequestProcessor
updated_date: 2012-04-01 00:01:15
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from invoice_payments\G
Empty set (0.00 sec)
MySQL [killbill]> select * from payments\G
Empty set (0.00 sec)
Note: if the payment didn’t go through because a custom payment control plugin aborted the payment (after the built-in INVOICE_PAYMENT_CONTROL_PLUGIN
), there would be a row in invoice_payments
(with the success
flag set to false and a NULL
payment_id
) and a corresponding ABORTED
row in payment_attempts
.
See below for an example of an actual payment and what would happen in case of payment failures.
Reference time and fixed offset timezone
Each account in Kill Bill has a reference time associated with it. It is the reference_time
value associated with the accounts
row (specified at account creation time, default value is the created_date
). Additionally, it is associated with a fixed offset timezone, a special timezone used by the system for dates manipulation which ignores changes like DST (you cannot change it either). This timezone depends on the account timezone (UTC if none specified) and if DST was in effect at the reference time (this lets us handle gracefully DST gaps throughout the year with respect to invoicing, handling subscription changes, etc.).
For example, if the account timezone is America/Los_Angeles and the reference time is 2015-03-07T10:00:01.000Z, DST was not in effect and the fixed offset timezone is -08:00. If the reference time however is 2015-03-08T10:00:01.000Z, DST was in effect and the fixed offset timezone is -07:00.
While most subsystems (entitlement, subscription, etc.) work at the time level (i.e. you could have several upgrades during the day), invoicing works at the day boundary level (Kill Bill doesn’t invoice for granularities smaller than a day). Both of these parameters help us convert LocalDate
(i.e. a specify day in a year, like 2012-04-01) to a DateTime
(i.e. a specific point in time, in a specific timezone), and vice versa. The former conversion is required for instance when computing the next notification time (based on the invoice item end date for instance). The latter can happen during invoice generation, when transforming a BillingEvent
effective DateTime
to a LocalDate
for the invoice item (service period).
A nice side effect of using this fixed reference time is that most system-driven operations will always happen at the same time during the day for a given account. It also helps spreading the load on the system since the distribution of these reference times should be uniform.
Phase transition
Let’s fast forward the time to 2012-05-02.
The notification for the phase event is processed by the subscription subsystem. There is nothing to be done in that case (in other scenarios, add-ons may need to be cancelled or a future phase event may need to be computed): it simply sends a message on the bus letting the system know about the phase transition.
The invoicing subsystem picks it up and re-compute the billing events:
DefaultBillingEvent{type=CREATE, effectiveDate=2012-04-01T00:01:14.000Z, planPhaseName=shotgun-monthly-trial, subscriptionId=d9c7bb57-675e-4419-a340-5f6b4fd612f4, totalOrdering=1}
DefaultBillingEvent{type=PHASE, effectiveDate=2012-05-01T00:01:14.000Z, planPhaseName=shotgun-monthly-evergreen, subscriptionId=d9c7bb57-675e-4419-a340-5f6b4fd612f4, totalOrdering=2}
Nothing has changed but since the target date is now 2012-05-01, both events need to be taken into account. The invoice subsystem recomputes all invoice items since the beginning of time, and come up with a FIXED
item (trial period) and a RECURRING
item (for the service period 2012-05-01 to 2012-06-01). Because the FIXED
item is already present in the database, only the second one is persisted on disk, on a new invoice:
MySQL [killbill]> select * from invoices order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 2
id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
invoice_date: 2012-05-02
target_date: 2012-05-01
currency: USD
status: COMMITTED
migrated: 0
parent_invoice: 0
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:14:43
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from invoice_items order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 2
id: 2326d3ff-e90d-43f0-b611-6c028bb88c71
type: RECURRING
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: a7a1370e-1fa4-4c32-abd5-223e1da97339
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
description: shotgun-monthly-evergreen
plan_name: shotgun-monthly
phase_name: shotgun-monthly-evergreen
usage_name: NULL
start_date: 2012-05-01
end_date: 2012-06-01
amount: 249.950000000
rate: 249.950000000
currency: USD
linked_item_id: NULL
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:14:43
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
The subscription charged_through_date
is updated to 2012-06-01:
MySQL [killbill]> select * from subscriptions\G
*************************** 1. row ***************************
record_id: 1
id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
bundle_id: a7a1370e-1fa4-4c32-abd5-223e1da97339
category: BASE
start_date: 2012-04-01 00:01:14
bundle_start_date: 2012-04-01 00:01:14
charged_through_date: 2012-06-01 00:00:00
migrated: 0
created_by: website-through-http-client
created_date: 2012-04-01 00:01:15
updated_by: SubscriptionBaseTransition
updated_date: 2012-05-02 00:14:44
account_record_id: 1
tenant_record_id: 0
The system will also generate a new (future) notification on 2012-06-01. This invoice notification will be the trigger for the next invoice generation:
MySQL [killbill]> select * from notifications\G
*************************** 1. row ***************************
record_id: 2
class_name: org.killbill.billing.invoice.notification.NextBillingDateNotificationKey
event_json: {"uuidKey":"d9c7bb57-675e-4419-a340-5f6b4fd612f4","targetDate":"2012-06-01T00:00:00.000Z","isDryRunForInvoiceNotification":false}
user_token: 892a1fdf-45b5-404d-a492-99f612ba8b55
created_date: 2012-05-02 00:14:44
creating_owner: 127.0.0.1
processing_owner: NULL
processing_available_date: NULL
processing_state: AVAILABLE
error_count: 0
search_key1: 1
search_key2: 0
queue_name: invoice-service:next-billing-date-queue
effective_date: 2012-06-01 00:00:00
future_user_token: aa2c96e2-71b4-4149-abdc-2889256c2b34
1 row in set (0.00 sec)
After the invoice is generated, an event is sent to the bus, which makes the payment subsystem react to it:
MySQL [killbill]> select * from invoice_payments order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 2
id: e6e534e1-2ffa-4d5e-bcac-6905d4d26f61
type: ATTEMPT
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
payment_id: b0e61973-a921-413d-a04b-84e36e3ad6bf
payment_date: 2012-05-02 00:14:44
amount: 249.950000000
currency: USD
processed_currency: USD
payment_cookie_id: 943d005c-5f89-4664-88f5-c65f39a3a9c8
linked_invoice_payment_id: NULL
success: 1
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:14:44
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payment_attempts order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 2
id: 090fa541-7b69-42b2-bec7-a16f3c616071
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
payment_external_key: d04ce5ad-e667-4113-8eb3-6d7f87f92bca
transaction_id: 8b671a2e-6556-4aa8-8464-ef1cb99e5189
transaction_external_key: 943d005c-5f89-4664-88f5-c65f39a3a9c8
transaction_type: PURCHASE
state_name: SUCCESS
amount: NULL
currency: USD
plugin_name: __INVOICE_PAYMENT_CONTROL_PLUGIN__
plugin_properties: ZV <[{"IPCD_INVOICE_ID":"fa759cb6-6702-4a1c-85a3-9df7b101d3bc"}]
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:14:44
updated_by: PaymentRequestProcessor
updated_date: 2012-05-02 00:14:44
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payments\G
*************************** 1. row ***************************
record_id: 1
id: b0e61973-a921-413d-a04b-84e36e3ad6bf
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
external_key: d04ce5ad-e667-4113-8eb3-6d7f87f92bca
state_name: PURCHASE_SUCCESS
last_success_state_name: PURCHASE_SUCCESS
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:14:44
updated_by: PaymentRequestProcessor
updated_date: 2012-05-02 00:14:44
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payment_transactions\G
*************************** 1. row ***************************
record_id: 1
id: 8b671a2e-6556-4aa8-8464-ef1cb99e5189
attempt_id: 090fa541-7b69-42b2-bec7-a16f3c616071
transaction_external_key: 943d005c-5f89-4664-88f5-c65f39a3a9c8
transaction_type: PURCHASE
effective_date: 2012-05-02 00:14:44
transaction_status: SUCCESS
amount: 249.950000000
currency: USD
processed_amount: 249.950000000
processed_currency: USD
payment_id: b0e61973-a921-413d-a04b-84e36e3ad6bf
gateway_error_code:
gateway_error_msg:
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:14:44
updated_by: PaymentRequestProcessor
updated_date: 2012-05-02 00:14:44
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
In this case, a PURCHASE
(i.e. auto-capture) payment was performed. The invoice_payments
entry is linked to the payments
entry via payment_id
and to the transactions
table via payment_cookie_id
(which is the transaction external key).
If the payment didn’t go through the first time (e.g. insufficient funds on the credit card), and the system was configured to retry the payments 8 days after, the data would look like this on a successful retry:
MySQL [killbill]> select * from invoice_payments order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 2
id: 6a45a92e-72ee-4415-9c5a-8066d4448cc5
type: ATTEMPT
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
payment_id: d0a6b1c4-44b0-4e84-8883-4ec4cf8a3b2a
payment_date: 2012-05-09 00:00:49
amount: 249.950000000
currency: USD
processed_currency: USD
payment_cookie_id: 6a5b6c15-0a8b-43ae-8b82-f6d66568eb8f
linked_invoice_payment_id: NULL
success: 1
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:14:44
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payment_attempts where record_id > 1\G
*************************** 1. row ***************************
record_id: 2
id: be5f3706-105d-4874-9857-2b0d197b7ff3
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
payment_external_key: 73035f59-9364-408f-a41f-d89b3483cd26
transaction_id: 4935d163-7f3a-4b5d-8ad2-13dcb6d4b540
transaction_external_key: 6a5b6c15-0a8b-43ae-8b82-f6d66568eb8f
transaction_type: PURCHASE
state_name: RETRIED
amount: NULL
currency: USD
plugin_name: __INVOICE_PAYMENT_CONTROL_PLUGIN__
plugin_properties: ZV <[{"IPCD_INVOICE_ID":"fa759cb6-6702-4a1c-85a3-9df7b101d3bc"}]
created_by: PaymentRequestProcessor
created_date: 2012-05-01 00:00:46
updated_by: PaymentRequestProcessor
updated_date: 2012-05-01 00:00:46
account_record_id: 1
tenant_record_id: 0
*************************** 2. row ***************************
record_id: 3
id: 672bc1a2-189a-4615-9619-544977cca8ea
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
payment_external_key: 73035f59-9364-408f-a41f-d89b3483cd26
transaction_id: 49ff12b5-7dfc-408d-956e-3d5335818738
transaction_external_key: 6a5b6c15-0a8b-43ae-8b82-f6d66568eb8f
transaction_type: PURCHASE
state_name: SUCCESS
amount: NULL
currency: USD
plugin_name: __INVOICE_PAYMENT_CONTROL_PLUGIN__
plugin_properties: ZV <[{"IPCD_INVOICE_ID":"fa759cb6-6702-4a1c-85a3-9df7b101d3bc"}]
created_by: payment-service-retry
created_date: 2012-05-09 00:00:49
updated_by: payment-service-retry
updated_date: 2012-05-09 00:00:49
account_record_id: 1
tenant_record_id: 0
2 rows in set (0.00 sec)
MySQL [killbill]> select * from payments\G
*************************** 1. row ***************************
record_id: 1
id: d0a6b1c4-44b0-4e84-8883-4ec4cf8a3b2a
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
external_key: 73035f59-9364-408f-a41f-d89b3483cd26
state_name: PURCHASE_SUCCESS
last_success_state_name: PURCHASE_SUCCESS
created_by: PaymentRequestProcessor
created_date: 2012-05-01 00:00:46
updated_by: payment-service-retry
updated_date: 2012-05-09 00:00:49
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payment_transactions\G
*************************** 1. row ***************************
record_id: 1
id: 4935d163-7f3a-4b5d-8ad2-13dcb6d4b540
attempt_id: be5f3706-105d-4874-9857-2b0d197b7ff3
transaction_external_key: 6a5b6c15-0a8b-43ae-8b82-f6d66568eb8f
transaction_type: PURCHASE
effective_date: 2012-05-01 00:00:46
transaction_status: PAYMENT_FAILURE
amount: 249.950000000
currency: USD
processed_amount: 0.000000000
processed_currency: USD
payment_id: d0a6b1c4-44b0-4e84-8883-4ec4cf8a3b2a
gateway_error_code: 500
gateway_error_msg: Insufficient funds
created_by: PaymentRequestProcessor
created_date: 2012-05-01 00:00:46
updated_by: PaymentRequestProcessor
updated_date: 2012-05-01 00:00:46
account_record_id: 1
tenant_record_id: 0
*************************** 2. row ***************************
record_id: 2
id: 49ff12b5-7dfc-408d-956e-3d5335818738
attempt_id: 672bc1a2-189a-4615-9619-544977cca8ea
transaction_external_key: 6a5b6c15-0a8b-43ae-8b82-f6d66568eb8f
transaction_type: PURCHASE
effective_date: 2012-05-09 00:00:49
transaction_status: SUCCESS
amount: 249.950000000
currency: USD
processed_amount: 249.950000000
processed_currency: USD
payment_id: d0a6b1c4-44b0-4e84-8883-4ec4cf8a3b2a
gateway_error_code:
gateway_error_msg:
created_by: payment-service-retry
created_date: 2012-05-09 00:00:49
updated_by: payment-service-retry
updated_date: 2012-05-09 00:00:49
account_record_id: 1
tenant_record_id: 0
2 rows in set (0.00 sec)
A few things to notice:
-
There is a single
invoice_payments
entry pointing to a singlepayments
entry -
There are two
payment_attempts
, oneRETRIED
and oneSUCCESS
, pointing to two transactions inPAYMENT_FAILURE
andSUCCESS
respectfully -
The transaction external key is shared for both transactions
Invoice item adjustment
Let’s consider the case where the administrator item adjusts for $10 the recurring item (for the service period 2012-05-01 to 2012-06-01). The second invoice now has 2 new items:
MySQL [killbill]> select * from invoice_items where invoice_id = 'fa759cb6-6702-4a1c-85a3-9df7b101d3bc'\G
*************************** 1. row ***************************
record_id: 2
id: 2326d3ff-e90d-43f0-b611-6c028bb88c71
type: RECURRING
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: a7a1370e-1fa4-4c32-abd5-223e1da97339
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
description: shotgun-monthly-evergreen
plan_name: shotgun-monthly
phase_name: shotgun-monthly-evergreen
usage_name: NULL
start_date: 2012-05-01
end_date: 2012-06-01
amount: 249.950000000
rate: 249.950000000
currency: USD
linked_item_id: NULL
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:14:43
account_record_id: 1
tenant_record_id: 0
*************************** 2. row ***************************
record_id: 3
id: e702518e-2da1-4d1a-8939-316a2fef4df3
type: ITEM_ADJ
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: NULL
subscription_id: NULL
description: Invoice item adjustment
plan_name: NULL
phase_name: NULL
usage_name: NULL
start_date: 2012-05-02
end_date: 2012-05-02
amount: -10.000000000
rate: NULL
currency: USD
linked_item_id: 2326d3ff-e90d-43f0-b611-6c028bb88c71
created_by: kaui-through-http-client
created_date: 2012-05-02 00:30:41
account_record_id: 1
tenant_record_id: 0
*************************** 3. row ***************************
record_id: 4
id: 74d1d3eb-d4b7-4b45-93ac-01805156d3db
type: CBA_ADJ
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: NULL
subscription_id: NULL
description: Adjustment (account credit)
plan_name: NULL
phase_name: NULL
usage_name: NULL
start_date: 2012-05-02
end_date: 2012-05-02
amount: 10.000000000
rate: NULL
currency: USD
linked_item_id: NULL
created_by: kaui-through-http-client
created_date: 2012-05-02 00:30:41
account_record_id: 1
tenant_record_id: 0
3 rows in set (0.00 sec)
The ITEM_ADJ
of $-10 points to the recurring item (see linked_item_id
). Because the balance of the invoice was $0, a credit item (CBA_ADJ
) of $10 is also added and will be available when the next invoice is being generated.
Refund with invoice item adjustment
A variation of the invoice item adjustment is refund with invoice item adjustment, i.e. refund the customer instead of generating a credit.
This time, there would only be a single ITEM_ADJ
(no CBA_ADJ
item):
MySQL [killbill]> select * from invoice_items where invoice_id = 'fa759cb6-6702-4a1c-85a3-9df7b101d3bc'\G
*************************** 1. row ***************************
record_id: 2
id: 2326d3ff-e90d-43f0-b611-6c028bb88c71
type: RECURRING
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: a7a1370e-1fa4-4c32-abd5-223e1da97339
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
description: shotgun-monthly-evergreen
plan_name: shotgun-monthly
phase_name: shotgun-monthly-evergreen
usage_name: NULL
start_date: 2012-05-01
end_date: 2012-06-01
amount: 249.950000000
rate: 249.950000000
currency: USD
linked_item_id: NULL
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:14:43
account_record_id: 1
tenant_record_id: 0
*************************** 2. row ***************************
record_id: 3
id: aadfc291-0981-4e20-b231-8ae047e5514b
type: ITEM_ADJ
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: NULL
subscription_id: NULL
description: NULL
plan_name: NULL
phase_name: NULL
usage_name: NULL
start_date: 2012-05-02
end_date: 2012-05-02
amount: -10.000000000
rate: NULL
currency: USD
linked_item_id: 2326d3ff-e90d-43f0-b611-6c028bb88c71
created_by: kaui-through-http-client
created_date: 2012-05-02 00:15:25
account_record_id: 1
tenant_record_id: 0
2 rows in set (0.01 sec)
Note that the refund would have actually happened first and the invoice_items
and invoice_payments
tables would have been updated upon success.
Here is the state of the payment related tables:
MySQL [killbill]> select * from invoice_payments where record_id > 2\G
*************************** 1. row ***************************
record_id: 3
id: 35159bde-7bf0-4b77-a506-43aa1f37a29d
type: REFUND
invoice_id: fa759cb6-6702-4a1c-85a3-9df7b101d3bc
payment_id: b0e61973-a921-413d-a04b-84e36e3ad6bf
payment_date: 2012-05-02 00:15:25
amount: -10.000000000
currency: USD
processed_currency: USD
payment_cookie_id: 212ce168-526e-4f23-85a5-ffaa67b189a8
linked_invoice_payment_id: e6e534e1-2ffa-4d5e-bcac-6905d4d26f61
success: 1
created_by: kaui-through-http-client
created_date: 2012-05-02 00:15:25
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payment_attempts where record_id > 2\G
*************************** 1. row ***************************
record_id: 3
id: 45e7adfc-7549-4f81-8562-8c6907485275
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
payment_external_key: d04ce5ad-e667-4113-8eb3-6d7f87f92bca
transaction_id: 0d6b12b6-b54a-4092-a977-9d950214d4e4
transaction_external_key: 212ce168-526e-4f23-85a5-ffaa67b189a8
transaction_type: REFUND
state_name: SUCCESS
amount: 10.000000000
currency: USD
plugin_name: __INVOICE_PAYMENT_CONTROL_PLUGIN__
plugin_properties: ZV j s[{"IPCD_REFUND_WITH_ADJUSTMENTS:"true"},�'IDS_AMOU`"{"2326d3ff-e90d-43f0-b611-6c028bb88c71":null}}]
created_by: kaui-through-http-client
created_date: 2012-05-02 00:15:25
updated_by: kaui-through-http-client
updated_date: 2012-05-02 00:15:25
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payments\G
*************************** 1. row ***************************
record_id: 1
id: b0e61973-a921-413d-a04b-84e36e3ad6bf
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
external_key: d04ce5ad-e667-4113-8eb3-6d7f87f92bca
state_name: REFUND_SUCCESS
last_success_state_name: REFUND_SUCCESS
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:14:44
updated_by: kaui-through-http-client
updated_date: 2012-05-02 00:15:25
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payment_transactions where record_id > 1\G
*************************** 1. row ***************************
record_id: 2
id: 0d6b12b6-b54a-4092-a977-9d950214d4e4
attempt_id: 45e7adfc-7549-4f81-8562-8c6907485275
transaction_external_key: 212ce168-526e-4f23-85a5-ffaa67b189a8
transaction_type: REFUND
effective_date: 2012-05-02 00:15:25
transaction_status: SUCCESS
amount: 10.000000000
currency: USD
processed_amount: 10.000000000
processed_currency: USD
payment_id: b0e61973-a921-413d-a04b-84e36e3ad6bf
gateway_error_code:
gateway_error_msg:
created_by: kaui-through-http-client
created_date: 2012-05-02 00:15:25
updated_by: kaui-through-http-client
updated_date: 2012-05-02 00:15:25
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
A few things to notice:
-
There is a new
invoice_payments
entry of typeREFUND
, linking to the attempt throughlinked_invoice_payment_id
-
There is still a single
payments
entry, but the state is nowREFUND_SUCCESS
-
The payment has an additional
payment_transactions
entry of typeREFUND
, linking to a newpayment_attempts
entry
Change plan
Let’s assume the user changes on 2012-05-02 to the blowdart-monthly plan.
The plan is defined as follows:
<plan name="blowdart-monthly">
<product>Blowdart</product>
<initialPhases>
<phase type="TRIAL">
<duration>
<unit>DAYS</unit>
<number>30</number>
</duration>
</phase>
<phase type="DISCOUNT">
<duration>
<unit>MONTHS</unit>
<number>6</number>
</duration>
<recurring>
<billingPeriod>MONTHLY</billingPeriod>
<recurringPrice>
<price>
<currency>USD</currency>
<value>9.95</value>
</price>
</recurringPrice>
</recurring>
</phase>
</initialPhases>
<finalPhase type="EVERGREEN">
<duration>
<unit>UNLIMITED</unit>
</duration>
<recurring>
<billingPeriod>MONTHLY</billingPeriod>
<recurringPrice>
<price>
<currency>USD</currency>
<value>29.95</value>
</price>
</recurringPrice>
</recurring>
</finalPhase>
</plan>
For this scenario, we assume a START_OF_SUBSCRIPTION
change alignment. Conceptually, the timeline for the subscriptions are as follows:
-
shotgun-monthly:
[TRIAL 2012-04-01 → 2012-05-01][EVERGREEN 2012-05-01 → …]
-
blowdart-monthly:
[TRIAL 2012-04-01 → 2012-05-01][DISCOUNT 2012-05-01 → 2012-11-01][EVERGREEN 2012-11-01 → …]
With a START_OF_SUBSCRIPTION
change alignment, both timelines align on the start of the subscription (2012-04-01). On 2012-05-02, the target phase is hence the DISCOUNT
one. If we had chosen a CHANGE_OF_PLAN
alignment instead, the blowdart-monthly
timeline would have been aligned on the date of the change (2012-05-02) and the target phase would have been the TRIAL
one.
Two new subscription events are recorded, one for the change and one for the future phase change:
MySQL [killbill]> select * from subscription_events where record_id > 2\G
*************************** 1. row ***************************
record_id: 3
id: c3a87783-b6fc-41bd-acc9-130aba08dc5c
event_type: API_USER
user_type: CHANGE
effective_date: 2012-05-02 00:37:59
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
plan_name: blowdart-monthly
phase_name: blowdart-monthly-discount
price_list_name: DEFAULT
billing_cycle_day_local: 0
is_active: 1
created_by: website-through-http-client
created_date: 2012-05-02 00:37:59
updated_by: website-through-http-client
updated_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
*************************** 2. row ***************************
record_id: 4
id: 0f448b11-b705-4074-b6a6-ecc62cc2b305
event_type: PHASE
user_type: NULL
effective_date: 2012-11-01 00:01:14
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
plan_name: NULL
phase_name: blowdart-monthly-evergreen
price_list_name: NULL
billing_cycle_day_local: 0
is_active: 1
created_by: website-through-http-client
created_date: 2012-05-02 00:37:59
updated_by: website-through-http-client
updated_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
2 rows in set (0.00 sec)
And a new invoice is generated with 3 items:
MySQL [killbill]> select * from invoices order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 3
id: 742a1a60-58f8-4ed2-b9db-b1816a741f44
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
invoice_date: 2012-05-02
target_date: 2012-05-02
currency: USD
status: COMMITTED
migrated: 0
parent_invoice: 0
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from invoice_items where record_id > 4\G
*************************** 1. row ***************************
record_id: 5
id: d6b71f66-d54a-4cfc-bb73-ba55c91d8429
type: RECURRING
invoice_id: 742a1a60-58f8-4ed2-b9db-b1816a741f44
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: a7a1370e-1fa4-4c32-abd5-223e1da97339
subscription_id: d9c7bb57-675e-4419-a340-5f6b4fd612f4
description: blowdart-monthly-discount
plan_name: blowdart-monthly
phase_name: blowdart-monthly-discount
usage_name: NULL
start_date: 2012-05-02
end_date: 2012-06-01
amount: 9.630000000
rate: 9.950000000
currency: USD
linked_item_id: NULL
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
*************************** 2. row ***************************
record_id: 6
id: 592d7318-8fea-4e3b-b843-f6d444857132
type: REPAIR_ADJ
invoice_id: 742a1a60-58f8-4ed2-b9db-b1816a741f44
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: NULL
subscription_id: NULL
description: Adjustment (subscription change)
plan_name: NULL
phase_name: NULL
usage_name: NULL
start_date: 2012-05-02
end_date: 2012-06-01
amount: -239.950000000
rate: NULL
currency: USD
linked_item_id: 2326d3ff-e90d-43f0-b611-6c028bb88c71
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
*************************** 3. row ***************************
record_id: 7
id: 3413d964-90d3-4aa0-8b0e-406d02eb231e
type: CBA_ADJ
invoice_id: 742a1a60-58f8-4ed2-b9db-b1816a741f44
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
child_account_id: NULL
bundle_id: NULL
subscription_id: NULL
description: Adjustment (account credit)
plan_name: NULL
phase_name: NULL
usage_name: NULL
start_date: 2012-05-02
end_date: 2012-05-02
amount: 230.320000000
rate: NULL
currency: USD
linked_item_id: NULL
created_by: SubscriptionBaseTransition
created_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
3 rows in set (0.00 sec)
The REPAIR_ADJ
item on the new invoice points to the RECURRING
item on the second invoice that is being adjusted (repaired). The amount repaired is only $239.95 because of the previous item adjustment.
Because the new rate is only $9.95, the account has extra credit of $239.95 - $9.63 (pro-rated) = $230.32 (new CBA_ADJ
item).
Note that the previous invoices have not been updated in that case.
A new payment attempt is triggered, but aborted since the invoice balance is $0:
MySQL [killbill]> select * from invoice_payments order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 3
id: e7f6b1d0-3823-4c5b-9226-12327af64492
type: ATTEMPT
invoice_id: 742a1a60-58f8-4ed2-b9db-b1816a741f44
payment_id: NULL
payment_date: 2012-05-02 00:37:59
amount: 0.000000000
currency: USD
processed_currency: USD
payment_cookie_id: bac67833-ac7c-458d-8e2a-659c0cdfb0b6
linked_invoice_payment_id: NULL
success: 0
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
MySQL [killbill]> select * from payment_attempts order by record_id desc limit 1\G
*************************** 1. row ***************************
record_id: 3
id: 0703a3de-9216-4601-95cd-8540e0687bf5
account_id: 8e4f353f-ddbb-4155-b52d-9fe77b8e96e3
payment_method_id: c046e5be-e632-444a-905f-c4bc0c5c0086
payment_external_key: dff4c517-b229-4796-afe2-112aa4ba35e5
transaction_id: NULL
transaction_external_key: bac67833-ac7c-458d-8e2a-659c0cdfb0b6
transaction_type: PURCHASE
state_name: ABORTED
amount: NULL
currency: USD
plugin_name: __INVOICE_PAYMENT_CONTROL_PLUGIN__
plugin_properties: ZV <[{"IPCD_INVOICE_ID":"742a1a60-58f8-4ed2-b9db-b1816a741f44"}]
created_by: PaymentRequestProcessor
created_date: 2012-05-02 00:37:59
updated_by: PaymentRequestProcessor
updated_date: 2012-05-02 00:37:59
account_record_id: 1
tenant_record_id: 0
1 row in set (0.00 sec)
Invoicing internals
The invoice subsystem computes new invoices in several stages.
First, it loads all existing invoices on disk for the account being processed, and creates a tree per subscription (SubscriptionItemTree
) containing all of these invoice items. Each node in the tree represents all invoice items for a given service period. The service period of a parent node overlaps all service periods of its children.
For example, let’s assume a subscription was billed in advance from 2012-05-01 to 2012-06-01, and a change plan occured on 2012-05-07. There would be 3 items on disk, one recurring for the first plan (2012-05-01 to 2012-06-01), a second recurring for the second plan (2012-05-07 to 2012-06-01) and a repair adjustment (from 2012-05-07 to 2012-06-01) associated with the initial item.
The tree would look like this:
A
/
B
/
C
A: [2012-05-01,2012-06-01]
B: [2012-05-01,2012-06-01](A)
C: [2012-05-07,2012-06-01](A,C)
A
is the root node, it doesn’t contain any item and the service period is the largest period containing all items in the tree. Node B
represents the initial recurring period (the node has one ADD
— or A
— invoice item). B
has one child node — C
— which has two items, the ADD
item representing the new recurring item, and the CANCEL
item, pointing to the ADD
item in the node B
, representing the repair.
The next step is to build the tree: resulting invoice items for each service period are computed and item adjustments processed (if any). In this case, 2 items would be generated (one from 2012-05-01 to 2012-05-07 and one from 2012-05-07 to 2012-06-01). The original tree is then replaced (flatten phase) by a shallow one containing these items (it’s really a list at this point):
A
/
Bc
A: [2012-05-01,2012-06-01]
B: [2012-05-01,2012-05-07](C)
c: [2012-05-07,2012-06-01](C)
The items already invoiced are also reversed and labeled as C
(CANCEL
) before the next step.
Billing events are then processed and a list of proposed invoice items is generated. Each item will either be matched to an (existing) item in the flattened list, trigger a repair (in case of new change plan for instance) or kept as-is.
For example, if a proposed RECURRING
item from 2012-05-01 to 2012-05-07 is merged, the flattened list becomes:
A
/
Bc
A: [2012-05-01,2012-06-01]
B: [2012-05-01,2012-05-07]
c: [2012-05-07,2012-06-01](C)
Note that node B
is empty now, the existing item has been removed (matched).
If a change plan would occur on 2012-05-08 and a proposed RECURRING
item from 2012-05-07 to 2012-05-08 is merged, the list would become:
A
/
B-c
/
D
A: [2012-05-01,2012-06-01]
B: [2012-05-01,2012-05-07]
c: [2012-05-07,2012-06-01](C)
D: [2012-05-07,2012-05-08](A)
A new node D
, child of c
, is inserted representing that period. After the RECURRING
from 2012-05-08 to 2012-06-01 is merged, the list would be unchanged (no match found and no repair to create) and the RECURRING
item kept on the side.
Finally, one last time, the tree is built and the computed invoice items become the resulting items, i.e. the items for the resulting invoice. In our case, two items are generated: the RECURRING
kept as-is (outside the tree), and a repair from 2012-05-08 to 2012-06-01.
Keeping invoices in DRAFT mode
In some situations, we would like Kill Bill to generate the invoices, but leave them in a DRAFT
mode.
Examples:
-
Case where we need to append invoice items after generation
-
Case where we want payment system to ignore the invoice until it has been
COMMITTED
This behavior can be configured at the account level through the AUTO_INVOICING_DRAFT
tag: when an Account
is tagged with AUTO_INVOICING_DRAFT
, any automatic invoice generated by the system (such as when generating invoices for recurring and usage subscriptions) will end up in DRAFT
mode. Generating invoices through API calls (e.g. adding external charges) will not be affected by this tag.
Implementation expectation
Any DRAFT
invoice containing RECURRING
or USAGE
items will be taken into consideration by the system when computing subsequent invoices, regardless of the AUTO_INVOICING_DRAFT
tag.
As an example, let’s assume a monthly recurring subscription billed on the 1st of the month:
-
AUTO_INVOICING_DRAFT
is added -
January 1st :
DRAFT
invoice generated with aRECURRING
item from January 1st to February 1st -
AUTO_INVOICING_DRAFT
is removed -
February 1st :
COMMITTED
invoice generated with aRECURRING
item from February 1st to March 1st — although the previous invoice has not been committed yet
In other words, the feature assumes that at some point such DRAFT
invoices will be committed by the user. Not doing so will lead to users not being billed for those specific periods.
Invoice Date
The invoice date is the one from when the DRAFT
invoice was generated — the date at which this invoice gets committed is not reflected. In particular, this may have some impact if the payment associated with such invoice fails, and dunning kicks-in: the timeSinceEarliestUnpaidInvoiceEqualsOrExceeds
overdue configuration option will be based on the original invoice creation date (DRAFT
).
Re-using invoices in DRAFT mode
The account level AUTO_INVOICING_REUSE_DRAFT
control tag will make the invoicing system reuse an existing DRAFT
invoice instead of creating a new invoice when generating invoices for recurring and usage subscriptions, or when generating parent summary invoices:
-
If there is no existing
DRAFT
invoice, the system would default to normal behavior and create a new invoice -
If there is one existing
DRAFT
invoice, the system would use thisDRAFT
invoice instead of creating a new one -
If there is more than one, the system will use the earliest
DRAFT
invoice
Similar to the AUTO_INVOICING_DRAFT
feature, it is the responsibility of the user to to commit such DRAFT
invoices.
Parked Accounts
Because invoicing drives payments, any issue in the system or misconfiguration leading to wrong invoices could have a direct impact on your customers. In order to avoid bad invoicing (and potentially invalid customer payment) we have introduced a mechanism to PARK the accounts. Once an account has been parked, no more invoicing will happen until its state has been fixed (or until safety bounds have been removed). A account can become parked in the following scenarios:
First, the system now has a safety bound mechanism on the upper limit of invoice items generated for a given subscription on a given day. This safety bound is controlled by a system property org.killbill.invoice.maxDailyNumberOfItemsSafetyBound
whose default value is set to 15
but that can also be disabled by setting it to -1
. The safety bound will kick-in if a customer tries to buy too many things during a single day or in situations where the customer has not been invoiced for a while and the system is catching up (could happen if the account was tagged with AUTO_INVOICING_OFF
and the tag gets removed).
Second, the invoicing system will automatically park an account when an illegal state is reached during invoicing. This could be because the data associated with the account looks corrupted (which could happen because of previous bugs that did corrupt the data, manual data manipulation, or existing bug in the system).
The way to get out of that state is to fix the issue that led to that state in the first place and trigger a new invoice generation. If this issue arose because of a bug, fixing the issue may require voiding invoices creating the issues.
For cases where there are many parked accounts, we have added an administrative api to go through all parked accounts and issue an invoice generation for each of those, only leaving in the parked state, accounts whose state has not been fixed yet. If you think the issue that led into that state is not a problem with the account state (data) but a bug in the system, please report to the mailing list.
Summary
The Kill Bill invoicing system generates invoices on a per account level based on some system trigger:
-
Bus event: Those are usually subscription events like when a subscription starts, when there is a phase transition (trial → evergreen), upon plan change or cancellation (there are also other bus events in the system that could trigger an invoice generation, but not discussed for sake of simplicity).
-
Future notfications: The recurring piece of the invoiving is based on the invoice system inserting future notfications on the right date (so as to be called back at the right time and generate the next invoice).
Note also that invoice generation can be triggered through API but this is not the default use case.
The computation of the next invoice (and matching invoice items) is based upon the following:
-
A billing mode (e.g
IN_ADVANCE
) -
A set of billing events (mostly aggregated across
subscritpion_events
andblocking_states
tables) -
A
targetDate
specifying upon which point to bill (and which billing events to consider)
The invoicing algorithm recomputes the full view at each invocation and compares the existing view on disk (what was already billed) with the new set of proposed items, and based on the difference generates the next invoice (or nothing if there is no change since the previous invocation). If a new invoice was generated, it also inserts a new future notification and updates the chargedThroughDate
for all subscriptions.
In terms of payments, the source of truth resides in the payments
(and associated payments_transactions
) tables. However, the payment view of the invoicing subsystem relies on the invoice_payments
table and is used to compute things like invoice and account balance: The invoice_payments
table is updated using a 2-phase commit algorithm where a row is first inserted with a status
set to false and then upon payment completion is updated to true
.