Ograničenje konekcija BE emulatora, sopstveni kontekst fiskalizacije nezavisan od prekida zahteva, validacija porta na setup stranici, serverska optimizacija kratke pretrage bez eval-a, testovi
This commit is contained in:
@@ -36,7 +36,7 @@ func (h *Handler) Dobavljaci(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
pretraga := r.URL.Query().Get("pretraga")
|
||||
pretraga := ignorisiKratkuPretragu(r.URL.Query().Get("pretraga"))
|
||||
|
||||
dobavljaci, err := h.DobavljaciRepo.Lista(r.Context(), pretraga)
|
||||
if err != nil {
|
||||
|
||||
@@ -63,7 +63,7 @@ func (h *Handler) FiskalniPazar(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
klijent := h.fiskalKlijent()
|
||||
klijent := h.fiskalKlijent(r.Context())
|
||||
if klijent == nil {
|
||||
podaci.Greska = "Fiskalizacija nije podešena — unesi URL PFR servera u Podešavanja → Fiskalizacija."
|
||||
h.renderujTemplate(w, "fiskal_pazar", podaci)
|
||||
@@ -94,7 +94,7 @@ func (h *Handler) ZakljuciFiskalniDan(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
klijent := h.fiskalKlijent()
|
||||
klijent := h.fiskalKlijent(r.Context())
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije podešena.")
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
@@ -127,7 +127,7 @@ func (h *Handler) FiskalniIzvestaj(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
klijent := h.fiskalKlijent()
|
||||
klijent := h.fiskalKlijent(r.Context())
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije podešena.")
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ntech/internal/config"
|
||||
"ntech/internal/db"
|
||||
@@ -276,9 +277,9 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
// fiskalKlijent vraća inicijalizovan fiskalni HTTP klijent na osnovu trenutnih
|
||||
// podešavanja. Vraća nil ako pfr_url nije podešen. URL se čita dinamički pri
|
||||
// svakom pozivu — nema keširanja, pa promena podešavanja stupa na snagu odmah.
|
||||
func (h *Handler) fiskalKlijent() *fiskal.Klijent {
|
||||
url, _ := sqlite.DohvatiPodesavanje(context.Background(), h.DB, "pfr_url")
|
||||
key, _ := sqlite.DohvatiPodesavanje(context.Background(), h.DB, "pfr_api_key")
|
||||
func (h *Handler) fiskalKlijent(ctx context.Context) *fiskal.Klijent {
|
||||
url, _ := sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_url")
|
||||
key, _ := sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_api_key")
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -288,6 +289,17 @@ func (h *Handler) fiskalKlijent() *fiskal.Klijent {
|
||||
return fiskal.NoviKlijent(url, key)
|
||||
}
|
||||
|
||||
// odvojenKontekstFiskalizacije vraća kontekst koji preživljava otkazivanje/prekid
|
||||
// originalnog HTTP zahteva (npr. korisnik zatvori tab, proxy timeout) — fiskalni
|
||||
// poziv ka PFR serveru i upis lokalnog zapisa moraju da se završe i kad browser
|
||||
// konekcija padne, jer je fiskalizacija nepovratna radnja (ponovni pokušaj bez
|
||||
// ovoga bi mogao izdati duplirani fiskalni račun). Vrednosti iz ctx (npr. korisnik
|
||||
// iz middleware) ostaju dostupne; samo se otkazivanje/deadline originalnog ctx
|
||||
// ignoriše u korist sopstvenog, ograničenog roka.
|
||||
func odvojenKontekstFiskalizacije(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.WithoutCancel(ctx), 20*time.Second)
|
||||
}
|
||||
|
||||
// imeKasira vraća ime i prezime prijavljenog korisnika za polje "kasir" na fiskalnom
|
||||
// računu (korisničko ime kao rezerva ako ime/prezime nisu uneti). Pada nazad na
|
||||
// podešavanje pfr_kasir (npr. za pozadinske pozive bez korisnika u kontekstu), pa na
|
||||
|
||||
@@ -53,7 +53,7 @@ func (h *Handler) magacinPrikaz(w http.ResponseWriter, r *http.Request, tip, str
|
||||
}
|
||||
|
||||
filter := db.ArtikalFilter{
|
||||
Pretraga: r.URL.Query().Get("pretraga"),
|
||||
Pretraga: ignorisiKratkuPretragu(r.URL.Query().Get("pretraga")),
|
||||
Tip: tip,
|
||||
SamoKriticni: r.URL.Query().Get("kriticni") == "1",
|
||||
Arhivirani: r.URL.Query().Get("arhivirani") == "1",
|
||||
|
||||
@@ -320,7 +320,7 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
|
||||
// Fiskalizacija — ako je modul uključen (best-effort: prodaja ostaje validna i bez fiskalizacije)
|
||||
racunKreiran := false
|
||||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
if klijent := h.fiskalKlijent(); klijent != nil {
|
||||
if klijent := h.fiskalKlijent(r.Context()); klijent != nil {
|
||||
primljeno, _ := strconv.ParseFloat(strings.TrimSpace(r.FormValue("primljeno")), 64)
|
||||
h.fiskalizujProdaju(r.Context(), id, klijent, primljeno)
|
||||
if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil {
|
||||
@@ -729,6 +729,8 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
|
||||
// stornirajProdaju (odmah posle storna) i ručni retry (PokusajRefundProdaje) kad prvi
|
||||
// pokušaj padne. poreskiBrojKupca se koristi samo ako klijent nema PIB/JMBG na kartici.
|
||||
func (h *Handler) posaljiFiskalniRefundProdaje(ctx context.Context, id int64, poreskiBrojKupca string) error {
|
||||
ctx, cancel := odvojenKontekstFiskalizacije(ctx)
|
||||
defer cancel()
|
||||
fr, err := h.FiskalRepo.DohvatiPoProdaji(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -736,7 +738,7 @@ func (h *Handler) posaljiFiskalniRefundProdaje(ctx context.Context, id int64, po
|
||||
if fr == nil || fr.Storniran || fr.TipTransakcije != "Sale" {
|
||||
return nil
|
||||
}
|
||||
fk := h.fiskalKlijent()
|
||||
fk := h.fiskalKlijent(ctx)
|
||||
if fk == nil {
|
||||
return errors.New("fiskalizacija nije podešena")
|
||||
}
|
||||
@@ -827,6 +829,8 @@ func (h *Handler) PokusajRefundProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
// primljeno je iznos koji je kupac predao (za tačan povraćaj na računu) — 0 ako
|
||||
// nije poznat (npr. ručni retry), tada se šalje tačan dug bez povraćaja.
|
||||
func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijent *fiskal.Klijent, primljeno float64) {
|
||||
ctx, cancel := odvojenKontekstFiskalizacije(ctx)
|
||||
defer cancel()
|
||||
nalog, err := h.ProdajaRepo.DohvatiID(ctx, prodajaID)
|
||||
if err != nil {
|
||||
slog.Error("fiskalizujProdaju: nije pronađen nalog", "prodaja_id", prodajaID, "error", err)
|
||||
@@ -905,7 +909,7 @@ func (h *Handler) RetryFiskalizacijaProdaje(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
klijent := h.fiskalKlijent()
|
||||
klijent := h.fiskalKlijent(r.Context())
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalni servis nije dostupan. Proverite vezu sa ESIR/PFR.")
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
|
||||
@@ -656,7 +656,7 @@ func (h *Handler) StornoNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// fiskalni refund — best-effort
|
||||
if originalniFiskal != nil {
|
||||
if fk := h.fiskalKlijent(); fk != nil {
|
||||
if fk := h.fiskalKlijent(ctx); fk != nil {
|
||||
if err := h.refundujServis(ctx, id, fk, originalniFiskal.ID, originalniFiskal.PfrBroj, poreskiBrojKupca); err != nil {
|
||||
slog.Error("fiskalni refund servisa nije uspeo", "servis_id", id, "error", err)
|
||||
}
|
||||
@@ -691,7 +691,7 @@ func (h *Handler) PokusajRefundServisa(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
fk := h.fiskalKlijent()
|
||||
fk := h.fiskalKlijent(ctx)
|
||||
if fk == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije podešena.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
@@ -2447,7 +2447,7 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// konačan račun da zatvori avans i pokrene eventualni povraćaj viška.
|
||||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
if fr, _ := h.FiskalRepo.DohvatiPoServisuITip(r.Context(), id, "Normal", "Sale"); fr == nil {
|
||||
klijent := h.fiskalKlijent()
|
||||
klijent := h.fiskalKlijent(r.Context())
|
||||
if klijent != nil {
|
||||
h.fiskalizujServis(r.Context(), id, klijent, nacin, iznos, primljeno)
|
||||
}
|
||||
@@ -2512,6 +2512,8 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// avansa ne fiskalizuje ceo iznos ponovo. Bez efekta ako razlika nije pozitivna
|
||||
// (smanjenje avansa se ne fiskalizuje automatski — v. razgovor o povraćaju).
|
||||
func (h *Handler) fiskalizujAvansServisa(ctx context.Context, servisID int64, noviAvans float64, nacinPlacanja string) {
|
||||
ctx, cancel := odvojenKontekstFiskalizacije(ctx)
|
||||
defer cancel()
|
||||
if noviAvans <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -2527,7 +2529,7 @@ func (h *Handler) fiskalizujAvansServisa(ctx context.Context, servisID int64, no
|
||||
if delta <= 0 {
|
||||
return
|
||||
}
|
||||
klijent := h.fiskalKlijent()
|
||||
klijent := h.fiskalKlijent(ctx)
|
||||
if klijent == nil {
|
||||
return
|
||||
}
|
||||
@@ -2580,6 +2582,8 @@ func (h *Handler) fiskalizujAvansServisa(ctx context.Context, servisID int64, no
|
||||
// na računu se iskazuje stvarno primljen iznos i PFR sam izračunava povraćaj
|
||||
// (isto kao fiskal.NapraviZahtev za Prodaju); 0 ili manje od duga → bez povraćaja.
|
||||
func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent *fiskal.Klijent, nacinPlacanja string, iznos, primljeno float64) {
|
||||
ctx, cancel := odvojenKontekstFiskalizacije(ctx)
|
||||
defer cancel()
|
||||
nalog, err := h.ServisRepo.DohvatiID(ctx, servisID)
|
||||
if err != nil {
|
||||
slog.Error("fiskalizujServis: nije pronađen nalog", "id", servisID, "error", err)
|
||||
@@ -2782,6 +2786,8 @@ func stavkeFiskalnogServisa(radovi []model.ServisniRad, delovi []model.ServisniD
|
||||
// refunda; PraznineKnjigovodstva prati takve slučajeve preko stornoBezRefunda). Vraća
|
||||
// grešku i pozivaocu (npr. PokusajRefundServisa) da bi ručni retry mogao da je prikaže.
|
||||
func (h *Handler) refundujServis(ctx context.Context, servisID int64, klijent *fiskal.Klijent, originalniFiskalID int64, referentBroj, poreskiBrojKupca string) error {
|
||||
ctx, cancel := odvojenKontekstFiskalizacije(ctx)
|
||||
defer cancel()
|
||||
nalog, err := h.ServisRepo.DohvatiID(ctx, servisID)
|
||||
if err != nil {
|
||||
slog.Error("refundujServis: nije pronađen nalog", "id", servisID, "error", err)
|
||||
@@ -2902,7 +2908,7 @@ func (h *Handler) RetryFiskalizacija(w http.ResponseWriter, r *http.Request) {
|
||||
// Naplaceno==0 je dozvoljeno kad avans u potpunosti pokriva cenu — konačan
|
||||
// račun se i tada mora izdati da zatvori avans (i eventualno vrati višak)
|
||||
|
||||
klijent := h.fiskalKlijent()
|
||||
klijent := h.fiskalKlijent(r.Context())
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalni servis nije dostupan. Proverite vezu sa ESIR/PFR.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
|
||||
@@ -38,7 +38,7 @@ func (h *Handler) Troskovi(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
pretraga := r.URL.Query().Get("pretraga")
|
||||
pretraga := ignorisiKratkuPretragu(r.URL.Query().Get("pretraga"))
|
||||
troskovi, err := h.TroskoviRepo.Lista(r.Context(), db.TrosakFilter{Pretraga: pretraga})
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju troškova", http.StatusInternalServerError)
|
||||
|
||||
@@ -39,7 +39,7 @@ func (h *Handler) Usluge(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
pretraga := r.URL.Query().Get("pretraga")
|
||||
pretraga := ignorisiKratkuPretragu(r.URL.Query().Get("pretraga"))
|
||||
usluge, err := h.UslugeRepo.Lista(r.Context(), db.UslugaFilter{Pretraga: pretraga})
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju usluga", http.StatusInternalServerError)
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseID parsira string ID iz URL parametra u int64
|
||||
@@ -13,3 +14,15 @@ func parseID(s string) (int64, error) {
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ignorisiKratkuPretragu vraća pretragu nepromenjenu ako je prazna ili ima 3+
|
||||
// znaka, a inače "" (1-2 znaka se ignorišu). Ranije je ovo bio klijentski
|
||||
// hx-trigger uslov (keyup[length==0||length>=3]) — uklonjen jer ga htmx
|
||||
// evaluira preko eval()/new Function(), što CSP bez 'unsafe-eval' blokira
|
||||
// (v. BUG.md #41). Ovde se ista optimizacija radi na serveru, bez eval-a.
|
||||
func ignorisiKratkuPretragu(pretraga string) string {
|
||||
if l := len([]rune(strings.TrimSpace(pretraga))); l > 0 && l < 3 {
|
||||
return ""
|
||||
}
|
||||
return pretraga
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user