WIP: add ability to specify non-dollar currencies #37
17 changed files with 130 additions and 101 deletions
|
@ -34,6 +34,7 @@ Interested in learning more? How about a trip to the the `docs/` folder:
|
|||
- [With docker-compose](./docs/local-set-up.md#docker_compose)
|
||||
- [**Configuring `capsul-flask`**](./docs/configuration.md)
|
||||
- [Example configuration from capsul.org (production)](./docs/configuration.md#example)
|
||||
- [Configuring Currency](./docs/configuration.md#currency)
|
||||
- [Configuration-type-stuff that lives in the database ](./docs/configuration.md#config_that_lives_in_db)
|
||||
- [Loading variables from files (docker secrets)](./docs/configuration.md#docker_secrets)
|
||||
- [**`capsul-flask`'s relationship to its Database Server**](./docs/database.md)
|
||||
|
|
|
@ -74,7 +74,13 @@ app.config.from_mapping(
|
|||
#STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="")
|
||||
|
||||
BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"),
|
||||
BTCPAY_URL=os.environ.get("BTCPAY_URL", default="")
|
||||
BTCPAY_URL=os.environ.get("BTCPAY_URL", default=""),
|
||||
|
||||
CURRENCY_NAME=os.environ.get("CURRENCY_NAME", default="dollar"), # display name (singular)
|
||||
CURRENCY_SYMBOL=os.environ.get("CURRENCY_SYMBOL", default="$"), # display prefix symbol
|
||||
CURRENCY_SWIPE_NAME=os.environ.get("CURRENCY_SWIPE_NAME", default="usd"), # identifier swipe expects in swipe.checkout.session.create
|
||||
CURRENCY_BTCPAY_NAME=os.environ.get("CURRENCY_BTCPAY_NAME", default="USD"), # identifier btcpay expects in BTCPAY_CLIENT.create_invoice
|
||||
CURRENCY_SWIPE_CONVERSION_FACTOR=os.environ.get("CURRENCY_SWIPE_CONVERSION_FACTOR", default=100) # swipe wants to work in the smallest unit - pennies, cents, etc
|
||||
)
|
||||
|
||||
app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL'])
|
||||
|
|
|
@ -121,7 +121,7 @@ def clean_up_unresolved_btcpay_invoices():
|
|||
if btcpay_invoice['status'] == "complete":
|
||||
current_app.logger.info(
|
||||
f"resolving btcpay invoice {invoice_id} "
|
||||
f"({unresolved_invoice['email']}, ${unresolved_invoice['dollars']}) as completed "
|
||||
f"({unresolved_invoice['email']}, {config['CURRENCY_SYMBOL']}{unresolved_invoice['amount']}) as completed "
|
||||
)
|
||||
resolved_invoice_email = get_model().btcpay_invoice_resolved(invoice_id, True)
|
||||
|
||||
|
@ -131,13 +131,13 @@ def clean_up_unresolved_btcpay_invoices():
|
|||
elif days >= 1:
|
||||
current_app.logger.info(
|
||||
f"resolving btcpay invoice {invoice_id} "
|
||||
f"({unresolved_invoice['email']}, ${unresolved_invoice['dollars']}) as invalidated, "
|
||||
f"({unresolved_invoice['email']}, {config['CURRENCY_SYMBOL']}{unresolved_invoice['amount']}) as invalidated, "
|
||||
f"btcpay server invoice status: {btcpay_invoice['status']}"
|
||||
)
|
||||
get_model().btcpay_invoice_resolved(invoice_id, False)
|
||||
get_model().delete_payment_session("btcpay", invoice_id)
|
||||
|
||||
delete_at_account_balance_dollars = -10
|
||||
delete_at_account_balance_amount = -10
|
||||
|
||||
def get_warning_headline(warning_id, pluralize_capsul):
|
||||
return dict(
|
||||
|
@ -150,23 +150,23 @@ def get_warning_headline(warning_id, pluralize_capsul):
|
|||
zero_now= (
|
||||
f"You have run out of funds! You will no longer be able to create Capsuls.\n\n"
|
||||
f"As a courtesy, we'll let your existing Capsul{pluralize_capsul} keep running until your account "
|
||||
"reaches a -$10 balance, at which point they will be deleted.\n\n"
|
||||
"reaches a -{config['CURRENCY_SYMBOL']}10 balance, at which point they will be deleted.\n\n"
|
||||
),
|
||||
delete_1w= (
|
||||
"You have run out of funds and have not refilled your account.\n\n"
|
||||
f"As a courtesy, we've let your existing Capsul{pluralize_capsul} keep running. "
|
||||
f"However, your account will reach a -$10 balance some time next week and your Capsul{pluralize_capsul} "
|
||||
f"However, your account will reach a -{config['CURRENCY_SYMBOL']}10 balance some time next week and your Capsul{pluralize_capsul} "
|
||||
"will be deleted.\n\n"
|
||||
),
|
||||
delete_1d= (
|
||||
"You have run out of funds and have not refilled your account.\n\n"
|
||||
f"As a courtesy, we have let your existing Capsul{pluralize_capsul} keep running. "
|
||||
f"However, your account will reach a -$10 balance by this time tomorrow and "
|
||||
f"However, your account will reach a -{config['CURRENCY_SYMBOL']}10 balance by this time tomorrow and "
|
||||
f"your Capsul{pluralize_capsul} will be deleted.\n\n"
|
||||
f"Last chance to deposit funds now and keep your Capsul{pluralize_capsul} running! "
|
||||
),
|
||||
delete_now= (
|
||||
f"Your account reached a -$10 balance and your Capsul{pluralize_capsul} were deleted."
|
||||
f"Your account reached a -{config['CURRENCY_SYMBOL']}10 balance and your Capsul{pluralize_capsul} were deleted."
|
||||
)
|
||||
)[warning_id]
|
||||
|
||||
|
@ -205,7 +205,7 @@ def get_warnings_list():
|
|||
),
|
||||
dict(
|
||||
id='delete_1w',
|
||||
get_active=lambda balance_1w, balance_1d, balance_now: balance_1w < delete_at_account_balance_dollars,
|
||||
get_active=lambda balance_1w, balance_1d, balance_now: balance_1w < delete_at_account_balance_amount,
|
||||
get_subject=lambda pluralize_capsul: f"Your Capsul{pluralize_capsul} Will be Deleted In Less Than a Week",
|
||||
get_body=lambda base_url, pluralize_capsul: (
|
||||
f"{get_warning_headline('delete_1w', pluralize_capsul)}"
|
||||
|
@ -216,7 +216,7 @@ def get_warnings_list():
|
|||
),
|
||||
dict(
|
||||
id='delete_1d',
|
||||
get_active=lambda balance_1w, balance_1d, balance_now: balance_1d < delete_at_account_balance_dollars,
|
||||
get_active=lambda balance_1w, balance_1d, balance_now: balance_1d < delete_at_account_balance_amount,
|
||||
get_subject=lambda pluralize_capsul: f"Last Chance to Save your Capsul{pluralize_capsul}: Gone Tomorrow",
|
||||
get_body=lambda base_url, pluralize_capsul: (
|
||||
f"{get_warning_headline('delete_1d', pluralize_capsul)}"
|
||||
|
@ -225,7 +225,7 @@ def get_warnings_list():
|
|||
),
|
||||
dict(
|
||||
id='delete_now',
|
||||
get_active=lambda balance_1w, balance_1d, balance_now: balance_now < delete_at_account_balance_dollars,
|
||||
get_active=lambda balance_1w, balance_1d, balance_now: balance_now < delete_at_account_balance_amount,
|
||||
get_subject=lambda pluralize_capsul: f"Capsul{pluralize_capsul} Deleted",
|
||||
get_body=lambda base_url, pluralize_capsul: (
|
||||
f"{get_warning_headline('delete_now', pluralize_capsul)}"
|
||||
|
|
|
@ -212,12 +212,12 @@ def create():
|
|||
# if a user deposits $7.50 and then creates an f1-s vm which costs 7.50 a month,
|
||||
# then they have to delete the vm and re-create it, they will not be able to, they will have to pay again.
|
||||
# so for UX it makes a lot of sense to give a small margin of 25 cents for usability sake
|
||||
if vm_size["dollars_per_month"] <= account_balance_in_one_month+0.25:
|
||||
if vm_size["amount_per_month"] <= account_balance_in_one_month+0.25:
|
||||
month_funded_vm_sizes[key] = vm_size
|
||||
|
||||
one_month_in_hours = float(730.5)
|
||||
two_hours_as_fraction_of_month = float(2)/one_month_in_hours
|
||||
if float(vm_size["dollars_per_month"])*two_hours_as_fraction_of_month <= account_balance:
|
||||
if float(vm_size["amount_per_month"])*two_hours_as_fraction_of_month <= account_balance:
|
||||
hour_funded_vm_sizes[key] = vm_size
|
||||
|
||||
if request.method == "POST":
|
||||
|
@ -411,11 +411,11 @@ def account_balance():
|
|||
vm_months = get_vm_months_float(vm, datetime.utcnow())
|
||||
vms_billed.append(dict(
|
||||
id=vm["id"],
|
||||
dollars_per_month=vm["dollars_per_month"],
|
||||
amount_per_month=vm["amount_per_month"],
|
||||
created=vm["created"].strftime("%b %d %Y"),
|
||||
deleted=vm["deleted"].strftime("%b %d %Y") if vm["deleted"] else "N/A",
|
||||
months=format(vm_months, '.3f'),
|
||||
dollars=format(vm_months * float(vm["dollars_per_month"]), '.2f')
|
||||
amount=format(vm_months * float(vm["amount_per_month"]), '.2f')
|
||||
))
|
||||
|
||||
return render_template(
|
||||
|
@ -427,7 +427,7 @@ def account_balance():
|
|||
btcpay_is_working="BTCPAY_CLIENT" in current_app.config,
|
||||
payments=list(map(
|
||||
lambda x: dict(
|
||||
dollars=x["dollars"],
|
||||
amount=x["amount"],
|
||||
class_name="invalidated" if x["invalidated"] else "",
|
||||
created=x["created"].strftime("%b %d %Y")
|
||||
),
|
||||
|
|
|
@ -50,7 +50,7 @@ def init_app(app, is_running_server):
|
|||
hasSchemaVersionTable = False
|
||||
actionWasTaken = False
|
||||
schemaVersion = 0
|
||||
desiredSchemaVersion = 23
|
||||
desiredSchemaVersion = 24
|
||||
|
||||
cursor = connection.cursor()
|
||||
|
||||
|
|
|
@ -106,11 +106,11 @@ class DBModel:
|
|||
return operatingSystems
|
||||
|
||||
def vm_sizes_dict(self):
|
||||
self.cursor.execute("SELECT id, dollars_per_month, vcpus, memory_mb, bandwidth_gb_per_month FROM vm_sizes")
|
||||
self.cursor.execute("SELECT id, amount_per_month, vcpus, memory_mb, bandwidth_gb_per_month FROM vm_sizes")
|
||||
|
||||
vmSizes = dict()
|
||||
for row in self.cursor.fetchall():
|
||||
vmSizes[row[0]] = dict(dollars_per_month=row[1], vcpus=row[2], memory_mb=row[3], bandwidth_gb_per_month=row[4])
|
||||
vmSizes[row[0]] = dict(amount_per_month=row[1], vcpus=row[2], memory_mb=row[3], bandwidth_gb_per_month=row[4])
|
||||
|
||||
return vmSizes
|
||||
|
||||
|
@ -140,13 +140,13 @@ class DBModel:
|
|||
|
||||
def list_vms_for_account(self, email):
|
||||
self.cursor.execute("""
|
||||
SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.shortterm, vms.os, vms.created, vms.deleted, vm_sizes.dollars_per_month
|
||||
SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.shortterm, vms.os, vms.created, vms.deleted, vm_sizes.amount_per_month
|
||||
FROM vms JOIN vm_sizes on vms.size = vm_sizes.id
|
||||
WHERE vms.email = %s""",
|
||||
(email, )
|
||||
)
|
||||
return list(map(
|
||||
lambda x: dict(id=x[0], ipv4=x[1], ipv6=x[2], size=x[3], shortterm=x[4], os=x[5], created=x[6], deleted=x[7], dollars_per_month=x[8]),
|
||||
lambda x: dict(id=x[0], ipv4=x[1], ipv6=x[2], size=x[3], shortterm=x[4], os=x[5], created=x[6], deleted=x[7], amount_per_month=x[8]),
|
||||
self.cursor.fetchall()
|
||||
))
|
||||
|
||||
|
@ -188,7 +188,7 @@ class DBModel:
|
|||
def get_vm_detail(self, email, id):
|
||||
self.cursor.execute("""
|
||||
SELECT vms.id, vms.public_ipv4, vms.public_ipv6, os_images.description, vms.created, vms.deleted,
|
||||
vm_sizes.id, vms.shortterm, vm_sizes.dollars_per_month, vm_sizes.vcpus, vm_sizes.memory_mb, vm_sizes.bandwidth_gb_per_month
|
||||
vm_sizes.id, vms.shortterm, vm_sizes.amount_per_month, vm_sizes.vcpus, vm_sizes.memory_mb, vm_sizes.bandwidth_gb_per_month
|
||||
FROM vms
|
||||
JOIN os_images on vms.os = os_images.id
|
||||
JOIN vm_sizes on vms.size = vm_sizes.id
|
||||
|
@ -201,7 +201,7 @@ class DBModel:
|
|||
|
||||
vm = dict(
|
||||
id=row[0], ipv4=row[1], ipv6=row[2], os_description=row[3], created=row[4], deleted=row[5],
|
||||
size=row[6], shortterm=row[7], dollars_per_month=row[8], vcpus=row[9], memory_mb=row[10],
|
||||
size=row[6], shortterm=row[7], amount_per_month=row[8], vcpus=row[9], memory_mb=row[10],
|
||||
bandwidth_gb_per_month=row[11],
|
||||
)
|
||||
|
||||
|
@ -232,32 +232,32 @@ class DBModel:
|
|||
|
||||
def list_payments_for_account(self, email):
|
||||
self.cursor.execute("""
|
||||
SELECT id, dollars, invalidated, created
|
||||
SELECT id, amount, invalidated, created
|
||||
FROM payments WHERE payments.email = %s""",
|
||||
(email, )
|
||||
)
|
||||
return list(map(
|
||||
lambda x: dict(id=x[0], dollars=x[1], invalidated=x[2], created=x[3]),
|
||||
lambda x: dict(id=x[0], amount=x[1], invalidated=x[2], created=x[3]),
|
||||
self.cursor.fetchall()
|
||||
))
|
||||
|
||||
def create_payment_session(self, payment_type, id, email, dollars):
|
||||
def create_payment_session(self, payment_type, id, email, amount):
|
||||
self.cursor.execute("""
|
||||
INSERT INTO payment_sessions (id, type, email, dollars)
|
||||
INSERT INTO payment_sessions (id, type, email, amount)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""",
|
||||
(id, payment_type, email, dollars)
|
||||
(id, payment_type, email, amount)
|
||||
)
|
||||
self.connection.commit()
|
||||
|
||||
def list_payment_sessions_for_account(self, email):
|
||||
self.cursor.execute("""
|
||||
SELECT id, type, dollars, created
|
||||
SELECT id, type, amount, created
|
||||
FROM payment_sessions WHERE email = %s""",
|
||||
(email, )
|
||||
)
|
||||
return list(map(
|
||||
lambda x: dict(id=x[0], type=x[1], dollars=x[2], created=x[3]),
|
||||
lambda x: dict(id=x[0], type=x[1], amount=x[2], created=x[3]),
|
||||
self.cursor.fetchall()
|
||||
))
|
||||
|
||||
|
@ -276,21 +276,21 @@ class DBModel:
|
|||
return None
|
||||
|
||||
|
||||
def consume_payment_session(self, payment_type, id, dollars):
|
||||
self.cursor.execute("SELECT email, dollars FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type))
|
||||
def consume_payment_session(self, payment_type, id, amount):
|
||||
self.cursor.execute("SELECT email, amount FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type))
|
||||
row = self.cursor.fetchone()
|
||||
if row:
|
||||
if int(row[1]) != int(dollars):
|
||||
if int(row[1]) != int(amount):
|
||||
current_app.logger.warning(f"""
|
||||
{payment_type} gave us a completed payment session with a different dollar amount than what we had recorded!!
|
||||
{payment_type} gave us a completed payment session with a different {current_app.config['CURRENCY_NAME']} amount than what we had recorded!!
|
||||
id: {id}
|
||||
account: {row[0]}
|
||||
Recorded amount: {int(row[1])}
|
||||
{payment_type} sent: {int(dollars)}
|
||||
{payment_type} sent: {int(amount)}
|
||||
""")
|
||||
# not sure what to do here. For now just log and do nothing.
|
||||
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) RETURNING id", (row[0], row[1]) )
|
||||
self.cursor.execute( "INSERT INTO payments (email, amount) VALUES (%s, %s) RETURNING id", (row[0], row[1]) )
|
||||
|
||||
if payment_type == "btcpay":
|
||||
payment_id = self.cursor.fetchone()[0]
|
||||
|
@ -326,10 +326,10 @@ class DBModel:
|
|||
|
||||
def get_unresolved_btcpay_invoices(self):
|
||||
self.cursor.execute("""
|
||||
SELECT unresolved_btcpay_invoices.id, payments.created, payments.dollars, unresolved_btcpay_invoices.email
|
||||
SELECT unresolved_btcpay_invoices.id, payments.created, payments.amount, unresolved_btcpay_invoices.email
|
||||
FROM unresolved_btcpay_invoices JOIN payments on payment_id = payments.id
|
||||
""")
|
||||
return list(map(lambda row: dict(id=row[0], created=row[1], dollars=row[2], email=row[3]), self.cursor.fetchall()))
|
||||
return list(map(lambda row: dict(id=row[0], created=row[1], amount=row[2], email=row[3]), self.cursor.fetchall()))
|
||||
|
||||
def get_account_balance_warning(self, email):
|
||||
self.cursor.execute("SELECT account_balance_warning FROM accounts WHERE email = %s", (email,))
|
||||
|
|
|
@ -28,27 +28,27 @@ from capsulflask.shared import my_exec_info_message, get_account_balance, averag
|
|||
|
||||
bp = Blueprint("payment", __name__, url_prefix="/payment")
|
||||
|
||||
def validate_dollars(min: float, max: float):
|
||||
def validate_amount(min: float, max: float):
|
||||
errors = list()
|
||||
dollars = None
|
||||
if "dollars" not in request.form:
|
||||
errors.append("dollars is required")
|
||||
amount = None
|
||||
if "amount" not in request.form:
|
||||
errors.append("amount is required")
|
||||
else:
|
||||
dollars = None
|
||||
amount = None
|
||||
try:
|
||||
dollars = float(request.form["dollars"])
|
||||
amount = float(request.form["amount"])
|
||||
except:
|
||||
errors.append("dollars must be a number")
|
||||
errors.append("amount must be a number")
|
||||
|
||||
#current_app.logger.info(f"{str(dollars)} {str(min)} {str(dollars < min)}")
|
||||
#current_app.logger.info(f"{str(amount)} {str(min)} {str(amount < min)}")
|
||||
|
||||
if dollars is not None and dollars < min:
|
||||
errors.append(f"dollars must be {format(min, '.2f')} or more")
|
||||
elif dollars is not None and dollars >= max:
|
||||
errors.append(f"dollars must be less than {format(max, '.2f')}")
|
||||
if amount is not None and amount < min:
|
||||
errors.append(f"amount must be {format(min, '.2f')} or more")
|
||||
elif amount is not None and amount >= max:
|
||||
errors.append(f"amount must be less than {format(max, '.2f')}")
|
||||
|
||||
current_app.logger.info(f"{len(errors)} {errors}")
|
||||
return (dollars, errors)
|
||||
return (amount, errors)
|
||||
|
||||
@bp.route("/btcpay", methods=("GET", "POST"))
|
||||
@account_required
|
||||
|
@ -64,14 +64,14 @@ def btcpay_payment():
|
|||
return redirect(url_for("console.account_balance"))
|
||||
|
||||
if request.method == "POST":
|
||||
dollars, errors = validate_dollars(0.01, 1000.0)
|
||||
amount, errors = validate_amount(0.01, 1000.0)
|
||||
#current_app.logger.info(f"{len(errors)} {errors}")
|
||||
|
||||
if len(errors) == 0:
|
||||
try:
|
||||
invoice = current_app.config['BTCPAY_CLIENT'].create_invoice(dict(
|
||||
price=float(dollars),
|
||||
currency="USD",
|
||||
price=float(amount),
|
||||
currency=current_app.config['CURRENCY_BTCPAY_NAME'],
|
||||
itemDesc="Capsul Cloud Compute",
|
||||
transactionSpeed="high",
|
||||
redirectURL=f"{current_app.config['BASE_URL']}/console/account-balance",
|
||||
|
@ -86,9 +86,9 @@ def btcpay_payment():
|
|||
|
||||
# print(invoice)
|
||||
|
||||
current_app.logger.info(f"created btcpay invoice_id={invoice['id']} ( {session['account']}, ${dollars} )")
|
||||
current_app.logger.info(f"created btcpay invoice_id={invoice['id']} ( {session['account']}, {current_app.config['CURRENCY_SYMBOL']}{amount} )")
|
||||
|
||||
get_model().create_payment_session("btcpay", invoice["id"], session["account"], dollars)
|
||||
get_model().create_payment_session("btcpay", invoice["id"], session["account"], amount)
|
||||
|
||||
return redirect(invoice["url"])
|
||||
|
||||
|
@ -115,18 +115,18 @@ def poll_btcpay_session(invoice_id):
|
|||
return [503, "error was thrown when contacting btcpay server"]
|
||||
|
||||
|
||||
if invoice['currency'] != "USD":
|
||||
if invoice['currency'] != current_app.config['CURRENCY_BTCPAY_NAME']:
|
||||
return [400, "invalid currency"]
|
||||
|
||||
dollars = invoice['price']
|
||||
amount = invoice['price']
|
||||
|
||||
current_app.logger.info(f"poll_btcpay_session invoice_id={invoice_id}, status={invoice['status']} dollars={dollars}")
|
||||
current_app.logger.info(f"poll_btcpay_session invoice_id={invoice_id}, status={invoice['status']} amount={amount}")
|
||||
|
||||
if invoice['status'] == "paid" or invoice['status'] == "confirmed" or invoice['status'] == "complete":
|
||||
success_account = get_model().consume_payment_session("btcpay", invoice_id, dollars)
|
||||
success_account = get_model().consume_payment_session("btcpay", invoice_id, amount)
|
||||
|
||||
if success_account:
|
||||
current_app.logger.info(f"{success_account} paid ${dollars} successfully (btcpay_invoice_id={invoice_id})")
|
||||
current_app.logger.info(f"{success_account} paid {current_app.config['CURRENCY_SYMBOL']}{amount} successfully (btcpay_invoice_id={invoice_id})")
|
||||
|
||||
if invoice['status'] == "complete":
|
||||
resolved_invoice_email = get_model().btcpay_invoice_resolved(invoice_id, True)
|
||||
|
@ -188,11 +188,11 @@ def stripe_payment():
|
|||
errors = list()
|
||||
|
||||
if request.method == "POST":
|
||||
dollars, errors = validate_dollars(0.5, 1000000)
|
||||
amount, errors = validate_amount(0.5, 1000000)
|
||||
|
||||
if len(errors) == 0:
|
||||
|
||||
current_app.logger.info(f"creating stripe checkout session for {session['account']}, ${dollars}")
|
||||
current_app.logger.info(f"creating stripe checkout session for {session['account']}, {current_app.config['CURRENCY_SYMBOL']}{amount}")
|
||||
|
||||
checkout_session = stripe.checkout.Session.create(
|
||||
success_url=current_app.config['BASE_URL'] + "/payment/stripe/success?session_id={CHECKOUT_SESSION_ID}",
|
||||
|
@ -204,16 +204,16 @@ def stripe_payment():
|
|||
"name": "Capsul Cloud Compute",
|
||||
"images": [current_app.config['BASE_URL']+"/static/capsul-product-image.png"],
|
||||
"quantity": 1,
|
||||
"currency": "usd",
|
||||
"amount": int(dollars*100)
|
||||
"currency": current_app.config['CURRENCY_SWIPE_NAME'],
|
||||
"amount": int(amount*100)
|
||||
}
|
||||
]
|
||||
)
|
||||
stripe_checkout_session_id = checkout_session['id']
|
||||
|
||||
current_app.logger.info(f"stripe_checkout_session_id={stripe_checkout_session_id} ( {session['account']}, ${dollars} )")
|
||||
current_app.logger.info(f"stripe_checkout_session_id={stripe_checkout_session_id} ( {session['account']}, {current_app.config['CURRENCY_SYMBOL']}{amount} )")
|
||||
|
||||
get_model().create_payment_session("stripe", stripe_checkout_session_id, session["account"], dollars)
|
||||
get_model().create_payment_session("stripe", stripe_checkout_session_id, session["account"], amount)
|
||||
|
||||
# We can't do this because stripe requires a bunch of server-authenticated data to be sent in the hash
|
||||
# of the URL. I briefly looked into reverse-engineering their proprietary javascript in order to try to figure out
|
||||
|
@ -280,17 +280,17 @@ def validate_stripe_checkout_session(stripe_checkout_session_id):
|
|||
|
||||
if checkout_session and 'id' in checkout_session and checkout_session['id'] == stripe_checkout_session_id:
|
||||
cents = checkout_session['amount_total']
|
||||
dollars = decimal.Decimal(cents)/100
|
||||
amount = decimal.Decimal(cents)/100
|
||||
|
||||
#consume_payment_session deletes the checkout session row and inserts a payment row
|
||||
# its ok to call consume_payment_session more than once because it only takes an action if the session exists
|
||||
success_email = get_model().consume_payment_session("stripe", stripe_checkout_session_id, dollars)
|
||||
success_email = get_model().consume_payment_session("stripe", stripe_checkout_session_id, amount)
|
||||
|
||||
if success_email is not None:
|
||||
check_if_shortterm_flag_can_be_unset(success_email)
|
||||
|
||||
if success_email:
|
||||
return dict(email=success_email, dollars=dollars)
|
||||
return dict(email=success_email, amount=amount)
|
||||
|
||||
return None
|
||||
|
||||
|
@ -304,7 +304,7 @@ def success():
|
|||
for _ in range(0, 5):
|
||||
paid = validate_stripe_checkout_session(stripe_checkout_session_id)
|
||||
if paid:
|
||||
current_app.logger.info(f"{paid['email']} paid ${paid['dollars']} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||
current_app.logger.info(f"{paid['email']} paid {current_app.config['CURRENCY_SYMBOL']}{paid['amount']} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||
|
||||
return redirect(url_for("console.account_balance"))
|
||||
else:
|
||||
|
@ -325,14 +325,14 @@ def success():
|
|||
# secret=current_app.config['STRIPE_WEBHOOK_SECRET']
|
||||
# )
|
||||
# if event['type'] == 'checkout.session.completed':
|
||||
# dollars = event['data']['object']['amount_total']
|
||||
# amount = event['data']['object']['amount_total']
|
||||
# stripe_checkout_session_id = event['data']['object']['id']
|
||||
|
||||
# #consume_payment_session deletes the checkout session row and inserts a payment row
|
||||
# # 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_payment_session("stripe", stripe_checkout_session_id, dollars)
|
||||
# success_account = get_model().consume_payment_session("stripe", stripe_checkout_session_id, amount)
|
||||
# if success_account:
|
||||
# print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||
# print(f"{success_account} paid {current_app.config['CURRENCY_SYMBOL']}{amount} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||
|
||||
# return jsonify({'status': 'success'})
|
||||
# except ValueError as e:
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
ALTER TABLE vm_sizes RENAME amount_per_month TO dollars_per_month;
|
||||
ALTER TABLE payments RENAME amount TO dollars;
|
||||
ALTER TABLE payment_sessions RENAME amount TO dollars;
|
||||
|
||||
UPDATE schemaversion SET version = 23;
|
6
capsulflask/schema_migrations/24_up_generic_currency.sql
Normal file
6
capsulflask/schema_migrations/24_up_generic_currency.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
ALTER TABLE vm_sizes RENAME dollars_per_month TO amount_per_month;
|
||||
ALTER TABLE payments RENAME dollars TO amount;
|
||||
ALTER TABLE payment_sessions RENAME dollars TO amount;
|
||||
|
||||
UPDATE schemaversion SET version = 24;
|
|
@ -62,14 +62,14 @@ def authorized_as_hub(headers):
|
|||
|
||||
def get_account_balance(vms, payments, as_of):
|
||||
|
||||
vm_cost_dollars = 0.0
|
||||
vm_cost_amount = 0.0
|
||||
for vm in vms:
|
||||
vm_months = get_vm_months_float(vm, as_of)
|
||||
vm_cost_dollars += vm_months * float(vm["dollars_per_month"])
|
||||
vm_cost_amount += vm_months * float(vm["amount_per_month"])
|
||||
|
||||
payment_dollars_total = float( sum(map(lambda x: 0 if x["invalidated"] else x["dollars"], payments)) )
|
||||
payment_amount_total = float( sum(map(lambda x: 0 if x["invalidated"] else x["amount"], payments)) )
|
||||
|
||||
return payment_dollars_total - vm_cost_dollars
|
||||
return payment_amount_total - vm_cost_amount
|
||||
|
||||
|
||||
average_number_of_days_in_a_month = 30.44
|
||||
|
@ -98,4 +98,4 @@ def my_exec_info_message(exec_info):
|
|||
if isinstance(traceback_result, list):
|
||||
traceback_result = "\n".join(traceback_result)
|
||||
|
||||
return f"{exec_info[0].__module__}.{exec_info[0].__name__}: {exec_info[1]}: {traceback_result}"
|
||||
return f"{exec_info[0].__module__}.{exec_info[0].__name__}: {exec_info[1]}: {traceback_result}"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="row third-margin">
|
||||
<h1>Account Balance: ${{ account_balance }}</h1>
|
||||
<h1>Account Balance: {{config["CURRENCY_SYMBOL"]}}{{ account_balance }}</h1>
|
||||
</div>
|
||||
<div class="half-margin">
|
||||
|
||||
|
@ -30,8 +30,8 @@
|
|||
<tbody>
|
||||
{% for payment in payments %}
|
||||
<tr>
|
||||
<td class="{{ payment['class_name'] }}">${{ payment["dollars"] }}</td>
|
||||
<td class="{{ payment['class_name'] }}">{{ payment["created"] }}</td>
|
||||
<td class="{{ payment['class_name'] }}">{{config["CURRENCY_SYMBOL"]}}{{ payment["amount"] }}</td>
|
||||
<td class="{{ payment['class_name'] }}">{{payment["created"]}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -81,9 +81,9 @@
|
|||
<th>id</th>
|
||||
<th>created</th>
|
||||
<th>deleted</th>
|
||||
<th>$/month</th>
|
||||
<th>{{config["CURRENCY_SYMBOL"]}}/month</th>
|
||||
<th>months</th>
|
||||
<th>$ billed</th>
|
||||
<th>{{config["CURRENCY_SYMBOL"]}} billed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -92,9 +92,9 @@
|
|||
<td>{{ vm["id"] }}</td>
|
||||
<td>{{ vm["created"] }}</td>
|
||||
<td>{{ vm["deleted"] }}</td>
|
||||
<td>${{ vm["dollars_per_month"] }}</td>
|
||||
<td>{{config["CURRENCY_SYMBOL"]}}{{ vm["amount_per_month"] }}</td>
|
||||
<td>{{ vm["months"] }}</td>
|
||||
<td>${{ vm["dollars"] }}</td>
|
||||
<td>{{config["CURRENCY_SYMBOL"]}}{{ vm["amount"] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
<form method="POST" action="/payment/btcpay">
|
||||
<div class="row">
|
||||
<span>
|
||||
<label for="btcpay-input-price">$</label>
|
||||
<label for="btcpay-input-price">{{config['CURRENCY_SYMBOL']}}</label>
|
||||
<input
|
||||
id="btcpay-input-price"
|
||||
name="dollars"
|
||||
name="amount"
|
||||
type="text"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -76,8 +76,8 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<div class="row justify-start">
|
||||
<label class="align" for="dollars_per_month">Monthly Cost</label>
|
||||
<span id="dollars_per_month">${{ vm['dollars_per_month'] }}</span>
|
||||
<label class="align" for="amount_per_month">Monthly Cost</label>
|
||||
<span id="amount_per_month">{{config['CURRENCY_SYMBOL']}}{{ vm['amount_per_month'] }}</span>
|
||||
</div>
|
||||
<div class="row justify-start">
|
||||
<label class="align" for="ipv4">IPv4 Address</label>
|
||||
|
|
|
@ -13,19 +13,19 @@
|
|||
============
|
||||
type monthly* cpus mem ssd net*
|
||||
----- ------- ---- --- --- ---
|
||||
f1-xs $5.00 1 512M 25G .5TB
|
||||
f1-s $7.50 1 1024M 25G 1TB
|
||||
f1-m $12.50 1 2048M 25G 2TB
|
||||
f1-l $20.00 2 3072M 25G 3TB
|
||||
f1-x $27.50 3 4096M 25G 4TB
|
||||
f1-xx $50.00 4 8192M 25G 5TB
|
||||
f1-xs {{config['CURRENCY_SYMBOL']}}5.00 1 512M 25G .5TB
|
||||
f1-s {{config['CURRENCY_SYMBOL']}}7.50 1 1024M 25G 1TB
|
||||
f1-m {{config['CURRENCY_SYMBOL']}}12.50 1 2048M 25G 2TB
|
||||
f1-l {{config['CURRENCY_SYMBOL']}}20.00 2 3072M 25G 3TB
|
||||
f1-x {{config['CURRENCY_SYMBOL']}}27.50 3 4096M 25G 4TB
|
||||
f1-xx {{config['CURRENCY_SYMBOL']}}50.00 4 8192M 25G 5TB
|
||||
|
||||
* all VMs come standard with one public IPv4 address
|
||||
* vms are billed for a minimum of 1 hour upon creation</pre>
|
||||
<pre>
|
||||
Your <a href="/console/account-balance">current account balance</a>: ${{ account_balance }} {% if account_balance != account_balance_in_one_month %}
|
||||
Your <a href="/console/account-balance">current account balance</a>: {{config['CURRENCY_SYMBOL']}}{{ account_balance }} {% if account_balance != account_balance_in_one_month %}
|
||||
|
||||
Projected account balance in one month: ${{ account_balance_in_one_month }}{% endif %}
|
||||
Projected account balance in one month: {{config['CURRENCY_SYMBOL']}}{{ account_balance_in_one_month }}{% endif %}
|
||||
</pre>
|
||||
{% if cant_afford %}
|
||||
<p>Please <a href="/console/account-balance">fund your account</a> in order to create Capsuls</p>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
{% for vm_size_key, vm_size in vm_sizes.items() %}
|
||||
<tr>
|
||||
<td>{{ vm_size_key }}</td>
|
||||
<td>${{ vm_size['dollars_per_month'] }}</td>
|
||||
<td>{{config['CURRENCY_SYMBOL']}}{{ vm_size['amount_per_month'] }}</td>
|
||||
<td>{{ vm_size['vcpus'] }}</td>
|
||||
<td>{{ vm_size['memory_mb'] }}</td>
|
||||
<td>25G</td>
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
<div class="row half-margin">
|
||||
<form method="post">
|
||||
<div class="row justify-start">
|
||||
<label for="dollars">$</label>
|
||||
<input type="number" id="dollars" name="dollars"></input>
|
||||
<label for="amount">{{config['CURRENCY_SYMBOL']}}</label>
|
||||
<input type="number" id="amount" name="amount"></input>
|
||||
</div>
|
||||
<div class="row justify-end">
|
||||
<input type="submit" value="Pay With Stripe">
|
||||
|
|
|
@ -63,6 +63,16 @@ BTCPAY_URL="https://beeteeceepae2.cyberia.club"
|
|||
BTCPAY_PRIVATE_KEY='-----BEGIN EC PRIVATE KEY-----\n<redacted>\n-----END EC PRIVATE KEY-----'
|
||||
```
|
||||
|
||||
## <a name="currency"></a>Configuring Currency
|
||||
By default, capsul operates in USD. You can change that with the currency configuration settings:
|
||||
```
|
||||
CURRENCY_NAME=dollar <the name of the currency, singular>
|
||||
CURRENCY_SYMBOL=$ <the symbol to refer to the currency>
|
||||
CURRENCY_STRIPE_NAME=usd <a string stripe uses to identify what currency a payment is being requested in. £ is gbp>
|
||||
CURRENCY_BTCPAY_NAME=USD <likewise for btcpay>
|
||||
CURRENCY_STRIPE_CONVERSION_FACTOR=100 <stripe wants to work in the smallest unit - cents, pennies etc>
|
||||
```
|
||||
**Don't change the currency once capsul is up and running!** Amounts of money are stored in the database as plain numbers and won't be converted, so if someone has $10 and you move to using gbp they'll now have £10 in their account.
|
||||
## <a name="config_that_lives_in_db"></a>Configuration-type-stuff that lives in the database
|
||||
|
||||
- `hosts` table:
|
||||
|
|
Loading…
Reference in a new issue