feat(commercial): register workspace product and pricing

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Mortezakoohjani 2026-09-12 16:30:48 +03:30
parent d0c1d296b2
commit 0135c2137e
9 changed files with 1167 additions and 164 deletions

View File

@ -228,7 +228,8 @@ class AssessmentEngine:
"options": [ "options": [
{"value": "online_sales", "label": "فروش آنلاین"}, {"value": "online_sales", "label": "فروش آنلاین"},
{"value": "delivery", "label": "پیک"}, {"value": "delivery", "label": "پیک"},
{"value": "crm", "label": "CRM"}, {"value": "crm", "label": "مدیریت ارتباط با مشتری"},
{"value": "workspace", "label": "فضای کاری"},
{"value": "loyalty", "label": "وفاداری"}, {"value": "loyalty", "label": "وفاداری"},
{"value": "website", "label": "وب‌سایت"}, {"value": "website", "label": "وب‌سایت"},
], ],

View File

@ -301,10 +301,10 @@ class LicenseResolver:
state = (await self.db.execute(stmt)).scalar_one_or_none() state = (await self.db.execute(stmt)).scalar_one_or_none()
if state is None: if state is None:
steps = [ steps = [
{"step_key": "workspace", "label": "Workspace", "status": "done"}, {"step_key": "workspace", "label": "فضای کاری", "status": "done"},
{"step_key": "bundle", "label": "Bundle", "status": "done"}, {"step_key": "bundle", "label": "بسته خدمات", "status": "done"},
{"step_key": "trial", "label": "Trial", "status": "done"}, {"step_key": "trial", "label": "دوره آزمایشی", "status": "done"},
{"step_key": "dashboard", "label": "Dashboard", "status": "pending", "href": "/dashboard"}, {"step_key": "dashboard", "label": "داشبورد", "status": "pending", "href": "/dashboard"},
] ]
state = CommercialActivationState( state = CommercialActivationState(
tenant_id=tenant_id, tenant_id=tenant_id,
@ -312,9 +312,9 @@ class LicenseResolver:
completion_percent=75, completion_percent=75,
steps=steps, steps=steps,
checklist=[ checklist=[
{"id": "workspace", "label": "Workspace", "done": True}, {"id": "workspace", "label": "فضای کاری", "done": True},
{"id": "bundle", "label": "انتخاب باندل", "done": True}, {"id": "bundle", "label": "انتخاب بسته", "done": True},
{"id": "billing", "label": "Billing", "done": False, "href": "/billing"}, {"id": "billing", "label": "صورتحساب", "done": False, "href": "/billing"},
{"id": "domain", "label": "دامنه", "done": False, "href": "/domains"}, {"id": "domain", "label": "دامنه", "done": False, "href": "/domains"},
], ],
) )

View File

@ -341,3 +341,11 @@ async def test_policy_evaluate(client):
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json()["custom_domain_allowed"] is True assert resp.json()["custom_domain_allowed"] is True
def test_workspace_commercial_registry_catalog():
from pathlib import Path
import runpy
script = Path(__file__).resolve().parents[2] / "scripts" / "validate_commercial_workspace_registry.py"
runpy.run_path(str(script), run_name="__validate_workspace_registry__")

View File

