211 lines
9.4 KiB
Python
211 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
||
from dotenv import load_dotenv
|
||
load_dotenv('/opt/rinet-gpu/.env.master')
|
||
# auto-added by patch_scrapers_with_dotenv.sh
|
||
# 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",
|
||
]
|