Browse Source

cleanup: add .gitignore and remove backup/script noise from repo

main
def 1 month ago
parent
commit
446462e0a0
  1. 28
      .gitignore
  2. 6603
      backend/app.py.attach-debug.20260326-020213.bak
  3. 6603
      backend/app.py.attachment-fix.20260326-021226.bak
  4. 6738
      backend/app.py.auto-expire-pending-v2.20260327-040852.bak
  5. 6698
      backend/app.py.auto-expire-pending.20260327-035958.bak
  6. 6698
      backend/app.py.auto-expire-pending.20260327-040203.bak
  7. 6642
      backend/app.py.correct-payment-block.20260326-043806.bak
  8. 6582
      backend/app.py.email-attachment.20260325-022911.bak
  9. 6596
      backend/app.py.email-attachment.20260325-023107.bak
  10. 6663
      backend/app.py.email-body-explorer-fix.20260326-033501.bak
  11. 6698
      backend/app.py.email-body-fix-clean.20260326-040645.bak
  12. 6610
      backend/app.py.email-explorer-safe.20260326-040915.bak
  13. 6604
      backend/app.py.envload.20260326-025333.bak
  14. 6663
      backend/app.py.explorer-links.20260326-032929.bak
  15. 6663
      backend/app.py.explorer-links.20260326-032943.bak
  16. 6688
      backend/app.py.final-email-cleanup.20260327-020603.bak
  17. 6739
      backend/app.py.fix-dead-pending.20260327-042733.bak
  18. 6682
      backend/app.py.fix-missing-pdf-import.20260326-050344.bak
  19. 6682
      backend/app.py.helper-pdf-route-fix.20260327-015238.bak
  20. 6682
      backend/app.py.helper-rebuild.20260326-050853.bak
  21. 6641
      backend/app.py.invoice-pdf-order-fix.20260326-041449.bak
  22. 6610
      backend/app.py.invoice-pdf-payments-fix.20260326-030703.bak
  23. 6642
      backend/app.py.payment-block-global-fix.20260326-043120.bak
  24. 6642
      backend/app.py.payment-block-live-fix.20260326-044117.bak
  25. 6611
      backend/app.py.payment-block-polish.20260326-032040.bak
  26. 6641
      backend/app.py.payment-email-and-pdf-fix.20260326-041126.bak
  27. 6681
      backend/app.py.payment-email-logging.20260326-045639.bak
  28. 6682
      backend/app.py.payment-layout-fix.20260327-013334.bak
  29. 6642
      backend/app.py.payment-lines-and-rate.20260326-042154.bak
  30. 6610
      backend/app.py.pdf-context-fix.20260326-030343.bak
  31. 6607
      backend/app.py.pdf-fix.20260326-030059.bak
  32. 6687
      backend/app.py.pending-payment-logic.20260327-184101.bak
  33. 6684
      backend/app.py.pre-circular-import-removal.20260326-050604.bak
  34. 6752
      backend/app.py.pre-indent-fix.20260327-042939.bak
  35. 6687
      backend/app.py.retry-email.20260327-023137.bak
  36. 6687
      backend/app.py.retry-email.20260327-023404.bak
  37. 6603
      backend/app.py.reveal-payment-email-error.20260326-024852.bak
  38. 6687
      backend/app.py.safe-pending-fix.20260327-043630.bak
  39. 74
      backend/auto_expire_patch.sh
  40. 44
      backend/auto_expire_patch_v2.sh
  41. 116
      backend/fix_pending_payment_logic.sh
  42. 29
      backend/fix_portal_indent.sh
  43. 26
      backend/recover_otb_billing.sh
  44. 1184
      build-backups/backup_fix_void_route_2026-03-08/app.py.bak
  45. 100
      build-backups/backup_fix_void_route_2026-03-08/payments_list.html.bak
  46. 1583
      build-backups/backup_logo_support_2026-03-09/app.py.bak
  47. 36
      build-backups/backup_logo_support_2026-03-09/dashboard.html.bak
  48. 202
      build-backups/backup_logo_support_2026-03-09/invoice_view.html.bak
  49. 169
      build-backups/backup_logo_support_2026-03-09/settings.html.bak
  50. 2342
      build-backups/backup_restore_email_layer_2026-03-09/app.py.bak
  51. 60
      build-backups/backup_restore_email_layer_2026-03-09/dashboard.html.bak
  52. 235
      build-backups/backup_restore_email_layer_2026-03-09/invoices_view.html.bak
  53. 91
      build-backups/backup_restore_email_layer_2026-03-09/revenue.html.bak
  54. 202
      build-backups/backup_restore_email_layer_2026-03-09/settings.html.bak
  55. 49
      shell-scripts/backpatch.sh
  56. 60
      shell-scripts/email-retry-v2.sh
  57. 51
      shell-scripts/email-retry.sh
  58. 63
      shell-scripts/production-safe.sh
  59. 51
      shell-scripts/update1.sh
  60. 256
      templates/invoices/view.html.square_button_20260313-055733.bak
  61. 187
      templates/portal_invoice_detail.html.square_button_20260313-055733.bak

28
.gitignore vendored

@ -1,15 +1,19 @@
# backups
*.bak
*.bak.*
# archives
*.zip
# local scripts (not part of app)
shell-scripts/
# temp patch / helper scripts
backend/*.sh
# python cache
__pycache__/
*.pyc
# env files (just in case later)
.env
venv/
.venv/
instance/
*.sqlite
*.log
# local backup/runtime clutter
backups/
run/
backup_pre_*/
*.bak.*
backend/app.py.fix_indent_backup

6603
backend/app.py.attach-debug.20260326-020213.bak

File diff suppressed because it is too large Load Diff

6603
backend/app.py.attachment-fix.20260326-021226.bak

File diff suppressed because it is too large Load Diff

6738
backend/app.py.auto-expire-pending-v2.20260327-040852.bak

File diff suppressed because it is too large Load Diff

6698
backend/app.py.auto-expire-pending.20260327-035958.bak

File diff suppressed because it is too large Load Diff

6698
backend/app.py.auto-expire-pending.20260327-040203.bak

File diff suppressed because it is too large Load Diff

6642
backend/app.py.correct-payment-block.20260326-043806.bak

File diff suppressed because it is too large Load Diff

6582
backend/app.py.email-attachment.20260325-022911.bak

File diff suppressed because it is too large Load Diff

6596
backend/app.py.email-attachment.20260325-023107.bak

File diff suppressed because it is too large Load Diff

6663
backend/app.py.email-body-explorer-fix.20260326-033501.bak

File diff suppressed because it is too large Load Diff

6698
backend/app.py.email-body-fix-clean.20260326-040645.bak

