Skip to content

Commit 042828e

Browse files
committed
Process paypal webhook for transactions in USD
1 parent 3b098bd commit 042828e

File tree

8 files changed

+470
-20
lines changed

8 files changed

+470
-20
lines changed

poetry.lock

Lines changed: 26 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pytest-mock = "^3.14.0"
4646
djade = "^1.3.2"
4747
black = "^25.1.0"
4848
django-debug-toolbar = "^6.0.0"
49+
responses = "^0.25.8"
4950

5051
[build-system]
5152
requires = ["poetry-core"]

thebook/webhooks/models.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,33 @@ class PaypalWebhookPayload(models.Model):
148148

149149
objects = PayPalWebhookPayloadManager()
150150

151+
def _extract_amount(self, payload):
152+
currency = jmespath.search("resource.amount.currency", payload)
153+
154+
if currency == "BRL":
155+
amount = float(jmespath.search("resource.amount.total", payload))
156+
elif currency == "USD":
157+
amount = float(jmespath.search("resource.receivable_amount.value", payload))
158+
159+
return amount
160+
161+
def _extract_transaction_fee(self, payload):
162+
currency = jmespath.search("resource.transaction_fee.currency", payload)
163+
164+
if currency == "BRL":
165+
transaction_fee = -1 * float(
166+
jmespath.search("resource.transaction_fee.value", payload)
167+
)
168+
elif currency == "USD":
169+
transaction_fee = None
170+
171+
return transaction_fee
172+
151173
def process(self, bank_account=None, user=None):
174+
logger.info("paypalwebhookpayload.process.start", id=self.id)
175+
152176
if self.status == ProcessingStatus.PROCESSED:
177+
logger.info("paypalwebhookpayload.process.already_processed", id=self.id)
153178
return
154179

155180
if bank_account is None:
@@ -161,13 +186,15 @@ def process(self, bank_account=None, user=None):
161186
try:
162187
payload = json.loads(self.payload)
163188
except json.decoder.JSONDecodeError:
189+
logger.warning("paypalwebhookpayload.process.unparsable_event", id=self.id)
164190
self.status = ProcessingStatus.UNPARSABLE
165191
self.internal_notes = "webhooks.paypal.jsondecodeerror"
166192
self.save()
167193
return
168194

169195
event_type = payload.get("event_type")
170196
if event_type != "PAYMENT.SALE.COMPLETED":
197+
logger.warning("paypalwebhookpayload.process.unparsable_event", id=self.id)
171198
self.status = ProcessingStatus.UNPARSABLE
172199
self.internal_notes = "webhooks.paypal.unparsable_event"
173200
self.save()
@@ -177,6 +204,9 @@ def process(self, bank_account=None, user=None):
177204
jmespath.search("resource.billing_agreement_id", payload) or ""
178205
)
179206
if not billing_agreement_id:
207+
logger.warning(
208+
"paypalwebhookpayload.process.missing_billing_agreement_id", id=self.id
209+
)
180210
self.status = ProcessingStatus.UNPARSABLE
181211
self.internal_notes = "webhooks.paypal.missing_billing_agreement_id"
182212
self.save()
@@ -198,20 +228,26 @@ def process(self, bank_account=None, user=None):
198228
headers={"Authorization": f"Bearer {access_token}"},
199229
)
200230
subscription = response.json()
231+
logger.info(
232+
"paypalwebhookpayload.process.subscription",
233+
id=self.id,
234+
subscription=subscription,
235+
)
236+
237+
amount = self._extract_amount(payload)
238+
transaction_fee = self._extract_transaction_fee(payload)
201239

202-
amount = float(jmespath.search("resource.amount.total", payload))
203240
fee = -1 * float(jmespath.search("resource.transaction_fee.value", payload))
241+
204242
paid_at = jmespath.search("resource.create_time", payload)
205243
utc_transaction_date = datetime.datetime.strptime(paid_at, "%Y-%m-%dT%H:%M:%SZ")
206244

207-
logger.info(f"{subscription=}")
208245
given_name = jmespath.search("subscriber.name.given_name", subscription) or ""
209246
surname = jmespath.search("subscriber.name.surname", subscription) or ""
210247
full_name = " ".join([given_name, surname]).strip()
211-
payer_id = jmespath.search("subscriber.name.payer_id", subscription) or ""
248+
payer_id = jmespath.search("subscriber.payer_id", subscription) or ""
212249

213-
description = " - ".join([part for part in (payer_id, full_name) if part])
214-
logger.info(f"{description=}")
250+
description = " - ".join([part for part in (full_name, payer_id) if part])
215251

216252
reference = jmespath.search("resource.id", payload)
217253

@@ -228,15 +264,19 @@ def process(self, bank_account=None, user=None):
228264
bank_account=bank_account,
229265
created_by=user,
230266
)
231-
Transaction.objects.create(
232-
reference="T" + reference,
233-
date=utc_transaction_date,
234-
description="Taxa PayPal - " + description,
235-
amount=fee,
236-
bank_account=bank_account,
237-
category=bank_fee_category,
238-
created_by=user,
239-
)
267+
268+
if transaction_fee is not None:
269+
Transaction.objects.create(
270+
reference="T" + reference,
271+
date=utc_transaction_date,
272+
description="Taxa PayPal - " + description,
273+
amount=fee,
274+
bank_account=bank_account,
275+
category=bank_fee_category,
276+
created_by=user,
277+
)
240278

