Coverage for polar/payout/endpoints.py: 82%
58 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 fastapi import Depends, Query
2from fastapi.responses import StreamingResponse
3from pydantic import UUID4
5from polar.account.service import account as account_service
6from polar.exceptions import ResourceNotFound
7from polar.kit.db.postgres import AsyncSessionMaker
8from polar.kit.pagination import ListResource, PaginationParamsQuery
9from polar.kit.schemas import MultipleQueryFilter
10from polar.locker import Locker, get_locker
11from polar.models import Payout
12from polar.models.payout import PayoutStatus
13from polar.openapi import APITag
14from polar.postgres import AsyncSession, get_db_session, get_db_sessionmaker
15from polar.routing import APIRouter
17from . import auth as payouts_auth
18from . import sorting
19from .schemas import Payout as PayoutSchema
20from .schemas import PayoutCreate, PayoutEstimate, PayoutGenerateInvoice, PayoutInvoice
21from .service import InsufficientBalance, UnderReviewAccount
22from .service import payout as payout_service
24router = APIRouter(prefix="/payouts", tags=["payouts", APITag.private])
27@router.get("/", response_model=ListResource[PayoutSchema])
28async def list(
29 auth_subject: payouts_auth.PayoutsRead,
30 pagination: PaginationParamsQuery,
31 sorting: sorting.ListSorting,
32 account_id: MultipleQueryFilter[UUID4] | None = Query(
33 None, title="Account ID Filter", description="Filter by account ID."
34 ),
35 status: MultipleQueryFilter[PayoutStatus] | None = Query(
36 None, title="Status Filter", description="Filter by payout status."
37 ),
38 session: AsyncSession = Depends(get_db_session),
39) -> ListResource[PayoutSchema]:
40 """List payouts."""
41 results, count = await payout_service.list(
42 session,
43 auth_subject,
44 account_id=account_id,
45 status=status,
46 pagination=pagination,
47 sorting=sorting,
48 )
50 return ListResource.from_paginated_results(
51 [PayoutSchema.model_validate(result) for result in results], count, pagination
52 )
55@router.get(
56 "/estimate",
57 response_model=PayoutEstimate,
58 responses={
59 200: {
60 "description": "Payout estimate computed successfully.",
61 },
62 400: {
63 "description": "The balance is insufficient to create a payout.",
64 "model": InsufficientBalance.schema(),
65 },
66 403: {
67 "description": "The account is under review or not ready.",
68 "model": UnderReviewAccount.schema(),
69 },
70 404: {"description": "Account not found.", "model": ResourceNotFound.schema()},
71 },
72)
73async def get_estimate(
74 auth_subject: payouts_auth.PayoutsRead,
75 account_id: UUID4,
76 session: AsyncSession = Depends(get_db_session),
77) -> PayoutEstimate:
78 account = await account_service.get(session, auth_subject, account_id)
79 if account is None: 79 ↛ 82line 79 didn't jump to line 82 because the condition on line 79 was always true
80 raise ResourceNotFound()
82 return await payout_service.estimate(session, account=account)
85@router.post("/", response_model=PayoutSchema, status_code=201)
86async def create(
87 auth_subject: payouts_auth.PayoutsWrite,
88 payout_create: PayoutCreate,
89 session: AsyncSession = Depends(get_db_session),
90 locker: Locker = Depends(get_locker),
91) -> Payout:
92 account_id = payout_create.account_id
93 account = await account_service.get(session, auth_subject, account_id)
94 if account is None: 94 ↛ 97line 94 didn't jump to line 97 because the condition on line 94 was always true
95 raise ResourceNotFound()
97 return await payout_service.create(session, locker, account=account)
100@router.get("/{id}/csv")
101async def get_csv(
102 id: UUID4,
103 auth_subject: payouts_auth.PayoutsRead,
104 session: AsyncSession = Depends(get_db_session),
105 sessionmaker: AsyncSessionMaker = Depends(get_db_sessionmaker),
106) -> StreamingResponse:
107 payout = await payout_service.get(session, auth_subject, id)
109 if payout is None: 109 ↛ 112line 109 didn't jump to line 112 because the condition on line 109 was always true
110 raise ResourceNotFound()
112 content = payout_service.get_csv(session, sessionmaker, payout)
113 filename = f"polar-payout-{payout.created_at.isoformat()}.csv"
115 return StreamingResponse(
116 content,
117 media_type="text/csv",
118 headers={"Content-Disposition": f"attachment; filename={filename}"},
119 )
122@router.post("/{id}/invoice", status_code=202)
123async def generate_invoice(
124 id: UUID4,
125 payout_generate_invoice: PayoutGenerateInvoice,
126 auth_subject: payouts_auth.PayoutsWrite,
127 session: AsyncSession = Depends(get_db_session),
128) -> None:
129 """Trigger generation of an order's invoice."""
130 payout = await payout_service.get(session, auth_subject, id)
132 if payout is None: 132 ↛ 135line 132 didn't jump to line 135 because the condition on line 132 was always true
133 raise ResourceNotFound()
135 await payout_service.trigger_invoice_generation(
136 session, payout, payout_generate_invoice
137 )
140@router.get("/{id}/invoice")
141async def invoice(
142 id: UUID4,
143 auth_subject: payouts_auth.PayoutsRead,
144 session: AsyncSession = Depends(get_db_session),
145) -> PayoutInvoice:
146 """Get an order's invoice data."""
147 payout = await payout_service.get(session, auth_subject, id)
149 if payout is None: 149 ↛ 152line 149 didn't jump to line 152 because the condition on line 149 was always true
150 raise ResourceNotFound()
152 return await payout_service.get_invoice(payout)