@ -0,0 +1,324 @@
# Canonical machine-readable Business Bundle catalog.
# Source of truth with docs/reference/business-bundles.md
# Frontend MUST be generated from this file in a future phase — do not treat React as authoritative.
schema_version: 1
contract_version: business-bundle.v1
updated: "2026-07-28"
owner: core-platform
bundles:
- bundle_code: bundle.restaurant.starter
display_name: Restaurant Starter
status: active
version_code: "2026.07.1"
business_types: [restaurant]
services:
- { service_identifier: hospitality, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: experience, required: true }
- { service_identifier: delivery, required: false }
- { service_identifier: loyalty, required: false }
- { service_identifier: communication, required: false }
- { service_identifier: accounting, required: false }
pricing_refs:
- { pricing_item_code: price.restaurant.starter.monthly, role: primary }
- { pricing_item_code: price.restaurant.starter.yearly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.restaurant.growth
display_name: Restaurant Growth
status: active
version_code: "2026.07.1"
business_types: [restaurant]
services:
- { service_identifier: hospitality, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: experience, required: true }
- { service_identifier: delivery, required: true }
- { service_identifier: loyalty, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: accounting, required: false }
- { service_identifier: crm, required: false }
pricing_refs:
- { pricing_item_code: price.restaurant.growth.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.cafe.starter
display_name: Cafe Starter
status: active
version_code: "2026.07.1"
business_types: [cafe]
services:
- { service_identifier: hospitality, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: experience, required: true }
- { service_identifier: loyalty, required: false }
- { service_identifier: communication, required: false }
pricing_refs:
- { pricing_item_code: price.cafe.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.clinic.starter
display_name: Clinic Starter
status: active
version_code: "2026.07.1"
business_types: [clinic]
services:
- { service_identifier: experience, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: accounting, required: true }
- { service_identifier: payment, required: false }
- { service_identifier: crm, required: false }
pricing_refs:
- { pricing_item_code: price.clinic.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.beauty.starter
display_name: Beauty Salon Starter
status: active
version_code: "2026.07.1"
business_types: [beauty_salon]
services:
- { service_identifier: experience, required: true }
- { service_identifier: crm, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: loyalty, required: false }
pricing_refs:
- { pricing_item_code: price.beauty.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.sports.starter
display_name: Sports Center Starter
status: active
version_code: "2026.07.1"
business_types: [sports_center]
services:
- { service_identifier: sports-center, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: loyalty, required: true }
- { service_identifier: communication, required: false }
- { service_identifier: accounting, required: false }
pricing_refs:
- { pricing_item_code: price.sports.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.retail.starter
display_name: Retail Starter
status: active
version_code: "2026.07.1"
business_types: [retail_shop]
services:
- { service_identifier: experience, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: delivery, required: false }
- { service_identifier: loyalty, required: false }
- { service_identifier: accounting, required: false }
pricing_refs:
- { pricing_item_code: price.retail.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.marketplace.seller
display_name: Marketplace Seller
status: draft
version_code: "2026.07.1"
business_types: [marketplace_seller]
services:
- { service_identifier: payment, required: true }
- { service_identifier: accounting, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: marketplace, required: false }
pricing_refs:
- { pricing_item_code: price.marketplace.seller.monthly, role: primary }
visibility: { public_catalog: false, admin_only: true, hidden: false }
lifecycle: { sellable: false, renewable: false, grandfathered: false }
notes: Marketplace product not implemented in commercial-foundation wave.
- bundle_code: bundle.education.starter
display_name: Education Starter
status: active
version_code: "2026.07.1"
business_types: [educational_center]
services:
- { service_identifier: experience, required: true }
- { service_identifier: crm, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: payment, required: false }
- { service_identifier: accounting, required: false }
pricing_refs:
- { pricing_item_code: price.education.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.corporate.crm
display_name: Corporate CRM
status: active
version_code: "2026.07.1"
business_types: [corporate_company]
services:
- { service_identifier: crm, required: true }
- { service_identifier: accounting, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: identity-access, required: false }
- { service_identifier: experience, required: false }
pricing_refs:
- { pricing_item_code: price.corporate.crm.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.professional.starter
display_name: Professional Services
status: active
version_code: "2026.07.1"
business_types: [professional_services]
services:
- { service_identifier: crm, required: true }
- { service_identifier: accounting, required: true }
- { service_identifier: experience, required: true }
- { service_identifier: communication, required: false }
- { service_identifier: payment, required: false }
pricing_refs:
- { pricing_item_code: price.professional.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.freelancer.starter
display_name: Freelancer
status: active
version_code: "2026.07.1"
business_types: [freelancer]
services:
- { service_identifier: experience, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: accounting, required: false }
- { service_identifier: communication, required: false }
pricing_refs:
- { pricing_item_code: price.freelancer.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.manufacturer.starter
display_name: Manufacturer
status: active
version_code: "2026.07.1"
business_types: [manufacturer]
services:
- { service_identifier: crm, required: true }
- { service_identifier: accounting, required: true }
- { service_identifier: communication, required: false }
pricing_refs:
- { pricing_item_code: price.manufacturer.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.wholesale.starter
display_name: Wholesale
status: active
version_code: "2026.07.1"
business_types: [wholesale]
services:
- { service_identifier: crm, required: true }
- { service_identifier: accounting, required: true }
- { service_identifier: payment, required: true }
- { service_identifier: delivery, required: false }
pricing_refs:
- { pricing_item_code: price.wholesale.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.real_estate.starter
display_name: Real Estate
status: active
version_code: "2026.07.1"
business_types: [real_estate]
services:
- { service_identifier: experience, required: true }
- { service_identifier: crm, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: payment, required: false }
pricing_refs:
- { pricing_item_code: price.real_estate.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.government.starter
display_name: Government
status: active
version_code: "2026.07.1"
business_types: [government]
services:
- { service_identifier: experience, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: accounting, required: true }
pricing_refs:
- { pricing_item_code: price.enterprise.custom, role: enterprise_quote }
visibility: { public_catalog: false, admin_only: true, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.ngo.starter
display_name: NGO
status: active
version_code: "2026.07.1"
business_types: [ngo]
services:
- { service_identifier: experience, required: true }
- { service_identifier: crm, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: accounting, required: false }
pricing_refs:
- { pricing_item_code: price.ngo.starter.monthly, role: primary }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }
- bundle_code: bundle.platform.trial
display_name: Platform Trial
status: active
version_code: "2026.07.1"
business_types: [custom]
services:
- { service_identifier: experience, required: true }
- { service_identifier: payment, required: false }
- { service_identifier: communication, required: false }
pricing_refs:
- { pricing_item_code: price.platform.trial.14d, role: trial }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: false, grandfathered: false }
- bundle_code: bundle.workspace.starter
display_name: فضای کاری — نسخه شروع
status: active
version_code: "2026.07.1"
business_types:
- advertising_agency
- content_team
- software_company
- marketing_company
- team_freelancer
services:
- { service_identifier: workspace, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: crm, required: true }
- { service_identifier: accounting, required: true }
capability_codes:
- workspace.projects
- workspace.tasks
- workspace.kanban
- workspace.calendar
- workspace.content
- workspace.approvals
- workspace.comments
- workspace.mentions
- workspace.analytics
pricing_refs:
- { pricing_item_code: price.workspace.starter.monthly, role: primary }
- { pricing_item_code: price.workspace.business.monthly, role: upgrade }
- { pricing_item_code: price.workspace.enterprise, role: enterprise }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Validate Workspace Commercial Runtime registration (catalog + seed, no extra services)."""
from __future__ import annotations
import ast
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
CATALOG = ROOT / "scripts" / "data" / "business-bundles.catalog.yaml"
SEED = ROOT / "scripts" / "seed_commercial_runtime.py"
REQUIRED_PRODUCTS = {"workspace", "communication", "crm", "accounting"}
REQUIRED_PRICES = {
"price.workspace.starter.monthly",
"price.workspace.business.monthly",
"price.workspace.enterprise",
}
REQUIRED_CAPS = {
"workspace.projects",
"workspace.tasks",
"workspace.kanban",
"workspace.calendar",
"workspace.content",
"workspace.approvals",
"workspace.comments",
"workspace.mentions",
"workspace.analytics",
}
REQUIRED_TYPES = {
"advertising_agency",
"content_team",
"software_company",
"marketing_company",
"team_freelancer",
}
REQUIRED_RULES = {
"r.workspace.advertising_agency",
"r.workspace.content_team",
"r.workspace.software_company",
"r.workspace.marketing_company",
"r.workspace.team_freelancer",
}
def fail(msg: str) -> None:
print(f"FAIL {msg}")
raise SystemExit(1)
def main() -> int:
catalog = yaml.safe_load(CATALOG.read_text(encoding="utf-8")) or {}
bundles = {b["bundle_code"]: b for b in catalog.get("bundles") or []}
bundle = bundles.get("bundle.workspace.starter")
if not bundle:
fail("bundle.workspace.starter missing from catalog")
print("Registry Validation: bundle.workspace.starter PASS")
services = {s["service_identifier"] for s in bundle.get("services") or []}
if not REQUIRED_PRODUCTS.issubset(services):
fail(f"bundle products {services} missing {REQUIRED_PRODUCTS - services}")
prices = {
p.get("pricing_item_code")
for p in bundle.get("pricing_refs") or []
if isinstance(p, dict)
}
if prices != REQUIRED_PRICES:
fail(f"pricing refs {prices} != {REQUIRED_PRICES}")
print("Pricing Validation: 3 workspace prices PASS")
caps = set(bundle.get("capability_codes") or [])
if not REQUIRED_CAPS.issubset(caps):
fail(f"capabilities missing {REQUIRED_CAPS - caps}")
print("Cross Reference: workspace capabilities PASS")
seed = SEED.read_text(encoding="utf-8")
if '"workspace": ("فضای کاری"' not in seed and '"workspace": ("فضای کاری", "/workspace"' not in seed:
if '"workspace":' not in seed or "فضای کاری" not in seed:
fail("workspace product not in seed")
print("Registry Validation: workspace product PASS")
for item in REQUIRED_TYPES | REQUIRED_RULES | REQUIRED_PRICES:
if item not in seed:
fail(f"{item} missing from seed")
print("Recommendation Validation: 5 workspace rules + 5 business types PASS")
tree = ast.parse(seed)
print("Validation PASS")
print(f"ast_ok={bool(tree)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -290,3 +290,35 @@ bundles:
- { pricing_item_code: price.platform.trial.14d, role: trial } - { pricing_item_code: price.platform.trial.14d, role: trial }
visibility: { public_catalog: true, admin_only: false, hidden: false } visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: false, grandfathered: false } lifecycle: { sellable: true, renewable: false, grandfathered: false }
- bundle_code: bundle.workspace.starter
display_name: فضای کاری — نسخه شروع
status: active
version_code: "2026.07.1"
business_types:
- advertising_agency
- content_team
- software_company
- marketing_company
- team_freelancer
services:
- { service_identifier: workspace, required: true }
- { service_identifier: communication, required: true }
- { service_identifier: crm, required: true }
- { service_identifier: accounting, required: true }
capability_codes:
- workspace.projects
- workspace.tasks
- workspace.kanban
- workspace.calendar
- workspace.content
- workspace.approvals
- workspace.comments
- workspace.mentions
- workspace.analytics
pricing_refs:
- { pricing_item_code: price.workspace.starter.monthly, role: primary }
- { pricing_item_code: price.workspace.business.monthly, role: upgrade }
- { pricing_item_code: price.workspace.enterprise, role: enterprise }
visibility: { public_catalog: true, admin_only: false, hidden: false }
lifecycle: { sellable: true, renewable: true, grandfathered: false }

View File

@ -187,8 +187,8 @@ export function buildSetupChecklist(
const workspaceActions = const workspaceActions =
products.length === 0 products.length === 0
? [ ? [
{ id: "workspace", label: "Workspace", href: "/dashboard", required: true }, { id: "workspace", label: "فضای کاری", href: "/workspace", required: true },
{ id: "billing", label: "Billing", href: "/billing", required: false }, { id: "billing", label: "صورتحساب", href: "/billing", required: false },
] ]
: []; : [];

View File

@ -445,6 +445,17 @@ const TOKEN_LABELS: Readonly<Record<string, string>> = {
website: "وب‌سایت", website: "وب‌سایت",
experience: "وب‌سایت و تجربه دیجیتال", experience: "وب‌سایت و تجربه دیجیتال",
crm: "مدیریت ارتباط با مشتری", crm: "مدیریت ارتباط با مشتری",
workspace: "فضای کاری",
projects: "پروژه‌ها",
tasks: "وظایف",
kanban: "کانبان",
calendar: "تقویم",
content: "محتوا",
approvals: "تأییدیه‌ها",
comments: "نظرات",
mentions: "منشن",
analytics: "گزارش‌ها",
business: "کسب‌وکار",
inventory: "انبارداری", inventory: "انبارداری",
pos: "صندوق فروش", pos: "صندوق فروش",
communication: "ارتباط با مشتریان", communication: "ارتباط با مشتریان",
@ -810,7 +821,7 @@ export function looksTechnical(value: string): boolean {
export function formatMoney(amount?: number | null, currency?: string | null): string { export function formatMoney(amount?: number | null, currency?: string | null): string {
if (amount == null) return "استعلام قیمت"; if (amount == null) return "استعلام قیمت";
const unit = !currency || /irr|rial/i.test(currency) ? "ریال" : enumLabel(currency); const unit = !currency || /irr|rial|toman/i.test(currency) ? "تومان" : enumLabel(currency);
return `${new Intl.NumberFormat("fa-IR").format(amount)} ${unit}`; return `${new Intl.NumberFormat("fa-IR").format(amount)} ${unit}`;
} }