From 640a5f79e83bc90259a966d59ebbe1a6325ff9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20De=C5=BEulovi=C4=87?= Date: Fri, 20 Sep 2024 16:04:32 +0200 Subject: [PATCH] Improve schema and xml generation helper functions. Add tests and improve tests. Refactor CIS communication code for better readability and maintainability. Improve code comments and formatting. --- ciscomm.go | 6 +- fiskal-schema-helpers.go | 43 +++++---- fiskal-schema.go | 10 +-- fiskalhr_test.go | 186 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 26 deletions(-) diff --git a/ciscomm.go b/ciscomm.go index 2e5b6d3..65479f6 100644 --- a/ciscomm.go +++ b/ciscomm.go @@ -27,16 +27,12 @@ type SOAPBody struct { // SOAPEnvelopeNoNamespace represents a SOAP envelope without namespace (for CIS responses) // This to be more flexible and permissive on unmarhaling responses. -// This is a workaround because CIS responses sometimes don't have a namespace in the envelope -// But if we use requests without namespace, it will not work, their server return 500 error in that case -// So we have 2 separate structs for marshaling/un-marshaling requests and responses type SOAPEnvelopeNoNamespace struct { XMLName xml.Name `xml:"Envelope"` Body SOAPBodyNoNamespace `xml:"Body"` } -// SOAPBodyNoNamespace represents the body of a SOAP envelope without namespace -// This is a workaround because CIS responses sometimes don't have a namespace in the envelope +// SOAPBodyNoNamespace represents the body of a SOAP envelope without namespace (for CIS responses) type SOAPBodyNoNamespace struct { XMLName xml.Name `xml:"Body"` Content []byte `xml:",innerxml"` diff --git a/fiskal-schema-helpers.go b/fiskal-schema-helpers.go index 15296da..99986b0 100644 --- a/fiskal-schema-helpers.go +++ b/fiskal-schema-helpers.go @@ -15,9 +15,9 @@ import ( // // dateTime (time.Time): The date and time of the invoice. // centralized (bool): Indicates whether the sequence mark is centralized. -// brOznRac (int): The unique number of the invoice. -// oznPosPr (string): The identifier for the business location where the invoice was issued. -// oznNapUr (int): The identifier for the cash register device used to issue the invoice. +// invoiceNumber (uint): The unique number of the invoice. +// locationIdentifier (string): The identifier for the business location where the invoice was issued. +// registerDeviceID (uint): The identifier for the cash register device used to issue the invoice. // pdvValues ([][]interface{}): A 2D array for VAT details (nullable). // pnpValues ([][]interface{}): A 2D array for consumption tax details (nullable). // ostaliPorValues ([][]interface{}): A 2D array for other tax details (nullable). @@ -28,20 +28,19 @@ import ( // iznosUkupno (string): The total amount. // nacinPlac (string): The payment method. // oibOper (string): The OIB of the operator. -// zastKod (string): The protection code. // nakDost (bool): Indicates whether the invoice is delivered. // paragonBrRac (string): The paragon invoice number (optional). // specNamj (string): Special purpose (optional). // // Returns: // -// (*RacunType, error): A pointer to a new RacunType instance with the provided values, or an error if the input is invalid. +// (*RacunType, string, error): A pointer to a new RacunType instance with the provided values, generated zki or an error if the input is invalid. func (fe *FiskalEntity) NewCISInvoice( dateTime time.Time, centralized bool, - invoiceNumber int, + invoiceNumber uint, locationIdentifier string, - registerDeviceID int, + registerDeviceID uint, pdvValues [][]interface{}, pnpValues [][]interface{}, ostaliPorValues [][]interface{}, @@ -52,11 +51,10 @@ func (fe *FiskalEntity) NewCISInvoice( iznosUkupno string, nacinPlac string, oibOper string, - zastKod string, nakDost bool, paragonBrRac string, specNamj string, -) (*RacunType, error) { +) (*RacunType, string, error) { // Format the date and time formattedDate := dateTime.Format("2006-01-02T15:04:05") @@ -72,7 +70,7 @@ func (fe *FiskalEntity) NewCISInvoice( if pdvValues != nil { pdv, err = NewPdv(pdvValues) if err != nil { - return nil, err + return nil, "", err } } @@ -80,7 +78,7 @@ func (fe *FiskalEntity) NewCISInvoice( if pnpValues != nil { pnp, err = NewPNP(pnpValues) if err != nil { - return nil, err + return nil, "", err } } @@ -88,7 +86,7 @@ func (fe *FiskalEntity) NewCISInvoice( if ostaliPorValues != nil { ostaliPor, err = OtherTaxes(ostaliPorValues) if err != nil { - return nil, err + return nil, "", err } } @@ -96,7 +94,7 @@ func (fe *FiskalEntity) NewCISInvoice( if naknadeValues != nil { naknade, err = Naknade(naknadeValues) if err != nil { - return nil, err + return nil, "", err } } @@ -107,6 +105,19 @@ func (fe *FiskalEntity) NewCISInvoice( OznNapUr: registerDeviceID, } + //check means of payment can be: G - Cash, K - Card, O - Mix/other + // , T - Bank transfer (usually not sent to CIS not mandatory) + // , C - Check [deprecated] + if nacinPlac != "G" && nacinPlac != "K" && nacinPlac != "O" && nacinPlac != "T" && nacinPlac != "C" { + return nil, "", errors.New("NacinPlac must be one of the following values: G, K, O, T, C (deprecated)") + } + + zki, err := fe.GenerateZKI(dateTime, invoiceNumber, locationIdentifier, registerDeviceID, iznosUkupno) + + if err != nil { + return nil, "", err + } + return &RacunType{ Oib: fe.OIB, USustPdv: fe.SustPDV, @@ -123,11 +134,11 @@ func (fe *FiskalEntity) NewCISInvoice( IznosUkupno: iznosUkupno, NacinPlac: nacinPlac, OibOper: oibOper, - ZastKod: zastKod, + ZastKod: zki, NakDost: nakDost, ParagonBrRac: paragonBrRac, SpecNamj: specNamj, - }, nil + }, zki, nil } // NewFiskalHeader creates a new instance of ZaglavljeType with a unique message ID and the current timestamp @@ -377,7 +388,7 @@ func NewPdv(values [][]interface{}) (*PdvType, error) { // Returns: // // *BrojRacunaType: A pointer to a new BrojRacunaType instance with the provided values. -func NewInvoiceNumber(InvoiceNumber int, LocationIdentifier string, RegisterDeviceID int) *BrojRacunaType { +func NewInvoiceNumber(InvoiceNumber uint, LocationIdentifier string, RegisterDeviceID uint) *BrojRacunaType { return &BrojRacunaType{ BrOznRac: InvoiceNumber, OznPosPr: LocationIdentifier, diff --git a/fiskal-schema.go b/fiskal-schema.go index d6bfe78..f2c3f0d 100644 --- a/fiskal-schema.go +++ b/fiskal-schema.go @@ -185,13 +185,13 @@ type NapojnicaType struct { // GreskeType ... type GreskeType struct { - Greska []*GreskaType `xml:"tns:Greska"` + Greska []*GreskaType `xml:"Greska"` } // GreskaType ... type GreskaType struct { - SifraGreske string `xml:"tns:SifraGreske"` - PorukaGreske string `xml:"tns:PorukaGreske"` + SifraGreske string `xml:"SifraGreske"` + PorukaGreske string `xml:"PorukaGreske"` } // NaknadeType ... @@ -237,9 +237,9 @@ type PorezType struct { // BrojRacunaType ... type BrojRacunaType struct { - BrOznRac int `xml:"tns:BrOznRac"` + BrOznRac uint `xml:"tns:BrOznRac"` OznPosPr string `xml:"tns:OznPosPr"` - OznNapUr int `xml:"tns:OznNapUr"` + OznNapUr uint `xml:"tns:OznNapUr"` } // BrojPDType ... diff --git a/fiskalhr_test.go b/fiskalhr_test.go index 3f7a89c..59823be 100644 --- a/fiskalhr_test.go +++ b/fiskalhr_test.go @@ -1,10 +1,13 @@ package fiskalhrgo import ( + "encoding/xml" "fmt" "os" "testing" "time" + + "math/rand" ) var testCert *CertManager @@ -232,3 +235,186 @@ func TestCISEcho(t *testing.T) { t.Fatalf("Expected the sent message returned!") } } + +// Test CIS invoice with helper functions +func TestNewCISInvoice(t *testing.T) { + pdvValues := [][]interface{}{ + {25, "1000.00", "250.00"}, + } + + pnpValues := [][]interface{}{ + {3, "1000.00", "30.00"}, + } + + ostaliPorValues := [][]interface{}{ + {"Other Tax", 5, "1000.00", "50.00"}, + } + + naknadeValues := [][]string{ + {"Povratna", "0.50"}, + } + + dateTime := time.Now() + centralized := true + brOznRac := uint(rand.Intn(6901) + 100) + oznPosPr := "001" + oznNapUr := uint(1) + iznosUkupno := "1330.50" + nacinPlac := "G" + oibOper := "12345678901" + nakDost := true + paragonBrRac := "12345" + specNamj := "Special Purpose" + + invoice, zki, err := testEntity.NewCISInvoice( + dateTime, + centralized, + brOznRac, + oznPosPr, + oznNapUr, + pdvValues, + pnpValues, + ostaliPorValues, + "0", + "0", + "0", + naknadeValues, + iznosUkupno, + nacinPlac, + oibOper, + nakDost, + paragonBrRac, + specNamj, + ) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if invoice.Oib != testEntity.OIB { + t.Errorf("Expected Oib %v, got %v", testEntity.OIB, invoice.Oib) + } + + if invoice.USustPdv != true { + t.Errorf("Expected USustPdv true, got %v", invoice.USustPdv) + } + + if invoice.DatVrijeme != dateTime.Format("2006-01-02T15:04:05") { + t.Errorf("Expected DatVrijeme %v, got %v", dateTime.Format("2006-01-02T15:04:05"), invoice.DatVrijeme) + } + + expectedOznSlijed := "P" + if !centralized { + expectedOznSlijed = "N" + } + if invoice.OznSlijed != expectedOznSlijed { + t.Errorf("Expected OznSlijed %v, got %v", expectedOznSlijed, invoice.OznSlijed) + } + + if invoice.BrRac.BrOznRac != brOznRac { + t.Errorf("Expected BrOznRac %v, got %v", brOznRac, invoice.BrRac.BrOznRac) + } + + if invoice.BrRac.OznPosPr != oznPosPr { + t.Errorf("Expected OznPosPr %v, got %v", oznPosPr, invoice.BrRac.OznPosPr) + } + + if invoice.BrRac.OznNapUr != oznNapUr { + t.Errorf("Expected OznNapUr %v, got %v", oznNapUr, invoice.BrRac.OznNapUr) + } + + if invoice.IznosUkupno != iznosUkupno { + t.Errorf("Expected IznosUkupno %v, got %v", iznosUkupno, invoice.IznosUkupno) + } + + if invoice.NacinPlac != nacinPlac { + t.Errorf("Expected NacinPlac %v, got %v", nacinPlac, invoice.NacinPlac) + } + + if invoice.OibOper != oibOper { + t.Errorf("Expected OibOper %v, got %v", oibOper, invoice.OibOper) + } + + if invoice.ZastKod != zki { + t.Errorf("Expected ZastKod %v, got %v", zki, invoice.ZastKod) + } + + if invoice.NakDost != nakDost { + t.Errorf("Expected NakDost %v, got %v", nakDost, invoice.NakDost) + } + + if invoice.ParagonBrRac != paragonBrRac { + t.Errorf("Expected ParagonBrRac %v, got %v", paragonBrRac, invoice.ParagonBrRac) + } + + if invoice.SpecNamj != specNamj { + t.Errorf("Expected SpecNamj %v, got %v", specNamj, invoice.SpecNamj) + } + + // Additional checks for nullable fields + if invoice.Pdv == nil { + t.Errorf("Expected Pdv to be non-nil") + } + + if invoice.Pnp == nil { + t.Errorf("Expected Pnp to be non-nil") + } + + if invoice.OstaliPor == nil { + t.Errorf("Expected OstaliPor to be non-nil") + } + + if invoice.Naknade == nil { + t.Errorf("Expected Naknade to be non-nil") + } + + //Combine with zahtjev for final XML + zahtjev := RacunZahtjev{ + Zaglavlje: NewFiskalHeader(), + Racun: invoice, + Xmlns: DefaultNamespace, + IdAttr: fmt.Sprintf("%d", brOznRac), + } + + t.Logf("Zahtijev UUID: %s", zahtjev.Zaglavlje.IdPoruke) + t.Logf("Zahtijev Timestamp: %s", zahtjev.Zaglavlje.DatumVrijeme) + + // Marshal the RacunZahtjev to XML + xmlData, err := xml.MarshalIndent(zahtjev, "", " ") + if err != nil { + t.Fatalf("Error marshalling RacunZahtjev: %v", err) + } + + t.Log(string(xmlData)) + + /* TODO: we need to implement proper xml signing and verification for this to make sense + for now we get a response of the apropiate structure + with expected error s004: Neispravan digitalni potpis. + // Lets send it to CIS and see if we get a response + body, status, errComm := testEntity.GetResponse(xmlData) + + t.Errorf("error in response: %v", errComm) + + //unmarshad bodyu to get Racun Odgovor + var racunOdgovor RacunOdgovor + if err := xml.Unmarshal(body, &racunOdgovor); err != nil { + t.Fatalf("failed to unmarshal XML response: %v\n%v", err, string(body)) + } + + //output zaglavlje first all elements + t.Logf("Racun Odgovor IdPoruke: %s", racunOdgovor.Zaglavlje.IdPoruke) + t.Logf("Racun Odgovor DatumVrijeme: %s", racunOdgovor.Zaglavlje.DatumVrijeme) + + if status != 200 { + + //all errors one by one + for _, greska := range racunOdgovor.Greske.Greska { + t.Logf("Racun Odgovor Greska: %s: %s", greska.SifraGreske, greska.PorukaGreske) + } + + } else { + //output JIR: Jedinicni identifikator racuna + t.Logf("Racun Odgovor JIR: %s", racunOdgovor.Jir) + } + */ +}