Add a PayPal payment option to your page¶
Overview¶
This example shows how to process payments via PayPal. It contains a simplistic ShoppingCart which serves
to allow a user to select items to buy. This happens on the home page of the example. When the user is satisfied
with their selections, the user clicks on “Pay” which transitions them to another “Summary” page on which an overview
of the order is shown and where a PayPalButtonsPanel
is displayed.
Clicking on the PayPal button here ferries the user to PayPal and finally returns to the Summary page to show the status of the order after payment.
Create a ShoppingCart¶
Create a session_scoped()
ShoppingCart object to collect all the items you want to buy. It has a pay method which
creates a PayPalOrder
when called.
@session_scoped
class ShoppingCart(Base):
__tablename__ = 'paymentpaypal_shoppingcart'
id = Column(Integer, primary_key=True)
item_name = Column(UnicodeText)
quantity = Column(Integer)
price = Column(Numeric(precision=4, scale=2))
currency_code = Column(String(3))
paypal_order_id = Column(Integer, ForeignKey(PayPalOrder.id), nullable=True)
paypal_order = relationship(PayPalOrder)
fields = ExposedNames()
fields.item_name = lambda i: Field(label='Item name')
fields.quantity = lambda i: IntegerField(min_value=1, max_value=10, label='Quantity')
fields.price = lambda i: NumericField(min_value=1, label='Price')
fields.currency_code = lambda i: ChoiceField([Choice('USD', Field(label='USD')),
Choice('EUR', Field(label='EUR'))],
label='Currency')
events = ExposedNames()
events.pay_event = lambda i: Event(label='Pay', action=Action(i.pay))
events.clear_event = lambda i: Event(label='Continue shopping', action=Action(i.clear))
def pay(self):
json_dict = self.as_json()
print(json_dict)
order = PayPalOrder(json_string=json.dumps(json_dict))
Session.add(order)
self.paypal_order = order
def clear(self):
self.paypal_order = None
Create the order JSON¶
Also add an as_json method which represents your order as a dictionary that conforms to PayPal’s JSON specification for creating orders and with “intent” set to “CAPTURE”.
def as_json(self):
tax_rate = decimal.Decimal('0.15')
invoice_id = id(self)
item_price = self.price
item_tax = round(self.price * tax_rate, 2)
item_quantity = self.quantity
item_name = self.item_name
order_pre_tax_total = item_quantity*item_price
order_total_tax = item_quantity*item_tax
order_total_including_tax = item_quantity*(item_price+item_tax)
brand_name = 'My Example company'
return \
{
"intent": "CAPTURE",
"purchase_units": [
{
"description": item_name,
"invoice_id": invoice_id,
"soft_descriptor": brand_name,
"amount": {
"currency_code": self.currency_code,
"value": str(order_total_including_tax),
"breakdown": {
"item_total": {
"currency_code": self.currency_code,
"value": str(order_pre_tax_total)
},
"tax_total": {
"currency_code": self.currency_code,
"value": str(order_total_tax)
},
}
},
"items": [
{
"name": item_name,
"unit_amount": {
"currency_code": self.currency_code,
"value": str(item_price)
},
"tax": {
"currency_code": self.currency_code,
"value": str(item_tax)
},
"quantity": str(item_quantity),
"category": "DIGITAL_GOODS"
},
]
}
]
}
Create the PurchaseForm¶
Create a PurchaseForm class which is the user interface of your simple shopping cart. Wire its only button to the
ShoppingCart’s pay event so that a PayPalOrder
is created when clicked.
class PurchaseForm(Form):
def __init__(self, view, shopping_cart):
super().__init__(view, 'purchase')
self.use_layout(FormLayout())
if self.exception:
self.layout.add_alert_for_domain_exception(self.exception)
self.layout.add_input(TextInput(self, shopping_cart.fields.item_name))
self.layout.add_input(TextInput(self, shopping_cart.fields.quantity))
self.layout.add_input(TextInput(self, shopping_cart.fields.price))
self.layout.add_input(SelectInput(self, shopping_cart.fields.currency_code))
self.add_child(Button(self, shopping_cart.events.pay_event, style='primary'))
Create the PurchaseSummary¶
Create a Form to display the final order details. This Widget
should contain a PayPalButtonsPanel
, but only if the order
is ready to be paid. The same Widget
is again displayed upon return from PayPal, and in that case only display the order
success message.
class PurchaseSummary(Form):
def __init__(self, view, shopping_cart):
super().__init__(view, 'summary')
self.use_layout(FormLayout())
paypal_order = shopping_cart.paypal_order
if self.exception:
self.layout.add_alert_for_domain_exception(self.exception)
self.add_child(P(view, text='%s: %s' % (shopping_cart.fields.item_name.label, shopping_cart.item_name)))
self.add_child(P(view, text='%s: %s' % (shopping_cart.fields.quantity.label, shopping_cart.quantity)))
self.add_child(P(view, text='%s: %s' % (shopping_cart.fields.price.label, shopping_cart.price)))
self.add_child(P(view, text='%s: %s' % (shopping_cart.fields.currency_code.label, shopping_cart.currency_code)))
self.add_child(Alert(view, 'Your order(%s) status: %s (%s)' % (paypal_order.id, paypal_order.status, paypal_order.paypal_id), 'secondary'))
if paypal_order.is_due_for_payment:
credentails = self.get_credentials_from_configuration()
#credentails = self.get_credentials_hardcoded()
self.add_child(PayPalButtonsPanel(view, 'paypal_buttons', shopping_cart.paypal_order, credentails, shopping_cart.currency_code))
elif paypal_order.is_paid:
self.add_child(Alert(view, 'Your order has been paid successfully', 'primary'))
self.add_child(Button(self, shopping_cart.events.clear_event, style='primary'))
else:
self.add_child(Alert(view, 'We could not complete your payment at PayPal (Order status: %s)' % paypal_order.status, 'warning'))
def get_credentials_hardcoded(self):
return PayPalClientCredentials('test', 'invalidpassword', True)
Note
Notice that in this example is a call to get_credentials_from_configuration, and a commented-out call to get_credentials_hardcoded.
For now, comment out the call to get_credentials_from_configuration and use the one to get_credentials_hardcoded.
Combine it all in a UserInterface
¶
Add Purchaseform to your UserInterface
on the ‘/’ UrlBoundView
and PurchaseSummary on the ‘/order_summary’ UrlBoundView
.
Then define Transition
s between the two.
There is no need to add a Transition
to PayPal.
class ShoppingUI(UserInterface):
def assemble(self):
shopping_cart = ShoppingCart.for_current_session()
home = self.define_view('/', title='Paypal Example')
home.set_slot('main', PurchaseForm.factory(shopping_cart))
order_summary_page = self.define_view('/order_summary', title='Order Summary')
order_summary_page.set_slot('main', PurchaseSummary.factory(shopping_cart))
self.define_transition(shopping_cart.events.pay_event, home, order_summary_page)
self.define_transition(shopping_cart.events.clear_event, order_summary_page, home)
self.define_page(MainPage)
Obtain sandbox credentials at PayPal¶
At this point (provided you commented out the use of get_credentials_from_configuration) you can run the example up to a point.
To be able to interact with PayPal, register as a developer at PayPal
Create Configuration for your PayPal merchant account¶
Create a Configuration
for your project in which you can supply the credentials for interacting with PayPal while avoiding
hardcoding these in the source code of your app.
class PaymentPayPalConfig(Configuration):
filename = 'paymentpaypal.config.py'
config_key = 'paymentpaypal'
client_id = ConfigSetting(description='The PayPal client ID that needs to receive funds from payments')
client_secret = ConfigSetting(description='The PayPal client secret')
sandboxed = ConfigSetting(default=True, description='If False, use the PayPal live environment instead of sandboxed', dangerous=True)
Remember to also include this Configuration
in your pyproject.toml:
[build-system]
requires = [
"setuptools >= 68",
"wheel",
"setuptools-git >= 1.1",
"pytest-runner",
"toml",
"reahl-component-metadata >= 7.0.0"
]
build-backend = "setuptools.build_meta"
[project]
name = "paymentpaypal"
version = "0.1"
requires-python = ">=3.8"
dependencies = [
"reahl-web>=7.0,<7.1",
"reahl-component>=7.0,<7.1",
"reahl-sqlalchemysupport>=7.0,<7.1",
"reahl-sqlitesupport>=7.0,<7.1",
"reahl-web-declarative>=7.0,<7.1",
"reahl-domain>=7.0,<7.1",
"reahl-paypalsupport>=7.0,<7.1",
"SQLAlchemy>=1.4,<2.1.999"
]
authors = [
{name = "Iwan Vosloo", email = "[email protected]"},
{name = "Craig Sparks", email = "[email protected]"}
]
[tool.setuptools]
py-modules = ["paymentpaypal"]
[tool.setuptools.packages.find]
exclude = ["etc", "build", "dist"]
[tool.reahl-component]
configuration = "reahl.doc.examples.howtos.paymentpaypal.paymentpaypal:PaymentPayPalConfig"
persisted = [
"reahl.doc.examples.howtos.paymentpaypal.paymentpaypal:ShoppingCart"
]
Create a paymentpaypal.config.py file in your configuration directory, with the necessary settings:
#Read here to create a paypal sandbox account:
# https://developer.paypal.com/docs/api/overview/#create-sandbox-accounts
import os
paymentpaypal.client_id = os.environ['PAYPAL_CLIENT_ID']
paymentpaypal.client_secret = os.environ['PAYPAL_CLIENT_SECRET']
paymentpaypal.sandboxed = True
Note
Here, the config file is coded to read the settings from environment variables to make it easy for us to test, but you can hard-code the settings for yourself in the config file.
Update PurchaseSummary to read this configuration¶
In PurchaseSummary, write get_credentials_from_configuration to read the config and return a PayPalClientCredentials
object
with the result. (Remember to also update the commented-out code to call this method.)
def get_credentials_from_configuration(self):
config = ExecutionContext.get_context().config
paypal_client_id = config.paymentpaypal.client_id
paypal_client_secret = config.paymentpaypal.client_secret
sandboxed = config.paymentpaypal.sandboxed
return PayPalClientCredentials(paypal_client_id, paypal_client_secret, sandboxed)
Congratulations!¶
You should now be able to make payments to the PayPal sandbox environment using the example.