diff --git a/README-HR.md b/README-HR.md index c3086e7..311b2d6 100644 --- a/README-HR.md +++ b/README-HR.md @@ -126,13 +126,6 @@ func main() { "G", // payment method G - cash, K - credit card, T - // transfer, O - other, C - check (deprecated) "12345678901", // operator OIB - false, // late delivery, if previous attempt failed but the - // invoice was issued with just ZKI - "", // receipt book number, if the invoicing system was - // unusable and the invoice was issued manually, the - // number of the receipt book - "", // unused, reserved field for future or temporary - // unexpected use by the CIS, should be empty ) if err != nil { @@ -151,14 +144,14 @@ func main() { // serial of the certificate used to generate it for future reference. You // can get the cert serial with fiskalEntity.GetCertSERIAL(). - // Display the invoice + // Display the invoice for test fmt.Println(invoice) // NOW we should have a saved invoice with a valid ZKI and we are ready to // send the invoice to the CIS // Send test invoice to CIS with InvoiceRequest - jir, zkiR, err := fiskalEntity.InvoiceRequest(invoice) + jir, zkiR, err := invoice.InvoiceRequest() if err != nil { log.Fatalf("Failed to send invoice: %v", err) diff --git a/README.md b/README.md index 407e144..9ea8eb2 100644 --- a/README.md +++ b/README.md @@ -125,13 +125,6 @@ func main() { "G", // payment method G - cash, K - credit card, T - // transfer, O - other, C - check (deprecated) "12345678901", // operator OIB - false, // late delivery, if previous attempt failed but the - // invoice was issued with just ZKI - "", // receipt book number, if the invoicing system was - // unusable and the invoice was issued manually, the - // number of the receipt book - "", // unused, reserved field for future or temporary - // unexpected use by the CIS, should be empty ) if err != nil { @@ -150,14 +143,14 @@ func main() { // serial of the certificate used to generate it for future reference. You // can get the cert serial with fiskalEntity.GetCertSERIAL(). - // Display the invoice + // Display the invoice for test fmt.Println(invoice) // NOW we should have a saved invoice with a valid ZKI and we are ready to // send the invoice to the CIS // Send test invoice to CIS with InvoiceRequest - jir, zkiR, err := fiskalEntity.InvoiceRequest(invoice) + jir, zkiR, err := invoice.InvoiceRequest() if err != nil { log.Fatalf("Failed to send invoice: %v", err) diff --git a/basicvalidators.go b/basicvalidators.go index 23dba9d..1477697 100644 --- a/basicvalidators.go +++ b/basicvalidators.go @@ -17,6 +17,15 @@ func IsValidCurrencyFormat(amount string) bool { return validCurrency.MatchString(amount) } +// IsValidTaxRate checks if the given string is a valid non-negative tax rate with exactly two decimal places. +// Allows positive values and 0.00, but not negative values. +func IsValidTaxRate(rate string) bool { + // Regex pattern to match a positive or zero decimal number with exactly two decimal places + // Matches values like "0.00", "25.00", "5.00", etc. + validTaxRate := regexp.MustCompile(`^([0-9]+)(\.[0-9]{2})$`) + return validTaxRate.MatchString(rate) +} + // ValidateOIB checks if an OIB is valid using the Mod 11, 10 algorithm func ValidateOIB(oib string) bool { if len(oib) != 11 { @@ -86,3 +95,19 @@ func IsFileReadable(filePath string) bool { return true } + +// ValidateJIR checks if the given JIR is a valid UUID format (e.g., "9d6f5bb6-da48-4fcd-a803-4586a025e0e4"). +// Returns true if valid, otherwise false. +func ValidateJIR(jir string) bool { + // Regular expression to match UUID format (e.g., "9d6f5bb6-da48-4fcd-a803-4586a025e0e4") + var jirRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) + return jirRegex.MatchString(jir) +} + +// ValidateZKI checks if the given ZKI is a valid MD5 hash in hexadecimal format (32 characters). +// Returns true if valid, otherwise false. +func ValidateZKI(zki string) bool { + // Regular expression to match a 32-character hexadecimal MD5 hash + var zkiRegex = regexp.MustCompile(`^[0-9a-f]{32}$`) + return zkiRegex.MatchString(zki) +} diff --git a/dsignandverify.go b/dsignandverify.go index 98ff09b..1d4c501 100644 --- a/dsignandverify.go +++ b/dsignandverify.go @@ -11,16 +11,10 @@ import ( "crypto/x509" "encoding/base64" "fmt" - "time" "github.com/beevik/etree" ) -// generateUniqueID generates a unique ID -func generateUniqueID() string { - return fmt.Sprintf("%x", time.Now().UnixNano()) -} - // doc14n applies Exclusive Canonical XML (http://www.w3.org/2001/10/xml-exc-c14n#) to the input XML data func doc14n(xmlData []byte) ([]byte, error) { // Parse the input XML string into an etree.Document diff --git a/fiskal-schema-helpers.go b/fiskal-schema-helpers.go deleted file mode 100644 index a657c92..0000000 --- a/fiskal-schema-helpers.go +++ /dev/null @@ -1,399 +0,0 @@ -package fiskalhrgo - -// SPDX-License-Identifier: MIT -// Copyright (c) 2024 L. D. T. d.o.o. -// Copyright (c) contributors for their respective contributions. See https://github.com/l-d-t/fiskalhrgo/graphs/contributors - -import ( - "errors" - "time" - - "github.com/google/uuid" -) - -// NewCISInvoice initializes and returns a RacunType instance -// -// This method creates a new instance of RacunType, which represents an invoice with all necessary fields. -// -// Parameters: -// -// - dateTime (time.Time): The date and time of the invoice. -// - centralized (bool): Indicates whether the sequence mark is centralized. -// - 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). -// - iznosOslobPdv (string): The amount exempt from VAT (optional). -// - iznosMarza (string): The margin amount (optional). -// - iznosNePodlOpor (string): The amount not subject to taxation (optional). -// - naknadeValues ([][]string): A 2D array for fees details (nullable). -// - iznosUkupno (string): The total amount. -// - nacinPlac (string): The payment method. -// - oibOper (string): The OIB of the operator. -// - nakDost (bool): Indicates whether the invoice is delivered. -// - paragonBrRac (string): The paragon invoice number (optional). -// - specNamj (string): Special purpose (optional). -// -// Returns: -// -// (*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, - invoiceNumber uint, - registerDeviceID uint, - pdvValues [][]interface{}, - pnpValues [][]interface{}, - ostaliPorValues [][]interface{}, - iznosOslobPdv string, - iznosMarza string, - iznosNePodlOpor string, - naknadeValues [][]string, - iznosUkupno string, - nacinPlac string, - oibOper string, - nakDost bool, - paragonBrRac string, - specNamj string, -) (*RacunType, string, error) { - // Format the date and time - formattedDate := dateTime.Format("02.01.2006T15:04:05") - - // Determine the sequence mark - oznSlijed := "N" - if fe.centralizedInvoiceNumber { - oznSlijed = "P" - } - - // Use helper functions to create the necessary types - var pdv *PdvType - var err error - if pdvValues != nil { - pdv, err = NewPdv(pdvValues) - if err != nil { - return nil, "", err - } - } - - var pnp *PorezNaPotrosnjuType - if pnpValues != nil { - pnp, err = NewPNP(pnpValues) - if err != nil { - return nil, "", err - } - } - - var ostaliPor *OstaliPoreziType - if ostaliPorValues != nil { - ostaliPor, err = OtherTaxes(ostaliPorValues) - if err != nil { - return nil, "", err - } - } - - var naknade *NaknadeType - if naknadeValues != nil { - naknade, err = Naknade(naknadeValues) - if err != nil { - return nil, "", err - } - } - - // Create the BrojRacunaType instance - brRac := &BrojRacunaType{ - BrOznRac: invoiceNumber, - OznPosPr: fe.locationID, - 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, registerDeviceID, iznosUkupno) - - if err != nil { - return nil, "", err - } - - return &RacunType{ - Oib: fe.oib, - USustPdv: fe.sustPDV, - DatVrijeme: formattedDate, - OznSlijed: oznSlijed, - BrRac: brRac, - Pdv: pdv, - Pnp: pnp, - OstaliPor: ostaliPor, - IznosOslobPdv: iznosOslobPdv, - IznosMarza: iznosMarza, - IznosNePodlOpor: iznosNePodlOpor, - Naknade: naknade, - IznosUkupno: iznosUkupno, - NacinPlac: nacinPlac, - OibOper: oibOper, - ZastKod: zki, - NakDost: nakDost, - ParagonBrRac: paragonBrRac, - SpecNamj: specNamj, - }, zki, nil -} - -// NewFiskalHeader creates a new instance of ZaglavljeType with a unique message ID and the current timestamp -// -// This function generates a new UUIDv4 for the IdPoruke field to ensure that each message has a unique identifier. -// It also sets the DatumVrijeme field to the current time formatted as "2006-01-02T15:04:05" to indicate when the message was created. -// -// Returns: -// -// *ZaglavljeType: A pointer to a new ZaglavljeType instance with the IdPoruke and DatumVrijeme fields populated. -func NewFiskalHeader() *ZaglavljeType { - return &ZaglavljeType{ - IdPoruke: uuid.New().String(), - DatumVrijeme: time.Now().Format("02.01.2006T15:04:05"), - } -} - -// Naknade initializes and returns a NaknadeType instance -// -// This function creates a new instance of NaknadeType, which represents a collection of fees (NaknadaType) entries. -// It takes a 2D array of values where each inner array represents a single fee entry with the name and amount. -// -// Parameters: -// -// values ([][]string): A 2D array where each inner array contains two elements: -// - string: The name of the fee (NazivN) -// - string: The amount of the fee (IznosN) -// -// Returns: -// -// (*NaknadeType, error): A pointer to a new NaknadeType instance with the provided fee entries, or an error if the input is invalid. -// -// Example: -// -// values := [][]string{ -// {"Service Fee", "100"}, -// {"Delivery Fee", "50"}, -// } -// naknade, err := Naknade(values) -// if err != nil { -// fmt.Printf("Error: %v\n", err) -// } else { -// fmt.Printf("Naknade: %+v\n", naknade) -// } -func Naknade(values [][]string) (*NaknadeType, error) { - naknade := make([]*NaknadaType, len(values)) - for i, v := range values { - if len(v) != 2 { - return nil, errors.New("each inner array must contain exactly two elements") - } - feeName := v[0] - feeAmount := v[1] - if !IsValidCurrencyFormat(feeAmount) { - return nil, errors.New("the second element of each inner array must be a valid currency format (fee amount)") - } - naknade[i] = &NaknadaType{NazivN: feeName, IznosN: feeAmount} - } - return &NaknadeType{Naknada: naknade}, nil -} - -// OtherTaxes initializes and returns an OstaliPoreziType instance -// -// This function creates a new instance of OstaliPoreziType, which represents a collection of other taxes (PorezOstaloType) entries. -// It takes a 2D array of values where each inner array represents a single other tax entry with the name, rate, base, and amount. -// -// Parameters: -// -// values ([][]interface{}): A 2D array where each inner array contains four elements: -// - string: The name of the tax (Naziv) -// - int: The tax rate (Stopa) -// - string: The tax base (Osnovica) -// - string: The tax amount (Iznos) -// -// Returns: -// -// (*OstaliPoreziType, error): A pointer to a new OstaliPoreziType instance with the provided other tax entries, or an error if the input is invalid. -// -// Example: -// -// values := [][]interface{}{ -// {"Other Tax", 5, "1000", "50"}, -// } -// ostaliPorezi, err := OtherTaxes(values) -// if err != nil { -// fmt.Printf("Error: %v\n", err) -// } else { -// fmt.Printf("OstaliPorezi: %+v\n", ostaliPorezi) -// } -func OtherTaxes(values [][]interface{}) (*OstaliPoreziType, error) { - porezi := make([]*PorezOstaloType, len(values)) - for i, v := range values { - if len(v) != 4 { - return nil, errors.New("each inner array must contain exactly four elements") - } - name, ok := v[0].(string) - if !ok { - return nil, errors.New("the first element of each inner array must be a string (name)") - } - rate, ok := v[1].(string) - if !ok { - return nil, errors.New("the second element of each inner array must be an int (rate)") - } - base, ok := v[2].(string) - if !ok { - return nil, errors.New("the third element of each inner array must be a string (base)") - } - if !IsValidCurrencyFormat(base) { - return nil, errors.New("the third element of each inner array must be a valid currency format (base)") - } - amount, ok := v[3].(string) - if !ok { - return nil, errors.New("the fourth element of each inner array must be a string (amount)") - } - if !IsValidCurrencyFormat(amount) { - return nil, errors.New("the fourth element of each inner array must be a valid currency format (amount)") - } - porezi[i] = &PorezOstaloType{Naziv: name, Stopa: rate, Osnovica: base, Iznos: amount} - } - return &OstaliPoreziType{Porez: porezi}, nil -} - -// NewPNP initializes and returns a PorezNaPotrosnjuType instance -// -// This function creates a new instance of PorezNaPotrosnjuType, which represents a collection of consumption tax (PorezType) entries. -// It takes a 2D array of values where each inner array represents a single consumption tax entry with the tax rate, tax base, and tax amount. -// -// Parameters: -// -// values ([][]interface{}): A 2D array where each inner array contains three elements: -// - int: The tax rate (Stopa) -// - string: The tax base (Osnovica) -// - string: The tax amount (Iznos) -// -// Returns: -// -// (*PorezNaPotrosnjuType, error): A pointer to a new PorezNaPotrosnjuType instance with the provided consumption tax entries, or an error if the input is invalid. -// -// Example: -// -// values := [][]interface{}{ -// {3, "1000", "30"}, -// {5, "2000", "100"}, -// } -// pnp, err := NewPNP(values) -// if err != nil { -// fmt.Printf("Error: %v\n", err) -// } else { -// fmt.Printf("PorezNaPotrosnju: %+v\n", pnp) -// } -func NewPNP(values [][]interface{}) (*PorezNaPotrosnjuType, error) { - porezi := make([]*PorezType, len(values)) - for i, v := range values { - if len(v) != 3 { - return nil, errors.New("each inner array must contain exactly three elements") - } - taxRate, ok := v[0].(string) - if !ok { - return nil, errors.New("the first element of each inner array must be an int (tax rate)") - } - taxBase, ok := v[1].(string) - if !ok { - return nil, errors.New("the second element of each inner array must be a string (tax base)") - } - if !IsValidCurrencyFormat(taxBase) { - return nil, errors.New("the second element of each inner array must be a valid currency format (tax base)") - } - taxAmount, ok := v[2].(string) - if !ok { - return nil, errors.New("the third element of each inner array must be a string (tax amount)") - } - if !IsValidCurrencyFormat(taxAmount) { - return nil, errors.New("the third element of each inner array must be a valid currency format (tax amount)") - } - porezi[i] = &PorezType{Stopa: taxRate, Osnovica: taxBase, Iznos: taxAmount} - } - return &PorezNaPotrosnjuType{Porez: porezi}, nil -} - -// NewPdv initializes and returns a PdvType instance -// -// This function creates a new instance of PdvType, which represents a collection of VAT (PorezType) entries. -// It takes a 2D array of values where each inner array represents a single VAT entry with the tax rate, tax base, and tax amount. -// -// Parameters: -// -// values ([][]interface{}): A 2D array where each inner array contains three elements: -// - int: The tax rate (Stopa) -// - string: The tax base (Osnovica) -// - string: The tax amount (Iznos) -// -// Returns: -// -// (*PdvType, error): A pointer to a new PdvType instance with the provided VAT entries, or an error if the input is invalid. -// -// Example: -// -// values := [][]interface{}{ -// {25, "1000", "250"}, -// {13, "500", "65"}, -// } -// pdv, err := NewPdv(values) -// if err != nil { -// fmt.Printf("Error: %v\n", err) -// } else { -// fmt.Printf("Pdv: %+v\n", pdv) -// } -func NewPdv(values [][]interface{}) (*PdvType, error) { - porezi := make([]*PorezType, len(values)) - for i, v := range values { - if len(v) != 3 { - return nil, errors.New("each inner array must contain exactly three elements") - } - taxRate, ok := v[0].(string) - if !ok { - return nil, errors.New("the first element of each inner array must be an int (tax rate)") - } - taxBase, ok := v[1].(string) - if !ok { - return nil, errors.New("the second element of each inner array must be a string (tax base)") - } - if !IsValidCurrencyFormat(taxBase) { - return nil, errors.New("the second element of each inner array must be a valid currency format (tax base)") - } - taxAmount, ok := v[2].(string) - if !ok { - return nil, errors.New("the third element of each inner array must be a string (tax amount)") - } - if !IsValidCurrencyFormat(taxAmount) { - return nil, errors.New("the third element of each inner array must be a valid currency format (tax amount)") - } - porezi[i] = &PorezType{Stopa: taxRate, Osnovica: taxBase, Iznos: taxAmount} - } - return &PdvType{Porez: porezi}, nil -} - -// NewInvoiceNumber initializes and returns a BrojRacunaType instance -// -// This function creates a new instance of BrojRacunaType, which represents the structure for an invoice number. -// It takes three parameters: the invoice number, the location identifier, and the register device ID. -// -// Parameters: -// -// InvoiceNumber (int): The unique number of the invoice. -// LocationIdentifier (string): The identifier for the business location where the invoice was issued. -// RegisterDeviceID (int): The identifier for the cash register device used to issue the invoice. -// -// Returns: -// -// *BrojRacunaType: A pointer to a new BrojRacunaType instance with the provided values. -func NewInvoiceNumber(InvoiceNumber uint, LocationIdentifier string, RegisterDeviceID uint) *BrojRacunaType { - return &BrojRacunaType{ - BrOznRac: InvoiceNumber, - OznPosPr: LocationIdentifier, - OznNapUr: RegisterDeviceID, - } -} diff --git a/fiskal-schema.go b/fiskal-schema.go index 4d36bff..01009ec 100644 --- a/fiskal-schema.go +++ b/fiskal-schema.go @@ -4,7 +4,13 @@ package fiskalhrgo // Copyright (c) 2024 L. D. T. d.o.o. // Copyright (c) contributors for their respective contributions. See https://github.com/l-d-t/fiskalhrgo/graphs/contributors -import "encoding/xml" +import ( + "encoding/xml" + "fmt" + "time" + + "github.com/google/uuid" +) const DefaultNamespace = "http://www.apis-it.hr/fin/2012/types/f73" @@ -154,6 +160,15 @@ type RacunType struct { PrateciDokument *PrateciDokument `xml:"tns:PrateciDokument,omitempty"` PromijenjeniNacinPlac string `xml:"tns:PromijenjeniNacinPlac,omitempty"` Napojnica *NapojnicaType `xml:"tns:Napojnica,omitempty"` + + // Additional functional non XML fields + pointerToEntity *FiskalEntity // Pointer to the FiskalEntity + oldEntityForOldZKI *FiskalEntity // Pointer to the old FiskalEntity for the old ZKI + // This is used in the edge case that the ZKI was generated with one certificate and the fiscalization failed + // But the certificate expired or had to be changed and now fiscalization have to be repeated with new certificate + // If we replace the original ZKI its a problem we already gave the invoice with old ZKI out + // So we have to keep the old ZKI and validate it with the old certificate before signing and sending with new one + // In any case this is set by IhaveZKIwithExpiredCertificateEdgeCase(EntityWithOldCertLoaded *FiskalEntity) method } // PrateciDokumentType ... @@ -168,8 +183,8 @@ type PrateciDokumentType struct { // PrateciDokument ... type PrateciDokument struct { - JirPD []string `xml:"tns:JirPD"` - ZastKodPD []string `xml:"tns:ZastKodPD"` + JirPD string `xml:"tns:JirPD"` + ZastKodPD string `xml:"tns:ZastKodPD"` } // NapojnicaType ... @@ -243,3 +258,23 @@ type BrojPDType struct { OznPosPr string `xml:"tns:OznPosPr"` OznNapUr int `xml:"tns:OznNapUr"` } + +// generateUniqueID generates a unique ID +func generateUniqueID() string { + return fmt.Sprintf("%x", time.Now().UnixNano()) +} + +// newFiskalHeader creates a new instance of ZaglavljeType with a unique message ID and the current timestamp +// +// This function generates a new UUIDv4 for the IdPoruke field to ensure that each message has a unique identifier. +// It also sets the DatumVrijeme field to the current time formatted as "2006-01-02T15:04:05" to indicate when the message was created. +// +// Returns: +// +// *ZaglavljeType: A pointer to a new ZaglavljeType instance with the IdPoruke and DatumVrijeme fields populated. +func newFiskalHeader() *ZaglavljeType { + return &ZaglavljeType{ + IdPoruke: uuid.New().String(), + DatumVrijeme: time.Now().Format("02.01.2006T15:04:05"), + } +} diff --git a/fiskal-schema_test.go b/fiskal-schema_test.go index ce5179f..4514d4b 100644 --- a/fiskal-schema_test.go +++ b/fiskal-schema_test.go @@ -27,7 +27,7 @@ func TestEchoRequestMarshal(t *testing.T) { // Test for RacunZahtjev structure func TestRacunZahtjevMarshal(t *testing.T) { racun := RacunZahtjev{ - Zaglavlje: NewFiskalHeader(), + Zaglavlje: newFiskalHeader(), Racun: &RacunType{ Oib: "12345678901", USustPdv: true, diff --git a/fiskalhr.go b/fiskalhr.go index 93f34bd..bace76d 100644 --- a/fiskalhr.go +++ b/fiskalhr.go @@ -14,7 +14,6 @@ import ( "errors" "fmt" "strconv" - "strings" "time" ) @@ -321,107 +320,3 @@ 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 inspections 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 according to you business 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 7d84cb9..f5a8d41 100644 --- a/fiskalhr_test.go +++ b/fiskalhr_test.go @@ -231,11 +231,7 @@ func TestNewCISInvoice(t *testing.T) { brOznRac := uint(rand.Intn(6901) + 100) oznNapUr := uint(1) iznosUkupno := "1330.50" - nacinPlac := "G" oibOper := "12345678901" - nakDost := true - paragonBrRac := "12345" - specNamj := "" invoice, zki, err := testEntity.NewCISInvoice( dateTime, @@ -249,11 +245,8 @@ func TestNewCISInvoice(t *testing.T) { "0.00", naknadeValues, iznosUkupno, - nacinPlac, + CISCash, oibOper, - nakDost, - paragonBrRac, - specNamj, ) if err != nil { @@ -292,8 +285,8 @@ func TestNewCISInvoice(t *testing.T) { 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.NacinPlac != "G" { + t.Errorf("Expected NacinPlac G, got %v", invoice.NacinPlac) } if invoice.OibOper != oibOper { @@ -304,18 +297,6 @@ func TestNewCISInvoice(t *testing.T) { 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") @@ -342,7 +323,7 @@ func TestNewCISInvoice(t *testing.T) { t.Logf("Invoice XML: %s", xmlData) // Send test invoice to CIS with InvoiceRequest - jir, zkiR, err := testEntity.InvoiceRequest(invoice) + jir, zkiR, err := invoice.InvoiceRequest() if err != nil { t.Fatalf("Expected no error, got %v", err) @@ -378,16 +359,9 @@ func TestSimpleInvoiceFromReadme(t *testing.T) { // taxation. nil, // naknade "1250.00", // total - "G", // payment method G - cash, K - credit card, T - + CISCash, // payment method G - cash, K - credit card, T - // transfer, O - other, C - check (deprecated) "12345678901", // operator OIB - false, // late delivery, if previous attempt failed but the - // invoice was issued with just ZKI - "", // receipt book number, if the invoicing system was - // unusable and the invoice was issued manually, the - // number of the receipt book - "", // unused, reserved field for future or temporary - // unexpected use by the CIS, should be empty ) if err != nil { @@ -395,7 +369,7 @@ func TestSimpleInvoiceFromReadme(t *testing.T) { } // Send test invoice to CIS with InvoiceRequest - jir, zkiR, err := testEntity.InvoiceRequest(invoice) + jir, zkiR, err := invoice.InvoiceRequest() if err != nil { t.Fatalf("Expected no error, got %v", err) diff --git a/invoice.go b/invoice.go new file mode 100644 index 0000000..eced0fa --- /dev/null +++ b/invoice.go @@ -0,0 +1,623 @@ +package fiskalhrgo + +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 L. D. T. d.o.o. +// Copyright (c) contributors for their respective contributions. See https://github.com/l-d-t/fiskalhrgo/graphs/contributors + +import ( + "encoding/xml" + "errors" + "fmt" + "strings" + "time" +) + +// PaymentMethod defines a custom type for means of payment +type PaymentMethod string + +// Constants representing allowed values for PaymentMethod +const ( + CISCash PaymentMethod = "G" // Cash + CISCard PaymentMethod = "K" // Card + CISMixOther PaymentMethod = "O" // Mix/Other + CISBankTransfer PaymentMethod = "T" // Bank Transfer (usually not sent to CIS, not mandatory) + CISCheck PaymentMethod = "C" // Check [deprecated] +) + +// IsValid checks if PaymentMethod is one of the allowed values +func (p PaymentMethod) IsValid() error { + switch p { + case CISCash, CISCard, CISMixOther, CISBankTransfer, CISCheck: + return nil + default: + return errors.New("PaymentMethod must be one of the following values: G - Cash, K - Card, O - Mix/Other, T - Bank Transfer, C - Check (deprecated)") + } +} + +// NewCISInvoice initializes and returns a RacunType instance +// +// This method creates a new instance of RacunType, which represents an invoice with all necessary fields. +// The instance can be marshaled to XML and sent to the CIS for fiscalization. +// ALWAYS use the provided methods to set or modify the values of the RacunType instance. +// Using the provided methods is safe end ensure the correct format of the data and no discrepancies with the ZKI. +// DO NOT MODIFY the returned RacunType instance directly, as it may lead to invalid XML output or ZKI problems. +// Unsafe and unexpected results may happen if you modify the data manually and use it to send later. +// It can result in wrong data sent to CIS or discrepancy between sent data and ZKI, witch can lead to severe consequences. +// +// Parameters: +// +// - dateTime (time.Time): The date and time of the invoice. +// - centralized (bool): Indicates whether the sequence mark is centralized. +// - 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). +// - iznosOslobPdv (string): The amount exempt from VAT. +// - iznosMarza (string): The margin amount. +// - iznosNePodlOpor (string): The amount not subject to taxation. +// - naknadeValues ([][]string): A 2D array for fees details (nullable). +// - iznosUkupno (string): The total amount. +// - paymentMethod (string): The payment method. +// - oibOper (string): The OIB of the operator. +// - attachedDocumentJIRorZKI (string): The JIR or ZKI of the attached document (empty if no attached document). +// +// Returns: +// +// (*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, + invoiceNumber uint, + registerDeviceID uint, + pdvValues [][]interface{}, + pnpValues [][]interface{}, + ostaliPorValues [][]interface{}, + iznosOslobPdv string, + iznosMarza string, + iznosNePodlOpor string, + naknadeValues [][]string, + iznosUkupno string, + paymentMethod PaymentMethod, + oibOper string, +) (*RacunType, string, error) { + // Format the date and time + formattedDate := dateTime.Format("02.01.2006T15:04:05") + + // Determine the sequence mark + oznSlijed := "N" + if fe.centralizedInvoiceNumber { + oznSlijed = "P" + } + + if !IsValidCurrencyFormat(iznosUkupno) { + return nil, "", errors.New("the total amount must be a valid currency format") + } + + if !IsValidCurrencyFormat(iznosOslobPdv) { + return nil, "", errors.New("the amount exempt from VAT must be a valid currency format") + } + + if !IsValidCurrencyFormat(iznosMarza) { + return nil, "", errors.New("the margin amount must be a valid currency format") + } + + if !IsValidCurrencyFormat(iznosNePodlOpor) { + return nil, "", errors.New("the amount not subject to taxation must be a valid currency format") + } + + if iznosOslobPdv == "0.00" { + iznosOslobPdv = "" + } + if iznosMarza == "0.00" { + iznosMarza = "" + } + if iznosNePodlOpor == "0.00" { + iznosNePodlOpor = "" + } + + // Use helper functions to create the necessary types + var pdv *PdvType + var err error + if pdvValues != nil { + pdv, err = newPdv(pdvValues) + if err != nil { + return nil, "", err + } + } + + var pnp *PorezNaPotrosnjuType + if pnpValues != nil { + pnp, err = newPNP(pnpValues) + if err != nil { + return nil, "", err + } + } + + var ostaliPor *OstaliPoreziType + if ostaliPorValues != nil { + ostaliPor, err = otherTaxes(ostaliPorValues) + if err != nil { + return nil, "", err + } + } + + var naknade *NaknadeType + if naknadeValues != nil { + naknade, err = genNaknade(naknadeValues) + if err != nil { + return nil, "", err + } + } + + // Create the BrojRacunaType instance + brRac := &BrojRacunaType{ + BrOznRac: invoiceNumber, + OznPosPr: fe.locationID, + 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] + err = paymentMethod.IsValid() + if err != nil { + return nil, "", err + } + + zki, err := fe.GenerateZKI(dateTime, invoiceNumber, registerDeviceID, iznosUkupno) + + if err != nil { + return nil, "", err + } + + return &RacunType{ + Oib: fe.oib, + USustPdv: fe.sustPDV, + DatVrijeme: formattedDate, + OznSlijed: oznSlijed, + BrRac: brRac, + Pdv: pdv, + Pnp: pnp, + OstaliPor: ostaliPor, + IznosOslobPdv: iznosOslobPdv, + IznosMarza: iznosMarza, + IznosNePodlOpor: iznosNePodlOpor, + Naknade: naknade, + IznosUkupno: iznosUkupno, + NacinPlac: string(paymentMethod), + OibOper: oibOper, + ZastKod: zki, + NakDost: false, + pointerToEntity: fe, + oldEntityForOldZKI: nil, + }, zki, nil +} + +func (invoice *RacunType) GetZKI() string { + return invoice.ZastKod +} + +func (invoice *RacunType) GetOib() string { + return invoice.Oib +} + +// Set late delivery to true, and set the ZKI you pass from saved data when you issued the invoice to customer +// Don't worry the ZKI you set will be validated with the current certificate before sending unless to set +// IhaveZKIwithExpiredCertificateEdgeCase method then the old certificate provided will be used to validate the ZKI +// +// So just set the ZKI you got from the invoice you issued to the customer, +// and the system will validate it with the current certificate +func (invoice *RacunType) SetLateDelivery(ZKI string) error { + invoice.ZastKod = ZKI + invoice.NakDost = true + + invoiceTime, err := time.Parse("02.01.2006T15:04:05", invoice.DatVrijeme) + if err != nil { + return fmt.Errorf("failed to parse date: %w", err) + } + + // Validate the ZKI with the current certificate + calculatedZKI, err := invoice.pointerToEntity.GenerateZKI(invoiceTime, uint(invoice.BrRac.BrOznRac), uint(invoice.BrRac.OznNapUr), invoice.IznosUkupno) + + if err != nil { + return fmt.Errorf("failed to generate ZKI: %w", err) + } + + if calculatedZKI != invoice.ZastKod { + return errors.New("ZKI is not valid") + } + + return nil +} + +// IhaveZKIwithExpiredCertificateEdgeCase sets the old FiskalEntity instance for the old ZKI verification +// This is used in the edge case that the ZKI was generated with one certificate and the fiscalization failed +// But the certificate expired or had to be changed and now fiscalization have to be repeated with new certificate +// If we replace the original ZKI its a problem we already gave the invoice with old ZKI out +// So we have to keep the old ZKI and validate it with the old certificate before signing and sending with new one +func (invoice *RacunType) IhaveZKIwithExpiredCertificateEdgeCase(oldZKI string, oldCertPath string, oldCertPassword string) error { + invoice.ZastKod = oldZKI + invoice.NakDost = true + + // Create a new old FiskalEntity + oldFiskalEntity, err := NewFiskalEntity( + invoice.pointerToEntity.oib, + invoice.pointerToEntity.sustPDV, + invoice.pointerToEntity.locationID, + invoice.pointerToEntity.centralizedInvoiceNumber, + invoice.pointerToEntity.demoMode, + false, + oldCertPath, + oldCertPassword, + ) + if err != nil { + return fmt.Errorf("failed to create FiskalEntity: %v", err) + } + + invoice.oldEntityForOldZKI = oldFiskalEntity + + invoiceTime, err := time.Parse("02.01.2006T15:04:05", invoice.DatVrijeme) + if err != nil { + return fmt.Errorf("failed to parse date: %w", err) + } + + // Validate the ZKI with the current certificate + calculatedZKI, err := invoice.oldEntityForOldZKI.GenerateZKI(invoiceTime, uint(invoice.BrRac.BrOznRac), uint(invoice.BrRac.OznNapUr), invoice.IznosUkupno) + + if err != nil { + return fmt.Errorf("failed to generate ZKI: %w", err) + } + + if calculatedZKI != invoice.ZastKod { + return errors.New("ZKI is not valid") + } + + 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 inspections 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 according to you business 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 (invoice *RacunType) InvoiceRequest() (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") + } + + //check ZKI + invoiceTime, err := time.Parse("02.01.2006T15:04:05", invoice.DatVrijeme) + if err != nil { + return "", invoice.ZastKod, fmt.Errorf("failed to parse date: %w", err) + } + + var chkEntity *FiskalEntity + if invoice.oldEntityForOldZKI != nil { + chkEntity = invoice.oldEntityForOldZKI + } else { + chkEntity = invoice.pointerToEntity + } + + // Validate the ZKI with the old certificate + calculatedZKI, err := chkEntity.GenerateZKI(invoiceTime, uint(invoice.BrRac.BrOznRac), uint(invoice.BrRac.OznNapUr), invoice.IznosUkupno) + + if err != nil { + return "", invoice.ZastKod, fmt.Errorf("failed to check ZKI: %w", err) + } + + if calculatedZKI != invoice.ZastKod { + return "", invoice.ZastKod, errors.New("ZKI is not valid") + } + + //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 := invoice.pointerToEntity.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 ValidateJIR(racunOdgovor.Jir) { + return racunOdgovor.Jir, invoice.ZastKod, nil + } else { + return "", invoice.ZastKod, errors.New("JIR is not valid") + } + } + + // Add a default return statement to handle unexpected cases + return "", invoice.ZastKod, errors.New("unexpected error") +} + +// genNaknade initializes and returns a NaknadeType instance +// +// This function creates a new instance of NaknadeType, which represents a collection of fees (NaknadaType) entries. +// It takes a 2D array of values where each inner array represents a single fee entry with the name and amount. +// +// Parameters: +// +// values ([][]string): A 2D array where each inner array contains two elements: +// - string: The name of the fee (NazivN) +// - string: The amount of the fee (IznosN) +// +// Returns: +// +// (*NaknadeType, error): A pointer to a new NaknadeType instance with the provided fee entries, or an error if the input is invalid. +// +// Example: +// +// values := [][]string{ +// {"Service Fee", "100"}, +// {"Delivery Fee", "50"}, +// } +// naknade, err := genNaknade(values) +// if err != nil { +// fmt.Printf("Error: %v\n", err) +// } else { +// fmt.Printf("genNaknade: %+v\n", naknade) +// } +func genNaknade(values [][]string) (*NaknadeType, error) { + naknade := make([]*NaknadaType, len(values)) + for i, v := range values { + if len(v) != 2 { + return nil, errors.New("each inner array must contain exactly two elements") + } + feeName := v[0] + feeAmount := v[1] + if !IsValidCurrencyFormat(feeAmount) { + return nil, errors.New("the second element of each inner array must be a valid currency format (fee amount)") + } + naknade[i] = &NaknadaType{NazivN: feeName, IznosN: feeAmount} + } + return &NaknadeType{Naknada: naknade}, nil +} + +// otherTaxes initializes and returns an OstaliPoreziType instance +// +// This function creates a new instance of OstaliPoreziType, which represents a collection of other taxes (PorezOstaloType) entries. +// It takes a 2D array of values where each inner array represents a single other tax entry with the name, rate, base, and amount. +// +// Parameters: +// +// values ([][]interface{}): A 2D array where each inner array contains four elements: +// - string: The name of the tax (Naziv) +// - int: The tax rate (Stopa) +// - string: The tax base (Osnovica) +// - string: The tax amount (Iznos) +// +// Returns: +// +// (*OstaliPoreziType, error): A pointer to a new OstaliPoreziType instance with the provided other tax entries, or an error if the input is invalid. +// +// Example: +// +// values := [][]interface{}{ +// {"Other Tax", 5, "1000", "50"}, +// } +// ostaliPorezi, err := otherTaxes(values) +// if err != nil { +// fmt.Printf("Error: %v\n", err) +// } else { +// fmt.Printf("OstaliPorezi: %+v\n", ostaliPorezi) +// } +func otherTaxes(values [][]interface{}) (*OstaliPoreziType, error) { + porezi := make([]*PorezOstaloType, len(values)) + for i, v := range values { + if len(v) != 4 { + return nil, errors.New("each inner array must contain exactly four elements") + } + name, ok := v[0].(string) + if !ok { + return nil, errors.New("the first element of each inner array must be a string (name)") + } + rate, ok := v[1].(string) + if !ok { + return nil, errors.New("the second element of each inner array must be an int (rate)") + } + base, ok := v[2].(string) + if !ok { + return nil, errors.New("the third element of each inner array must be a string (base)") + } + if !IsValidCurrencyFormat(base) { + return nil, errors.New("the third element of each inner array must be a valid currency format (base)") + } + amount, ok := v[3].(string) + if !ok { + return nil, errors.New("the fourth element of each inner array must be a string (amount)") + } + if !IsValidCurrencyFormat(amount) { + return nil, errors.New("the fourth element of each inner array must be a valid currency format (amount)") + } + porezi[i] = &PorezOstaloType{Naziv: name, Stopa: rate, Osnovica: base, Iznos: amount} + } + return &OstaliPoreziType{Porez: porezi}, nil +} + +// newPNP initializes and returns a PorezNaPotrosnjuType instance +// +// This function creates a new instance of PorezNaPotrosnjuType, which represents a collection of consumption tax (PorezType) entries. +// It takes a 2D array of values where each inner array represents a single consumption tax entry with the tax rate, tax base, and tax amount. +// +// Parameters: +// +// values ([][]interface{}): A 2D array where each inner array contains three elements: +// - int: The tax rate (Stopa) +// - string: The tax base (Osnovica) +// - string: The tax amount (Iznos) +// +// Returns: +// +// (*PorezNaPotrosnjuType, error): A pointer to a new PorezNaPotrosnjuType instance with the provided consumption tax entries, or an error if the input is invalid. +// +// Example: +// +// values := [][]interface{}{ +// {3, "1000", "30"}, +// {5, "2000", "100"}, +// } +// pnp, err := newPNP(values) +// if err != nil { +// fmt.Printf("Error: %v\n", err) +// } else { +// fmt.Printf("PorezNaPotrosnju: %+v\n", pnp) +// } +func newPNP(values [][]interface{}) (*PorezNaPotrosnjuType, error) { + porezi := make([]*PorezType, len(values)) + for i, v := range values { + if len(v) != 3 { + return nil, errors.New("each inner array must contain exactly three elements") + } + taxRate, ok := v[0].(string) + if !ok { + return nil, errors.New("the first element of each inner array must be an int (tax rate)") + } + taxBase, ok := v[1].(string) + if !ok { + return nil, errors.New("the second element of each inner array must be a string (tax base)") + } + if !IsValidCurrencyFormat(taxBase) { + return nil, errors.New("the second element of each inner array must be a valid currency format (tax base)") + } + taxAmount, ok := v[2].(string) + if !ok { + return nil, errors.New("the third element of each inner array must be a string (tax amount)") + } + if !IsValidCurrencyFormat(taxAmount) { + return nil, errors.New("the third element of each inner array must be a valid currency format (tax amount)") + } + porezi[i] = &PorezType{Stopa: taxRate, Osnovica: taxBase, Iznos: taxAmount} + } + return &PorezNaPotrosnjuType{Porez: porezi}, nil +} + +// newPdv initializes and returns a PdvType instance +// +// This function creates a new instance of PdvType, which represents a collection of VAT (PorezType) entries. +// It takes a 2D array of values where each inner array represents a single VAT entry with the tax rate, tax base, and tax amount. +// +// Parameters: +// +// values ([][]interface{}): A 2D array where each inner array contains three elements: +// - int: The tax rate (Stopa) +// - string: The tax base (Osnovica) +// - string: The tax amount (Iznos) +// +// Returns: +// +// (*PdvType, error): A pointer to a new PdvType instance with the provided VAT entries, or an error if the input is invalid. +// +// Example: +// +// values := [][]interface{}{ +// {25, "1000", "250"}, +// {13, "500", "65"}, +// } +// pdv, err := newPdv(values) +// if err != nil { +// fmt.Printf("Error: %v\n", err) +// } else { +// fmt.Printf("Pdv: %+v\n", pdv) +// } +func newPdv(values [][]interface{}) (*PdvType, error) { + porezi := make([]*PorezType, len(values)) + for i, v := range values { + if len(v) != 3 { + return nil, errors.New("each inner array must contain exactly three elements") + } + taxRate, ok := v[0].(string) + if !ok { + return nil, errors.New("the first element of each inner array must be an int (tax rate)") + } + taxBase, ok := v[1].(string) + if !ok { + return nil, errors.New("the second element of each inner array must be a string (tax base)") + } + if !IsValidCurrencyFormat(taxBase) { + return nil, errors.New("the second element of each inner array must be a valid currency format (tax base)") + } + taxAmount, ok := v[2].(string) + if !ok { + return nil, errors.New("the third element of each inner array must be a string (tax amount)") + } + if !IsValidCurrencyFormat(taxAmount) { + return nil, errors.New("the third element of each inner array must be a valid currency format (tax amount)") + } + porezi[i] = &PorezType{Stopa: taxRate, Osnovica: taxBase, Iznos: taxAmount} + } + return &PdvType{Porez: porezi}, nil +}