billing frontend for mariadb. setup as otb_billing for outsidethebox.top accounting. also involved with outsidethedb
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1278 lines
40 KiB

cd /home/def/otb_billing || exit 1
cat > /tmp/otb_billing_service_templates_patch.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
APP_ROOT="/home/def/otb_billing"
BACKUP_DIR="/home/def/backuphere"
STAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
echo "===== sanity ====="
test -f "$APP_ROOT/backend/app.py"
test -f "$APP_ROOT/templates/services/list.html"
test -f "$APP_ROOT/templates/services/new.html"
test -f "$APP_ROOT/templates/services/edit.html"
echo "===== backups ====="
cp "$APP_ROOT/backend/app.py" "$BACKUP_DIR/app.py.service-templates.${STAMP}.bak"
cp "$APP_ROOT/templates/services/list.html" "$BACKUP_DIR/services-list.html.service-templates.${STAMP}.bak"
cp "$APP_ROOT/templates/services/new.html" "$BACKUP_DIR/services-new.html.service-templates.${STAMP}.bak"
cp "$APP_ROOT/templates/services/edit.html" "$BACKUP_DIR/services-edit.html.service-templates.${STAMP}.bak"
cp "$APP_ROOT/templates/includes/site_nav.html" "$BACKUP_DIR/site_nav.html.service-templates.${STAMP}.bak" || true
mkdir -p "$APP_ROOT/templates/service_templates"
echo "===== create database table if missing ====="
sudo mysql -D otb_billing <<'SQL'
CREATE TABLE IF NOT EXISTS service_templates (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
template_name VARCHAR(255) NOT NULL,
service_type ENUM('hosting','rpc','explorer','node','ipfs','consulting','crypto_infra','other') NOT NULL DEFAULT 'other',
billing_cycle ENUM('one_time','monthly','quarterly','yearly','manual') NOT NULL DEFAULT 'monthly',
currency_code VARCHAR(16) NOT NULL DEFAULT 'CAD',
recurring_amount DECIMAL(18,8) NOT NULL DEFAULT 0.00000000,
setup_amount DECIMAL(18,8) NOT NULL DEFAULT 0.00000000,
description TEXT DEFAULT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp(),
updated_at TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
KEY idx_service_templates_active (is_active),
KEY idx_service_templates_name (template_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL
echo "===== patch backend/app.py ====="
python3 <<'PY'
from pathlib import Path
path = Path("/home/def/otb_billing/backend/app.py")
text = path.read_text()
old_new_block = """@app.route("/services/new", methods=["GET", "POST"])
def new_service():
gate = admin_required()
if gate:
return gate
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if request.method == "POST":
client_id = request.form["client_id"]
service_name = request.form["service_name"]
service_type = request.form["service_type"]
billing_cycle = request.form["billing_cycle"]
currency_code = request.form["currency_code"]
recurring_amount = request.form["recurring_amount"]
status = request.form["status"]
start_date = request.form["start_date"] or None
description = request.form["description"]
cursor.execute("SELECT MAX(id) AS last_id FROM services")
result = cursor.fetchone()
last_number = result["last_id"] if result["last_id"] else 0
service_code = generate_service_code(service_name, last_number)
insert_cursor = conn.cursor()
insert_cursor.execute(
\"\"\"
INSERT INTO services
(
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
\"\"\",
(
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
)
)
conn.commit()
conn.close()
return redirect("/services")
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
conn.close()
return render_template("services/new.html", clients=clients)
"""
new_new_block = """@app.route("/services/new", methods=["GET", "POST"])
def new_service():
gate = admin_required()
if gate:
return gate
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if request.method == "POST":
client_id = request.form["client_id"]
service_name = request.form["service_name"]
service_type = request.form["service_type"]
billing_cycle = request.form["billing_cycle"]
currency_code = request.form["currency_code"]
recurring_amount = request.form["recurring_amount"]
status = request.form["status"]
start_date = request.form["start_date"] or None
description = request.form["description"]
cursor.execute("SELECT MAX(id) AS last_id FROM services")
result = cursor.fetchone()
last_number = result["last_id"] if result["last_id"] else 0
service_code = generate_service_code(service_name, last_number)
insert_cursor = conn.cursor()
insert_cursor.execute(
\"\"\"
INSERT INTO services
(
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
\"\"\",
(
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
)
)
conn.commit()
conn.close()
return redirect("/services")
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
cursor.execute(\"\"\"
SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description
FROM service_templates
WHERE is_active = 1
ORDER BY template_name ASC
\"\"\")
templates = cursor.fetchall()
conn.close()
return render_template("services/new.html", clients=clients, templates=templates)
"""
if old_new_block not in text:
raise SystemExit("Could not find /services/new block to replace.")
text = text.replace(old_new_block, new_new_block)
old_edit_block = """@app.route("/services/edit/<int:service_id>", methods=["GET", "POST"])
def edit_service(service_id):
gate = admin_required()
if gate:
return gate
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if request.method == "POST":
client_id = request.form.get("client_id", "").strip()
service_name = request.form.get("service_name", "").strip()
service_type = request.form.get("service_type", "").strip()
billing_cycle = request.form.get("billing_cycle", "").strip()
currency_code = request.form.get("currency_code", "").strip()
recurring_amount = request.form.get("recurring_amount", "").strip()
status = request.form.get("status", "").strip()
start_date = request.form.get("start_date", "").strip()
description = request.form.get("description", "").strip()
errors = []
if not client_id:
errors.append("Client is required.")
if not service_name:
errors.append("Service name is required.")
if not service_type:
errors.append("Service type is required.")
if not billing_cycle:
errors.append("Billing cycle is required.")
if not currency_code:
errors.append("Currency code is required.")
if not recurring_amount:
errors.append("Recurring amount is required.")
if not status:
errors.append("Status is required.")
if not errors:
try:
recurring_amount_value = float(recurring_amount)
if recurring_amount_value < 0:
errors.append("Recurring amount cannot be negative.")
except ValueError:
errors.append("Recurring amount must be a valid number.")
if errors:
cursor.execute(\"\"\"
SELECT s.*, c.company_name
FROM services s
LEFT JOIN clients c ON s.client_id = c.id
WHERE s.id = %s
\"\"\", (service_id,))
service = cursor.fetchone()
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
conn.close()
return render_template("services/edit.html", service=service, clients=clients, errors=errors)
update_cursor = conn.cursor()
update_cursor.execute(\"\"\"
UPDATE services
SET client_id = %s,
service_name = %s,
service_type = %s,
billing_cycle = %s,
status = %s,
currency_code = %s,
recurring_amount = %s,
start_date = %s,
description = %s
WHERE id = %s
\"\"\", (
client_id,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date or None,
description or None,
service_id
))
conn.commit()
conn.close()
return redirect("/services")
cursor.execute(\"\"\"
SELECT s.*, c.company_name
FROM services s
LEFT JOIN clients c ON s.client_id = c.id
WHERE s.id = %s
\"\"\", (service_id,))
service = cursor.fetchone()
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
conn.close()
if not service:
return "Service not found", 404
return render_template("services/edit.html", service=service, clients=clients, errors=[])
"""
new_edit_block = """@app.route("/services/edit/<int:service_id>", methods=["GET", "POST"])
def edit_service(service_id):
gate = admin_required()
if gate:
return gate
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if request.method == "POST":
client_id = request.form.get("client_id", "").strip()
service_name = request.form.get("service_name", "").strip()
service_type = request.form.get("service_type", "").strip()
billing_cycle = request.form.get("billing_cycle", "").strip()
currency_code = request.form.get("currency_code", "").strip()
recurring_amount = request.form.get("recurring_amount", "").strip()
status = request.form.get("status", "").strip()
start_date = request.form.get("start_date", "").strip()
description = request.form.get("description", "").strip()
errors = []
if not client_id:
errors.append("Client is required.")
if not service_name:
errors.append("Service name is required.")
if not service_type:
errors.append("Service type is required.")
if not billing_cycle:
errors.append("Billing cycle is required.")
if not currency_code:
errors.append("Currency code is required.")
if not recurring_amount:
errors.append("Recurring amount is required.")
if not status:
errors.append("Status is required.")
if not errors:
try:
recurring_amount_value = float(recurring_amount)
if recurring_amount_value < 0:
errors.append("Recurring amount cannot be negative.")
except ValueError:
errors.append("Recurring amount must be a valid number.")
if errors:
cursor.execute(\"\"\"
SELECT s.*, c.company_name
FROM services s
LEFT JOIN clients c ON s.client_id = c.id
WHERE s.id = %s
\"\"\", (service_id,))
service = cursor.fetchone()
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
cursor.execute(\"\"\"
SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description
FROM service_templates
WHERE is_active = 1
ORDER BY template_name ASC
\"\"\")
templates = cursor.fetchall()
conn.close()
return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=errors)
update_cursor = conn.cursor()
update_cursor.execute(\"\"\"
UPDATE services
SET client_id = %s,
service_name = %s,
service_type = %s,
billing_cycle = %s,
status = %s,
currency_code = %s,
recurring_amount = %s,
start_date = %s,
description = %s
WHERE id = %s
\"\"\", (
client_id,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date or None,
description or None,
service_id
))
conn.commit()
conn.close()
return redirect("/services")
cursor.execute(\"\"\"
SELECT s.*, c.company_name
FROM services s
LEFT JOIN clients c ON s.client_id = c.id
WHERE s.id = %s
\"\"\", (service_id,))
service = cursor.fetchone()
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
cursor.execute(\"\"\"
SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description
FROM service_templates
WHERE is_active = 1
ORDER BY template_name ASC
\"\"\")
templates = cursor.fetchall()
conn.close()
if not service:
return "Service not found", 404
return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=[])
"""
if old_edit_block not in text:
raise SystemExit("Could not find /services/edit block to replace.")
text = text.replace(old_edit_block, new_edit_block)
insert_after = """ return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=[])
@app.route("/invoices/export.csv")
"""
new_routes = """ return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=[])
@app.route("/service-templates")
def service_templates():
gate = admin_required()
if gate:
return gate
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute(\"\"\"
SELECT *
FROM service_templates
ORDER BY id DESC
\"\"\")
templates = cursor.fetchall()
conn.close()
return render_template("service_templates/list.html", templates=templates)
@app.route("/service-templates/new", methods=["GET", "POST"])
def new_service_template():
gate = admin_required()
if gate:
return gate
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
errors = []
if request.method == "POST":
template_name = request.form.get("template_name", "").strip()
service_type = request.form.get("service_type", "").strip()
billing_cycle = request.form.get("billing_cycle", "").strip()
currency_code = request.form.get("currency_code", "").strip()
recurring_amount = request.form.get("recurring_amount", "").strip()
setup_amount = request.form.get("setup_amount", "").strip()
description = request.form.get("description", "").strip()
is_active = 1 if request.form.get("is_active") == "1" else 0
if not template_name:
errors.append("Template name is required.")
if not service_type:
errors.append("Service type is required.")
if not billing_cycle:
errors.append("Billing cycle is required.")
if not currency_code:
errors.append("Currency code is required.")
if recurring_amount == "":
errors.append("Recurring amount is required.")
if setup_amount == "":
errors.append("Setup amount is required.")
if not errors:
try:
recurring_amount_value = float(recurring_amount)
setup_amount_value = float(setup_amount)
if recurring_amount_value < 0:
errors.append("Recurring amount cannot be negative.")
if setup_amount_value < 0:
errors.append("Setup amount cannot be negative.")
except ValueError:
errors.append("Amounts must be valid numbers.")
if not errors:
insert_cursor = conn.cursor()
insert_cursor.execute(\"\"\"
INSERT INTO service_templates
(
template_name,
service_type,
billing_cycle,
currency_code,
recurring_amount,
setup_amount,
description,
is_active
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
\"\"\", (
template_name,
service_type,
billing_cycle,
currency_code,
recurring_amount,
setup_amount,
description or None,
is_active
))
conn.commit()
conn.close()
return redirect("/service-templates")
conn.close()
return render_template("service_templates/new.html", errors=errors)
@app.route("/service-templates/edit/<int:template_id>", methods=["GET", "POST"])
def edit_service_template(template_id):
gate = admin_required()
if gate:
return gate
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if request.method == "POST":
template_name = request.form.get("template_name", "").strip()
service_type = request.form.get("service_type", "").strip()
billing_cycle = request.form.get("billing_cycle", "").strip()
currency_code = request.form.get("currency_code", "").strip()
recurring_amount = request.form.get("recurring_amount", "").strip()
setup_amount = request.form.get("setup_amount", "").strip()
description = request.form.get("description", "").strip()
is_active = 1 if request.form.get("is_active") == "1" else 0
errors = []
if not template_name:
errors.append("Template name is required.")
if not service_type:
errors.append("Service type is required.")
if not billing_cycle:
errors.append("Billing cycle is required.")
if not currency_code:
errors.append("Currency code is required.")
if recurring_amount == "":
errors.append("Recurring amount is required.")
if setup_amount == "":
errors.append("Setup amount is required.")
if not errors:
try:
recurring_amount_value = float(recurring_amount)
setup_amount_value = float(setup_amount)
if recurring_amount_value < 0:
errors.append("Recurring amount cannot be negative.")
if setup_amount_value < 0:
errors.append("Setup amount cannot be negative.")
except ValueError:
errors.append("Amounts must be valid numbers.")
if errors:
cursor.execute("SELECT * FROM service_templates WHERE id = %s", (template_id,))
template = cursor.fetchone()
conn.close()
return render_template("service_templates/edit.html", template=template, errors=errors)
update_cursor = conn.cursor()
update_cursor.execute(\"\"\"
UPDATE service_templates
SET template_name = %s,
service_type = %s,
billing_cycle = %s,
currency_code = %s,
recurring_amount = %s,
setup_amount = %s,
description = %s,
is_active = %s
WHERE id = %s
\"\"\", (
template_name,
service_type,
billing_cycle,
currency_code,
recurring_amount,
setup_amount,
description or None,
is_active,
template_id
))
conn.commit()
conn.close()
return redirect("/service-templates")
cursor.execute("SELECT * FROM service_templates WHERE id = %s", (template_id,))
template = cursor.fetchone()
conn.close()
if not template:
return "Service template not found", 404
return render_template("service_templates/edit.html", template=template, errors=[])
@app.route("/invoices/export.csv")
"""
if insert_after not in text:
raise SystemExit("Could not find insertion point before /invoices/export.csv.")
text = text.replace(insert_after, new_routes)
path.write_text(text)
PY
echo "===== rewrite templates/services/list.html ====="
cat > "$APP_ROOT/templates/services/list.html" <<'HTML'
<!doctype html>
<html>
<head>
<title>Services</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<h1>Services</h1>
<p><a href="/">Home</a></p>
<p>
<a href="/services/new">Add Service</a> |
<a href="/service-templates">Service Templates</a>
</p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Service Code</th>
<th>Client</th>
<th>Service Name</th>
<th>Type</th>
<th>Cycle</th>
<th>Currency</th>
<th>Amount</th>
<th>Status</th>
<th>Start Date</th>
<th>Actions</th>
</tr>
{% for s in services %}
<tr>
<td>{{ s.id }}</td>
<td>{{ s.service_code }}</td>
<td>{{ s.client_code }} - {{ s.company_name }}</td>
<td>{{ s.service_name }}</td>
<td>{{ s.service_type }}</td>
<td>{{ s.billing_cycle }}</td>
<td>{{ s.currency_code }}</td>
<td>{{ s.recurring_amount|money(s.currency_code) }}</td>
<td>{{ s.status }}</td>
<td>{{ s.start_date }}</td>
<td><a href="/services/edit/{{ s.id }}">Edit</a></td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>
HTML
echo "===== rewrite templates/services/new.html ====="
cat > "$APP_ROOT/templates/services/new.html" <<'HTML'
<!doctype html>
<html>
<head>
<title>New Service</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<h1>Add Service</h1>
<p><a href="/">Home</a></p>
<p>
<a href="/services">Back to Services</a> |
<a href="/service-templates">Service Templates</a>
</p>
<form method="post">
<p>
Client<br>
<select name="client_id" required>
<option value="">Select client</option>
{% for c in clients %}
<option value="{{ c.id }}">{{ c.client_code }} - {{ c.company_name }}</option>
{% endfor %}
</select>
</p>
<p>
Load from Template<br>
<select id="template_select">
<option value="">-- select template --</option>
{% for t in templates %}
<option
value="{{ t.id }}"
data-name="{{ t.template_name }}"
data-type="{{ t.service_type }}"
data-cycle="{{ t.billing_cycle }}"
data-currency="{{ t.currency_code }}"
data-recurring="{{ t.recurring_amount }}"
data-setup="{{ t.setup_amount }}"
data-description="{{ (t.description or '')|e }}"
>
{{ t.template_name }} ({{ t.recurring_amount|money(t.currency_code) }}{% if t.setup_amount and t.setup_amount != 0 %}, setup {{ t.setup_amount|money(t.currency_code) }}{% endif %})
</option>
{% endfor %}
</select>
</p>
<p>
Service Name<br>
<input name="service_name" required>
</p>
<p>
Service Type<br>
<select name="service_type" required>
<option value="hosting">hosting</option>
<option value="rpc">rpc</option>
<option value="explorer">explorer</option>
<option value="node">node</option>
<option value="ipfs">ipfs</option>
<option value="consulting">consulting</option>
<option value="crypto_infra">Crypto Infra</option>
<option value="other">other</option>
</select>
</p>
<p>
Billing Cycle<br>
<select name="billing_cycle" required>
<option value="one_time">one_time</option>
<option value="monthly" selected>monthly</option>
<option value="quarterly">quarterly</option>
<option value="yearly">yearly</option>
<option value="manual">manual</option>
</select>
</p>
<p>
Currency Code<br>
<select name="currency_code" required>
<option value="CAD" selected>CAD</option>
<option value="ETHO">ETHO</option>
<option value="EGAZ">EGAZ</option>
<option value="ALT">ALT</option>
</select>
</p>
<p>
Recurring Amount<br>
<input type="number" step="0.00000001" name="recurring_amount" value="0.00000000" required>
</p>
<p>
Status<br>
<select name="status" required>
<option value="pending">pending</option>
<option value="active" selected>active</option>
<option value="suspended">suspended</option>
<option value="cancelled">cancelled</option>
</select>
</p>
<p>
Start Date<br>
<input type="date" name="start_date">
</p>
<p>
Description<br>
<textarea name="description" rows="5" cols="60"></textarea>
</p>
<p>
<button type="submit">Create Service</button>
</p>
</form>
<script>
(function() {
var select = document.getElementById("template_select");
if (!select) return;
select.addEventListener("change", function() {
var opt = this.options[this.selectedIndex];
if (!opt || !opt.dataset || !opt.dataset.name) return;
document.querySelector('[name="service_name"]').value = opt.dataset.name || '';
document.querySelector('[name="service_type"]').value = opt.dataset.type || 'other';
document.querySelector('[name="billing_cycle"]').value = opt.dataset.cycle || 'monthly';
document.querySelector('[name="currency_code"]').value = opt.dataset.currency || 'CAD';
document.querySelector('[name="recurring_amount"]').value = opt.dataset.recurring || '0.00000000';
document.querySelector('[name="description"]').value = opt.dataset.description || '';
});
})();
</script>
{% include "footer.html" %}
</body>
</html>
HTML
echo "===== rewrite templates/services/edit.html ====="
cat > "$APP_ROOT/templates/services/edit.html" <<'HTML'
<!doctype html>
<html>
<head>
<title>Edit Service</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<h1>Edit Service</h1>
<p><a href="/">Home</a></p>
<p>
<a href="/services">Back to Services</a> |
<a href="/service-templates">Service Templates</a>
</p>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Service Code<br>
<input value="{{ service.service_code }}" readonly>
</p>
<p>
Client *<br>
<select name="client_id" required>
<option value="">Select client</option>
{% for c in clients %}
<option value="{{ c.id }}" {% if service.client_id == c.id %}selected{% endif %}>
{{ c.client_code }} - {{ c.company_name }}
</option>
{% endfor %}
</select>
</p>
<p>
Load from Template<br>
<select id="template_select">
<option value="">-- select template --</option>
{% for t in templates %}
<option
value="{{ t.id }}"
data-name="{{ t.template_name }}"
data-type="{{ t.service_type }}"
data-cycle="{{ t.billing_cycle }}"
data-currency="{{ t.currency_code }}"
data-recurring="{{ t.recurring_amount }}"
data-setup="{{ t.setup_amount }}"
data-description="{{ (t.description or '')|e }}"
>
{{ t.template_name }} ({{ t.recurring_amount|money(t.currency_code) }}{% if t.setup_amount and t.setup_amount != 0 %}, setup {{ t.setup_amount|money(t.currency_code) }}{% endif %})
</option>
{% endfor %}
</select>
</p>
<p>
Service Name *<br>
<input name="service_name" value="{{ service.service_name }}" required>
</p>
<p>
Service Type *<br>
<select name="service_type" required>
<option value="hosting" {% if service.service_type == 'hosting' %}selected{% endif %}>hosting</option>
<option value="rpc" {% if service.service_type == 'rpc' %}selected{% endif %}>rpc</option>
<option value="explorer" {% if service.service_type == 'explorer' %}selected{% endif %}>explorer</option>
<option value="node" {% if service.service_type == 'node' %}selected{% endif %}>node</option>
<option value="ipfs" {% if service.service_type == 'ipfs' %}selected{% endif %}>ipfs</option>
<option value="consulting" {% if service.service_type == 'consulting' %}selected{% endif %}>consulting</option>
<option value="crypto_infra" {% if service.service_type == 'crypto_infra' %}selected{% endif %}>Crypto Infra</option>
<option value="other" {% if service.service_type == 'other' %}selected{% endif %}>other</option>
</select>
</p>
<p>
Billing Cycle *<br>
<select name="billing_cycle" required>
<option value="one_time" {% if service.billing_cycle == 'one_time' %}selected{% endif %}>one_time</option>
<option value="monthly" {% if service.billing_cycle == 'monthly' %}selected{% endif %}>monthly</option>
<option value="quarterly" {% if service.billing_cycle == 'quarterly' %}selected{% endif %}>quarterly</option>
<option value="yearly" {% if service.billing_cycle == 'yearly' %}selected{% endif %}>yearly</option>
<option value="manual" {% if service.billing_cycle == 'manual' %}selected{% endif %}>manual</option>
</select>
</p>
<p>
Currency Code *<br>
<select name="currency_code" required>
<option value="CAD" {% if service.currency_code == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if service.currency_code == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if service.currency_code == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if service.currency_code == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Recurring Amount *<br>
<input type="number" step="0.00000001" min="0" name="recurring_amount" value="{{ service.recurring_amount }}" required>
</p>
<p>
Status *<br>
<select name="status" required>
<option value="pending" {% if service.status == 'pending' %}selected{% endif %}>pending</option>
<option value="active" {% if service.status == 'active' %}selected{% endif %}>active</option>
<option value="suspended" {% if service.status == 'suspended' %}selected{% endif %}>suspended</option>
<option value="cancelled" {% if service.status == 'cancelled' %}selected{% endif %}>cancelled</option>
</select>
</p>
<p>
Start Date<br>
<input type="date" name="start_date" value="{{ service.start_date }}">
</p>
<p>
Description<br>
<textarea name="description" rows="5" cols="60">{{ service.description or '' }}</textarea>
</p>
<p>
<button type="submit">Save Service</button>
</p>
</form>
<script>
(function() {
var select = document.getElementById("template_select");
if (!select) return;
select.addEventListener("change", function() {
var opt = this.options[this.selectedIndex];
if (!opt || !opt.dataset || !opt.dataset.name) return;
document.querySelector('[name="service_name"]').value = opt.dataset.name || '';
document.querySelector('[name="service_type"]').value = opt.dataset.type || 'other';
document.querySelector('[name="billing_cycle"]').value = opt.dataset.cycle || 'monthly';
document.querySelector('[name="currency_code"]').value = opt.dataset.currency || 'CAD';
document.querySelector('[name="recurring_amount"]').value = opt.dataset.recurring || '0.00000000';
document.querySelector('[name="description"]').value = opt.dataset.description || '';
});
})();
</script>
{% include "footer.html" %}
</body>
</html>
HTML
echo "===== create templates/service_templates/list.html ====="
cat > "$APP_ROOT/templates/service_templates/list.html" <<'HTML'
<!doctype html>
<html>
<head>
<title>Service Templates</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<h1>Service Templates</h1>
<p><a href="/">Home</a></p>
<p>
<a href="/services">Back to Services</a> |
<a href="/service-templates/new">Add Service Template</a>
</p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Template Name</th>
<th>Type</th>
<th>Cycle</th>
<th>Currency</th>
<th>Recurring</th>
<th>Setup</th>
<th>Active</th>
<th>Actions</th>
</tr>
{% for t in templates %}
<tr>
<td>{{ t.id }}</td>
<td>{{ t.template_name }}</td>
<td>{{ t.service_type }}</td>
<td>{{ t.billing_cycle }}</td>
<td>{{ t.currency_code }}</td>
<td>{{ t.recurring_amount|money(t.currency_code) }}</td>
<td>{{ t.setup_amount|money(t.currency_code) }}</td>
<td>{% if t.is_active %}yes{% else %}no{% endif %}</td>
<td><a href="/service-templates/edit/{{ t.id }}">Edit</a></td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>
HTML
echo "===== create templates/service_templates/new.html ====="
cat > "$APP_ROOT/templates/service_templates/new.html" <<'HTML'
<!doctype html>
<html>
<head>
<title>New Service Template</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<h1>Add Service Template</h1>
<p><a href="/">Home</a></p>
<p><a href="/service-templates">Back to Service Templates</a></p>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Template Name *<br>
<input name="template_name" required>
</p>
<p>
Service Type *<br>
<select name="service_type" required>
<option value="hosting">hosting</option>
<option value="rpc">rpc</option>
<option value="explorer">explorer</option>
<option value="node">node</option>
<option value="ipfs">ipfs</option>
<option value="consulting">consulting</option>
<option value="crypto_infra">Crypto Infra</option>
<option value="other">other</option>
</select>
</p>
<p>
Billing Cycle *<br>
<select name="billing_cycle" required>
<option value="one_time">one_time</option>
<option value="monthly" selected>monthly</option>
<option value="quarterly">quarterly</option>
<option value="yearly">yearly</option>
<option value="manual">manual</option>
</select>
</p>
<p>
Currency Code *<br>
<select name="currency_code" required>
<option value="CAD" selected>CAD</option>
<option value="ETHO">ETHO</option>
<option value="EGAZ">EGAZ</option>
<option value="ALT">ALT</option>
</select>
</p>
<p>
Recurring Amount *<br>
<input type="number" step="0.00000001" min="0" name="recurring_amount" value="0.00000000" required>
</p>
<p>
Setup Amount *<br>
<input type="number" step="0.00000001" min="0" name="setup_amount" value="0.00000000" required>
</p>
<p>
Description<br>
<textarea name="description" rows="6" cols="70"></textarea>
</p>
<p>
Active<br>
<select name="is_active">
<option value="1" selected>yes</option>
<option value="0">no</option>
</select>
</p>
<p>
<button type="submit">Create Service Template</button>
</p>
</form>
{% include "footer.html" %}
</body>
</html>
HTML
echo "===== create templates/service_templates/edit.html ====="
cat > "$APP_ROOT/templates/service_templates/edit.html" <<'HTML'
<!doctype html>
<html>
<head>
<title>Edit Service Template</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<h1>Edit Service Template</h1>
<p><a href="/">Home</a></p>
<p><a href="/service-templates">Back to Service Templates</a></p>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Template Name *<br>
<input name="template_name" value="{{ template.template_name }}" required>
</p>
<p>
Service Type *<br>
<select name="service_type" required>
<option value="hosting" {% if template.service_type == 'hosting' %}selected{% endif %}>hosting</option>
<option value="rpc" {% if template.service_type == 'rpc' %}selected{% endif %}>rpc</option>
<option value="explorer" {% if template.service_type == 'explorer' %}selected{% endif %}>explorer</option>
<option value="node" {% if template.service_type == 'node' %}selected{% endif %}>node</option>
<option value="ipfs" {% if template.service_type == 'ipfs' %}selected{% endif %}>ipfs</option>
<option value="consulting" {% if template.service_type == 'consulting' %}selected{% endif %}>consulting</option>
<option value="crypto_infra" {% if template.service_type == 'crypto_infra' %}selected{% endif %}>Crypto Infra</option>
<option value="other" {% if template.service_type == 'other' %}selected{% endif %}>other</option>
</select>
</p>
<p>
Billing Cycle *<br>
<select name="billing_cycle" required>
<option value="one_time" {% if template.billing_cycle == 'one_time' %}selected{% endif %}>one_time</option>
<option value="monthly" {% if template.billing_cycle == 'monthly' %}selected{% endif %}>monthly</option>
<option value="quarterly" {% if template.billing_cycle == 'quarterly' %}selected{% endif %}>quarterly</option>
<option value="yearly" {% if template.billing_cycle == 'yearly' %}selected{% endif %}>yearly</option>
<option value="manual" {% if template.billing_cycle == 'manual' %}selected{% endif %}>manual</option>
</select>
</p>
<p>
Currency Code *<br>
<select name="currency_code" required>
<option value="CAD" {% if template.currency_code == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if template.currency_code == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if template.currency_code == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if template.currency_code == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Recurring Amount *<br>
<input type="number" step="0.00000001" min="0" name="recurring_amount" value="{{ template.recurring_amount }}" required>
</p>
<p>
Setup Amount *<br>
<input type="number" step="0.00000001" min="0" name="setup_amount" value="{{ template.setup_amount }}" required>
</p>
<p>
Description<br>
<textarea name="description" rows="6" cols="70">{{ template.description or '' }}</textarea>
</p>
<p>
Active<br>
<select name="is_active">
<option value="1" {% if template.is_active %}selected{% endif %}>yes</option>
<option value="0" {% if not template.is_active %}selected{% endif %}>no</option>
</select>
</p>
<p>
<button type="submit">Save Service Template</button>
</p>
</form>
{% include "footer.html" %}
</body>
</html>
HTML
echo "===== verify syntax ====="
python3 -m py_compile "$APP_ROOT/backend/app.py"
echo "===== restart service ====="
sudo systemctl restart otb_billing.service
sleep 2
sudo systemctl --no-pager --full status otb_billing.service | sed -n '1,35p'
echo "===== verify routes present ====="
grep -nE '@app.route\("/service-templates"|def service_templates|def new_service_template|def edit_service_template' "$APP_ROOT/backend/app.py"
echo "===== verify template table ====="
sudo mysql -D otb_billing -e "SHOW TABLES LIKE 'service_templates';"
sudo mysql -D otb_billing -e "DESCRIBE service_templates;"
echo "===== completed ====="
echo "Backups saved under: $BACKUP_DIR"
EOF
chmod +x /tmp/otb_billing_service_templates_patch.sh
/tmp/otb_billing_service_templates_patch.sh