File diff suppressed because it is too large Load Diff

6610
backend/app.py.email-explorer-safe.20260326-040915.bak

File diff suppressed because it is too large Load Diff

6604
backend/app.py.envload.20260326-025333.bak

File diff suppressed because it is too large Load Diff

6663
backend/app.py.explorer-links.20260326-032929.bak

File diff suppressed because it is too large Load Diff

6663
backend/app.py.explorer-links.20260326-032943.bak

File diff suppressed because it is too large Load Diff

6688
backend/app.py.final-email-cleanup.20260327-020603.bak

File diff suppressed because it is too large Load Diff

6739
backend/app.py.fix-dead-pending.20260327-042733.bak

File diff suppressed because it is too large Load Diff

6682
backend/app.py.fix-missing-pdf-import.20260326-050344.bak

File diff suppressed because it is too large Load Diff

6682
backend/app.py.helper-pdf-route-fix.20260327-015238.bak

File diff suppressed because it is too large Load Diff

6682
backend/app.py.helper-rebuild.20260326-050853.bak

File diff suppressed because it is too large Load Diff

6641
backend/app.py.invoice-pdf-order-fix.20260326-041449.bak

File diff suppressed because it is too large Load Diff

6610
backend/app.py.invoice-pdf-payments-fix.20260326-030703.bak

File diff suppressed because it is too large Load Diff

6642
backend/app.py.payment-block-global-fix.20260326-043120.bak

File diff suppressed because it is too large Load Diff

6642
backend/app.py.payment-block-live-fix.20260326-044117.bak

File diff suppressed because it is too large Load Diff

6611
backend/app.py.payment-block-polish.20260326-032040.bak

File diff suppressed because it is too large Load Diff

6641
backend/app.py.payment-email-and-pdf-fix.20260326-041126.bak

File diff suppressed because it is too large Load Diff

6681
backend/app.py.payment-email-logging.20260326-045639.bak

File diff suppressed because it is too large Load Diff

6682
backend/app.py.payment-layout-fix.20260327-013334.bak

File diff suppressed because it is too large Load Diff

6642
backend/app.py.payment-lines-and-rate.20260326-042154.bak

File diff suppressed because it is too large Load Diff

6610
backend/app.py.pdf-context-fix.20260326-030343.bak

File diff suppressed because it is too large Load Diff

6607
backend/app.py.pdf-fix.20260326-030059.bak

File diff suppressed because it is too large Load Diff

6687
backend/app.py.pending-payment-logic.20260327-184101.bak

File diff suppressed because it is too large Load Diff

6684
backend/app.py.pre-circular-import-removal.20260326-050604.bak

File diff suppressed because it is too large Load Diff

6752
backend/app.py.pre-indent-fix.20260327-042939.bak

File diff suppressed because it is too large Load Diff

6687
backend/app.py.retry-email.20260327-023137.bak

File diff suppressed because it is too large Load Diff

6687
backend/app.py.retry-email.20260327-023404.bak

File diff suppressed because it is too large Load Diff

6603
backend/app.py.reveal-payment-email-error.20260326-024852.bak

File diff suppressed because it is too large Load Diff

6687
backend/app.py.safe-pending-fix.20260327-043630.bak

File diff suppressed because it is too large Load Diff

74
backend/auto_expire_patch.sh

@ -1,74 +0,0 @@
#!/bin/bash
set -e
STAMP="$(date +%Y%m%d-%H%M%S)"
cp app.py "app.py.auto-expire-pending.${STAMP}.bak"
python3 <<'PY'
from pathlib import Path
p = Path("app.py")
text = p.read_text()
anchor = "def portal_invoice"
if anchor not in text:
raise SystemExit("FAILED: portal_invoice route not found")
# find insertion point (start of function body)
idx = text.index(anchor)
start = text.index(":", idx) + 1
inject_code = '''
# === AUTO-EXPIRE STALE PENDING CRYPTO PAYMENTS ===
try:
from datetime import datetime, timedelta
conn2 = get_db_connection()
cur2 = conn2.cursor(dictionary=True)
cur2.execute(
"SELECT id, payment_status, txid, created_at "
"FROM payments "
"WHERE invoice_id = %s AND payment_method = 'crypto' "
"ORDER BY id DESC LIMIT 1",
(invoice_id,)
)
last_payment = cur2.fetchone()
if last_payment:
is_pending = str(last_payment.get("payment_status") or "").lower() == "pending"
has_tx = bool(last_payment.get("txid"))
created_at = last_payment.get("created_at")
is_expired = False
if created_at:
is_expired = datetime.utcnow() > (created_at + timedelta(minutes=15))
if is_pending and not has_tx and is_expired:
cur2.execute(
"UPDATE payments SET payment_status = 'expired' WHERE id = %s",
(last_payment["id"],)
)
conn2.commit()
print(f"[auto-expire] expired stale payment id={last_payment['id']} invoice_id={invoice_id}")
conn2.close()
except Exception as e:
print(f"[auto-expire] error: {e}")
# === END AUTO-EXPIRE ===
'''
text = text[:start] + inject_code + text[start:]
p.write_text(text)
print("OK: auto-expire logic injected cleanly")
PY
python3 -m py_compile app.py
echo "PY_COMPILE_OK"
sudo systemctl restart otb_billing
sudo systemctl status otb_billing --no-pager -l

44
backend/auto_expire_patch_v2.sh

@ -1,44 +0,0 @@
#!/bin/bash
set -e
STAMP="$(date +%Y%m%d-%H%M%S)"
cp app.py "app.py.auto-expire-pending-v2.${STAMP}.bak"
python3 <<'PY'
from pathlib import Path
p = Path("app.py")
text = p.read_text()
old = """ cur2.execute(
"SELECT id, payment_status, txid, created_at "
"FROM payments "
"WHERE invoice_id = %s AND payment_method = 'crypto' "
"ORDER BY id DESC LIMIT 1",
(invoice_id,)
)
"""
new = """ cur2.execute(
"SELECT id, payment_method, payment_currency, payment_status, txid, created_at "
"FROM payments "
"WHERE invoice_id = %s "
"AND UPPER(COALESCE(payment_currency,'')) IN ('ETHO','ETI','EGAZ','ETH','ARB') "
"ORDER BY id DESC LIMIT 1",
(invoice_id,)
)
"""
if old not in text:
raise SystemExit("FAILED: old auto-expire query not found")
text = text.replace(old, new, 1)
p.write_text(text)
print("OK: auto-expire query updated for real crypto rows")
PY
python3 -m py_compile app.py
echo "PY_COMPILE_OK"
sudo systemctl restart otb_billing
sudo systemctl status otb_billing --no-pager -l

116
backend/fix_pending_payment_logic.sh

