Skip to content
This repository has been archived by the owner on Apr 19, 2022. It is now read-only.

Commit

Permalink
Update python server and READMEs.
Browse files Browse the repository at this point in the history
  • Loading branch information
thorsten-stripe committed Feb 28, 2019
1 parent 51608dd commit 9c165a7
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 148 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ STRIPE_WEBHOOK_SECRET=
# Stripe account country (required for Payment Request).
STRIPE_ACCOUNT_COUNTRY=US

# Supported payment methods for the store.
# Some payment methods support only a subset of currencies.
# Make sure to check the docs: https://stripe.com/docs/sources
# Only used for the python server. For Node.js see the server/node/config.js file!
PAYMENT_METHODS="alipay, bancontact, card, eps, ideal, giropay, multibanco, sofort, wechat"

# Optional ngrok configuration for development (if you have a paid ngrok account).
NGROK_SUBDOMAIN=
NGROK_AUTHTOKEN=
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This demo provides an all-in-one example for integrating with Stripe on the web:
🚀 | **Built-in proxy for local HTTPS and webhooks.** Card payments require HTTPS and asynchronous payment methods with redirects rely on webhooks to complete transactions—[ngrok](https://ngrok.com/) is integrated so the app is served locally over HTTPS and an endpoint is publicly exposed for webhooks.
🔧 | **Webhook signing and idempotency keys**. We verify webhook signatures and pass idempotency keys to charge creations, two recommended practices for asynchronous payment flows.
📱 | **Responsive design**. The checkout experience works on all screen sizes. Apple Pay works on Safari for iPhone and iPad if the Wallet is enabled, and Payment Request works on Chrome for Android.
📦 | **No datastore required.** Products, SKUs, and Orders are stored using the [Stripe Orders API](https://stripe.com/docs/orders), which you can replace with your own database to keep track of orders and inventory.
📦 | **No datastore required.** Products, and SKUs are stored using the [Stripe API](https://stripe.com/docs/api/products), which you can replace with your own database to keep track of products and inventory.

## Payments Integration

Expand Down Expand Up @@ -67,9 +67,9 @@ There are a couple server implementations in the [`server`](/server) directory.

You’ll need the following:

* [Node.js](http://nodejs.org) >= 8.x.
* Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay).
* Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free).
- [Node.js](http://nodejs.org) >= 8.x.
- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay).
- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free).

In your Stripe Dashboard, you can [enable the payment methods](https://dashboard.stripe.com/payments/settings) you’d like to test with one click.

Expand All @@ -87,7 +87,7 @@ Install dependencies using npm:

npm install

This demo uses the Stripe API as a datastore for products and orders, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products within Stripe.
This demo uses the Stripe API as a datastore for products and SKUs, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products and SKUs within Stripe.

Run the app:

Expand All @@ -111,5 +111,5 @@ Use this second URL in your browser to start the demo.

## Credits

* Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/thorwebdev)
* Design: [Tatiana Van Campenhout](https://twitter.com/tatsvc)
- Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/thorwebdev)
- Design: [Tatiana Van Campenhout](https://twitter.com/tatsvc)
10 changes: 5 additions & 5 deletions server/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ This directory contains the main Node implementation of the payments server.

You’ll need the following:

* [Node.js](http://nodejs.org) >= 8.x.
* Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay).
* Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free).
- [Node.js](http://nodejs.org) >= 8.x.
- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay).
- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free).

