diff --git a/MAES.Fiskal2/IPosrednik.cs b/MAES.Fiskal2/IPosrednik.cs index 17a561f..2fb4490 100644 --- a/MAES.Fiskal2/IPosrednik.cs +++ b/MAES.Fiskal2/IPosrednik.cs @@ -9,6 +9,7 @@ namespace MAES.Fiskal2; [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(Super), "Super")] [JsonDerivedType(typeof(EPoslovanje), "EPoslovanje")] +[JsonDerivedType(typeof(Fina), "Fina")] public interface IPosrednik { /// diff --git a/MAES.Fiskal2/IzlazniERacun.cs b/MAES.Fiskal2/IzlazniERacun.cs index 6c79e3e..3ad4504 100644 --- a/MAES.Fiskal2/IzlazniERacun.cs +++ b/MAES.Fiskal2/IzlazniERacun.cs @@ -8,7 +8,7 @@ public class IzlazniERacun /// /// Jedinstveni identifikator računa u sustavu posrednika. /// - public Guid Guid { get; set; } + public string Id { get; set; } = ""; /// /// Redni broj računa. diff --git a/MAES.Fiskal2/MAES.Fiskal2.csproj b/MAES.Fiskal2/MAES.Fiskal2.csproj index 5abbd14..a61df78 100644 --- a/MAES.Fiskal2/MAES.Fiskal2.csproj +++ b/MAES.Fiskal2/MAES.Fiskal2.csproj @@ -10,7 +10,7 @@ MAES.Fiskal2 - 0.1.3 + 0.2.0 Roko Tomović MAES C# biblioteka za rad s Hrvatskim fiskalnim posrednicima diff --git a/MAES.Fiskal2/Posrednici/EPoslovanje.cs b/MAES.Fiskal2/Posrednici/EPoslovanje.cs index 8652ce5..27ae26a 100644 --- a/MAES.Fiskal2/Posrednici/EPoslovanje.cs +++ b/MAES.Fiskal2/Posrednici/EPoslovanje.cs @@ -32,93 +32,169 @@ public class EPoslovanje : IPosrednik /// public string Password { get; set; } = ""; - /// - /// Dohvaća XML/UBL sadržaj ulaznog računa. Nije implementirano. - /// - public Task UlazniUBLAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); + HttpClient createClient() + { + var client = new HttpClient + { + BaseAddress = new Uri(IsDev ? URI_DEV : URI) + }; + + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + return client; + } + + async Task apiKey() + { + using var client = createClient(); + + var request = new HttpRequestMessage(HttpMethod.Post, "/api/v2/account/apikey"); + request.Content = new StringContent(JsonSerializer.Serialize(new + { + username = Username, + password = Password, + vatId = OIB, + softwareId = "MAES.Fiskal2" + }), Encoding.UTF8, "application/json"); + + var response = await client.SendAsync(request); + var json = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(json); + + using var doc = JsonDocument.Parse(json); + return doc.RootElement.GetProperty("apiKey").GetString()!; + } + + async Task sendRequest(HttpMethod method, string url, object? body, CancellationToken token) + { + using var client = createClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await apiKey()); + + var req = new HttpRequestMessage(method, url); + if (body != null) req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + + var res = await client.SendAsync(req, token); + var json = await res.Content.ReadAsStringAsync(); + + if (!res.IsSuccessStatusCode) throw new HttpRequestException(json); + + return JsonDocument.Parse(json); + } + + async Task changeStatusAsync(string id, int status, string? note = null, double? partialPaymentAmount = null, CancellationToken token = default) + { + var body = new Dictionary + { + ["status"] = status, + ["changedOn"] = DateTimeOffset.Now.ToString("O") + }; + + if (!string.IsNullOrWhiteSpace(note)) body["note"] = note; + if (partialPaymentAmount.HasValue) body["partialPaymentAmount"] = partialPaymentAmount.Value; + + await sendRequest(HttpMethod.Post, $"/api/v2/document/changestatus/{id}", body, token); + } /// - /// Dohvaća PDF sadržaj ulaznog računa. Nije implementirano. + /// Dohvaća XML/UBL sadržaj ulaznog računa. /// - public Task UlazniPdfAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); + public async Task UlazniUBLAsync(string id, CancellationToken token = default) => + (await sendRequest(HttpMethod.Get, $"/api/v2/document/get/{id}", null, token)).RootElement.GetProperty("document").GetString()!; /// - /// Dohvaća popis ulaznih e-računa. Nije implementirano. + /// Dohvaća PDF sadržaj ulaznog računa. /// - public Task> UlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) => throw new NotImplementedException(); + public async Task UlazniPdfAsync(string id, CancellationToken token = default) => + Convert.FromBase64String((await sendRequest(HttpMethod.Get, $"/api/v2/document/visualization/{id}", null, token)).RootElement.GetProperty("pdf").GetString()!); /// - /// Dohvaća XML/UBL sadržaj izlaznog računa. Nije implementirano. + /// Dohvaća popis ulaznih e-računa. /// - public Task IzlazniUBLAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); + public async Task> UlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) + { + var doc = await sendRequest(HttpMethod.Get, $"/api/v2/document/incoming?insertedFrom={from:O}&insertedTo={to:O}&limit=1000&offset=0", null, token); + + var list = new List(); + + foreach (var item in doc.RootElement.EnumerateArray()) + { + list.Add(new UlazniERacun + { + Id = item.GetProperty("id").GetInt64().ToString(), + Datum = item.GetProperty("issuedOn").GetDateTime(), + Broj = item.GetProperty("documentId").GetString()!, + Status = UlazniERacunStatus.Zaprimljeno, // TODO: ovo treba popravit + Partner = item.GetProperty("customerPartyName").GetString()!, + PartnerOIB = item.GetProperty("customerPartyVATId").GetString()! + }); + } + + return list; + } /// - /// Dohvaća PDF sadržaj izlaznog računa. Nije implementirano. + /// Dohvaća XML/UBL sadržaj izlaznog računa. /// - public Task IzlazniPdfAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); + public Task IzlazniUBLAsync(string id, CancellationToken token = default) + => UlazniUBLAsync(id, token); /// - /// Dohvaća popis izlaznih e-računa. Nije implementirano. + /// Dohvaća PDF sadržaj izlaznog računa. /// - public Task> IzlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) => throw new NotImplementedException(); + public Task IzlazniPdfAsync(string id, CancellationToken token = default) + => UlazniPdfAsync(id, token); /// - /// Evidentira UBL dokument u ePoslovanje sustav. + /// Dohvaća popis izlaznih e-računa. /// - public async Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) + public async Task> IzlazniListAsync( + DateTime from, + DateTime to, + CancellationToken token = default) { - using var client = new HttpClient(); - client.BaseAddress = new Uri(IsDev ? URI_DEV : URI); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var doc = await sendRequest(HttpMethod.Get, $"/api/v2/document/outgoing?insertedFrom={from:O}&insertedTo={to:O}&limit=1000&offset=0", null, token); - var key = await apiKey(client); + var list = new List(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(key); - - using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v2/document/send"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Content = new StringContent(JsonSerializer.Serialize(new Dictionary + foreach (var item in doc.RootElement.EnumerateArray()) { - ["document"] = ubl, - ["softwareId"] = "MAES.Blagajna" - }), Encoding.UTF8, "application/json"); - - var response = await client.SendAsync(request); - var json = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) throw new HttpRequestException($"ePoslovanje send document error: {json}"); - - // TODO: Ovo treba popravit, dohvatit id od eposlovanja + list.Add(new IzlazniERacun + { + Id = item.GetProperty("id").GetInt64().ToString(), + Broj = item.GetProperty("documentId").GetString()!, + Datum = item.GetProperty("issuedOn").GetDateTime(), + PartnerNaziv = item.GetProperty("customerPartyName").GetString()!, + PartnerOIB = item.GetProperty("customerPartyVATId").GetString()!, + Status = IzlazniERacunStatus.Poslano // TODO: ovo treba popravit + }); + } + + return list; } /// - /// Evidentira uplatu za račun. Nije implementirano. + /// Evidentira UBL dokument u ePoslovanje sustav. /// - public Task EvidentirajUplatuAsync(string id, DateTime date, double amount, NacinPlacanja paymentMethod, CancellationToken token = default) => throw new NotImplementedException(); + public async Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) => await sendRequest(HttpMethod.Post, "/api/v2/document/send", new + { + document = ubl, + softwareId = "MAES.Blagajna" + }, token); /// - /// Odbija račun. Nije implementirano. + /// Evidentira uplatu za račun. /// - public Task OdbijRacunAsync(string id, RazlogOdbijanja razlog, string opis, CancellationToken token = default) => throw new NotImplementedException(); - - async Task apiKey(HttpClient client) + public Task EvidentirajUplatuAsync(string id, DateTime date, double amount, NacinPlacanja paymentMethod, CancellationToken token = default) { - using var request = new HttpRequestMessage(HttpMethod.Post, "api/v2/account/apikey"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Content = new StringContent(JsonSerializer.Serialize(new Dictionary - { - ["username"] = Username, - ["password"] = Password, - ["vatId"] = OIB, - ["softwareId"] = "MAES.Blagajna" - }), Encoding.UTF8, "application/json"); - - var response = await client.SendAsync(request); - var json = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) throw new HttpRequestException($"{json}"); - - using var doc = JsonDocument.Parse(json); - return doc.RootElement.GetProperty("apiKey").GetString()!; + var status = amount > 0 ? 8 : 7; // 8: partialno, 7: potpuno + return changeStatusAsync(id, status, partialPaymentAmount: status == 8 ? amount : null, token: token); } + + /// + /// Odbija račun. + /// + public Task OdbijRacunAsync(string id, RazlogOdbijanja razlog, string opis, CancellationToken token = default) => + changeStatusAsync(id, status: 6, $"{razlog}: {opis}", token: token); } \ No newline at end of file diff --git a/MAES.Fiskal2/Posrednici/Fina.cs b/MAES.Fiskal2/Posrednici/Fina.cs new file mode 100644 index 0000000..0c70206 --- /dev/null +++ b/MAES.Fiskal2/Posrednici/Fina.cs @@ -0,0 +1,113 @@ +namespace MAES.Fiskal2.Posrednici; + +/// +/// Implementacija informacijskog posrednika FINA za razmjenu e-računa. +/// +public class Fina : IPosrednik +{ + /// + /// Evidentira i šalje UBL/XML dokument prema FINA e-Račun sustavu. + /// + /// UBL/XML sadržaj dokumenta. + /// Cancellation token. + /// Asinkrona operacija slanja dokumenta. + public Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Evidentira uplatu za dokument unutar FINA sustava. + /// + /// ID dokumenta u sustavu posrednika. + /// Datum i vrijeme evidentiranja uplate. + /// Iznos uplate. + /// Način plaćanja. + /// Cancellation token. + /// Asinkrona operacija evidentiranja uplate. + public Task EvidentirajUplatuAsync(string id, DateTime date, double amount, NacinPlacanja paymentMethod, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Dohvaća popis izlaznih e-računa za zadani period. + /// + /// Početni datum pretrage. + /// Završni datum pretrage. + /// Cancellation token. + /// Popis izlaznih e-računa. + public Task> IzlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Dohvaća PDF vizualizaciju izlaznog dokumenta. + /// + /// ID dokumenta. + /// Cancellation token. + /// PDF dokument kao byte array. + public Task IzlazniPdfAsync(string id, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Dohvaća UBL/XML sadržaj izlaznog dokumenta. + /// + /// ID dokumenta. + /// Cancellation token. + /// UBL/XML sadržaj dokumenta. + public Task IzlazniUBLAsync(string id, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Odbija dokument uz zadani razlog i opis. + /// + /// ID dokumenta. + /// Razlog odbijanja. + /// Dodatni opis odbijanja. + /// Cancellation token. + /// Asinkrona operacija odbijanja dokumenta. + public Task OdbijRacunAsync(string id, RazlogOdbijanja razlog, string opis, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Dohvaća popis ulaznih e-računa za zadani period. + /// + /// Početni datum pretrage. + /// Završni datum pretrage. + /// Cancellation token. + /// Popis ulaznih e-računa. + public Task> UlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Dohvaća PDF vizualizaciju ulaznog dokumenta. + /// + /// ID dokumenta. + /// Cancellation token. + /// PDF dokument kao byte array. + public Task UlazniPdfAsync(string id, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Dohvaća UBL/XML sadržaj ulaznog dokumenta. + /// + /// ID dokumenta. + /// Cancellation token. + /// UBL/XML sadržaj dokumenta. + public Task UlazniUBLAsync(string id, CancellationToken token = default) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/MAES.Fiskal2/Posrednici/Super.cs b/MAES.Fiskal2/Posrednici/Super.cs index 699b2ce..1b33a9f 100644 --- a/MAES.Fiskal2/Posrednici/Super.cs +++ b/MAES.Fiskal2/Posrednici/Super.cs @@ -130,7 +130,7 @@ public async Task> UlazniListAsync(DateTime from, Date $"{x.GetProperty("SupplierAddress").GetString()}, " + $"{x.GetProperty("SupplierZip").GetString()} " + $"{x.GetProperty("SupplierCity").GetString()}", - Guid = x.GetProperty("Guid").GetGuid(), + Id = x.GetProperty("Guid").GetGuid().ToString(), Status = UlazniERacunStatus.Zaprimljeno // treba popravit }); } @@ -198,7 +198,7 @@ public async Task> IzlazniListAsync(DateTime from, Da $"{x.GetProperty("SupplierAddress").GetString()}, " + $"{x.GetProperty("SupplierZip").GetString()} " + $"{x.GetProperty("SupplierCity").GetString()}", - Guid = x.GetProperty("Guid").GetGuid(), + Id = x.GetProperty("Guid").GetGuid().ToString(), Status = IzlazniERacunStatus.Poslano // treba popravit }); } diff --git a/MAES.Fiskal2/UlazniERacun.cs b/MAES.Fiskal2/UlazniERacun.cs index 34cde12..6bb029d 100644 --- a/MAES.Fiskal2/UlazniERacun.cs +++ b/MAES.Fiskal2/UlazniERacun.cs @@ -8,7 +8,7 @@ public class UlazniERacun /// /// Jedinstveni identifikator računa u sustavu posrednika. /// - public Guid Guid { get; set; } + public string Id { get; set; } = ""; /// /// Redni broj računa. diff --git a/README.md b/README.md index 88ba92a..ad6cc59 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # MAES.Fiskal2 +[![CI/CD](https://github.com/MAES-Software/MAES.Fiskal2/actions/workflows/main.yml/badge.svg)](https://github.com/MAES-Software/MAES.Fiskal2/actions/workflows/main.yml) +[![Contributors](https://img.shields.io/github/contributors/MAES-Software/MAES.Fiskal2)](https://github.com/MAES-Software/MAES.Fiskal2/graphs/contributors) +[![Issues](https://img.shields.io/github/issues/MAES-Software/MAES.Fiskal2)](https://github.com/MAES-Software/MAES.Fiskal2/issues) +[![NuGet](https://img.shields.io/nuget/v/MAES.Fiskal2.svg)](https://www.nuget.org/packages/MAES.Fiskal2/) +[![NuGet Downloads](https://img.shields.io/nuget/dt/MAES.Fiskal2)](https://www.nuget.org/packages/MAES.Fiskal2/) + MAES.Fiskal2 je C# biblioteka za rad s Hrvatskim fiskalnim posrednicima. Cilj projekta je izraditi zajednički sloj za sve posrednike koji podržavaju razmjenu ulaznih i izlaznih e-računa u C#. ## Što projekt radi @@ -18,19 +24,15 @@ Modeli `UlazniERacun` i `IzlazniERacun` predstavljaju minimalne informacije o ra U `Posrednici/` direktoriju nalaze se konkretne implementacije -| Značajka / posrednik | `Super` | `EPoslovanje` | `Fina` | `bizBox` | `Editel` | -|---|:---:|:---:|:---:|:---:|:---:| -| Dohvat ulaznih e-računa | ✅ | ❌ | ❌ | ❌ | ❌ | -| Dohvat izlaznih e-računa | ✅ | ❌ | ❌ | ❌ | ❌ | -| Dohvat UBL sadržaja | ✅ | ❌ | ❌ | ❌ | ❌ | -| Dohvat PDF sadržaja | ✅ | ❌ | ❌ | ❌ | ❌ | -| Evidentiranje UBL dokumenta | ✅ | ✅* | ❌ | ❌ | ❌ | -| Evidentiranje uplate | ✅ | ❌ | ❌ | ❌ | ❌ | -| Odbijanje računa | ✅ | ❌ | ❌ | ❌ | ❌ | - -## Zahtjevi - -- .NET Standard 2.0 +| Značajka / posrednik | `Super` | `EPoslovanje` | `Fina` | +|---|:---:|:---:|:---:| +| Dohvat ulaznih e-računa | ✅ | ✅ | ❌ | +| Dohvat izlaznih e-računa | ✅ | ✅ | ❌ | +| Dohvat UBL sadržaja | ✅ | ✅ | ❌ | +| Dohvat PDF sadržaja | ✅ | ✅ | ❌ | +| Evidentiranje UBL dokumenta | ✅ | ✅ | ❌ | +| Evidentiranje uplate | ✅ | ✅ | ❌ | +| Odbijanje računa | ✅ | ✅ | ❌ | ## Instalacija @@ -48,9 +50,9 @@ Ili direktno u datoteku `.csproj`: ``` -## Kako koristiti +## Inicijalizacija posrednika -Primjer inicijalizacije posrednika: +### Primjer inicijalizacije Super.hr posrednika: ```csharp using MAES.Fiskal2.Posrednici; @@ -62,51 +64,65 @@ var posrednik = new Super Username = "...", Password = "..." }; +``` + +### Primjer inicijalizacije eposlovanje.hr posrednika: + +```csharp +using MAES.Fiskal2.Posrednici; -var racuni = await posrednik.UlazniListAsync(DateTime.Today.AddDays(-7), DateTime.Today); +var posrednik = new EPoslovanje +{ + IsDev = true, + OIB = "...", + Username = "...", + Password = "..." +}; ``` ## Dostupne metode +> Svaka metoda ima na kraju CancellationToken kojeg je poželjno postaviti, ali se može izostaviti + Sučelje `IPosrednik` nudi sljedeće metode: ### Dohvat ulaznih e-računa -- `Task> UlazniListAsync(DateTime from, DateTime to, CancellationToken token = default)` +- `Task> UlazniListAsync(DateTime from, DateTime to)` Dohvaća popis ulaznih računa u vremenskom rasponu -- `Task UlazniUBLAsync(string id, CancellationToken token = default)` +- `Task UlazniUBLAsync(string id)` Dohvaća XML/UBL sadržaj ulaznog računa -- `Task UlazniPdfAsync(string id, CancellationToken token = default)` +- `Task UlazniPdfAsync(string id)` Dohvaća PDF sadržaj ulaznog računa ### Dohvat izlaznih e-računa -- `Task> IzlazniListAsync(DateTime from, DateTime to, CancellationToken token = default)` +- `Task> IzlazniListAsync(DateTime from, DateTime to)` Dohvaća popis izlaznih računa u vremenskom rasponu -- `Task IzlazniUBLAsync(string id, CancellationToken token = default)` +- `Task IzlazniUBLAsync(string id)` Dohvaća XML/UBL sadržaj izlaznog računa -- `Task IzlazniPdfAsync(string id, CancellationToken token = default)` +- `Task IzlazniPdfAsync(string id)` Dohvaća PDF sadržaj izlaznog računa ### Operacije na računima -- `Task EvidentirajUBLAsync(string ubl, CancellationToken token = default)` +- `Task EvidentirajUBLAsync(string ubl)` Evidentira UBL dokument -- `Task EvidentirajUplatuAsync(string id, CancellationToken token = default)` +- `Task EvidentirajUplatuAsync(string id)` Evidentira uplatu za račun -- `Task OdbijRacunAsync(string id, CancellationToken token = default)` +- `Task OdbijRacunAsync(string id)` Odbija račun @@ -128,8 +144,4 @@ dotnet pack MAES.Fiskal2.csproj --configuration Release - Projekt je u razvoju. - Neke metode još nisu implementirane. -- Trenutna sučelja i model podataka mogu se mijenjati dok se dovršava podrška za različite posrednike. - -## Licenca - -Ovaj projekt je licenciran pod MIT licencom. Vidjeti [LICENSE](LICENSE) datoteku za više detalja. +- Trenutna sučelja i model podataka mogu se mijenjati dok se dovršava podrška za različite posrednike. \ No newline at end of file