(try again) add ability to specify non-dollar currencies #38

Open
asa wants to merge 9 commits from asa/capsul-flask:main into main
17 changed files with 130 additions and 101 deletions

View File

@ -42,6 +42,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)

View File

@ -76,7 +76,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'])

View File

@ -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)}"

View File

@ -215,12 +215,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":
@ -414,11 +414,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(
@ -430,7 +430,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")
),

View File

@ -50,7 +50,7 @@ def init_app(app, is_running_server):
hasSchemaVersionTable = False
actionWasTaken = False
schemaVersion = 0
desiredSchemaVersion = 24
desiredSchemaVersion = 25
cursor = connection.cursor()

View File

@ -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,))

View File

@ -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:

View File

@ -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 = 24;

View 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 = 25;

View File

@ -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}"

View File

@ -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>

View File

@ -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>

View File

@ -81,8 +81,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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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: