Data in addins
Prices are not columns on plans. Each amount is a row in plan_prices linking planId + priceSchemeId.
addins.plan_prices, price_schemes, currencies
How ST-admin edits regional subscription prices in the addins database — same rows checkout uses via Pricing.php.
plans describes capability limits and whether the plan is subscription or event (isEvent). Prices live elsewhere.
| Table | Role |
|---|---|
| price_schemes | Region label + currency (e.g. Europe/EUR, Global/USD). isDefault is a fallback. |
| currencies | ISO code, symbol, display pattern for checkout formatting. |
| countries.priceSchemeId | Maps a billing country to which scheme (and thus which column) applies at checkout. |
| plan_prices | One row per (plan, scheme). price and priceMonth are integer cents. |
The matrix shows Y and M for subscription plans, matching Pricing::getPricing():
price) — per month when the customer pays yearly (annual billing uses price × 12 as billed yearly total in PHP). priceMonth) — per month when the customer chooses monthly billing. price (one-time). The UI hides M; the API rejects priceMonth on event plans. ST-admin displays and edits major units (e.g. 99.00); helpers in types/planPricing.ts convert to/from cents on save.
Prices are not columns on plans. Each amount is a row in plan_prices linking planId + priceSchemeId.
addins.plan_prices, price_schemes, currencies
PHP Pricing picks a price scheme from the customer country, then loads the matching plan_prices row.
api/common/components/pricing/Pricing.php
FastAPI reads/writes addins directly. GET returns a full matrix; PUT upserts one or more cells.
st-admin/backend/app/services/plan_prices.py
Plans as rows, price schemes as columns. Edit unlocks a row; blur on a cell saves that cell.
st-admin/frontend/components/plans-pricing/PlanPricingMatrix.vue
Pricing reuses the same Edit / Done editing pattern as the Features tab:
acknowledgeLiveImpact. ConfirmPlanEditModal with subject="pricing"); user must type the plan name. Frontend sends acknowledgeLiveImpact: true; backend returns 403 otherwise. useState('st-admin-unlocked-pricing-plan-ids')). upsertCell — optimistic local patch, rollback on error. plan_prices row for that scheme. Catalog pills (Active / Legacy / Draft / All) share useState('st-admin-plan-catalog') with the Features tab so filters stay in sync when switching tabs.
Both routes require bearer token + superadmin role (see get_superadmin_user).
GET /api/plan-prices
Authorization: Bearer <superadmin token>
{
"plans": [{ "id": 1, "name": "Pro", "isEvent": false, "isDraft": false, ... }],
"priceSchemes": [
{ "id": 1, "description": "Europe", "currencyIso": "EUR", "currencySymbol": "€", "isDefault": true }
],
"planPrices": {
"1": {
"1": { "id": 42, "price": 9900, "priceMonth": 12900 }
}
}
}planPrices keys are stringified plan and scheme IDs. Missing combinations are null (shown as — in the UI).
PUT /api/plans/1/prices
{
"cells": [
{ "priceSchemeId": 1, "price": 9900, "priceMonth": 12900 }
],
"acknowledgeLiveImpact": true
}async def set_plan_prices(db, plan_id, cells, *, acknowledge_live_impact=False):
# Draft: always allowed. Active/legacy: requires acknowledge_live_impact.
# Event plans: only price (one-time); priceMonth must be null.
# Both null → DELETE row for that scheme.POST /api/plans/{id}/duplicate clones plan metadata, all plan_features, and all plan_prices via copy_plan_prices() into the new draft. Edit prices on the draft before going live.
| Area | Path |
|---|---|
| Backend router | backend/app/routers/plan_prices.py |
| Backend service | backend/app/services/plan_prices.py |
| Pydantic schemas | backend/app/schemas/plan_prices.py |
| SQLAlchemy models | backend/app/models/entities.py (PlanPriceRow, PriceSchemeRow) |
| Duplicate plan | backend/app/services/plans.py → copy_plan_prices() |
| Fetch matrix | frontend/composables/usePlanPricing.ts |
| Save / unlock | frontend/composables/usePlanPricingAdmin.ts |
| Types & cents helpers | frontend/types/planPricing.ts |
| Matrix UI | frontend/components/plans-pricing/PlanPricingMatrix.vue |
| Page wiring | frontend/pages/plans-features.vue (Pricing tab) |
| Shared catalog filter | frontend/composables/usePlanCatalogFilter.ts |