feat(cloud): show purchased plan revision alongside the latest revision

- Expose the workspace's purchased plan revision from the entitlement
  snapshot.
- Stack the purchased revision card in front of the latest revision in
  the plan comparison dialog; both cards share the same layout, and the
  purchased card's copy and prices come from the Space revision endpoint
  while benefits stay on the entitlement snapshot.
- Add a cancel-first tooltip for subscribing to the newer revision.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
RockChinQ
2026-10-17 00:25:41 +08:00
co-authored by Claude Opus 5.5
parent ececcb8749
commit 16cfb6f8bd
22 changed files with 1054 additions and 144 deletions
@@ -160,6 +160,7 @@ class WorkspacesRouterGroup(group.RouterGroup):
workspace = await self.ap.workspace_service.get_workspace(request_context.workspace_uuid)
plan_name: str | None = None
limitation = None
current_plan = None
resolver = getattr(self.ap, 'entitlement_resolver', None)
if workspace.source == WorkspaceSource.CLOUD_PROJECTION.value and resolver is not None:
entitlement = await resolver.resolve(
@@ -167,6 +168,15 @@ class WorkspacesRouterGroup(group.RouterGroup):
minimum_revision=request_context.entitlement_revision,
)
plan_name = entitlement.plan_name
if entitlement.plan_revision is not None and entitlement.monthly_bonus_credits is not None:
current_plan = {
'name': entitlement.plan_name,
'subscription_started_at': entitlement.subscription_started_at,
'revision': entitlement.plan_revision,
'monthly_bonus_credits': entitlement.monthly_bonus_credits,
'limits': entitlement.limits,
'entitlement_features': entitlement.features,
}
from ...service.workspace_limits import LIMIT_KEYS
limitation = {key: entitlement.limit(value) for key, value in LIMIT_KEYS.items()}
@@ -188,6 +198,7 @@ class WorkspacesRouterGroup(group.RouterGroup):
'permissions': sorted(request_context.workspace.permissions),
'placement_generation': request_context.placement_generation,
'plan_name': plan_name,
'current_plan': current_plan,
'limitation': limitation,
}
)
@@ -205,6 +216,7 @@ class WorkspacesRouterGroup(group.RouterGroup):
'permissions': sorted(request_context.workspace.permissions),
'placement_generation': request_context.placement_generation,
'plan_name': plan_name,
'current_plan': current_plan,
'limitation': limitation,
}
)
+4
View File
@@ -55,6 +55,10 @@ class EntitlementSnapshot(pydantic.BaseModel):
# must continue to use generic ``features`` and ``limits`` exclusively.
plan_name: str | None = pydantic.Field(default=None, min_length=1, max_length=128)
subscription_started_at: int | None = pydantic.Field(default=None, ge=0)
plan_revision: int | None = pydantic.Field(default=None, ge=1)
monthly_bonus_credits: int | None = pydantic.Field(default=None, ge=0)
@pydantic.field_validator('features')
@classmethod
def _validate_feature_names(cls, value: dict[str, bool]) -> dict[str, bool]: