Skip to content

Commit 757050b

Browse files
committed
Refactor subscription_complete into paddle_transaction_complete and remove the dev only call for checking paddle directly.
1 parent 593b325 commit 757050b

File tree

4 files changed

+140
-72
lines changed

4 files changed

+140
-72
lines changed

assets/app/vue/views/SubscribeView/components/CheckoutStep.vue

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,13 @@ const areWeDoneHere = async () => {
8585
if (['completed', 'paid'].indexOf(status) > -1) {
8686
// Remove the Paddle checkout form, and after a short wait reload the page.
8787
paymentComplete.value = true;
88-
}
89-
90-
if (status === 'completed') {
9188
// We re-use this handler since it's not currently used, and it's hooked up to unMount.
9289
doneCheckerHandler = window.setTimeout(() => {
9390
window.location.reload();
9491
}, SHORT_WAIT_MS);
9592
return;
9693
}
97-
94+
9895
// Lastly clear up the exception counter, if we've reached here there's no exceptions happening and no errors need to be shown.
9996
exceptionCounter = 0;
10097
} catch (e) {
@@ -271,10 +268,10 @@ export default {
271268
<h2>{{ t('views.subscribe.title') }}</h2>
272269
<notice-bar v-if="paddleUnknownError" :type="NoticeBarTypes.Critical">{{
273270
t('views.subscribe.paddleUnknownError')
274-
}}</notice-bar>
271+
}}</notice-bar>
275272
<notice-bar v-if="planSystemError" :type="NoticeBarTypes.Critical">{{
276273
t('views.subscribe.planSystemError')
277-
}}</notice-bar>
274+
}}</notice-bar>
278275
<div class="container">
279276
<card-container class="summary-card">
280277
<ul class="summary">

src/thunderbird_accounts/subscription/tests/test_views.py

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,48 @@ def test_transaction_found_but_not_doneish(self):
121121

122122
instance.notifications.list.assert_not_called()
123123

124-
def _test_doneish_by_status(self, tx_status):
124+
125+
class PaddleTransactionCompleteCase(TestCase):
126+
def setUp(self):
127+
self.client = RequestClient()
128+
self.user = User.objects.create(
129+
username=f'test@{settings.PRIMARY_EMAIL_DOMAIN}', oidc_id='1234', is_awaiting_payment_verification=False
130+
)
131+
oidc_force_login(self.client, self.user)
132+
self.url = reverse('paddle_completed')
133+
self.txid = 'abc123'
134+
135+
def set_paddle_transaction_session_data(self, payment_type):
136+
"""This actually tests set_paddle_transaction_id too!"""
137+
txid_response = self.client.put(
138+
reverse('paddle_txid'), data=json.dumps({'txid': self.txid, 'payment_type': payment_type})
139+
)
140+
self.assertEqual(txid_response.status_code, 200)
141+
data = txid_response.json()
142+
self.assertTrue(data.get('success'))
143+
144+
def _test_doneish_by_status(self, tx_status, is_popup_payment_provider=False):
125145
"""Helper function so we can reduce some code without introducing artifacts between test runs."""
126-
# Make sure we have a txid in session
127-
self.set_paddle_transaction_id()
146+
# Make sure we have a txid and payment type in session
147+
payment_type = 'card' if not is_popup_payment_provider else 'paypal'
148+
ok_status_code = 200 if is_popup_payment_provider else 302
149+
ok_payment_verification = tx_status in [
150+
Transaction.StatusValues.PAID.value,
151+
Transaction.StatusValues.COMPLETED.value,
152+
]
153+
154+
self.set_paddle_transaction_session_data(payment_type)
128155

129-
transaction = Transaction.objects.create(paddle_id=self.txid, status=tx_status)
156+
transaction = Transaction.objects.create(paddle_id=self.txid, status=tx_status.value)
130157
self.assertIsNotNone(transaction)
131158

132159
with patch('thunderbird_accounts.subscription.tasks.dev_only_paddle_fake_webhook', MagicMock()) as task_mock:
133160
with patch('thunderbird_accounts.subscription.decorators.Client', MagicMock()) as paddle_client_mock:
134161
instance = paddle_client_mock()
162+
status_mock = MagicMock()
163+
164+
status_mock.status = tx_status
165+
instance.transactions.get.return_value = status_mock
135166

136167
self.assertFalse(self.user.is_awaiting_payment_verification)
137168

@@ -140,26 +171,31 @@ def _test_doneish_by_status(self, tx_status):
140171
follow=False,
141172
)
142173
self.assertTrue(response)
143-
self.assertEqual(response.status_code, 200)
144-
145-
data = response.json()
146-
self.assertTrue(data)
147-
self.assertEqual(data.get('status'), tx_status)
174+
self.assertEqual(response.status_code, ok_status_code)
148175

149176
self.user.refresh_from_db()
150-
self.assertTrue(self.user.is_awaiting_payment_verification)
177+
self.assertEqual(self.user.is_awaiting_payment_verification, ok_payment_verification)
151178

179+
instance.transactions.get.assert_called()
152180
instance.notifications.list.assert_not_called()
153181
task_mock.assert_not_called()
154182

183+
@override_settings(IS_DEV=False)
184+
def test_transaction_found_and_is_not_doneish_by_being_ready(self):
185+
"""We found the transaction but it's doneish (status=READY). This shouldn't do anything!
186+
187+
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
188+
189+
self._test_doneish_by_status(Transaction.StatusValues.READY)
190+
155191
@override_settings(IS_DEV=False)
156192
def test_transaction_found_and_is_doneish_by_being_paid(self):
157193
"""We found the transaction but it's doneish (status=PAID). This should trigger payment verification
158194
and remove txid from session.
159195
160196
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
161197

162-
self._test_doneish_by_status(Transaction.StatusValues.PAID.value)
198+
self._test_doneish_by_status(Transaction.StatusValues.PAID)
163199

164200
@override_settings(IS_DEV=False)
165201
def test_transaction_found_and_is_doneish_by_being_completed(self):
@@ -168,4 +204,22 @@ def test_transaction_found_and_is_doneish_by_being_completed(self):
168204
169205
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
170206

171-
self._test_doneish_by_status(Transaction.StatusValues.COMPLETED.value)
207+
self._test_doneish_by_status(Transaction.StatusValues.COMPLETED)
208+
209+
@override_settings(IS_DEV=False)
210+
def test_transaction_found_and_is_doneish_by_being_paid_popup_popup_edition(self):
211+
"""We found the transaction but it's doneish (status=PAID). This should trigger payment verification
212+
and remove txid from session.
213+
214+
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
215+
216+
self._test_doneish_by_status(Transaction.StatusValues.PAID, True)
217+
218+
@override_settings(IS_DEV=False)
219+
def test_transaction_found_and_is_doneish_by_being_completed_popup_edition(self):
220+
"""We found the transaction but it's doneish (status=COMPLETED). This should trigger payment verification
221+
and remove txid from session.
222+
223+
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
224+
225+
self._test_doneish_by_status(Transaction.StatusValues.COMPLETED, True)

src/thunderbird_accounts/subscription/views.py

Lines changed: 70 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -40,49 +40,6 @@ def prefilter_paddle_webhook(event_type: str, event_data: dict) -> bool:
4040
return True
4141

4242

43-
@login_required
44-
@inject_paddle
45-
def subscription_complete(request: HttpRequest, paddle: Client):
46-
"""User is redirected by Paddle via the successUrl."""
47-
user = request.user
48-
transaction_id = request.session.pop(SESSION_PADDLE_TRANSACTION_ID)
49-
payment_type = request.session.pop(SESSION_PADDLE_PAYMENT_TYPE)
50-
51-
# Hmm this shouldn't happen...send them home. They'll be redirected to subscribe if they're not subscribed anyways.
52-
if not transaction_id or not payment_type:
53-
return HttpResponseRedirect('/')
54-
55-
transaction = paddle.transactions.get(transaction_id=transaction_id)
56-
status = transaction.status.value
57-
58-
if transaction and status in [Transaction.StatusValues.COMPLETED.value, Transaction.StatusValues.PAID.value]:
59-
user.is_awaiting_payment_verification = True
60-
user.save()
61-
62-
if settings.IS_DEV:
63-
tasks.dev_only_paddle_fake_webhook.delay(transaction_id=transaction_id, user_uuid=user.uuid.hex)
64-
65-
"""
66-
Paddle folks mentioned these are the types that open pop-up windows:
67-
"""
68-
if payment_type in [
69-
PaymentMethodType.Paypal.value,
70-
# We don't use these, but for completeness' sake.
71-
PaymentMethodType.Alipay.value,
72-
PaymentMethodType.Bancontact.value,
73-
PaymentMethodType.Ideal.value,
74-
PaymentMethodType.KoreaLocal.value,
75-
]:
76-
# Tell their window to close
77-
return TemplateResponse(
78-
request,
79-
'close_window.html',
80-
status=200,
81-
)
82-
83-
return HttpResponseRedirect('/subscribe')
84-
85-
8643
@login_required
8744
@require_http_methods(['POST'])
8845
def get_paddle_information(request: Request):
@@ -118,8 +75,7 @@ def set_paddle_transaction_id(request: Request):
11875

11976
@login_required
12077
@require_http_methods(['POST'])
121-
@inject_paddle
122-
def is_paddle_transaction_done(request: Request, paddle: Client):
78+
def is_paddle_transaction_done(request: Request):
12379
"""Checks if the Paddle transaction has finished and returns True or False.
12480
Also cleans up transaction id once the transaction is completed."""
12581
transaction_id = request.session.get(SESSION_PADDLE_TRANSACTION_ID, default=None)
@@ -132,18 +88,79 @@ def is_paddle_transaction_done(request: Request, paddle: Client):
13288

13389
status = Transaction.StatusValues.DRAFT.value
13490

135-
# For dev machines we have to inquire directly, otherwise we rely on information given to us by the Paddle webhooks
136-
if settings.IS_DEV:
137-
transaction = paddle.transactions.get(transaction_id=transaction_id)
138-
status = transaction.status.value
139-
else:
140-
transaction = Transaction.objects.filter(paddle_id=transaction_id).first()
141-
if transaction:
142-
status = transaction.status
91+
transaction = Transaction.objects.filter(paddle_id=transaction_id).first()
92+
if transaction:
93+
status = transaction.status
14394

14495
return JsonResponse({'status': status})
14596

14697

98+
@login_required
99+
@inject_paddle
100+
def paddle_transaction_complete(request: HttpRequest, paddle: Client):
101+
"""User is redirected by Paddle via the successUrl. This means we have a transaction that's paid or
102+
completed (noted as doneish.)
103+
104+
There's some special logic for certain payment processors that open a pop-up window instead of
105+
redirecting to another page or completeing on page. Those processors are defined in code, and
106+
will redirect the pop-up window to a django template that _should_ immediately close the window.
107+
For those processors the doneish/redirect logic is handled in
108+
:any:`thunderbird_accounts.subscription.views.is_paddle_transaction_done`
109+
110+
For regular payment processors like the default ``card`` we will simply redirect to the subscribe page.
111+
The front-end will handle if the check to see if the user's transaction and subscription has been pulled
112+
in our db via webhooks.
113+
"""
114+
user = request.user
115+
transaction_id = request.session.pop(SESSION_PADDLE_TRANSACTION_ID)
116+
payment_type = request.session.pop(SESSION_PADDLE_PAYMENT_TYPE)
117+
redirect_response = HttpResponseRedirect('/subscribe')
118+
119+
# Hmm this shouldn't happen...
120+
if not transaction_id or not payment_type:
121+
return redirect_response
122+
123+
transaction = paddle.transactions.get(transaction_id=transaction_id)
124+
status = transaction.status.value
125+
126+
if transaction and status in [Transaction.StatusValues.COMPLETED.value, Transaction.StatusValues.PAID.value]:
127+
user.is_awaiting_payment_verification = True
128+
user.save()
129+
130+
if settings.IS_DEV:
131+
tasks.dev_only_paddle_fake_webhook.delay(transaction_id=transaction_id, user_uuid=user.uuid.hex)
132+
133+
"""
134+
Paddle folks mentioned these are the types that open pop-up windows:
135+
* PayPal
136+
* Alipay
137+
* Bancontact
138+
* BLIK
139+
* iDEAL
140+
* MB WAY
141+
* South Korea local cards
142+
* Naver Pay, Kakao Pay, Samsung Pay, Payco
143+
* Pix
144+
* UPI
145+
"""
146+
if payment_type in [
147+
PaymentMethodType.Paypal.value,
148+
# We don't use these, but for completeness' sake.
149+
PaymentMethodType.Alipay.value,
150+
PaymentMethodType.Bancontact.value,
151+
PaymentMethodType.Ideal.value,
152+
PaymentMethodType.KoreaLocal.value,
153+
]:
154+
# Tell their window to close
155+
return TemplateResponse(
156+
request,
157+
'close_window.html',
158+
status=200,
159+
)
160+
161+
return redirect_response
162+
163+
147164
@login_required
148165
@require_http_methods(['POST'])
149166
@inject_paddle

src/thunderbird_accounts/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
# Authentication
5151
path('users/sign-up/', auth_views.sign_up, name='sign_up'),
5252
# Subscription
53-
path('subscription/paddle/complete/', subscription_views.subscription_complete, name='paddle_completed'),
53+
path('subscription/paddle/complete/', subscription_views.paddle_transaction_complete, name='paddle_completed'),
5454
# CalDAV auto-setup for Appointment
5555
path('appointment/caldav/setup/', mail_views.appointment_caldav_setup, name='appointment_caldav_setup'),
5656
# API

0 commit comments

Comments
 (0)