diff --git a/canonicalization_test.go b/canonicalization_test.go
new file mode 100644
index 0000000..e0d40e1
--- /dev/null
+++ b/canonicalization_test.go
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: Apache-2.0
+// This file is adapted from the github.com/russellhaering/goxmldsig project.
+package fiskalhrgo
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/beevik/etree"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ assertion = `https://saml2.test.astuart.co/sso/saml2urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport`
+ c14n11 = `https://saml2.test.astuart.co/sso/saml2urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport`
+ assertionC14ned = `https://saml2.test.astuart.co/sso/saml2urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport`
+ c14n11Comment = `https://saml2.test.astuart.co/sso/saml2urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport`
+ assertionC14nedComment = `https://saml2.test.astuart.co/sso/saml2urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport`
+)
+
+const (
+ xmldoc = ``
+ xmldocC14N10ExclusiveCanonicalized = ``
+ xmldocC14N11Canonicalized = ``
+)
+
+func runCanonicalizationTest(t *testing.T, canonicalizer Canonicalizer, xmlstr string, canonicalXmlstr string) {
+ raw := etree.NewDocument()
+ err := raw.ReadFromString(xmlstr)
+ require.NoError(t, err)
+
+ canonicalized, err := canonicalizer.Canonicalize(raw.Root())
+ require.NoError(t, err)
+ require.Equal(t, canonicalXmlstr, string(canonicalized))
+}
+
+func TestExcC14N10(t *testing.T) {
+ runCanonicalizationTest(t, MakeC14N10ExclusiveCanonicalizerWithPrefixList(""), assertion, assertionC14ned)
+}
+
+func TestExcC14N10WithComments(t *testing.T) {
+ runCanonicalizationTest(t, MakeC14N10ExclusiveWithCommentsCanonicalizerWithPrefixList(""), assertion, assertionC14nedComment)
+}
+
+func TestC14N11(t *testing.T) {
+ runCanonicalizationTest(t, MakeC14N11Canonicalizer(), assertion, c14n11)
+}
+
+func TestC14N11WithComments(t *testing.T) {
+ runCanonicalizationTest(t, MakeC14N11WithCommentsCanonicalizer(), assertion, c14n11Comment)
+}
+
+func TestXmldocC14N10Exclusive(t *testing.T) {
+ runCanonicalizationTest(t, MakeC14N10ExclusiveCanonicalizerWithPrefixList(""), xmldoc, xmldocC14N10ExclusiveCanonicalized)
+}
+
+func TestXmldocC14N11(t *testing.T) {
+ runCanonicalizationTest(t, MakeC14N11Canonicalizer(), xmldoc, xmldocC14N11Canonicalized)
+}
+
+func TestNestedExcC14N11(t *testing.T) {
+ input := ``
+ expected := ``
+ runCanonicalizationTest(t, MakeC14N11Canonicalizer(), input, expected)
+}
+
+func TestExcC14nDefaultNamespace(t *testing.T) {
+ input := ``
+ expected := ``
+ runCanonicalizationTest(t, MakeC14N10ExclusiveCanonicalizerWithPrefixList(""), input, expected)
+}
+
+func TestExcC14nWithPrefixList(t *testing.T) {
+ input := ``
+ expected := ``
+ canonicalizer := MakeC14N10ExclusiveCanonicalizerWithPrefixList("xs")
+ runCanonicalizationTest(t, canonicalizer, input, expected)
+}
+
+func TestExcC14nRedeclareDefaultNamespace(t *testing.T) {
+ input := ``
+ expected := ``
+ canonicalizer := MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
+ runCanonicalizationTest(t, canonicalizer, input, expected)
+}
+
+func TestC14N10RecCanonicalizer(t *testing.T) {
+ // From https://www.w3.org/TR/2001/REC-xml-c14n-20010315#Example-SETags
+ input := `
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ expected := `
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+ canonicalizer := MakeC14N10RecCanonicalizer()
+ runCanonicalizationTest(t, canonicalizer, input, expected)
+}
+
+func TestC14N10RecCanonicalizerWithNamespaceInheritance(t *testing.T) {
+ input := `
+
+ Hello, World!
+
+ `
+
+ expected := `
+ Hello, World!
+ `
+
+ doc := etree.NewDocument()
+ if err := doc.ReadFromString(input); err != nil {
+ t.Fatalf("Error parsing input XML: %v", err)
+ }
+
+ childElement := doc.FindElement(".//ns2:ChildElement")
+ if childElement == nil {
+ t.Fatal("Error: childElement not found")
+ }
+
+ canonicalizer := MakeC14N10RecCanonicalizer()
+ canonicalized, err := canonicalizer.Canonicalize(childElement)
+ require.NoError(t, err)
+ require.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(canonicalized)))
+
+}
diff --git a/ciscomm.go b/ciscomm.go
index fd41bf2..a24837e 100644
--- a/ciscomm.go
+++ b/ciscomm.go
@@ -70,7 +70,6 @@ func (fe *FiskalEntity) GetResponse(xmlPayload []byte, sign bool) ([]byte, int,
return nil, 0, fmt.Errorf("failed to sign XML: %w", err)
}
xmlPayload = signedXML
- fmt.Println(string(xmlPayload))
}
// Prepare the SOAP envelope with the payload
diff --git a/dsignandverify.go b/dsignandverify.go
index f8eb71b..4b3571c 100644
--- a/dsignandverify.go
+++ b/dsignandverify.go
@@ -28,7 +28,7 @@ func docC14N10(xmlData string) ([]byte, error) {
return nil, fmt.Errorf("failed to parse XML: %v", err)
}
- // Use the Canonical XML 1.0 algorithm from goxmldsig
+ // Use the Canonical XML 1.0 algorithm
canonicalizer := MakeC14N10RecCanonicalizer() // Without comments
canonicalizedXML, err := canonicalizer.Canonicalize(doc.Root())
if err != nil {
diff --git a/fiskal-schema.go b/fiskal-schema.go
index e3d3565..10dcfc2 100644
--- a/fiskal-schema.go
+++ b/fiskal-schema.go
@@ -130,6 +130,7 @@ type ZaglavljeOdgovorType struct {
// RacunType represents the invoice type with various details required for fiscalization.
type RacunType struct {
+ XMLName xml.Name `xml:"tns:Racun"`
Oib string `xml:"tns:Oib"`
USustPdv bool `xml:"tns:USustPdv"`
DatVrijeme string `xml:"tns:DatVrijeme"`
diff --git a/fiskalhr.go b/fiskalhr.go
index eed66e1..19bd1f8 100644
--- a/fiskalhr.go
+++ b/fiskalhr.go
@@ -13,6 +13,7 @@ import (
"errors"
"fmt"
"strconv"
+ "strings"
"time"
)
@@ -319,3 +320,107 @@ func (fe *FiskalEntity) PingCIS() error {
}
return nil
}
+
+// InvoiceRequest sends an invoice request to the CIS (Croatian Fiscalization System) and processes the response.
+//
+// This function performs the following steps:
+// 1. Minimally validates the provided invoice for required fields
+// (any business logic and math is the responsibility of the invoicing application using the library)
+// PLEASE NOTE: the CIS also don't do any extensive validation of the invoice, only basic checks.
+// so you could get a JIR back even if the invoice is not correct.
+// But if you do that you can have problems later with inspecions or periodic CIS checks of the data.
+// The library will send the data as is to the CIS.
+// So please validate and chek the invoice data acording to you buinies logic
+// before sending it to the CIS.
+// 2. Sends the XML request to the CIS and receives the response.
+// 3. Unmarshals the response XML to extract the response data.
+// 4. Checks for errors in the response and aggregates them if any are found.
+// 5. Returns the JIR (Unique Invoice Identifier) if the request was successful.
+//
+// Parameters:
+// - invoice: A pointer to a RacunType struct representing the invoice to be sent.
+//
+// Returns:
+// - A string representing the JIR (Unique Invoice Identifier) if the request was successful.
+// - A string representing the ZKI (Protection Code of the Issuer) from the invoice.
+// - An error if any issues occurred during the process.
+//
+// Possible errors:
+// - If the invoice is nil or something is invalid (only basic checks).
+// - If the SpecNamj field of the invoice is not empty.
+// - If the ZastKod field of the invoice is empty.
+// - If there is an error marshalling the request to XML.
+// - If there is an error making the request to the CIS.
+// - If there is an error unmarshalling the response XML.
+// - If the IdPoruke in the response does not match the request.
+// - If the response status is not 200 and there are errors in the response.
+// - If the JIR in the response is empty.
+// - If an unexpected error occurs.
+func (fe *FiskalEntity) InvoiceRequest(invoice *RacunType) (string, string, error) {
+
+ //some basic tests for invoice
+ if invoice == nil {
+ return "", "", errors.New("invoice is nil")
+ }
+
+ if invoice.SpecNamj != "" {
+ return "", "", errors.New("invoice SpecNamj must be empty")
+ }
+
+ if invoice.ZastKod == "" {
+ return "", "", errors.New("invoice ZKI (Zastitni Kod Izdavatelja) must be set")
+ }
+
+ //Combine with zahtjev for final XML
+ zahtjev := RacunZahtjev{
+ Zaglavlje: NewFiskalHeader(),
+ Racun: invoice,
+ Xmlns: DefaultNamespace,
+ IdAttr: generateUniqueID(),
+ }
+
+ // Marshal the RacunZahtjev to XML
+ xmlData, err := xml.MarshalIndent(zahtjev, "", " ")
+ if err != nil {
+ return "", invoice.ZastKod, fmt.Errorf("error marshalling RacunZahtjev: %w", err)
+ }
+
+ // Let's send it to CIS
+ body, status, errComm := fe.GetResponse(xmlData, true)
+
+ if errComm != nil {
+ return "", invoice.ZastKod, fmt.Errorf("failed to make request: %w", errComm)
+ }
+
+ //unmarshad body to get Racun Odgovor
+ var racunOdgovor RacunOdgovor
+ if err := xml.Unmarshal(body, &racunOdgovor); err != nil {
+ return "", invoice.ZastKod, fmt.Errorf("failed to unmarshal XML response: %w", err)
+ }
+
+ if zahtjev.Zaglavlje.IdPoruke != racunOdgovor.Zaglavlje.IdPoruke {
+ return "", invoice.ZastKod, errors.New("IdPoruke mismatch")
+ }
+
+ if status != 200 {
+
+ // Aggregate all errors into a single error message
+ var errorMessages []string
+ for _, greska := range racunOdgovor.Greske.Greska {
+ errorMessages = append(errorMessages, fmt.Sprintf("%s: %s", greska.SifraGreske, greska.PorukaGreske))
+ }
+ if len(errorMessages) > 0 {
+ return "", invoice.ZastKod, fmt.Errorf("errors in response: %s", strings.Join(errorMessages, "; "))
+ }
+
+ } else {
+ if racunOdgovor.Jir != "" {
+ return racunOdgovor.Jir, invoice.ZastKod, nil
+ } else {
+ return "", invoice.ZastKod, errors.New("JIR is empty")
+ }
+ }
+
+ // Add a default return statement to handle unexpected cases
+ return "", invoice.ZastKod, errors.New("unexpected error")
+}
diff --git a/fiskalhr_test.go b/fiskalhr_test.go
index abd5ab7..a11c2fd 100644
--- a/fiskalhr_test.go
+++ b/fiskalhr_test.go
@@ -196,7 +196,7 @@ func TestNewCISInvoice(t *testing.T) {
oibOper := "12345678901"
nakDost := true
paragonBrRac := "12345"
- specNamj := "Special Purpose"
+ specNamj := ""
invoice, zki, err := testEntity.NewCISInvoice(
dateTime,
@@ -294,51 +294,25 @@ func TestNewCISInvoice(t *testing.T) {
t.Errorf("Expected Naknade to be non-nil")
}
- //Combine with zahtjev for final XML
- zahtjev := RacunZahtjev{
- Zaglavlje: NewFiskalHeader(),
- Racun: invoice,
- Xmlns: DefaultNamespace,
- IdAttr: generateUniqueID(),
- }
-
- 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, "", " ")
+ //marshal invoice and log XML
+ xmlData, err := xml.MarshalIndent(invoice, "", " ")
if err != nil {
- t.Fatalf("Error marshalling RacunZahtjev: %v", err)
+ t.Fatalf("Expected no error, got %v", err)
}
- t.Log(string(xmlData))
+ t.Logf("Invoice XML: %s", xmlData)
- // Lets send it to CIS and see if we get a response
- body, status, errComm := testEntity.GetResponse(xmlData, true)
+ // Send test invoice to CIS with InvoiceRequest
+ jir, zkiR, err := testEntity.InvoiceRequest(invoice)
- if errComm != nil {
- t.Fatalf("Failed to make request: %v", errComm)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
}
- //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))
+ if zkiR != zki {
+ t.Errorf("Expected ZKI %v, got %v", zki, zkiR)
}
- //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)
- }
+ t.Logf("We got a JIR!: %v, ZKI: %v", jir, zkiR)
- } else {
- //output JIR: Jedinicni identifikator racuna
- t.Logf("Racun Odgovor JIR: %s", racunOdgovor.Jir)
- }
}