Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions MAES.Fiskal.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
};
Expand Down
2 changes: 1 addition & 1 deletion MAES.Fiskal.Tests/FiskalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
13 changes: 7 additions & 6 deletions MAES.Fiskal/MAES.Fiskal.csproj
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- <TargetFramework>netstandard2.0</TargetFramework> -->
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>

<!-- NuGet Metadata -->
<PackageId>MAES.Fiskal</PackageId>
<Version>1.2.0</Version>
<Version>1.3.0</Version>
<Authors>Roko Tomović</Authors>
<Company>MAES</Company>
<Description>This is a wrapper for invoice fiscalization in Republic of Croatia using C# and .NET 8+</Description>
Expand All @@ -22,9 +22,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.ServiceModel.Duplex" Version="6.0.0" />
<PackageReference Include="System.ServiceModel.Http" Version="6.0.0" />
<PackageReference Include="System.ServiceModel.Primitives" Version="6.0.0" />
<PackageReference Include="System.ServiceModel.Duplex" Version="4.8.1" />
<PackageReference Include="System.ServiceModel.Http" Version="4.8.1" />
<PackageReference Include="System.ServiceModel.Primitives" Version="4.8.1" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<None Include="../README.md" Pack="true" PackagePath="\"/>
<None Include="../LICENSE.md" Pack="true" PackagePath="" />
</ItemGroup>
Expand Down
83 changes: 47 additions & 36 deletions MAES.Fiskal/ReferenceTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Numerics;

namespace MAES.Fiskal;

Expand Down Expand Up @@ -40,8 +41,8 @@ public static class ReferenceTypeExtensions
/// <exception cref="ArgumentNullException"></exception>
public static async Task<RacunOdgovor> 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);

Expand Down Expand Up @@ -70,8 +71,8 @@ public static async Task<RacunOdgovor> SendAsync(this RacunType invoice, X509Cer
/// <exception cref="ArgumentNullException"></exception>
public async static Task<napojnicaResponse> 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);

Expand Down Expand Up @@ -130,11 +131,12 @@ public static RacunNapojnicaType ToRacunNapojnicaType(this RacunType invoice, Na
/// <returns>ZKI string (Maybe GUID idk...)</returns>
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")));
}

/// <summary>
Expand All @@ -145,11 +147,12 @@ public static string ZKI(this RacunType invoice, X509Certificate2 certificate)
/// <returns>ZKI string (Maybe GUID idk...)</returns>
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)
Expand Down Expand Up @@ -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<Reference>()
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<Reference>()
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}"))}");
}
}
Loading
Loading