(try again) add ability to specify non-dollar currencies #38
17 changed files with 130 additions and 101 deletions
|
@ -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)
|
- [With docker-compose](./docs/local-set-up.md#docker_compose)
|
||||||
- [**Configuring `capsul-flask`**](./docs/configuration.md)
|
- [**Configuring `capsul-flask`**](./docs/configuration.md)
|
||||||
- [Example configuration from capsul.org (production)](./docs/configuration.md#example)
|
- [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)
|
- [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)
|
- [Loading variables from files (docker secrets)](./docs/configuration.md#docker_secrets)
|
||||||
- [**`capsul-flask`'s relationship to its Database Server**](./docs/database.md)
|
- [**`capsul-flask`'s relationship to its Database Server**](./docs/database.md)
|
||||||
|
|
|
@ -76,7 +76,13 @@ app.config.from_mapping(
|
||||||
#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="").replace("\\n", "\n"),
|
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'])
|
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":
|
if btcpay_invoice['status'] == "complete":
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"resolving btcpay invoice {invoice_id} "
|
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)
|
resolved_invoice_email = get_model().btcpay_invoice_resolved(invoice_id, True)
|
||||||
|
|
||||||
|
@ -131,13 +131,13 @@ def clean_up_unresolved_btcpay_invoices():
|
||||||
elif days >= 1:
|
elif days >= 1:
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"resolving btcpay invoice {invoice_id} "
|
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']}"
|
f"btcpay server invoice status: {btcpay_invoice['status']}"
|
||||||
)
|
)
|
||||||
get_model().btcpay_invoice_resolved(invoice_id, False)
|
get_model().btcpay_invoice_resolved(invoice_id, False)
|
||||||
get_model().delete_payment_session("btcpay", invoice_id)
|
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):
|
def get_warning_headline(warning_id, pluralize_capsul):
|
||||||
return dict(
|
return dict(
|
||||||
|
@ -150,23 +150,23 @@ def get_warning_headline(warning_id, pluralize_capsul):
|
||||||
zero_now= (
|
zero_now= (
|
||||||
f"You have run out of funds! You will no longer be able to create Capsuls.\n\n"
|
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 "
|
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= (
|
delete_1w= (
|
||||||
"You have run out of funds and have not refilled your account.\n\n"
|
"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"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"
|
"will be deleted.\n\n"
|
||||||
),
|
),
|
||||||
delete_1d= (
|
delete_1d= (
|
||||||
"You have run out of funds and have not refilled your account.\n\n"
|
"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"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"your Capsul{pluralize_capsul} will be deleted.\n\n"
|
||||||
f"Last chance to deposit funds now and keep your Capsul{pluralize_capsul} running! "
|
f"Last chance to deposit funds now and keep your Capsul{pluralize_capsul} running! "
|
||||||
),
|
),
|
||||||
delete_now= (
|
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]
|
)[warning_id]
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ def get_warnings_list():
|
||||||
),
|
),
|
||||||
dict(
|
dict(
|
||||||
id='delete_1w',
|
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_subject=lambda pluralize_capsul: f"Your Capsul{pluralize_capsul} Will be Deleted In Less Than a Week",
|
||||||
get_body=lambda base_url, pluralize_capsul: (
|
get_body=lambda base_url, pluralize_capsul: (
|
||||||
f"{get_warning_headline('delete_1w', pluralize_capsul)}"
|
f"{get_warning_headline('delete_1w', pluralize_capsul)}"
|
||||||
|
@ -216,7 +216,7 @@ def get_warnings_list():
|
||||||
),
|
),
|
||||||
dict(
|
dict(
|
||||||
id='delete_1d',
|
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_subject=lambda pluralize_capsul: f"Last Chance to Save your Capsul{pluralize_capsul}: Gone Tomorrow",
|
||||||
get_body=lambda base_url, pluralize_capsul: (
|
get_body=lambda base_url, pluralize_capsul: (
|
||||||
f"{get_warning_headline('delete_1d', pluralize_capsul)}"
|
f"{get_warning_headline('delete_1d', pluralize_capsul)}"
|
||||||
|
@ -225,7 +225,7 @@ def get_warnings_list():
|
||||||
),
|
),
|
||||||
dict(
|
dict(
|
||||||
id='delete_now',
|
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_subject=lambda pluralize_capsul: f"Capsul{pluralize_capsul} Deleted",
|
||||||
get_body=lambda base_url, pluralize_capsul: (
|
get_body=lambda base_url, pluralize_capsul: (
|
||||||
f"{get_warning_headline('delete_now', pluralize_capsul)}"
|
f"{get_warning_headline('delete_now', pluralize_capsul)}"
|
||||||
|
|
|
@ -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,
|
# 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.
|
# 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
|
# 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
|
month_funded_vm_sizes[key] = vm_size
|
||||||
|
|
||||||
one_month_in_hours = float(730.5)
|
one_month_in_hours = float(730.5)
|
||||||
two_hours_as_fraction_of_month = float(2)/one_month_in_hours
|
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
|
hour_funded_vm_sizes[key] = vm_size
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
@ -414,11 +414,11 @@ def account_balance():
|
||||||
vm_months = get_vm_months_float(vm, datetime.utcnow())
|
vm_months = get_vm_months_float(vm, datetime.utcnow())
|
||||||
vms_billed.append(dict(
|
vms_billed.append(dict(
|
||||||
id=vm["id"],
|
id=vm["id"],
|
||||||
dollars_per_month=vm["dollars_per_month"],
|
amount_per_month=vm["amount_per_month"],
|
||||||
created=vm["created"].strftime("%b %d %Y"),
|
created=vm["created"].strftime("%b %d %Y"),
|
||||||
deleted=vm["deleted"].strftime("%b %d %Y") if vm["deleted"] else "N/A",
|
deleted=vm["deleted"].strftime("%b %d %Y") if vm["deleted"] else "N/A",
|
||||||
months=format(vm_months, '.3f'),
|
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(
|
return render_template(
|
||||||
|
@ -430,7 +430,7 @@ def account_balance():
|
||||||
btcpay_is_working="BTCPAY_CLIENT" in current_app.config,
|
btcpay_is_working="BTCPAY_CLIENT" in current_app.config,
|
||||||
payments=list(map(
|
payments=list(map(
|
||||||
lambda x: dict(
|
lambda x: dict(
|
||||||
dollars=x["dollars"],
|
amount=x["amount"],
|
||||||
class_name="invalidated" if x["invalidated"] else "",
|
class_name="invalidated" if x["invalidated"] else "",
|
||||||
created=x["created"].strftime("%b %d %Y")
|
created=x["created"].strftime("%b %d %Y")
|
||||||
),
|
),
|
||||||
|
|
|
@ -50,7 +50,7 @@ def init_app(app, is_running_server):
|
||||||
hasSchemaVersionTable = False
|
hasSchemaVersionTable = False
|
||||||
actionWasTaken = False
|
actionWasTaken = False
|
||||||
schemaVersion = 0
|
schemaVersion = 0
|
||||||
desiredSchemaVersion = 24
|
desiredSchemaVersion = 25
|
||||||
|
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
|
|
@ -106,11 +106,11 @@ class DBModel:
|
||||||
return operatingSystems
|
return operatingSystems
|
||||||
|
|
||||||
def vm_sizes_dict(self):
|
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()
|
vmSizes = dict()
|
||||||
for row in self.cursor.fetchall():
|
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
|
return vmSizes
|
||||||
|
|
||||||
|
@ -140,13 +140,13 @@ class DBModel:
|
||||||
|
|
||||||
def list_vms_for_account(self, email):
|
def list_vms_for_account(self, email):
|
||||||
self.cursor.execute("""
|
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
|
FROM vms JOIN vm_sizes on vms.size = vm_sizes.id
|
||||||
WHERE vms.email = %s""",
|
WHERE vms.email = %s""",
|
||||||
(email, )
|
(email, )
|
||||||
)
|
)
|
||||||
return list(map(
|
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()
|
self.cursor.fetchall()
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ class DBModel:
|
||||||
def get_vm_detail(self, email, id):
|
def get_vm_detail(self, email, id):
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
SELECT vms.id, vms.public_ipv4, vms.public_ipv6, os_images.description, vms.created, vms.deleted,
|
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
|
FROM vms
|
||||||
JOIN os_images on vms.os = os_images.id
|
JOIN os_images on vms.os = os_images.id
|
||||||
JOIN vm_sizes on vms.size = vm_sizes.id
|
JOIN vm_sizes on vms.size = vm_sizes.id
|
||||||
|
@ -201,7 +201,7 @@ class DBModel:
|
||||||
|
|
||||||
vm = dict(
|
vm = dict(
|
||||||
id=row[0], ipv4=row[1], ipv6=row[2], os_description=row[3], created=row[4], deleted=row[5],
|
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],
|
bandwidth_gb_per_month=row[11],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -232,32 +232,32 @@ class DBModel:
|
||||||
|
|
||||||
def list_payments_for_account(self, email):
|
def list_payments_for_account(self, email):
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
SELECT id, dollars, invalidated, created
|
SELECT id, amount, 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(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()
|
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("""
|
self.cursor.execute("""
|
||||||
INSERT INTO payment_sessions (id, type, email, dollars)
|
INSERT INTO payment_sessions (id, type, email, amount)
|
||||||
VALUES (%s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s)
|
||||||
""",
|
""",
|
||||||
(id, payment_type, email, dollars)
|
(id, payment_type, email, amount)
|
||||||
)
|
)
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
|
|
||||||
def list_payment_sessions_for_account(self, email):
|
def list_payment_sessions_for_account(self, email):
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
SELECT id, type, dollars, created
|
SELECT id, type, amount, created
|
||||||
FROM payment_sessions WHERE email = %s""",
|
FROM payment_sessions WHERE email = %s""",
|
||||||
(email, )
|
(email, )
|
||||||
)
|
)
|
||||||
return list(map(
|
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()
|
self.cursor.fetchall()
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -276,21 +276,21 @@ class DBModel:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def consume_payment_session(self, payment_type, id, dollars):
|
def consume_payment_session(self, payment_type, id, amount):
|
||||||
self.cursor.execute("SELECT email, dollars FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type))
|
self.cursor.execute("SELECT email, amount FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type))
|
||||||
row = self.cursor.fetchone()
|
row = self.cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
if int(row[1]) != int(dollars):
|
if int(row[1]) != int(amount):
|
||||||
current_app.logger.warning(f"""
|
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}
|
id: {id}
|
||||||
account: {row[0]}
|
account: {row[0]}
|
||||||
Recorded amount: {int(row[1])}
|
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.
|
# 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( "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":
|
if payment_type == "btcpay":
|
||||||
payment_id = self.cursor.fetchone()[0]
|
payment_id = self.cursor.fetchone()[0]
|
||||||
|
@ -326,10 +326,10 @@ class DBModel:
|
||||||
|
|
||||||
def get_unresolved_btcpay_invoices(self):
|
def get_unresolved_btcpay_invoices(self):
|
||||||
self.cursor.execute("""
|
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
|
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):
|
def get_account_balance_warning(self, email):
|
||||||
self.cursor.execute("SELECT account_balance_warning FROM accounts WHERE email = %s", (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")
|
bp = Blueprint("payment", __name__, url_prefix="/payment")
|
||||||
|
|
||||||
def validate_dollars(min: float, max: float):
|
def validate_amount(min: float, max: float):
|
||||||
errors = list()
|
errors = list()
|
||||||
dollars = None
|
amount = None
|
||||||
if "dollars" not in request.form:
|
if "amount" not in request.form:
|
||||||
errors.append("dollars is required")
|
errors.append("amount is required")
|
||||||
else:
|
else:
|
||||||
dollars = None
|
amount = None
|
||||||
try:
|
try:
|
||||||
dollars = float(request.form["dollars"])
|
amount = float(request.form["amount"])
|
||||||
except:
|
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:
|
if amount is not None and amount < min:
|
||||||
errors.append(f"dollars must be {format(min, '.2f')} or more")
|
errors.append(f"amount must be {format(min, '.2f')} or more")
|
||||||
elif dollars is not None and dollars >= max:
|
elif amount is not None and amount >= max:
|
||||||
errors.append(f"dollars must be less than {format(max, '.2f')}")
|
errors.append(f"amount must be less than {format(max, '.2f')}")
|
||||||
|
|
||||||
current_app.logger.info(f"{len(errors)} {errors}")
|
current_app.logger.info(f"{len(errors)} {errors}")
|
||||||
return (dollars, errors)
|
return (amount, errors)
|
||||||
|
|
||||||
@bp.route("/btcpay", methods=("GET", "POST"))
|
@bp.route("/btcpay", methods=("GET", "POST"))
|
||||||
@account_required
|
@account_required
|
||||||
|
@ -64,14 +64,14 @@ def btcpay_payment():
|
||||||
return redirect(url_for("console.account_balance"))
|
return redirect(url_for("console.account_balance"))
|
||||||
|
|
||||||
if request.method == "POST":
|
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}")
|
#current_app.logger.info(f"{len(errors)} {errors}")
|
||||||
|
|
||||||
if len(errors) == 0:
|
if len(errors) == 0:
|
||||||
try:
|
try:
|
||||||
invoice = current_app.config['BTCPAY_CLIENT'].create_invoice(dict(
|
invoice = current_app.config['BTCPAY_CLIENT'].create_invoice(dict(
|
||||||
price=float(dollars),
|
price=float(amount),
|
||||||
currency="USD",
|
currency=current_app.config['CURRENCY_BTCPAY_NAME'],
|
||||||
itemDesc="Capsul Cloud Compute",
|
itemDesc="Capsul Cloud Compute",
|
||||||
transactionSpeed="high",
|
transactionSpeed="high",
|
||||||
redirectURL=f"{current_app.config['BASE_URL']}/console/account-balance",
|
redirectURL=f"{current_app.config['BASE_URL']}/console/account-balance",
|
||||||
|
@ -86,9 +86,9 @@ def btcpay_payment():
|
||||||
|
|
||||||
# print(invoice)
|
# 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"])
|
return redirect(invoice["url"])
|
||||||
|
|
||||||
|
@ -115,18 +115,18 @@ def poll_btcpay_session(invoice_id):
|
||||||
return [503, "error was thrown when contacting btcpay server"]
|
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"]
|
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":
|
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:
|
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":
|
if invoice['status'] == "complete":
|
||||||
resolved_invoice_email = get_model().btcpay_invoice_resolved(invoice_id, True)
|
resolved_invoice_email = get_model().btcpay_invoice_resolved(invoice_id, True)
|
||||||
|
@ -188,11 +188,11 @@ def stripe_payment():
|
||||||
errors = list()
|
errors = list()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
dollars, errors = validate_dollars(0.5, 1000000)
|
amount, errors = validate_amount(0.5, 1000000)
|
||||||
|
|
||||||
if len(errors) == 0:
|
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(
|
checkout_session = stripe.checkout.Session.create(
|
||||||
success_url=current_app.config['BASE_URL'] + "/payment/stripe/success?session_id={CHECKOUT_SESSION_ID}",
|
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",
|
"name": "Capsul Cloud Compute",
|
||||||
"images": [current_app.config['BASE_URL']+"/static/capsul-product-image.png"],
|
"images": [current_app.config['BASE_URL']+"/static/capsul-product-image.png"],
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"currency": "usd",
|
"currency": current_app.config['CURRENCY_SWIPE_NAME'],
|
||||||
"amount": int(dollars*100)
|
"amount": int(amount*100)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
stripe_checkout_session_id = checkout_session['id']
|
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
|
# 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
|
# 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:
|
if checkout_session and 'id' in checkout_session and checkout_session['id'] == stripe_checkout_session_id:
|
||||||
cents = checkout_session['amount_total']
|
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
|
#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
|
# 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:
|
if success_email is not None:
|
||||||
check_if_shortterm_flag_can_be_unset(success_email)
|
check_if_shortterm_flag_can_be_unset(success_email)
|
||||||
|
|
||||||
if success_email:
|
if success_email:
|
||||||
return dict(email=success_email, dollars=dollars)
|
return dict(email=success_email, amount=amount)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -304,7 +304,7 @@ def success():
|
||||||
for _ in range(0, 5):
|
for _ in range(0, 5):
|
||||||
paid = validate_stripe_checkout_session(stripe_checkout_session_id)
|
paid = validate_stripe_checkout_session(stripe_checkout_session_id)
|
||||||
if paid:
|
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"))
|
return redirect(url_for("console.account_balance"))
|
||||||
else:
|
else:
|
||||||
|
@ -325,14 +325,14 @@ def success():
|
||||||
# secret=current_app.config['STRIPE_WEBHOOK_SECRET']
|
# secret=current_app.config['STRIPE_WEBHOOK_SECRET']
|
||||||
# )
|
# )
|
||||||
# if event['type'] == 'checkout.session.completed':
|
# 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']
|
# stripe_checkout_session_id = event['data']['object']['id']
|
||||||
|
|
||||||
# #consume_payment_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_payment_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_payment_session("stripe", stripe_checkout_session_id, dollars)
|
# success_account = get_model().consume_payment_session("stripe", stripe_checkout_session_id, amount)
|
||||||
# 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 {current_app.config['CURRENCY_SYMBOL']}{amount} 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:
|
||||||
|
|
|
@ -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;
|
6
capsulflask/schema_migrations/25_up_generic_currency.sql
Normal file
6
capsulflask/schema_migrations/25_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 = 25;
|
|
@ -62,14 +62,14 @@ def authorized_as_hub(headers):
|
||||||
|
|
||||||
def get_account_balance(vms, payments, as_of):
|
def get_account_balance(vms, payments, as_of):
|
||||||
|
|
||||||
vm_cost_dollars = 0.0
|
vm_cost_amount = 0.0
|
||||||
for vm in vms:
|
for vm in vms:
|
||||||
vm_months = get_vm_months_float(vm, as_of)
|
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
|
average_number_of_days_in_a_month = 30.44
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row third-margin">
|
<div class="row third-margin">
|
||||||
<h1>Account Balance: ${{ account_balance }}</h1>
|
<h1>Account Balance: {{config["CURRENCY_SYMBOL"]}}{{ account_balance }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="half-margin">
|
<div class="half-margin">
|
||||||
|
|
||||||
|
@ -30,8 +30,8 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for payment in payments %}
|
{% for payment in payments %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="{{ payment['class_name'] }}">${{ payment["dollars"] }}</td>
|
<td class="{{ payment['class_name'] }}">{{config["CURRENCY_SYMBOL"]}}{{ payment["amount"] }}</td>
|
||||||
<td class="{{ payment['class_name'] }}">{{ payment["created"] }}</td>
|
<td class="{{ payment['class_name'] }}">{{payment["created"]}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -81,9 +81,9 @@
|
||||||
<th>id</th>
|
<th>id</th>
|
||||||
<th>created</th>
|
<th>created</th>
|
||||||
<th>deleted</th>
|
<th>deleted</th>
|
||||||
<th>$/month</th>
|
<th>{{config["CURRENCY_SYMBOL"]}}/month</th>
|
||||||
<th>months</th>
|
<th>months</th>
|
||||||
<th>$ billed</th>
|
<th>{{config["CURRENCY_SYMBOL"]}} billed</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -92,9 +92,9 @@
|
||||||
<td>{{ vm["id"] }}</td>
|
<td>{{ vm["id"] }}</td>
|
||||||
<td>{{ vm["created"] }}</td>
|
<td>{{ vm["created"] }}</td>
|
||||||
<td>{{ vm["deleted"] }}</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["months"] }}</td>
|
||||||
<td>${{ vm["dollars"] }}</td>
|
<td>{{config["CURRENCY_SYMBOL"]}}{{ vm["amount"] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
<form method="POST" action="/payment/btcpay">
|
<form method="POST" action="/payment/btcpay">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span>
|
<span>
|
||||||
<label for="btcpay-input-price">$</label>
|
<label for="btcpay-input-price">{{config['CURRENCY_SYMBOL']}}</label>
|
||||||
<input
|
<input
|
||||||
id="btcpay-input-price"
|
id="btcpay-input-price"
|
||||||
name="dollars"
|
name="amount"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -81,8 +81,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-start">
|
<div class="row justify-start">
|
||||||
<label class="align" for="dollars_per_month">Monthly Cost</label>
|
<label class="align" for="amount_per_month">Monthly Cost</label>
|
||||||
<span id="dollars_per_month">${{ vm['dollars_per_month'] }}</span>
|
<span id="amount_per_month">{{config['CURRENCY_SYMBOL']}}{{ vm['amount_per_month'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-start">
|
<div class="row justify-start">
|
||||||
<label class="align" for="ipv4">IPv4 Address</label>
|
<label class="align" for="ipv4">IPv4 Address</label>
|
||||||
|
|
|
@ -13,19 +13,19 @@
|
||||||
============
|
============
|
||||||
type monthly* cpus mem ssd net*
|
type monthly* cpus mem ssd net*
|
||||||
----- ------- ---- --- --- ---
|
----- ------- ---- --- --- ---
|
||||||
f1-xs $5.00 1 512M 25G .5TB
|
f1-xs {{config['CURRENCY_SYMBOL']}}5.00 1 512M 25G .5TB
|
||||||
f1-s $7.50 1 1024M 25G 1TB
|
f1-s {{config['CURRENCY_SYMBOL']}}7.50 1 1024M 25G 1TB
|
||||||
f1-m $12.50 1 2048M 25G 2TB
|
f1-m {{config['CURRENCY_SYMBOL']}}12.50 1 2048M 25G 2TB
|
||||||
f1-l $20.00 2 3072M 25G 3TB
|
f1-l {{config['CURRENCY_SYMBOL']}}20.00 2 3072M 25G 3TB
|
||||||
f1-x $27.50 3 4096M 25G 4TB
|
f1-x {{config['CURRENCY_SYMBOL']}}27.50 3 4096M 25G 4TB
|
||||||
f1-xx $50.00 4 8192M 25G 5TB
|
f1-xx {{config['CURRENCY_SYMBOL']}}50.00 4 8192M 25G 5TB
|
||||||
|
|
||||||
* all VMs come standard with one public IPv4 address
|
* all VMs come standard with one public IPv4 address
|
||||||
* vms are billed for a minimum of 1 hour upon creation</pre>
|
* vms are billed for a minimum of 1 hour upon creation</pre>
|
||||||
<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>
|
</pre>
|
||||||
{% if cant_afford %}
|
{% if cant_afford %}
|
||||||
<p>Please <a href="/console/account-balance">fund your account</a> in order to create Capsuls</p>
|
<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() %}
|
{% for vm_size_key, vm_size in vm_sizes.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ vm_size_key }}</td>
|
<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['vcpus'] }}</td>
|
||||||
<td>{{ vm_size['memory_mb'] }}</td>
|
<td>{{ vm_size['memory_mb'] }}</td>
|
||||||
<td>25G</td>
|
<td>25G</td>
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
<div class="row half-margin">
|
<div class="row half-margin">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="row justify-start">
|
<div class="row justify-start">
|
||||||
<label for="dollars">$</label>
|
<label for="amount">{{config['CURRENCY_SYMBOL']}}</label>
|
||||||
<input type="number" id="dollars" name="dollars"></input>
|
<input type="number" id="amount" name="amount"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-end">
|
<div class="row justify-end">
|
||||||
<input type="submit" value="Pay With Stripe">
|
<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-----'
|
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
|
## <a name="config_that_lives_in_db"></a>Configuration-type-stuff that lives in the database
|
||||||
|
|
||||||
- `hosts` table:
|
- `hosts` table:
|
||||||
|
|
Loading…
Reference in a new issue