Coverage for polar/integrations/stripe/payment.py: 17%

99 statements  

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

1import uuid 1a

2 

3import stripe as stripe_lib 1a

4 

5from polar.checkout.repository import CheckoutRepository 1a

6from polar.checkout.service import checkout as checkout_service 1a

7from polar.exceptions import PolarError 1a

8from polar.integrations.stripe.utils import get_expandable_id 1a

9from polar.models import ( 1a

10 Checkout, 

11 Order, 

12 Payment, 

13 PaymentMethod, 

14 Wallet, 

15 WalletTransaction, 

16) 

17from polar.models.checkout import CheckoutStatus 1a

18from polar.order.repository import OrderRepository 1a

19from polar.order.service import order as order_service 1a

20from polar.payment.service import payment as payment_service 1a

21from polar.payment_method.service import payment_method as payment_method_service 1a

22from polar.postgres import AsyncSession 1a

23from polar.transaction.service.payment import ( 1a

24 payment_transaction as payment_transaction_service, 

25) 

26from polar.wallet.repository import WalletRepository, WalletTransactionRepository 1a

27 

28 

29class OrderDoesNotExist(PolarError): 1a

30 def __init__(self, order_id: str) -> None: 1a

31 self.order_id = order_id 

32 message = f"Order with id {order_id} does not exist." 

33 super().__init__(message) 

34 

35 

36class OutdatedCheckoutIntent(PolarError): 1a

37 """ 

38 Raised when a received succeeded setup intent is different from the current one 

39 associated with the checkout. 

40 

41 Usually happens after a TrialAlreadyRedeemed error, where we convert the checkout 

42 to a paid one and create a new intent. 

43 """ 

44 

45 def __init__(self, checkout_id: uuid.UUID, intent_id: str) -> None: 1a

46 self.checkout_id = checkout_id 

47 self.intent_id = intent_id 

48 message = f"Intent with id {intent_id} for checkout {checkout_id} is outdated." 

49 super().__init__(message) 

50 

51 

52async def resolve_checkout( 1a

53 session: AsyncSession, 

54 object: stripe_lib.Charge | stripe_lib.PaymentIntent | stripe_lib.SetupIntent, 

55) -> Checkout | None: 

56 if object.metadata is None: 

57 return None 

58 

59 if (checkout_id := object.metadata.get("checkout_id")) is None: 

60 return None 

61 

62 repository = CheckoutRepository.from_session(session) 

63 return await repository.get_by_id( 

64 uuid.UUID(checkout_id), options=repository.get_eager_options() 

65 ) 

66 

67 

68async def resolve_wallet( 1a

69 session: AsyncSession, 

70 object: stripe_lib.Charge | stripe_lib.PaymentIntent | stripe_lib.SetupIntent, 

71) -> tuple[Wallet | None, WalletTransaction | None]: 

72 if object.metadata is None: 

73 return None, None 

74 

75 wallet_id = object.metadata.get("wallet_id") 

76 wallet_transaction_id = object.metadata.get("wallet_transaction_id") 

77 

78 if wallet_transaction_id is not None: 

79 wallet_transaction_repository = WalletTransactionRepository.from_session( 

80 session 

81 ) 

82 wallet_transaction = await wallet_transaction_repository.get_by_id( 

83 uuid.UUID(wallet_transaction_id), 

84 options=wallet_transaction_repository.get_eager_options(), 

85 ) 

86 if wallet_transaction is not None: 

87 return wallet_transaction.wallet, wallet_transaction 

88 

89 if wallet_id is not None: 

90 wallet_repository = WalletRepository.from_session(session) 

91 wallet = await wallet_repository.get_by_id( 

92 uuid.UUID(wallet_id), options=wallet_repository.get_eager_options() 

93 ) 

94 return wallet, None 

95 

96 return None, None 

97 

98 

99async def resolve_order( 1a

100 session: AsyncSession, 

101 object: stripe_lib.Charge | stripe_lib.PaymentIntent | stripe_lib.SetupIntent, 

102 checkout: Checkout | None, 

103) -> Order | None: 