@ -1,116 +0,0 @@
#!/bin/bash
set -e
cd /home/def/otb_billing/backend || exit 1
STAMP="$(date +%Y%m%d-%H%M%S)"
cp app.py "app.py.pending-payment-logic.${STAMP}.bak"
python3 <<'PY'
from pathlib import Path
p = Path("app.py")
text = p.read_text()
old_block_1 = """ payment_id = (request.args.get("payment_id") or "").strip()
if not payment_id and pending_crypto_payment:
payment_id = str(pending_crypto_payment.get("id") or "").strip()
if payment_id.isdigit():
"""
new_block_1 = """ payment_id = (request.args.get("payment_id") or "").strip()
if not payment_id and pending_crypto_payment:
stale_pending_without_tx = False
try:
created_dt = pending_crypto_payment.get("created_at")
if created_dt and created_dt.tzinfo is None:
created_dt = created_dt.replace(tzinfo=timezone.utc)
stale_pending_without_tx = (
str(pending_crypto_payment.get("payment_status") or "").lower() == "pending"
and not pending_crypto_payment.get("txid")
and created_dt is not None
and datetime.now(timezone.utc) >= (created_dt + timedelta(minutes=2))
)
except Exception:
stale_pending_without_tx = False
if stale_pending_without_tx:
try:
cursor.execute(\"\"\"
UPDATE payments
SET payment_status = 'failed'
WHERE id = %s
AND payment_status = 'pending'
AND (txid IS NULL OR txid = '')
\"\"\", (pending_crypto_payment["id"],))
conn.commit()
except Exception:
pass
pending_crypto_payment = None
else:
payment_id = str(pending_crypto_payment.get("id") or "").strip()
if payment_id.isdigit():
"""
if old_block_1 not in text:
raise SystemExit("FAILED: block 1 not found")
text = text.replace(old_block_1, new_block_1, 1)
old_block_2 = """ else:
pending_crypto_payment["created_at_local"] = ""
pending_crypto_payment["lock_expires_at_local"] = ""
pending_crypto_payment["lock_expires_at_iso"] = ""
pending_crypto_payment["lock_expired"] = True
received_dt = pending_crypto_payment.get("received_at")
"""
new_block_2 = """ else:
pending_crypto_payment["created_at_local"] = ""
pending_crypto_payment["lock_expires_at_local"] = ""
pending_crypto_payment["lock_expires_at_iso"] = ""
pending_crypto_payment["lock_expired"] = True
if (
str(pending_crypto_payment.get("payment_status") or "").lower() == "pending"
and not pending_crypto_payment.get("txid")
and pending_crypto_payment.get("lock_expired")
):
try:
cursor.execute(\"\"\"
UPDATE payments
SET payment_status = 'failed'
WHERE id = %s
AND payment_status = 'pending'
AND (txid IS NULL OR txid = '')
\"\"\", (pending_crypto_payment["id"],))
conn.commit()
except Exception:
pass
pending_crypto_payment = None
payment_id = ""
received_dt = pending_crypto_payment.get("received_at") if pending_crypto_payment else None
"""
if old_block_2 not in text:
raise SystemExit("FAILED: block 2 not found")
text = text.replace(old_block_2, new_block_2, 1)
p.write_text(text)
print("OK: pending payment logic patched")
PY
python3 -m py_compile app.py
echo "PY_COMPILE_OK"
sudo systemctl restart otb_billing
sudo systemctl status otb_billing --no-pager -l
echo
echo "===== verify patched area ====="
sed -n '5220,5325p' app.py

29
backend/fix_portal_indent.sh

@ -1,29 +0,0 @@
#!/bin/bash
set -e
python3 <<'PY'
from pathlib import Path
import re
p = Path("app.py")
text = p.read_text()
pattern = re.compile(
r"\n\s*# === KILL DEAD PENDING PAYMENT \(no txid \+ expired lock\) ===.*?"
r'print\("\[dead pending cleanup error\]", e\)\n',
re.DOTALL
)
new_text, count = pattern.subn("\n", text, count=1)
if count == 0:
raise SystemExit("FAILED: broken dead-pending block not found")
p.write_text(new_text)
print("OK: removed broken dead-pending block")
PY
python3 -m py_compile app.py
echo "PY_COMPILE_OK"
sudo systemctl restart otb_billing
sudo systemctl status otb_billing --no-pager -l

26
backend/recover_otb_billing.sh

@ -1,26 +0,0 @@
#!/bin/bash
set -e
cd /home/def/otb_billing/backend || exit 1
GOOD_BAK="app.py.retry-email.20260327-023404.bak"
echo "===== restoring known-good app.py ====="
cp "$GOOD_BAK" app.py
echo
echo "===== compile check ====="
python3 -m py_compile app.py
echo "PY_COMPILE_OK"
echo
echo "===== restart service ====="
sudo systemctl restart otb_billing
echo
echo "===== service status ====="
sudo systemctl status otb_billing --no-pager -l
echo
echo "===== last 20 journal lines ====="
sudo journalctl -u otb_billing.service -n 20 --no-pager

1184
build-backups/backup_fix_void_route_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

100
build-backups/backup_fix_void_route_2026-03-08/payments_list.html.bak

