diff --git a/userguide/assets/img/aviate/revenue-recognition/accounting-periods.png b/userguide/assets/img/aviate/revenue-recognition/accounting-periods.png new file mode 100644 index 000000000..b7400c2e2 Binary files /dev/null and b/userguide/assets/img/aviate/revenue-recognition/accounting-periods.png differ diff --git a/userguide/assets/img/aviate/revenue-recognition/recognition-rates.png b/userguide/assets/img/aviate/revenue-recognition/recognition-rates.png new file mode 100644 index 000000000..86ac1d06e Binary files /dev/null and b/userguide/assets/img/aviate/revenue-recognition/recognition-rates.png differ diff --git a/userguide/preview/aviate-revenue-recognition.adoc b/userguide/preview/aviate-revenue-recognition.adoc index c05366a7f..16135de6d 100644 --- a/userguide/preview/aviate-revenue-recognition.adoc +++ b/userguide/preview/aviate-revenue-recognition.adoc @@ -2,45 +2,119 @@ include::{sourcedir}/includes/insider-preview-card.adoc[] +[NOTE] +*Beta.* Revenue Recognition is an Insider Preview (beta) feature. Its behavior, API and screens can change between releases. The amounts, identifiers and screens on this page are examples, some of them produced with sample data. Always check revenue figures against your own invoices before you use them to close a month, and send your feedback through Kill Bill support. + == Introduction -The Aviate Revenue Recognition module provides tools for managing when and how revenue is recognized from your subscriptions and contracts. It supports the standard accounting practices defined by ASC 606 (Revenue from Contracts with Customers), and it gives finance teams visibility into recognized vs. deferred revenue through managed accounting periods. +The Aviate Revenue Recognition module manages when revenue is recognized from the invoices that Kill Bill generates. It spreads each invoiced amount over the period in which the service is delivered, and tracks how much of it has been recognized and how much is still deferred. Finance teams get visibility into recognized vs. deferred revenue through monthly accounting periods that they can close and reopen. + +Your accounting policy remains the responsibility of your finance team. Using this module does not by itself make your accounting compliant with a revenue standard such as ASC 606 or IFRS 15. Key capabilities include: -* *Accounting periods* -- monthly periods that can be opened, closed, and reported on -* *Revenue schedules* -- automatic recognition of revenue across the term of a contract -* *Contract-aware allocation* -- revenue allocated across multiple performance obligations using relative standalone selling prices -* *Waterfall reports* -- period-by-period view of recognized and deferred revenue +* *Automatic revenue schedules* -- every eligible invoice item gets a schedule that spreads its amount over the item's service period +* *Daily recognition* -- a background process recognizes revenue as each day of service is delivered +* *Accounting periods* -- monthly periods that are created automatically and can be closed, reopened and reported on +* *Contract-aware allocation* -- revenue allocated across the subscriptions of a contract using relative standalone selling prices +* *Reports* -- period summaries, a period-by-period waterfall of recognized and deferred revenue, revenue by plan and recognition rate +* *Schedule lookups* -- schedules and their daily entries by invoice item, account, subscription, plan, currency or contract == Key Concepts +=== Revenue Schedules and Entries + +When Kill Bill generates an invoice, Aviate creates one *revenue schedule* for each eligible invoice item. The schedule records the invoice, invoice item, account, subscription, plan and phase it came from, together with its currency, total amount, service period and recognition method. It also keeps running *recognized* and *deferred* amounts. At creation, the full amount is deferred. + +Each schedule is split into dated *revenue entries*. An entry is recognized once its date is reached. When the full amount of a schedule has been recognized, the schedule is closed. + +[[revrec-recognition-methods]] +=== Recognition Methods + +The recognition method depends only on the type of the invoice item. In this release it cannot be set per plan, product or customer. + +[cols="1,1,2", options="header"] +|=== +| Invoice item type | Recognition method | How the amount is spread + +| Recurring +| Straight-line +| Evenly, one entry per day of the service period + +| Usage +| Straight-line +| Evenly, one entry per day of the item's service period + +| Fixed (for example a setup fee) +| Immediate +| One entry for the full amount on the service start date + +| External charge +| Immediate +| One entry for the full amount on the service start date +|=== + +Items with a zero or negative amount, and other item types such as taxes and credits, do not get a schedule. Each invoice item has at most one schedule. + +For straight-line schedules: + +* The service period runs from the item's start date up to, but *not including*, its end date. A 1 January to 1 February period covers the 31 days of January. +* The daily amount is the total divided by the number of days, rounded to nine decimal places. Monthly amounts therefore follow the number of days in each month. +* The last day takes any rounding difference, so the entries always add up exactly to the schedule total. +* When a contract allocation applies (see <>), the schedule total is the allocated amount instead of the invoice item amount. + +For example, an annual subscription of $1,200.00 for the period from 1 January 2026 to 1 January 2027 (365 days) gives about $3.29 per day: + +[cols="2,1,1", options="header"] +|=== +| Month | Days | Recognized + +| January 2026 +| 31 +| $101.92 + +| February 2026 +| 28 +| $92.05 + +| April 2026 +| 30 +| $98.63 + +| Full year +| 365 +| $1,200.00 +|=== + +Monthly amounts are rounded to the cent, so the sum of the twelve rounded months can differ from the total by one cent. + +A $500 one-time setup fee is recognized in full on its service start date. + === Accounting Periods -An accounting period represents a calendar month during which revenue recognition entries are recorded. Each period follows a simple lifecycle: +An accounting period groups revenue entries by calendar month. The system identifies each period by a name in the format `YYYY-MM` (for example, `2026-04`). The system assigns each revenue entry to the accounting period corresponding to the month of its date. For example, the system assigns an entry dated `2026-04-17` to the period `2026-04`. -* *OPEN* -- entries can be added or modified -* *CLOSED* -- entries are frozen; no changes allowed -* *REOPENED* -- a closed period has been reopened by an administrator (for corrections) -Periods are auto-created monthly (named `2026-01`, `2026-02`, etc.) as invoices are generated. They can be explicitly closed at month-end to freeze the financials. +A period is either *open* or *closed*: -=== Revenue Recognition Schedules +* *Open* -- the daily recognition process recognizes the period's entries as their dates are reached. +* *Closed* -- the daily recognition process no longer recognizes the period's remaining entries, and the date and user that closed it are recorded. A closed period can be reopened, which makes it open again. -When an invoice is generated, the system creates recognition entries that spread the revenue across the appropriate periods. For example: +Periods are created automatically, one per calendar month, as invoices are generated. You can also create a period ahead of time. Close a period at month-end once its revenue has been recognized. + +[NOTE] +*Note:* Closing a period does not block billing. Kill Bill still accepts invoices, credits, adjustments and payments dated in a closed period. See <>. -* A $1,200 annual subscription generates $100/month recognition entries across 12 periods -* A $500 one-time setup fee is recognized immediately (or spread, depending on your rules) -* Usage revenue is recognized in the period it was consumed +=== Contract Revenue Allocation -=== Contract Revenue Allocation (ASC 606) +When a contract covers several subscriptions (for example a subscription, an implementation service and a support plan), Aviate allocates the contract's transaction price across them in proportion to their *standalone selling prices* (SSP): -When a contract includes multiple performance obligations (e.g., a subscription + setup + support), revenue is allocated proportionally based on *standalone selling prices* (SSP). This ensures compliance with ASC 606: +. Each subscription in the contract (a _contract term_) is treated as a performance obligation. +. The SSP for each term is looked up from the SSPs you configure for its plan, phase and currency. +. The transaction price is allocated across the terms in proportion to their SSPs. +. Each allocated amount is recognized using the recognition method of its invoice items. -. Identify the performance obligations in the contract -. Determine the standalone selling price (SSP) for each obligation -. Allocate the total transaction price proportionally -. Recognize revenue as each obligation is satisfied +See <> for details. == Getting Started with Revenue Recognition @@ -50,56 +124,262 @@ Install the Aviate plugin as described in the https://docs.killbill.io/latest/ho === Enabling Revenue Recognition -To enable revenue recognition, start Kill Bill with the following system property: +Revenue recognition is disabled by default. To enable it, start Kill Bill with the following system property: [source,bash] ---- com.killbill.billing.plugin.aviate.enableRevenueRecognition=true ---- +The following properties control revenue recognition: + +[cols="2,1,3", options="header"] +|=== +| Property | Default | Description + +| `com.killbill.billing.plugin.aviate.enableRevenueRecognition` +| `false` +| Set to `true` to create revenue schedules for new invoices and to start the daily recognition process. + +| `com.killbill.billing.plugin.aviate.revenueRecognition.delaySeconds` +| `86400` (one day) +| How often the recognition process runs, in seconds. +|=== + +Revenue schedules are created as invoices are generated. Enable the feature before you start billing the customers whose revenue you want to track. + For details on setting configuration properties, refer to the https://docs.killbill.io/latest/userguide_configuration.html[Kill Bill Configuration Guide]. === Using Revenue Recognition APIs Full API documentation is available at https://apidocs.killbill.io/aviate-revrec[our API documentation]. +All revenue recognition endpoints are under `${KB_URL}/plugins/aviate-plugin/v1/revenue`. Every request needs your Aviate token (`Authorization: Bearer ...`) and your tenant's Kill Bill API key and secret. Parameters are passed in the query string. Send `POST` requests with `Content-Type: application/json` and an empty JSON body (`{}`). + +== Using Revenue Recognition in Aviate + +Insider Preview participants get access to the Revenue Recognition screens in Aviate. The screenshots below were taken with sample data: plans, amounts, accounts and email addresses are examples. The layout can change during the preview. + +[cols="1,3", options="header"] +|=== +| Screen | What it shows + +| Dashboard +| Recognized and deferred revenue, recent accounting periods, and the plans with the most revenue + +| Waterfall +| Recognized and deferred revenue per accounting period, filtered by period range and currency + +| Periods +| Accounting periods, with their status and the close and reopen actions + +| Schedules +| One row per revenue schedule, filtered by method, period, account, plan or dates. Open a schedule to see its invoice, invoice item, item type, method, service period and entries + +| By Plan +| Total, recognized and deferred revenue per plan + +| Rates +| The share of revenue already recognized, per method (straight line or immediate) + +| SSP +| Standalone selling prices per plan, phase and currency, with their source and validity dates + +| Allocations +| Contract allocations: number of terms, transaction price and allocation method +|=== + +=== Accounting Periods Screen + +image::https://github.com/killbill/killbill-docs/raw/v3/userguide/assets/img/aviate/revenue-recognition/accounting-periods.png[Accounting periods screen, example with sample data,align=center] + +* A period is either *Open* or *Closed*. +* An open period whose end date has passed is flagged *Overdue*. This is a reminder to close it, not a separate status. +* The *Close* action is offered on the oldest open period whose end date has passed, so months are closed in order. +* The *Reopen* action is offered on closed periods. +* *Closed By* shows who closed the period. + +=== Recognition Rates Screen + +image::https://github.com/killbill/killbill-docs/raw/v3/userguide/assets/img/aviate/revenue-recognition/recognition-rates.png[Recognition rates screen, example with sample data,align=center] + +This screen compares straight-line and immediate revenue: number of schedules, total, recognized and deferred amounts, and the average number of days since invoicing. + +== How Revenue Is Recognized + +=== Schedule Creation + +When Kill Bill generates an invoice, Aviate creates a revenue schedule for each eligible invoice item and generates its entries (see <>). It also creates the monthly accounting periods those entries fall into, if they do not exist yet. Creating a schedule does not recognize any revenue. + +=== Daily Recognition + +A background process runs at the configured interval, once a day by default. Each run: + +* Recognizes every unrecognized entry dated *on or before the current date (UTC)* in an open period, including entries dated today. +* Updates each affected schedule's recognized and deferred amounts. +* Closes a schedule once its full amount has been recognized. + +Revenue belongs to the accounting period of its service date, not to the date the process ran. Revenue for April recognized by a run in May stays in April. + +=== Recognizing Revenue for a Specific Month + +In production, regular runs recognize revenue for each day of the month as it passes. Once a run executes on or after the last day of the month, the system has processed all revenue entries for that month. + +The month-end workflow is: + +. Wait for a recognition run on or after the last day of the month. +. Review the month with the period summary and the waterfall report. +. Close the period. + +See <> for the steps. + +==== Running Recognition On Demand + +The `triggerRevenueRecognition` endpoint runs recognition immediately, up to a date that you choose: + +[source,bash] +---- +curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/triggerRevenueRecognition?recognitionDate=2026-04-30" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" \ + -H "Content-Type: application/json" \ + -d '{}' +---- + +[cols="1,1,3", options="header"] +|=== +| Parameter | Required | Description + +| `recognitionDate` +| Yes +| Recognize entries dated on or before this date (`YYYY-MM-DD`) +|=== + +A successful request returns `204 No Content`. A missing `recognitionDate` is rejected with `recognitionDate is required`. + +The run works like a daily run, except that it uses `recognitionDate` instead of the current date: + +* It recognizes every unrecognized entry dated on or before `recognitionDate` in an open period, and updates and closes schedules in the same way. +* It applies to *all accounts of the tenant*. It cannot be limited to an account, a contract or a single month. +* A `recognitionDate` in the future recognizes revenue for service that has not been delivered yet. +* Recognized entries cannot be reverted. Reopening a period does not change entries that were already recognized. +* Do not run it at the same time as the scheduled recognition run. + == Managing Accounting Periods +=== Creating a Period + +Periods are created automatically, but you can create one ahead of time. All three parameters are required. The name must be unique, and the end date cannot be before the start date. A successful request returns `201 Created`. + +[source,bash] +---- +curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods?periodName=2026-05&startDate=2026-05-01&endDate=2026-05-31" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" \ + -H "Content-Type: application/json" \ + -d '{}' +---- + +[NOTE] +*Note:* Revenue entries are assigned to periods by name. Use the `YYYY-MM` format with the first and last day of the month. A period with any other name receives no revenue entries. + === Listing Periods +Add `closed=true` or `closed=false` to list only closed or only open periods. + [source,bash] ---- -curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods" \ +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods?closed=false" \ -H "Authorization: Bearer ${ID_TOKEN}" \ -H "X-Killbill-ApiKey: ${API_KEY}" \ -H "X-Killbill-ApiSecret: ${API_SECRET}" ---- -Response: +To view a single period, use `GET /v1/revenue/periods/{name}`, for example `/v1/revenue/periods/2026-04`. -[source,json] +Each period includes: + +[cols="1,3", options="header"] +|=== +| Field | Description + +| `periodName` +| The period name, for example `2026-04` + +| `startDate`, `endDate` +| The first and last day of the period + +| `closed` +| `true` if the period is closed + +| `closedDate`, `closedBy` +| When and by whom the period was closed. Empty for open periods. +|=== + +=== Viewing a Period Summary + +The period summary returns the recognized amount, the deferred (not yet recognized) amount and the number of entries in the period, for all currencies combined: + +[source,bash] ---- -[ - { "name": "2026-01", "status": "CLOSED", "totalRecognized": 45230.00 }, - { "name": "2026-02", "status": "OPEN", "totalRecognized": 38100.00 } -] +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods/2026-04/summary" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" ---- +For one row per currency, use `currencySummary` instead: + +[source,bash] +---- +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods/2026-04/currencySummary" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" +---- + +If you bill in more than one currency, use the currency summary. The plain summary adds amounts in different currencies together. + +[[revrec-closing-period]] === Closing a Period +To close a month: + +. Check that a recognition run has taken place on or after the last day of the month. +. Review the period summary and the waterfall report for that month. +. Close the period. A successful close returns `204 No Content`. + [source,bash] ---- curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods/2026-01/close" \ -H "Authorization: Bearer ${ID_TOKEN}" \ -H "X-Killbill-ApiKey: ${API_KEY}" \ - -H "X-Killbill-ApiSecret: ${API_SECRET}" + -H "X-Killbill-ApiSecret: ${API_SECRET}" \ + -H "Content-Type: application/json" \ + -d '{}' +---- + +The close is rejected if the period does not exist, is already closed, or still contains unrecognized revenue entries. In the last case the error reports how many entries remain, for example `Cannot close period 2026-01: 12 unrecognized entries remain`. + +If the close is still rejected after a recognition run has taken place on or after the last day of the month, review the remaining entries (see <>). You can then override this check with `force=true`: + +[source,bash] +---- +curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods/2026-01/close?force=true" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" \ + -H "Content-Type: application/json" \ + -d '{}' ---- -The system validates that all invoices in the period have recognition entries before allowing the close. If unrecognized entries exist, the close is rejected with a list of the affected invoices. +Closing a period does not block new invoices, credits or adjustments for that month. See <>. -=== Reopening a Period (Admin Only) +=== Reopening a Period -In exceptional cases, a closed period can be reopened for corrections: +In exceptional cases, a closed period can be reopened for corrections. A successful reopen returns `204 No Content`. The request is rejected if the period does not exist or is not closed. [source,bash] ---- @@ -108,130 +388,249 @@ curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/periods/2026-01/reopen" -H "X-Killbill-ApiKey: ${API_KEY}" \ -H "X-Killbill-ApiSecret: ${API_SECRET}" \ -H "Content-Type: application/json" \ - -d '{ "reason": "Correcting misclassified setup fee" }' + -d '{}' ---- -[NOTE] -*Note:* Reopening a period requires administrator privileges and creates an audit trail entry. +Reopening clears the closed date and closed-by user. Entries in the period that were not yet recognized become eligible again, and the next recognition run can pick them up. Entries that were already recognized are not changed. + +== Revenue Reports + +=== Revenue Waterfall + +The waterfall report shows recognized and deferred revenue for each accounting period, with one row per period and currency: + +[source,bash] +---- +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/reports/waterfall?startPeriod=2026-01&endPeriod=2026-06¤cy=USD" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" +---- + +All parameters are optional: + +[cols="1,3", options="header"] +|=== +| Parameter | Description -== Revenue Waterfall Report +| `startPeriod` +| First period to include (`YYYY-MM`, inclusive) -The waterfall report shows recognized revenue by period across all contracts: +| `endPeriod` +| Last period to include (`YYYY-MM`, inclusive) + +| `currency` +| Only include revenue in this currency, for example `USD` +|=== + +Each row contains the accounting period, the currency, the recognized amount, the deferred (not yet recognized) amount and the number of entries. The report is returned as JSON, so you can load it into a spreadsheet or another reporting tool. + +=== Revenue by Plan + +Total, recognized and deferred amounts and the number of schedules for each plan, across the whole tenant. Takes no parameters. [source,bash] ---- -curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/waterfall?from=2026-01&to=2026-06" \ +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/reports/by-plan" \ -H "Authorization: Bearer ${ID_TOKEN}" \ -H "X-Killbill-ApiKey: ${API_KEY}" \ -H "X-Killbill-ApiSecret: ${API_SECRET}" ---- -Response: - -[source,json] ----- -{ - "periods": ["2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06"], - "rows": [ - { - "contractId": "ctr-001", - "contractName": "Acme Corp -- Enterprise", - "totalValue": 12000.00, - "recognized": [1000.00, 1000.00, 1000.00, 1000.00, 1000.00, 1000.00], - "deferred": 6000.00 - }, - { - "contractId": "ctr-002", - "contractName": "Beta Inc -- Standard", - "totalValue": 2400.00, - "recognized": [200.00, 200.00, 200.00, 200.00, 200.00, 200.00], - "deferred": 1200.00 - } - ], - "totals": { - "recognized": [1200.00, 1200.00, 1200.00, 1200.00, 1200.00, 1200.00], - "deferred": 7200.00 - } -} ----- - -This report is suitable for board-level financial reporting and can be exported for use in external accounting systems. +=== Recognition Rate + +For each recognition method (straight-line or immediate): total amount, recognized amount, the share recognized so far and the number of schedules, across the whole tenant. Takes no parameters. + +[source,bash] +---- +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/reports/recognition-rate" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" +---- + +=== How Reports Are Calculated + +Reports are built from two different sources. Their figures can differ after adjustments, credits or cancellations. + +[cols="1,2,2", options="header"] +|=== +| Report | Built from | Currency handling +| Period summary +| Revenue entries in the period +| All currencies added together + +| Period currency summary +| Revenue entries in the period +| One row per currency + +| Revenue waterfall +| Revenue entries, by period +| One row per period and currency + +| Revenue by plan +| Schedule totals +| All currencies added together + +| Recognition rate +| Schedule totals, by recognition method +| All currencies added together +|=== + +In entry-based reports, "deferred" is the total of entries *not yet recognized*. + +[[revrec-allocation-details]] == Contract Revenue Allocation -When contracts have multiple performance obligations, Aviate uses standalone selling prices (SSP) to allocate revenue per ASC 606. +When a contract covers several subscriptions, Aviate uses standalone selling prices (SSP) to allocate the contract's transaction price across them. === How Allocation Works -Consider a contract with three components: +Each subscription in the contract is a contract term. Consider a contract with three subscriptions whose first invoice totals $15,000: * Enterprise subscription: $10,000/year (SSP: $10,000) * Implementation services: $3,000 one-time (SSP: $5,000) * Premium support: $2,000/year (SSP: $2,000) -* *Total contract price*: $15,000 +* *Transaction price*: $15,000 Using relative SSP allocation: [cols="1,1,1,1", options="header"] |=== -| Obligation | SSP | SSP % | Allocated Revenue +| Contract term | SSP | SSP % | Allocated Revenue | Subscription | $10,000 | 58.8% -| $8,824 +| $8,823.53 | Implementation | $5,000 | 29.4% -| $4,412 +| $4,411.76 | Support | $2,000 | 11.8% -| $1,765 +| $1,764.71 |=== -Note that the implementation fee ($3,000 charged) receives an allocation of $4,412 because the SSP-based allocation distributes the total contract price proportionally. This is the standard ASC 606 treatment for contracts where any individual obligation is discounted. +The implementation fee ($3,000 charged) receives an allocation of $4,411.76 because relative-SSP allocation distributes the total transaction price proportionally. Any rounding difference is assigned to the last term, so the allocations add up exactly to the transaction price. + +The following rules apply: + +* *When allocation happens.* A contract is allocated automatically when its first invoice is generated. The transaction price is the total of that invoice's positive charges for the contract's subscriptions. Once a contract has allocations, later invoices do not recalculate them, so later changes to SSPs do not affect it. +* *Which SSP is used.* For each term, Aviate uses the most recent SSP for the term's plan, phase and currency that is in effect on the allocation date. +* *Missing SSPs.* If a term has no SSP in effect, its SSP is taken as an equal share of the transaction price (the transaction price divided by the number of terms). +* *How allocations are used.* Once a contract term has an allocation, Aviate uses the allocated amount as the total of every revenue schedule created for that subscription's invoice items, instead of the invoice item amount. +* *Allocating explicitly.* You can run the allocation yourself with a transaction price of your choice (see below). Doing so replaces any existing allocation for the contract's terms. Schedules that already exist are not recalculated. +* *Subscriptions without a contract.* Subscriptions that are not part of a contract, such as self-service sign-ups, get revenue schedules and daily recognition, but no allocation. -The allocation is computed once at contract inception and frozen -- subsequent changes to SSPs do not retroactively affect existing contracts. +We recommend setting explicit SSPs for every plan and phase used in contracts, so that allocations are consistent and auditable. === Setting Standalone Selling Prices -SSPs can be configured at the product offering level: +SSPs are set per plan, phase and currency, with a validity period. You can manage them on the SSP screen in Aviate, or through the API: [source,bash] ---- -curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/standalonePrices" \ +curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/ssp?planName=implementation-services&phaseName=implementation-services-fixedterm¤cy=USD&amount=5000.00&effectiveDate=2026-01-01" \ -H "Authorization: Bearer ${ID_TOKEN}" \ -H "X-Killbill-ApiKey: ${API_KEY}" \ -H "X-Killbill-ApiSecret: ${API_SECRET}" \ -H "Content-Type: application/json" \ - -d '{ - "productOfferingId": "po-impl", - "standaloneSellingPrice": 5000.00, - "currency": "USD", - "effectiveDate": "2026-01-01" - }' + -d '{}' ---- -=== SSP Fallback Order +[cols="1,1,3", options="header"] +|=== +| Parameter | Required | Description + +| `planName` +| Yes +| Kill Bill plan name + +| `phaseName` +| Yes +| Kill Bill phase name of the plan + +| `currency` +| Yes +| Currency of the SSP, for example `USD` -If no explicit SSP is set, the system uses the following fallback order: +| `amount` +| Yes +| Standalone selling price + +| `effectiveDate` +| Yes +| First day the SSP applies (`YYYY-MM-DD`) + +| `expirationDate` +| No +| Last day the SSP applies. Leave empty for no end date. + +| `source` +| No +| Free-text label for where the price comes from. Defaults to `LIST_PRICE`. +|=== -. The configured standalone selling price for the product offering -. The list price of the product offering (from the pricing tower) -. The contract line item price (as a last resort) +A successful request returns `201 Created`. To list all SSPs, use `GET /v1/revenue/ssp`. To delete one, use `DELETE /v1/revenue/ssp/{id}`. Deleting an SSP removes it permanently and does not change allocations that were already made. -We recommend setting explicit SSPs for all product offerings to ensure consistent and auditable allocations. +=== Allocating a Contract Explicitly + +`currency` and `transactionPrice` are required. The response lists the resulting allocation for each contract term. + +[source,bash] +---- +curl -X POST "${KB_URL}/plugins/aviate-plugin/v1/revenue/contracts/${CONTRACT_ID}/allocations?currency=USD&transactionPrice=15000.00" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" \ + -H "Content-Type: application/json" \ + -d '{}' +---- + +To view a contract's allocations without changing them, send a `GET` request to the same path. For each term, the result shows the plan, phase, list price, SSP, SSP ratio, allocated amount, transaction price, currency and allocation method. == Viewing Revenue Details -=== By Account +=== By Account, Subscription or Plan + +List revenue schedules with any combination of these filters: + +[cols="1,3", options="header"] +|=== +| Parameter | Description + +| `accountId` +| Schedules of one account + +| `subscriptionId` +| Schedules of one subscription + +| `planName` +| Schedules for one plan + +| `currency` +| Schedules in one currency + +| `closed` +| `true` for closed schedules, `false` for open ones + +| `servicePeriodStart` +| Schedules whose service period starts on or after this date (`YYYY-MM-DD`) + +| `offset`, `limit` +| Paging. `limit` defaults to and cannot exceed 1000. +|=== + +Results are returned newest first. [source,bash] ---- -curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/accounts/a1b2c3d4-..." \ +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/schedules?accountId=${ACCOUNT_ID}&closed=false&limit=50" \ -H "Authorization: Bearer ${ID_TOKEN}" \ -H "X-Killbill-ApiKey: ${API_KEY}" \ -H "X-Killbill-ApiSecret: ${API_SECRET}" @@ -239,39 +638,136 @@ curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/accounts/a1b2c3d4-..." \ === By Contract +Contract allocations and the revenue schedules of a contract are available through two calls. The contract ID is a UUID. + +[source,bash] +---- +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/contracts/${CONTRACT_ID}/allocations" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" + +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/contracts/${CONTRACT_ID}/schedules" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" +---- + +=== By Invoice Item or Schedule + +Get the schedule of an invoice item, using its Kill Bill invoice item ID: + [source,bash] ---- -curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/contracts/ctr-001" \ +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/${INVOICE_ITEM_ID}/schedule" \ -H "Authorization: Bearer ${ID_TOKEN}" \ -H "X-Killbill-ApiKey: ${API_KEY}" \ -H "X-Killbill-ApiSecret: ${API_SECRET}" ---- -Response: - -[source,json] ----- -{ - "contractId": "ctr-001", - "totalValue": 15000.00, - "totalRecognized": 8823.53, - "totalDeferred": 6176.47, - "obligations": [ - { - "name": "Enterprise Subscription", - "allocatedRevenue": 8824.00, - "recognized": 5149.33, - "deferred": 3674.67, - "recognitionPattern": "STRAIGHT_LINE", - "termMonths": 12 - }, - { - "name": "Implementation Services", - "allocatedRevenue": 4412.00, - "recognized": 4412.00, - "deferred": 0.00, - "recognitionPattern": "POINT_IN_TIME" - } - ] -} +Get a schedule by its ID. Schedule IDs are numbers and are returned in the schedule details. + +[source,bash] +---- +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/schedules/${SCHEDULE_ID}" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" +---- + +List the revenue entries of a schedule, showing each entry's date, amount, accounting period and whether it has been recognized: + +[source,bash] +---- +curl "${KB_URL}/plugins/aviate-plugin/v1/revenue/${SCHEDULE_ID}/entriesPerSchedule" \ + -H "Authorization: Bearer ${ID_TOKEN}" \ + -H "X-Killbill-ApiKey: ${API_KEY}" \ + -H "X-Killbill-ApiSecret: ${API_SECRET}" ---- + +== Business Rules and Constraints + +=== Recognition + +* Recognition follows the service dates on the invoice item, not the invoice date or the payment date. Revenue is recognized whether or not the invoice has been paid. +* Revenue is always assigned to calendar months. Custom fiscal calendars (for example 4-4-5 periods) are not supported. + +=== Closed Periods + +* Closing a period does not block billing. Kill Bill still accepts invoices, credits, adjustments and payments dated in a closed period. +* If a new invoice item has service dates in a closed period, Aviate does not create its revenue entries. If you expect back-dated invoices, reopen the affected period *before* the invoice is generated. + +=== Data You Cannot Edit + +Revenue schedules and entries are derived from invoices. You cannot create, edit or delete them directly. + +[[revrec-adjustments]] +== Invoice Adjustments, Credits and Cancellations + +Adjustments, credits and cancellations can change revenue schedules. How these changes affect recognized and deferred revenue, and how they interact with closed periods, is still being finalized in this preview. During the preview, review revenue for any month that contains an adjustment, a credit or a mid-period cancellation before you close it. If you are testing this module, tell us which treatment your accounting policy requires. + +== Not Included in This Preview + +* Accounting journal entries, or an export to an ERP or general ledger +* A recognition method set per plan or per product +* Equal monthly amounts with prorated first and last months. Straight-line revenue is calculated per day +* Editing or reversing a revenue entry by hand. Schedules are created from invoices +* Approval of a schedule before recognition +* Blocking invoices, credits or adjustments in a closed period + +== Examples + +=== Monthly Subscription + +A customer is invoiced *$31.00* for a monthly plan covering `2026-01-01` to `2026-02-01`. + +* Aviate creates a straight-line schedule with a total of $31.00, all deferred. +* The service period is 31 days, so Aviate creates 31 entries of $1.00, dated `2026-01-01` to `2026-01-31`, all in period `2026-01`. +* After the run on `2026-01-10`, $10.00 is recognized and $21.00 is deferred. +* After the run on `2026-01-31`, the full $31.00 is recognized and the schedule is closed. + +=== Quarterly Subscription Spanning Three Months + +A customer is invoiced *$90.00* for a quarterly plan covering `2026-01-01` to `2026-04-01`, which is 90 days. The daily amount is $1.00: + +[cols="1,1,1", options="header"] +|=== +| Period | Days | Revenue + +| `2026-01` +| 31 +| $31.00 + +| `2026-02` +| 28 +| $28.00 + +| `2026-03` +| 31 +| $31.00 + +| *Total* +| *90* +| *$90.00* +|=== + +At the end of January, $31.00 is recognized and $59.00 is still deferred. + +=== Rounding + +A customer is invoiced *$100.00* for `2026-04-01` to `2026-05-01` (30 days). Each of the first 29 entries is $3.333333333 (100 รท 30, rounded to nine decimals). The last entry, on `2026-04-30`, is $3.333333343, so that the 30 entries add up to exactly $100.00. + +=== One-Time Setup Fee + +A customer is invoiced a fixed *$250.00* setup fee with a service start date of `2026-04-15`. Aviate creates an immediate schedule with one entry of $250.00 in period `2026-04`. The first run on or after `2026-04-15` recognizes the full amount and closes the schedule. + +=== Month-End Close + +On `2026-05-02`, the finance team closes April: + +. The daily run on `2026-04-30` or later has recognized all April entries. +. `GET /v1/revenue/periods/2026-04/summary` shows the recognized total for April with a deferred amount of 0. +. `POST /v1/revenue/periods/2026-04/close` returns `204 No Content`. +. `GET /v1/revenue/periods?closed=true` now lists `2026-04` with the closing date and user. + +If unrecognized entries had remained in April, step 3 would have been rejected with `Cannot close period 2026-04: N unrecognized entries remain`. After reviewing them, the team could close the month with `force=true`. \ No newline at end of file