Files
GoNtech/internal/db/sqlite/prodaja_storno_test.go
T
Dasko 99ae077082 Dashboard/izveštaji: ispravka obračuna prihoda + KIR servisa, testovi
- Prihod meseca: izbačeni stornirani nalozi, servis preko naplaceno+avans
- Servis KIR: neto cena + PDV po stvarnoj stopi (PdvKir.DodajNeto), bez hardkodovanih 20%
- TopKlijenti: samo preuzeti nalozi sa naplatom/avansom
- Izveštaji: mesečni ključevi sidreni na 1. u mesecu (bez preliva dana)
- BUG-04: konačna cena servisa bez dvostrukog računanja delova
- Dashboard: uklonjena kartica „Prihod ovog meseca"
- Default port 3000
- Dodati testovi: prihod/storno/delovi/nivelacija/KIR/mesečni ključevi
2026-06-27 09:41:07 +02:00

275 lines
8.8 KiB
Go

package sqlite
import (
"context"
"testing"
"time"
"ntech/internal/model"
)
// ─── helpers ─────────────────────────────────────────────────────────────────
// seedArtikalSaStanjem kreira artikal sa zadatom količinom i vraća njegov ID.
func seedArtikalSaStanjem(t *testing.T, ctx context.Context, repo *ArtikalRepo, naziv string, kolicina int) int64 {
t.Helper()
id, err := repo.Kreiraj(ctx, &model.Artikal{
Naziv: naziv,
Kolicina: kolicina,
Tip: model.TipProizvod,
})
if err != nil {
t.Fatalf("seedArtikalSaStanjem %q: %v", naziv, err)
}
return id
}
// seedUsluga kreira artikal tipa "usluga" (bez praćenja lagera).
func seedUsluga(t *testing.T, ctx context.Context, repo *ArtikalRepo, naziv string) int64 {
t.Helper()
id, err := repo.Kreiraj(ctx, &model.Artikal{
Naziv: naziv,
Tip: model.TipUsluga,
})
if err != nil {
t.Fatalf("seedUsluga %q: %v", naziv, err)
}
return id
}
// prodajNalog kreira prodajni nalog sa zadatim stavkama i vraća ID naloga.
func prodajNalog(t *testing.T, ctx context.Context, repo *ProdajaRepo, broj string, stavke []model.StavkaProdaje) int64 {
t.Helper()
ukupno := 0.0
for _, s := range stavke {
ukupno += float64(s.Kolicina) * s.CenaPoKomadu
}
id, err := repo.Kreiraj(ctx, &model.ProdajniNalog{
BrojNaloga: broj,
Ukupno: ukupno,
NacinPlacanja: "gotovina",
Datum: time.Now(),
}, stavke, nil)
if err != nil {
t.Fatalf("prodajNalog %q: %v", broj, err)
}
return id
}
// dohvatiKolicinu čita trenutnu količinu artikla iz baze.
func dohvatiKolicinu(t *testing.T, ctx context.Context, repo *ArtikalRepo, id int64) int {
t.Helper()
a, err := repo.DohvatiID(ctx, id)
if err != nil || a == nil {
t.Fatalf("dohvatiKolicinu id=%d: %v", id, err)
}
return a.Kolicina
}
// ─── testovi ─────────────────────────────────────────────────────────────────
// Osnovni slučaj: storno vraća tačnu količinu u magacin.
func TestStornoVracaKolicinuUMagacin(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
artRepo := NoviArtikalRepo(baza)
prodRepo := NoviProdajaRepo(baza)
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Punjač", 10)
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-001", []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 3, CenaPoKomadu: 100},
})
// poslije prodaje: 10 - 3 = 7
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 7 {
t.Fatalf("posle prodaje: kolicina = %d, očekivano 7", got)
}
if err := prodRepo.Storno(ctx, nalogID, "test storno", nil); err != nil {
t.Fatalf("Storno: %v", err)
}
// posle storna: 7 + 3 = 10 (prvobitno stanje)
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 10 {
t.Errorf("posle storna: kolicina = %d, očekivano 10", got)
}
}
// Nalog sa više stavki — sve se vraćaju.
func TestStornoViseSlavki(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
artRepo := NoviArtikalRepo(baza)
prodRepo := NoviProdajaRepo(baza)
aID := seedArtikalSaStanjem(t, ctx, artRepo, "Miš", 20)
bID := seedArtikalSaStanjem(t, ctx, artRepo, "Tastatura", 15)
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-002", []model.StavkaProdaje{
{ArtikalID: aID, Kolicina: 5, CenaPoKomadu: 50},
{ArtikalID: bID, Kolicina: 3, CenaPoKomadu: 80},
})
if err := prodRepo.Storno(ctx, nalogID, "povrat", nil); err != nil {
t.Fatalf("Storno: %v", err)
}
if got := dohvatiKolicinu(t, ctx, artRepo, aID); got != 20 {
t.Errorf("Miš: kolicina = %d, očekivano 20", got)
}
if got := dohvatiKolicinu(t, ctx, artRepo, bID); got != 15 {
t.Errorf("Tastatura: kolicina = %d, očekivano 15", got)
}
}
// Usluga (tip != "proizvod") — storno je dozvoljen ali se količina ne vraća.
func TestStornoUslugaNeVracaKolicinu(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
artRepo := NoviArtikalRepo(baza)
prodRepo := NoviProdajaRepo(baza)
// proizvod koji ima lager
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Kabl", 10)
// usluga koja nema lager
uslugaID := seedUsluga(t, ctx, artRepo, "Instalacija")
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-003", []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 2, CenaPoKomadu: 200},
{ArtikalID: uslugaID, Kolicina: 1, CenaPoKomadu: 500},
})
if err := prodRepo.Storno(ctx, nalogID, "test", nil); err != nil {
t.Fatalf("Storno: %v", err)
}
// proizvod se vraća: 10 - 2 + 2 = 10
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 10 {
t.Errorf("Kabl: kolicina = %d, očekivano 10", got)
}
// usluga nema količinu — proveri da nije pogrešno promenjena
usluga, _ := artRepo.DohvatiID(ctx, uslugaID)
if usluga.Kolicina != 0 {
t.Errorf("Instalacija (usluga): kolicina = %d, očekivano 0 (usluge nemaju lager)", usluga.Kolicina)
}
}
// Dupli storno — drugi pokušaj mora da vrati grešku.
func TestStornoVecStorniran(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
artRepo := NoviArtikalRepo(baza)
prodRepo := NoviProdajaRepo(baza)
artID := seedArtikalSaStanjem(t, ctx, artRepo, "USB hub", 5)
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-004", []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 300},
})
if err := prodRepo.Storno(ctx, nalogID, "prvi", nil); err != nil {
t.Fatalf("prvi Storno: %v", err)
}
// drugi storno mora da vrati grešku
if err := prodRepo.Storno(ctx, nalogID, "drugi", nil); err == nil {
t.Error("drugi Storno nije vratio grešku — trebalo je da odbije dupli storno")
}
// količina ne sme biti duplo vraćena: 5 - 1 + 1 = 5, ne 6
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 5 {
t.Errorf("posle duplog storna: kolicina = %d, očekivano 5 (nema duplog vraćanja)", got)
}
}
// Storno pa Obrisi — Obrisi ne sme ponovo da vrati robu (storno je već vratio).
func TestObrisiPosleStornaNeVracaDuplo(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
artRepo := NoviArtikalRepo(baza)
prodRepo := NoviProdajaRepo(baza)
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Adapter", 8)
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-005", []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 2, CenaPoKomadu: 150},
})
// storno vraća 2 kom: 8 - 2 + 2 = 8
if err := prodRepo.Storno(ctx, nalogID, "storno pre brisanja", nil); err != nil {
t.Fatalf("Storno: %v", err)
}
// brisanje storniranog naloga — ne sme ponovo da vrati robu
if err := prodRepo.Obrisi(ctx, nalogID, nil); err != nil {
t.Fatalf("Obrisi: %v", err)
}
// mora ostati 8, ne 10 (8 + 2 duplo)
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 8 {
t.Errorf("posle storno+obrisi: kolicina = %d, očekivano 8 (ne 10)", got)
}
}
// Magacinska promena se beleži pri stornu (tip = PromenaPovracaj).
func TestStornoBeležiMagacinskuPromenu(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
artRepo := NoviArtikalRepo(baza)
prodRepo := NoviProdajaRepo(baza)
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Monitor", 3)
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-006", []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 500},
})
if err := prodRepo.Storno(ctx, nalogID, "povrat kupca", nil); err != nil {
t.Fatalf("Storno: %v", err)
}
// proveravamo magacinsku promenu direktno u bazi
var tipPromene string
var promenaKolicine int
err := baza.QueryRowContext(ctx,
`SELECT tip_promene, promena_kolicine FROM magacinske_promene
WHERE artikal_id = ? AND referentni_id = ? AND tip_promene = ?`,
artID, nalogID, model.PromenaPovracaj,
).Scan(&tipPromene, &promenaKolicine)
if err != nil {
t.Fatalf("magacinska promena nije pronađena: %v", err)
}
if tipPromene != model.PromenaPovracaj {
t.Errorf("tip_promene = %q, očekivano %q", tipPromene, model.PromenaPovracaj)
}
if promenaKolicine != 1 {
t.Errorf("promena_kolicine = %d, očekivano 1", promenaKolicine)
}
}
// Delimični storno: dva naloga za isti artikal — storno prvog ne dira drugi.
func TestStornoDvaOdvojenaNaloga(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
artRepo := NoviArtikalRepo(baza)
prodRepo := NoviProdajaRepo(baza)
artID := seedArtikalSaStanjem(t, ctx, artRepo, "SSD", 10)
nalog1 := prodajNalog(t, ctx, prodRepo, "PR-S-007", []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 3, CenaPoKomadu: 200},
})
prodajNalog(t, ctx, prodRepo, "PR-S-008", []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 2, CenaPoKomadu: 200},
})
// posle oba: 10 - 3 - 2 = 5
// storniramo samo prvi
if err := prodRepo.Storno(ctx, nalog1, "greška", nil); err != nil {
t.Fatalf("Storno nalog1: %v", err)
}
// 5 + 3 (vraćen nalog1) = 8; nalog2 (2 kom) ostaje prodat
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 8 {
t.Errorf("posle storna prvog: kolicina = %d, očekivano 8", got)
}
}