Browse Source

Bump to v2.0.6 health revenue dashboard

main
def 3 weeks ago
parent
commit
2626e513a4
  1. 10
      PROJECT_STATE.md
  2. 10
      README.md
  3. 2
      VERSION
  4. 247
      backend/health.py
  5. 53
      templates/health.html

10
PROJECT_STATE.md

@ -1,3 +1,13 @@
## v2.0.6 health revenue dashboard - 2026-05-29 UTC
- Added Square / Revenue Health panel to /health.
- Added confirmed payment totals for today, this month, and this year.
- Added all-confirmed payment totals alongside Square-only totals.
- Added latest confirmed payment summary.
- Added Receivables Aging panel showing unpaid invoice totals and aging buckets.
- Added matching revenue and receivables data to /health.json.
- Cleaned health labels from Operations Bal / Treasury Bal to Operations Balance / Treasury Balance.
## v2.0.5 final invoice workflow - 2026-05-29 UTC
- Create Invoice now supports a real invoice line description, quantity, unit cost, and optional 13% HST checkbox.

10
README.md

@ -1,3 +1,13 @@
## v2.0.6 health revenue dashboard - 2026-05-29 UTC
- Added Square / Revenue Health panel to /health.
- Added confirmed payment totals for today, this month, and this year.
- Added all-confirmed payment totals alongside Square-only totals.
- Added latest confirmed payment summary.
- Added Receivables Aging panel showing unpaid invoice totals and aging buckets.
- Added matching revenue and receivables data to /health.json.
- Cleaned health labels from Operations Bal / Treasury Bal to Operations Balance / Treasury Balance.
## v2.0.5 final invoice workflow - 2026-05-29 UTC
- Create Invoice now supports a real invoice line description, quantity, unit cost, and optional 13% HST checkbox.

2
VERSION

@ -1 +1 @@
v2.0.5
v2.0.6

247
backend/health.py

