Skip to content

Commit ebe6d42

Browse files
authored
feat: Implement PayPal checkout (#165)
* Implement payment provider `PayPalCheckout` * Add PayPal SDK as `paypal` extra * Generalize some methods in `PaymentProviderAbstract`
1 parent 9f965df commit ebe6d42

File tree

16 files changed

+475
-71
lines changed

16 files changed

+475
-71
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ version = { attr = "viur.shop.version.__version__" }
4242
unzer = [
4343
"unzer~=1.3",
4444
]
45+
paypal = [
46+
"paypal-server-sdk~=1.0",
47+
]
4548

4649
[tool.setuptools.packages.find]
4750
where = ["src"]

src/viur/shop/data/translations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@
105105
"_default_text": "Payment in advance",
106106
"de": "Bezahlen über Vorkasse",
107107
},
108+
"viur.shop.payment_provider.paypal_checkout": {
109+
"_default_text": "PayPal",
110+
"de": "Bezahlen über PayPal",
111+
},
108112

109113
# --- Enums ---------------------------------------------------------------
110114
# Availability

src/viur/shop/globals.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
"""
2+
Global constants and settings for the viur-shop.
3+
"""
4+
15
import logging
26
import typing as t
37

src/viur/shop/payment_providers/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from .abstract import PaymentProviderAbstract
22
from .amazon_pay import AmazonPay
33
from .invoice import Invoice
4-
from .paypal_plus import PayPalPlus
54
from .prepayment import PrePayment, Prepayment
65

76
try:
@@ -20,3 +19,12 @@
2019
from .unzer_paylater_invoice import UnzerPaylaterInvoice
2120
from .unzer_paypal import UnzerPayPal
2221
from .unzer_sofort import UnzerSofort
22+
23+
try:
24+
import paypalserversdk
25+
except ImportError:
26+
# The paypal extra was not enabled, we don't import the related providers
27+
...
28+
else:
29+
del paypalserversdk
30+
from .paypal_checkout import PayPalCheckout

src/viur/shop/payment_providers/abstract.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import abc
2+
import enum
23
import functools
34
import uuid
45

5-
from viur import toolkit
6-
from viur.core import Module, translate, utils
6+
from viur.core import CallDeferred, Module, current, db, translate, utils
77
from viur.core.prototypes.instanced_module import InstancedModule
88
from viur.core.skeleton import SkeletonInstance
9+
10+
from viur import toolkit
911
from viur.shop.skeletons.order import OrderSkel
1012
from ..types import *
1113

@@ -58,10 +60,14 @@ def description(self) -> translate:
5860
"""Define the description of the payment provider"""
5961
return translate(f"viur.shop.payment_provider.{self.name}.descr", self.name)
6062

63+
# --- Internal Checks & Actions during the payment flow -------------------
64+
# --- (controlled by the order module) ------------------------------------
65+
6166
def is_available(
6267
self: t.Self,
6368
order_skel: SkeletonInstance_T[OrderSkel] | None,
6469
) -> bool:
70+
"""Decide whether the payment provider is available."""
6571
return True
6672

6773
def can_checkout(
@@ -113,25 +119,52 @@ def check_payment_state(
113119
"""
114120
...
115121

122+
@CallDeferred
123+
# @log_unzer_error
124+
def check_payment_deferred(self, order_key: db.Key) -> None:
125+
"""Check the status for a payment deferred"""
126+
logger.debug(f"Checking payment for {order_key=!r} deferred")
127+
order_skel = self.shop.order.skel().read(order_key)
128+
logger.debug(f"Checking payment for {order_skel=!r} deferred")
129+
# TODO: duplicate check / code?
130+
is_paid, payment = self.check_payment_state(order_skel)
131+
if is_paid and order_skel["is_paid"]:
132+
logger.info(f'Order {order_skel["key"]!r} already marked as paid. Nothing to do.')
133+
elif is_paid:
134+
logger.info(f'Mark order {order_skel["key"]!r} as paid')
135+
self.shop.order.set_paid(order_skel)
136+
else:
137+
logger.info(f'Order {order_skel["key"]!r} is not paid')
138+
139+
# --- API Endpoints ------------------------------------------------------
140+
116141
@abc.abstractmethod
117142
# @exposed
118143
def return_handler(self):
144+
"""Frontend Endpoint where the might be redirected to by the payment provider during the payment flow"""
119145
...
120146

121147
@abc.abstractmethod
122148
# @exposed
123149
def webhook(self):
150+
"""API Endpoint (Webhook) to listen for events from payment provider"""
124151
...
125152

126153
@abc.abstractmethod
127154
# @exposed
128155
def get_debug_information(self):
156+
"""Provide information about the payment of an order.
157+
158+
Only for debugging purposes. It's not an API endpoint.
159+
"""
129160
...
130161

162+
# --- utils ---------------------------------------------------------------
163+
131164
def _append_payment_to_order_skel(
132165
self,
133166
order_skel: SkeletonInstance_T[OrderSkel],
134-
payment: dict[str, t.Any] | None = None,
167+
payment: PaymentTransactionSpecific | None = None,
135168
) -> SkeletonInstance_T[OrderSkel]:
136169
"""Append payment data to an order
137170
@@ -147,8 +180,10 @@ def set_payment(skel: SkeletonInstance):
147180
"payment_provider": self.name,
148181
"creationdate": utils.utcNow().isoformat(),
149182
"uuid": str(uuid.uuid4()),
183+
"client_ip": current.request.get().request.client_addr,
184+
"user_agent": current.request.get().request.user_agent,
150185
}
151-
| (payment or {})
186+
| (payment or {}) # type: PaymentTransaction
152187
)
153188

154189
order_skel = toolkit.set_status(
@@ -162,7 +197,7 @@ def serialize_for_api(
162197
self,
163198
order_skel: SkeletonInstance_T[OrderSkel] | None,
164199
) -> PaymentProviderResult:
165-
"""Serialize this Payment Provder for the API
200+
"""Serialize this Payment Provider for the API
166201
167202
Used by :meth:`Order.get_payment_providers` and :meth:`Order.payment_providers_list`
168203
Can be subclasses to expose more information via API.
@@ -174,5 +209,17 @@ def serialize_for_api(
174209
is_available=self.is_available(order_skel),
175210
)
176211

212+
@classmethod
213+
def model_to_dict(cls, obj: t.Any) -> t.Any:
214+
"""Convert any nested model to a JSON-compatible representation
215+
"""
216+
if isinstance(obj, dict):
217+
return {k: cls.model_to_dict(v) for k, v in obj.items()}
218+
elif isinstance(obj, list | tuple):
219+
return [cls.model_to_dict(v) for v in obj]
220+
elif isinstance(obj, enum.Enum):
221+
return f"{obj!r}"
222+
return obj
223+
177224

178225
PaymentProviderAbstract.html = True

src/viur/shop/payment_providers/invoice.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from viur.core import errors, exposed
44
from viur.core.skeleton import SkeletonInstance
5+
56
from . import PaymentProviderAbstract
67
from ..globals import SHOP_LOGGER
78
from ..skeletons import OrderSkel

0 commit comments

Comments
 (0)