Pricing Plans
This document describes the pricing plan system used by bbserver billing.
At a high level:
- A price plan is selected by
(market_id, item_code)at a given timestamp. - A plan contains a base
unit_price_minor(and optional micro pricing fields). - A plan contains tiers that apply
multiplier_bpsbased on device count. - Billing code computes an amount and records an
issue_chargesrow, then debits the user wallet.
Where pricing plans live
Source config (JSON)
Pricing plans are authored in:
apps/bbserver/config_dir/pricing_plans.json
Format:
- Root object contains
plans: [] - Each entry is a plan object, optionally with
tiers: []
Markets config
Valid markets are configured in:
apps/bbserver/config_dir/markets.json
market_id in pricing plans should match one of the configured market IDs (e.g. US, CN).
Loaded storage (MySQL)
Plans and tiers are stored in MySQL tables:
price_plansprice_plan_tiers
The query used to resolve an active plan is implemented in:
libs/bbdb/src/billing_store_mysql.cpp
Resolution logic:
WHERE market_id = ? AND item_code = ? AND is_active = 1- Optional validity window filters:
valid_from,valid_to - Choose best match:
ORDER BY priority DESC, valid_from DESC LIMIT 1
Admin APIs
Plans can be listed/edited/reloaded via the admin pricing handler:
- Routes:
apps/bbserver/include/handler_route_builder.hpp - Handler:
apps/bbserver/include/http_handlers/admin_pricing_handler.hpp
Endpoints (server-side):
GET /apiv1/admin/pricing/plans→ list plansGET /apiv1/admin/pricing/plans/:plan_id→ get a planPUT /apiv1/admin/pricing/plans/:plan_id→ update a plan + tiersPOST /apiv1/admin/pricing/plans/reload→ reload plans from config (pricing_plans)POST /apiv1/admin/pricing/markets/reload→ reload markets from config (markets)
Reload implementation is in:
libs/bbdb/include/payment/price_plan_admin_service.cpp
Schema (plan fields)
Each plan entry supports (most common):
market_id(string, required)- Example:
US,CN - Must be non-empty when resolving a plan (resolver returns HTTP 422 otherwise).
- Example:
item_code(string, required for authoring; defaulted by some call sites)- Used by billing code to select the plan.
name(string)- Human-readable unique-ish identifier.
currency(string)- Example:
USD,CNY
- Example:
unit_price_minor(integer)- The base price per unit in minor currency units.
- Example: USD cents.
priority(integer)- Higher wins when multiple active plans match.
is_active(boolean)- Only active plans are considered.
- Optional validity window:
valid_from/valid_to(MySQL timestamps)
Optional micro-pricing (used for higher precision or alternative units):
unit_price_micro(integer)micro_precision(integer, default = 6)amount_micro(integer)
Notes:
- During config reload, the service will default missing micro fields:
micro_precision = 6unit_price_micro = 0amount_micro = unit_price_micro
Schema (tier fields)
Tiers define how the plan scales with device count:
min_devices(integer, required)max_devices(integer, optional)- If omitted, the tier is unbounded on the high end.
multiplier_bps(integer, optional; default = 10000)- Basis points multiplier.
10000= 1.0x. 20000= 2.0x.
- Basis points multiplier.
label(string, optional)
Tier validation (server-side) requires:
min_devices >= 1- Tiers must be ordered, non-overlapping, and contiguous-ish by increasing ranges
max_devices >= min_deviceswhen present- Only one unbounded tier allowed
(See tier checks in libs/bbdb/include/payment/price_plan_admin_service.cpp.)
(See tier checks in libs/bbdb/include/payment/price_plan_admin_service.cpp.)
How amounts are computed
1) Pick a plan
Given (market_id, item_code, at_epoch):
- Find the best active plan in
price_plans. - Load its tiers from
price_plan_tiers.
Resolution service:
libs/bbdb/include/payment/price_plan_resolver_service.cpp
2) Pick a tier
The tier is selected by comparing a device count to tier ranges.
Typical patterns:
- Issuance charge uses
agents_count(device count for the certificate). - Device attach charge uses
total_devices_after_attach.
3) Compute unit price and amount
Minor units:
unit_price_minor_effective = unit_price_minor * multiplier_bps / 10000amount_minor = unit_price_minor_effective * quantity
Micro units (if used):
amount_micro = unit_price_micro * multiplier_bps / 10000 * quantity
Exact details differ slightly per billing call-site.
Canonical item_code catalog
These are the item_code values currently used by billing code and config.
certificate.issue
Meaning:
- Charges for certificate issuance and/or master-wrapped device attachment.
Used by:
- Issuance charge runner:
apps/bbserver/src/issue_charge_runner.cpp - Default for device attach billing:
libs/bbdb/include/billing/device_attach_billing_service.cpp
ca.distribution
Meaning:
- Charges when a CA trust bundle is distributed/attached to a device.
Used by:
- Device CA assignment billing:
apps/bbserver/include/http_handlers/devices_handler.hpp
ip.issuance
Meaning:
- Reserved for IP issuance pricing (currently present in config).
Used by:
- Config only (as of current repo state):
apps/bbserver/config_dir/pricing_plans.json
Legacy / test-only
cert.issueappears in some tests as a historical item code.- It should not be relied on for production configs unless the production billing code uses it.
Example config
A minimal plan:
{
"market_id": "US",
"item_code": "certificate.issue",
"name": "us-cert-issue",
"unit_price_minor": 100,
"currency": "USD",
"priority": 0,
"is_active": true,
"tiers": [
{"min_devices": 1, "max_devices": 5, "multiplier_bps": 10000, "label": "base"},
{"min_devices": 6, "max_devices": 15, "multiplier_bps": 20000, "label": "6-15"},
{"min_devices": 16, "multiplier_bps": 40000, "label": "15+"}
]
}
Troubleshooting
“No active price plan for device attachment”
This error comes from device attach billing when no plan is found for (market_id, item_code).
- Code path:
libs/bbdb/include/billing/device_attach_billing_service.cpp - Confirm the user has a
market_id(preferred market) and the plan exists for that market. - Ensure the plan is active and valid for the current time window.
Reloading plans after editing config
After updating the config JSON, invoke the reload endpoint:
POST /apiv1/admin/pricing/plans/reload
If you changed markets:
POST /apiv1/admin/pricing/markets/reload