fix: replace datetime.utcnow() with utcnow()/utcnow_iso() across all source files

Migrates 15 source files from the deprecated datetime.utcnow() API.
Uses utcnow() for in-memory math and utcnow_iso() (strftime format)
for SQLite TEXT column writes to preserve lexicographic sort order.
Also fixes datetime.utcfromtimestamp() in seo/_bing.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-24 10:22:42 +01:00
parent f76d2889e5
commit 5644a1ebf8
18 changed files with 116 additions and 113 deletions

View File

@@ -29,6 +29,8 @@ from ..core import (
fetch_one, fetch_one,
send_email, send_email,
slugify, slugify,
utcnow,
utcnow_iso,
) )
# Blueprint with its own template folder # Blueprint with its own template folder
@@ -64,9 +66,9 @@ def _admin_context():
async def get_dashboard_stats() -> dict: async def get_dashboard_stats() -> dict:
"""Get admin dashboard statistics.""" """Get admin dashboard statistics."""
now = datetime.utcnow() now = utcnow()
today = now.date().isoformat() today = now.date().isoformat()
week_ago = (now - timedelta(days=7)).isoformat() week_ago = (now - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
users_total = await fetch_one("SELECT COUNT(*) as count FROM users WHERE deleted_at IS NULL") users_total = await fetch_one("SELECT COUNT(*) as count FROM users WHERE deleted_at IS NULL")
users_today = await fetch_one( users_today = await fetch_one(
"SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND deleted_at IS NULL", "SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND deleted_at IS NULL",
@@ -211,7 +213,7 @@ async def retry_task(task_id: int) -> bool:
SET status = 'pending', run_at = ?, error = NULL SET status = 'pending', run_at = ?, error = NULL
WHERE id = ? AND status = 'failed' WHERE id = ? AND status = 'failed'
""", """,
(datetime.utcnow().isoformat(), task_id) (utcnow_iso(), task_id)
) )
return result > 0 return result > 0
@@ -522,7 +524,7 @@ async def lead_new():
from ..credits import HEAT_CREDIT_COSTS from ..credits import HEAT_CREDIT_COSTS
credit_cost = HEAT_CREDIT_COSTS.get(heat_score, 8) credit_cost = HEAT_CREDIT_COSTS.get(heat_score, 8)
now = datetime.utcnow().isoformat() now = utcnow_iso()
verified_at = now if status != "pending_verification" else None verified_at = now if status != "pending_verification" else None
lead_id = await execute( lead_id = await execute(
@@ -567,7 +569,7 @@ async def lead_forward(lead_id: int):
await flash("Already forwarded to this supplier.", "warning") await flash("Already forwarded to this supplier.", "warning")
return redirect(url_for("admin.lead_detail", lead_id=lead_id)) return redirect(url_for("admin.lead_detail", lead_id=lead_id))
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"""INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, status, created_at) """INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, status, created_at)
VALUES (?, ?, 0, 'sent', ?)""", VALUES (?, ?, 0, 'sent', ?)""",
@@ -771,7 +773,7 @@ async def supplier_new():
instagram_url = form.get("instagram_url", "").strip() instagram_url = form.get("instagram_url", "").strip()
youtube_url = form.get("youtube_url", "").strip() youtube_url = form.get("youtube_url", "").strip()
now = datetime.utcnow().isoformat() now = utcnow_iso()
supplier_id = await execute( supplier_id = await execute(
"""INSERT INTO suppliers """INSERT INTO suppliers
(name, slug, country_code, city, region, website, description, category, (name, slug, country_code, city, region, website, description, category,
@@ -865,7 +867,7 @@ async def flag_toggle():
return redirect(url_for("admin.flags")) return redirect(url_for("admin.flags"))
new_enabled = 0 if row["enabled"] else 1 new_enabled = 0 if row["enabled"] else 1
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"UPDATE feature_flags SET enabled = ?, updated_at = ? WHERE name = ?", "UPDATE feature_flags SET enabled = ?, updated_at = ? WHERE name = ?",
(new_enabled, now, flag_name), (new_enabled, now, flag_name),
@@ -940,7 +942,7 @@ async def get_email_stats() -> dict:
total = await fetch_one("SELECT COUNT(*) as cnt FROM email_log") total = await fetch_one("SELECT COUNT(*) as cnt FROM email_log")
delivered = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE last_event = 'delivered'") delivered = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE last_event = 'delivered'")
bounced = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE last_event = 'bounced'") bounced = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE last_event = 'bounced'")
today = datetime.utcnow().date().isoformat() today = utcnow().date().isoformat()
sent_today = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE created_at >= ?", (today,)) sent_today = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE created_at >= ?", (today,))
return { return {
"total": total["cnt"] if total else 0, "total": total["cnt"] if total else 0,
@@ -1487,7 +1489,7 @@ async def scenario_edit(scenario_id: int):
dbl = state.get("dblCourts", 0) dbl = state.get("dblCourts", 0)
sgl = state.get("sglCourts", 0) sgl = state.get("sglCourts", 0)
court_config = f"{dbl} double + {sgl} single" court_config = f"{dbl} double + {sgl} single"
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"""UPDATE published_scenarios """UPDATE published_scenarios
@@ -1740,7 +1742,7 @@ async def article_new():
md_dir.mkdir(parents=True, exist_ok=True) md_dir.mkdir(parents=True, exist_ok=True)
(md_dir / f"{article_slug}.md").write_text(body) (md_dir / f"{article_slug}.md").write_text(body)
pub_dt = published_at or datetime.utcnow().isoformat() pub_dt = published_at or utcnow_iso()
await execute( await execute(
"""INSERT INTO articles """INSERT INTO articles
@@ -1800,7 +1802,7 @@ async def article_edit(article_id: int):
md_dir.mkdir(parents=True, exist_ok=True) md_dir.mkdir(parents=True, exist_ok=True)
(md_dir / f"{article['slug']}.md").write_text(body) (md_dir / f"{article['slug']}.md").write_text(body)
now = datetime.utcnow().isoformat() now = utcnow_iso()
pub_dt = published_at or article["published_at"] pub_dt = published_at or article["published_at"]
await execute( await execute(
@@ -1867,7 +1869,7 @@ async def article_publish(article_id: int):
return redirect(url_for("admin.articles")) return redirect(url_for("admin.articles"))
new_status = "published" if article["status"] == "draft" else "draft" new_status = "published" if article["status"] == "draft" else "draft"
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"UPDATE articles SET status = ?, updated_at = ? WHERE id = ?", "UPDATE articles SET status = ?, updated_at = ? WHERE id = ?",
(new_status, now, article_id), (new_status, now, article_id),

View File

@@ -208,7 +208,7 @@ def create_app() -> Quart:
@app.context_processor @app.context_processor
def inject_globals(): def inject_globals():
from datetime import datetime from .core import utcnow as _utcnow
lang = g.get("lang") or _detect_lang() lang = g.get("lang") or _detect_lang()
g.lang = lang # ensure g.lang is always set (e.g. for dashboard/billing routes) g.lang = lang # ensure g.lang is always set (e.g. for dashboard/billing routes)
effective_lang = lang if lang in SUPPORTED_LANGS else "en" effective_lang = lang if lang in SUPPORTED_LANGS else "en"
@@ -217,7 +217,7 @@ def create_app() -> Quart:
"user": g.get("user"), "user": g.get("user"),
"subscription": g.get("subscription"), "subscription": g.get("subscription"),
"is_admin": "admin" in (g.get("user") or {}).get("roles", []), "is_admin": "admin" in (g.get("user") or {}).get("roles", []),
"now": datetime.utcnow(), "now": _utcnow(),
"csrf_token": get_csrf_token, "csrf_token": get_csrf_token,
"ab_variant": getattr(g, "ab_variant", None), "ab_variant": getattr(g, "ab_variant", None),
"ab_tag": getattr(g, "ab_tag", None), "ab_tag": getattr(g, "ab_tag", None),

View File

@@ -18,6 +18,8 @@ from ..core import (
fetch_one, fetch_one,
is_disposable_email, is_disposable_email,
is_flag_enabled, is_flag_enabled,
utcnow,
utcnow_iso,
) )
from ..i18n import SUPPORTED_LANGS, get_translations from ..i18n import SUPPORTED_LANGS, get_translations
@@ -64,7 +66,7 @@ async def get_user_by_email(email: str) -> dict | None:
async def create_user(email: str) -> int: async def create_user(email: str) -> int:
"""Create new user, return ID.""" """Create new user, return ID."""
now = datetime.utcnow().isoformat() now = utcnow_iso()
return await execute( return await execute(
"INSERT INTO users (email, created_at) VALUES (?, ?)", (email.lower(), now) "INSERT INTO users (email, created_at) VALUES (?, ?)", (email.lower(), now)
) )
@@ -82,10 +84,10 @@ async def update_user(user_id: int, **fields) -> None:
async def create_auth_token(user_id: int, token: str, minutes: int = None) -> int: async def create_auth_token(user_id: int, token: str, minutes: int = None) -> int:
"""Create auth token for user.""" """Create auth token for user."""
minutes = minutes or config.MAGIC_LINK_EXPIRY_MINUTES minutes = minutes or config.MAGIC_LINK_EXPIRY_MINUTES
expires = datetime.utcnow() + timedelta(minutes=minutes) expires = utcnow() + timedelta(minutes=minutes)
return await execute( return await execute(
"INSERT INTO auth_tokens (user_id, token, expires_at) VALUES (?, ?, ?)", "INSERT INTO auth_tokens (user_id, token, expires_at) VALUES (?, ?, ?)",
(user_id, token, expires.isoformat()), (user_id, token, expires.strftime("%Y-%m-%dT%H:%M:%S")),
) )
@@ -98,14 +100,14 @@ async def get_valid_token(token: str) -> dict | None:
JOIN users u ON u.id = at.user_id JOIN users u ON u.id = at.user_id
WHERE at.token = ? AND at.expires_at > ? AND at.used_at IS NULL WHERE at.token = ? AND at.expires_at > ? AND at.used_at IS NULL
""", """,
(token, datetime.utcnow().isoformat()), (token, utcnow_iso()),
) )
async def mark_token_used(token_id: int) -> None: async def mark_token_used(token_id: int) -> None:
"""Mark token as used.""" """Mark token as used."""
await execute( await execute(
"UPDATE auth_tokens SET used_at = ? WHERE id = ?", (datetime.utcnow().isoformat(), token_id) "UPDATE auth_tokens SET used_at = ? WHERE id = ?", (utcnow_iso(), token_id)
) )
@@ -331,7 +333,7 @@ async def verify():
await mark_token_used(token_data["id"]) await mark_token_used(token_data["id"])
# Update last login # Update last login
await update_user(token_data["user_id"], last_login_at=datetime.utcnow().isoformat()) await update_user(token_data["user_id"], last_login_at=utcnow_iso())
# Set session # Set session
session.permanent = True session.permanent = True

View File

@@ -5,7 +5,7 @@ Payment provider: paddle
import json import json
import secrets import secrets
from datetime import datetime from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from paddle_billing import Client as PaddleClient from paddle_billing import Client as PaddleClient
@@ -14,7 +14,7 @@ from paddle_billing.Notifications import Secret, Verifier
from quart import Blueprint, flash, g, jsonify, redirect, render_template, request, session, url_for from quart import Blueprint, flash, g, jsonify, redirect, render_template, request, session, url_for
from ..auth.routes import login_required from ..auth.routes import login_required
from ..core import config, execute, fetch_one, get_paddle_price from ..core import config, execute, fetch_one, get_paddle_price, utcnow, utcnow_iso
from ..i18n import get_translations from ..i18n import get_translations
@@ -69,7 +69,7 @@ async def upsert_subscription(
current_period_end: str = None, current_period_end: str = None,
) -> int: ) -> int:
"""Create or update subscription. Finds existing by provider_subscription_id.""" """Create or update subscription. Finds existing by provider_subscription_id."""
now = datetime.utcnow().isoformat() now = utcnow_iso()
existing = await fetch_one( existing = await fetch_one(
"SELECT id FROM subscriptions WHERE provider_subscription_id = ?", "SELECT id FROM subscriptions WHERE provider_subscription_id = ?",
@@ -104,7 +104,7 @@ async def get_subscription_by_provider_id(subscription_id: str) -> dict | None:
async def update_subscription_status(provider_subscription_id: str, status: str, **extra) -> None: async def update_subscription_status(provider_subscription_id: str, status: str, **extra) -> None:
"""Update subscription status by provider subscription ID.""" """Update subscription status by provider subscription ID."""
extra["updated_at"] = datetime.utcnow().isoformat() extra["updated_at"] = utcnow_iso()
extra["status"] = status extra["status"] = status
sets = ", ".join(f"{k} = ?" for k in extra) sets = ", ".join(f"{k} = ?" for k in extra)
values = list(extra.values()) values = list(extra.values())
@@ -343,7 +343,7 @@ async def _handle_supplier_subscription_activated(data: dict, custom_data: dict)
base_plan, tier = _derive_tier_from_plan(plan) base_plan, tier = _derive_tier_from_plan(plan)
monthly_credits = PLAN_MONTHLY_CREDITS.get(base_plan, 0) monthly_credits = PLAN_MONTHLY_CREDITS.get(base_plan, 0)
now = datetime.utcnow().isoformat() now = utcnow_iso()
async with db_transaction() as db: async with db_transaction() as db:
# Update supplier record — Basic tier also gets is_verified = 1 # Update supplier record — Basic tier also gets is_verified = 1
@@ -392,7 +392,7 @@ async def _handle_transaction_completed(data: dict, custom_data: dict) -> None:
"""Handle one-time transaction completion (credit packs, sticky boosts, business plan).""" """Handle one-time transaction completion (credit packs, sticky boosts, business plan)."""
supplier_id = custom_data.get("supplier_id") supplier_id = custom_data.get("supplier_id")
user_id = custom_data.get("user_id") user_id = custom_data.get("user_id")
now = datetime.utcnow().isoformat() now = utcnow_iso()
items = data.get("items", []) items = data.get("items", [])
for item in items: for item in items:
@@ -412,10 +412,8 @@ async def _handle_transaction_completed(data: dict, custom_data: dict) -> None:
# Sticky boost purchases # Sticky boost purchases
elif key == "boost_sticky_week" and supplier_id: elif key == "boost_sticky_week" and supplier_id:
from datetime import timedelta
from ..core import transaction as db_transaction from ..core import transaction as db_transaction
expires = (datetime.utcnow() + timedelta(weeks=1)).isoformat() expires = (utcnow() + timedelta(weeks=1)).strftime("%Y-%m-%dT%H:%M:%S")
country = custom_data.get("sticky_country", "") country = custom_data.get("sticky_country", "")
async with db_transaction() as db: async with db_transaction() as db:
await db.execute( await db.execute(
@@ -430,10 +428,8 @@ async def _handle_transaction_completed(data: dict, custom_data: dict) -> None:
) )
elif key == "boost_sticky_month" and supplier_id: elif key == "boost_sticky_month" and supplier_id:
from datetime import timedelta
from ..core import transaction as db_transaction from ..core import transaction as db_transaction
expires = (datetime.utcnow() + timedelta(days=30)).isoformat() expires = (utcnow() + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%S")
country = custom_data.get("sticky_country", "") country = custom_data.get("sticky_country", "")
async with db_transaction() as db: async with db_transaction() as db:
await db.execute( await db.execute(

View File

@@ -15,7 +15,7 @@ import yaml
from jinja2 import ChainableUndefined, Environment from jinja2 import ChainableUndefined, Environment
from ..analytics import fetch_analytics from ..analytics import fetch_analytics
from ..core import execute, fetch_one, slugify from ..core import execute, fetch_one, slugify, utcnow_iso
# ── Constants ──────────────────────────────────────────────────────────────── # ── Constants ────────────────────────────────────────────────────────────────
@@ -301,7 +301,7 @@ async def generate_articles(
publish_date = start_date publish_date = start_date
published_today = 0 published_today = 0
generated = 0 generated = 0
now_iso = datetime.now(UTC).isoformat() now_iso = utcnow_iso()
for row in rows: for row in rows:
for lang in config["languages"]: for lang in config["languages"]:

View File

@@ -5,9 +5,7 @@ All balance mutations go through this module to keep credit_ledger (source of tr
and suppliers.credit_balance (denormalized cache) in sync within a single transaction. and suppliers.credit_balance (denormalized cache) in sync within a single transaction.
""" """
from datetime import datetime from .core import execute, fetch_all, fetch_one, transaction, utcnow_iso
from .core import execute, fetch_all, fetch_one, transaction
# Credit cost per heat tier # Credit cost per heat tier
HEAT_CREDIT_COSTS = {"hot": 35, "warm": 20, "cool": 8} HEAT_CREDIT_COSTS = {"hot": 35, "warm": 20, "cool": 8}
@@ -44,7 +42,7 @@ async def add_credits(
note: str = None, note: str = None,
) -> int: ) -> int:
"""Add credits to a supplier. Returns new balance.""" """Add credits to a supplier. Returns new balance."""
now = datetime.utcnow().isoformat() now = utcnow_iso()
async with transaction() as db: async with transaction() as db:
row = await db.execute_fetchall( row = await db.execute_fetchall(
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,) "SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
@@ -73,7 +71,7 @@ async def spend_credits(
note: str = None, note: str = None,
) -> int: ) -> int:
"""Spend credits from a supplier. Returns new balance. Raises InsufficientCredits.""" """Spend credits from a supplier. Returns new balance. Raises InsufficientCredits."""
now = datetime.utcnow().isoformat() now = utcnow_iso()
async with transaction() as db: async with transaction() as db:
row = await db.execute_fetchall( row = await db.execute_fetchall(
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,) "SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
@@ -116,7 +114,7 @@ async def unlock_lead(supplier_id: int, lead_id: int) -> dict:
raise ValueError("Lead not found") raise ValueError("Lead not found")
cost = lead["credit_cost"] or compute_credit_cost(lead) cost = lead["credit_cost"] or compute_credit_cost(lead)
now = datetime.utcnow().isoformat() now = utcnow_iso()
async with transaction() as db: async with transaction() as db:
# Check balance # Check balance
@@ -180,7 +178,7 @@ async def monthly_credit_refill(supplier_id: int) -> int:
if not row or not row["monthly_credits"]: if not row or not row["monthly_credits"]:
return 0 return 0
now = datetime.utcnow().isoformat() now = utcnow_iso()
new_balance = await add_credits( new_balance = await add_credits(
supplier_id, supplier_id,
row["monthly_credits"], row["monthly_credits"],

View File

@@ -1,13 +1,12 @@
""" """
Dashboard domain: user dashboard and settings. Dashboard domain: user dashboard and settings.
""" """
from datetime import datetime
from pathlib import Path from pathlib import Path
from quart import Blueprint, flash, g, redirect, render_template, request, url_for from quart import Blueprint, flash, g, redirect, render_template, request, url_for
from ..auth.routes import login_required, update_user from ..auth.routes import login_required, update_user
from ..core import csrf_protect, fetch_one, soft_delete from ..core import csrf_protect, fetch_one, soft_delete, utcnow_iso
from ..i18n import get_translations from ..i18n import get_translations
bp = Blueprint( bp = Blueprint(
@@ -57,7 +56,7 @@ async def settings():
await update_user( await update_user(
g.user["id"], g.user["id"],
name=form.get("name", "").strip() or None, name=form.get("name", "").strip() or None,
updated_at=datetime.utcnow().isoformat(), updated_at=utcnow_iso(),
) )
t = get_translations(g.get("lang") or "en") t = get_translations(g.get("lang") or "en")
await flash(t["dash_settings_saved"], "success") await flash(t["dash_settings_saved"], "success")

View File

@@ -2,12 +2,11 @@
Supplier directory: public, searchable listing of padel court suppliers. Supplier directory: public, searchable listing of padel court suppliers.
""" """
from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from quart import Blueprint, g, make_response, redirect, render_template, request, url_for from quart import Blueprint, g, make_response, redirect, render_template, request, url_for
from ..core import csrf_protect, execute, fetch_all, fetch_one from ..core import csrf_protect, execute, fetch_all, fetch_one, utcnow_iso
from ..i18n import get_translations from ..i18n import get_translations
bp = Blueprint( bp = Blueprint(
@@ -89,7 +88,7 @@ async def _build_directory_query(q, country, category, region, page, per_page=24
lang = g.get("lang", "en") lang = g.get("lang", "en")
cat_labels, country_labels, region_labels = get_directory_labels(lang) cat_labels, country_labels, region_labels = get_directory_labels(lang)
now = datetime.now(UTC).isoformat() now = utcnow_iso()
params: list = [] params: list = []
wheres: list[str] = [] wheres: list[str] = []

View File

@@ -27,6 +27,7 @@ from ..core import (
is_disposable_email, is_disposable_email,
is_plausible_phone, is_plausible_phone,
send_email, send_email,
utcnow_iso,
) )
from ..i18n import get_translations from ..i18n import get_translations
@@ -102,7 +103,7 @@ async def suppliers():
form.get("court_count", 0), form.get("court_count", 0),
form.get("budget", 0), form.get("budget", 0),
form.get("message", ""), form.get("message", ""),
datetime.utcnow().isoformat(), utcnow_iso(),
), ),
) )
# Notify admin # Notify admin
@@ -147,7 +148,7 @@ async def financing():
form.get("court_count", 0), form.get("court_count", 0),
form.get("budget", 0), form.get("budget", 0),
form.get("message", ""), form.get("message", ""),
datetime.utcnow().isoformat(), utcnow_iso(),
), ),
) )
await send_email( await send_email(
@@ -375,7 +376,7 @@ async def quote_request():
status, status,
credit_cost, credit_cost,
secrets.token_urlsafe(16), secrets.token_urlsafe(16),
datetime.utcnow().isoformat(), utcnow_iso(),
), ),
) )
@@ -520,7 +521,7 @@ async def verify_quote():
from ..credits import compute_credit_cost from ..credits import compute_credit_cost
credit_cost = compute_credit_cost(dict(lead)) credit_cost = compute_credit_cost(dict(lead))
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"UPDATE lead_requests SET status = 'new', verified_at = ?, credit_cost = ? WHERE id = ?", "UPDATE lead_requests SET status = 'new', verified_at = ?, credit_cost = ? WHERE id = ?",
(now, credit_cost, lead["id"]), (now, credit_cost, lead["id"]),

View File

@@ -4,7 +4,6 @@ Planner domain: padel court financial planner + scenario management.
import json import json
import math import math
from datetime import datetime
from pathlib import Path from pathlib import Path
from quart import Blueprint, Response, g, jsonify, render_template, request from quart import Blueprint, Response, g, jsonify, render_template, request
@@ -18,6 +17,7 @@ from ..core import (
fetch_all, fetch_all,
fetch_one, fetch_one,
get_paddle_price, get_paddle_price,
utcnow_iso,
) )
from ..i18n import get_translations from ..i18n import get_translations
from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state
@@ -502,7 +502,7 @@ async def save_scenario():
location = form.get("location", "") location = form.get("location", "")
scenario_id = form.get("scenario_id") scenario_id = form.get("scenario_id")
now = datetime.utcnow().isoformat() now = utcnow_iso()
is_first_save = not scenario_id and (await count_scenarios(g.user["id"])) == 0 is_first_save = not scenario_id and (await count_scenarios(g.user["id"])) == 0
@@ -563,7 +563,7 @@ async def get_scenario(scenario_id: int):
@login_required @login_required
@csrf_protect @csrf_protect
async def delete_scenario(scenario_id: int): async def delete_scenario(scenario_id: int):
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"UPDATE scenarios SET deleted_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL", "UPDATE scenarios SET deleted_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL",
(now, scenario_id, g.user["id"]), (now, scenario_id, g.user["id"]),

View File

@@ -18,7 +18,7 @@ import json
import os import os
import sqlite3 import sqlite3
import sys import sys
from datetime import date, timedelta from datetime import UTC, date, datetime, timedelta
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -1390,7 +1390,7 @@ def seed_templates(conn: sqlite3.Connection) -> dict[str, int]:
def seed_data_rows(conn: sqlite3.Connection, template_ids: dict[str, int]) -> int: def seed_data_rows(conn: sqlite3.Connection, template_ids: dict[str, int]) -> int:
"""Insert template_data rows for all cities × languages. Returns count inserted.""" """Insert template_data rows for all cities × languages. Returns count inserted."""
now = __import__("datetime").datetime.utcnow().isoformat() now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S")
inserted = 0 inserted = 0
en_id = template_ids.get("city-padel-cost-en") en_id = template_ids.get("city-padel-cost-en")

View File

@@ -10,7 +10,7 @@ Usage:
import os import os
import sqlite3 import sqlite3
import sys import sys
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -292,7 +292,7 @@ def main():
conn.execute("PRAGMA foreign_keys=ON") conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
now = datetime.utcnow() now = datetime.now(UTC)
# 1. Create dev user # 1. Create dev user
print("Creating dev user (dev@localhost)...") print("Creating dev user (dev@localhost)...")
@@ -303,7 +303,7 @@ def main():
else: else:
cursor = conn.execute( cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
("dev@localhost", "Dev User", now.isoformat()), ("dev@localhost", "Dev User", now.strftime("%Y-%m-%dT%H:%M:%S")),
) )
dev_user_id = cursor.lastrowid dev_user_id = cursor.lastrowid
print(f" Created (id={dev_user_id})") print(f" Created (id={dev_user_id})")
@@ -336,7 +336,7 @@ def main():
s["website"], s["description"], s["category"], s["tier"], s["website"], s["description"], s["category"], s["tier"],
s["credit_balance"], s["monthly_credits"], s["contact_name"], s["credit_balance"], s["monthly_credits"], s["contact_name"],
s["contact_email"], s["years_in_business"], s["project_count"], s["contact_email"], s["years_in_business"], s["project_count"],
s["service_area"], now.isoformat(), s["service_area"], now.strftime("%Y-%m-%dT%H:%M:%S"),
), ),
) )
supplier_ids[s["slug"]] = cursor.lastrowid supplier_ids[s["slug"]] = cursor.lastrowid
@@ -349,7 +349,7 @@ def main():
("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"), ("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"),
("desert-padel-fze", "supplier_pro", "ahmed@desertpadel.example.com", "Ahmed Al-Rashid"), ("desert-padel-fze", "supplier_pro", "ahmed@desertpadel.example.com", "Ahmed Al-Rashid"),
] ]
period_end = (now + timedelta(days=30)).isoformat() period_end = (now + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%S")
for slug, plan, email, name in claimed_suppliers: for slug, plan, email, name in claimed_suppliers:
sid = supplier_ids.get(slug) sid = supplier_ids.get(slug)
if not sid: if not sid:
@@ -364,14 +364,14 @@ def main():
else: else:
cursor = conn.execute( cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
(email, name, now.isoformat()), (email, name, now.strftime("%Y-%m-%dT%H:%M:%S")),
) )
owner_id = cursor.lastrowid owner_id = cursor.lastrowid
# Claim the supplier # Claim the supplier
conn.execute( conn.execute(
"UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL", "UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL",
(owner_id, now.isoformat(), sid), (owner_id, now.strftime("%Y-%m-%dT%H:%M:%S"), sid),
) )
# Create billing customer record # Create billing customer record
@@ -382,7 +382,7 @@ def main():
conn.execute( conn.execute(
"""INSERT INTO billing_customers (user_id, provider_customer_id, created_at) """INSERT INTO billing_customers (user_id, provider_customer_id, created_at)
VALUES (?, ?, ?)""", VALUES (?, ?, ?)""",
(owner_id, f"ctm_dev_{slug}", now.isoformat()), (owner_id, f"ctm_dev_{slug}", now.strftime("%Y-%m-%dT%H:%M:%S")),
) )
# Create active subscription # Create active subscription
@@ -396,7 +396,7 @@ def main():
current_period_end, created_at) current_period_end, created_at)
VALUES (?, ?, 'active', ?, ?, ?)""", VALUES (?, ?, 'active', ?, ?, ?)""",
(owner_id, plan, f"sub_dev_{slug}", (owner_id, plan, f"sub_dev_{slug}",
period_end, now.isoformat()), period_end, now.strftime("%Y-%m-%dT%H:%M:%S")),
) )
print(f" {slug} -> owner {email} ({plan})") print(f" {slug} -> owner {email} ({plan})")

