"""Plugins routes: browse and install plugins from the marketplace. Backed by ``PluginRegistry`` and ``RevenueShare`` service classes introduced in Step 10. Step 12 will swap those services' in-memory stores for PostgreSQL persistence. """ from __future__ import annotations from typing import Any, Literal from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from app.api.deps import get_current_user from app.marketplace.plugin_registry import registry from app.marketplace.revenue_share import revenue_share from app.schemas import PluginInstallRequest, PluginListResponse, PluginManifest, UserProfile router = APIRouter(prefix="/plugins", tags=["plugins"]) # ── Tier gate ───────────────────────────────────────────────────────── def _require_plugin_tier(user: UserProfile) -> None: """Raise HTTP 403 for users below Power tier.""" if user.tier not in ("power", "team"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Plugin marketplace requires Power tier or above", ) # ── Local detail schema ──────────────────────────────────────────────── class _PluginDetail(BaseModel): plugin: PluginManifest install_count: int ratings: list[Any] # Step 12 populates from plugin_reviews table # ── Routes ──────────────────────────────────────────────────────────── @router.get("", response_model=PluginListResponse) async def list_plugins( category: str | None = Query(default=None), q: str | None = Query(default=None), page: int = Query(default=1, ge=1), sort: Literal["rating", "installs", "newest"] = Query(default="newest"), current_user: UserProfile = Depends(get_current_user), ) -> PluginListResponse: """Browse the plugin marketplace. Requires Power tier or above.""" _require_plugin_tier(current_user) return await registry.list_plugins(category=category, query=q, page=page, sort=sort) @router.get("/{plugin_id}", response_model=_PluginDetail) async def get_plugin( plugin_id: str, current_user: UserProfile = Depends(get_current_user), ) -> _PluginDetail: """Get full plugin details including install count. Requires Power tier or above.""" _require_plugin_tier(current_user) entry = await registry.get_plugin(plugin_id) if entry is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plugin not found") return _PluginDetail( plugin=entry["manifest"], install_count=entry["install_count"], ratings=[], # Step 12 populates from plugin_reviews table ) @router.post("/{plugin_id}/install", response_model=dict) async def install_plugin( plugin_id: str, body: PluginInstallRequest, # noqa: ARG001 — reserved for future fields current_user: UserProfile = Depends(get_current_user), ) -> dict[str, Any]: """Install a plugin. Triggers Stripe Connect revenue split for paid plugins. Requires Power tier or above. """ _require_plugin_tier(current_user) entry = await registry.get_plugin(plugin_id) if entry is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plugin not found") await revenue_share.record_install( plugin_id=plugin_id, user_id=current_user.id, amount_cents=entry["manifest"].price_cents, ) download_url = f"https://cdn.adiuva.app/plugins/{plugin_id}/package.zip" return {"ok": True, "download_url": download_url} @router.delete("/{plugin_id}/install", response_model=dict) async def uninstall_plugin( plugin_id: str, current_user: UserProfile = Depends(get_current_user), ) -> dict[str, bool]: """Unregister a plugin installation.""" await registry.record_uninstall(plugin_id) return {"ok": True}