In your Stripe Dashboard, you can [enable the payment methods](https://dashboard.stripe.com/payments/settings) you’d like to test with one click.

Expand All @@ -26,7 +26,7 @@ Install dependencies using npm:

npm install

This demo uses the Stripe API as a datastore for products and orders, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products within Stripe.
This demo uses the Stripe API as a datastore for products and SKUs, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products and SKUs within Stripe.

Run the app:

Expand All @@ -50,4 +50,4 @@ Use this second URL in your browser to start the demo.

## Credits

* Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/schaeff_t)
- Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/schaeff_t)
18 changes: 9 additions & 9 deletions server/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ This demo uses a simple [Flask](http://flask.pocoo.org/) application as the serv

## Payments Integration

* [`app.py`](app.py) contains the routes that interface with Stripe to create charges and receive webhook events.
* [`setup.py`](setup.py) a simple setup script to make some fake Products and SKUs for our Stripe store.
* [`tests/tests.py`](tests/tests.py) some unit tests that test the logic of our heavier APIs like `orders/<string:id>/pay` and `/webhook`.
* [`test_data.py`](tests/tests.py) contains some hardcoded mocked responses to test with.
* [`inventory_manager.py`](stripe_lib.py) a minimal wrapper over the Stripe Python SDK that handles creating/fetching orders and products. You can override this class with your own order management system code.
- [`app.py`](app.py) contains the routes that interface with Stripe to create charges and receive webhook events.
- [`setup.py`](setup.py) a simple setup script to make some fake Products and SKUs for our Stripe store.
- [`tests/tests.py`](tests/tests.py) some unit tests that test the logic of our heavier APIs like `/webhook`.
- [`test_data.py`](tests/tests.py) contains some hardcoded mocked responses to test with.
- [`inventory_manager.py`](stripe_lib.py) a minimal wrapper over the Stripe Python SDK that handles creating/fetching products and caluclating payment amounts from SKUs. You can override this class with your own product and order management system code.

## Requirements

You’ll need the following:

* [Python 3.6.5](https://www.python.org/downloads/release/python-365/)
* Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay).
* Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free!)
- [Python 3.6.5](https://www.python.org/downloads/release/python-365/)
- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay).
- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free!)

## Getting Started

Expand Down Expand Up @@ -87,4 +87,4 @@ python tests.py

## Credits