View File

@@ -3,12 +3,12 @@
Uses an API key for auth. Fetches query stats and page stats. Uses an API key for auth. Fetches query stats and page stats.
""" """
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
from ..core import config, execute from ..core import config, execute, utcnow, utcnow_iso
_TIMEOUT_SECONDS = 30 _TIMEOUT_SECONDS = 30
@@ -27,7 +27,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
if not config.BING_WEBMASTER_API_KEY or not config.BING_SITE_URL: if not config.BING_WEBMASTER_API_KEY or not config.BING_SITE_URL:
return 0 # Bing not configured — skip silently return 0 # Bing not configured — skip silently
started_at = datetime.utcnow() started_at = utcnow()
try: try:
rows_synced = 0 rows_synced = 0
@@ -48,14 +48,14 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
if not isinstance(entries, list): if not isinstance(entries, list):
entries = [] entries = []
cutoff = datetime.utcnow() - timedelta(days=days_back) cutoff = utcnow() - timedelta(days=days_back)
for entry in entries: for entry in entries:
# Bing date format: "/Date(1708905600000)/" (ms since epoch) # Bing date format: "/Date(1708905600000)/" (ms since epoch)
date_str = entry.get("Date", "") date_str = entry.get("Date", "")
if "/Date(" in date_str: if "/Date(" in date_str:
ms = int(date_str.split("(")[1].split(")")[0]) ms = int(date_str.split("(")[1].split(")")[0])
entry_date = datetime.utcfromtimestamp(ms / 1000) entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
else: else:
continue continue
@@ -99,7 +99,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
date_str = entry.get("Date", "") date_str = entry.get("Date", "")
if "/Date(" in date_str: if "/Date(" in date_str:
ms = int(date_str.split("(")[1].split(")")[0]) ms = int(date_str.split("(")[1].split(")")[0])
entry_date = datetime.utcfromtimestamp(ms / 1000) entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
else: else:
continue continue
@@ -122,21 +122,21 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
) )
rows_synced += 1 rows_synced += 1
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000) duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute( await execute(
"""INSERT INTO seo_sync_log """INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms) (source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('bing', 'success', ?, ?, ?, ?)""", VALUES ('bing', 'success', ?, ?, ?, ?)""",
(rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms), (rows_synced, started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
) )
return rows_synced return rows_synced
except Exception as exc: except Exception as exc:
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000) duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute( await execute(
"""INSERT INTO seo_sync_log """INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms) (source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('bing', 'failed', 0, ?, ?, ?, ?)""", VALUES ('bing', 'failed', 0, ?, ?, ?, ?)""",
(str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms), (str(exc), started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
) )
raise raise

View File

@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
from ..core import config, execute from ..core import config, execute, utcnow, utcnow_iso
# GSC returns max 25K rows per request # GSC returns max 25K rows per request
_ROWS_PER_PAGE = 25_000 _ROWS_PER_PAGE = 25_000
@@ -95,11 +95,11 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
if not config.GSC_SERVICE_ACCOUNT_PATH or not config.GSC_SITE_URL: if not config.GSC_SERVICE_ACCOUNT_PATH or not config.GSC_SITE_URL:
return 0 # GSC not configured — skip silently return 0 # GSC not configured — skip silently
started_at = datetime.utcnow() started_at = utcnow()
# GSC has ~2 day delay; fetch from days_back ago to 2 days ago # GSC has ~2 day delay; fetch from days_back ago to 2 days ago
end_date = (datetime.utcnow() - timedelta(days=2)).strftime("%Y-%m-%d") end_date = (utcnow() - timedelta(days=2)).strftime("%Y-%m-%d")
start_date = (datetime.utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d") start_date = (utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d")
try: try:
rows = await asyncio.to_thread( rows = await asyncio.to_thread(
@@ -122,21 +122,21 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
) )
rows_synced += 1 rows_synced += 1
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000) duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute( await execute(
"""INSERT INTO seo_sync_log """INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms) (source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('gsc', 'success', ?, ?, ?, ?)""", VALUES ('gsc', 'success', ?, ?, ?, ?)""",
(rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms), (rows_synced, started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
) )
return rows_synced return rows_synced
except Exception as exc: except Exception as exc:
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000) duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute( await execute(
"""INSERT INTO seo_sync_log """INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms) (source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('gsc', 'failed', 0, ?, ?, ?, ?)""", VALUES ('gsc', 'failed', 0, ?, ?, ?, ?)""",
(str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms), (str(exc), started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
) )
raise raise

View File

@@ -4,14 +4,14 @@ All heavy lifting happens in SQL. Functions accept filter parameters
and return plain dicts/lists. and return plain dicts/lists.
""" """
from datetime import datetime, timedelta from datetime import timedelta
from ..core import execute, fetch_all, fetch_one from ..core import execute, fetch_all, fetch_one, utcnow
def _date_cutoff(date_range_days: int) -> str: def _date_cutoff(date_range_days: int) -> str:
"""Return ISO date string for N days ago.""" """Return ISO date string for N days ago."""
return (datetime.utcnow() - timedelta(days=date_range_days)).strftime("%Y-%m-%d") return (utcnow() - timedelta(days=date_range_days)).strftime("%Y-%m-%d")
async def get_search_performance( async def get_search_performance(

View File

@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
import httpx import httpx
from ..core import config, execute from ..core import config, execute, utcnow, utcnow_iso
_TIMEOUT_SECONDS = 15 _TIMEOUT_SECONDS = 15
@@ -21,7 +21,7 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
if not config.UMAMI_API_TOKEN or not config.UMAMI_API_URL: if not config.UMAMI_API_TOKEN or not config.UMAMI_API_URL:
return 0 # Umami not configured — skip silently return 0 # Umami not configured — skip silently
started_at = datetime.utcnow() started_at = utcnow()
try: try:
rows_synced = 0 rows_synced = 0
@@ -34,7 +34,7 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
# (Umami's metrics endpoint returns totals for the period, # (Umami's metrics endpoint returns totals for the period,
# so we query one day at a time for daily granularity) # so we query one day at a time for daily granularity)
for day_offset in range(days_back): for day_offset in range(days_back):
day = datetime.utcnow() - timedelta(days=day_offset + 1) day = utcnow() - timedelta(days=day_offset + 1)
metric_date = day.strftime("%Y-%m-%d") metric_date = day.strftime("%Y-%m-%d")
start_ms = int(day.replace(hour=0, minute=0, second=0).timestamp() * 1000) start_ms = int(day.replace(hour=0, minute=0, second=0).timestamp() * 1000)
end_ms = int(day.replace(hour=23, minute=59, second=59).timestamp() * 1000) end_ms = int(day.replace(hour=23, minute=59, second=59).timestamp() * 1000)
@@ -96,21 +96,21 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
(metric_date, page_count, visitors, br, avg_time), (metric_date, page_count, visitors, br, avg_time),
) )
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000) duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute( await execute(
"""INSERT INTO seo_sync_log """INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms) (source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('umami', 'success', ?, ?, ?, ?)""", VALUES ('umami', 'success', ?, ?, ?, ?)""",
(rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms), (rows_synced, started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
) )
return rows_synced return rows_synced
except Exception as exc: except Exception as exc:
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000) duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute( await execute(
"""INSERT INTO seo_sync_log """INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms) (source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('umami', 'failed', 0, ?, ?, ?, ?)""", VALUES ('umami', 'failed', 0, ?, ?, ?, ?)""",
(str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms), (str(exc), started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
) )
raise raise

View File

@@ -5,12 +5,10 @@ NOT behind @role_required: Resend posts here unauthenticated.
Verification uses RESEND_WEBHOOK_SECRET via the Resend SDK. Verification uses RESEND_WEBHOOK_SECRET via the Resend SDK.
""" """
from datetime import datetime
import resend import resend
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from .core import config, execute from .core import config, execute, utcnow_iso
bp = Blueprint("webhooks", __name__, url_prefix="/webhooks") bp = Blueprint("webhooks", __name__, url_prefix="/webhooks")
@@ -67,7 +65,7 @@ async def _handle_delivery_event(event_type: str, data: dict) -> None:
return return
last_event, ts_col = _EVENT_UPDATES[event_type] last_event, ts_col = _EVENT_UPDATES[event_type]
now = datetime.utcnow().isoformat() now = utcnow_iso()
if ts_col: if ts_col:
await execute( await execute(
@@ -87,7 +85,7 @@ async def _handle_inbound(data: dict) -> None:
if not resend_id: if not resend_id:
return return
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"""INSERT OR IGNORE INTO inbound_emails """INSERT OR IGNORE INTO inbound_emails
(resend_id, message_id, in_reply_to, from_addr, to_addr, (resend_id, message_id, in_reply_to, from_addr, to_addr,

View File

@@ -7,7 +7,17 @@ import json
import traceback import traceback
from datetime import datetime, timedelta from datetime import datetime, timedelta
from .core import EMAIL_ADDRESSES, config, execute, fetch_all, fetch_one, init_db, send_email from .core import (
EMAIL_ADDRESSES,
config,
execute,
fetch_all,
fetch_one,
init_db,
send_email,
utcnow,
utcnow_iso,
)
from .i18n import get_translations from .i18n import get_translations
# Task handlers registry # Task handlers registry
@@ -29,7 +39,7 @@ def _email_wrap(body: str, lang: str = "en", preheader: str = "") -> str:
preheader: hidden preview text shown in email client list views. preheader: hidden preview text shown in email client list views.
""" """
year = datetime.utcnow().year year = utcnow().year
tagline = _t("email_footer_tagline", lang) tagline = _t("email_footer_tagline", lang)
copyright_text = _t("email_footer_copyright", lang, year=year, app_name=config.APP_NAME) copyright_text = _t("email_footer_copyright", lang, year=year, app_name=config.APP_NAME)
# Hidden preheader trick: visible text + invisible padding to prevent # Hidden preheader trick: visible text + invisible padding to prevent
@@ -132,15 +142,15 @@ async def enqueue(task_name: str, payload: dict = None, run_at: datetime = None)
( (
task_name, task_name,
json.dumps(payload or {}), json.dumps(payload or {}),
(run_at or datetime.utcnow()).isoformat(), (run_at or utcnow()).strftime("%Y-%m-%dT%H:%M:%S"),
datetime.utcnow().isoformat(), utcnow_iso(),
), ),
) )
async def get_pending_tasks(limit: int = 10) -> list[dict]: async def get_pending_tasks(limit: int = 10) -> list[dict]:
"""Get pending tasks ready to run.""" """Get pending tasks ready to run."""
now = datetime.utcnow().isoformat() now = utcnow_iso()
return await fetch_all( return await fetch_all(
""" """
SELECT * FROM tasks SELECT * FROM tasks
@@ -156,7 +166,7 @@ async def mark_complete(task_id: int) -> None:
"""Mark task as completed.""" """Mark task as completed."""
await execute( await execute(
"UPDATE tasks SET status = 'complete', completed_at = ? WHERE id = ?", "UPDATE tasks SET status = 'complete', completed_at = ? WHERE id = ?",
(datetime.utcnow().isoformat(), task_id), (utcnow_iso(), task_id),
) )
@@ -167,7 +177,7 @@ async def mark_failed(task_id: int, error: str, retries: int) -> None:
if retries < max_retries: if retries < max_retries:
# Exponential backoff: 1min, 5min, 25min # Exponential backoff: 1min, 5min, 25min
delay = timedelta(minutes=5**retries) delay = timedelta(minutes=5**retries)
run_at = datetime.utcnow() + delay run_at = utcnow() + delay
await execute( await execute(
""" """
@@ -385,13 +395,13 @@ async def handle_send_waitlist_confirmation(payload: dict) -> None:
@task("cleanup_expired_tokens") @task("cleanup_expired_tokens")
async def handle_cleanup_tokens(payload: dict) -> None: async def handle_cleanup_tokens(payload: dict) -> None:
"""Clean up expired auth tokens.""" """Clean up expired auth tokens."""
await execute("DELETE FROM auth_tokens WHERE expires_at < ?", (datetime.utcnow().isoformat(),)) await execute("DELETE FROM auth_tokens WHERE expires_at < ?", (utcnow_iso(),))
@task("cleanup_rate_limits") @task("cleanup_rate_limits")
async def handle_cleanup_rate_limits(payload: dict) -> None: async def handle_cleanup_rate_limits(payload: dict) -> None:
"""Clean up old rate limit entries.""" """Clean up old rate limit entries."""
cutoff = (datetime.utcnow() - timedelta(hours=1)).isoformat() cutoff = (utcnow() - timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%S")
await execute("DELETE FROM rate_limits WHERE timestamp < ?", (cutoff,)) await execute("DELETE FROM rate_limits WHERE timestamp < ?", (cutoff,))
@@ -497,7 +507,7 @@ async def handle_send_lead_forward_email(payload: dict) -> None:
) )
# Update email_sent_at on lead_forward # Update email_sent_at on lead_forward
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"UPDATE lead_forwards SET email_sent_at = ? WHERE lead_id = ? AND supplier_id = ?", "UPDATE lead_forwards SET email_sent_at = ? WHERE lead_id = ? AND supplier_id = ?",
(now, lead_id, supplier_id), (now, lead_id, supplier_id),
@@ -621,7 +631,7 @@ async def handle_generate_business_plan(payload: dict) -> None:
file_path.write_bytes(pdf_bytes) file_path.write_bytes(pdf_bytes)
# Update record # Update record
now = datetime.utcnow().isoformat() now = utcnow_iso()
await execute( await execute(
"UPDATE business_plan_exports SET status = 'ready', file_path = ?, completed_at = ? WHERE id = ?", "UPDATE business_plan_exports SET status = 'ready', file_path = ?, completed_at = ? WHERE id = ?",
(str(file_path), now, export_id), (str(file_path), now, export_id),
@@ -664,7 +674,7 @@ async def handle_generate_business_plan(payload: dict) -> None:
@task("cleanup_old_tasks") @task("cleanup_old_tasks")
async def handle_cleanup_tasks(payload: dict) -> None: async def handle_cleanup_tasks(payload: dict) -> None:
"""Clean up completed/failed tasks older than 7 days.""" """Clean up completed/failed tasks older than 7 days."""
cutoff = (datetime.utcnow() - timedelta(days=7)).isoformat() cutoff = (utcnow() - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
await execute( await execute(
"DELETE FROM tasks WHERE status IN ('complete', 'failed') AND created_at < ?", (cutoff,) "DELETE FROM tasks WHERE status IN ('complete', 'failed') AND created_at < ?", (cutoff,)
) )
@@ -791,9 +801,7 @@ async def run_scheduler() -> None:
await enqueue("cleanup_old_tasks") await enqueue("cleanup_old_tasks")
# Monthly credit refill — run on the 1st of each month # Monthly credit refill — run on the 1st of each month
from datetime import datetime today = utcnow()
today = datetime.utcnow()
this_month = f"{today.year}-{today.month:02d}" this_month = f"{today.year}-{today.month:02d}"
if today.day == 1 and last_credit_refill != this_month: if today.day == 1 and last_credit_refill != this_month:
await enqueue("refill_monthly_credits") await enqueue("refill_monthly_credits")