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

1from fastapi import Depends, Query 

2from fastapi.responses import StreamingResponse 

3from pydantic import UUID4 

4 

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 

16 

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 

23 

24router = APIRouter(prefix="/payouts", tags=["payouts", APITag.private]) 

25 

26 

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 ) 

49 

50 return ListResource.from_paginated_results( 

51 [PayoutSchema.model_validate(result) for result in results], count, pagination 

52 ) 

53 

54 

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() 

81 

82 return await payout_service.estimate(session, account=account) 

83 

84 

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() 

96 

97 return await payout_service.create(session, locker, account=account) 

98 

99 

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) 

108 

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() 

111 

112 content = payout_service.get_csv(session, sessionmaker, payout) 

113 filename = f"polar-payout-{payout.created_at.isoformat()}.csv" 

114 

115 return StreamingResponse( 

116 content, 

117 media_type="text/csv", 

118 headers={"Content-Disposition": f"attachment; filename={filename}"}, 

119 ) 

120 

121 

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) 

131 

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() 

134 

135 await payout_service.trigger_invoice_generation( 

136 session, payout, payout_generate_invoice 

137 ) 

138 

139 

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) 

148 

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() 

151 

152 return await payout_service.get_invoice(payout)