@ -562,10 +562,255 @@ def _crypto_reconcile_status(app):
}
# OTB_HEALTH_REVENUE_HELPERS_V0_6_0
def _health_db_rows(app, query, params=None):
"""Run a small health/reporting SQL query and return dict rows."""
connector = app.config.get("OTB_HEALTH_DB_CONNECTOR")
if not connector:
return []
conn = None
cur = None
try:
conn = connector()
try:
cur = conn.cursor(dictionary=True)
except TypeError:
cur = conn.cursor()
cur.execute(query, params or ())
rows = cur.fetchall() or []
if rows and not isinstance(rows[0], dict):
cols = [d[0] for d in (cur.description or [])]
rows = [dict(zip(cols, r)) for r in rows]
return rows
finally:
try:
if cur:
cur.close()
except Exception:
pass
try:
if conn:
conn.close()
except Exception:
pass
def _money_cad(value):
try:
return "${:,.2f} CAD".format(float(value or 0))
except Exception:
return "$0.00 CAD"
def _health_period_bounds():
"""Return Toronto business-day/month/year bounds converted to UTC-naive DB timestamps."""
from datetime import datetime, timezone
try:
from zoneinfo import ZoneInfo
tz = ZoneInfo("America/Toronto")
except Exception:
tz = timezone.utc
now_local = datetime.now(tz)
day_start = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
month_start = day_start.replace(day=1)
year_start = day_start.replace(month=1, day=1)
if month_start.month == 12:
next_month = month_start.replace(year=month_start.year + 1, month=1)
else:
next_month = month_start.replace(month=month_start.month + 1)
next_year = year_start.replace(year=year_start.year + 1)
next_day = day_start.replace(day=day_start.day) # placeholder before timedelta import
from datetime import timedelta
next_day = day_start + timedelta(days=1)
def utc_naive(dt):
return dt.astimezone(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%d %H:%M:%S")
return {
"today": (utc_naive(day_start), utc_naive(next_day)),
"month": (utc_naive(month_start), utc_naive(next_month)),
"year": (utc_naive(year_start), utc_naive(next_year)),
}
def _payment_period_summary(app, start_utc, end_utc, extra_where="", extra_params=None):
value_expr = "COALESCE(received_amount_cad, cad_value_at_payment, expected_amount_cad, payment_amount, 0)"
query = f"""
SELECT
COUNT(*) AS payment_count,
COALESCE(SUM({value_expr}), 0) AS cad_total
FROM payments
WHERE payment_status = 'confirmed'
AND COALESCE(received_at, updated_at, created_at) >= %s
AND COALESCE(received_at, updated_at, created_at) < %s
{extra_where}
"""
params = [start_utc, end_utc]
if extra_params:
params.extend(extra_params)
rows = _health_db_rows(app, query, params)
row = rows[0] if rows else {}
return {
"count": int(row.get("payment_count") or 0),
"cad_total": float(row.get("cad_total") or 0),
"cad_total_display": _money_cad(row.get("cad_total") or 0),
}
def _revenue_health(app):
"""Revenue/confirmed payment summary for /health."""
try:
bounds = _health_period_bounds()
square_where = "AND payment_method = 'square'"
crypto_where = "AND UPPER(COALESCE(payment_currency,'')) IN ('USDC','ETH','ETHO','EGAZ','ETI','ALT','ETHO','CAD')"
out = {
"available": True,
"square": {},
"all_confirmed": {},
"crypto": {},
}
for key, (start_utc, end_utc) in bounds.items():
out["square"][key] = _payment_period_summary(app, start_utc, end_utc, square_where)
out["all_confirmed"][key] = _payment_period_summary(app, start_utc, end_utc)
out["crypto"][key] = _payment_period_summary(app, start_utc, end_utc, crypto_where)
recent = _health_db_rows(app, """
SELECT
id,
invoice_id,
payment_method,
payment_currency,
COALESCE(received_amount_cad, cad_value_at_payment, expected_amount_cad, payment_amount, 0) AS cad_total,
COALESCE(received_at, updated_at, created_at) AS payment_time
FROM payments
WHERE payment_status = 'confirmed'
ORDER BY COALESCE(received_at, updated_at, created_at) DESC, id DESC
LIMIT 1
""")
if recent:
r = recent[0]
out["last_payment"] = {
"id": r.get("id"),
"invoice_id": r.get("invoice_id"),
"method": r.get("payment_method") or "unknown",
"currency": r.get("payment_currency") or "",
"cad_total": float(r.get("cad_total") or 0),
"cad_total_display": _money_cad(r.get("cad_total") or 0),
"payment_time": str(r.get("payment_time") or ""),
}
else:
out["last_payment"] = None
return out
except Exception as exc:
return {
"available": False,
"error": str(exc),
}
def _receivables_aging(app):
"""Accounts receivable aging from unpaid invoice balances."""
try:
rows = _health_db_rows(app, """
SELECT
COUNT(*) AS invoice_count,
COALESCE(SUM(GREATEST(total_amount - amount_paid, 0)), 0) AS total_unpaid,
COALESCE(SUM(CASE
WHEN due_at IS NULL OR due_at >= UTC_TIMESTAMP()
THEN GREATEST(total_amount - amount_paid, 0) ELSE 0 END), 0) AS current_total,
SUM(CASE
WHEN due_at IS NULL OR due_at >= UTC_TIMESTAMP()
THEN 1 ELSE 0 END) AS current_count,
COALESCE(SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) BETWEEN 1 AND 30
THEN GREATEST(total_amount - amount_paid, 0) ELSE 0 END), 0) AS d1_30_total,
SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) BETWEEN 1 AND 30
THEN 1 ELSE 0 END) AS d1_30_count,
COALESCE(SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) BETWEEN 31 AND 60
THEN GREATEST(total_amount - amount_paid, 0) ELSE 0 END), 0) AS d31_60_total,
SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) BETWEEN 31 AND 60
THEN 1 ELSE 0 END) AS d31_60_count,
COALESCE(SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) BETWEEN 61 AND 90
THEN GREATEST(total_amount - amount_paid, 0) ELSE 0 END), 0) AS d61_90_total,
SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) BETWEEN 61 AND 90
THEN 1 ELSE 0 END) AS d61_90_count,
COALESCE(SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) > 90
THEN GREATEST(total_amount - amount_paid, 0) ELSE 0 END), 0) AS d90_plus_total,
SUM(CASE
WHEN due_at < UTC_TIMESTAMP()
AND DATEDIFF(UTC_TIMESTAMP(), due_at) > 90
THEN 1 ELSE 0 END) AS d90_plus_count
FROM invoices
WHERE status NOT IN ('paid','cancelled','draft')
AND GREATEST(total_amount - amount_paid, 0) > 0
""")
row = rows[0] if rows else {}
def bucket(prefix):
return {
"count": int(row.get(prefix + "_count") or 0),
"cad_total": float(row.get(prefix + "_total") or 0),
"cad_total_display": _money_cad(row.get(prefix + "_total") or 0),
}
return {
"available": True,
"invoice_count": int(row.get("invoice_count") or 0),
"total_unpaid": float(row.get("total_unpaid") or 0),
"total_unpaid_display": _money_cad(row.get("total_unpaid") or 0),
"current": bucket("current"),
"d1_30": bucket("d1_30"),
"d31_60": bucket("d31_60"),
"d61_90": bucket("d61_90"),
"d90_plus": bucket("d90_plus"),
}
except Exception as exc:
return {
"available": False,
"error": str(exc),
}
def register_health_routes(app):
@app.route("/health", methods=["GET"])
def health_page():
health, http_code = _health_payload(app)
health["revenue_health"] = _revenue_health(app)
health["receivables_aging"] = _receivables_aging(app)
health["operations_balances"] = _wallet_balances(
os.environ.get("OTB_OPERATIONS_WALLET", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5"),
"Operations Bal",
@ -612,6 +857,8 @@ def register_health_routes(app):
@app.route("/health.json", methods=["GET"])
def health_json():
health, http_code = _health_payload(app)
health["revenue_health"] = _revenue_health(app)
health["receivables_aging"] = _receivables_aging(app)
health["operations_balances"] = _wallet_balances(
os.environ.get("OTB_OPERATIONS_WALLET", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5"),
"Operations Bal",

53
templates/health.html

@ -207,8 +207,57 @@
<p><strong>Used %:</strong> {{ health.disk_root.used_percent }}%</p>
</div>
<!-- OTB_HEALTH_REVENUE_PANELS_V0_6_0 -->
<div class="health-card">
<h3>Square / Revenue Health</h3>
{% if health.revenue_health and health.revenue_health.available %}
<p><strong>Square confirmed payments</strong></p>
<p><strong>Today:</strong> {{ health.revenue_health.square.today.count }} payment{% if health.revenue_health.square.today.count != 1 %}s{% endif %} | {{ health.revenue_health.square.today.cad_total_display }}</p>
<p><strong>This Month:</strong> {{ health.revenue_health.square.month.count }} payment{% if health.revenue_health.square.month.count != 1 %}s{% endif %} | {{ health.revenue_health.square.month.cad_total_display }}</p>
<p><strong>This Year:</strong> {{ health.revenue_health.square.year.count }} payment{% if health.revenue_health.square.year.count != 1 %}s{% endif %} | {{ health.revenue_health.square.year.cad_total_display }}</p>
<hr>
<p><strong>All confirmed payments</strong></p>
<p><strong>Today:</strong> {{ health.revenue_health.all_confirmed.today.count }} payment{% if health.revenue_health.all_confirmed.today.count != 1 %}s{% endif %} | {{ health.revenue_health.all_confirmed.today.cad_total_display }}</p>
<p><strong>This Month:</strong> {{ health.revenue_health.all_confirmed.month.count }} payment{% if health.revenue_health.all_confirmed.month.count != 1 %}s{% endif %} | {{ health.revenue_health.all_confirmed.month.cad_total_display }}</p>
<p><strong>This Year:</strong> {{ health.revenue_health.all_confirmed.year.count }} payment{% if health.revenue_health.all_confirmed.year.count != 1 %}s{% endif %} | {{ health.revenue_health.all_confirmed.year.cad_total_display }}</p>
{% if health.revenue_health.last_payment %}
<hr>
<p><strong>Last Payment:</strong> {{ health.revenue_health.last_payment.cad_total_display }} {{ health.revenue_health.last_payment.currency }}</p>
<p class="monoish"><strong>When:</strong> {{ health.revenue_health.last_payment.payment_time }}</p>
{% endif %}
{% elif health.revenue_health %}
<p class="status-bad">Unavailable</p>
<p class="monoish"><strong>Error:</strong> {{ health.revenue_health.error }}</p>
{% else %}
<p class="status-bad">Unavailable</p>
{% endif %}
</div>
<div class="health-card">
<h3>Receivables Aging</h3>
{% if health.receivables_aging and health.receivables_aging.available %}
<p><strong>Total Unpaid:</strong> {{ health.receivables_aging.total_unpaid_display }}</p>
<p><strong>Open Invoices:</strong> {{ health.receivables_aging.invoice_count }}</p>
<hr>
<p><strong>Current:</strong> {{ health.receivables_aging.current.count }} invoice{% if health.receivables_aging.current.count != 1 %}s{% endif %} | {{ health.receivables_aging.current.cad_total_display }}</p>
<p><strong>1-30 Days:</strong> {{ health.receivables_aging.d1_30.count }} invoice{% if health.receivables_aging.d1_30.count != 1 %}s{% endif %} | {{ health.receivables_aging.d1_30.cad_total_display }}</p>
<p><strong>31-60 Days:</strong> {{ health.receivables_aging.d31_60.count }} invoice{% if health.receivables_aging.d31_60.count != 1 %}s{% endif %} | {{ health.receivables_aging.d31_60.cad_total_display }}</p>
<p><strong>61-90 Days:</strong> {{ health.receivables_aging.d61_90.count }} invoice{% if health.receivables_aging.d61_90.count != 1 %}s{% endif %} | {{ health.receivables_aging.d61_90.cad_total_display }}</p>
<p><strong>90+ Days:</strong> {{ health.receivables_aging.d90_plus.count }} invoice{% if health.receivables_aging.d90_plus.count != 1 %}s{% endif %} | {{ health.receivables_aging.d90_plus.cad_total_display }}</p>
{% elif health.receivables_aging %}
<p class="status-bad">Unavailable</p>
<p class="monoish"><strong>Error:</strong> {{ health.receivables_aging.error }}</p>
{% else %}
<p class="status-bad">Unavailable</p>
{% endif %}
</div>
<div class="health-card">
<h3>Operations Bal</h3>
<h3>Operations Balance</h3>
{% if health.operations_balances %}
{% for asset in health.operations_balances.assets %}
<p>
@ -227,7 +276,7 @@
</div>
<div class="health-card">
<h3>Treasury Bal</h3>
<h3>Treasury Balance</h3>
{% if health.treasury_balances %}
{% for asset in health.treasury_balances.assets %}
<p>

Loading…
Cancel
Save