diff --git a/MAES.Fiskal.Example/Program.cs b/MAES.Fiskal.Example/Program.cs index 8cb021d..7e6f0a5 100644 --- a/MAES.Fiskal.Example/Program.cs +++ b/MAES.Fiskal.Example/Program.cs @@ -16,14 +16,15 @@ Oib = "18945722090", // Identification number of company OibOper = "18945722090", // Odentitfication numer of person operating POS OznSlijed = OznakaSlijednostiType.N, - Pdv = [ // Taxes list - new () - { - Stopa = "25.00", // Tax percentage (must be format 0.00) - Osnovica = "10.00", // Tax base (must be format 0.00) - Iznos = "2.50" // Tax amount (must be format 0.00) - } - ], + Pdv = new PorezType[] + { // Taxes list + new PorezType + { + Stopa = "25.00", // Tax percentage (must be format 0.00) + Osnovica = "10.00", // Tax base (must be format 0.00) + Iznos = "2.50" // Tax amount (must be format 0.00) + } + }, USustPdv = true, // Does company falls under tax obligation laws NacinPlac = NacinPlacanjaType.G // Type of payment (G - Cash, K - Cards, etc...) }; diff --git a/MAES.Fiskal.Tests/FiskalTests.cs b/MAES.Fiskal.Tests/FiskalTests.cs index 68bcc09..3967a33 100644 --- a/MAES.Fiskal.Tests/FiskalTests.cs +++ b/MAES.Fiskal.Tests/FiskalTests.cs @@ -44,7 +44,7 @@ public class FiskalTests Oib = "18945722090", OibOper = "18945722090", OznSlijed = OznakaSlijednostiType.N, - Pdv = [ new() { Stopa = "25.00", Osnovica = "80.00", Iznos = "20.00" } ], + Pdv = new PorezType[] { new PorezType { Stopa = "25.00", Osnovica = "80.00", Iznos = "20.00" } }, USustPdv = true, NacinPlac = NacinPlacanjaType.G }; diff --git a/MAES.Fiskal/MAES.Fiskal.csproj b/MAES.Fiskal/MAES.Fiskal.csproj index a6c9f30..d089347 100644 --- a/MAES.Fiskal/MAES.Fiskal.csproj +++ b/MAES.Fiskal/MAES.Fiskal.csproj @@ -1,16 +1,16 @@  - net8.0 - + netstandard2.0 enable enable + 10.0 true true MAES.Fiskal - 1.2.0 + 1.3.0 Roko Tomović MAES This is a wrapper for invoice fiscalization in Republic of Croatia using C# and .NET 8+ @@ -22,9 +22,10 @@ - - - + + + + diff --git a/MAES.Fiskal/ReferenceTypeExtensions.cs b/MAES.Fiskal/ReferenceTypeExtensions.cs index 7e3361d..4b4bf43 100644 --- a/MAES.Fiskal/ReferenceTypeExtensions.cs +++ b/MAES.Fiskal/ReferenceTypeExtensions.cs @@ -6,6 +6,7 @@ using System.Text; using System.Xml; using System.Xml.Serialization; +using System.Numerics; namespace MAES.Fiskal; @@ -40,8 +41,8 @@ public static class ReferenceTypeExtensions /// public static async Task SendAsync(this RacunType invoice, X509Certificate2 certificate, string url) { - ArgumentNullException.ThrowIfNull(invoice); - ArgumentNullException.ThrowIfNull(certificate); + if (invoice is null) throw new ArgumentNullException(nameof(invoice)); + if (certificate is null) throw new ArgumentNullException(nameof(certificate)); if (string.IsNullOrEmpty(invoice.ZastKod)) invoice.ZastKod = invoice.ZKI(certificate); @@ -70,8 +71,8 @@ public static async Task SendAsync(this RacunType invoice, X509Cer /// public async static Task SendAsync(this RacunNapojnicaType invoiceTip, X509Certificate2 certificate, string url) { - ArgumentNullException.ThrowIfNull(invoiceTip); - ArgumentNullException.ThrowIfNull(certificate); + if (invoiceTip is null) throw new ArgumentNullException(nameof(invoiceTip)); + if (certificate is null) throw new ArgumentNullException(nameof(certificate)); if (string.IsNullOrEmpty(invoiceTip.ZastKod)) invoiceTip.ZastKod = invoiceTip.ZKI(certificate); @@ -130,11 +131,12 @@ public static RacunNapojnicaType ToRacunNapojnicaType(this RacunType invoice, Na /// ZKI string (Maybe GUID idk...) public static string ZKI(this RacunType invoice, X509Certificate2 certificate) { - ArgumentNullException.ThrowIfNull(certificate); + if (certificate is null) throw new ArgumentNullException(nameof(certificate)); var b = Encoding.ASCII.GetBytes(invoice.Oib + invoice.DatVrijeme + invoice.BrRac.BrOznRac + invoice.BrRac.OznPosPr + invoice.BrRac.OznNapUr + invoice.IznosUkupno); var signData = (certificate.GetRSAPrivateKey()?.SignData(b, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1)) ?? throw new Exception("Invalid cerrtificate. No RSA Private key."); - return new string([.. MD5.HashData(signData).SelectMany(x => x.ToString("x2"))]); + var hash = MD5.Create().ComputeHash(signData); + return string.Concat(hash.Select(x => x.ToString("x2"))); } /// @@ -145,11 +147,12 @@ public static string ZKI(this RacunType invoice, X509Certificate2 certificate) /// ZKI string (Maybe GUID idk...) public static string ZKI(this RacunNapojnicaType invoiceTip, X509Certificate2 certificate) { - ArgumentNullException.ThrowIfNull(certificate); + if (certificate is null) throw new ArgumentNullException(nameof(certificate)); var b = Encoding.ASCII.GetBytes(invoiceTip.Oib + invoiceTip.DatVrijeme + invoiceTip.BrRac.BrOznRac + invoiceTip.BrRac.OznPosPr + invoiceTip.BrRac.OznNapUr + invoiceTip.IznosUkupno); var signData = (certificate.GetRSAPrivateKey()?.SignData(b, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1)) ?? throw new Exception("Invalid cerrtificate. No RSA Private key."); - return new string([.. MD5.HashData(signData).SelectMany(x => x.ToString("x2"))]); + var hash2 = MD5.Create().ComputeHash(signData); + return string.Concat(hash2.Select(x => x.ToString("x2"))); } static void sign(dynamic request, X509Certificate2 certificate) @@ -191,58 +194,66 @@ static void sign(dynamic request, X509Certificate2 certificate) var s = xml.Signature; - if (keyInfoData.IssuerSerials[0] is not X509IssuerSerial serial) throw new Exception("There is no issuer serial in supplied certificate"); + // Use certificate values directly (avoids depending on internal X509IssuerSerial type) + var certIssuerName = certificate.Issuer; + // Convert hex serial to decimal string for XML schema compatibility + string HexToDecimalString(string hex) + { + if (string.IsNullOrEmpty(hex)) return string.Empty; + var bytes = Enumerable.Range(0, hex.Length) + .Where(i => i % 2 == 0) + .Select(i => Convert.ToByte(hex.Substring(i, 2), 16)) + .ToArray(); + var little = bytes.Reverse().ToArray(); + var bigInt = new BigInteger(little.Concat(new byte[] { 0 }).ToArray()); + return bigInt.ToString(); + } + + var certSerialNumber = HexToDecimalString(certificate.GetSerialNumberString()); - var certSerial = serial; request.Signature = new SignatureType { SignedInfo = new SignedInfoType { CanonicalizationMethod = new CanonicalizationMethodType { Algorithm = s.SignedInfo.CanonicalizationMethod }, SignatureMethod = new SignatureMethodType { Algorithm = s.SignedInfo.SignatureMethod }, - Reference = - (from x in s.SignedInfo.References.OfType() - select new ReferenceType - { - URI = x.Uri, - Transforms = - (from t in transforms - select new TransformType { Algorithm = t.Algorithm }).ToArray(), - DigestMethod = new DigestMethodType { Algorithm = x.DigestMethod }, - DigestValue = x.DigestValue - }).ToArray() + Reference = (from x in s.SignedInfo.References.OfType() + select new ReferenceType + { + URI = x.Uri, + Transforms = (from t in transforms + select new TransformType { Algorithm = t.Algorithm }).ToArray(), + DigestMethod = new DigestMethodType { Algorithm = x.DigestMethod }, + DigestValue = x.DigestValue + }).ToArray() }, SignatureValue = new SignatureValueType { Value = s.SignatureValue }, KeyInfo = new KeyInfoType { - ItemsElementName = [ItemsChoiceType2.X509Data], - Items = - [ + ItemsElementName = new ItemsChoiceType2[] { ItemsChoiceType2.X509Data }, + Items = new object[] + { new X509DataType { - ItemsElementName = - [ - ItemsChoiceType.X509IssuerSerial, - ItemsChoiceType.X509Certificate - ], - Items = - [ + ItemsElementName = new ItemsChoiceType[] { ItemsChoiceType.X509IssuerSerial, ItemsChoiceType.X509Certificate }, + Items = new object[] + { new X509IssuerSerialType { - X509IssuerName = certSerial.IssuerName, - X509SerialNumber = certSerial.SerialNumber + X509IssuerName = certIssuerName, + X509SerialNumber = certSerialNumber }, certificate.RawData - ] + } } - ] + } } }; } static void throwOnResponseErrors(dynamic response) { - if (response.Greske is not GreskaType[] greske || greske.Length != 0) return; + if (response.Greske is not GreskaType[] greske || greske.Length == 0) return; throw new Exception($"Greška u fiskalizaciji: {string.Join("\n", greske.Select(x => $"{x.SifraGreske}: {x.PorukaGreske}"))}"); } } \ No newline at end of file diff --git a/README.md b/README.md index e76a862..4ce4061 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,148 @@ -# MAES.Fiskal +# MAES.Fiskal -[![CI/CD](https://github.com/MAES-Software/MAES.Fiskal/actions/workflows/main.yml/badge.svg)](https://github.com/MAES-Software/MAES.Fiskal/actions/workflows/main.yml) +[![CI/CD](https://github.com/MAES-Software/MAES.Fiskal/actions/workflows/dotnet-ci.yml/badge.svg)](https://github.com/MAES-Software/MAES.Fiskal/actions/workflows/dotnet-ci.yml) [![Contributors](https://img.shields.io/github/contributors/MAES-Software/MAES.Fiskal)](https://github.com/MAES-Software/MAES.Fiskal/graphs/contributors) -[![Forks](https://img.shields.io/github/forks/MAES-Software/MAES.Fiskal)](https://github.com/MAES-Software/MAES.Fiskal/network/members) -[![Stars](https://img.shields.io/github/stars/MAES-Software/MAES.Fiskal)](https://github.com/MAES-Software/MAES.Fiskal/stargazers) [![Issues](https://img.shields.io/github/issues/MAES-Software/MAES.Fiskal)](https://github.com/MAES-Software/MAES.Fiskal/issues) -[![License](https://img.shields.io/github/license/MAES-Software/MAES.Fiskal)](https://github.com/MAES-Software/MAES.Fiskal/LICENSE) -[![LinkedIn](https://img.shields.io/badge/LinkedIn-Profile-0077B5?logo=linkedin&logoColor=white)](https://www.linkedin.com/company/maes-software/) [![NuGet](https://img.shields.io/nuget/v/MAES.Fiskal.svg)](https://www.nuget.org/packages/MAES.Fiskal/) [![NuGet Downloads](https://img.shields.io/nuget/dt/MAES.Fiskal)](https://www.nuget.org/packages/MAES.Fiskal/) -**MAES.Fiskal** is a fiscalization tool for invoices developed in **C#** using **.NET 8**. It enables automatic generation and submission of fiscal data according to current regulations. +## Overview -Latest version: 1.2.0 +**MAES.Fiskal** is a .NET fiscalization library for Croatian invoicing. It supports invoice creation, ZKI generation, and fiscalization service submission. + +## Features + +- Create fiscal invoices with `RacunType` +- Generate ZKI signatures for fiscal documents +- Submit invoices and tips to the fiscalization service +- Support for certificate-based authentication ## Requirements -1. .NET 8+ -2. Fiscalization WSDL ver. 2.6 -## Features -- ZKI Generation -- Invoice fiscalization -- Tip fiscalization -- Free and open source +- .NET 8+ +- Fiscalization WSDL version 2.6 ## Installation -**Nuget:** https://www.nuget.org/packages/MAES.Fiskal -or +Install from NuGet: + +```bash +dotnet add package MAES.Fiskal +``` + +Or clone the repository: ```bash git clone https://github.com/MAES-Software/MAES.Fiskal.git ``` -### Define endpoint address (Url) -1. Demo endpoint - ```csharp - string url = "https://cistest.apis-it.hr:8449/FiskalizacijaServiceTest"; - ``` -2. Poduction endpoint - ```csharp - string url = "https://cis.porezna-uprava.hr:8449/FiskalizacijaService"; - ``` - -### Load X509Certificate2 -1. From file (You can use relative path eg. "./cert.p12") - ```csharp - var certificate = new X509Certificate2("filename"); - ``` -2. From some data stream with byte[] bytes - ```csharp - var certificate = new X509Certificate2(bytes); - ``` - -You must also supply password for given certificate if it has one (by default certificates given from goverment are locked but they can be repackaged) +## Configuration + +### Fiscalization endpoint + +Use the test endpoint during development: + ```csharp -var certificate = new X509Certificate2("filename", "password"); +string url = "https://cistest.apis-it.hr:8449/FiskalizacijaServiceTest"; ``` -## Usage Example +Use the production endpoint in live environments: + +```csharp +string url = "https://cis.porezna-uprava.hr:8449/FiskalizacijaService"; +``` + +### Load X509 certificate + +Load a certificate from a file: + +```csharp +var certificate = new X509Certificate2("cert.p12", "password"); +``` + +Load from raw bytes: + +```csharp +var certificate = new X509Certificate2(bytes, "password"); +``` + +## Usage + +Add the library namespace: -### Define using MAES.Fiskal to get all classes for fiscalization ```csharp using MAES.Fiskal; ``` -### Sending invoice -1. Create invoice - ```csharp - var invoice = new RacunType +### Create and send an invoice + +```csharp +var invoice = new RacunType +{ + BrRac = new BrojRacunaType { - BrRac = new BrojRacunaType - { - BrOznRac = "1", // Invoice number (incremental for each receipt) - OznPosPr = "POSL1", // Workspace code - OznNapUr = "1" // Cash reegister number - }, - DatVrijeme = DateTime.Now.ToString("dd.MM.yyyyTHH:mm:ss"), // DateTime of invoice - IznosUkupno = "12.50", // Total amount (must be format 0.00) - NakDost = false, - Oib = "51560545524", // Identification number of company - OibOper = "51560545524", // Odentitfication numer of person operating POS - OznSlijed = OznakaSlijednostiType.N, - Pdv = [ // Taxes list - new () - { - Stopa = "25.00", // Tax percentage (must be format 0.00) - Osnovica = "10.00", // Tax base (must be format 0.00) - Iznos = "2.50" // Tax amount (must be format 0.00) - } - ], - Pnp = [], // Fill tax on spending if nececary :S (if empty it this must not be present or fucking xml will start retardmaxxing) - USustPdv = true, // Does company falls under tax obligation laws - NacinPlac = NacinPlacanjaType.G // Type of payment (G - Cash, K - Cards, etc...) - }; - ``` -2. Send invoice - ```csharp - // Call to service - var res = await invoice.SendAsync(certificate, url); - - // Check if there are errors - if(res.Greske.Length != 0) Console.WriteLine(res.Greske.Join(',')); - - // Get jir to store - string jir = res.Jir; - ``` - -### Sending invoice tip -1. Create RacunNapojnicaType from RacunType with NapojnicaTypee as parameter - ```csharp - RacunNapojnicaType invoiceTip = invoice.ToInvoiceTipAsnyc(new () + BrOznRac = "1", + OznPosPr = "POSL1", + OznNapUr = "1" + }, + DatVrijeme = DateTime.Now.ToString("dd.MM.yyyyTHH:mm:ss"), + IznosUkupno = "12.50", + NakDost = false, + Oib = "51560545524", + OibOper = "51560545524", + OznSlijed = OznakaSlijednostiType.N, + Pdv = new[] { - iznosNapojnice = "1.00", // Tip amount - nacinPlacanjaNapojnice = NacinPlacanjaType.G // Tip type of payment (G - Cash, K - Cards, etc...) - }); - ``` + new PorezType + { + Stopa = "25.00", + Osnovica = "10.00", + Iznos = "2.50" + } + }, + USustPdv = true, + NacinPlac = NacinPlacanjaType.G +}; + +var response = await invoice.SendAsync(certificate, url); + +if (response.Greske.Length > 0) +{ + Console.WriteLine(string.Join(", ", response.Greske.Select(g => g.PorukaGreske))); +} + +string jir = response.Jir; +``` -2. Send RacunNapojnicaType - ```csharp - // Call to service - var res = await invoiceTip.SendAsync(certificate, url); +> If you do not need consumption tax (`Pnp`), omit the property entirely. An empty `Pnp` collection may produce invalid XML. - // Check if there are errors - if(res.Greske.Length != 0) Console.WriteLine(res.Greske.Join(',')); - ``` +### Create and send a tip invoice + +```csharp +var tipData = new NapojnicaType +{ + iznosNapojnice = "1.00", + nacinPlacanjaNapojnice = NacinPlacanjaType.G +}; + +var invoiceTip = invoice.ToRacunNapojnicaType(tipData); +var tipResponse = await invoiceTip.SendAsync(certificate, url); + +if (tipResponse.Greske.Length > 0) +{ + Console.WriteLine(string.Join(", ", tipResponse.Greske.Select(g => g.PorukaGreske))); +} +``` ### Generate ZKI + ```csharp string zki = invoice.ZKI(certificate); ``` -> Both invoice and invoiceTip have .ZKI(certificate) methods -### Disabling SSL Certificate Validation (Not Recommended) +## SSL validation -If you encounter issues with SSL certificate validation, you can disable certificate checks as follows: +If you encounter certificate validation issues during development, you can temporarily disable SSL validation: ```csharp ReferenceTypeExtensions.SslCertificateAuthentication = new() @@ -144,4 +152,8 @@ ReferenceTypeExtensions.SslCertificateAuthentication = new() }; ``` -> **Warning:** Disabling SSL certificate validation is **not recommended** for production environments, as it reduces security and exposes your application to potential risks. Use this option only for testing or troubleshooting purposes. \ No newline at end of file +> **Warning:** Disabling SSL validation is not recommended for production. Use only for local testing or troubleshooting. + +## Contribution + +Contributions are welcome. Please open an issue or submit a pull request on GitHub.