Coverage for polar/transaction/service/platform_fee.py: 20%
108 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
1import datetime 1a
2from uuid import UUID 1a
4import structlog 1a
5from sqlalchemy import select 1a
7from polar.account.repository import AccountRepository 1a
8from polar.enums import AccountType 1a
9from polar.integrations.stripe.service import stripe as stripe_service 1a
10from polar.logging import Logger 1a
11from polar.models import Account, Transaction 1a
12from polar.models.transaction import PlatformFeeType, Processor, TransactionType 1a
13from polar.postgres import AsyncSession 1a
14from polar.transaction.fees.stripe import ( 1a
15 get_reverse_stripe_payout_fees,
16 get_stripe_account_fee,
17 get_stripe_international_fee,
18 get_stripe_invoice_fee,
19 get_stripe_subscription_fee,
20)
22from .balance import balance_transaction as balance_transaction_service 1a
23from .base import BaseTransactionService, BaseTransactionServiceError 1a
25log: Logger = structlog.get_logger() 1a
28class PlatformFeeTransactionError(BaseTransactionServiceError): ... 1a
31class PayoutAmountTooLow(PlatformFeeTransactionError): 1a
32 def __init__(self, balance_amount: int) -> None: 1a
33 self.balance_amount = balance_amount
34 message = "Fees are higher than the amount to be paid out."
35 super().__init__(message)
38class PlatformFeeTransactionService(BaseTransactionService): 1a
39 async def create_fees_reversal_balances( 1a
40 self,
41 session: AsyncSession,
42 *,
43 balance_transactions: tuple[Transaction, Transaction],
44 ) -> list[tuple[Transaction, Transaction]]:
45 return await self._create_payment_fees(
46 session, balance_transactions=balance_transactions
47 )
49 async def create_dispute_fees_balances( 1a
50 self,
51 session: AsyncSession,
52 *,
53 dispute_fees: list[Transaction],
54 balance_transactions: tuple[Transaction, Transaction],
55 ) -> list[tuple[Transaction, Transaction]]:
56 outgoing, incoming = balance_transactions
58 fees_balances: list[tuple[Transaction, Transaction]] = []
59 for dispute_fee in dispute_fees:
60 fee_balances = await balance_transaction_service.create_reversal_balance(
61 session,
62 balance_transactions=balance_transactions,
63 amount=-dispute_fee.amount,
64 platform_fee_type=PlatformFeeType.dispute,
65 outgoing_incurred_by=incoming,
66 incoming_incurred_by=outgoing,
67 )
68 fees_balances.append(fee_balances)
70 return fees_balances
72 async def get_payout_fees( 1a
73 self, session: AsyncSession, *, account: Account, balance_amount: int
74 ) -> list[tuple[PlatformFeeType, int]]:
75 if not account.processor_fees_applicable:
76 return []
78 if account.account_type != AccountType.stripe:
79 return []
81 payout_fees: list[tuple[PlatformFeeType, int]] = []
83 last_payout = await self._get_last_payout(session, account)
84 if last_payout is None:
85 account_fee_amount = get_stripe_account_fee()
86 balance_amount -= account_fee_amount
87 payout_fees.append((PlatformFeeType.account, account_fee_amount))
89 try:
90 transfer_fee_amount, payout_fee_amount = get_reverse_stripe_payout_fees(
91 balance_amount, account.country
92 )
93 except ValueError as e:
94 raise PayoutAmountTooLow(balance_amount) from e
96 if transfer_fee_amount > 0:
97 payout_fees.append(
98 (PlatformFeeType.cross_border_transfer, transfer_fee_amount)
99 )
101 if payout_fee_amount > 0:
102 payout_fees.append((PlatformFeeType.payout, payout_fee_amount))
104 return payout_fees
106 async def create_payout_fees_balances( 1a
107 self, session: AsyncSession, *, account: Account, balance_amount: int
108 ) -> tuple[int, list[tuple[Transaction, Transaction]]]:
109 payout_fees = await self.get_payout_fees(
110 session, account=account, balance_amount=balance_amount
111 )
113 payout_fees_balances: list[tuple[Transaction, Transaction]] = []
114 for payout_fee_type, fee_amount in payout_fees:
115 fee_balances = await balance_transaction_service.create_balance(
116 session,
117 source_account=account,
118 destination_account=None,
119 amount=fee_amount,
120 platform_fee_type=payout_fee_type,
121 )
122 payout_fees_balances.append(fee_balances)
123 balance_amount -= fee_amount
125 return balance_amount, payout_fees_balances
127 async def _create_payment_fees( 1a
128 self,
129 session: AsyncSession,
130 *,
131 balance_transactions: tuple[Transaction, Transaction],
132 ) -> list[tuple[Transaction, Transaction]]:
133 outgoing, incoming = balance_transactions
135 assert incoming.account_id is not None
136 account_repository = AccountRepository.from_session(session)
137 account = await account_repository.get_by_id(incoming.account_id)
138 assert account is not None
140 total_amount = incoming.amount + incoming.tax_amount
141 fees_balances: list[tuple[Transaction, Transaction]] = []
143 # Payment fee
144 fee_amount = account.calculate_fee_in_cents(total_amount)
145 fee_balances = await balance_transaction_service.create_reversal_balance(
146 session,
147 balance_transactions=balance_transactions,
148 amount=fee_amount,
149 platform_fee_type=PlatformFeeType.payment,
150 outgoing_incurred_by=incoming,
151 incoming_incurred_by=outgoing,
152 )
153 fees_balances.append(fee_balances)
155 # International fee
156 if incoming.payment_transaction_id is not None:
157 if await self._is_international_payment_transaction(
158 session, incoming.payment_transaction_id
159 ):
160 international_fee_amount = get_stripe_international_fee(total_amount)
161 fee_balances = (
162 await balance_transaction_service.create_reversal_balance(
163 session,
164 balance_transactions=balance_transactions,
165 amount=international_fee_amount,
166 platform_fee_type=PlatformFeeType.international_payment,
167 outgoing_incurred_by=incoming,
168 incoming_incurred_by=outgoing,
169 )
170 )
171 fees_balances.append(fee_balances)
173 # Subscription fee
174 if incoming.order_id is not None:
175 await session.refresh(incoming, {"order"})
176 assert incoming.order is not None
177 if incoming.order.subscription_id is not None:
178 subscription_fee_amount = get_stripe_subscription_fee(total_amount)
179 fee_balances = (
180 await balance_transaction_service.create_reversal_balance(
181 session,
182 balance_transactions=balance_transactions,
183 amount=subscription_fee_amount,
184 platform_fee_type=PlatformFeeType.subscription,
185 outgoing_incurred_by=incoming,
186 incoming_incurred_by=outgoing,
187 )
188 )
189 fees_balances.append(fee_balances)
191 # Invoice fee
192 pledge = incoming.pledge
193 if pledge is not None and pledge.invoice_id is not None:
194 invoice_fee_amount = get_stripe_invoice_fee(total_amount)
195 fee_balances = await balance_transaction_service.create_reversal_balance(
196 session,
197 balance_transactions=balance_transactions,
198 amount=invoice_fee_amount,
199 platform_fee_type=PlatformFeeType.invoice,
200 outgoing_incurred_by=incoming,
201 incoming_incurred_by=outgoing,
202 )
203 fees_balances.append(fee_balances)
205 return fees_balances
207 async def _is_international_payment_transaction( 1a
208 self, session: AsyncSession, payment_transaction_id: UUID
209 ) -> bool:
210 """
211 Check if the payment transaction is an international payment.
213 Currently, we only check if the payment was made using Stripe
214 and the card is not from the US.
215 """
216 payment_transaction = await self.get(session, payment_transaction_id)
217 assert payment_transaction is not None
219 if payment_transaction.processor != Processor.stripe:
220 return False
222 if payment_transaction.charge_id is None:
223 return False
225 charge = await stripe_service.get_charge(payment_transaction.charge_id)
227 if (payment_method_details := charge.payment_method_details) is None:
228 return False
230 if (
231 payment_method_details.type == "card"
232 and payment_method_details.card is not None
233 ):
234 return payment_method_details.card.country != "US"
236 if (
237 payment_method_details.type == "link"
238 and payment_method_details.link is not None
239 ):
240 return payment_method_details.link.country != "US"
242 return False
244 async def _get_last_payout( 1a
245 self, session: AsyncSession, account: Account
246 ) -> Transaction | None:
247 statement = (
248 select(Transaction)
249 .where(
250 Transaction.type == TransactionType.payout,
251 Transaction.account_id == account.id,
252 Transaction.created_at
253 > datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=30),
254 )
255 .limit(1)
256 .order_by(Transaction.created_at.desc())
257 )
258 result = await session.execute(statement)
259 return result.scalar_one_or_none()
262platform_fee_transaction = PlatformFeeTransactionService(Transaction) 1a