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