241279
self.status = ProcessingStatus.PROCESSED
242280
self.save()
281+
282+
logger.info("paypalwebhookpayload.process.finished", id=self.id)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"status": "ACTIVE",
3+
"status_update_time": "2026-02-13T10:10:12Z",
4+
"id": "I-BBBBBBBBBBBB",
5+
"start_time": "2025-03-13T16:04:09Z",
6+
"shipping_amount": {
7+
"currency_code": "BRL",
8+
"value": "0.0"
9+
},
10+
"subscriber": {
11+
"email_address": "douglas.brsoftware@gmail.com",
12+
"payer_id": "L4AVQLJR8GMZY",
13+
"name": {
14+
"given_name": "Bruce",
15+
"surname": "Wayne"
16+
},
17+
"tenant": "PAYPAL"
18+
},
19+
"billing_info": {
20+
"outstanding_balance": {
21+
"currency_code": "BRL",
22+
"value": "0.0"
23+
},
24+
"cycle_executions": [
25+
{
26+
"tenure_type": "REGULAR",
27+
"sequence": 1,
28+
"cycles_completed": 12,
29+
"cycles_remaining": 0,
30+
"total_cycles": 0
31+
}
32+
],
33+
"last_payment": {
34+
"amount": {
35+
"currency_code": "BRL",
36+
"value": "85.0"
37+
},
38+
"time": "2026-02-13T10:10:10Z"
39+
},
40+
"next_billing_time": "2026-03-13T10:00:00Z",
41+
"failed_payments_count": 0
42+
},
43+
"create_time": "2025-03-13T16:04:36Z",
44+
"update_time": "2026-02-13T10:10:12Z",
45+
"links": [
46+
{
47+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-BBBBBBBBBBBB/cancel",
48+
"rel": "cancel",
49+
"method": "POST"
50+
},
51+
{
52+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-BBBBBBBBBBBB",
53+
"rel": "edit",
54+
"method": "PATCH"
55+
},
56+
{
57+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-BBBBBBBBBBBB",
58+
"rel": "self",
59+
"method": "GET"
60+
},
61+
{
62+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-BBBBBBBBBBBB/suspend",
63+
"rel": "suspend",
64+
"method": "POST"
65+
},
66+
{
67+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-BBBBBBBBBBBB/capture",
68+
"rel": "capture",
69+
"method": "POST"
70+
}
71+
]
72+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"status": "ACTIVE",
3+
"status_update_time": "2026-02-12T10:28:06Z",
4+
"id": "I-AAAAAAAAAAAA",
5+
"start_time": "2020-01-12T02:00:00Z",
6+
"shipping_amount": {
7+
"currency_code": "USD",
8+
"value": "0.0"
9+
},
10+
"subscriber": {
11+
"email_address": "elvis.presley@example.com",
12+
"payer_id": "Q64J6VDR3DBHN",
13+
"name": {
14+
"given_name": "Elvis",
15+
"surname": "Presley"
16+
},
17+
"tenant": "PAYPAL",
18+
"shipping_address": {
19+
"address": {
20+
"address_line_1": "3734 Elvis Presley Boulevard",
21+
"admin_area_2": "Memphis",
22+
"admin_area_1": "TN",
23+
"postal_code": "38116",
24+
"country_code": "US"
25+
}
26+
}
27+
},
28+
"billing_info": {
29+
"outstanding_balance": {
30+
"currency_code": "USD",
31+
"value": "0.0"
32+
},
33+
"cycle_executions": [
34+
{
35+
"tenure_type": "REGULAR",
36+
"sequence": 1,
37+
"cycles_completed": 74,
38+
"cycles_remaining": 0,
39+
"total_cycles": 0
40+
}
41+
],
42+
"last_payment": {
43+
"amount": {
44+
"currency_code": "USD",
45+
"value": "50.0"
46+
},
47+
"time": "2026-02-12T10:28:05Z"
48+
},
49+
"next_billing_time": "2026-03-12T10:00:00Z",
50+
"failed_payments_count": 3
51+
},
52+
"create_time": "2020-01-12T14:23:37Z",
53+
"update_time": "2026-02-12T10:28:06Z",
54+
"links": [
55+
{
56+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-AAAAAAAAAAAA/cancel",
57+
"rel": "cancel",
58+
"method": "POST"
59+
},
60+
{
61+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-AAAAAAAAAAAA",
62+
"rel": "edit",
63+
"method": "PATCH"
64+
},
65+
{
66+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-AAAAAAAAAAAA",
67+
"rel": "self",
68+
"method": "GET"
69+
},
70+
{
71+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-AAAAAAAAAAAA/suspend",
72+
"rel": "suspend",
73+
"method": "POST"
74+
},
75+
{
76+
"href": "https://api.paypal.com/v1/billing/subscriptions/I-AAAAAAAAAAAA/capture",
77+
"rel": "capture",
78+
"method": "POST"
79+
}
80+
]
81+
}

0 commit comments

Comments
 (0)