@ -1,100 +0,0 @@
<!doctype html>
<html>
<head>
<title>Payments</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-confirmed { background: #dcfce7; color: #166534; }
.status-reversed { background: #fee2e2; color: #991b1b; }
.invoice-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.invoice-pending { background: #dbeafe; color: #1d4ed8; }
.invoice-partial { background: #fef3c7; color: #92400e; }
.invoice-paid { background: #dcfce7; color: #166534; }
.invoice-overdue { background: #fee2e2; color: #991b1b; }
.invoice-cancelled { background: #e5e7eb; color: #4b5563; }
.invoice-draft { background: #e5e7eb; color: #111827; }
.inline-form {
display: inline;
margin: 0;
}
.void-btn {
background: #991b1b;
color: white;
border: 0;
padding: 4px 8px;
cursor: pointer;
}
.void-btn:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<h1>Payments</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments/new">Record Payment</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Method</th>
<th>Currency</th>
<th>Amount</th>
<th>CAD Value</th>
<th>Payment Status</th>
<th>Invoice Status</th>
<th>Received</th>
<th>Actions</th>
</tr>
{% for p in payments %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.invoice_number }}</td>
<td>{{ p.client_code }} - {{ p.company_name }}</td>
<td>{{ p.payment_method }}</td>
<td>{{ p.payment_currency }}</td>
<td>{{ p.payment_amount|money(p.payment_currency) }}</td>
<td>{{ p.cad_value_at_payment|money('CAD') }}</td>
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td>
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td>
<td>{{ p.received_at|localtime }}</td>
<td>
<a href="/payments/edit/{{ p.id }}">Edit</a>
{% if p.payment_status == 'confirmed' %}
|
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');">
<button type="submit" class="void-btn">Void</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1583
build-backups/backup_logo_support_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

36
build-backups/backup_logo_support_2026-03-09/dashboard.html.bak

@ -1,36 +0,0 @@
<!doctype html>
<html>
<head>
<title>OTB Billing Dashboard</title>
</head>
<body>
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1>
<p><a href="/clients">Clients</a></p>
<p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p>
<p><a href="/settings">Settings / Config</a></p>
<p><a href="/dbtest">DB Test</a></p>
<table border="1" cellpadding="10">
<tr>
<th>Total Clients</th>
<th>Active Services</th>
<th>Outstanding Invoices</th>
<th>Revenue Received (CAD)</th>
</tr>
<tr>
<td>{{ total_clients }}</td>
<td>{{ active_services }}</td>
<td>{{ outstanding_invoices }}</td>
<td>{{ revenue_received|money('CAD') }}</td>
</tr>
</table>
<p>Displayed times are shown in Eastern Time (Toronto).</p>
{% include "footer.html" %}
</body>
</html>

202
build-backups/backup_logo_support_2026-03-09/invoice_view.html.bak

@ -1,202 +0,0 @@
<!doctype html>
<html>
<head>
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 30px;
color: #111;
}
.top-links {
margin-bottom: 20px;
}
.top-links a {
margin-right: 15px;
}
.invoice-wrap {
max-width: 900px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.title-box h1 {
margin: 0 0 8px 0;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 25px;
}
.info-card {
border: 1px solid #ccc;
padding: 15px;
}
.info-card h3 {
margin-top: 0;
}
.summary-table,
.total-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}
.summary-table th,
.summary-table td,
.total-table th,
.total-table td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.total-table {
max-width: 420px;
margin-left: auto;
}
.notes-box {
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
margin-top: 20px;
}
.print-only {
display: none;
}
@media print {
.top-links,
.screen-only,
footer {
display: none !important;
}
.print-only {
display: block;
}
body {
margin: 0;
}
}
</style>
</head>
<body>
<div class="invoice-wrap">
<div class="top-links screen-only">
<a href="/">Home</a>
<a href="/invoices">Back to Invoices</a>
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a>
<a href="#" onclick="window.print(); return false;">Print</a>
<a href="/invoices/pdf/{{ invoice.id }}">PDF</a>
</div>
<div class="header-row">
<div class="title-box">
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
<div style="text-align:right;">
<strong>{{ settings.business_name or 'OTB Billing' }}</strong><br>
{{ settings.business_tagline or '' }}<br>
{% if settings.business_address %}{{ settings.business_address }}<br>{% endif %}
{% if settings.business_email %}{{ settings.business_email }}<br>{% endif %}
{% if settings.business_phone %}{{ settings.business_phone }}<br>{% endif %}
{% if settings.business_website %}{{ settings.business_website }}{% endif %}
</div>
</div>
<div class="info-grid">
<div class="info-card">
<h3>Bill To</h3>
<strong>{{ invoice.company_name }}</strong><br>
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %}
{% if invoice.email %}{{ invoice.email }}<br>{% endif %}
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %}
Client Code: {{ invoice.client_code }}
</div>
<div class="info-card">
<h3>Invoice Details</h3>
Invoice #: {{ invoice.invoice_number }}<br>
Issued: {{ invoice.issued_at|localtime }}<br>
Due: {{ invoice.due_at|localtime }}<br>
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %}
Currency: {{ invoice.currency_code }}<br>
{% if settings.tax_number %}{{ settings.tax_label or 'Tax' }} Number: {{ settings.tax_number }}<br>{% endif %}
{% if settings.business_number %}Business Number: {{ settings.business_number }}{% endif %}
</div>
</div>
<table class="summary-table">
<tr>
<th>Service Code</th>
<th>Service</th>
<th>Description</th>
<th>Total</th>
</tr>
<tr>
<td>{{ invoice.service_code or '-' }}</td>
<td>{{ invoice.service_name or '-' }}</td>
<td>{{ invoice.notes or '-' }}</td>
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
<table class="total-table">
<tr>
<th>Subtotal</th>
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>{{ settings.tax_label or 'Tax' }}</th>
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Total</th>
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td>
</tr>
<tr>
<th>Paid</th>
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Remaining</th>
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
{% if settings.payment_terms %}
<div class="notes-box">
<strong>Payment Terms</strong><br><br>
{{ settings.payment_terms }}
</div>
{% endif %}
{% if settings.invoice_footer %}
<div class="notes-box">
<strong>Footer</strong><br><br>
{{ settings.invoice_footer }}
</div>
{% endif %}
</div>
{% include "footer.html" %}
</body>
</html>

169
build-backups/backup_logo_support_2026-03-09/settings.html.bak

@ -1,169 +0,0 @@
<!doctype html>
<html>
<head>
<title>Settings</title>
<style>
body { font-family: Arial, sans-serif; }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1100px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
width: 100%;
box-sizing: border-box;
margin-top: 4px;
margin-bottom: 12px;
padding: 8px;
}
textarea { min-height: 90px; }
.checkbox-row {
margin: 8px 0 14px 0;
}
.save-row {
margin-top: 18px;
}
</style>
</head>
<body>
<h1>Settings / Config</h1>
<p><a href="/">Home</a></p>
<form method="post">
<div class="form-grid">
<div class="card">
<h2>Business Identity</h2>
Business Name<br>
<input type="text" name="business_name" value="{{ settings.business_name }}"><br>
Slogan / Tagline<br>
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br>
Business Email<br>
<input type="email" name="business_email" value="{{ settings.business_email }}"><br>
Business Phone<br>
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br>
Business Address<br>
<textarea name="business_address">{{ settings.business_address }}</textarea><br>
Website<br>
<input type="text" name="business_website" value="{{ settings.business_website }}"><br>
Business Number / Registration Number<br>
<input type="text" name="business_number" value="{{ settings.business_number }}"><br>
Default Currency<br>
<select name="default_currency">
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option>
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option>
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</div>
<div class="card">
<h2>Tax Settings</h2>
Local Country<br>
<input type="text" name="local_country" value="{{ settings.local_country }}"><br>
Tax Label<br>
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br>
Tax Rate (%)<br>
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br>
Tax Number<br>
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}>
Apply tax only to local clients
</label>
</div>
Payment Terms<br>
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br>
Invoice Footer<br>
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br>
</div>
<div class="card">
<h2>Email / SMTP</h2>
SMTP Host<br>
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br>
SMTP Port<br>
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br>
SMTP Username<br>
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br>
SMTP Password<br>
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br>
From Email<br>
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br>
From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>
Use TLS
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}>
Use SSL
</label>
</div>
</div>
<div class="card">
<h2>Notes</h2>
<p>
These settings become the identity and delivery configuration for this installation.
</p>
<p>
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
</p>
<p>
Tax settings are also stored now so invoice and automation logic can use them later.
</p>
</div>
</div>
<div class="save-row">
<button type="submit">Save Settings</button>
</div>
</form>
{% include "footer.html" %}
</body>
</html>

