stripe payment processor

This commit is contained in:
2020-05-12 12:38:36 -05:00
parent d293d43392
commit 08e23cf0d1
12 changed files with 264 additions and 11 deletions

View File

@ -1,3 +1,4 @@
import stripe
import os
from dotenv import load_dotenv, find_dotenv
@ -23,11 +24,15 @@ app.config.from_mapping(
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""),
MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="forest@nullhex.com"),
STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default=""),
STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default="2020-03-02"),
STRIPE_SECRET_KEY=os.environ.get("STRIPE_SECRET_KEY", default=""),
STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default="")
#STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="")
)
stripe.api_key = app.config['STRIPE_SECRET_KEY']
stripe.api_version = app.config['STRIPE_API_VERSION']
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
@ -35,11 +40,12 @@ from capsulflask import db
db.init_app(app)
from capsulflask import auth, landing, console
from capsulflask import auth, landing, console, stripe
app.register_blueprint(landing.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(console.bp)
app.register_blueprint(stripe.bp)
app.add_url_rule("/", endpoint="index")

View File

@ -5,8 +5,6 @@ from flask import Blueprint
from flask import flash
from flask import current_app
from flask import g
from flask import redirect
from flask import url_for
from flask import request
from flask import session
from flask import render_template

View File

@ -23,7 +23,7 @@ class DBModel:
return token
def consume_token(self, token):
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s", (token, ))
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, ))
rows = self.cursor.fetchall()
if len(rows) > 0:
email = rows[0][0]
@ -150,4 +150,35 @@ class DBModel:
lambda x: dict(dollars=x[0], created=x[1]),
self.cursor.fetchall()
))
def create_stripe_checkout_session(self, id, email, dollars):
self.cursor.execute("""
INSERT INTO stripe_checkout_sessions (id, email, dollars)
VALUES (%s, %s, %d)
""",
(id, email, dollars)
)
self.connection.commit()
def consume_stripe_checkout_session(self, id, dollars):
self.cursor.execute("SELECT email, dollars FROM stripe_checkout_sessions WHERE id = %s", (id,))
rows = self.cursor.fetchall()
if len(rows) > 0:
if int(rows[0][1]) != int(dollars):
print(f"""
Stripe sent us a completed checkout session with a different dollar amount than what we had recorded!!
stripe_checkout_session_id: {id}
account: {rows[0][0]}
Recorded amount: {int(rows[0][1])}
Stripe sent: {int(dollars)}
""")
# not sure what to do here. For now just log and do nothing.
self.cursor.execute( "DELETE FROM stripe_checkout_sessions WHERE id = %s", (id,) )
self.cursor.execute( "INSERT INTO payments (email, dollars) VALUES (%s, %d)", (rows[0][0], rows[0][1]) )
self.connection.commit()
return rows[0][0]
else:
return None

View File