* Code: [Adrienne Dreyfus](http://twitter.com/adrind)
- Code: [Adrienne Dreyfus](http://twitter.com/adrind)
141 changes: 53 additions & 88 deletions server/python/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Stripe Payments Demo. Created by Adrienne Dreyfus (@adrind).
This is our Flask server that handles requests from our Stripe checkout flow.
It has all the endpoints you need to accept payments and manage orders.
It has all the endpoints you need to accept payments.
Python 3.6 or newer required.
"""
Expand All @@ -16,7 +16,7 @@
import os

from inventory import Inventory
from stripe_types import Source, Order
from stripe_types import Source
from flask import Flask, render_template, jsonify, request, send_from_directory
from dotenv import load_dotenv, find_dotenv

Expand Down Expand Up @@ -53,7 +53,8 @@ def get_config():
'stripePublishableKey': os.getenv('STRIPE_PUBLISHABLE_KEY'),
'stripeCountry': os.getenv('STRIPE_ACCOUNT_COUNTRY') or 'US',
'country': 'US',
'currency': 'eur'
'currency': 'eur',
'paymentMethods': os.getenv('PAYMENT_METHODS').split(', ') or ['card']
})


Expand All @@ -74,51 +75,37 @@ def retrieve_product(product_id):
return jsonify(Inventory.retrieve_product(product_id))


@app.route('/orders', methods=['POST'])
def make_order():
# Creates a new Order with items that the user selected.
@app.route('/payment_intents', methods=['POST'])
def make_payment_intent():
# Creates a new PaymentIntent with items from the cart.
data = json.loads(request.data)
try:
order = Inventory.create_order(currency=data['currency'], items=data['items'], email=data['email'],
shipping=data['shipping'], create_intent=data['createIntent'])

return jsonify({'order': order})
payment_intent = stripe.PaymentIntent.create(
amount=Inventory.calculate_payment_amount(items=data['items']),
currency=data['currency'],
payment_method_types=os.getenv(
'PAYMENT_METHODS').split(', ') or ['card']
)

return jsonify({'paymentIntent': payment_intent})
except Exception as e:
return jsonify(e), 403


@app.route('/orders/<string:order_id>/pay', methods=['POST'])
def pay_order(order_id):
"""
Creates a Charge for an Order using a payment Source provided by the user.
"""

@app.route('/payment_intents/<string:id>/shipping_change', methods=['POST'])
def update_payment_intent(id):
data = json.loads(request.data)
source = data['source']

order = Inventory.retrieve_order(order_id)

if order['metadata']['status'] == 'pending' or order['metadata']['status'] == 'paid':
# Somehow this Order has already been paid for -- abandon request.
return jsonify({'source': source, 'order': order}), 403

if source['status'] == 'chargeable':
# Yay! Our user gave us a valid payment Source we can charge.
charge = stripe.Charge.create(source=source['id'], amount=order['amount'], currency=order['currency'],
receipt_email=order['email'], idempotency_key=order['id'])

if charge and charge['status'] == 'succeeded':
status = 'paid'
elif charge and 'status' in charge:
status = charge['status']
else:
status = 'failed'

# Update the Order with a new status based on what happened with the Charge.
Inventory.update_order(
properties={'metadata': {'status': status}}, order=order)
amount = Inventory.calculate_payment_amount(items=data['items'])
amount += Inventory.get_shipping_cost(data['shippingOption']['id'])
try:
payment_intent = stripe.PaymentIntent.modify(
id,
amount=amount
)

return jsonify({'order': order, 'source': source})
return jsonify({'paymentIntent': payment_intent})
except Exception as e:
return jsonify(e), 403


@app.route('/webhook', methods=['POST'])
Expand Down Expand Up @@ -146,9 +133,8 @@ def webhook_received():

# PaymentIntent Beta, see https://stripe.com/docs/payments/payment-intents
# Monitor payment_intent.succeeded & payment_intent.payment_failed events.
if data_object['object'] == 'payment_intent' and 'order' in data_object['metadata']:
if data_object['object'] == 'payment_intent':
payment_intent = data_object
order = stripe.Order.retrieve(payment_intent['metadata']['order'])

if event_type == 'payment_intent.succeeded':
print('🔔 Webhook received! Payment for PaymentIntent ' +
Expand All @@ -160,61 +146,40 @@ def webhook_received():
# Monitor `source.chargeable` events.
if data_object['object'] == 'source' \
and data_object['status'] == 'chargeable' \
and 'order' in data_object['metadata']:
and 'paymentIntent' in data_object['metadata']:
source = data_object
print(f'Webhook received! The source {source["id"]} is chargeable')
print(f'🔔 Webhook received! The source {source["id"]} is chargeable')

# Find the corresponding Order this Source is for by looking in its metadata.
order = Inventory.retrieve_order(source['metadata']['order'])
# Find the corresponding PaymentIntent this Source is for by looking in its metadata.
payment_intent = stripe.PaymentIntent.retrieve(
source['metadata']['paymentIntent'])

# Verify that this Order actually needs to be paid.
order_status = order['metadata']['status']
if order_status in ['pending', 'paid', 'failed']:
return jsonify({'error': f'Order already has a status of {order_status}'}), 403
# Verify that this PaymentIntent actually needs to be paid.
if payment_intent['status'] != 'requires_payment_method':
return jsonify({'error': f'PaymentIntent already has a status of {payment_intent["status"]}'}), 403

# Create a Charge to pay the Order using the Source we just received.
try:
charge = stripe.Charge.create(source=source['id'], amount=order['amount'], currency=order['currency'],
receipt_email=order['email'], idempotency_key=order['id'])

if charge and charge['status'] == 'succeeded':
status = 'paid'
elif charge:
status = charge['status']
else:
status = 'failed'

except stripe.error.CardError:
# This is where you handle declines and errors.
# For the demo, we simply set the status to mark the Order as failed.
status = 'failed'

Inventory.update_order(
properties={'metadata': {'status': status}}, order=order)

# Monitor `charge.succeeded` events.
if data_object['object'] == 'charge' \
and data_object['status'] == 'succeeded' \
and 'order' in data_object['source']['metadata']:
charge = data_object
print(f'Webhook received! The charge {charge["id"]} succeeded.')
Inventory.update_order(properties={'metadata': {'status': 'paid'}},
order_id=charge['source']['metadata']['order'])

# Monitor `source.failed`, `source.canceled`, and `charge.failed` events.
if data_object['object'] in ['source', 'charge'] and data_object['status'] in ['failed', 'canceled']:
source = data_object['source'] if data_object['source'] else data_object
print(f'Webhook received! Failure for {data_object["id"]}.`')

if source['metadata']['order']:
Inventory.update_order(properties={'metadata': {'status': 'failed'}},
order_id=source['metadata']['order']['id'])
# Confirm the PaymentIntent with the chargeable source.
payment_intent.confirm(source=source['id'])

# Monitor `source.failed` and `source.canceled` events.
if data_object['object'] == 'source' and data_object['status'] in ['failed', 'canceled']:
# Cancel the PaymentIntent.
source = data_object
intent = stripe.PaymentIntent.retrieve(
source['metadata']['paymentIntent'])
intent.cancel()

return jsonify({'status': 'success'})


@app.route('/payment_intents/<string:id>/status', methods=['GET'])
def retrieve_payment_intent_status(id):
payment_intent = stripe.PaymentIntent.retrieve(id)
return jsonify({'paymentIntent': {'status': payment_intent["status"]}})


if __name__ == '__main__':
load_dotenv(find_dotenv())
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
stripe.api_version = '2018-02-06'
stripe.api_version = '2019-02-11'
app.run()
54 changes: 21 additions & 33 deletions server/python/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,49 @@
inventory.py
Stripe Payments Demo. Created by Adrienne Dreyfus (@adrind).
Simple library to store and interact with orders and products.
These methods are using the Stripe Orders API, but we tried to abstract them
from the main code if you'd like to use your own order management system instead.
Simple library to store and interact with products and SKUs.
These methods are using the Stripe Product API, but we tried to abstract them
from the main code if you'd like to use your own product and order management system instead.
"""

import stripe
import os
from functools import reduce
from stripe_types import Order, Product
from stripe_types import Product
from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
stripe.api_version = '2018-02-06'
stripe.api_version = '2019-02-11'


class Inventory:
@staticmethod
def create_order(currency: str, items: list, email: str, shipping: dict, create_intent: bool) -> Order:
order = stripe.Order.create(currency=currency, items=items,
email=email, shipping=shipping, metadata={'status': 'created'})
if create_intent:
# Create PaymentIntent to represent customers intent to pay this order.
# Note: PaymentIntents currently only support card sources to enable dynamic authentication:
# https://stripe.com/docs/payments/dynamic-authentication
payment_intent = stripe.PaymentIntent.create(
amount=order['amount'], currency=currency, metadata={'order': order['id']}, allowed_source_types=['card'])
# Add PaymentIntent to order object so our frontend can access the client_secret.
# The client_secret is used on the frontend to confirm the PaymentIntent and create a payment.
# Therefore, do not log, store, or append the client_secret to a URL.
order['paymentIntent'] = payment_intent
return order

@staticmethod
def retrieve_order(order_id: str) -> Order:
return stripe.Order.retrieve(order_id)
def calculate_payment_amount(items: list) -> int:
product_list = stripe.Product.list(
limit=3, stripe_version='2018-02-28')
product_list_data = product_list['data']
total = 0
for item in items:
sku_id = item['parent']
product = next(
filter(lambda p: p['skus']['data'][0]['id'] == sku_id, product_list_data))
total += (product['skus']['data'][0]['price'] * item['quantity'])
return total

@staticmethod
def update_order(properties: dict, order: Order = None, order_id: str = None) -> Order:
if not order:
if not order_id:
print('Error when fetching order -- no id or object given')
order = Inventory.retrieve_order(order_id)

order.update(properties)
return order
def get_shipping_cost(id) -> int:
shipping_cost = {'free': 0, 'express': 500}
return shipping_cost[id]

@staticmethod
def list_products() -> [Product]:
return stripe.Product.list(limit=3)
return stripe.Product.list(limit=3, stripe_version='2018-02-28')

@staticmethod
def retrieve_product(product_id) -> Product:
return stripe.Product.retrieve(product_id)
return stripe.Product.retrieve(product_id, stripe_version='2018-02-28')

@staticmethod
def products_exist(product_list: [Product]) -> bool:
Expand Down
Loading

0 comments on commit 9c165a7

Please sign in to comment.