2342
build-backups/backup_restore_email_layer_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

60
build-backups/backup_restore_email_layer_2026-03-09/dashboard.html.bak

@ -1,60 +0,0 @@
<!doctype html>
<html>
<head>
<title>OTB Billing Dashboard</title>
</head>
<body>
{% if app_settings.business_logo_url %}
<div style="margin-bottom:15px;">
<img src="{{ app_settings.business_logo_url }}" style="height:60px;">
</div>
{% endif %}
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1>
{% if request.args.get('pkg_email') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package emailed successfully.
</div>
{% endif %}
{% if request.args.get('pkg_email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package email failed. Check SMTP settings or server log.
</div>
{% endif %}
{% if request.args.get('pkg_email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package email failed. Check SMTP settings or server log.
</div>
{% endif %}
<p><a href="/clients">Clients</a></p>
<p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p>
<p><a href="/reports/revenue">Revenue Report</a></p>
<p><a href="/reports/accounting-package.zip">Monthly Accounting Package</a></p>
<form method="post" action="/reports/accounting-package/email" style="margin:0 0 16px 0;"><button type="submit">Email Accounting Package</button></form>
<p><a href="/settings">Settings / Config</a></p>
<p><a href="/dbtest">DB Test</a></p>
<table border="1" cellpadding="10">
<tr>
<th>Total Clients</th>
<th>Active Services</th>
<th>Outstanding Invoices</th>
<th>Revenue Received (CAD)</th>
</tr>
<tr>
<td>{{ total_clients }}</td>
<td>{{ active_services }}</td>
<td>{{ outstanding_invoices }}</td>
<td>{{ revenue_received|money('CAD') }}</td>
</tr>
</table>
<p>Displayed times are shown in Eastern Time (Toronto).</p>
{% include "footer.html" %}
</body>
</html>

235
build-backups/backup_restore_email_layer_2026-03-09/invoices_view.html.bak

@ -1,235 +0,0 @@
<!doctype html>
<html>
<head>
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 30px;
color: #111;
}
.top-links {
margin-bottom: 20px;
}
.top-links a {
margin-right: 15px;
}
.invoice-wrap {
max-width: 900px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.title-box h1 {
margin: 0 0 8px 0;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 25px;
}
.info-card {
border: 1px solid #ccc;
padding: 15px;
}
.info-card h3 {
margin-top: 0;
}
.summary-table,
.total-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}
.summary-table th,
.summary-table td,
.total-table th,
.total-table td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.total-table {
max-width: 420px;
margin-left: auto;
}
.notes-box {
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
margin-top: 20px;
}
.print-only {
display: none;
}
@media print {
.top-links,
.screen-only,
footer {
display: none !important;
}
.print-only {
display: block;
}
body {
margin: 0;
}
}
</style>
</head>
<body>
<div class="invoice-wrap">
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;">
Invoice email sent successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;">
Invoice email failed. Check SMTP settings or server log.
</div>
{% endif %}
<div class="top-links screen-only">
<a href="/">Home</a>
<a href="/invoices">Back to Invoices</a>
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a>
<a href="#" onclick="window.print(); return false;">Print</a>
<a href="/invoices/pdf/{{ invoice.id }}">PDF</a>
{% if invoice.email %}
<form method="post" action="/invoices/email/{{ invoice.id }}" style="display:inline;">
<button type="submit">Send Email</button>
</form>
{% endif %}
</div>
<div class="header-row">
{% if settings.business_logo_url %}
<img src="{{ settings.business_logo_url }}" style="height:70px;margin-bottom:10px;">
{% endif %}
<div class="title-box">
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
<div style="text-align:right;">
<strong>{{ settings.business_name or 'OTB Billing' }}</strong><br>
{{ settings.business_tagline or '' }}<br>
{% if settings.business_address %}{{ settings.business_address }}<br>{% endif %}
{% if settings.business_email %}{{ settings.business_email }}<br>{% endif %}
{% if settings.business_phone %}{{ settings.business_phone }}<br>{% endif %}
{% if settings.business_website %}{{ settings.business_website }}{% endif %}
</div>
</div>
<div class="info-grid">
<div class="info-card">
<h3>Bill To</h3>
<strong>{{ invoice.company_name }}</strong><br>
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %}
{% if invoice.email %}{{ invoice.email }}<br>{% endif %}
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %}
Client Code: {{ invoice.client_code }}
</div>
<div class="info-card">
<h3>Invoice Details</h3>
Invoice #: {{ invoice.invoice_number }}<br>
Issued: {{ invoice.issued_at|localtime }}<br>
Due: {{ invoice.due_at|localtime }}<br>
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %}
Currency: {{ invoice.currency_code }}<br>
{% if settings.tax_number %}{{ settings.tax_label or 'Tax' }} Number: {{ settings.tax_number }}<br>{% endif %}
{% if settings.business_number %}Business Number: {{ settings.business_number }}{% endif %}
</div>
</div>
<table class="summary-table">
<tr>
<th>Service Code</th>
<th>Service</th>
<th>Description</th>
<th>Total</th>
</tr>
<tr>
<td>{{ invoice.service_code or '-' }}</td>
<td>{{ invoice.service_name or '-' }}</td>
<td>{{ invoice.notes or '-' }}</td>
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
<table class="total-table">
<tr>
<th>Subtotal</th>
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>{{ settings.tax_label or 'Tax' }}</th>
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Total</th>
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td>
</tr>
<tr>
<th>Paid</th>
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Remaining</th>
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
{% if latest_email_log %}
<div class="notes-box">
<strong>Latest Email Activity</strong><br><br>
Status: {{ latest_email_log.status }}<br>
Recipient: {{ latest_email_log.recipient_email }}<br>
Subject: {{ latest_email_log.subject }}<br>
Sent At: {{ latest_email_log.sent_at|localtime }}<br>
{% if latest_email_log.error_message %}
Error: {{ latest_email_log.error_message }}
{% endif %}
</div>
{% endif %}
{% if settings.payment_terms %}
<div class="notes-box">
<strong>Payment Terms</strong><br><br>
{{ settings.payment_terms }}
</div>
{% endif %}
{% if settings.invoice_footer %}
<div class="notes-box">
<strong>Footer</strong><br><br>
{{ settings.invoice_footer }}
</div>
{% endif %}
</div>
{% include "footer.html" %}
</body>
</html>

91
build-backups/backup_restore_email_layer_2026-03-09/revenue.html.bak

@ -1,91 +0,0 @@
<!doctype html>
<html>
<head>
<title>Revenue Report</title>
<style>
body { font-family: Arial, sans-serif; }
.report-grid {
display: grid;
grid-template-columns: repeat(2, minmax(260px, 1fr));
gap: 18px;
max-width: 900px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
margin-bottom: 10px;
}
.value {
font-size: 28px;
font-weight: bold;
}
.action-links a {
margin-right: 16px;
}
</style>
</head>
<body>
<h1>Revenue Report</h1>
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report JSON emailed successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report email failed. Check SMTP settings or server log.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report email failed. Check SMTP settings or server log.
</div>
{% endif %}
<p><a href="/">Home</a></p>
<div class="action-links">
<a href="/reports/revenue.json">Export JSON</a>
<a href="/reports/revenue/print">Print Report Now</a>
<form method="post" action="/reports/revenue/email" style="display:inline;">
<button type="submit">Email JSON Report</button>
</form>
</div>
<p>
Frequency: <strong>{{ report.frequency }}</strong><br>
Period: <strong>{{ report.period_label }}</strong>
</p>
<div class="report-grid">
<div class="card">
<h2>Collected (CAD)</h2>
<div class="value">{{ report.collected_cad|money('CAD') }}</div>
</div>
<div class="card">
<h2>Invoices Issued</h2>
<div class="value">{{ report.invoice_count }}</div>
<div>{{ report.invoiced_total|money('CAD') }} CAD total</div>
</div>
<div class="card">
<h2>Outstanding Invoices</h2>
<div class="value">{{ report.outstanding_count }}</div>
<div>{{ report.outstanding_balance|money('CAD') }} CAD outstanding</div>
</div>
<div class="card">
<h2>Overdue Invoices</h2>
<div class="value">{{ report.overdue_count }}</div>
<div>{{ report.overdue_balance|money('CAD') }} CAD overdue</div>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

202
build-backups/backup_restore_email_layer_2026-03-09/settings.html.bak

@ -1,202 +0,0 @@
<!doctype html>
<html>
<head>
<title>Settings</title>
<style>
body { font-family: Arial, sans-serif; }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1100px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
width: 100%;
box-sizing: border-box;
margin-top: 4px;
margin-bottom: 12px;
padding: 8px;
}
textarea { min-height: 90px; }
.checkbox-row {
margin: 8px 0 14px 0;
}
.save-row {
margin-top: 18px;
}
.logo-preview {
margin: 10px 0 14px 0;
}
.logo-preview img {
max-height: 70px;
max-width: 220px;
border: 1px solid #ccc;
padding: 6px;
background: #fff;
}
small {
color: #444;
}
</style>
</head>
<body>
<h1>Settings / Config</h1>
<p><a href="/">Home</a></p>
<form method="post">
<div class="form-grid">
<div class="card">
<h2>Business Identity</h2>
Business Name<br>
<input type="text" name="business_name" value="{{ settings.business_name }}"><br>
Business Logo URL<br>
<input type="text" name="business_logo_url" value="{{ settings.business_logo_url }}"><br>
<small>Example: /static/favicon.png or https://site.com/logo.png</small><br>
{% if settings.business_logo_url %}
<div class="logo-preview">
<img src="{{ settings.business_logo_url }}" alt="Business Logo Preview">
</div>
{% endif %}
Slogan / Tagline<br>
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br>
Business Email<br>
<input type="email" name="business_email" value="{{ settings.business_email }}"><br>
Business Phone<br>
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br>
Business Address<br>
<textarea name="business_address">{{ settings.business_address }}</textarea><br>
Website<br>
<input type="text" name="business_website" value="{{ settings.business_website }}"><br>
Business Number / Registration Number<br>
<input type="text" name="business_number" value="{{ settings.business_number }}"><br>
Default Currency<br>
<select name="default_currency">
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option>
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option>
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
Report Frequency<br>
<select name="report_frequency">
<option value="monthly" {% if settings.report_frequency == 'monthly' %}selected{% endif %}>monthly</option>
<option value="quarterly" {% if settings.report_frequency == 'quarterly' %}selected{% endif %}>quarterly</option>
<option value="yearly" {% if settings.report_frequency == 'yearly' %}selected{% endif %}>yearly</option>
</select>
</div>
<div class="card">
<h2>Tax Settings</h2>
Local Country<br>
<input type="text" name="local_country" value="{{ settings.local_country }}"><br>
Tax Label<br>
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br>
Tax Rate (%)<br>
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br>
Tax Number<br>
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}>
Apply tax only to local clients
</label>
</div>
Payment Terms<br>
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br>
Invoice Footer<br>
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br>
</div>
<div class="card">
<h2>Advanced / Email / SMTP</h2>
SMTP Host<br>
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br>
SMTP Port<br>
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br>
SMTP Username<br>
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br>
SMTP Password<br>
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br>
From Email<br>
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br>
From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br>
Report / Accounting Delivery Email<br>
<input type="email" name="report_delivery_email" value="{{ settings.report_delivery_email }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>
Use TLS
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}>
Use SSL
</label>
</div>
</div>
<div class="card">
<h2>Notes</h2>
<p>
Branding, tax identity, and SMTP values are stored here for this installation.
</p>
<p>
Logo can be a local static path like <strong>/static/favicon.png</strong> or a full external/IPFS URL.
</p>
<p>
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
</p>
</div>
</div>
<div class="save-row">
<button type="submit">Save Settings</button>
</div>
</form>
{% include "footer.html" %}
</body>
</html>

