Coverage for polar/integrations/stripe/payment.py: 17%
99 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 15:52 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 15:52 +0000
1import uuid 1a
3import stripe as stripe_lib 1a
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
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)
36class OutdatedCheckoutIntent(PolarError): 1a
37 """
38 Raised when a received succeeded setup intent is different from the current one
39 associated with the checkout.
41 Usually happens after a TrialAlreadyRedeemed error, where we convert the checkout
42 to a paid one and create a new intent.
43 """
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)
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
59 if (checkout_id := object.metadata.get("checkout_id")) is None:
60 return None
62 repository = CheckoutRepository.from_session(session)
63 return await repository.get_by_id(
64 uuid.UUID(checkout_id), options=repository.get_eager_options()
65 )
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
75 wallet_id = object.metadata.get("wallet_id")
76 wallet_transaction_id = object.metadata.get("wallet_transaction_id")
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
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
96 return None, None
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
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
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 )
132 return None
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)
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)
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)
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 )
166 await checkout_service.handle_success(
167 session,
168 checkout,
169 payment=payment,
170 payment_method=payment_method,
171 )
173 if wallet_transaction is not None:
174 await order_service.create_wallet_order(session, wallet_transaction, payment)
176 if order is not None:
177 await order_service.handle_payment(session, order, payment)
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)
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 )
198 if checkout is not None:
199 await checkout_service.handle_failure(session, checkout, payment=payment)
201 if order is not None:
202 await order_service.handle_payment_failure(session, order)
205__all__ = [ 1a
206 "handle_success",
207 "handle_failure",
208 "resolve_checkout",
209 "resolve_order",
210]