Skip to content

Commit

Permalink
Improve schema and xml generation helper functions.
Browse files Browse the repository at this point in the history
Add tests and improve tests.
Refactor CIS communication code for better readability and maintainability.
Improve code comments and formatting.
  • Loading branch information
arvvoid committed Sep 20, 2024
1 parent 3ca1828 commit 640a5f7
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 26 deletions.
6 changes: 1 addition & 5 deletions ciscomm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
43 changes: 27 additions & 16 deletions fiskal-schema-helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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{},
Expand All @@ -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")

Expand All @@ -72,31 +70,31 @@ func (fe *FiskalEntity) NewCISInvoice(
if pdvValues != nil {
pdv, err = NewPdv(pdvValues)
if err != nil {
return nil, err
return nil, "", err
}
}

var pnp *PorezNaPotrosnjuType
if pnpValues != nil {
pnp, err = NewPNP(pnpValues)
if err != nil {
return nil, err
return nil, "", err
}
}

var ostaliPor *OstaliPoreziType
if ostaliPorValues != nil {
ostaliPor, err = OtherTaxes(ostaliPorValues)
if err != nil {
return nil, err
return nil, "", err
}
}

var naknade *NaknadeType
if naknadeValues != nil {
naknade, err = Naknade(naknadeValues)
if err != nil {
return nil, err
return nil, "", err
}
}

Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions fiskal-schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...
Expand Down Expand Up @@ -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 ...
Expand Down
186 changes: 186 additions & 0 deletions fiskalhr_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package fiskalhrgo

import (
"encoding/xml"
"fmt"
"os"
"testing"
"time"

"math/rand"
)

var testCert *CertManager
Expand Down Expand Up @@ -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)
}
*/
}

0 comments on commit 640a5f7

Please sign in to comment.