M7 CRM Članarine: CRUD + dug + uplata + HUB-3 PDF + EPC QR
- /api/crm/clanarine[CRUD] s filterima (godina/klub/clan/status), summary
- /api/crm/clanarine/dug — dužnici (z opcionim days_overdue)
- /api/crm/clanarine/{id}/uplata — registracija parcijalne/cijele uplate
- /api/crm/clanarine/notify-bulk — mock e-mail kampanja (lista primatelja)
- /api/crm/clanarine/{id}/uplatnica.pdf — HUB-3 A4 PDF s ugrađenim EPC QR
- /api/crm/clanarine/{id}/qr.png — samo EPC BCD/002 SCT QR PNG
- /api/crm/clanarine/{id}/payment-info — JSON za UI gumbe + bank deep linkovi
crm/payments.py — HUB-3 PDF generator (ReportLab) + EPC QR (qrcode lib),
poziv-na-broj model HR00 = OIB-godina-id, format_eur HR notation.
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
#!/usr/bin/env python3
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Fajl: routers/clanarine_router.py | v1.0.0 | 04.05.2026
|
||||
# Autor: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
|
||||
# Lokacija: /opt/pgz-sport/routers/clanarine_router.py
|
||||
# Svrha: M7 — CRM Članarine: CRUD + dug + uplata + HUB-3 PDF + EPC QR + bulk notify
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
"""M7 Članarine router.
|
||||
|
||||
Endpointi (montirani s prefixom /api/crm):
|
||||
GET /clanarine → lista (filteri)
|
||||
POST /clanarine → kreiraj zaduženje
|
||||
GET /clanarine/{id} → detalji
|
||||
PUT /clanarine/{id} → update
|
||||
DELETE /clanarine/{id} → soft delete (status=storno)
|
||||
POST /clanarine/{id}/uplata → registriraj uplatu
|
||||
GET /clanarine/dug → svi koji duguju
|
||||
POST /clanarine/notify-bulk → mock e-mail notifikacija
|
||||
GET /clanarine/{id}/uplatnica.pdf → HUB-3 PDF
|
||||
GET /clanarine/{id}/qr.png → EPC QR PNG
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import re
|
||||
from datetime import date
|
||||
from typing import Optional, List
|
||||
from decimal import Decimal
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from fastapi import APIRouter, HTTPException, Query, Body
|
||||
from fastapi.responses import Response
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
sys.path.insert(0, "/opt/pgz-sport")
|
||||
from crm.payments import (
|
||||
build_hub3_pdf, build_epc_qr_png, build_epc_payload,
|
||||
make_poziv_na_broj, build_bank_deep_links, normalize_iban,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/crm", tags=["crm-clanarine"])
|
||||
|
||||
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
||||
|
||||
# Default IBAN PGŽ Sport (placeholder ako klub nema svoj). Realan IBAN PGŽ
|
||||
# Odjela za sport stavi ovdje kad bude poznat — za sada koristimo neutralni
|
||||
# format za demo svrhe.
|
||||
DEFAULT_PRIMATELJ_IBAN = "HR0000000000000000000"
|
||||
DEFAULT_PRIMATELJ_NAZIV = "PGŽ Odjel za sport"
|
||||
DEFAULT_PRIMATELJ_ADRESA = "Adamićeva 10, 51000 Rijeka"
|
||||
|
||||
|
||||
def _conn():
|
||||
return psycopg2.connect(DSN, cursor_factory=RealDictCursor)
|
||||
|
||||
|
||||
def _conv(v):
|
||||
if isinstance(v, (date,)):
|
||||
return v.isoformat()
|
||||
if isinstance(v, Decimal):
|
||||
return float(v)
|
||||
return v
|
||||
|
||||
|
||||
def _row(d):
|
||||
return {k: _conv(v) for k, v in dict(d).items()}
|
||||
|
||||
|
||||
def _compute_status(propisan: float, placen: float) -> str:
|
||||
p, pl = float(propisan or 0), float(placen or 0)
|
||||
if pl <= 0:
|
||||
return "nepodmireno"
|
||||
if pl + 0.005 < p:
|
||||
return "djelomicno"
|
||||
return "podmireno"
|
||||
|
||||
|
||||
# ───────────── modeli ─────────────
|
||||
|
||||
class ClanarinaIn(BaseModel):
|
||||
clan_id: int
|
||||
klub_id: Optional[int] = None
|
||||
godina: int
|
||||
razdoblje: Optional[str] = "godišnja"
|
||||
iznos_propisan: float
|
||||
iznos_placen: Optional[float] = 0
|
||||
datum_uplate: Optional[date] = None
|
||||
nacin_uplate: Optional[str] = None
|
||||
napomena: Optional[str] = None
|
||||
|
||||
|
||||
class ClanarinaPatch(BaseModel):
|
||||
klub_id: Optional[int] = None
|
||||
godina: Optional[int] = None
|
||||
razdoblje: Optional[str] = None
|
||||
iznos_propisan: Optional[float] = None
|
||||
iznos_placen: Optional[float] = None
|
||||
datum_uplate: Optional[date] = None
|
||||
nacin_uplate: Optional[str] = None
|
||||
napomena: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class UplataIn(BaseModel):
|
||||
iznos: float = Field(..., gt=0)
|
||||
datum_uplate: Optional[date] = None
|
||||
nacin_uplate: Optional[str] = "transakcijski"
|
||||
referenca: Optional[str] = None
|
||||
racun_broj: Optional[str] = None
|
||||
|
||||
|
||||
class NotifyBulkIn(BaseModel):
|
||||
klub_id: Optional[int] = None
|
||||
godina: Optional[int] = None
|
||||
template: Optional[str] = "Poštovani, podsjećamo na nepodmirenu članarinu."
|
||||
|
||||
|
||||
# ───────────── lista ─────────────
|
||||
|
||||
@router.get("/clanarine")
|
||||
def list_clanarine(
|
||||
godina: Optional[int] = Query(None),
|
||||
klub_id: Optional[int] = Query(None),
|
||||
clan_id: Optional[int] = Query(None),
|
||||
status: Optional[str] = Query(None, description="nepodmireno|djelomicno|podmireno|storno"),
|
||||
sort: str = Query("godina"),
|
||||
order: str = Query("desc"),
|
||||
limit: int = Query(500, le=2000),
|
||||
):
|
||||
where, params = [], []
|
||||
if godina:
|
||||
where.append("c.godina = %s"); params.append(godina)
|
||||
if klub_id:
|
||||
where.append("c.klub_id = %s"); params.append(klub_id)
|
||||
if clan_id:
|
||||
where.append("c.clan_id = %s"); params.append(clan_id)
|
||||
if status:
|
||||
where.append("c.status = %s"); params.append(status)
|
||||
|
||||
sort_map = {
|
||||
"godina": "c.godina",
|
||||
"iznos": "c.iznos_propisan",
|
||||
"klub": "k.naziv",
|
||||
"datum_uplate": "c.datum_uplate",
|
||||
"status": "c.status",
|
||||
"dug": "(c.iznos_propisan - COALESCE(c.iznos_placen,0))",
|
||||
}
|
||||
sort_col = sort_map.get(sort, "c.godina")
|
||||
order_sql = "DESC" if order.lower() == "desc" else "ASC"
|
||||
where_sql = ("WHERE " + " AND ".join(where)) if where else ""
|
||||
|
||||
sql = f"""
|
||||
SELECT c.id, c.clan_id, c.klub_id, c.godina, c.razdoblje,
|
||||
c.iznos_propisan, c.iznos_placen,
|
||||
(c.iznos_propisan - COALESCE(c.iznos_placen,0))::numeric(10,2) AS dug,
|
||||
c.datum_uplate, c.nacin_uplate, c.referenca, c.racun_broj,
|
||||
c.status, c.napomena, c.created_at, c.updated_at,
|
||||
cl.ime || ' ' || cl.prezime AS clan,
|
||||
cl.oib AS clan_oib,
|
||||
k.naziv AS klub, k.oib AS klub_oib, k.iban AS klub_iban
|
||||
FROM pgz_sport.clanarine c
|
||||
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
{where_sql}
|
||||
ORDER BY {sort_col} {order_sql}
|
||||
LIMIT %s
|
||||
"""
|
||||
params.append(limit)
|
||||
|
||||
sum_sql = f"""
|
||||
SELECT COUNT(*) AS total,
|
||||
COALESCE(SUM(c.iznos_propisan), 0)::numeric(10,2) AS total_propisan,
|
||||
COALESCE(SUM(c.iznos_placen), 0)::numeric(10,2) AS total_placen,
|
||||
COALESCE(SUM(c.iznos_propisan - COALESCE(c.iznos_placen,0)), 0)::numeric(10,2) AS total_dug,
|
||||
COUNT(*) FILTER (WHERE c.status='nepodmireno') AS n_nepodmireno,
|
||||
COUNT(*) FILTER (WHERE c.status='djelomicno') AS n_djelomicno,
|
||||
COUNT(*) FILTER (WHERE c.status='podmireno') AS n_podmireno
|
||||
FROM pgz_sport.clanarine c
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
{where_sql}
|
||||
"""
|
||||
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
rows = [_row(r) for r in cur.fetchall()]
|
||||
cur.execute(sum_sql, params[:-1]) # bez LIMIT
|
||||
summary = _row(cur.fetchone() or {})
|
||||
|
||||
return {"count": len(rows), "rows": rows, "summary": summary}
|
||||
|
||||
|
||||
# ───────────── dug (samo neplaćene) ─────────────
|
||||
|
||||
@router.get("/clanarine/dug")
|
||||
def list_dug(
|
||||
klub_id: Optional[int] = Query(None),
|
||||
godina: Optional[int] = Query(None),
|
||||
days_overdue: int = Query(0, ge=0,
|
||||
description="koliko dana mora biti od kreiranja dužnosti (default 0)"),
|
||||
limit: int = Query(500, le=2000),
|
||||
):
|
||||
where = ["c.status IN ('nepodmireno', 'djelomicno')"]
|
||||
params: list = []
|
||||
if klub_id:
|
||||
where.append("c.klub_id = %s"); params.append(klub_id)
|
||||
if godina:
|
||||
where.append("c.godina = %s"); params.append(godina)
|
||||
if days_overdue > 0:
|
||||
where.append("c.created_at < (now() - (%s || ' days')::interval)")
|
||||
params.append(str(days_overdue))
|
||||
|
||||
where_sql = "WHERE " + " AND ".join(where)
|
||||
params.append(limit)
|
||||
sql = f"""
|
||||
SELECT c.id, c.clan_id, c.klub_id, c.godina, c.razdoblje,
|
||||
c.iznos_propisan, c.iznos_placen,
|
||||
(c.iznos_propisan - COALESCE(c.iznos_placen,0))::numeric(10,2) AS dug,
|
||||
c.status, c.created_at,
|
||||
cl.ime || ' ' || cl.prezime AS clan,
|
||||
cl.email AS clan_email, cl.telefon AS clan_telefon,
|
||||
k.naziv AS klub, k.oib AS klub_oib, k.iban AS klub_iban
|
||||
FROM pgz_sport.clanarine c
|
||||
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
{where_sql}
|
||||
ORDER BY (c.iznos_propisan - COALESCE(c.iznos_placen,0)) DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
rows = [_row(r) for r in cur.fetchall()]
|
||||
total_dug = sum(float(r["dug"] or 0) for r in rows)
|
||||
return {"count": len(rows), "total_dug": round(total_dug, 2), "rows": rows}
|
||||
|
||||
|
||||
# ───────────── detalji ─────────────
|
||||
|
||||
@router.get("/clanarine/{cid}")
|
||||
def get_clanarina(cid: int):
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT c.*,
|
||||
(c.iznos_propisan - COALESCE(c.iznos_placen,0))::numeric(10,2) AS dug,
|
||||
cl.ime || ' ' || cl.prezime AS clan,
|
||||
cl.oib AS clan_oib, cl.email AS clan_email,
|
||||
k.naziv AS klub, k.oib AS klub_oib, k.iban AS klub_iban,
|
||||
k.adresa AS klub_adresa, k.grad AS klub_grad
|
||||
FROM pgz_sport.clanarine c
|
||||
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
WHERE c.id = %s
|
||||
""", (cid,))
|
||||
r = cur.fetchone()
|
||||
if not r:
|
||||
raise HTTPException(404, "Članarina ne postoji")
|
||||
return _row(r)
|
||||
|
||||
|
||||
# ───────────── kreiraj ─────────────
|
||||
|
||||
@router.post("/clanarine")
|
||||
def create_clanarina(body: ClanarinaIn):
|
||||
klub_id = body.klub_id
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
if not klub_id:
|
||||
cur.execute("SELECT klub_id FROM pgz_sport.clanovi WHERE id=%s", (body.clan_id,))
|
||||
r = cur.fetchone()
|
||||
klub_id = r["klub_id"] if r else None
|
||||
status = _compute_status(body.iznos_propisan, body.iznos_placen or 0)
|
||||
cur.execute("""
|
||||
INSERT INTO pgz_sport.clanarine
|
||||
(clan_id, klub_id, godina, razdoblje, iznos_propisan, iznos_placen,
|
||||
datum_uplate, nacin_uplate, status, napomena)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
RETURNING *
|
||||
""", (body.clan_id, klub_id, body.godina, body.razdoblje,
|
||||
body.iznos_propisan, body.iznos_placen or 0,
|
||||
body.datum_uplate, body.nacin_uplate, status, body.napomena))
|
||||
r = cur.fetchone()
|
||||
conn.commit()
|
||||
return _row(r)
|
||||
|
||||
|
||||
# ───────────── update / delete ─────────────
|
||||
|
||||
@router.put("/clanarine/{cid}")
|
||||
def update_clanarina(cid: int, patch: ClanarinaPatch):
|
||||
fields, params = [], []
|
||||
for f in ("klub_id", "godina", "razdoblje", "iznos_propisan", "iznos_placen",
|
||||
"datum_uplate", "nacin_uplate", "napomena", "status"):
|
||||
v = getattr(patch, f)
|
||||
if v is not None:
|
||||
fields.append(f"{f} = %s"); params.append(v)
|
||||
if not fields:
|
||||
raise HTTPException(400, "Nema polja za izmjenu")
|
||||
fields.append("updated_at = now()")
|
||||
params.append(cid)
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute(f"UPDATE pgz_sport.clanarine SET {', '.join(fields)} WHERE id=%s RETURNING *",
|
||||
params)
|
||||
r = cur.fetchone()
|
||||
if not r:
|
||||
raise HTTPException(404, "Članarina ne postoji")
|
||||
# ako nije eksplicitno postavljen status, izračunaj iz iznos_*
|
||||
if patch.status is None:
|
||||
new_status = _compute_status(r["iznos_propisan"], r["iznos_placen"] or 0)
|
||||
if new_status != r["status"]:
|
||||
cur.execute("UPDATE pgz_sport.clanarine SET status=%s WHERE id=%s RETURNING *",
|
||||
(new_status, cid))
|
||||
r = cur.fetchone()
|
||||
conn.commit()
|
||||
return _row(r)
|
||||
|
||||
|
||||
@router.delete("/clanarine/{cid}")
|
||||
def delete_clanarina(cid: int):
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("UPDATE pgz_sport.clanarine SET status='storno', updated_at=now() WHERE id=%s RETURNING id",
|
||||
(cid,))
|
||||
r = cur.fetchone()
|
||||
if not r:
|
||||
raise HTTPException(404, "Članarina ne postoji")
|
||||
return {"ok": True, "id": cid, "status": "storno"}
|
||||
|
||||
|
||||
# ───────────── uplata ─────────────
|
||||
|
||||
@router.post("/clanarine/{cid}/uplata")
|
||||
def register_uplata(cid: int, body: UplataIn):
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM pgz_sport.clanarine WHERE id=%s", (cid,))
|
||||
cur_row = cur.fetchone()
|
||||
if not cur_row:
|
||||
raise HTTPException(404, "Članarina ne postoji")
|
||||
novi_iznos = float(cur_row["iznos_placen"] or 0) + float(body.iznos)
|
||||
novi_status = _compute_status(cur_row["iznos_propisan"], novi_iznos)
|
||||
cur.execute("""
|
||||
UPDATE pgz_sport.clanarine
|
||||
SET iznos_placen = %s,
|
||||
datum_uplate = COALESCE(%s, datum_uplate, now()::date),
|
||||
nacin_uplate = COALESCE(%s, nacin_uplate),
|
||||
referenca = COALESCE(%s, referenca),
|
||||
racun_broj = COALESCE(%s, racun_broj),
|
||||
status = %s,
|
||||
updated_at = now()
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
""", (novi_iznos, body.datum_uplate, body.nacin_uplate,
|
||||
body.referenca, body.racun_broj, novi_status, cid))
|
||||
r = cur.fetchone()
|
||||
# log u audit_feed (ako postoji); nepoznata schema → silent skip
|
||||
try:
|
||||
cur.execute("""INSERT INTO pgz_sport.audit_feed (entity_type, entity_id, action, payload)
|
||||
VALUES (%s,%s,%s,%s::jsonb)""",
|
||||
("clanarina", cid, "uplata",
|
||||
f'{{"iznos":{body.iznos}, "novi_status":"{novi_status}"}}'))
|
||||
except Exception:
|
||||
pass
|
||||
conn.commit()
|
||||
return {"ok": True, "id": cid, "iznos_uplata": body.iznos,
|
||||
"iznos_placen_total": novi_iznos, "status": novi_status,
|
||||
"clanarina": _row(r)}
|
||||
|
||||
|
||||
# ───────────── notify-bulk (mock email) ─────────────
|
||||
|
||||
@router.post("/clanarine/notify-bulk")
|
||||
def notify_bulk(body: NotifyBulkIn):
|
||||
"""
|
||||
Stvarno slanje pošte se još ne implementira (M9 SMTP integration TODO),
|
||||
ali endpoint vraća listu primatelja koji bi bili kontaktirani.
|
||||
"""
|
||||
where = ["c.status IN ('nepodmireno','djelomicno')", "cl.email IS NOT NULL"]
|
||||
params = []
|
||||
if body.klub_id:
|
||||
where.append("c.klub_id=%s"); params.append(body.klub_id)
|
||||
if body.godina:
|
||||
where.append("c.godina =%s"); params.append(body.godina)
|
||||
where_sql = "WHERE " + " AND ".join(where)
|
||||
sql = f"""
|
||||
SELECT c.id, c.godina, c.iznos_propisan,
|
||||
(c.iznos_propisan - COALESCE(c.iznos_placen,0))::numeric(10,2) AS dug,
|
||||
cl.ime || ' ' || cl.prezime AS clan, cl.email,
|
||||
k.naziv AS klub
|
||||
FROM pgz_sport.clanarine c
|
||||
JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
{where_sql}
|
||||
ORDER BY dug DESC
|
||||
LIMIT 500
|
||||
"""
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
recipients = [_row(r) for r in cur.fetchall()]
|
||||
return {
|
||||
"ok": True,
|
||||
"queued": len(recipients),
|
||||
"template": body.template,
|
||||
"note": "Mock — SMTP nije konfiguriran. Lista primatelja vraćena za pregled.",
|
||||
"recipients": recipients,
|
||||
}
|
||||
|
||||
|
||||
# ───────────── HUB-3 PDF + EPC QR ─────────────
|
||||
|
||||
@router.get("/clanarine/{cid}/uplatnica.pdf")
|
||||
def uplatnica_pdf(cid: int):
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.godina, c.razdoblje, c.iznos_propisan, c.iznos_placen,
|
||||
(c.iznos_propisan - COALESCE(c.iznos_placen,0))::numeric(10,2) AS dug,
|
||||
cl.ime || ' ' || cl.prezime AS clan,
|
||||
cl.adresa AS clan_adresa, cl.grad AS clan_grad,
|
||||
k.naziv AS klub, k.oib AS klub_oib, k.iban AS klub_iban,
|
||||
k.adresa AS klub_adresa, k.grad AS klub_grad
|
||||
FROM pgz_sport.clanarine c
|
||||
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
WHERE c.id=%s
|
||||
""", (cid,))
|
||||
r = cur.fetchone()
|
||||
if not r:
|
||||
raise HTTPException(404, "Članarina ne postoji")
|
||||
|
||||
dug = float(r["dug"] or 0)
|
||||
if dug <= 0:
|
||||
# podmireno — uplatnicu generiramo na cijeli iznos kao podsjetnik
|
||||
dug = float(r["iznos_propisan"] or 0)
|
||||
|
||||
iban = normalize_iban(r["klub_iban"] or DEFAULT_PRIMATELJ_IBAN)
|
||||
primatelj_naziv = r["klub"] or DEFAULT_PRIMATELJ_NAZIV
|
||||
primatelj_adresa = ", ".join(filter(None, [r.get("klub_adresa"), r.get("klub_grad")])) \
|
||||
or DEFAULT_PRIMATELJ_ADRESA
|
||||
platitelj_naziv = r["clan"] or "Član"
|
||||
platitelj_adresa = ", ".join(filter(None, [r.get("clan_adresa"), r.get("clan_grad")])) or "—"
|
||||
poziv = make_poziv_na_broj(r.get("klub_oib"), int(r["godina"]), int(r["id"]))
|
||||
|
||||
pdf = build_hub3_pdf(
|
||||
platitelj_naziv=platitelj_naziv,
|
||||
platitelj_adresa=platitelj_adresa,
|
||||
primatelj_naziv=primatelj_naziv,
|
||||
primatelj_adresa=primatelj_adresa,
|
||||
iban=iban,
|
||||
amount_eur=dug,
|
||||
model="HR00",
|
||||
poziv_na_broj=poziv,
|
||||
opis=f"Članarina {r['godina']} — {r['razdoblje'] or 'godišnja'}",
|
||||
sifra_namjene="OTHR",
|
||||
)
|
||||
return Response(content=pdf, media_type="application/pdf",
|
||||
headers={"Content-Disposition":
|
||||
f"inline; filename=uplatnica-clanarina-{cid}.pdf"})
|
||||
|
||||
|
||||
@router.get("/clanarine/{cid}/qr.png")
|
||||
def uplatnica_qr(cid: int):
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.godina, c.razdoblje, c.iznos_propisan, c.iznos_placen,
|
||||
(c.iznos_propisan - COALESCE(c.iznos_placen,0))::numeric(10,2) AS dug,
|
||||
k.naziv AS klub, k.oib AS klub_oib, k.iban AS klub_iban
|
||||
FROM pgz_sport.clanarine c
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
WHERE c.id=%s
|
||||
""", (cid,))
|
||||
r = cur.fetchone()
|
||||
if not r:
|
||||
raise HTTPException(404, "Članarina ne postoji")
|
||||
dug = float(r["dug"] or 0)
|
||||
if dug <= 0:
|
||||
dug = float(r["iznos_propisan"] or 0)
|
||||
iban = normalize_iban(r["klub_iban"] or DEFAULT_PRIMATELJ_IBAN)
|
||||
primatelj = r["klub"] or DEFAULT_PRIMATELJ_NAZIV
|
||||
poziv = make_poziv_na_broj(r.get("klub_oib"), int(r["godina"]), int(r["id"]))
|
||||
payload = build_epc_payload(
|
||||
primatelj=primatelj, iban=iban, amount_eur=dug,
|
||||
opis=f"Članarina {r['godina']}", model="HR00", poziv_na_broj=poziv,
|
||||
)
|
||||
png = build_epc_qr_png(payload, box_size=10)
|
||||
return Response(content=png, media_type="image/png",
|
||||
headers={"Cache-Control": "public, max-age=300"})
|
||||
|
||||
|
||||
@router.get("/clanarine/{cid}/payment-info")
|
||||
def payment_info(cid: int):
|
||||
"""Vraća JSON s IBAN, poziv-na-broj, EPC payload i deep linkovima — za UI gumbe."""
|
||||
with _conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.godina, c.razdoblje,
|
||||
c.iznos_propisan, c.iznos_placen,
|
||||
(c.iznos_propisan - COALESCE(c.iznos_placen,0))::numeric(10,2) AS dug,
|
||||
k.naziv AS klub, k.oib AS klub_oib, k.iban AS klub_iban
|
||||
FROM pgz_sport.clanarine c
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
WHERE c.id=%s
|
||||
""", (cid,))
|
||||
r = cur.fetchone()
|
||||
if not r:
|
||||
raise HTTPException(404, "Članarina ne postoji")
|
||||
dug = float(r["dug"] or 0)
|
||||
if dug <= 0:
|
||||
dug = float(r["iznos_propisan"] or 0)
|
||||
iban = normalize_iban(r["klub_iban"] or DEFAULT_PRIMATELJ_IBAN)
|
||||
primatelj = r["klub"] or DEFAULT_PRIMATELJ_NAZIV
|
||||
poziv = make_poziv_na_broj(r.get("klub_oib"), int(r["godina"]), int(r["id"]))
|
||||
opis = f"Članarina {r['godina']} — {r['razdoblje'] or 'godišnja'}"
|
||||
payload = build_epc_payload(primatelj=primatelj, iban=iban, amount_eur=dug,
|
||||
opis=opis, model="HR00", poziv_na_broj=poziv)
|
||||
return {
|
||||
"id": cid,
|
||||
"iznos_eur": round(dug, 2),
|
||||
"primatelj": primatelj,
|
||||
"iban": iban,
|
||||
"model": "HR00",
|
||||
"poziv_na_broj": poziv,
|
||||
"opis": opis,
|
||||
"epc_payload": payload,
|
||||
"qr_url": f"/sport/api/crm/clanarine/{cid}/qr.png",
|
||||
"pdf_url": f"/sport/api/crm/clanarine/{cid}/uplatnica.pdf",
|
||||
"deep_links": build_bank_deep_links(iban, dug, opis,
|
||||
model="HR00", poziv_na_broj=poziv),
|
||||
}
|
||||
Reference in New Issue
Block a user