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

1import datetime 1a

2from uuid import UUID 1a

3 

4import structlog 1a

5from sqlalchemy import select 1a

6 

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) 

21 

22from .balance import balance_transaction as balance_transaction_service 1a

23from .base import BaseTransactionService, BaseTransactionServiceError 1a

24 

25log: Logger = structlog.get_logger() 1a

26 

27 

28class PlatformFeeTransactionError(BaseTransactionServiceError): ... 1a

29 

30 

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) 

36 

37 

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 ) 

48 

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 

57 

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) 

69 

70 return fees_balances 

71 

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 [] 

77 

78 if account.account_type != AccountType.stripe: 

79 return [] 

80 

81 payout_fees: list[tuple[PlatformFeeType, int]] = [] 

82 

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

88 

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 

95 

96 if transfer_fee_amount > 0: 

97 payout_fees.append( 

98 (PlatformFeeType.cross_border_transfer, transfer_fee_amount) 

99 ) 

100 

101 if payout_fee_amount > 0: 

102 payout_fees.append((PlatformFeeType.payout, payout_fee_amount)) 

103 

104 return payout_fees 

105 

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 ) 

112 

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 

124 

125 return balance_amount, payout_fees_balances 

126 

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 

134 

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 

139 

140 total_amount = incoming.amount + incoming.tax_amount 

141 fees_balances: list[tuple[Transaction, Transaction]] = [] 

142 

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) 

154 

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) 

172 

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) 

190 

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) 

204 

205 return fees_balances 

206 

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. 

212 

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 

218 

219 if payment_transaction.processor != Processor.stripe: 

220 return False 

221 

222 if payment_transaction.charge_id is None: 

223 return False 

224 

225 charge = await stripe_service.get_charge(payment_transaction.charge_id) 

226 

227 if (payment_method_details := charge.payment_method_details) is None: 

228 return False 

229 

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" 

235 

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" 

241 

242 return False 

243 

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

260 

261 

262platform_fee_transaction = PlatformFeeTransactionService(Transaction) 1a