> For the complete documentation index, see [llms.txt](https://docs.layeronecloud.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.layeronecloud.com/platform/billing/model-and-catalog.md).

# Billing model, catalog and checkout

LayerOne billing is account-credit based:

## 4.1 The billing model: an account-credit wallet <a href="#id-41-the-billing-model-an-account-credit-wallet" id="id-41-the-billing-model-an-account-credit-wallet"></a>

LayerOne billing is **account-credit based**:

* `BillingAccount` is the wallet: billing status, current suspension state, per-account overrides.
* `AccountLedgerEntry` is the **source of truth for the balance**. There is no balance column. Credit-like entries increase it, usage/debit entries decrease it, and `account_credit_balance` is the sum.
* `Invoice` is a **receipt for money that actually arrived**. Open invoices have `due_at=None` and stay hidden from receipt views; paid invoices are the customer- and admin-visible receipts.
* `Order` is a VPS/hosting purchase or an account-credit top-up.
* `PaymentAttempt` records the provider request and idempotency state.
* `Subscription` is the **billing record** for one service ([4.8](/platform/billing/ledger-and-metering.md#id-48-billing-records-subscription)).
* `VirtualMachine`, `WebHostingService`, `FloatingIPAllocation` and usage records are the billable resources that produce runtime ledger entries.

**Unpaid checkout attempts are internal payment artifacts only.** They have no due dates, are not shown as customer invoices, and **must not** trigger dunning, past-due status, suspension or service deletion. There is no invoice due-date dunning anywhere in the credit-wallet flow — **and no receivable either.** Metered usage stops at $0.00 ([4.10](/platform/billing/credit-automation.md#id-410-credit-automation-the-000-floor-grace-window-suspension-termination)), so the wallet does not go into deficit on its own: an account is funded, or it is in the grace window at zero, or it is suspended. A balance below zero can only have come from a refund, a chargeback or a staff adjustment.

Expected result: an abandoned checkout leaves no customer-visible debt, and only a successful payment creates a visible receipt and adds credit.

## 4.2 Catalog: products, plans, and the pricing rules <a href="#id-42-catalog-products-plans-and-the-pricing-rules" id="id-42-catalog-products-plans-and-the-pricing-rules"></a>

`Product` groups `Plan` rows. A plan carries `monthly_price`, `annual_price`, `hourly_price`, `calculated_annual_price`, spec columns (`cpu_cores`/`memory_mb`/`disk_gb`/`bandwidth_tb`), `compute_family`, `network_rate`, `is_active`, `metadata` (including `catalog_group` and `hidden_from_catalog`) and its `UsageMeter` rows.

**A plan's price is a live input to billing, so repricing means a new `Plan` row, never an edit.** Nothing snapshots a rate onto a server: `vm_effective_hourly_rate` calls `effective_hourly_rate_for_cadence(vm.plan, vm_billing_cadence(vm))`, which recomputes from `plan.calculated_annual_price` or `plan.hourly_price` on **every hourly metering pass, forever**. `order.metadata["pricing_snapshot"]` records what was sold but is never read for billing, and `UsageMeter.unit_price` is not read by the runtime path either.

So editing `annual_price` raises the hourly burn of every server already running on that plan, **retroactively, with no event and no audit row**. Repricing is:

1. new slugs carry the new offer;
2. old slugs move to a legacy list where the retirement helper writes `is_active=False` **and nothing else** — one field in one `.update()` cannot move a price, which is the point.

`is_active=False` is safe for a live server and is what stops new orders: checkout's only gate is `get_object_or_404(Plan..., is_active=True, product__is_active=True)`, while metering never reads `Plan.is_active` at all.

**Two traps already paid for:**

* **A frozen row must keep a non-NULL `hourly_price`,** even if it was only ever sold yearly. `_aggregate_hourly_usage` gates its queryset on `Q(plan__hourly_price__isnull=False) | Q(hourly_rate_override__isnull=False)`, so nulling it drops every server on that plan out of the metering pass entirely — not reported as skipped, simply never billed again, while the console still shows a rate. Do not "tidy" a retired yearly-only row.
* **A retired slug must forward, not 404.** The forum post that sold it is permanent. Exact-slug lookup only, never a prefix rule, and a **302** rather than a 301, because promo generations rotate by design and a permanently cached redirect would outlive the mapping.

**Rounding rules, both in the customer's favour:**

* `hourly_price_from_monthly` **rounds down**. A monthly figure is published, so the derived rate must not bill above it over 730 hours: half-up made $3.50/mo meter to $3.504 and an $80.00 custom quote meter to $80.008. `test_hourly_rate_never_bills_above_the_advertised_month` guards it.
* `annual_discount_percent_from_prices` computes a clone's discount label from its **own** two prices, floored, so a page can never advertise a saving checkout does not honour.

**Hourly billing means a 31-day month bills 744 hours,** so never promise a flat monthly figure for an hourly-cadence plan: quote the rate and the 730-hour equivalent.

**Stock switches.** The console Catalog page has independent **VPS hosting in stock** and **Web hosting in stock** switches. Turning one off blocks only new order intake for that product (including VPS API deployments). Paid orders, existing servers and services, queued provisioning, reinstall and redeploy actions remain available and never consult the switches.

## 4.3 Orders and checkout <a href="#id-43-orders-and-checkout" id="id-43-orders-and-checkout"></a>

1. The customer picks a plan, cadence and OS template. Signed-in **Deploy** (`/client/products/virtual-servers/`) uses a native grouped `<select>` built by `build_catalog_plan_options()` (`&#123;name&#125; — &#123;vcpu&#125; / &#123;ram&#125; / &#123;disk&#125; — $&#123;monthly_equivalent&#125;/mo`, `&lt;optgroup>` from `metadata.catalog_group`); the configure page (`/client/order/<slug>/`) adds a plan switcher that navigates by GET preserving cadence and template.
2. `create_order_intake` creates the `Order`. Cadence is normalised by `normalize_cadence_for_plan`; `available_plan_cadences` deliberately offers **hourly and annual only** to customers.
3. Monthly and annual orders require the upfront plan price. **Hourly orders** need `hourly_billing_eligibility`: positive credit makes the order $0.00 due; an armed saved card with a balance ≤ 0 produces an order for exactly one month of the plan's credit, charged before provisioning is queued; otherwise the customer adds credit or saves a card.
4. Checkout creates an open invoice and a payment attempt with **no due date**.
5. A successful provider confirmation marks the attempt succeeded, the invoice paid, the order paid, and posts account credit for the paid amount.
6. A paid VPS order queues provisioning; a paid top-up returns to billing.

**An hourly order is $0.00 upfront and is born PAID in `create_order_intake`** — the one paid path with no payment confirmation to hang activation off, so it activates its billing record inline.

**Optional root password.** Both Deploy and configure POST `root_password` into `create_order_intake`. Blank means auto-generate. Non-blank goes through `apps/billing/passwords.validate_root_password()` (12–72 printable ASCII, no spaces, Django `validate_password`); invalid values set `order_error` and create no order, and the posted secret is never echoed into HTML, flash messages, `BillingEvent.metadata` or order snapshots. Because Stripe and webhooks run without the browser session, a chosen password is **Fernet-staged on `Order.staged_root_password`** at intake; `queue_provisioning_for_paid_order` reads it when `DeploymentOptions.root_password` is blank, passes it into `_ensure_client_visible_virtual_machine`, then clears the order column so the credential lives on the VM only.

**OS selection.** Deploy chooses an **image source** first: **Templates** or **Bring your own ISO** (`image_source=template|iso`). Templates post `proxmox_template_id` from a hidden select driven by OS family tiles and an image-version dropdown (`os_family_picker`, `static/js/os-picker.js`). Families are `OperatingSystemFamily` rows: Debian, Ubuntu, Rocky Linux, AlmaLinux, Alpine Linux, FreeBSD, Windows and pfSense are seeded with bundled marks in `static/website/img/os/`; operators can upload a replacement icon (stored on the family row, not the container filesystem). Templates point at a family; the customer picks the OS, then a version. Image versions are listed **newest first** from numbers in the template name (`Ubuntu 24.04` above `Ubuntu 22.04`, `Debian 12` above `Debian 9`). Picking an OS tile selects the highest version that still fits the plan, unless a specific image was already chosen (product default, query, or a prior selection). An image whose `minimum_storage_gb` exceeds the plan (or the server's disk on reinstall) **stays listed**. Clicking it does not select it; it tells the customer to change to a plan with at least that much storage. Deploy and reinstall POST, `create_order_intake`, and `request_virtual_machine_reinstall` still refuse it — the provision sequence resizes the root volume to an absolute `{disk_gb}G`, and Proxmox treats a shrink as a hard failure, which on reinstall lands after destroy-vm. The deployable subset is still `client_services.proxmox_template_options_for_plan` (shared with the API, so no surface can deploy an image the other refuses). The picker catalog is `proxmox_templates_for_plan`, the same cluster/plan filter without the storage cut.

**Bring your own ISO** posts `iso_image_id` for an `available` ISO in the organization's library. It is mutually exclusive with a template. The guest is created blank with the ISO as a CD-ROM (`create-from-iso`); it is **not** a clone of Ubuntu or any other template, and Cloud-Init credentials are not applied. The installer is finished in web VNC. Unsigned configure shows a sign-in CTA on that source. Customers upload and delete ISOs under Images, and attach or eject them from a deployed server on the instance overview ([5.19](/platform/vps/images-and-backups.md#id-519-iso-library)).

**Deploy action state** comes from `hourly_billing_eligibility()`, the same service rule that prices the order — there is no second template-specific approximation. Positive balance → **Deploy VPS now**; armed card with balance ≤ 0 → **Charge card & deploy**; commitment already covered by credit → **Review & deploy**; anything still owed → **Continue to payment**.

Checkout has a 20-second duplicate-order window (fingerprinted over plan, cadence, template, ISO and networking choices). It deliberately does **not** apply to `POST /api/v1/servers`.

**Stale checkout cleanup.** Orders left in `Awaiting payment` for three days are deleted with their unpaid checkout invoices and payment attempts (`BILLING_UNPAID_ORDER_RETENTION_DAYS`). Paid/verifying payments, ledger activity, subscriptions, provisioning jobs and virtual machines prevent deletion. Unpaid invoices not attached to those orders are removed on the same window, since credit billing never collects them.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.layeronecloud.com/platform/billing/model-and-catalog.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
