Files
pgz-sport/erp/notifications.py_prije_env_deepseek
T

208 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# erp/notifications.py — PGŽ Sport ERP mock e-mail notifikacije (R6)
# Author: Damir Radulić <damir@rinet.one> / dradulic@outlook.com
# Date: 2026-05-04
# Description: Mock e-mail / channel='email' notifikacije pri promjeni statusa
# ERP entiteta. Upisuje u pgz_sport.notifications + log line.
# U produkciji se može zamijeniti pravim SMTP/Mailgun adapterom.
from __future__ import annotations
import json
import logging
import os
from datetime import datetime
from typing import Optional, Iterable
import psycopg2
import psycopg2.extras
DB = dict(host="10.10.0.2", port=6432, dbname="rinet_v3", user="rinet",
password=os.environ["DB_PASSWORD"])
LOG_PATH = os.environ.get("ERP_NOTIFY_LOG", "/var/log/pgz-sport-erp-notify.log")
logger = logging.getLogger("erp.notifications")
if not logger.handlers:
logger.setLevel(logging.INFO)
try:
fh = logging.FileHandler(LOG_PATH)
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logger.addHandler(fh)
except Exception:
pass
sh = logging.StreamHandler()
sh.setFormatter(logging.Formatter("[ERP-NOTIFY] %(message)s"))
logger.addHandler(sh)
def _db():
c = psycopg2.connect(**DB); c.autocommit = True; return c
def _resolve_recipients(klub_id: Optional[int], user_id: Optional[int]) -> list[dict]:
"""Vrati listu primatelja: voditelj putovanja (user_id), klub_admin svog kluba,
+ pgz_admin kao info kopija."""
out: list[dict] = []
seen = set()
try:
with _db() as c:
cur = c.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
if user_id:
cur.execute(
"SELECT id, email, COALESCE(full_name, ime||' '||prezime, email) AS name "
"FROM pgz_sport.users WHERE id=%s AND status='active'", (user_id,))
r = cur.fetchone()
if r and r["email"] and r["id"] not in seen:
out.append({**r, "rola": "voditelj"}); seen.add(r["id"])
if klub_id:
cur.execute(
"""SELECT id, email, COALESCE(full_name, ime||' '||prezime, email) AS name
FROM pgz_sport.users
WHERE klub_id=%s AND user_type='klub_admin' AND status='active'""",
(klub_id,))
for r in cur.fetchall():
if r["email"] and r["id"] not in seen:
out.append({**r, "rola": "klub_admin"}); seen.add(r["id"])
cur.execute(
"""SELECT id, email, COALESCE(full_name, ime||' '||prezime, email) AS name
FROM pgz_sport.users
WHERE user_type='pgz_admin' AND status='active' LIMIT 5""")
for r in cur.fetchall():
if r["email"] and r["id"] not in seen:
out.append({**r, "rola": "pgz_admin"}); seen.add(r["id"])
except Exception as e:
logger.warning("recipients fetch fail: %s", e)
return out
def _store(user_id: Optional[int], subject: str, body: str, meta: dict,
channel: str = "email", status: str = "queued") -> Optional[int]:
try:
with _db() as c:
cur = c.cursor()
cur.execute(
"""INSERT INTO pgz_sport.notifications
(user_id, channel, subject, body, status, scheduled_at, meta)
VALUES (%s,%s,%s,%s,%s,NOW(),%s)
RETURNING id""",
(user_id, channel, subject[:200], body[:5000], status,
json.dumps(meta, ensure_ascii=False, default=str)),
)
return cur.fetchone()[0]
except Exception as e:
logger.warning("notification insert fail: %s", e)
return None
def _dispatch(subject: str, body: str, *, klub_id: Optional[int] = None,
user_id: Optional[int] = None, meta: Optional[dict] = None) -> dict:
meta = dict(meta or {})
recipients = _resolve_recipients(klub_id, user_id)
delivered = []
if not recipients:
# Mock: nemamo korisnika, samo log + jedan info zapis bez user_id
nid = _store(None, subject, body,
{**meta, "to": "(no_recipient)", "klub_id": klub_id})
logger.info("MOCK email (no recipient) [%s] %s", nid, subject)
return {"sent": 0, "queued": 1 if nid else 0, "ids": [nid] if nid else [],
"recipients": []}
for r in recipients:
nid = _store(r["id"], subject, body,
{**meta, "to": r["email"], "rola": r.get("rola"),
"name": r.get("name")})
if nid:
delivered.append({"id": nid, "user_id": r["id"], "email": r["email"]})
logger.info(
"MOCK email queued [%s] to=%s rola=%s subj=%r",
nid, r["email"], r.get("rola"), subject,
)
return {"sent": 0, "queued": len(delivered), "ids": [d["id"] for d in delivered],
"recipients": [d["email"] for d in delivered]}
# ─── Public helpers ────────────────────────────────────────────────────
def notify_invoice_created(invoice: dict) -> dict:
"""Račun spremljen iz OCR-a — info klub_admin."""
subj = f"Novi račun #{invoice.get('id')}: {invoice.get('vendor_name','')} (€{invoice.get('amount_gross')})"
body = (
f"Račun {invoice.get('invoice_no')} od {invoice.get('vendor_name')} "
f"(OIB {invoice.get('vendor_oib')}) iznosa €{invoice.get('amount_gross')} "
f"na datum {invoice.get('invoice_date')} unesen je u sustav.\n\n"
f"Klub: {invoice.get('klub_id')} · Vrsta: {invoice.get('invoice_kind')} · Status: {invoice.get('payment_status')}"
)
return _dispatch(subj, body, klub_id=invoice.get("klub_id"),
meta={"event": "invoice_created", "invoice_id": invoice.get("id")})
def notify_invoice_paid(invoice: dict, payment: Optional[dict] = None) -> dict:
iban = (payment or {}).get("iban_to") or invoice.get("iban_to") or ""
subj = f"Račun #{invoice.get('id')} označen kao plaćen — €{invoice.get('amount_gross')}"
body = (
f"Račun {invoice.get('invoice_no')} izdan od {invoice.get('vendor_name')} "
f"je označen kao plaćen.\n"
f"Iznos: €{invoice.get('amount_gross')}\n"
f"Datum uplate: {invoice.get('paid_date')}\n"
f"IBAN primatelja: {iban}\n"
f"Referenca: {(payment or {}).get('reference','')}"
)
return _dispatch(subj, body, klub_id=invoice.get("klub_id"),
meta={"event": "invoice_paid", "invoice_id": invoice.get("id")})
def notify_invoice_cancelled(invoice: dict, razlog: str = "") -> dict:
subj = f"Račun #{invoice.get('id')} otkazan"
body = f"Račun {invoice.get('invoice_no')} ({invoice.get('vendor_name')}) je otkazan.\nRazlog: {razlog or ''}"
return _dispatch(subj, body, klub_id=invoice.get("klub_id"),
meta={"event": "invoice_cancelled", "invoice_id": invoice.get("id"),
"razlog": razlog})
def notify_pn_submitted(pn: dict) -> dict:
subj = f"Putni nalog #{pn.get('id')} poslan na odobrenje (€{pn.get('cost_total')})"
body = (
f"Putni nalog za destinaciju '{pn.get('destination')}' "
f"({pn.get('date_from')} {pn.get('date_to')}) "
f"poslan je na odobrenje.\nIznos: €{pn.get('cost_total')}"
)
return _dispatch(subj, body, klub_id=pn.get("klub_id"), user_id=pn.get("user_id"),
meta={"event": "pn_submitted", "pn_id": pn.get("id")})
def notify_pn_approved(pn: dict) -> dict:
subj = f"Putni nalog #{pn.get('id')} ODOBREN — €{pn.get('cost_total')}"
body = (
f"Putni nalog za '{pn.get('destination')}' "
f"({pn.get('date_from')} {pn.get('date_to')}) je odobren.\n"
f"Iznos: €{pn.get('cost_total')}"
)
return _dispatch(subj, body, klub_id=pn.get("klub_id"), user_id=pn.get("user_id"),
meta={"event": "pn_approved", "pn_id": pn.get("id")})
def notify_pn_rejected(pn: dict, razlog: str = "") -> dict:
subj = f"Putni nalog #{pn.get('id')} ODBIJEN"
body = f"Putni nalog za '{pn.get('destination')}' je odbijen.\nRazlog: {razlog or ''}"
return _dispatch(subj, body, klub_id=pn.get("klub_id"), user_id=pn.get("user_id"),
meta={"event": "pn_rejected", "pn_id": pn.get("id"), "razlog": razlog})
def notify_pn_paid(pn: dict, payment: Optional[dict] = None) -> dict:
iban = (payment or {}).get("iban_to") or ""
subj = f"Putni nalog #{pn.get('id')} ISPLAĆEN — €{pn.get('cost_total')}"
body = (
f"Putni nalog za '{pn.get('destination')}' isplaćen je voditelju.\n"
f"Iznos: €{(payment or {}).get('amount') or pn.get('cost_total')}\n"
f"IBAN primatelja: {iban}\n"
f"Datum isplate: {pn.get('paid_at') or (payment or {}).get('payment_date')}\n"
f"Referenca: {(payment or {}).get('reference','')}"
)
return _dispatch(subj, body, klub_id=pn.get("klub_id"), user_id=pn.get("user_id"),
meta={"event": "pn_paid", "pn_id": pn.get("id")})
__all__ = [
"notify_invoice_created", "notify_invoice_paid", "notify_invoice_cancelled",
"notify_pn_submitted", "notify_pn_approved", "notify_pn_rejected", "notify_pn_paid",
]