Transactions are recorded in an append-only double-entry book keeping ledger
using the following
|created_at||Date of creation|
|descr||Free-form text description (optional)|
|event_id||Tie-in to other models or third-party systems (optional)|
|dest_account||Target account (Funds, Income, Expenses, etc.)|
|dest_amount||Target amount in
|dest_unit||Currency unit of the target amount (defaults to ‘usd’)|
|orig_account||Source account (Funds, Income, Expenses, etc.)|
|orig_amount||Source amount in
|orig_unit||Currency unit of the source amount (defaults to ‘usd’)|
Transaction records the movement of an amount from an source
to a target.
All transactions can be expored in ledger-cli format using the export command:
python manage.py ledger export
In a minimal cash flow accounting system, orig_account and dest_account
are optional, or rather each
Organization only has one account (Funds)
because we only keep track of the actual transfer of funds.
In a more complex system, like here, we want to keep track of cash assets,
revenue and expenses separately because those numbers are meaningful
to understand the business. The balance sheet we want to generate at the end
of each accounting period will dictate the number of accounts each
Organization has as well as the movements recorded in the double-entry
In an online subscription business, there are two chain of events that
Transaction to be recorded: the
subscription pipeline itself and the charge pipeline.
- place a subscription order from a
- period start
- period end
- charge sucessful
- refund or chargeback (optional)
- refund or chargeback expiration (cannot be disputed afterwards)
The balance sheet we are working out of leads to 11 accounts, 9 directly derived from above then 2 more (Withdraw and Writeoff) to balance the books.
- Cash received by a provider that was received in advance of earning it.
- Cash taken back out of a provider funds by the platform on a dispute.
- Receivables are written off
- Fees paid by provider to a processor to settle a credit card payment.
- Cash amount currently held on the platform by a provider.
- Taxable income on a provider for service provided and invoiced.
- Balance due by a subscriber.
- Order of a subscription to a plan as recorded by a subscriber.
- Record an offline payment to a provider (ex: paper check).
- Order of a subscription to a plan as recorded by a provider.
- Cash willingly transfered out of a provider funds held on the platform.
- Cash transfered back to a subscriber credit card.
- Cash that was taken out of the platform by a provider.
- Payables that cannot and will not be collected by a provider
Place a subscription order from a
new_subscription_order(subscription, nb_natural_periods, prorated_amount=0, created_at=None, descr=None, discount_percent=0, descr_suffix=None)¶
Each time a subscriber places an order through the /billing/:organization/cart/ page, a
Transactionis recorded as follow:
yyyy/mm/dd sub_***** description subscriber:Payable amount provider:Receivable
2014/09/10 subscribe to open-space plan xia:Payable $179.99 cowork:Receivable
nb_periods, the number of period paid in advance, is stored in the
Transactionis created in
TransactionManager.new_subscription_order, then only later saved when
TransactionManager.record_orderis called through
orig_amountby the correct amount in the expected currency.
When a charge through the payment processor is sucessful, a unique
Transactionrecords the charge through the processor. The amount of the charge is then redistributed to the providers (minus processor fee):
; Record the charge yyyy/mm/dd cha_***** charge event processor:Funds charge_amount subscriber:Liability ; Compensate for atomicity of charge record (when necessary) yyyy/mm/dd sub_***** invoiced-item event subscriber:Liability min(invoiced_item_amount, subscriber:Payable balance_payable) ; Distribute processor fee and funds to the provider yyyy/mm/dd cha_***** processor fee paid by provider provider:Expenses processor_fee processor:Backlog yyyy/mm/dd cha_***** broker fee paid by provider provider:Expenses broker_fee_amount broker:Backlog yyyy/mm/dd cha_***** distribution to broker broker:Funds broker_fee_amount processor:Funds yyyy/mm/dd sub_***** distribution to provider (backlog accounting) provider:Receivable plan_amount provider:Backlog yyyy/mm/dd cha_***** distribution to provider provider:Funds distribute_amount processor:Funds
2014/09/10 Charge ch_ABC123 on credit card of xia stripe:Funds $179.99 xia:Liability 2014/09/10 Keep a balanced ledger xia:Liability $179.99 xia:Payable 2014/09/10 Charge ch_ABC123 broker fee to cowork cowork:Expenses $17.99 broker:Backlog 2014/09/10 Charge ch_ABC123 distribution due to cowork broker:Funds $17.99 stripe:Funds 2014/09/10 Charge ch_ABC123 processor fee for open-space cowork:Expenses $5.22 stripe:Backlog 2014/09/10 Charge ch_ABC123 distribution for open-space cowork:Receivable $179.99 cowork:Backlog 2014/09/10 Charge ch_ABC123 distribution for open-space cowork:Funds $156.78 stripe:Funds
Refund and Chargeback¶
Refunds are initiated by the provider while chargebacks are initated by the subscriber. In either case, they represent a loss of income while the service was provided.
create_refund_transactions(refunded_amount, charge_available, charge_processor_fee, charge_broker_fee, corrected_available, corrected_processor_fee, corrected_broker_fee, created_at=None, refund_type=None)¶
ChargeItemcan be partially refunded:
yyyy/mm/dd cha_*****_*** refund to subscriber provider:Refund refunded_amount subscriber:Refunded yyyy/mm/dd cha_*****_*** refund of processor fee processor:Refund processor_fee processor:Funds yyyy/mm/dd cha_*****_*** refund of broker fee processor:Refund broker_fee broker:Funds yyyy/mm/dd cha_*****_*** cancel distribution processor:Refund distribute_amount provider:Funds
Refundis replaced by
Chargebackif the refund was initiated by a chargeback event.
2014/09/10 Charge ch_ABC123 refund for subscribe to open-space plan cowork:Refund $179.99 xia:Refunded 2014/09/10 Charge ch_ABC123 refund processor fee stripe:Refund $5.22 stripe:Funds 2014/09/10 Charge ch_ABC123 refund broker fee stripe:Refund $17.99 broker:Funds 2014/09/10 Charge ch_ABC123 cancel distribution stripe:Refund $156.78 cowork:Funds
Stripe allows you to issue a refund at any time up to 90 days after the charge while for most transactions, subscribers have 120 days from the sale or when they discovered a problem with the product to dispute a charge.
The provider will incur an extra fee on the chargeback that we record as such:
yyyy/mm/dd chargeback fee processor:Funds chargeback_fee provider:Funds
create_withdraw_transactions(event_id, amount, unit, descr, created_at=None, dry_run=False)¶
Withdraw funds from the site into the provider’s bank account.
We record one straightforward
Transactionfor the withdrawal and an additional one in case there is a processor transfer fee:
yyyy/mm/dd po_***** withdrawal to provider bank account processor:Withdraw amount provider:Funds ; With StripeConnect there are no processor fees anymore ; for Payouts. yyyy/mm/dd processor fee paid by provider processor:Funds processor_fee provider:Funds
2014/09/10 withdraw from cowork stripe:Withdraw $174.52 cowork:Funds
payment_successful generates a seemingly
complex set of
Transaction. Now we see how the following events
build on the previously recorded transactions to implement deferred revenue
The following events create “accounting” transactions. No actual funds is transfered between the organizations.
When a period starts and we have a payable balance for a subscription, we transfer it to a
Liabilityaccount, recorded as follow:
yyyy/mm/dd sub_***** description subscriber:Liability period_amount subscriber:Payable
2014/09/10 past due for period 2014/09/10 to 2014/10/10 xia:Liability $179.99 xia:Payable
create_income_recognized(subscription, amount=0, starts_at=None, ends_at=None, descr=None, event_id=None, dry_run=False)¶
When a period ends and we either have a
Backlog(payment was made before the period starts) or a
Receivable(invoice is submitted after the period ends). Either way we must recognize income for that period since the subscription was serviced:
yyyy/mm/dd sub_***** When payment was made at begining of period provider:Backlog period_amount provider:Income yyyy/mm/dd sub_***** When service is invoiced after period ends provider:Receivable period_amount provider:Income
2014/09/10 recognized income for period 2014/09/10 to 2014/10/10 cowork:Backlog $179.99 cowork:Income
Sometimes, a provider will give up and assume receivables cannot be recovered from a subscriber. At that point the receivables are written off.:
yyyy/mm/dd sub_***** balance ledger subscriber:Liability payable_amount subscriber:Payable yyyy/mm/dd sub_***** write off liability provider:Writeoff liability_amount subscriber:Liability yyyy/mm/dd sub_***** write off receivable subscriber:Canceled liability_amount provider:Receivable
2014/09/10 balance ledger xia:Liability $179.99 xia:Payable 2014/09/10 write off liability cowork:Writeoff $179.99 xia:Liability 2014/09/10 write off receivable xia:Canceled $179.99 cowork:Receivable
new_subscription_statement(subscription, created_at=None, descr_pat=None, balance_now=None)¶
Since the ordering system is tightly coupled to the
Transactionledger, we create special “none” transaction that are referenced when a
Chargeis created for payment of a balance due by a subcriber:
yyyy/mm/dd sub_***** description subscriber:Settled amount provider:Settled
2014/09/10 balance due xia:Settled $179.99 cowork:Settled
Charges are recorded in a table separate from the ledger. They undergo their own state diagram as follows.
ChargeItem records every line item for a
Charge. The recorded
is critical to easily record refunds, chargeback disputes and reverted
chargebacks in an append-only double-entry bookkeeping system.