@ -62,6 +62,13 @@ CREATE TABLE login_tokens (
PRIMARY KEY (email, created)
);
CREATE TABLE stripe_checkout_sessions (
id TEXT PRIMARY KEY,
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL DEFAULT NOW(),
dollars NUMERIC(8, 2) NOT NULL
);
INSERT INTO os_images (id, template_image_file_name, description)
VALUES ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'),
('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic Beaver)'),

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -122,7 +122,7 @@ select {
padding-right: 2em;
}
input[type=text], textarea {
input[type=text], input[type=number], textarea {
font: calc(0.40rem + 1vmin) monospace;
border: 1px solid #777e73;
outline: 0;
@ -152,6 +152,17 @@ input[type=submit], select {
cursor: pointer;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
h1, h2, h3, h4, h5 {
font-size:calc(0.40rem + 1vmin);
margin: initial;
@ -195,7 +206,7 @@ td {
.code {
display: inline-block;
padding: 0.5em 2em;
padding: 0.5em 1.2em;
border-radius: 0.5em;
border: 1px solid #777e73;
background: #bdc7b810;

View File

@ -1 +1,116 @@
import stripe
import stripe
import json
import decimal
from flask import Blueprint
from flask import request
from flask import current_app
from flask import session
from flask import redirect
from flask import url_for
from flask import jsonify
from flask import flash
from flask import render_template
from werkzeug.exceptions import abort
from capsulflask.auth import account_required
from capsulflask.db import get_model
bp = Blueprint("stripe", __name__, url_prefix="/stripe")
@bp.route("/", methods=("GET", "POST"))
@account_required
def index():
stripe_checkout_session_id=None
if request.method == "POST":
errors = list()
if "dollars" not in request.form:
errors.append("dollars is required")
elif decimal.Decimal(request.form["dollars"]) < decimal.Decimal(1):
errors.append("dollars must be >= 1")
if len(errors) == 0:
print(f"creating stripe checkout session for {session['account']}, ${request.form['dollars']}")
checkout_session = stripe.checkout.Session.create(
success_url=current_app.config['BASE_URL'] + "/stripe/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url=current_app.config['BASE_URL'] + "/stripe",
payment_method_types=["card"],
customer_email=session["account"],
line_items=[
{
"name": "Capsul Cloud Compute",
"images": [current_app.config['BASE_URL']+"/static/capsul-product-image.png"],
"quantity": 1,
"currency": "usd",
"amount": request.form["dollars"]
}
]
)
stripe_checkout_session_id = checkout_session['id']
print(f"stripe_checkout_session_id={stripe_checkout_session_id} ( {session['account']}, ${request.form['dollars']} )")
get_model().create_stripe_checkout_session(stripe_checkout_session_id, session["account"], request.form["dollars"])
for error in errors:
flash(error)
return render_template(
"stripe.html",
stripe_checkout_session_id=stripe_checkout_session_id,
stripe_public_key=current_app.config["STRIPE_PUBLISHABLE_KEY"]
)
@bp.route("/success", methods=("GET",))
def success():
stripe_checkout_session_id = request.args.get('session_id')
if not stripe_checkout_session_id:
print("/stripe/success returned 400: missing required URL parameter session_id")
abort(400, "missing required URL parameter session_id")
else:
checkout_session = stripe.checkout.Session.retrieve(stripe_checkout_session_id)
if checkout_session and 'display_items' in checkout_session:
dollars = checkout_session['display_items'][0]['amount']
#consume_stripe_checkout_session deletes the checkout session row and inserts a payment row
# its ok to call consume_stripe_checkout_session more than once because it only takes an action if the session exists
success_account = get_model().consume_stripe_checkout_session(stripe_checkout_session_id, dollars)
if success_account:
print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
return redirect(url_for("console.account_balance"))
# I don't think the webhook is needed
# @bp.route("/webhook", methods=("POST",))
# def webhook():
# request_data = json.loads(request.data)
# signature = request.headers.get('stripe-signature')
# try:
# event = stripe.Webhook.construct_event(
# payload=request_data,
# sig_header=signature,
# secret=current_app.config['STRIPE_WEBHOOK_SECRET']
# )
# if event['type'] == 'checkout.session.completed':
# dollars = event['data']['object']['display_items'][0]['amount']
# stripe_checkout_session_id = event['data']['object']['id']
# #consume_stripe_checkout_session deletes the checkout session row and inserts a payment row
# # its ok to call consume_stripe_checkout_session more than once because it only takes an action if the session exists
# get_model().consume_stripe_checkout_session(stripe_checkout_session_id, dollars)
# return jsonify({'status': 'success'})
# except ValueError as e:
# print("/stripe/webhook returned 400: bad request", e)
# abort(400, "bad request")
# except stripe.error.SignatureVerificationError:
# print("/stripe/webhook returned 400: invalid signature")
# abort(400, "invalid signature")

View File

@ -38,10 +38,10 @@
<h1>PAYMENT OPTIONS</h1>
<ul>
<li>
<a href="/console/stripe">Add funds with Credit/Debit (stripe)</a>
<a href="/stripe">Add funds with Credit/Debit (stripe)</a>
<ul><li>notice: stripe will load nonfree javascript </li></ul>
</li>
<li><a href="/console/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li>
<li><a href="/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li>
<li>Cash: email treasurer@cyberia.club</li>
</ul>

View File

@ -5,6 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="Description" content="Cyberia Capsul">
{% block head %}{% endblock %}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>

View File

@ -0,0 +1,39 @@
{% extends 'base.html' %}
{% block title %}Capsuls{% endblock %}
{% block head %}<script src="https://js.stripe.com/v3/"></script>{% endblock %}
{% block content %}
<div class="third-margin">
<h1>PAY WITH STRIPE</h1>
</div>
<div class="row half-margin">
<form method="post">
<div class="row justify-start">
<label for="content">$</label>
<input type="number" id="name" name="name"></input>
</div>
<div class="row justify-end">
<input type="submit" value="Pay With Stripe">
</div>
</form>
</div>
{% if stripe_checkout_session_id %}
<script>
Stripe("{{ stripe_public_key }}")
.redirectToCheckout({
sessionId: "{{ stripe_checkout_session_id }}",
})
.then(function(result) {
if (result.error) {
alert("Stripe.redirectToCheckout() failed with: " + result.error.message)
}
});
</script>
{% endif %}
{% endblock %}
{% block pagesource %}/templates/stripe.html{% endblock %}