feat(commercial): register workspace product and pricing
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d0c1d296b2
commit
0135c2137e
@ -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": "وبسایت"},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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"},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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__")
|
||||||
|
|||||||
324
backend/core-service/scripts/data/business-bundles.catalog.yaml
Normal file
324
backend/core-service/scripts/data/business-bundles.catalog.yaml
Normal 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
@ -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())
|
||||||
@ -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 }
|
||||||
|
|||||||
@ -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 },
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user