49
shell-scripts/backpatch.sh

@ -1,49 +0,0 @@
cd /home/def/otb_billing/backend || exit 1
set -e
STAMP="$(date +%Y%m%d-%H%M%S)"
cp app.py "app.py.fix-dead-pending.${STAMP}.bak"
python3 <<'PY'
from pathlib import Path
p = Path("app.py")
text = p.read_text()
needle = "if payment_id.isdigit():"
if needle not in text:
raise SystemExit("FAILED: anchor not found")
inject = """
# === KILL DEAD PENDING PAYMENT (no txid + expired lock) ===
if pending_crypto_payment:
try:
txid = pending_crypto_payment.get("txid")
lock_expired = pending_crypto_payment.get("lock_expired")
if (not txid) and lock_expired:
pending_crypto_payment = None
payment_id = ""
except Exception as e:
print("[dead pending cleanup error]", e)
"""
text = text.replace(needle, inject + "\n" + needle, 1)
p.write_text(text)
print("OK: dead pending cleanup injected")
PY
echo "===== compile check ====="
python3 -m py_compile app.py && echo "PY_COMPILE_OK"
echo "===== restart service ====="
sudo systemctl restart otb_billing
echo "===== status ====="
sudo systemctl status otb_billing --no-pager -l
echo
echo "DONE"
echo "Now refresh your invoice page"

