Coverage for polar/payout/repository.py: 79%

52 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 12:42 +0000

1from collections.abc import Sequence 

2from datetime import timedelta 

3from uuid import UUID 

4 

5from sqlalchemy import Select, false 

6from sqlalchemy.orm import joinedload 

7 

8from polar.auth.models import AuthSubject, Organization, User, is_organization, is_user 

9from polar.config import settings 

10from polar.enums import AccountType 

11from polar.kit.repository import ( 

12 Options, 

13 RepositoryBase, 

14 RepositorySoftDeletionIDMixin, 

15 RepositorySoftDeletionMixin, 

16 RepositorySortingMixin, 

17 SortingClause, 

18) 

19from polar.kit.utils import utc_now 

20from polar.models import Account, Payout, Transaction 

21from polar.models.payout import PayoutStatus 

22from polar.payout.sorting import PayoutSortProperty 

23 

24 

25class PayoutRepository( 

26 RepositorySoftDeletionIDMixin[Payout, UUID], 

27 RepositorySoftDeletionMixin[Payout], 

28 RepositorySortingMixin[Payout, PayoutSortProperty], 

29 RepositoryBase[Payout], 

30): 

31 model = Payout 

32 sorting_enum = PayoutSortProperty 

33 

34 async def count_by_account(self, account: UUID) -> int: 

35 statement = self.get_base_statement().where(Payout.account_id == account) 

36 return await self.count(statement) 

37 

38 async def get_by_processor_id( 

39 self, 

40 processor: AccountType, 

41 processor_id: str, 

42 *, 

43 options: Options = (), 

44 ) -> Payout | None: 

45 statement = ( 

46 self.get_base_statement() 

47 .where( 

48 Payout.processor == processor, 

49 Payout.processor_id == processor_id, 

50 ) 

51 .options(*options) 

52 ) 

53 return await self.get_one_or_none(statement) 

54 

55 async def get_all_stripe_pending( 

56 self, delay: timedelta = settings.ACCOUNT_PAYOUT_DELAY 

57 ) -> Sequence[Payout]: 

58 statement = ( 

59 self.get_base_statement() 

60 .distinct(Payout.account_id) 

61 .where( 

62 Payout.processor == AccountType.stripe, 

63 Payout.status == PayoutStatus.pending, 

64 Payout.processor_id.is_(None), 

65 Payout.created_at < utc_now() - delay, 

66 ) 

67 .order_by(Payout.account_id.asc(), Payout.created_at.asc()) 

68 ) 

69 return await self.get_all(statement) 

70 

71 async def get_by_account_and_invoice_number( 

72 self, account: UUID, invoice_number: str 

73 ) -> Payout | None: 

74 statement = self.get_base_statement().where( 

75 Payout.account_id == account, 

76 Payout.invoice_number == invoice_number, 

77 ) 

78 return await self.get_one_or_none(statement) 

79 

80 def get_eager_options(self) -> Options: 

81 return ( 

82 joinedload(Payout.account), 

83 joinedload(Payout.transaction).selectinload( 

84 Transaction.incurred_transactions 

85 ), 

86 ) 

87 

88 def get_readable_statement( 

89 self, auth_subject: AuthSubject[User | Organization] 

90 ) -> Select[tuple[Payout]]: 

91 statement = self.get_base_statement() 

92 

93 if is_user(auth_subject): 93 ↛ 98line 93 didn't jump to line 98 because the condition on line 93 was always true

94 user = auth_subject.subject 

95 statement = statement.join(Payout.account).where( 

96 Account.admin_id == user.id 

97 ) 

98 elif is_organization(auth_subject): 

99 # Only the admin of the account can access it 

100 statement = statement.where(false()) 

101 

102 return statement 

103 

104 def get_sorting_clause(self, property: PayoutSortProperty) -> SortingClause: 

105 match property: 

106 case PayoutSortProperty.created_at: 

107 return Payout.created_at 

108 case PayoutSortProperty.amount: 

109 return Payout.amount 

110 case PayoutSortProperty.fees_amount: 

111 return Payout.fees_amount 

112 case PayoutSortProperty.status: 

113 return Payout.status 

114 case PayoutSortProperty.paid_at: 

115 return Payout.paid_at 

116 case PayoutSortProperty.account_id: 116 ↛ exitline 116 didn't return from function 'get_sorting_clause' because the pattern on line 116 always matched

117 return Payout.account_id