fix: resolve open risks — invoice race condition, SMTP config, workflow seeds
- billing/service.py: pg_advisory_xact_lock on invoice_number_seq per year → prevents duplicate INV-YYYY-NNNN under concurrent requests - admin.py: SMTP settings in system_settings (smtp_host/port/user/password/ from_address/enabled) with GET+PUT support; seed-workflows endpoint creates 4 standard workflow definitions (still-cycles, still-eevee, turntable, multi-angle) idempotently - notifications/service.py: send_email_notification_stub now sends real SMTP email via smtplib when smtp_enabled=true in system_settings - Admin.tsx: SMTP settings panel (host/port/user/password/from + enable toggle, save button); Seed Standard Workflows maintenance button - Upload.tsx: fix TS error — title→aria-label on Lucide icons - Admin.tsx Settings type: add render_backend/flamenco_* fields (TS fix) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -200,8 +200,16 @@ VALID_STATUSES = {"draft", "sent", "paid", "cancelled"}
|
||||
|
||||
|
||||
async def generate_invoice_number(db: AsyncSession, tenant_id: uuid.UUID | None) -> str:
|
||||
"""Generate sequential invoice number: INV-YYYY-NNNN."""
|
||||
"""Generate sequential invoice number: INV-YYYY-NNNN.
|
||||
|
||||
Uses a PostgreSQL advisory lock to prevent race conditions when multiple
|
||||
requests create invoices concurrently. The lock is automatically released
|
||||
when the surrounding transaction commits or rolls back.
|
||||
"""
|
||||
from sqlalchemy import text
|
||||
year = datetime.utcnow().year
|
||||
# Advisory lock keyed on year — serialises concurrent invoice creation
|
||||
await db.execute(text("SELECT pg_advisory_xact_lock(hashtext(:key))"), {"key": f"invoice_number_seq_{year}"})
|
||||
count_result = await db.execute(
|
||||
select(func.count()).select_from(Invoice).where(
|
||||
func.extract("year", Invoice.created_at) == year
|
||||
|
||||
@@ -112,11 +112,64 @@ def send_email_notification_stub(
|
||||
subject: str,
|
||||
body: str,
|
||||
) -> None:
|
||||
"""Email notification stub — logs only, email sending not yet active."""
|
||||
logger.info(
|
||||
"[EMAIL STUB] Would send email to user=%s event=%s subject=%s",
|
||||
to_user_id, event_type, subject
|
||||
)
|
||||
"""Send email notification via SMTP if configured and enabled, else log only."""
|
||||
engine = _get_engine()
|
||||
try:
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.system_setting import SystemSetting
|
||||
with Session(engine) as s:
|
||||
rows = s.execute(
|
||||
__import__("sqlalchemy").select(SystemSetting).where(
|
||||
SystemSetting.key.in_(["smtp_enabled", "smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_from_address"])
|
||||
)
|
||||
).scalars().all()
|
||||
cfg = {r.key: r.value for r in rows}
|
||||
except Exception:
|
||||
cfg = {}
|
||||
|
||||
smtp_enabled = cfg.get("smtp_enabled", "false").lower() == "true"
|
||||
smtp_host = cfg.get("smtp_host", "")
|
||||
|
||||
if not smtp_enabled or not smtp_host:
|
||||
logger.info(
|
||||
"[EMAIL STUB] Would send email to user=%s event=%s subject=%s (smtp disabled)",
|
||||
to_user_id, event_type, subject
|
||||
)
|
||||
return
|
||||
|
||||
# Resolve to_address from user record
|
||||
to_address: str | None = None
|
||||
try:
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.user import User
|
||||
with Session(engine) as s:
|
||||
u = s.get(User, to_user_id) if to_user_id else None
|
||||
if u:
|
||||
to_address = u.email
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not to_address:
|
||||
logger.warning("[EMAIL] Could not resolve email for user=%s", to_user_id)
|
||||
return
|
||||
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
msg = MIMEText(body, "plain", "utf-8")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = cfg.get("smtp_from_address") or cfg.get("smtp_user", "noreply@schaeffler.com")
|
||||
msg["To"] = to_address
|
||||
port = int(cfg.get("smtp_port", "587"))
|
||||
with smtplib.SMTP(smtp_host, port) as smtp:
|
||||
smtp.ehlo()
|
||||
smtp.starttls()
|
||||
if cfg.get("smtp_user") and cfg.get("smtp_password"):
|
||||
smtp.login(cfg["smtp_user"], cfg["smtp_password"])
|
||||
smtp.sendmail(msg["From"], [to_address], msg.as_string())
|
||||
logger.info("[EMAIL] Sent to %s event=%s", to_address, event_type)
|
||||
except Exception as exc:
|
||||
logger.error("[EMAIL] Failed to send to %s: %s", to_address, exc)
|
||||
|
||||
|
||||
async def get_notification_configs(db: AsyncSession, user_id: uuid.UUID) -> list:
|
||||
|
||||
Reference in New Issue
Block a user