- 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
275 lines
8.8 KiB
Go
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)
|
|
}
|
|
}
|