"""Plugins routes: browse and install plugins from the marketplace. Backed by ``PluginRegistry`` and ``RevenueShare`` service classes that persist data in the PostgreSQL ``plugins`` and ``revenue_events`` tables. """ from __future__ import annotations from typing import Any, Literal from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user from app.db import get_session from app.marketplace.plugin_registry import registry from app.marketplace.revenue_share import revenue_share from app.models import PluginInstallation, PluginReview as PluginReviewModel 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] # ── 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), db: AsyncSession = Depends(get_session), ) -> PluginListResponse: """Browse the plugin marketplace. Requires Power tier or above.""" _require_plugin_tier(current_user) return await registry.list_plugins(db, 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), db: AsyncSession = Depends(get_session), ) -> _PluginDetail: """Get full plugin details including install count. Requires Power tier or above.""" _require_plugin_tier(current_user) entry = await registry.get_plugin(db, plugin_id) if entry is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plugin not found") # Fetch review ratings for this plugin review_result = await db.execute( select(PluginReviewModel).where(PluginReviewModel.plugin_id == plugin_id) ) reviews = review_result.scalars().all() ratings = [ { "reviewer_id": r.reviewer_id, "decision": r.decision, "notes": r.notes, "reviewed_at": int(r.reviewed_at.timestamp() * 1000) if r.reviewed_at else None, } for r in reviews ] return _PluginDetail( plugin=entry["manifest"], install_count=entry["install_count"], ratings=ratings, ) @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), db: AsyncSession = Depends(get_session), ) -> 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(db, plugin_id) if entry is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plugin not found") # Record the installation in plugin_installations installation = PluginInstallation( plugin_id=plugin_id, user_id=current_user.id, ) db.add(installation) await db.flush() await revenue_share.record_install( db, 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), db: AsyncSession = Depends(get_session), ) -> dict[str, bool]: """Unregister a plugin installation.""" result = await db.execute( select(PluginInstallation).where( PluginInstallation.plugin_id == plugin_id, PluginInstallation.user_id == current_user.id, ) ) installation = result.scalar_one_or_none() if installation is not None: await db.delete(installation) await db.commit() await registry.record_uninstall(db, plugin_id) return {"ok": True}