Coverage for polar/product/endpoints.py: 100%
47 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
1from typing import Annotated
3from fastapi import Depends, Query
5from polar.benefit.schemas import BenefitID
6from polar.exceptions import NotPermitted, ResourceNotFound
7from polar.kit.metadata import MetadataQuery, get_metadata_query_openapi_schema
8from polar.kit.pagination import ListResource, PaginationParamsQuery
9from polar.kit.schemas import MultipleQueryFilter
10from polar.kit.sorting import Sorting, SortingGetter
11from polar.models import Product
12from polar.openapi import APITag
13from polar.organization.schemas import OrganizationID
14from polar.postgres import (
15 AsyncReadSession,
16 AsyncSession,
17 get_db_read_session,
18 get_db_session,
19)
20from polar.routing import APIRouter
22from . import auth
23from .schemas import Product as ProductSchema
24from .schemas import ProductBenefitsUpdate, ProductCreate, ProductID, ProductUpdate
25from .service import product as product_service
26from .sorting import ProductSortProperty
28router = APIRouter(
29 prefix="/products",
30 tags=["products", APITag.public, APITag.mcp],
31)
33ProductNotFound = {
34 "description": "Product not found.",
35 "model": ResourceNotFound.schema(),
36}
39ListSorting = Annotated[
40 list[Sorting[ProductSortProperty]],
41 Depends(SortingGetter(ProductSortProperty, ["-created_at"])),
42]
45@router.get(
46 "/",
47 summary="List Products",
48 response_model=ListResource[ProductSchema],
49 openapi_extra={"parameters": [get_metadata_query_openapi_schema()]},
50)
51async def list(
52 pagination: PaginationParamsQuery,
53 sorting: ListSorting,
54 auth_subject: auth.CreatorProductsRead,
55 metadata: MetadataQuery,
56 id: MultipleQueryFilter[ProductID] | None = Query(
57 None, title="ProductID Filter", description="Filter by product ID."
58 ),
59 organization_id: MultipleQueryFilter[OrganizationID] | None = Query(
60 None, title="OrganizationID Filter", description="Filter by organization ID."
61 ),
62 query: str | None = Query(None, description="Filter by product name."),
63 is_archived: bool | None = Query(None, description="Filter on archived products."),
64 is_recurring: bool | None = Query(
65 None,
66 description=(
67 "Filter on recurring products. "
68 "If `true`, only subscriptions tiers are returned. "
69 "If `false`, only one-time purchase products are returned. "
70 ),
71 ),
72 benefit_id: MultipleQueryFilter[BenefitID] | None = Query(
73 None,
74 title="BenefitID Filter",
75 description="Filter products granting specific benefit.",
76 ),
77 session: AsyncReadSession = Depends(get_db_read_session),
78) -> ListResource[ProductSchema]:
79 """List products."""
80 results, count = await product_service.list(
81 session,
82 auth_subject,
83 id=id,
84 organization_id=organization_id,
85 query=query,
86 is_archived=is_archived,
87 is_recurring=is_recurring,
88 benefit_id=benefit_id,
89 metadata=metadata,
90 pagination=pagination,
91 sorting=sorting,
92 )
94 return ListResource.from_paginated_results(
95 [ProductSchema.model_validate(result) for result in results],
96 count,
97 pagination,
98 )
101@router.get(
102 "/{id}",
103 summary="Get Product",
104 response_model=ProductSchema,
105 responses={404: ProductNotFound},
106)
107async def get(
108 id: ProductID,
109 auth_subject: auth.CreatorProductsRead,
110 session: AsyncReadSession = Depends(get_db_read_session),
111) -> Product:
112 """Get a product by ID."""
113 product = await product_service.get(session, auth_subject, id)
115 if product is None:
116 raise ResourceNotFound()
118 return product
121@router.post(
122 "/",
123 response_model=ProductSchema,
124 status_code=201,
125 summary="Create Product",
126 responses={201: {"description": "Product created."}},
127)
128async def create(
129 product_create: ProductCreate,
130 auth_subject: auth.CreatorProductsWrite,
131 session: AsyncSession = Depends(get_db_session),
132) -> Product:
133 """Create a product."""
134 return await product_service.create(session, product_create, auth_subject)
137@router.patch(
138 "/{id}",
139 response_model=ProductSchema,
140 summary="Update Product",
141 responses={
142 200: {"description": "Product updated."},
143 403: {
144 "description": "You don't have the permission to update this product.",
145 "model": NotPermitted.schema(),
146 },
147 404: ProductNotFound,
148 },
149)
150async def update(
151 id: ProductID,
152 product_update: ProductUpdate,
153 auth_subject: auth.CreatorProductsWrite,
154 session: AsyncSession = Depends(get_db_session),
155) -> Product:
156 """Update a product."""
157 product = await product_service.get(session, auth_subject, id)
159 if product is None:
160 raise ResourceNotFound()
162 return await product_service.update(session, product, product_update, auth_subject)
165@router.post(
166 "/{id}/benefits",
167 response_model=ProductSchema,
168 summary="Update Product Benefits",
169 responses={
170 200: {"description": "Product benefits updated."},
171 403: {
172 "description": "You don't have the permission to update this product.",
173 "model": NotPermitted.schema(),
174 },
175 404: ProductNotFound,
176 },
177)
178async def update_benefits(
179 id: ProductID,
180 benefits_update: ProductBenefitsUpdate,
181 auth_subject: auth.CreatorProductsWrite,
182 session: AsyncSession = Depends(get_db_session),
183) -> Product:
184 """Update benefits granted by a product."""
185 product = await product_service.get(session, auth_subject, id)
187 if product is None:
188 raise ResourceNotFound()
190 product, _, _ = await product_service.update_benefits(
191 session, product, benefits_update.benefits, auth_subject
192 )
193 return product