104 order_repository = OrderRepository.from_session(session) 

105 if ( 

106 object.metadata is not None 

107 and (order_id := object.metadata.get("order_id")) is not None 

108 ): 

109 order = await order_repository.get_by_id( 

110 uuid.UUID(order_id), options=order_repository.get_eager_options() 

111 ) 

112 if order is None: 

113 raise OrderDoesNotExist(order_id) 

114 return order 

115 

116 if ( 

117 object.OBJECT_NAME == "charge" or object.OBJECT_NAME == "payment_intent" 

118 ) and object.invoice is not None: 

119 invoice_id = get_expandable_id(object.invoice) 

120 order = await order_repository.get_by_stripe_invoice_id( 

121 invoice_id, options=order_repository.get_eager_options() 

122 ) 

123 if order is None: 

124 raise OrderDoesNotExist(invoice_id) 

125 return order 

126 

127 if checkout is not None: 

128 return await order_repository.get_earliest_by_checkout_id( 

129 checkout.id, options=order_repository.get_eager_options() 

130 ) 

131 

132 return None 

133 

134 

135async def handle_success( 1a

136 session: AsyncSession, object: stripe_lib.Charge | stripe_lib.SetupIntent 

137) -> None: 

138 checkout = await resolve_checkout(session, object) 

139 wallet, wallet_transaction = await resolve_wallet(session, object) 

140 order = await resolve_order(session, object, checkout) 

141 

142 payment: Payment | None = None 

143 if object.OBJECT_NAME == "charge": 

144 payment = await payment_service.upsert_from_stripe_charge( 

145 session, object, checkout, wallet, order 

146 ) 

147 await payment_transaction_service.create_payment(session, charge=object) 

148 

149 if checkout is not None: 

150 if object.OBJECT_NAME == "setup_intent": 

151 checkout_intent_client_secret = checkout.payment_processor_metadata.get( 

152 "intent_client_secret" 

153 ) 

154 if checkout.status == CheckoutStatus.expired or ( 

155 checkout_intent_client_secret is not None 

156 and object.client_secret != checkout_intent_client_secret 

157 ): 

158 raise OutdatedCheckoutIntent(checkout.id, object.id) 

159 

160 payment_method: PaymentMethod | None = None 

161 if checkout.should_save_payment_method: 

162 payment_method = await payment_method_service.upsert_from_stripe_intent( 

163 session, object, checkout 

164 ) 

165 

166 await checkout_service.handle_success( 

167 session, 

168 checkout, 

169 payment=payment, 

170 payment_method=payment_method, 

171 ) 

172 

173 if wallet_transaction is not None: 

174 await order_service.create_wallet_order(session, wallet_transaction, payment) 

175 

176 if order is not None: 

177 await order_service.handle_payment(session, order, payment) 

178 

179 

180async def handle_failure( 1a

181 session: AsyncSession, 

182 object: stripe_lib.Charge | stripe_lib.PaymentIntent | stripe_lib.SetupIntent, 

183) -> None: 

184 checkout = await resolve_checkout(session, object) 

185 wallet, _ = await resolve_wallet(session, object) 

186 order = await resolve_order(session, object, checkout) 

187 

188 payment: Payment | None = None 

189 if object.OBJECT_NAME == "charge": 

190 payment = await payment_service.upsert_from_stripe_charge( 

191 session, object, checkout, wallet, order 

192 ) 

193 elif object.OBJECT_NAME == "payment_intent": 

194 payment = await payment_service.upsert_from_stripe_payment_intent( 

195 session, object, checkout, order 

196 ) 

197 

198 if checkout is not None: 

199 await checkout_service.handle_failure(session, checkout, payment=payment) 

200 

201 if order is not None: 

202 await order_service.handle_payment_failure(session, order) 

203 

204 

205__all__ = [ 1a

206 "handle_success", 

207 "handle_failure", 

208 "resolve_checkout", 

209 "resolve_order", 

210]