package sqlite import ( "context" "database/sql" "fmt" "testing" "ntech/internal/model" ) func TestIzvestajArtikliBrojaci(t *testing.T) { ctx := context.Background() db := testDB(t) art := NoviArtikalRepo(db) izv := NoviIzvestajRepo(db) dodaj := func(a *model.Artikal) { if _, err := art.Kreiraj(ctx, a); err != nil { t.Fatalf("Kreiraj: %v", err) } } dodaj(&model.Artikal{Naziv: "A", Kolicina: 10, KolicinMin: 5}) dodaj(&model.Artikal{Naziv: "B", Kolicina: 2, KolicinMin: 5}) dodaj(&model.Artikal{Naziv: "C", Kolicina: 0, KolicinMin: 5}) if n, err := izv.BrojArtikala(ctx); err != nil || n != 3 { t.Fatalf("BrojArtikala = %d, err=%v; očekivano 3", n, err) } if n, err := izv.BrojKriticnihZaliha(ctx); err != nil || n != 2 { t.Fatalf("BrojKriticnihZaliha = %d, err=%v; očekivano 2", n, err) } zalihe, err := izv.KriticneZalihe(ctx, 5) if err != nil { t.Fatalf("KriticneZalihe: %v", err) } if len(zalihe) != 2 { t.Fatalf("KriticneZalihe vratio %d, očekivano 2", len(zalihe)) } if zalihe[0].Kolicina != 0 { t.Fatalf("prvi kritičan treba da ima količinu 0, ima %d", zalihe[0].Kolicina) } } func TestIzvestajPraznaBaza(t *testing.T) { ctx := context.Background() db := testDB(t) izv := NoviIzvestajRepo(db) if n, err := izv.BrojArtikala(ctx); err != nil || n != 0 { t.Errorf("BrojArtikala = %d, err=%v", n, err) } if v, err := izv.PrihodTekuciMesec(ctx); err != nil || v != 0 { t.Errorf("PrihodTekuciMesec = %v, err=%v", v, err) } if l, err := izv.PoslednjiServisi(ctx, 5); err != nil || len(l) != 0 { t.Errorf("PoslednjiServisi len=%d, err=%v", len(l), err) } if l, err := izv.TopKlijenti(ctx, 10); err != nil || len(l) != 0 { t.Errorf("TopKlijenti len=%d, err=%v", len(l), err) } if l, err := izv.MesecniPrihodProdaja(ctx); err != nil || len(l) != 0 { t.Errorf("MesecniPrihodProdaja len=%d, err=%v", len(l), err) } } // ─── BUG-01: stornirani prodajni nalozi ne smeju ući u prihod ─────────────── func TestPrihodTekuciMesec_IgnorisuSeStornirani(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-T-001', 1000.0, 0, date('now','localtime'))`) mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-T-002', 500.0, 1, date('now','localtime'))`) prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } if prihod != 1000.0 { t.Errorf("prihod = %.2f; očekivano 1000.00 — stornirani nalog (500) ne sme da uđe", prihod) } } func TestMesecniPrihodProdaja_IgnorisuSeStornirani(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-M-001', 2000.0, 0, date('now','localtime'))`) mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-M-002', 300.0, 1, date('now','localtime'))`) meseci, err := izv.MesecniPrihodProdaja(ctx) if err != nil { t.Fatalf("MesecniPrihodProdaja: %v", err) } if len(meseci) != 1 { t.Fatalf("očekivan 1 mesec, dobijeno %d", len(meseci)) } if meseci[0].Iznos != 2000.0 { t.Errorf("iznos = %.2f; očekivano 2000.00 — stornirani (300) ne sme da uđe", meseci[0].Iznos) } } func TestPoslednjeProdaje_IgnorisuSeStornirani(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-P-001', 100.0, 0, date('now','localtime'))`) mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-P-002', 200.0, 1, date('now','localtime'))`) lista, err := izv.PoslednjeProdaje(ctx, 10) if err != nil { t.Fatalf("PoslednjeProdaje: %v", err) } if len(lista) != 1 { t.Fatalf("lista ima %d redova, očekivan 1 — stornirani ne sme da uđe", len(lista)) } if lista[0].BrojNaloga != "PR-P-001" { t.Errorf("pogrešan nalog: %s", lista[0].BrojNaloga) } } func TestTopKlijenti_IgnorisuSeStornirani(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) mustExec(t, baza, `INSERT INTO klijenti (ime, prezime) VALUES ('Pera', 'Perić')`) var klijentID int64 if err := baza.QueryRowContext(ctx, `SELECT id FROM klijenti LIMIT 1`).Scan(&klijentID); err != nil { t.Fatalf("dohvati klijent id: %v", err) } mustExec(t, baza, fmt.Sprintf( `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum, klijent_id) VALUES ('PR-K-001', 5000.0, 0, date('now','localtime'), %d)`, klijentID)) mustExec(t, baza, fmt.Sprintf( `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum, klijent_id) VALUES ('PR-K-002', 1000.0, 1, date('now','localtime'), %d)`, klijentID)) top, err := izv.TopKlijenti(ctx, 5) if err != nil { t.Fatalf("TopKlijenti: %v", err) } if len(top) != 1 { t.Fatalf("top lista ima %d redova, očekivan 1", len(top)) } if top[0].UkupnoVrednost != 5000.0 { t.Errorf("vrednost = %.2f; očekivano 5000.00 — stornirani (1000) ne sme da uđe", top[0].UkupnoVrednost) } } // ─── BUG-02/03: servis prihod koristi naplaceno (bruto, uključuje delove) ─── func TestPrihodTekuciMesec_ServisKoristiNaplaceno(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) // cena_konacna=800 (neto, bez delova), naplaceno=1500 (bruto sa delovima) mustExec(t, baza, `INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, naplaceno, datum_prijema, datum_zavrsetka, javni_token) VALUES ('SR-T-001', 'Telefon', 'Kvar', 'Preuzeto', 800.0, 1500.0, date('now'), date('now','localtime'), 'tok1')`) prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } if prihod != 1500.0 { t.Errorf("prihod = %.2f; očekivano 1500.00 (naplaceno, ne cena_konacna 800)", prihod) } } func TestPrihodTekuciMesec_ServisNaplaceno0NeUlazi(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) // garancijska popravka — cena_konacna postoji ali naplaceno=0 mustExec(t, baza, `INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, naplaceno, datum_prijema, datum_zavrsetka, javni_token) VALUES ('SR-T-002', 'Laptop', 'Kvar', 'Preuzeto', 1200.0, 0, date('now'), date('now','localtime'), 'tok2')`) prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } if prihod != 0.0 { t.Errorf("prihod = %.2f; očekivano 0 (naplaceno=0 ne sme da uđe)", prihod) } } func TestMesecniPrihodServis_KoristiNaplaceno(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) mustExec(t, baza, `INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, naplaceno, datum_prijema, datum_zavrsetka, javni_token) VALUES ('SR-M-001', 'Tablet', 'Kvar', 'Preuzeto', 500.0, 900.0, date('now'), date('now','localtime'), 'tok3')`) meseci, err := izv.MesecniPrihodServis(ctx) if err != nil { t.Fatalf("MesecniPrihodServis: %v", err) } if len(meseci) != 1 { t.Fatalf("očekivan 1 mesec, dobijeno %d", len(meseci)) } if meseci[0].Iznos != 900.0 { t.Errorf("iznos = %.2f; očekivano 900.00 (naplaceno, ne cena_konacna 500)", meseci[0].Iznos) } } // ─── BUG-10: avans se uračunava u prihod (naplaceno + avans) ───────────────── func TestPrihodTekuciMesec_ServisSaAvansom(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) // avans 400 (ranije), naplaceno 600 (ostatak pri preuzimanju) → ukupno 1000 mustExec(t, baza, `INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, avans, naplaceno, datum_prijema, datum_zavrsetka, javni_token) VALUES ('SR-AV-001', 'PC', 'Kvar', 'Preuzeto', 1000.0, 400.0, 600.0, date('now'), date('now','localtime'), 'tokav1')`) prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } if prihod != 1000.0 { t.Errorf("prihod = %.2f; očekivano 1000.00 (naplaceno 600 + avans 400)", prihod) } } func TestPrihodTekuciMesec_ServisPotpunoAvansiran(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) // avans pokrio ceo iznos → naplaceno=0, ali prihod NIJE 0 (avans 1000) mustExec(t, baza, `INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, avans, naplaceno, datum_prijema, datum_zavrsetka, javni_token) VALUES ('SR-AV-002', 'Laptop', 'Kvar', 'Preuzeto', 1000.0, 1000.0, 0.0, date('now'), date('now','localtime'), 'tokav2')`) prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } if prihod != 1000.0 { t.Errorf("prihod = %.2f; očekivano 1000.00 (potpuno avansiran ne sme da nestane)", prihod) } } // garancija/besplatna popravka: naplaceno=0 i avans=NULL → ne ulazi u prihod func TestPrihodTekuciMesec_GarancijaNeUlazi(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) mustExec(t, baza, `INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, naplaceno, datum_prijema, datum_zavrsetka, javni_token) VALUES ('SR-GAR-001', 'Telefon', 'Garancija', 'Preuzeto', 0.0, 0.0, date('now'), date('now','localtime'), 'tokgar')`) prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } if prihod != 0.0 { t.Errorf("prihod = %.2f; očekivano 0 (garancija bez naplate i avansa)", prihod) } } // ─── BUG-08: TopKlijenti broji samo preuzete naloge sa naplatom/avansom ────── func TestTopKlijenti_SamoPreuzetiNalozi(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) mustExec(t, baza, `INSERT INTO klijenti (ime, prezime) VALUES ('Mika', 'Mikić')`) var klijentID int64 if err := baza.QueryRowContext(ctx, `SELECT id FROM klijenti LIMIT 1`).Scan(&klijentID); err != nil { t.Fatalf("dohvati klijent id: %v", err) } // preuzet nalog sa naplatom 800 — broji se mustExec(t, baza, fmt.Sprintf(`INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, naplaceno, datum_prijema, datum_zavrsetka, javni_token, klijent_id) VALUES ('SR-TK-001', 'PC', 'Kvar', 'Preuzeto', 800.0, 800.0, date('now'), date('now','localtime'), 'toktk1', %d)`, klijentID)) // nalog u popravci sa cena_konacna 5000 — NE sme da se broji (nije preuzet) mustExec(t, baza, fmt.Sprintf(`INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, naplaceno, datum_prijema, javni_token, klijent_id) VALUES ('SR-TK-002', 'Laptop', 'Kvar', 'U popravci', 5000.0, 0.0, date('now'), 'toktk2', %d)`, klijentID)) top, err := izv.TopKlijenti(ctx, 5) if err != nil { t.Fatalf("TopKlijenti: %v", err) } if len(top) != 1 { t.Fatalf("top lista ima %d redova, očekivan 1", len(top)) } if top[0].UkupnoVrednost != 800.0 { t.Errorf("vrednost = %.2f; očekivano 800.00 (samo preuzet nalog, ne 5000 u popravci)", top[0].UkupnoVrednost) } } // ─── Kombinovani test: prodaja (bez storniranih) + servis (naplaceno) ──────── func TestPrihodTekuciMesec_Kombinovani(t *testing.T) { ctx := context.Background() baza := testDB(t) izv := NoviIzvestajRepo(baza) // prodaja: 1000 normalna + 400 stornirana mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-C-001', 1000.0, 0, date('now','localtime'))`) mustExec(t, baza, `INSERT INTO prodajni_nalozi (broj_naloga, ukupno, stornirano, datum) VALUES ('PR-C-002', 400.0, 1, date('now','localtime'))`) // servis: naplaceno=600 (cena_konacna=200 neto) mustExec(t, baza, `INSERT INTO servisni_nalozi (broj_naloga, uredjaj, opis_kvara, status, cena_konacna, naplaceno, datum_prijema, datum_zavrsetka, javni_token) VALUES ('SR-C-001', 'PC', 'Kvar', 'Preuzeto', 200.0, 600.0, date('now'), date('now','localtime'), 'tok4')`) // 1000 (prodaja) + 600 (servis naplaceno) = 1600; 400 storniranih ne ulazi prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } const ocekivano = 1600.0 if prihod != ocekivano { t.Errorf("prihod = %.2f; očekivano %.2f", prihod, ocekivano) } } // ─── BUG-04: auto-izračun cena_konacna ne sme da uključuje delove ──────────── // // Testiramo repo sloj direktno: simuliramo šta handler radi pri prelasku u // Preuzeto kad CenaKonacna == nil — upisujemo samo dijagnostiku+radove, pa // proveravamo da li naplaceno (= cena_konacna + delovi) nije dupliran. func TestServisAutoIzracunCeneKonacne_BezDuplogDela(t *testing.T) { ctx := context.Background() baza := testDB(t) servisRepo := NoviServisRepo(baza) radoviRepo := NoviServisniRadoviRepo(baza) izv := NoviIzvestajRepo(baza) // kreiraj nalog bez cene_konacne nalogID, err := servisRepo.Kreiraj(ctx, &model.ServisniNalog{ BrojNaloga: "SR-BUG04", Uredjaj: "Monitor", OpisKvara: "Ne pali", Status: "U popravci", CenaDijagnostike: 200.0, }) if err != nil { t.Fatalf("Kreiraj nalog: %v", err) } // dodaj rad direktno u bazu (200 din) mustExec(t, baza, fmt.Sprintf( `INSERT INTO servis_radovi (nalog_id, naziv, kolicina, cena_komada, predlozeno) VALUES (%d, 'Zamena baterijice', 1, 200.0, 0)`, nalogID)) // dodaj deo direktno u bazu (300 din) — potreban je artikal u bazi mustExec(t, baza, `INSERT INTO artikli (naziv, kolicina, prodajna_cena) VALUES ('Baterija', 5, 300.0)`) var artID int64 _ = baza.QueryRowContext(ctx, `SELECT id FROM artikli WHERE naziv='Baterija'`).Scan(&artID) mustExec(t, baza, fmt.Sprintf( `INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (%d, %d, 1, 300.0, 0)`, nalogID, artID)) // simuliramo ispravnu handler logiku (BUG-04 fix): // cena_konacna = dijagnostika + radovi (BEZ delova) cenaKonacna := 200.0 + 200.0 // dijagnostika + rad if err := servisRepo.AzurirajCenuKonacnu(ctx, nalogID, cenaKonacna); err != nil { t.Fatalf("AzurirajCenuKonacnu: %v", err) } // iznos za naplatu = cena_konacna + delovi (bez dupliranja) iznosNaplacen := cenaKonacna + 300.0 // = 700 if err := servisRepo.SacuvajNaplatu(ctx, nalogID, "Gotovina", iznosNaplacen); err != nil { t.Fatalf("SacuvajNaplatu: %v", err) } // postavi datum_zavrsetka i status Preuzeto if err := servisRepo.AzurirajStatus(ctx, nalogID, "Preuzeto"); err != nil { t.Fatalf("AzurirajStatus: %v", err) } // prihod treba da bude tačno 700 (nije 1000 = 400+300+300 duplikat) prihod, err := izv.PrihodTekuciMesec(ctx) if err != nil { t.Fatalf("PrihodTekuciMesec: %v", err) } const ocekivano = 700.0 if prihod != ocekivano { t.Errorf("prihod = %.2f; očekivano %.2f (400 rad+dijag + 300 deo, bez duplikata)", prihod, ocekivano) } // proveri i da cena_konacna nije kontaminirana delovima nalog, err := servisRepo.DohvatiID(ctx, nalogID) if err != nil || nalog == nil { t.Fatalf("DohvatiID: %v", err) } if nalog.CenaKonacna == nil { t.Fatal("cena_konacna je nil — nije sačuvana") } if *nalog.CenaKonacna != 400.0 { t.Errorf("cena_konacna = %.2f; očekivano 400.00 (dijagnostika+rad, bez delova)", *nalog.CenaKonacna) } _ = radoviRepo // korišćen indirektno kroz bazu } // ─── helper ────────────────────────────────────────────────────────────────── func mustExec(t *testing.T, db *sql.DB, query string) { t.Helper() if _, err := db.ExecContext(context.Background(), query); err != nil { t.Fatalf("mustExec: %v\nSQL: %s", err, query) } }