60
shell-scripts/email-retry-v2.sh

@ -1,60 +0,0 @@
cd /home/def/otb_billing/backend || exit 1
STAMP="$(date +%Y%m%d-%H%M%S)"
cp app.py "app.py.retry-email.${STAMP}.bak"
python3 <<'PY'
from pathlib import Path
p = Path("app.py")
text = p.read_text()
old = ''' send_configured_email(
to_email=invoice_email_row.get("email"),
subject=subject,
body=body,
attachments=attachments,
email_type="payment_received",
invoice_id=invoice_id
)
return True
except Exception:
return False
'''
new = ''' import time
for attempt in range(3):
try:
send_configured_email(
to_email=invoice_email_row.get("email"),
subject=subject,
body=body,
attachments=attachments,
email_type="payment_received",
invoice_id=invoice_id
)
return True
except Exception as e:
print(f"[email retry] invoice_id={invoice_id} attempt={attempt+1} error={type(e).__name__}: {e}")
if attempt < 2:
time.sleep(2)
print(f"[send_payment_received_email] FAILED after retries invoice_id={invoice_id}")
return False
except Exception:
return False
'''
if old not in text:
raise SystemExit("FAILED: exact helper send block not found")
text = text.replace(old, new, 1)
p.write_text(text)
print("OK: retry logic added to helper")
PY
python3 -m py_compile app.py && echo "PY_COMPILE_OK" || echo "PY_COMPILE_FAILED"
sudo systemctl restart otb_billing
sudo systemctl status otb_billing --no-pager -l

51
shell-scripts/email-retry.sh

@ -1,51 +0,0 @@
cd /home/def/otb_billing/backend || exit 1
STAMP="$(date +%Y%m%d-%H%M%S)"
cp app.py "app.py.retry-email.${STAMP}.bak"
python3 <<'PY'
from pathlib import Path
p = Path("app.py")
text = p.read_text()
old = """ send_configured_email(
to_email=invoice_email_row.get("client_email"),
subject=subject,
html_body=html,
attachments=attachments
)
return True
except Exception:
return False
"""
new = """ import time
for attempt in range(3):
try:
send_configured_email(
to_email=invoice_email_row.get("client_email"),
subject=subject,
html_body=html,
attachments=attachments
)
return True
except Exception as e:
print(f"[email retry] attempt {attempt+1} failed: {e}")
time.sleep(2)
print(f"[send_payment_received_email] FAILED after retries invoice_id={invoice_id}")
return False
"""
if old not in text:
raise SystemExit("FAILED: send email block not found")
text = text.replace(old, new, 1)
p.write_text(text)
print("OK: retry logic added")
PY
python3 -m py_compile app.py && echo "PY_COMPILE_OK" || echo "PY_COMPILE_FAILED"
sudo systemctl restart otb_billing

63
shell-scripts/production-safe.sh

@ -1,63 +0,0 @@
cd /home/def/otb_billing || exit 1
set -e
NEWVER="v0.5.1"
STAMP="$(date '+%Y-%m-%d %H:%M:%S')"
ZIPNAME="otb_billing-${NEWVER}.zip"
echo "===== git status ====="
git status --short || true
echo
echo "===== update VERSION ====="
echo "${NEWVER}" > VERSION
echo
echo "===== backup README ====="
cp README.md "README.md.bak.${NEWVER}"
echo
echo "===== prepend README entry ====="
python3 <<'PY'
from pathlib import Path
from datetime import datetime
p = Path("README.md")
old = p.read_text()
entry = f"""## {datetime.now().strftime('%Y-%m-%d')} — v0.5.1
- Fixed crypto payment email auto-send path
- Fixed payment-received emails to attach the real invoice PDF
- Switched helper PDF generation to use the working invoice PDF route
- Added explorer link into the payment-received email body
- Improved invoice PDF payment section with time, TXID, wallet, and rate display
- Cleaned helper error handling back to safe non-debug behavior
"""
p.write_text(entry + "\n" + old)
print("README updated")
PY
echo
echo "===== git add ====="
git add .
echo
echo "===== git commit ====="
git commit -m "v0.5.1 - crypto email/pdf fixes and payment layout cleanup"
echo
echo "===== git push ====="
git push
echo
echo "===== build full zip backup ====="
cd /home/def || exit 1
rm -f "${ZIPNAME}"
zip -r "${ZIPNAME}" otb_billing >/dev/null
echo
echo "===== done ====="
echo "ZIP: /home/def/${ZIPNAME}"

51
shell-scripts/update1.sh

@ -1,51 +0,0 @@
cd /home/def/otb_billing || exit 1
set -e
NEWVER="v0.5.1"
STAMP="$(date '+%Y-%m-%d %H:%M:%S')"
ZIPNAME="otb_billing-${NEWVER}.zip"
echo "===== git status ====="
git status --short || true
echo "===== update VERSION ====="
echo "${NEWVER}" > VERSION
echo "===== update README.md ====="
cp README.md "README.md.bak.${NEWVER}"
python3 <<'PY'
from pathlib import Path
from datetime import datetime
p = Path("README.md")
text = p.read_text()
entry = f"""## {datetime.now().strftime('%Y-%m-%d')} — v0.5.1
- Fixed crypto payment email auto-send failure
- Replaced internal PDF generator call with route-based PDF fetch
- Restored PDF attachments in payment emails
- Improved Payments Applied layout in invoice PDF (multi-line details + rate display)
- Stabilized send_payment_received_email() (removed debug raise, safe failure handling)
"""
p.write_text(entry + "\n" + text)
print("OK: README updated")
PY
echo "===== git add ====="
git add .
echo "===== git commit ====="
git commit -m "v0.5.1 - crypto email fix, PDF attachment fix, payment layout improvements"
echo "===== git push ====="
git push
echo "===== build zip ====="
cd /home/def || exit 1
zip -r "${ZIPNAME}" otb_billing >/dev/null
echo "ZIP CREATED: /home/def/${ZIPNAME}"

256
templates/invoices/view.html.square_button_20260313-055733.bak

