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
« 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
5from sqlalchemy import Select, false
6from sqlalchemy.orm import joinedload
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
25class PayoutRepository(
26 RepositorySoftDeletionIDMixin[Payout, UUID],
27 RepositorySoftDeletionMixin[Payout],
28 RepositorySortingMixin[Payout, PayoutSortProperty],
29 RepositoryBase[Payout],
30):
31 model = Payout
32 sorting_enum = PayoutSortProperty
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)
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)
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)
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)
80 def get_eager_options(self) -> Options:
81 return (
82 joinedload(Payout.account),
83 joinedload(Payout.transaction).selectinload(
84 Transaction.incurred_transactions
85 ),
86 )
88 def get_readable_statement(
89 self, auth_subject: AuthSubject[User | Organization]
90 ) -> Select[tuple[Payout]]:
91 statement = self.get_base_statement()
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())
102 return statement
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