Files
orion/app/modules/contracts/features.py
Samir Boulahtit f20266167d
Some checks failed
CI / ruff (push) Failing after 7s
CI / pytest (push) Failing after 1s
CI / architecture (push) Failing after 9s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 8s
CI / docs (push) Has been skipped
fix(lint): auto-fix ruff violations and tune lint rules
- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.)
- Added ignore rules for patterns intentional in this codebase:
  E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from),
  SIM108/SIM105/SIM117 (readability preferences)
- Added per-file ignores for tests and scripts
- Excluded broken scripts/rename_terminology.py (has curly quotes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:10:42 +01:00

264 lines
9.1 KiB
Python

# app/modules/contracts/features.py
"""
Feature provider protocol for cross-module feature declaration and usage tracking.
This module defines the protocol that modules implement to declare their billable
features. The billing module's FeatureAggregator discovers and aggregates all providers.
Benefits:
- Each module owns its feature declarations (no cross-module coupling)
- Billing is feature-agnostic (discovers features dynamically)
- Adding features only requires changes in the owning module
- Scope-aware usage tracking (per-store or per-merchant)
Usage:
# 1. Implement the protocol in your module
class CatalogFeatureProvider:
@property
def feature_category(self) -> str:
return "catalog"
def get_feature_declarations(self) -> list[FeatureDeclaration]:
return [
FeatureDeclaration(
code="products_limit",
name_key="catalog.features.products_limit.name",
description_key="catalog.features.products_limit.description",
category="catalog",
feature_type=FeatureType.QUANTITATIVE,
scope=FeatureScope.STORE,
default_limit=200,
unit_key="catalog.features.products_limit.unit",
)
]
def get_store_usage(self, db, store_id) -> list[FeatureUsage]:
count = db.query(Product).filter(Product.store_id == store_id).count()
return [FeatureUsage(feature_code="products_limit", current_count=count, label="Products")]
def get_merchant_usage(self, db, merchant_id, platform_id) -> list[FeatureUsage]:
count = db.query(Product).join(Store).filter(
Store.merchant_id == merchant_id
).count()
return [FeatureUsage(feature_code="products_limit", current_count=count, label="Products")]
# 2. Register in module definition
def _get_feature_provider():
from app.modules.catalog.services.catalog_features import catalog_feature_provider
return catalog_feature_provider
catalog_module = ModuleDefinition(
code="catalog",
feature_provider=_get_feature_provider,
# ...
)
# 3. Features appear automatically in billing when module is enabled
"""
import enum
from dataclasses import dataclass
from typing import TYPE_CHECKING, Protocol, runtime_checkable
if TYPE_CHECKING:
from sqlalchemy.orm import Session
class FeatureType(str, enum.Enum):
"""Type of feature for billing purposes."""
BINARY = "binary" # On/off feature (e.g., "analytics_dashboard")
QUANTITATIVE = "quantitative" # Countable resource with limits (e.g., "products_limit")
class FeatureScope(str, enum.Enum):
"""Scope at which quantitative limits apply."""
STORE = "store" # Limit applies per-store (e.g., 200 products per store)
MERCHANT = "merchant" # Limit applies across all merchant's stores (e.g., 3 team members total)
@dataclass
class FeatureDeclaration:
"""
Static declaration of a billable feature.
Returned by feature providers at startup. Contains no database state,
only metadata about what a feature is and how it should be billed.
Attributes:
code: Unique identifier (e.g., "products_limit", "analytics_dashboard")
name_key: i18n key for display name (e.g., "catalog.features.products_limit.name")
description_key: i18n key for description
category: Grouping category (matches module domain, e.g., "catalog")
feature_type: BINARY or QUANTITATIVE
scope: STORE or MERCHANT (for QUANTITATIVE; BINARY always merchant-level)
default_limit: Sensible default for QUANTITATIVE (None = unlimited)
unit_key: i18n key for unit label (e.g., "catalog.features.products_limit.unit")
is_per_period: Whether the limit resets each billing period (e.g., orders/month)
ui_icon: Lucide icon name for display
display_order: Sort order within category (lower = higher priority)
Example:
FeatureDeclaration(
code="products_limit",
name_key="catalog.features.products_limit.name",
description_key="catalog.features.products_limit.description",
category="catalog",
feature_type=FeatureType.QUANTITATIVE,
scope=FeatureScope.STORE,
default_limit=200,
unit_key="catalog.features.products_limit.unit",
)
"""
code: str
name_key: str
description_key: str
category: str
feature_type: FeatureType
scope: FeatureScope = FeatureScope.MERCHANT
default_limit: int | None = None
unit_key: str | None = None
is_per_period: bool = False
ui_icon: str | None = None
display_order: int = 0
@dataclass
class FeatureUsage:
"""
Current usage count for a quantitative feature.
Returned by feature providers when asked about current resource consumption.
Attributes:
feature_code: Which feature this usage relates to
current_count: Current count of the resource
label: Human-readable label for display
"""
feature_code: str
current_count: int
label: str
@runtime_checkable
class FeatureProviderProtocol(Protocol):
"""
Protocol for modules that declare billable features.
Each module implements this to declare what features it provides
and how to measure current usage. The billing module's FeatureAggregator
discovers all providers and uses them for feature gating.
Implementation Notes:
- get_feature_declarations() must be static (no DB needed)
- Usage methods should use efficient aggregation queries
- Return empty list if no usage data available (don't raise)
Example Implementation:
class CatalogFeatureProvider:
@property
def feature_category(self) -> str:
return "catalog"
def get_feature_declarations(self) -> list[FeatureDeclaration]:
return [
FeatureDeclaration(
code="products_limit",
name_key="catalog.features.products_limit.name",
description_key="catalog.features.products_limit.description",
category="catalog",
feature_type=FeatureType.QUANTITATIVE,
scope=FeatureScope.STORE,
default_limit=200,
unit_key="catalog.features.products_limit.unit",
),
]
def get_store_usage(self, db, store_id) -> list[FeatureUsage]:
count = db.query(Product).filter(Product.store_id == store_id).count()
return [FeatureUsage(feature_code="products_limit", current_count=count, label="Products")]
def get_merchant_usage(self, db, merchant_id, platform_id) -> list[FeatureUsage]:
count = db.query(Product).join(Store).filter(Store.merchant_id == merchant_id).count()
return [FeatureUsage(feature_code="products_limit", current_count=count, label="Products")]
"""
@property
def feature_category(self) -> str:
"""
Category name for this provider's features.
Should match the module's domain (e.g., "catalog", "orders", "tenancy").
Returns:
Category string used for grouping features
"""
...
def get_feature_declarations(self) -> list[FeatureDeclaration]:
"""
Get static feature declarations for this module.
Called at startup to build the feature catalog. No database access needed.
These declarations describe what features exist and how they're billed.
Returns:
List of FeatureDeclaration objects for this module
"""
...
def get_store_usage(
self,
db: "Session",
store_id: int,
) -> list[FeatureUsage]:
"""
Get current usage counts for a specific store.
Called when checking quantitative limits scoped to STORE.
Should only count resources belonging to the specified store.
Args:
db: Database session for queries
store_id: ID of the store to get usage for
Returns:
List of FeatureUsage objects with current counts
"""
...
def get_merchant_usage(
self,
db: "Session",
merchant_id: int,
platform_id: int,
) -> list[FeatureUsage]:
"""
Get current usage counts aggregated across all merchant's stores.
Called when checking quantitative limits scoped to MERCHANT.
Should aggregate usage across all stores owned by the merchant
on the specified platform.
Args:
db: Database session for queries
merchant_id: ID of the merchant
platform_id: ID of the platform
Returns:
List of FeatureUsage objects with aggregated counts
"""
...
__all__ = [
"FeatureType",
"FeatureScope",
"FeatureDeclaration",
"FeatureUsage",
"FeatureProviderProtocol",
]