Files
GoNtech/internal/be/kartica.go
T
Dasko 5f8fbf5b46 Kartica emulator (BE) + fiskalna verifikacija + ime/prezime korisnika
- internal/be: emulator kartice (TCP listener za L-PFR mock)
- /v/: javna stranica za verifikaciju fiskalnog računa (QR kod)
- /servis/{id}/fiskalni-racun: štampa fiskalnog računa
- Podešavanja fiskalizacije: BE status/reset, proširen UI
- Korisnik: polja ime i prezime (model, DB, handler, admin profil)
- Fisk server: ažuriran receipt.py i server.py
- Koncept.md, Magacin.md: dokumentacija
2026-06-26 23:17:52 +02:00

300 lines
7.2 KiB
Go

// Package be implementira emulator bezbednosnog elementa (pametne kartice).
// Sluša na TCP portu i obrađuje 4 JSON komande: status, certificate, verify_pin, sign.
// Podatke o firmi čita iz NTech SQLite baze pri pokretanju.
package be
import (
"database/sql"
"encoding/base64"
"fmt"
"log/slog"
"math/rand/v2"
"os"
"strconv"
"strings"
"sync"
"time"
)
// tipExt mapira (invoiceType, transactionType) → (ćirilični sufiks, ključ brojača)
var tipExt = map[[2]string][2]string{
{"Normal", "Sale"}: {"ПП", "pp"},
{"Normal", "Refund"}: {"ПР", "pr"},
{"Advance", "Sale"}: {"АП", "ap"},
{"Advance", "Refund"}: {"АР", "ar"},
{"Copy", "Sale"}: {"КП", "kp"},
{"Copy", "Refund"}: {"КР", "kr"},
{"Training", "Sale"}: {"ОП", "op"},
{"Training", "Refund"}: {"ОР", "or"},
{"Proforma", "Sale"}: {"РП", "rp"},
{"Proforma", "Refund"}: {"РР", "rr"},
}
// Kartica drži stanje emuliranog bezbednosnog elementa.
type Kartica struct {
mu sync.Mutex
// identitet (zamrznuto pri "personalizaciji" — čitamo iz DB pri pokretanju)
JID string
PIN string
TIN string // RS+PIB
TINPlain string // samo PIB
Name string
Address string
City string
District string
BusinessUnitID string
LocationName string
ValidFrom string
ValidTo string
Issuer string
// stanje u toku rada
pinUnesen bool
totalCounter int
counters map[string]int
Limit float64
unreadAmount float64
}
func novaKartica(db *sql.DB) *Kartica {
k := &Kartica{
JID: env("BE_JID", "TRNMOCK1"),
PIN: env("BE_PIN", "1234"),
Limit: envFloat("BE_LIMIT", 500000),
Issuer: "Poreska uprava RS",
counters: map[string]int{
"pp": 0, "pr": 0, "ap": 0, "ar": 0,
"kp": 0, "kr": 0, "op": 0, "or": 0,
"rp": 0, "rr": 0,
},
}
now := time.Now()
k.ValidFrom = now.Format("2006-01-02T00:00:00+01:00")
k.ValidTo = now.AddDate(3, 0, 0).Format("2006-01-02T00:00:00+01:00")
// čitaj podatke firme, PIN i limit iz baze (env var ima prednost)
k.ucitajFirmu(db)
slog.Info("kartica emulator inicijalizovan",
"jid", k.JID,
"tin", k.TINPlain,
"firma", k.Name,
"limit", k.Limit,
)
return k
}
func (k *Kartica) ucitajFirmu(db *sql.DB) {
rows, err := db.Query(
"SELECT kljuc, vrednost FROM podesavanja WHERE kljuc IN " +
"('naziv_firme','pib','maticni_broj','adresa','telefon'," +
"'poslovna_jedinica_naziv','poslovna_jedinica_oznaka','opstina','grad'," +
"'be_pin','be_limit')",
)
if err != nil {
slog.Warn("be: ne mogu da čitam podesavanja", "error", err)
k.postaviTestPodatke()
return
}
defer rows.Close()
p := map[string]string{}
for rows.Next() {
var k2, v string
if err := rows.Scan(&k2, &v); err == nil {
p[k2] = v
}
}
naziv := p["naziv_firme"]
if naziv == "" {
naziv = "Test Company DOO"
}
pib := p["pib"]
if pib == "" {
pib = "123456789"
}
k.Name = naziv
k.TINPlain = pib
k.TIN = "RS" + pib
k.Address = p["adresa"]
if k.Address == "" {
k.Address = "Test Adresa 1"
}
k.City = p["grad"]
if k.City == "" {
k.City = "Beograd"
}
k.District = p["opstina"]
if k.District == "" {
k.District = "Savski Venac"
}
k.LocationName = p["poslovna_jedinica_naziv"]
if k.LocationName == "" {
k.LocationName = naziv
}
k.BusinessUnitID = p["poslovna_jedinica_oznaka"]
if k.BusinessUnitID == "" {
k.BusinessUnitID = "BU-001"
}
// PIN i limit iz baze — samo ako env var nije postavljen
if os.Getenv("BE_PIN") == "" {
if pin := p["be_pin"]; pin != "" {
k.PIN = pin
}
}
if os.Getenv("BE_LIMIT") == "" {
if lim := p["be_limit"]; lim != "" {
if f, err := strconv.ParseFloat(lim, 64); err == nil && f > 0 {
k.Limit = f
}
}
}
}
func (k *Kartica) postaviTestPodatke() {
k.Name = "Test Company DOO"
k.TINPlain = "123456789"
k.TIN = "RS123456789"
k.Address = "Test Adresa 1"
k.City = "Beograd"
k.District = "Savski Venac"
k.LocationName = "Test Company DOO"
k.BusinessUnitID = "BU-001"
}
// ── Komande ────────────────────────────────────────────────
func (k *Kartica) cmdStatus() map[string]any {
k.mu.Lock()
defer k.mu.Unlock()
counters := map[string]int{}
for key, val := range k.counters {
counters[key] = val
}
return map[string]any{
"status": "ok",
"total_counter": k.totalCounter,
"counters": counters,
"limit": k.Limit,
"unread_amount": k.unreadAmount,
"pin_required": !k.pinUnesen,
}
}
func (k *Kartica) cmdCertificate() map[string]any {
k.mu.Lock()
defer k.mu.Unlock()
return map[string]any{
"status": "ok",
"jid": k.JID,
"tin": k.TIN,
"tin_plain": k.TINPlain,
"name": k.Name,
"address": k.Address,
"city": k.City,
"district": k.District,
"business_unit_id": k.BusinessUnitID,
"location_name": k.LocationName,
"valid_from": k.ValidFrom,
"valid_to": k.ValidTo,
"issuer": k.Issuer,
}
}
func (k *Kartica) cmdVerifyPin(pin string) map[string]any {
k.mu.Lock()
defer k.mu.Unlock()
if pin != k.PIN {
return map[string]any{"status": "error", "code": "2100", "message": "Pogrešan PIN"}
}
k.pinUnesen = true
return map[string]any{"status": "ok"}
}
func (k *Kartica) cmdResetAudit() map[string]any {
k.mu.Lock()
defer k.mu.Unlock()
k.unreadAmount = 0
return map[string]any{"status": "ok", "unread_amount": 0}
}
func (k *Kartica) cmdSign(invoiceType, transactionType string, totalAmount float64) map[string]any {
k.mu.Lock()
defer k.mu.Unlock()
// blokada limite
if k.unreadAmount >= k.Limit {
return map[string]any{
"status": "blocked",
"message": fmt.Sprintf("Limit iščitavanja dostignut (%.2f / %.2f)", k.unreadAmount, k.Limit),
}
}
// određi tip i ext
ext, tipKey := "", "pp"
if v, ok := tipExt[[2]string{invoiceType, transactionType}]; ok {
ext = v[0]
tipKey = v[1]
} else {
ext = "ПП"
tipKey = "pp"
}
k.totalCounter++
k.counters[tipKey]++
typCounter := k.counters[tipKey]
// za fiskalne račune akumuliraj neisčitani iznos
nonFiscal := strings.HasPrefix(tipKey, "k") || strings.HasPrefix(tipKey, "o") || strings.HasPrefix(tipKey, "r")
if !nonFiscal && transactionType == "Sale" {
k.unreadAmount += totalAmount
} else if !nonFiscal && transactionType == "Refund" {
k.unreadAmount -= totalAmount
if k.unreadAmount < 0 {
k.unreadAmount = 0
}
}
// lažni potpis — random base64 dovoljno dug da izgleda realistično
sig := mockPotpis()
return map[string]any{
"status": "ok",
"counter": k.totalCounter,
"counter_extension": ext,
"type_counter": typCounter,
"signature": sig,
"blocked": false,
}
}
// mockPotpis generiše 64-bajtni nasumični base64 string koji imitira RSA potpis.
func mockPotpis() string {
b := make([]byte, 64)
for i := range b {
b[i] = byte(rand.IntN(256))
}
return base64.StdEncoding.EncodeToString(b)
}
func env(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envFloat(key string, def float64) float64 {
if v := os.Getenv(key); v != "" {
if f, err := strconv.ParseFloat(v, 64); err == nil {
return f
}
}
return def
}