btcpay generating invoices and payments can be invalidated
This commit is contained in:
parent
6440961433
commit
8de802aff5
@ -50,7 +50,8 @@ Run the app in gunicorn
|
|||||||
.venv/bin/gunicorn --bind 127.0.0.1:5000 capsulflask:app
|
.venv/bin/gunicorn --bind 127.0.0.1:5000 capsulflask:app
|
||||||
```
|
```
|
||||||
|
|
||||||
## postgres database schema management
|
|
||||||
|
# postgres database schema management
|
||||||
|
|
||||||
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
|
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
|
||||||
`schemaversion` that has one row and one column (`version`). If the `version` it finds is not equal to the `desiredSchemaVersion` variable set in `db.py`, it will run migration scripts from the `schema_migrations` folder one by one until the `schemaversion` table shows the correct version.
|
`schemaversion` that has one row and one column (`version`). If the `version` it finds is not equal to the `desiredSchemaVersion` variable set in `db.py`, it will run migration scripts from the `schema_migrations` folder one by one until the `schemaversion` table shows the correct version.
|
||||||
@ -62,7 +63,8 @@ For example, the script named `02_up_xyz.sql` should contain code that migrates
|
|||||||
In general, for safety, schema version upgrades should not delete data. Schema version downgrades will simply throw an error and exit for now.
|
In general, for safety, schema version upgrades should not delete data. Schema version downgrades will simply throw an error and exit for now.
|
||||||
|
|
||||||
|
|
||||||
## how to setup btcpay server
|
|
||||||
|
# how to setup btcpay server
|
||||||
|
|
||||||
Generate a private key and the accompanying bitpay SIN for the bitpay API client.
|
Generate a private key and the accompanying bitpay SIN for the bitpay API client.
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import stripe
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import stripe
|
||||||
|
from bitpay import client as btcpay
|
||||||
from dotenv import load_dotenv, find_dotenv
|
from dotenv import load_dotenv, find_dotenv
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
@ -31,7 +33,8 @@ app.config.from_mapping(
|
|||||||
STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default=""),
|
STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default=""),
|
||||||
#STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="")
|
#STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="")
|
||||||
|
|
||||||
BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="")
|
BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default=""),
|
||||||
|
BTCPAY_URL=os.environ.get("BTCPAY_URL", default="https://btcpay.cyberia.club")
|
||||||
)
|
)
|
||||||
|
|
||||||
stripe.api_key = app.config['STRIPE_SECRET_KEY']
|
stripe.api_key = app.config['STRIPE_SECRET_KEY']
|
||||||
@ -39,18 +42,18 @@ stripe.api_version = app.config['STRIPE_API_VERSION']
|
|||||||
|
|
||||||
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
|
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
|
||||||
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
|
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
|
||||||
|
app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY'])
|
||||||
|
|
||||||
from capsulflask import db
|
from capsulflask import db
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
from capsulflask import auth, landing, console, payment_stripe, payment_btcpay, metrics
|
from capsulflask import auth, landing, console, payment, metrics
|
||||||
|
|
||||||
app.register_blueprint(landing.bp)
|
app.register_blueprint(landing.bp)
|
||||||
app.register_blueprint(auth.bp)
|
app.register_blueprint(auth.bp)
|
||||||
app.register_blueprint(console.bp)
|
app.register_blueprint(console.bp)
|
||||||
app.register_blueprint(payment_stripe.bp)
|
app.register_blueprint(payment.bp)
|
||||||
app.register_blueprint(payment_btcpay.bp)
|
|
||||||
app.register_blueprint(metrics.bp)
|
app.register_blueprint(metrics.bp)
|
||||||
|
|
||||||
app.add_url_rule("/", endpoint="index")
|
app.add_url_rule("/", endpoint="index")
|
||||||
|
@ -254,7 +254,7 @@ def get_account_balance():
|
|||||||
vm_months = ( end_datetime - vm["created"] ).days / average_number_of_days_in_a_month
|
vm_months = ( end_datetime - vm["created"] ).days / average_number_of_days_in_a_month
|
||||||
vm_cost_dollars += vm_months * float(vm["dollars_per_month"])
|
vm_cost_dollars += vm_months * float(vm["dollars_per_month"])
|
||||||
|
|
||||||
payment_dollars_total = float( sum(map(lambda x: x["dollars"], get_payments())) )
|
payment_dollars_total = float( sum(map(lambda x: 0 if x["invalidated"] else x["dollars"], get_payments())) )
|
||||||
|
|
||||||
return payment_dollars_total - vm_cost_dollars
|
return payment_dollars_total - vm_cost_dollars
|
||||||
|
|
||||||
@ -268,8 +268,8 @@ def account_balance():
|
|||||||
|
|
||||||
for vm in get_vms():
|
for vm in get_vms():
|
||||||
end_datetime = vm["deleted"] if vm["deleted"] else datetime.utcnow()
|
end_datetime = vm["deleted"] if vm["deleted"] else datetime.utcnow()
|
||||||
print(end_datetime)
|
# print(end_datetime)
|
||||||
print(vm["created"])
|
# print(vm["created"])
|
||||||
vm_months = (end_datetime - vm["created"]).days / average_number_of_days_in_a_month
|
vm_months = (end_datetime - vm["created"]).days / average_number_of_days_in_a_month
|
||||||
vms_billed.append(dict(
|
vms_billed.append(dict(
|
||||||
id=vm["id"],
|
id=vm["id"],
|
||||||
@ -284,7 +284,14 @@ def account_balance():
|
|||||||
"account-balance.html",
|
"account-balance.html",
|
||||||
has_vms=len(vms_billed)>0,
|
has_vms=len(vms_billed)>0,
|
||||||
vms_billed=vms_billed,
|
vms_billed=vms_billed,
|
||||||
payments=list(map(lambda x: dict(dollars=x["dollars"], created=x["created"].strftime("%b %d %Y")), payments)),
|
payments=list(map(
|
||||||
|
lambda x: dict(
|
||||||
|
dollars=x["dollars"],
|
||||||
|
class_name="invalidated" if x["invalidated"] else "",
|
||||||
|
created=x["created"].strftime("%b %d %Y")
|
||||||
|
),
|
||||||
|
payments
|
||||||
|
)),
|
||||||
has_payments=len(payments)>0,
|
has_payments=len(payments)>0,
|
||||||
account_balance=format(account_balance, '.2f')
|
account_balance=format(account_balance, '.2f')
|
||||||
)
|
)
|
@ -146,38 +146,38 @@ class DBModel:
|
|||||||
|
|
||||||
def list_payments_for_account(self, email):
|
def list_payments_for_account(self, email):
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
SELECT payments.dollars, payments.created
|
SELECT dollars, invalidated, created
|
||||||
FROM payments WHERE payments.email = %s""",
|
FROM payments WHERE payments.email = %s""",
|
||||||
(email, )
|
(email, )
|
||||||
)
|
)
|
||||||
return list(map(
|
return list(map(
|
||||||
lambda x: dict(dollars=x[0], created=x[1]),
|
lambda x: dict(dollars=x[0], invalidated=x[1], created=x[2]),
|
||||||
self.cursor.fetchall()
|
self.cursor.fetchall()
|
||||||
))
|
))
|
||||||
|
|
||||||
def create_stripe_checkout_session(self, id, email, dollars):
|
def create_payment_session(self, payment_type, id, email, dollars):
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
INSERT INTO stripe_checkout_sessions (id, email, dollars)
|
INSERT INTO payment_sessions (id, type, email, dollars)
|
||||||
VALUES (%s, %s, %s)
|
VALUES (%s, %s, %s, %s)
|
||||||
""",
|
""",
|
||||||
(id, email, dollars)
|
(id, payment_type, email, dollars)
|
||||||
)
|
)
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
|
|
||||||
def consume_stripe_checkout_session(self, id, dollars):
|
def consume_payment_session(self, payment_type, id, dollars):
|
||||||
self.cursor.execute("SELECT email, dollars FROM stripe_checkout_sessions WHERE id = %s", (id,))
|
self.cursor.execute("SELECT email, dollars FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type))
|
||||||
rows = self.cursor.fetchall()
|
rows = self.cursor.fetchall()
|
||||||
if len(rows) > 0:
|
if len(rows) > 0:
|
||||||
if int(rows[0][1]) != int(dollars):
|
if int(rows[0][1]) != int(dollars):
|
||||||
print(f"""
|
print(f"""
|
||||||
Stripe sent us a completed checkout session with a different dollar amount than what we had recorded!!
|
{payment_type} gave us a completed payment session with a different dollar amount than what we had recorded!!
|
||||||
stripe_checkout_session_id: {id}
|
id: {id}
|
||||||
account: {rows[0][0]}
|
account: {rows[0][0]}
|
||||||
Recorded amount: {int(rows[0][1])}
|
Recorded amount: {int(rows[0][1])}
|
||||||
Stripe sent: {int(dollars)}
|
{payment_type} sent: {int(dollars)}
|
||||||
""")
|
""")
|
||||||
# not sure what to do here. For now just log and do nothing.
|
# 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( "DELETE FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type) )
|
||||||
self.cursor.execute( "INSERT INTO payments (email, dollars) VALUES (%s, %s)", (rows[0][0], rows[0][1]) )
|
self.cursor.execute( "INSERT INTO payments (email, dollars) VALUES (%s, %s)", (rows[0][0], rows[0][1]) )
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return rows[0][0]
|
return rows[0][0]
|
||||||
|
@ -18,34 +18,79 @@ from capsulflask.auth import account_required
|
|||||||
|
|
||||||
from capsulflask.db import get_model
|
from capsulflask.db import get_model
|
||||||
|
|
||||||
bp = Blueprint("stripe", __name__, url_prefix="/stripe")
|
bp = Blueprint("payment", __name__, url_prefix="/payment")
|
||||||
|
|
||||||
@bp.route("/", methods=("GET", "POST"))
|
def validate_dollars():
|
||||||
@account_required
|
|
||||||
def index():
|
|
||||||
|
|
||||||
stripe_checkout_session_id=None
|
|
||||||
errors = list()
|
errors = list()
|
||||||
|
dollars = None
|
||||||
if request.method == "POST":
|
|
||||||
if "dollars" not in request.form:
|
if "dollars" not in request.form:
|
||||||
errors.append("dollars is required")
|
errors.append("dollars is required")
|
||||||
|
else:
|
||||||
dollars = None
|
dollars = None
|
||||||
try:
|
try:
|
||||||
dollars = decimal.Decimal(request.form["dollars"])
|
dollars = decimal.Decimal(request.form["dollars"])
|
||||||
except:
|
except:
|
||||||
errors.append("dollars must be a number")
|
errors.append("dollars must be a number")
|
||||||
|
|
||||||
if dollars and dollars < decimal.Decimal(1):
|
# TODO re enable this
|
||||||
errors.append("dollars must be >= 1")
|
# if dollars and dollars < decimal.Decimal(1):
|
||||||
|
# errors.append("dollars must be >= 1")
|
||||||
|
|
||||||
|
return [errors, dollars]
|
||||||
|
|
||||||
|
@bp.route("/btcpay", methods=("GET", "POST"))
|
||||||
|
@account_required
|
||||||
|
def btcpay_payment():
|
||||||
|
errors = list()
|
||||||
|
invoice_id = None
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
result = validate_dollars()
|
||||||
|
errors = result[0]
|
||||||
|
dollars = result[1]
|
||||||
|
|
||||||
|
if len(errors) == 0:
|
||||||
|
invoice = current_app.config['BTCPAY_CLIENT'].create_invoice(dict(
|
||||||
|
price=float(dollars),
|
||||||
|
currency="USD",
|
||||||
|
itemDesc="Capsul Cloud Compute",
|
||||||
|
transactionSpeed="high",
|
||||||
|
redirectURL=f"{current_app.config['BASE_URL']}/account-balance"
|
||||||
|
))
|
||||||
|
# print(invoice)
|
||||||
|
invoice_id = invoice["id"]
|
||||||
|
|
||||||
|
print(f"created btcpay invoice_id={invoice_id} ( {session['account']}, ${request.form['dollars']} )")
|
||||||
|
|
||||||
|
get_model().create_payment_session("btcpay", invoice_id, session["account"], dollars)
|
||||||
|
|
||||||
|
return redirect(invoice["url"])
|
||||||
|
|
||||||
|
for error in errors:
|
||||||
|
flash(error)
|
||||||
|
|
||||||
|
return render_template("btcpay.html", invoice_id=invoice_id)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/stripe", methods=("GET", "POST"))
|
||||||
|
@account_required
|
||||||
|
def stripe_payment():
|
||||||
|
|
||||||
|
stripe_checkout_session_id=None
|
||||||
|
errors = list()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
result = validate_dollars()
|
||||||
|
errors = result[0]
|
||||||
|
dollars = result[1]
|
||||||
|
|
||||||
if len(errors) == 0:
|
if len(errors) == 0:
|
||||||
|
|
||||||
print(f"creating stripe checkout session for {session['account']}, ${request.form['dollars']}")
|
print(f"creating stripe checkout session for {session['account']}, ${dollars}")
|
||||||
|
|
||||||
checkout_session = stripe.checkout.Session.create(
|
checkout_session = stripe.checkout.Session.create(
|
||||||
success_url=current_app.config['BASE_URL'] + "/stripe/success?session_id={CHECKOUT_SESSION_ID}",
|
success_url=current_app.config['BASE_URL'] + "/payment/stripe/success?session_id={CHECKOUT_SESSION_ID}",
|
||||||
cancel_url=current_app.config['BASE_URL'] + "/stripe",
|
cancel_url=current_app.config['BASE_URL'] + "/payment/stripe",
|
||||||
payment_method_types=["card"],
|
payment_method_types=["card"],
|
||||||
customer_email=session["account"],
|
customer_email=session["account"],
|
||||||
line_items=[
|
line_items=[
|
||||||
@ -60,9 +105,9 @@ def index():
|
|||||||
)
|
)
|
||||||
stripe_checkout_session_id = checkout_session['id']
|
stripe_checkout_session_id = checkout_session['id']
|
||||||
|
|
||||||
print(f"stripe_checkout_session_id={stripe_checkout_session_id} ( {session['account']}, ${request.form['dollars']} )")
|
print(f"stripe_checkout_session_id={stripe_checkout_session_id} ( {session['account']}, ${dollars} )")
|
||||||
|
|
||||||
get_model().create_stripe_checkout_session(stripe_checkout_session_id, session["account"], request.form["dollars"])
|
get_model().create_payment_session("stripe", stripe_checkout_session_id, session["account"], dollars)
|
||||||
|
|
||||||
for error in errors:
|
for error in errors:
|
||||||
flash(error)
|
flash(error)
|
||||||
@ -73,11 +118,11 @@ def index():
|
|||||||
stripe_public_key=current_app.config["STRIPE_PUBLISHABLE_KEY"]
|
stripe_public_key=current_app.config["STRIPE_PUBLISHABLE_KEY"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route("/success", methods=("GET",))
|
@bp.route("/stripe/success", methods=("GET",))
|
||||||
def success():
|
def success():
|
||||||
stripe_checkout_session_id = request.args.get('session_id')
|
stripe_checkout_session_id = request.args.get('session_id')
|
||||||
if not stripe_checkout_session_id:
|
if not stripe_checkout_session_id:
|
||||||
print("/stripe/success returned 400: missing required URL parameter session_id")
|
print("/payment/stripe/success returned 400: missing required URL parameter session_id")
|
||||||
abort(400, "missing required URL parameter session_id")
|
abort(400, "missing required URL parameter session_id")
|
||||||
else:
|
else:
|
||||||
checkout_session_completed_events = stripe.Event.list(
|
checkout_session_completed_events = stripe.Event.list(
|
||||||
@ -95,9 +140,9 @@ def success():
|
|||||||
cents = checkout_session['display_items'][0]['amount']
|
cents = checkout_session['display_items'][0]['amount']
|
||||||
dollars = decimal.Decimal(cents)/100
|
dollars = decimal.Decimal(cents)/100
|
||||||
|
|
||||||
#consume_stripe_checkout_session deletes the checkout session row and inserts a payment row
|
#consume_payment_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
|
# its ok to call consume_payment_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)
|
success_account = get_model().consume_payment_session(stripe_checkout_session_id, dollars)
|
||||||
|
|
||||||
if success_account:
|
if success_account:
|
||||||
print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||||
@ -121,17 +166,17 @@ def success():
|
|||||||
# dollars = event['data']['object']['display_items'][0]['amount']
|
# dollars = event['data']['object']['display_items'][0]['amount']
|
||||||
# stripe_checkout_session_id = event['data']['object']['id']
|
# stripe_checkout_session_id = event['data']['object']['id']
|
||||||
|
|
||||||
# #consume_stripe_checkout_session deletes the checkout session row and inserts a payment row
|
# #consume_payment_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
|
# # its ok to call consume_payment_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)
|
# success_account = get_model().consume_payment_session(stripe_checkout_session_id, dollars)
|
||||||
# if success_account:
|
# if success_account:
|
||||||
# print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
# print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||||
|
|
||||||
# return jsonify({'status': 'success'})
|
# return jsonify({'status': 'success'})
|
||||||
# except ValueError as e:
|
# except ValueError as e:
|
||||||
# print("/stripe/webhook returned 400: bad request", e)
|
# print("/payment/stripe/webhook returned 400: bad request", e)
|
||||||
# abort(400, "bad request")
|
# abort(400, "bad request")
|
||||||
# except stripe.error.SignatureVerificationError:
|
# except stripe.error.SignatureVerificationError:
|
||||||
# print("/stripe/webhook returned 400: invalid signature")
|
# print("/payment/stripe/webhook returned 400: invalid signature")
|
||||||
# abort(400, "invalid signature")
|
# abort(400, "invalid signature")
|
||||||
|
|
@ -1,12 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
from flask import render_template
|
|
||||||
|
|
||||||
from capsulflask.db import get_model
|
|
||||||
from capsulflask.auth import account_required
|
|
||||||
|
|
||||||
bp = Blueprint("btcpay", __name__, url_prefix="/btcpay")
|
|
||||||
|
|
||||||
@bp.route("/")
|
|
||||||
@account_required
|
|
||||||
def index():
|
|
||||||
return render_template("btcpay.html")
|
|
@ -52,6 +52,7 @@ CREATE TABLE payments (
|
|||||||
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
||||||
created TIMESTAMP NOT NULL DEFAULT NOW(),
|
created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
dollars NUMERIC(8, 2) NOT NULL,
|
dollars NUMERIC(8, 2) NOT NULL,
|
||||||
|
invalidated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
PRIMARY KEY (email, created)
|
PRIMARY KEY (email, created)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -62,13 +63,21 @@ CREATE TABLE login_tokens (
|
|||||||
PRIMARY KEY (email, created)
|
PRIMARY KEY (email, created)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE stripe_checkout_sessions (
|
CREATE TABLE payment_sessions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
||||||
|
type TEXT NOT NULL,
|
||||||
created TIMESTAMP NOT NULL DEFAULT NOW(),
|
created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
dollars NUMERIC(8, 2) NOT NULL
|
dollars NUMERIC(8, 2) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE unconfirmed_btcpay_invoices (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
||||||
|
created TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY (email, created) REFERENCES payments(email, created) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO os_images (id, template_image_file_name, description)
|
INSERT INTO os_images (id, template_image_file_name, description)
|
||||||
VALUES ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'),
|
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)'),
|
('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic)'),
|
||||||
|
@ -226,6 +226,9 @@ th {
|
|||||||
td {
|
td {
|
||||||
border-bottom: 2px dotted #777e7355;
|
border-bottom: 2px dotted #777e7355;
|
||||||
}
|
}
|
||||||
|
.invalidated {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.waiting-pulse {
|
.waiting-pulse {
|
||||||
animation: waiting-pulse 1s ease-in-out 0s infinite forwards alternate;
|
animation: waiting-pulse 1s ease-in-out 0s infinite forwards alternate;
|
||||||
|
@ -25,8 +25,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for payment in payments %}
|
{% for payment in payments %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>${{ payment["dollars"] }}</td>
|
<td class="{{ payment['class_name'] }}">${{ payment["dollars"] }}</td>
|
||||||
<td>{{ payment["created"] }}</td>
|
<td class="{{ payment['class_name'] }}">{{ payment["created"] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -38,10 +38,10 @@
|
|||||||
<h1>PAYMENT OPTIONS</h1>
|
<h1>PAYMENT OPTIONS</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/stripe">Add funds with Credit/Debit (stripe)</a>
|
<a href="/payment/stripe">Add funds with Credit/Debit (stripe)</a>
|
||||||
<ul><li>notice: stripe will load nonfree javascript </li></ul>
|
<ul><li>notice: stripe will load nonfree javascript </li></ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li>
|
<li><a href="/payment/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li>
|
||||||
|
|
||||||
<li>Cash: email treasurer@cyberia.club</li>
|
<li>Cash: email treasurer@cyberia.club</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -10,18 +10,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row half-margin">
|
<div class="row half-margin">
|
||||||
|
|
||||||
<form method="POST" action="https://btcpay.cyberia.club/api/v1/invoices">
|
<form method="POST" action="/payment/btcpay">
|
||||||
<input type="hidden" name="storeId" value="FgYNGKEHKm2tBhwejo1zdSQ15DknPWvip2pXLKBv96wc">
|
|
||||||
<input type="hidden" name="currency" value="USD">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="btcpay-input-price">$</label>
|
<label for="btcpay-input-price">$</label>
|
||||||
<input
|
<!--TODO-- <input
|
||||||
id="btcpay-input-price"
|
id="btcpay-input-price"
|
||||||
name="price"
|
name="price"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="2000"
|
max="2000"
|
||||||
oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector('#btcpay-input-price').value = 0 : event.target.value"
|
oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector('#btcpay-input-price').value = 0 : event.target.value"
|
||||||
|
/>-->
|
||||||
|
<input
|
||||||
|
id="btcpay-input-price"
|
||||||
|
name="dollars"
|
||||||
|
type="text"
|
||||||
/>
|
/>
|
||||||
<input type="image" class="submit" name="submit"
|
<input type="image" class="submit" name="submit"
|
||||||
src="https://btcpay.cyberia.club/img/paybutton/pay.svg"
|
src="https://btcpay.cyberia.club/img/paybutton/pay.svg"
|
||||||
|
Loading…
Reference in New Issue
Block a user