Coverage for polar/payout/endpoints.py: 49%

58 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-12-05 16:17 +0000

1from fastapi import Depends, Query 1a

2from fastapi.responses import StreamingResponse 1a

3from pydantic import UUID4 1a

4 

5from polar.account.service import account as account_service 1a

6from polar.exceptions import ResourceNotFound 1a

7from polar.kit.db.postgres import AsyncSessionMaker 1a

8from polar.kit.pagination import ListResource, PaginationParamsQuery 1a

9from polar.kit.schemas import MultipleQueryFilter 1a

10from polar.locker import Locker, get_locker 1a

11from polar.models import Payout 1a

12from polar.models.payout import PayoutStatus 1a

13from polar.openapi import APITag 1a

14from polar.postgres import AsyncSession, get_db_session, get_db_sessionmaker 1a

15from polar.routing import APIRouter 1a

16 

17from . import auth as payouts_auth 1a

18from . import sorting 1a

19from .schemas import Payout as PayoutSchema 1a

20from .schemas import PayoutCreate, PayoutEstimate, PayoutGenerateInvoice, PayoutInvoice 1a

21from .service import InsufficientBalance, UnderReviewAccount 1a

22from .service import payout as payout_service 1a

23 

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

25 

26 

27@router.get("/", response_model=ListResource[PayoutSchema]) 1a

28async def list( 1a

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( 1a

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( 1a

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: 

80 raise ResourceNotFound() 

81 

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

83 

84 

85@router.post("/", response_model=PayoutSchema, status_code=201) 1a

86async def create( 1a

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: 

95 raise ResourceNotFound() 

96 

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

98 

99 

100@router.get("/{id}/csv") 1a

101async def get_csv( 1a

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: 

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) 1a

123async def generate_invoice( 1a

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: 

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") 1a

141async def invoice( 1a

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: 

150 raise ResourceNotFound() 

151 

152 return await payout_service.get_invoice(payout)