@ -1,256 +0,0 @@
<!doctype html>
<html>
<head>
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 30px;
color: #111;
}
.top-links {
margin-bottom: 20px;
}
.top-links a {
margin-right: 15px;
}
.invoice-wrap {
max-width: 900px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.title-box h1 {
margin: 0 0 8px 0;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 25px;
}
.info-card {
border: 1px solid #ccc;
padding: 15px;
}
.info-card h3 {
margin-top: 0;
}
.summary-table,
.total-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}
.summary-table th,
.summary-table td,
.total-table th,
.total-table td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.total-table {
max-width: 420px;
margin-left: auto;
}
.notes-box {
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
margin-top: 20px;
}
.print-only {
display: none;
}
@media print {
.top-links,
.screen-only,
footer {
display: none !important;
}
.print-only {
display: block;
}
body {
margin: 0;
}
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div class="invoice-wrap">
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;">
Invoice email sent successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;">
Invoice email failed. Check SMTP settings or server log.
</div>
{% endif %}
<div class="top-links screen-only">
<a href="/">Home</a>
<a href="/invoices">Back to Invoices</a>
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a>
<a href="#" onclick="window.print(); return false;">Print</a>
<a href="/invoices/pdf/{{ invoice.id }}">PDF</a>
{% if invoice.email %}
<form method="post" action="/invoices/email/{{ invoice.id }}" style="display:inline;">
<button type="submit">Send Email</button>
</form>
{% endif %}
</div>
<div class="header-row">
{% if settings.business_logo_url %}
<img src="{{ settings.business_logo_url }}" style="height:70px;margin-bottom:10px;">
{% endif %}
<div class="title-box">
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
<div style="text-align:right;">
<strong>{{ settings.business_name or 'OTB Billing' }}</strong><br>
{{ settings.business_tagline or '' }}<br>
{% if settings.business_address %}{{ settings.business_address }}<br>{% endif %}
{% if settings.business_email %}{{ settings.business_email }}<br>{% endif %}
{% if settings.business_phone %}{{ settings.business_phone }}<br>{% endif %}
{% if settings.business_website %}{{ settings.business_website }}{% endif %}
</div>
</div>
<div class="info-grid">
<div class="info-card">
<h3>Bill To</h3>
<strong>{{ invoice.company_name }}</strong><br>
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %}
{% if invoice.email %}{{ invoice.email }}<br>{% endif %}
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %}
Client Code: {{ invoice.client_code }}
</div>
<div class="info-card">
<h3>Invoice Details</h3>
Invoice #: {{ invoice.invoice_number }}<br>
Issued: {{ invoice.issued_at|localtime }}<br>
Due: {{ invoice.due_at|localtime }}<br>
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %}
Currency: {{ invoice.currency_code }}<br>
{% if settings.tax_number %}{{ settings.tax_label or 'Tax' }} Number: {{ settings.tax_number }}<br>{% endif %}
{% if settings.business_number %}Business Number: {{ settings.business_number }}{% endif %}
</div>
</div>
<table class="summary-table">
<tr>
<th>Service Code</th>
<th>Service</th>
<th>Description</th>
<th>Total</th>
</tr>
<tr>
<td>{{ invoice.service_code or '-' }}</td>
<td>{{ invoice.service_name or '-' }}</td>
<td>{{ invoice.notes or '-' }}</td>
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
<table class="total-table">
<tr>
<th>Subtotal</th>
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>{{ settings.tax_label or 'Tax' }}</th>
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Total</th>
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td>
</tr>
<tr>
<th>Paid</th>
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Remaining</th>
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
{% if latest_email_log %}
<div class="notes-box">
<strong>Latest Email Activity</strong><br><br>
Status: {{ latest_email_log.status }}<br>
Recipient: {{ latest_email_log.recipient_email }}<br>
Subject: {{ latest_email_log.subject }}<br>
Sent At: {{ latest_email_log.sent_at|localtime }}<br>
{% if latest_email_log.error_message %}
Error: {{ latest_email_log.error_message }}
{% endif %}
</div>
{% endif %}
{% if settings.payment_terms %}
<div class="notes-box">
<strong>Payment Terms</strong><br><br>
{{ settings.payment_terms }}
</div>
{% endif %}
{% if settings.invoice_footer %}
<div class="notes-box">
<strong>Footer</strong><br><br>
{{ settings.invoice_footer }}
</div>
{% endif %}
</div>
{% include "footer.html" %}
<hr>
<h3>Payment Instructions</h3>
<p><strong>Interac e-Transfer</strong><br>
Send payment to:<br>
payment@outsidethebox.top<br>
Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}
</p>
<p><strong>Credit Card (Square)</strong><br>
Contact us for a secure Square payment link.</p>
<p>
If you have questions please contact
<a href="mailto:support@outsidethebox.top">support@outsidethebox.top</a>
</p>
</body>
</html>

187
templates/portal_invoice_detail.html.square_button_20260313-055733.bak

@ -1,187 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.detail-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.detail-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.detail-card h3 {
margin-top: 0;
margin-bottom: 0.4rem;
}
table.portal-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-actions {
margin-top: 1rem;
}
.invoice-actions a {
margin-right: 1rem;
text-decoration: underline;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Invoice Detail</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/dashboard">Back to Dashboard</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="detail-grid">
<div class="detail-card">
<h3>Invoice</h3>
<div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div>
</div>
<div class="detail-card">
<h3>Status</h3>
{% set s = (invoice.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ invoice.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ invoice.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ invoice.status }}</span>
{% else %}
<span class="status-badge status-other">{{ invoice.status }}</span>
{% endif %}
</div>
<div class="detail-card">
<h3>Created</h3>
<div>{{ invoice.created_at }}</div>
</div>
<div class="detail-card">
<h3>Total</h3>
<div>{{ invoice.total_amount }}</div>
</div>
<div class="detail-card">
<h3>Paid</h3>
<div>{{ invoice.amount_paid }}</div>
</div>
<div class="detail-card">
<h3>Outstanding</h3>
<div>{{ invoice.outstanding }}</div>
</div>
</div>
<h2>Invoice Items</h2>
<table class="portal-table">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Unit Price</th>
<th>Line Total</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.description }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.unit_price }}</td>
<td>{{ item.line_total }}</td>
</tr>
{% else %}
<tr>
<td colspan="4">No invoice line items found.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pdf_url %}
<div class="invoice-actions">
<a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a>
</div>
{% endif %}
</div>
{% include "footer.html" %}
<hr>
<h3>Payment Instructions</h3>
<p><strong>Interac e-Transfer</strong><br>
Send payment to:<br>
payment@outsidethebox.top<br>
Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}
</p>
<p><strong>Credit Card (Square)</strong><br>
Contact us for a secure Square payment link.</p>
<p>
If you have questions please contact
<a href="mailto:support@outsidethebox.top">support@outsidethebox.top</a>
</p>
</body>
</html>
Loading…
Cancel
Save