From 7159710e0871c32f9f5bfe364e5ec2c5431023c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 03:17:06 +0200 Subject: [PATCH 1/8] pun kurac toga napravljeno, evidentiranje racuna doku, parra izbacena jer nema test api --- MAES.Fiskal2.Tests/Fiskal2Tests.cs | 229 +++++++++++++++++++++++------ MAES.Fiskal2/Posrednici/AdeoPOS.cs | 20 +++ MAES.Fiskal2/Posrednici/Doku.cs | 63 ++++++++ MAES.Fiskal2/Posrednici/MER.cs | 2 +- MAES.Fiskal2/Posrednici/Parra.cs | 203 ------------------------- MAES.Fiskal2/Posrednici/TER.cs | 20 +++ README.md | 20 +-- 7 files changed, 301 insertions(+), 256 deletions(-) create mode 100644 MAES.Fiskal2/Posrednici/AdeoPOS.cs create mode 100644 MAES.Fiskal2/Posrednici/Doku.cs delete mode 100644 MAES.Fiskal2/Posrednici/Parra.cs create mode 100644 MAES.Fiskal2/Posrednici/TER.cs diff --git a/MAES.Fiskal2.Tests/Fiskal2Tests.cs b/MAES.Fiskal2.Tests/Fiskal2Tests.cs index bd568cb..73b1a7e 100644 --- a/MAES.Fiskal2.Tests/Fiskal2Tests.cs +++ b/MAES.Fiskal2.Tests/Fiskal2Tests.cs @@ -1,8 +1,10 @@ -using MAES.Fiskal2.Posrednici; + +using MAES.Fiskal2.Posrednici; +using Xunit.Abstractions; namespace MAES.Fiskal2.Tests; -public class SuperTests +public class PosredniciTests(ITestOutputHelper output) { readonly List posrednici = [ @@ -13,60 +15,201 @@ public class SuperTests Password = Environment.GetEnvironmentVariable("SUPER_PASSWORD") ?? throw new InvalidOperationException("SUPER_PASSWORD environment variable is not set."), IsDev = true }, - // new EPoslovanje - // { - // OIB = Environment.GetEnvironmentVariable("EPOSLOVANJE_OIB") ?? throw new InvalidOperationException("EPOSLOVANJE_OIB environment variable is not set."), - // Username = Environment.GetEnvironmentVariable("EPOSLOVANJE_USERNAME") ?? throw new InvalidOperationException("EPOSLOVANJE_USERNAME environment variable is not set."), - // Password = Environment.GetEnvironmentVariable("EPOSLOVANJE_PASSWORD") ?? throw new InvalidOperationException("EPOSLOVANJE_PASSWORD environment variable is not set."), - // IsDev = true - // }, - // new Fina - // { - // OIB = Environment.GetEnvironmentVariable("FINA_OIB") ?? throw new InvalidOperationException("FINA_OIB environment variable is not set."), - // Certificate = ..., - // IsDev = true - // }, - // new MER - // { - // OIB = Environment.GetEnvironmentVariable("MER_OIB") ?? throw new InvalidOperationException("MER_OIB environment variable is not set."), - // Username = Environment.GetEnvironmentVariable("MER_USERNAME") ?? throw new InvalidOperationException("MER_USERNAME environment variable is not set."), - // Password = Environment.GetEnvironmentVariable("MER_PASSWORD") ?? throw new InvalidOperationException("MER_PASSWORD environment variable is not set."), - // IsDev = true - // } + new EPoslovanje + { + OIB = "51560545524", + Username = Environment.GetEnvironmentVariable("EPOSLOVANJE_USERNAME") ?? throw new InvalidOperationException("EPOSLOVANJE_USERNAME environment variable is not set."), + Password = Environment.GetEnvironmentVariable("EPOSLOVANJE_PASSWORD") ?? throw new InvalidOperationException("EPOSLOVANJE_PASSWORD environment variable is not set."), + IsDev = true + }, + new Doku + { + ApiKey = Environment.GetEnvironmentVariable("DOKU_API_KEY") ?? throw new InvalidOperationException("DOKU_API_KEY environment variable is not set."), + IsDev = true + }, + new Fina + { + OIB = "51560545524", + //Certificate = ..., + IsDev = true + }, + new MER + { + OIB = "51560545524", + Username = Environment.GetEnvironmentVariable("MER_USERNAME") ?? throw new InvalidOperationException("MER_USERNAME environment variable is not set."), + Password = Environment.GetEnvironmentVariable("MER_PASSWORD") ?? throw new InvalidOperationException("MER_PASSWORD environment variable is not set."), + IsDev = true + } ]; [Fact] - public async Task SendInvoiceUBL() + public async Task EvidentirajUBL() + { + foreach (var posrednik in posrednici.Where(p => p is not Fina)) + { + try + { + await posrednik.EvidentirajUBLAsync(File.ReadAllText("ubl.xml")); + output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUBL OK"); + } + catch (Exception ex) + { + output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUBL FAIL"); + output.WriteLine(ex.ToString()); + } + } + } + + [Fact] + public async Task DohvatiIzlazneRacune() + { + foreach (var posrednik in posrednici.Where(p => p is not Fina)) + { + try + { + var izlazni = await posrednik.IzlazniListAsync( + DateTime.UtcNow.AddDays(-30), + DateTime.UtcNow); + + output.WriteLine($"{posrednik.GetType().Name}: IzlazniList OK ({izlazni.Count()})"); + } + catch (Exception ex) + { + output.WriteLine($"{posrednik.GetType().Name}: IzlazniList FAIL"); + output.WriteLine(ex.ToString()); + } + } + } + + [Fact] + public async Task DohvatiPrviIzlazniPdfIUBL() + { + foreach (var posrednik in posrednici.Where(p => p is not Fina)) + { + try + { + var izlazni = await posrednik.IzlazniListAsync( + DateTime.UtcNow.AddDays(-30), + DateTime.UtcNow); + + var first = izlazni.FirstOrDefault(); + + if (first == null) + { + output.WriteLine($"{posrednik.GetType().Name}: nema izlaznih računa"); + continue; + } + + await posrednik.IzlazniPdfAsync(first.Id); + await posrednik.IzlazniUBLAsync(first.Id); + + output.WriteLine($"{posrednik.GetType().Name}: Izlazni PDF + UBL OK"); + } + catch (Exception ex) + { + output.WriteLine($"{posrednik.GetType().Name}: Izlazni PDF + UBL FAIL"); + output.WriteLine(ex.ToString()); + } + } + } + + [Fact] + public async Task EvidentirajUplatu() + { + foreach (var posrednik in posrednici.Where(p => p is not Fina)) + { + try + { + var izlazni = await posrednik.IzlazniListAsync( + DateTime.UtcNow.AddDays(-30), + DateTime.UtcNow); + + var first = izlazni.FirstOrDefault(); + + if (first == null) + { + output.WriteLine($"{posrednik.GetType().Name}: nema izlaznih računa za uplatu"); + continue; + } + + await posrednik.EvidentirajUplatuAsync( + first.Id, + DateTime.UtcNow, + 100, + NacinPlacanja.TransakcijskiRaCun); + + output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUplatu OK"); + } + catch (Exception ex) + { + output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUplatu FAIL"); + output.WriteLine(ex.ToString()); + } + } + } + + [Fact] + public async Task UlazniRacuni() { foreach (var posrednik in posrednici.Where(p => p is not Fina)) { - // evidencija računa - await posrednik.EvidentirajUBLAsync(File.ReadAllText("ubl.xml")); + try + { + var ulazni = await posrednik.UlazniListAsync( + DateTime.UtcNow.AddDays(-30), + DateTime.UtcNow); - // izlazni računi - // var izlazni = await posrednik.IzlazniListAsync(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow); - // Assert.NotNull(izlazni); + var first = ulazni.FirstOrDefault(); - // var first = izlazni.FirstOrDefault(); - // if(first != null) - // { - // Assert.NotNull(await posrednik.IzlazniPdfAsync(first.Id)); - // Assert.NotNull(await posrednik.IzlazniUBLAsync(first.Id)); + if (first == null) + { + output.WriteLine($"{posrednik.GetType().Name}: nema ulaznih računa"); + continue; + } - // await posrednik.EvidentirajUplatuAsync(first.Id, DateTime.UtcNow, 100, NacinPlacanja.TransakcijskiRaCun); - // } + await posrednik.UlazniPdfAsync(first.Id); + await posrednik.UlazniUBLAsync(first.Id); - // ulazni računi - var ulazni = await posrednik.UlazniListAsync(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow); - Assert.NotNull(ulazni); + output.WriteLine($"{posrednik.GetType().Name}: Ulazni PDF + UBL OK"); + } + catch (Exception ex) + { + output.WriteLine($"{posrednik.GetType().Name}: Ulazni FAIL"); + output.WriteLine(ex.ToString()); + } + } + } - var firstUlazni = ulazni.FirstOrDefault(); - if(firstUlazni != null) + [Fact] + public async Task OdbijPrviUlazniRacun() + { + foreach (var posrednik in posrednici.Where(p => p is not Fina)) + { + try { - Assert.NotNull(await posrednik.UlazniPdfAsync(firstUlazni.Id)); - Assert.NotNull(await posrednik.UlazniUBLAsync(firstUlazni.Id)); + var ulazni = await posrednik.UlazniListAsync( + DateTime.UtcNow.AddDays(-30), + DateTime.UtcNow); - await posrednik.OdbijRacunAsync(firstUlazni.Id, RazlogOdbijanja.NeusklađenostKojaNeUtjeceNaObracunPoreza, "Nedostaje OIB"); + var first = ulazni.FirstOrDefault(); + + if (first == null) + { + output.WriteLine($"{posrednik.GetType().Name}: nema ulaznih za odbijanje"); + continue; + } + + await posrednik.OdbijRacunAsync( + first.Id, + RazlogOdbijanja.NeusklađenostKojaNeUtjeceNaObracunPoreza, + "Nedostaje OIB"); + + output.WriteLine($"{posrednik.GetType().Name}: OdbijRacun OK"); + } + catch (Exception ex) + { + output.WriteLine($"{posrednik.GetType().Name}: OdbijRacun FAIL"); + output.WriteLine(ex.ToString()); } } } diff --git a/MAES.Fiskal2/Posrednici/AdeoPOS.cs b/MAES.Fiskal2/Posrednici/AdeoPOS.cs new file mode 100644 index 0000000..735ba87 --- /dev/null +++ b/MAES.Fiskal2/Posrednici/AdeoPOS.cs @@ -0,0 +1,20 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; + +namespace MAES.Fiskal2.Posrednici; + +/// +/// Implementacija posrednika za AdeoPOS. https://adeopos.hr/api-za-fiskalizaciju/ +/// +public class AdeoPOS : Posrednik +{ + /// + /// Konstruktor za inicijalizaciju AdeoPOS posrednika. + /// + public AdeoPOS() + { + //UriProd = "https://api.adeopos.hr"; + //UriDev = "https://api-test.adeopos.hr"; + } +} \ No newline at end of file diff --git a/MAES.Fiskal2/Posrednici/Doku.cs b/MAES.Fiskal2/Posrednici/Doku.cs new file mode 100644 index 0000000..5c78731 --- /dev/null +++ b/MAES.Fiskal2/Posrednici/Doku.cs @@ -0,0 +1,63 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; + +namespace MAES.Fiskal2.Posrednici; + +/// +/// Implementacija posrednika za Doku. https://api.doku.hr/docs/ +/// +public class Doku : Posrednik +{ + /// + /// API ključ za autentifikaciju. + /// + public string ApiKey { get; set; } = ""; + + /// + /// Konstruktor za inicijalizaciju Doku posrednika. + /// + public Doku() + { + UriProd = "https://api.doku.hr"; + UriDev = "https://api-test.doku.hr"; + } + + /// + /// Evidentira UBL dokument u Doku sustavu. Doku očekuje Base64 enkodirani XML sadržaj UBL-a unutar JSON objekta s ključem "xml". + /// + /// + /// + /// + /// + public override async Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) + { + await sendRequest(HttpMethod.Post, "/documents/invoices/outgoing/upload?publicLink=false", new + { + xml = Convert.ToBase64String(Encoding.UTF8.GetBytes(ubl)) + }, token); + } + + async Task sendRequest(HttpMethod method, string url, object? body, CancellationToken token) + { + using var client = new HttpClient + { + BaseAddress = new Uri(Uri) + }; + + client.DefaultRequestHeaders.TryAddWithoutValidation("DOKU-API-KEY", ApiKey); + + using var request = new HttpRequestMessage(method, url) + { + Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json") + }; + + using var response = await client.SendAsync(request, token); + + var responseBody = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) throw new HttpRequestException(responseBody); + + return responseBody; + } +} \ No newline at end of file diff --git a/MAES.Fiskal2/Posrednici/MER.cs b/MAES.Fiskal2/Posrednici/MER.cs index 7d53a9e..8261503 100644 --- a/MAES.Fiskal2/Posrednici/MER.cs +++ b/MAES.Fiskal2/Posrednici/MER.cs @@ -62,7 +62,7 @@ async Task sendRequest(HttpMethod method, string url, object? body, Canc /// /// UBL XML sadržaj e-računa. /// Token za otkazivanje operacije. - public override async Task EvidentirajUBLAsync(string ubl, CancellationToken cancellationToken = default) =>await sendRequest(HttpMethod.Post, "/apis/v2/send", new + public override async Task EvidentirajUBLAsync(string ubl, CancellationToken cancellationToken = default) => await sendRequest(HttpMethod.Post, "/apis/v2/send", new { Username, Password, diff --git a/MAES.Fiskal2/Posrednici/Parra.cs b/MAES.Fiskal2/Posrednici/Parra.cs deleted file mode 100644 index 28413f9..0000000 --- a/MAES.Fiskal2/Posrednici/Parra.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; - -namespace MAES.Fiskal2.Posrednici; - -/// -/// Implementacija posrednika Parra v1.1. https://api.parra.hr/docs/#api-v11 -/// -public class Parra : Posrednik -{ - /// - /// OIB poslovnog subjekta. - /// - public string OIB { get; set; } = ""; - - /// - /// Korisničko ime za autentifikaciju. - /// - public string Username { get; set; } = ""; - - /// - /// Lozinka za autentifikaciju. - /// - public string Password { get; set; } = ""; - - /// - /// Inicijalizira novog Parra posrednika s definiranim URI postavkama za produkcijsko i razvojno okruženje. - /// - public Parra() - { - UriProd = "https://api.parra.hr"; - UriDev = ""; - } - - HttpClient createClient() - { - var client = new HttpClient - { - BaseAddress = new Uri(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") - { - 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 XML/UBL sadržaj ulaznog računa. - /// - public override 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 PDF sadržaj ulaznog računa. - /// - public override 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 popis ulaznih e-računa. - /// - public override 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 XML/UBL sadržaj izlaznog računa. - /// - public override async Task IzlazniUBLAsync(string id, CancellationToken token = default) - => await UlazniUBLAsync(id, token); - - /// - /// Dohvaća PDF sadržaj izlaznog računa. - /// - public override async Task IzlazniPdfAsync(string id, CancellationToken token = default) - => await UlazniPdfAsync(id, token); - - /// - /// Dohvaća popis izlaznih e-računa. - /// - public override async Task> IzlazniListAsync( - DateTime from, - DateTime to, - CancellationToken token = default) - { - var doc = await sendRequest(HttpMethod.Get, $"/api/v2/document/outgoing?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 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 UBL dokument u ePoslovanje sustav. - /// - public override async Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) => await sendRequest(HttpMethod.Post, "/api/v2/document/send", new - { - document = ubl, - softwareId = "MAES.Blagajna" - }, token); - - /// - /// Evidentira uplatu za račun. - /// - public override Task EvidentirajUplatuAsync(string id, DateTime date, double amount, NacinPlacanja paymentMethod, CancellationToken token = default) - { - 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 override 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/TER.cs b/MAES.Fiskal2/Posrednici/TER.cs new file mode 100644 index 0000000..89311eb --- /dev/null +++ b/MAES.Fiskal2/Posrednici/TER.cs @@ -0,0 +1,20 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; + +namespace MAES.Fiskal2.Posrednici; + +/// +/// Implementacija posrednika za Tvoj eRačun. https://ter.hr/api-za-fiskalizaciju/ +/// +public class TER : Posrednik +{ + /// + /// Konstruktor za inicijalizaciju TER posrednika. + /// + public TER() + { + //UriProd = "https://api.ter.hr"; + //UriDev = "https://api-test.ter.hr"; + } +} \ No newline at end of file diff --git a/README.md b/README.md index f82bee2..5d0aca9 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,17 @@ Modeli `UlazniERacun` i `IzlazniERacun` predstavljaju minimalne informacije o ra U `Posrednici/` direktoriju nalaze se konkretne implementacije -| Značajka / posrednik | `Super` | `EPoslovanje` | `Fina` | `Moj eRačun` | `Redok` | `Parra` | -|---|:---:|:---:|:---:|:---:|:---:|:---:| -| 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 | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | +| Značajka / posrednik | `Super` | `EPoslovanje` | `Fina` | `Moj eRačun` | `Redok` | `Tvoj eRačun` | `AdeoPOS` | `Doku` | +|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| Ulazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | +| Ulazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | +| Izlazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | +| Izlazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | +| Dohvat UBL sadržaja | ✅ | ✅ | ❌* | ✅ | ❌ | ❌ | ❌ | ❌ | +| Dohvat PDF sadržaja | ✅ | ✅ | ❌* | ❌** | ❌ | ❌ | ❌ | ❌ | +| Evidentiranje UBL dokumenta | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | +| Evidentiranje uplate | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | +| Odbijanje računa | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | \* Fina nema pola ovih api callova ili su na nekom drugom endpointu treba vidit From 47851ae5c988f922cc024caa22624b9576e62e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 06:16:41 +0200 Subject: [PATCH 2/8] opet promjenjeno pun kurac toga --- MAES.Fiskal2/Posrednici/AdeoPOS.cs | 20 ------ MAES.Fiskal2/Posrednici/Doku.cs | 61 +++++++++++------- MAES.Fiskal2/Posrednici/EPoslovanje.cs | 6 +- MAES.Fiskal2/Posrednici/Fina.cs | 8 +-- MAES.Fiskal2/Posrednici/MER.cs | 6 +- MAES.Fiskal2/Posrednici/Super.cs | 6 +- MAES.Fiskal2/Posrednik.cs | 88 +++++++++++++++++--------- README.md | 29 +++++---- 8 files changed, 127 insertions(+), 97 deletions(-) delete mode 100644 MAES.Fiskal2/Posrednici/AdeoPOS.cs diff --git a/MAES.Fiskal2/Posrednici/AdeoPOS.cs b/MAES.Fiskal2/Posrednici/AdeoPOS.cs deleted file mode 100644 index 735ba87..0000000 --- a/MAES.Fiskal2/Posrednici/AdeoPOS.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; - -namespace MAES.Fiskal2.Posrednici; - -/// -/// Implementacija posrednika za AdeoPOS. https://adeopos.hr/api-za-fiskalizaciju/ -/// -public class AdeoPOS : Posrednik -{ - /// - /// Konstruktor za inicijalizaciju AdeoPOS posrednika. - /// - public AdeoPOS() - { - //UriProd = "https://api.adeopos.hr"; - //UriDev = "https://api-test.adeopos.hr"; - } -} \ No newline at end of file diff --git a/MAES.Fiskal2/Posrednici/Doku.cs b/MAES.Fiskal2/Posrednici/Doku.cs index 5c78731..befe3e3 100644 --- a/MAES.Fiskal2/Posrednici/Doku.cs +++ b/MAES.Fiskal2/Posrednici/Doku.cs @@ -19,45 +19,60 @@ public class Doku : Posrednik /// public Doku() { - UriProd = "https://api.doku.hr"; - UriDev = "https://api-test.doku.hr"; + BaseAddressProd = "https://api.doku.hr"; + BaseAddressDev = "https://api-test.doku.hr"; + OnClientCreated += (s, e) => + { + e.Client.DefaultRequestHeaders.TryAddWithoutValidation("DOKU-API-KEY", ApiKey); + }; } /// /// Evidentira UBL dokument u Doku sustavu. Doku očekuje Base64 enkodirani XML sadržaj UBL-a unutar JSON objekta s ključem "xml". /// /// - /// + /// Token za otkazivanje operacije. /// /// public override async Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) { - await sendRequest(HttpMethod.Post, "/documents/invoices/outgoing/upload?publicLink=false", new + await SendRequest(HttpMethod.Post, "/documents/invoices/outgoing/upload", new { xml = Convert.ToBase64String(Encoding.UTF8.GetBytes(ubl)) }, token); } - async Task sendRequest(HttpMethod method, string url, object? body, CancellationToken token) + /// + /// Dohvaća listu izlaznih e-računa za zadano razdoblje. + /// + /// Datum početka razdoblja. + /// Datum kraja razdoblja. + /// Token za otkazivanje operacije. + /// + public override async Task> IzlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) { - using var client = new HttpClient - { - BaseAddress = new Uri(Uri) - }; - - client.DefaultRequestHeaders.TryAddWithoutValidation("DOKU-API-KEY", ApiKey); - - using var request = new HttpRequestMessage(method, url) + var res = await SendRequest(HttpMethod.Get, $"/documents/invoices/outgoing?IssueDateFrom={from:O}&IssueDateTo={to:O}", null, token); + var doc = JsonDocument.Parse(res); + + var list = new List(); + foreach (var item in doc.RootElement.GetProperty("records").EnumerateArray()) { - Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json") - }; - - using var response = await client.SendAsync(request, token); - - var responseBody = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) throw new HttpRequestException(responseBody); - - return responseBody; + list.Add(new IzlazniERacun + { + Id = item.GetProperty("id").GetInt64().ToString(), + Broj = item.GetProperty("name").GetString()!, + Datum = item.GetProperty("issueDate").GetDateTime(), + PartnerNaziv = item.GetProperty("receiver").GetProperty("name").GetString()!, + PartnerOIB = item.GetProperty("receiver").GetProperty("oib").GetString()!, + Status = item.GetProperty("status").GetString() switch // <-- ovo treba popravit + { + "DRAFT" => IzlazniERacunStatus.Nacrt, + "SENT" => IzlazniERacunStatus.Poslano, + "REJECTED" => IzlazniERacunStatus.Odbijeno, + _ => IzlazniERacunStatus.Dostavljeno + } + }); + } + return list; } } \ No newline at end of file diff --git a/MAES.Fiskal2/Posrednici/EPoslovanje.cs b/MAES.Fiskal2/Posrednici/EPoslovanje.cs index 0340c6f..a3e26f5 100644 --- a/MAES.Fiskal2/Posrednici/EPoslovanje.cs +++ b/MAES.Fiskal2/Posrednici/EPoslovanje.cs @@ -29,15 +29,15 @@ public class EPoslovanje : Posrednik /// public EPoslovanje() { - UriProd = "https://eracun.eposlovanje.hr"; - UriDev = "https://test.eposlovanje.hr"; + BaseAddressProd = "https://eracun.eposlovanje.hr"; + BaseAddressDev = "https://test.eposlovanje.hr"; } HttpClient createClient() { var client = new HttpClient { - BaseAddress = new Uri(Uri) + BaseAddress = new Uri(BaseAddress) }; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); diff --git a/MAES.Fiskal2/Posrednici/Fina.cs b/MAES.Fiskal2/Posrednici/Fina.cs index 7f789fa..af20131 100644 --- a/MAES.Fiskal2/Posrednici/Fina.cs +++ b/MAES.Fiskal2/Posrednici/Fina.cs @@ -25,8 +25,8 @@ public class Fina : Posrednik /// public Fina() { - UriProd = "https://eracun.fina.hr/eracun-b2b/services/eRacunB2BPortType"; - UriDev = "https://eracun-test.fina.hr/eracun-b2b/services/eRacunB2BPortType"; + BaseAddressProd = "https://eracun.fina.hr/eracun-b2b/services/eRacunB2BPortType"; + BaseAddressDev = "https://eracun-test.fina.hr/eracun-b2b/services/eRacunB2BPortType"; } /// @@ -66,7 +66,7 @@ public override async Task EvidentirajUBLAsync(string ubl, CancellationToken tok } }; - using var client = new eRacunB2BPortTypeClient(eRacunB2BPortTypeClient.EndpointConfiguration.eRacunB2BPortType, Uri); + using var client = new eRacunB2BPortTypeClient(eRacunB2BPortTypeClient.EndpointConfiguration.eRacunB2BPortType, BaseAddress); var res = await client.sendB2BOutgoingInvoiceAsync(msg); @@ -182,7 +182,7 @@ static string GetBuyerId(XDocument xml) ?? throw new InvalidOperationException("UBL nema BuyerID."); } - eRacunB2BPortTypeClient CreateClient() => new (eRacunB2BPortTypeClient.EndpointConfiguration.eRacunB2BPortType, Uri); + eRacunB2BPortTypeClient CreateClient() => new (eRacunB2BPortTypeClient.EndpointConfiguration.eRacunB2BPortType, BaseAddress); HeaderSupplierType Header() => new() { diff --git a/MAES.Fiskal2/Posrednici/MER.cs b/MAES.Fiskal2/Posrednici/MER.cs index 8261503..bcc70af 100644 --- a/MAES.Fiskal2/Posrednici/MER.cs +++ b/MAES.Fiskal2/Posrednici/MER.cs @@ -31,15 +31,15 @@ public class MER : Posrednik /// public MER() { - UriProd = "https://www.moj-eracun.hr"; - UriDev = "https://demo.moj-eracun.hr"; + BaseAddressProd = "https://www.moj-eracun.hr"; + BaseAddressDev = "https://demo.moj-eracun.hr"; } async Task sendRequest(HttpMethod method, string url, object? body, CancellationToken token) { var client = new HttpClient { - BaseAddress = new Uri(Uri) + BaseAddress = new Uri(BaseAddress) }; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); diff --git a/MAES.Fiskal2/Posrednici/Super.cs b/MAES.Fiskal2/Posrednici/Super.cs index 273b7e1..284bd3f 100644 --- a/MAES.Fiskal2/Posrednici/Super.cs +++ b/MAES.Fiskal2/Posrednici/Super.cs @@ -31,14 +31,14 @@ public class Super : Posrednik /// public Super() { - UriProd = "https://api.super.hr/"; - UriDev = "https://apitest.super.hr/"; + BaseAddressProd = "https://api.super.hr/"; + BaseAddressDev = "https://apitest.super.hr/"; } async Task postRequest(string uri, Dictionary body, CancellationToken cancellationToken) { using var client = new HttpClient(); - client.BaseAddress = new Uri(Uri); + client.BaseAddress = new Uri(BaseAddress); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (token == null || DateTime.UtcNow >= token.Value.Value) diff --git a/MAES.Fiskal2/Posrednik.cs b/MAES.Fiskal2/Posrednik.cs index 730d3b7..2d0512f 100644 --- a/MAES.Fiskal2/Posrednik.cs +++ b/MAES.Fiskal2/Posrednik.cs @@ -1,3 +1,5 @@ +using System.Text; +using System.Text.Json; using System.Text.Json.Serialization; using MAES.Fiskal2.Posrednici; @@ -11,28 +13,30 @@ namespace MAES.Fiskal2; [JsonDerivedType(typeof(Super), "Super")] [JsonDerivedType(typeof(EPoslovanje), "EPoslovanje")] [JsonDerivedType(typeof(Fina), "Fina")] +[JsonDerivedType(typeof(Doku), "Doku")] +[JsonDerivedType(typeof(MER), "MER")] public abstract class Posrednik { /// /// Označava koristi li se razvojno okruženje. - /// Ako je true, koristi se ; inače . + /// Ako je true, koristi se ; inače . /// public bool IsDev { get; set; } /// /// Produkcijski URI servisa. /// - protected string UriProd { private get; set; } = ""; + protected string BaseAddressProd { private get; set; } = ""; /// /// URI razvojnog (testnog) okruženja servisa. /// - protected string UriDev { private get; set; } = ""; + protected string BaseAddressDev { private get; set; } = ""; /// /// Aktivni URI servisa ovisno o odabranom okruženju. /// - protected string Uri => IsDev ? UriDev : UriProd; + protected string BaseAddress => IsDev ? BaseAddressDev : BaseAddressProd; /// /// Evidentira izlazni eRačun na temelju UBL sadržaja. @@ -67,15 +71,6 @@ public virtual Task EvidentirajUplatuAsync( /// Kolekcija izlaznih eRačuna. public virtual Task> IzlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) => throw new NotImplementedException(); - /// - /// Dohvaća popis izlaznih eRačuna unutar zadanog razdoblja. - /// - /// Stranica rezultata. - /// Broj rezultata po stranici. - /// Token za otkazivanje operacije. - /// Kolekcija izlaznih eRačuna. - public virtual Task> IzlazniListAsync(int page, int pageSize, CancellationToken token = default) => throw new NotImplementedException(); - /// /// Dohvaća PDF prikaz izlaznog računa. /// @@ -121,25 +116,13 @@ public virtual Task OdbijRacunAsync( /// Kolekcija ulaznih eRačuna. public virtual Task> UlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) => throw new NotImplementedException(); - /// - /// Dohvaća popis ulaznih eRačuna unutar zadanog razdoblja. - /// - /// Stranica rezultata. - /// Broj rezultata po stranici. - /// Token za otkazivanje operacije. - /// Kolekcija ulaznih eRačuna. - public virtual Task> UlazniListAsync(int page, int pageSize, CancellationToken token = default) => throw new NotImplementedException(); - /// /// Dohvaća PDF prikaz ulaznog računa. /// /// Identifikator računa. /// Token za otkazivanje operacije. /// Sadržaj PDF dokumenta kao niz bajtova. - public virtual Task UlazniPdfAsync( - string id, - CancellationToken token = default) => - throw new NotImplementedException(); + public virtual Task UlazniPdfAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); /// /// Dohvaća UBL XML ulaznog računa. @@ -147,8 +130,53 @@ public virtual Task UlazniPdfAsync( /// Identifikator računa. /// Token za otkazivanje operacije. /// UBL XML sadržaj računa. - public virtual Task UlazniUBLAsync( - string id, - CancellationToken token = default) => - throw new NotImplementedException(); + public virtual Task UlazniUBLAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); + + /// + /// Generička metoda za slanje HTTP zahtjeva prema posredniku. Koristi se unutar specifičnih implementacija posrednika za komunikaciju s njihovim API-jem. + /// + /// Metoda HTTP zahtjeva. + /// URL zahtjeva. + /// Tijelo zahtjeva. (ostavi null ako je prazno) + /// Token za otkazivanje operacije. + /// Content string + /// + protected async Task SendRequest(HttpMethod method, string url, object? body = null, CancellationToken token = default) + { + using var client = new HttpClient + { + BaseAddress = new Uri(BaseAddress) + }; + + OnClientCreated?.Invoke(this, new (client)); + + using var request = new HttpRequestMessage(method, url); + + if(body != null) request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + + using var response = await client.SendAsync(request, token); + + var content = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) throw new HttpRequestException(content); + + return content; + } + + /// + /// Događaj koji se pokreće nakon kreiranja HTTP klijenta unutar metode . Omogućuje dodatnu konfiguraciju klijenta prije slanja zahtjeva. + /// + protected event EventHandler? OnClientCreated; +} + +/// +/// Podaci događaja koji se prosljeđuju prilikom pokretanja događaja . Sadrži referencu na kreirani koji se koristi za slanje zahtjeva prema posredniku. +/// +/// Kreirani HTTP klijent. +public class ClientCreatedEventArgs(HttpClient client) : EventArgs +{ + /// + /// Kreirani HTTP klijent koji se koristi za komunikaciju s posrednikom. Dopušta dodatnu konfiguraciju (npr. dodavanje zaglavlja) prije slanja zahtjeva. + /// + public HttpClient Client { get; } = client; } \ No newline at end of file diff --git a/README.md b/README.md index 5d0aca9..a42939c 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,17 @@ Modeli `UlazniERacun` i `IzlazniERacun` predstavljaju minimalne informacije o ra U `Posrednici/` direktoriju nalaze se konkretne implementacije -| Značajka / posrednik | `Super` | `EPoslovanje` | `Fina` | `Moj eRačun` | `Redok` | `Tvoj eRačun` | `AdeoPOS` | `Doku` | -|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| Ulazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | -| Ulazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | -| Izlazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | -| Izlazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | -| Dohvat UBL sadržaja | ✅ | ✅ | ❌* | ✅ | ❌ | ❌ | ❌ | ❌ | -| Dohvat PDF sadržaja | ✅ | ✅ | ❌* | ❌** | ❌ | ❌ | ❌ | ❌ | -| Evidentiranje UBL dokumenta | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | -| Evidentiranje uplate | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | -| Odbijanje računa | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | ❌ | +| Značajka / posrednik | `Super` | `EPoslovanje` | `Fina` | `MER` | `Redok` | `Tvoj eRačun` | `Doku` | +|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| Ulazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | +| Ulazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | +| Izlazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | +| Izlazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | +| Dohvat UBL sadržaja | ✅ | ✅ | ❌* | ✅ | ❌ | ❌ | ❌ | +| Dohvat PDF sadržaja | ✅ | ✅ | ❌* | ❌** | ❌ | ❌ | ❌ | +| Evidentiranje UBL dokumenta | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | +| Evidentiranje uplate | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | +| Odbijanje računa | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | \* Fina nema pola ovih api callova ili su na nekom drugom endpointu treba vidit @@ -95,6 +95,13 @@ var posrednik = new MER Password = "...", OIB = "..." }; + +// Moj eRačun +var posrednik = new MER +{ + ApiKey = "...", + OIB = "..." +}; ``` ### Primjer korištenja posrednika From 4527526177b1bc54302ac467b8823125b1438f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 07:09:35 +0200 Subject: [PATCH 3/8] scaffoldan doku do kraja pormjenjen malo posrednik --- .../{Fiskal2Tests.cs => PosredniciTests.cs} | 13 --- MAES.Fiskal2/Posrednici/Doku.cs | 110 +++++++++++++++++- README.md | 28 +++-- 3 files changed, 121 insertions(+), 30 deletions(-) rename MAES.Fiskal2.Tests/{Fiskal2Tests.cs => PosredniciTests.cs} (92%) diff --git a/MAES.Fiskal2.Tests/Fiskal2Tests.cs b/MAES.Fiskal2.Tests/PosredniciTests.cs similarity index 92% rename from MAES.Fiskal2.Tests/Fiskal2Tests.cs rename to MAES.Fiskal2.Tests/PosredniciTests.cs index 73b1a7e..a698fb1 100644 --- a/MAES.Fiskal2.Tests/Fiskal2Tests.cs +++ b/MAES.Fiskal2.Tests/PosredniciTests.cs @@ -26,19 +26,6 @@ public class PosredniciTests(ITestOutputHelper output) { ApiKey = Environment.GetEnvironmentVariable("DOKU_API_KEY") ?? throw new InvalidOperationException("DOKU_API_KEY environment variable is not set."), IsDev = true - }, - new Fina - { - OIB = "51560545524", - //Certificate = ..., - IsDev = true - }, - new MER - { - OIB = "51560545524", - Username = Environment.GetEnvironmentVariable("MER_USERNAME") ?? throw new InvalidOperationException("MER_USERNAME environment variable is not set."), - Password = Environment.GetEnvironmentVariable("MER_PASSWORD") ?? throw new InvalidOperationException("MER_PASSWORD environment variable is not set."), - IsDev = true } ]; diff --git a/MAES.Fiskal2/Posrednici/Doku.cs b/MAES.Fiskal2/Posrednici/Doku.cs index befe3e3..e04946d 100644 --- a/MAES.Fiskal2/Posrednici/Doku.cs +++ b/MAES.Fiskal2/Posrednici/Doku.cs @@ -30,7 +30,7 @@ public Doku() /// /// Evidentira UBL dokument u Doku sustavu. Doku očekuje Base64 enkodirani XML sadržaj UBL-a unutar JSON objekta s ključem "xml". /// - /// + /// UBL dokument. /// Token za otkazivanje operacije. /// /// @@ -48,7 +48,7 @@ public override async Task EvidentirajUBLAsync(string ubl, CancellationToken tok /// Datum početka razdoblja. /// Datum kraja razdoblja. /// Token za otkazivanje operacije. - /// + /// Lista izlaznih e-računa. public override async Task> IzlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) { var res = await SendRequest(HttpMethod.Get, $"/documents/invoices/outgoing?IssueDateFrom={from:O}&IssueDateTo={to:O}", null, token); @@ -75,4 +75,110 @@ public override async Task> IzlazniListAsync(DateTime } return list; } + + /// + /// Dohvaća UBL/XML sadržaj izlaznog računa. Doku vraća Base64 enkodirani XML sadržaj unutar JSON objekta s ključem "xml". + /// + /// ID izlaznog računa. + /// Token za otkazivanje operacije. + /// Base64 enkodirani XML sadržaj izlaznog računa. + public override async Task IzlazniUBLAsync(string id, CancellationToken token = default) + { + var content = await SendRequest(HttpMethod.Get, $"/documents/invoices/outgoing/{id}/download?format=ubl", null, token); + var doc = JsonDocument.Parse(content); + return doc.RootElement.GetProperty("data").GetProperty("xml").GetString()!; + } + + /// + /// Evidentira uplatu za izlazni račun. + /// + /// ID izlaznog računa. + /// Datum uplate. + /// Iznos uplate. + /// Način plaćanja. + /// Token za otkazivanje operacije. + /// + public override async Task EvidentirajUplatuAsync(string id, DateTime date, double amount, NacinPlacanja paymentMethod, CancellationToken token = default) + { + await SendRequest(HttpMethod.Post, $"/documents/invoices/outgoing/{id}/payments", new + { + datumNaplate = date, + naplaceniIznos = amount, + nacinPlacanja = paymentMethod switch + { + NacinPlacanja.ObračunskoPlaćanje => "Z", + NacinPlacanja.TransakcijskiRaCun => "T", + _ => "O" + } + }, token); + } + + /// + /// Dohvaća ulazne e-račune za zadano razdoblje. + /// + /// Početni datum. + /// Završni datum. + /// Token za otkazivanje operacije. + /// Lista ulaznih e-računa. + public override async Task> UlazniListAsync(DateTime from, DateTime to, CancellationToken token = default) + { + var content = await SendRequest(HttpMethod.Get, $"/documents/invoices/incoming?IssueDateFrom={from:O}&IssueDateTo={to:O}", null, token); + var doc = JsonDocument.Parse(content); + var list = new List(); + foreach (var item in doc.RootElement.GetProperty("records").EnumerateArray()) + { + list.Add(new UlazniERacun + { + Id = item.GetProperty("id").GetInt64().ToString(), + Broj = item.GetProperty("name").GetString()!, + Datum = item.GetProperty("issueDate").GetDateTime(), + Partner = item.GetProperty("sender").GetProperty("name").GetString()!, + PartnerOIB = item.GetProperty("sender").GetProperty("oib").GetString()!, + Status = item.GetProperty("status").GetString() switch + { + "RECIVED" => UlazniERacunStatus.Zaprimljeno, // <-- ovo treba popravit mozda treba pogledat dokumentaciju + "APPROVED" => UlazniERacunStatus.Odobreno, + "REJECTED" => UlazniERacunStatus.Odbijeno, + _ => UlazniERacunStatus.Likvidirano + } + }); + } + return list; + } + + /// + /// Dohvaća UBL/XML sadržaj ulaznog računa. Doku vraća Base64 enkodirani XML sadržaj. + /// + /// ID ulaznog računa. + /// Token za otkazivanje operacije. + /// Base64 enkodirani XML sadržaj ulaznog računa. + public override async Task UlazniUBLAsync(string id, CancellationToken token = default) + { + var content = await SendRequest(HttpMethod.Get, $"/documents/invoices/incoming/{id}/export", null, token); + var doc = JsonDocument.Parse(content); + return doc.RootElement.GetProperty("data").GetProperty("xml").GetString()!; + } + + /// + /// Odbija ulazni račun uz navedeni razlog i opis. + /// + /// ID ulaznog računa. + /// Razlog odbijanja. + /// Opis odbijanja. + /// Token za otkazivanje operacije. + /// + public override async Task OdbijRacunAsync(string id, RazlogOdbijanja razlog, string opis, CancellationToken token = default) + { + await SendRequest(HttpMethod.Post, $"/documents/invoices/incoming/{id}/reject", new + { + datumOdbijanja = DateTime.Now.ToString("yyyy-MM-dd"), + vrstaRazlogaOdbijanja = razlog switch + { + RazlogOdbijanja.NeusklađenostKojaNeUtjeceNaObracunPoreza => "N", + RazlogOdbijanja.NeusklađenostKojaUtjeceNaObracunPoreza => "U", + _ => "O" + }, + razlogOdbijanja = opis + }, token); + } } \ No newline at end of file diff --git a/README.md b/README.md index a42939c..974b496 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,18 @@ U `Posrednici/` direktoriju nalaze se konkretne implementacije | Značajka / posrednik | `Super` | `EPoslovanje` | `Fina` | `MER` | `Redok` | `Tvoj eRačun` | `Doku` | |---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| Ulazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | -| Ulazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | -| Izlazni računi (datum) | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | -| Izlazni računi (paginacija) | ❌ | ❌ | ❌* | ❌ | ❌ | ❌ | ❌ | -| Dohvat UBL sadržaja | ✅ | ✅ | ❌* | ✅ | ❌ | ❌ | ❌ | -| Dohvat PDF sadržaja | ✅ | ✅ | ❌* | ❌** | ❌ | ❌ | ❌ | -| Evidentiranje UBL dokumenta | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | -| Evidentiranje uplate | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | -| Odbijanje računa | ✅ | ✅ | ❌* | ❌ | ❌ | ❌ | ❌ | - -\* Fina nema pola ovih api callova ili su na nekom drugom endpointu treba vidit - -\*\* MER ne podržava dohvat PDF byte[] +| Dohvat ulaznih računa | ✅ | ✅ | ❌ | 🚧 | 🚧 | 🚧 | ⚠️ | +| Dohvat izlaznih računa | ✅ | ✅ | ❌ | 🚧 | 🚧 | 🚧 | ⚠️ | +| Dohvat UBL sadržaja | ✅ | ✅ | ❌ | ⚠️ | 🚧 | 🚧 | ⚠️ | +| Dohvat PDF sadržaja | ✅ | ✅ | ❌ | ❌ | 🚧 | 🚧 | ❌ | +| Evidentiranje UBL dokumenta | ✅ | ✅ | ⚠️ | ⚠️ | 🚧 | 🚧 | ⚠️ | +| Evidentiranje uplate | ✅ | ✅ | ❌ | 🚧 | 🚧 | 🚧 | ⚠️ | +| Odbijanje računa | ✅ | ✅ | ❌ | 🚧 | 🚧 | 🚧 | ⚠️ | + +* ✅ — Implementirano +* ⚠️ — Nije testirano/Ne prolazi testove +* 🚧 — Nije još implementirano +* ❌ — Posrednik ne podržava ## Instalacija @@ -107,9 +106,8 @@ var posrednik = new MER ### Primjer korištenja posrednika ```csharp -// dohvat računa u razdoblju zadnjih mj. dana ili dohvati paginacijom zavisi sto posrednik podržava +// dohvat računa u razdoblju zadnjih mj. dana var racuni = posrednik.UlazniListAsync(DateTime.Now.AddMonths(-1), DateTime.Now); -var racuni = posrednik.UlazniListAsync(1, 20); var racun = racuni.FirstOrDefault(); if(racun != null) From 95164df9083a8aee32cb042260d157da63f165b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 07:40:20 +0200 Subject: [PATCH 4/8] masu toga promjenjeno --- MAES.Fiskal2/ClientCreatedEventArgs.cs | 13 ++++ MAES.Fiskal2/Posrednici/Doku.cs | 6 +- MAES.Fiskal2/Posrednici/EPoslovanje.cs | 99 +++++++++++--------------- MAES.Fiskal2/Posrednici/MER.cs | 28 ++------ MAES.Fiskal2/Posrednici/Super.cs | 75 ++++++------------- MAES.Fiskal2/Posrednik.cs | 52 +++++--------- 6 files changed, 101 insertions(+), 172 deletions(-) create mode 100644 MAES.Fiskal2/ClientCreatedEventArgs.cs diff --git a/MAES.Fiskal2/ClientCreatedEventArgs.cs b/MAES.Fiskal2/ClientCreatedEventArgs.cs new file mode 100644 index 0000000..b94d918 --- /dev/null +++ b/MAES.Fiskal2/ClientCreatedEventArgs.cs @@ -0,0 +1,13 @@ +namespace MAES.Fiskal2; + +/// +/// Podaci događaja koji se prosljeđuju prilikom pokretanja događaja . Sadrži referencu na kreirani koji se koristi za slanje zahtjeva prema posredniku. +/// +/// Kreirani HTTP klijent. +public class ClientCreatedEventArgs(HttpClient client) : EventArgs +{ + /// + /// Kreirani HTTP klijent koji se koristi za komunikaciju s posrednikom. Dopušta dodatnu konfiguraciju (npr. dodavanje zaglavlja) prije slanja zahtjeva. + /// + public HttpClient Client { get; } = client; +} \ No newline at end of file diff --git a/MAES.Fiskal2/Posrednici/Doku.cs b/MAES.Fiskal2/Posrednici/Doku.cs index e04946d..f3bfc00 100644 --- a/MAES.Fiskal2/Posrednici/Doku.cs +++ b/MAES.Fiskal2/Posrednici/Doku.cs @@ -28,7 +28,7 @@ public Doku() } /// - /// Evidentira UBL dokument u Doku sustavu. Doku očekuje Base64 enkodirani XML sadržaj UBL-a unutar JSON objekta s ključem "xml". + /// Evidentira UBL dokument u Doku sustavu. /// /// UBL dokument. /// Token za otkazivanje operacije. @@ -77,7 +77,7 @@ public override async Task> IzlazniListAsync(DateTime } /// - /// Dohvaća UBL/XML sadržaj izlaznog računa. Doku vraća Base64 enkodirani XML sadržaj unutar JSON objekta s ključem "xml". + /// Dohvaća UBL/XML sadržaj izlaznog računa. /// /// ID izlaznog računa. /// Token za otkazivanje operacije. @@ -147,7 +147,7 @@ public override async Task> UlazniListAsync(DateTime f } /// - /// Dohvaća UBL/XML sadržaj ulaznog računa. Doku vraća Base64 enkodirani XML sadržaj. + /// Dohvaća UBL/XML sadržaj ulaznog računa. /// /// ID ulaznog računa. /// Token za otkazivanje operacije. diff --git a/MAES.Fiskal2/Posrednici/EPoslovanje.cs b/MAES.Fiskal2/Posrednici/EPoslovanje.cs index a3e26f5..d9cff9f 100644 --- a/MAES.Fiskal2/Posrednici/EPoslovanje.cs +++ b/MAES.Fiskal2/Posrednici/EPoslovanje.cs @@ -24,6 +24,8 @@ public class EPoslovanje : Posrednik /// public string Password { get; set; } = ""; + string apiKey = ""; + /// /// Inicijalizira novog ePoslovanje posrednika s definiranim URI postavkama za produkcijsko i razvojno okruženje. /// @@ -31,59 +33,40 @@ public EPoslovanje() { BaseAddressProd = "https://eracun.eposlovanje.hr"; BaseAddressDev = "https://test.eposlovanje.hr"; - } - - HttpClient createClient() - { - var client = new HttpClient - { - BaseAddress = new Uri(BaseAddress) - }; - - 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") + OnClientCreated += async (s, e) => { - Content = new StringContent(JsonSerializer.Serialize(new + if(string.IsNullOrWhiteSpace(apiKey)) { - username = Username, - password = Password, - vatId = OIB, - softwareId = "MAES.Fiskal2" - }), Encoding.UTF8, "application/json") + var client = new HttpClient + { + BaseAddress = new Uri(IsDev ? "https://test.eposlovanje.hr" : "https://eracun.eposlovanje.hr") + }; + + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var request = new HttpRequestMessage(HttpMethod.Post, "/api/v2/account/apikey") + { + 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); + apiKey = doc.RootElement.GetProperty("apiKey").GetString()!; + } + + e.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); }; - - 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) @@ -97,28 +80,29 @@ async Task changeStatusAsync(string id, int status, string? note = null, double? 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); + await SendRequest(HttpMethod.Post, $"/api/v2/document/changestatus/{id}", body, token); } /// /// Dohvaća XML/UBL sadržaj ulaznog računa. /// public override async Task UlazniUBLAsync(string id, CancellationToken token = default) => - (await sendRequest(HttpMethod.Get, $"/api/v2/document/get/{id}", null, token)).RootElement.GetProperty("document").GetString()!; + JsonDocument.Parse(await SendRequest(HttpMethod.Get, $"/api/v2/document/get/{id}", null, token)).RootElement.GetProperty("document").GetString()!; /// /// Dohvaća PDF sadržaj ulaznog računa. /// public override 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()!); + Convert.FromBase64String(JsonDocument.Parse(await SendRequest(HttpMethod.Get, $"/api/v2/document/visualization/{id}", null, token)).RootElement.GetProperty("pdf").GetString()!); /// /// Dohvaća popis ulaznih e-računa. /// public override 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 content = await SendRequest(HttpMethod.Get, $"/api/v2/document/incoming?insertedFrom={from:O}&insertedTo={to:O}&limit=1000&offset=0", null, token); + var doc = JsonDocument.Parse(content); + var list = new List(); foreach (var item in doc.RootElement.EnumerateArray()) @@ -157,7 +141,8 @@ public override async Task> IzlazniListAsync( DateTime to, CancellationToken token = default) { - var doc = await sendRequest(HttpMethod.Get, $"/api/v2/document/outgoing?insertedFrom={from:O}&insertedTo={to:O}&limit=1000&offset=0", null, token); + var content = await SendRequest(HttpMethod.Get, $"/api/v2/document/outgoing?insertedFrom={from:O}&insertedTo={to:O}&limit=1000&offset=0", null, token); + var doc = JsonDocument.Parse(content); var list = new List(); @@ -180,7 +165,7 @@ public override async Task> IzlazniListAsync( /// /// Evidentira UBL dokument u ePoslovanje sustav. /// - public override async Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) => await sendRequest(HttpMethod.Post, "/api/v2/document/send", new + public override async Task EvidentirajUBLAsync(string ubl, CancellationToken token = default) => await SendRequest(HttpMethod.Post, "/api/v2/document/send", new { document = ubl, softwareId = "MAES.Blagajna" diff --git a/MAES.Fiskal2/Posrednici/MER.cs b/MAES.Fiskal2/Posrednici/MER.cs index bcc70af..7bfe60d 100644 --- a/MAES.Fiskal2/Posrednici/MER.cs +++ b/MAES.Fiskal2/Posrednici/MER.cs @@ -35,26 +35,6 @@ public MER() BaseAddressDev = "https://demo.moj-eracun.hr"; } - async Task sendRequest(HttpMethod method, string url, object? body, CancellationToken token) - { - var client = new HttpClient - { - BaseAddress = new Uri(BaseAddress) - }; - - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - 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 json; - } - /// /// Šalje UBL/XML dokument izlaznog e-računa na Moj-eRačun servis. /// Dokument se validira i obrađuje, a u slučaju uspješnog slanja @@ -62,7 +42,7 @@ async Task sendRequest(HttpMethod method, string url, object? body, Canc /// /// UBL XML sadržaj e-računa. /// Token za otkazivanje operacije. - public override async Task EvidentirajUBLAsync(string ubl, CancellationToken cancellationToken = default) => await sendRequest(HttpMethod.Post, "/apis/v2/send", new + public override async Task EvidentirajUBLAsync(string ubl, CancellationToken cancellationToken = default) => await SendRequest(HttpMethod.Post, "/apis/v2/send", new { Username, Password, @@ -76,7 +56,7 @@ async Task sendRequest(HttpMethod method, string url, object? body, Canc /// /// Identifikator dokumenta. /// Token za otkazivanje operacije. - public override async Task UlazniUBLAsync(string id, CancellationToken cancellationToken = default) => await sendRequest(HttpMethod.Post, $"/apis/v2/receive", new + public override async Task UlazniUBLAsync(string id, CancellationToken cancellationToken = default) => await SendRequest(HttpMethod.Post, $"/apis/v2/receive", new { Username, Password, @@ -146,7 +126,7 @@ public override Task> IzlazniListAsync( /// Token za otkazivanje operacije. public override async Task EvidentirajUplatuAsync(string id, DateTime date, double amount, NacinPlacanja paymentMethod, CancellationToken cancellationToken = default) { - await sendRequest(HttpMethod.Post, $"/api/fiscalization/markPaid", new + await SendRequest(HttpMethod.Post, $"/api/fiscalization/markPaid", new { Username, Password, @@ -168,7 +148,7 @@ public override async Task EvidentirajUplatuAsync(string id, DateTime date, doub /// Token za otkazivanje operacije. public override async Task OdbijRacunAsync(string id, RazlogOdbijanja razlog, string opis, CancellationToken cancellationToken = default) { - await sendRequest(HttpMethod.Post, $"/api/fiscalization/rejectWithoutElectronicID", new + await SendRequest(HttpMethod.Post, $"/api/fiscalization/rejectWithoutElectronicID", new { Username, Password, diff --git a/MAES.Fiskal2/Posrednici/Super.cs b/MAES.Fiskal2/Posrednici/Super.cs index 284bd3f..6e79137 100644 --- a/MAES.Fiskal2/Posrednici/Super.cs +++ b/MAES.Fiskal2/Posrednici/Super.cs @@ -33,48 +33,10 @@ public Super() { BaseAddressProd = "https://api.super.hr/"; BaseAddressDev = "https://apitest.super.hr/"; - } - - async Task postRequest(string uri, Dictionary body, CancellationToken cancellationToken) - { - using var client = new HttpClient(); - client.BaseAddress = new Uri(BaseAddress); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - if (token == null || DateTime.UtcNow >= token.Value.Value) + OnClientCreated += (s, e) => { - using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "Token"); - tokenRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - tokenRequest.Content = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "password", ["username"] = Username, ["password"] = Password }); - - var tokenResponse = await client.SendAsync(tokenRequest, cancellationToken); - - if (!tokenResponse.IsSuccessStatusCode) throw new HttpRequestException($"Greška prilikom dohvaćanja tokena"); - - using var doc = JsonDocument.Parse(await tokenResponse.Content.ReadAsStringAsync()); - - token = new KeyValuePair( - doc.RootElement.GetProperty("access_token").GetString()!, - DateTime.Now.AddSeconds(doc.RootElement.GetProperty("expires_in").GetInt32() - 60) - ); - } - - body.Add("MessageId", Guid.NewGuid().ToString()); - body.Add("CompanyGuid", BusinessGuid); - - using var request = new HttpRequestMessage(HttpMethod.Post, uri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token!.Value.Key); - request.Content = new FormUrlEncodedContent(body); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - - var response = await client.SendAsync(request, cancellationToken); - - if (!response.IsSuccessStatusCode) throw new HttpRequestException($"Greška prilikom slanja zahtjeva: {uri}"); - - var jsonDocument = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); - if(jsonDocument.RootElement.TryGetProperty("errorMessage", out var errorMessage) && errorMessage.GetString() is string error && !string.IsNullOrWhiteSpace(error)) throw new Exception(error); - return jsonDocument; + e.Client.DefaultRequestHeaders.TryAddWithoutValidation("Bearer", token!.Value.Key); + }; } /// @@ -85,12 +47,12 @@ async Task postRequest(string uri, Dictionary body /// XML/UBL sadržaj računa kao tekst. public override async Task UlazniUBLAsync(string id, CancellationToken cancellationToken = default) { - var jDoc = await postRequest("api/Invoice/GetInvoice", new Dictionary + var content = await SendRequest(HttpMethod.Post, "api/Invoice/GetInvoice", new Dictionary { ["Guid"] = id.ToString() }, cancellationToken); - if (!jDoc.RootElement.TryGetProperty("invoiceUbl", out var el)) throw new Exception("UBL not found in response"); + if (!JsonDocument.Parse(content).RootElement.TryGetProperty("invoiceUbl", out var el)) throw new Exception("UBL not found in response"); return Encoding.UTF8.GetString(Convert.FromBase64String(el.GetString())); } @@ -100,7 +62,7 @@ public override async Task UlazniUBLAsync(string id, CancellationToken c /// Identifikator ulaznog računa. /// Token za otkazivanje operacije. /// PDF sadržaj računa kao bajtni niz. - public override async Task UlazniPdfAsync(string id, CancellationToken cancellationToken = default) => Convert.FromBase64String((await postRequest("api/Invoice/GetInvoiceDetailVisualization", new Dictionary + public override async Task UlazniPdfAsync(string id, CancellationToken cancellationToken = default) => Convert.FromBase64String(JsonDocument.Parse(await SendRequest(HttpMethod.Post, "api/Invoice/GetInvoiceDetailVisualization", new Dictionary { ["Guid"] = id.ToString() }, cancellationToken)).RootElement.GetProperty("invoiceDetailVisualization").GetString()!); @@ -114,13 +76,13 @@ public override async Task UlazniUBLAsync(string id, CancellationToken c /// Popis ulaznih e-računa. public override async Task> UlazniListAsync(DateTime from, DateTime to, CancellationToken cancellationToken = default) { - var jsonDocument = await postRequest("api/Invoice/GetInvoiceList", new Dictionary + var content = await SendRequest(HttpMethod.Post, "api/Invoice/GetInvoiceList", new Dictionary { ["DateFrom"] = from.ToString("yyyy-MM-dd"), ["DateTo"] = to.ToString("yyyy-MM-dd") }, cancellationToken); - return jsonDocument.RootElement.GetProperty("invoices").EnumerateArray().Select(x => new UlazniERacun + return JsonDocument.Parse(content).RootElement.GetProperty("invoices").EnumerateArray().Select(x => new UlazniERacun { Broj = x.GetProperty("UniqueId").GetString() ?? "", Datum = x.GetProperty("IssueDate").GetDateTime(), @@ -143,12 +105,13 @@ public override async Task> UlazniListAsync(DateTime f /// XML/UBL sadržaj računa kao tekst. public override async Task IzlazniUBLAsync(string id, CancellationToken cancellationToken = default) { - var jDoc = await postRequest("api/SendingInvoice/GetSendingInvoice", new Dictionary + var content = await SendRequest(HttpMethod.Post, "api/SendingInvoice/GetSendingInvoice", new Dictionary { ["Guid"] = id.ToString() }, cancellationToken); + var doc = JsonDocument.Parse(content); - if (!jDoc.RootElement.TryGetProperty("sendingInvoiceUbl", out var el)) throw new Exception("UBL not found in response"); + if (!doc.RootElement.TryGetProperty("sendingInvoiceUbl", out var el)) throw new Exception("UBL not found in response"); return Encoding.UTF8.GetString(Convert.FromBase64String(el.GetString())); } @@ -161,12 +124,12 @@ public override async Task IzlazniUBLAsync(string id, CancellationToken /// PDF sadržaj računa kao bajtni niz. public override async Task IzlazniPdfAsync(string id, CancellationToken cancellationToken = default) { - var jDoc = await postRequest("api/SendingInvoice/GetSendingInvoiceDetailVisualization", new Dictionary + var content = await SendRequest(HttpMethod.Post, "api/SendingInvoice/GetSendingInvoiceDetailVisualization", new Dictionary { ["Guid"] = id.ToString() }, cancellationToken); - var root = jDoc.RootElement; + var root = JsonDocument.Parse(content).RootElement; if (!root.TryGetProperty("sendingInvoiceDetailVisualization", out var el)) throw new Exception("PDF not found in response"); return Convert.FromBase64String(el.GetString()); @@ -182,13 +145,15 @@ public override async Task IzlazniPdfAsync(string id, CancellationToken /// Popis izlaznih e-računa. public override async Task> IzlazniListAsync(DateTime from, DateTime to, CancellationToken cancellationToken) { - var jsonDocument = await postRequest("api/SendingInvoice/GetSendingInvoiceList", new Dictionary + var content = await SendRequest(HttpMethod.Post, "api/SendingInvoice/GetSendingInvoiceList", new Dictionary { ["DateFrom"] = from.ToString("yyyy-MM-dd"), ["DateTo"] = to.ToString("yyyy-MM-dd") }, cancellationToken); - return jsonDocument.RootElement.GetProperty("invoices").EnumerateArray().Select(x => new IzlazniERacun + var doc = JsonDocument.Parse(content); + + return doc.RootElement.GetProperty("invoices").EnumerateArray().Select(x => new IzlazniERacun { Broj = x.GetProperty("UniqueId").GetString() ?? "", Datum = x.GetProperty("IssueDate").GetDateTime(), @@ -208,7 +173,7 @@ public override async Task> IzlazniListAsync(DateTime /// /// UBL XML dokument ulaznog računa. /// Token za otkazivanje operacije. - public override async Task EvidentirajUBLAsync(string ubl, CancellationToken cancellationToken = default) => await postRequest("api/SendingInvoice/GetSendingInvoiceList", new Dictionary + public override async Task EvidentirajUBLAsync(string ubl, CancellationToken cancellationToken = default) => await SendRequest(HttpMethod.Post, "api/SendingInvoice/GetSendingInvoiceList", new Dictionary { ["Base64EncodedUbl"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(ubl)), ["UblDocumentType"] = "1", // 1 = račun, 2 = odobrenje @@ -224,7 +189,7 @@ public override async Task> IzlazniListAsync(DateTime /// Token za otkazivanje operacije. public override async Task EvidentirajUplatuAsync(string id, DateTime date, double amount, NacinPlacanja paymentMethod, CancellationToken cancellationToken = default) { - await postRequest("api/Invoice/SetInvoicePayment", new Dictionary + await SendRequest(HttpMethod.Post, "api/Invoice/SetInvoicePayment", new Dictionary { ["Guid"] = id.ToString() }, cancellationToken); @@ -239,7 +204,7 @@ public override async Task EvidentirajUplatuAsync(string id, DateTime date, doub /// Token za otkazivanje operacije. public override async Task OdbijRacunAsync(string id, RazlogOdbijanja razlog, string opis, CancellationToken cancellationToken = default) { - await postRequest("api/SendingInvoice/RejectSendingInvoice", new Dictionary + await SendRequest(HttpMethod.Post, "api/SendingInvoice/RejectSendingInvoice", new Dictionary { ["Guid"] = id.ToString(), ["RejectReasonType"] = razlog.ToString(), diff --git a/MAES.Fiskal2/Posrednik.cs b/MAES.Fiskal2/Posrednik.cs index 2d0512f..96954fc 100644 --- a/MAES.Fiskal2/Posrednik.cs +++ b/MAES.Fiskal2/Posrednik.cs @@ -33,10 +33,15 @@ public abstract class Posrednik /// protected string BaseAddressDev { private get; set; } = ""; + /// + /// Događaj koji se pokreće nakon kreiranja HTTP klijenta unutar metode . Omogućuje dodatnu konfiguraciju klijenta prije slanja zahtjeva. + /// + protected event EventHandler? OnClientCreated; + /// /// Aktivni URI servisa ovisno o odabranom okruženju. /// - protected string BaseAddress => IsDev ? BaseAddressDev : BaseAddressProd; + public string BaseAddress => IsDev ? BaseAddressDev : BaseAddressProd; /// /// Evidentira izlazni eRačun na temelju UBL sadržaja. @@ -77,10 +82,7 @@ public virtual Task EvidentirajUplatuAsync( /// Identifikator računa. /// Token za otkazivanje operacije. /// Sadržaj PDF dokumenta kao niz bajtova. - public virtual Task IzlazniPdfAsync( - string id, - CancellationToken token = default) => - throw new NotImplementedException(); + public virtual Task IzlazniPdfAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); /// /// Dohvaća UBL XML izlaznog računa. @@ -88,24 +90,16 @@ public virtual Task IzlazniPdfAsync( /// Identifikator računa. /// Token za otkazivanje operacije. /// UBL XML sadržaj računa. - public virtual Task IzlazniUBLAsync( - string id, - CancellationToken token = default) => - throw new NotImplementedException(); + public virtual Task IzlazniUBLAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); /// - /// Odbija račun uz navedeni razlog i opis. + /// Odbija ulazni eRačun uz navedeni razlog i opis. /// /// Identifikator računa. /// Razlog odbijanja. /// Dodatni opis odbijanja. /// Token za otkazivanje operacije. - public virtual Task OdbijRacunAsync( - string id, - RazlogOdbijanja razlog, - string opis, - CancellationToken token = default) => - throw new NotImplementedException(); + public virtual Task OdbijRacunAsync(string id, RazlogOdbijanja razlog, string opis, CancellationToken token = default) => throw new NotImplementedException(); /// /// Dohvaća popis ulaznih eRačuna unutar zadanog razdoblja. @@ -132,6 +126,15 @@ public virtual Task OdbijRacunAsync( /// UBL XML sadržaj računa. public virtual Task UlazniUBLAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); + /// + /// Prihvaća ulazni eRačun, označavajući ga kao zaprimljen ili odobren (ovisno o implementaciji posrednika). + /// + /// Identifikator računa. + /// Token za otkazivanje operacije. + /// + /// + public virtual Task PrihvatiRacunAsync(string id, CancellationToken token = default) => throw new NotImplementedException(); + /// /// Generička metoda za slanje HTTP zahtjeva prema posredniku. Koristi se unutar specifičnih implementacija posrednika za komunikaciju s njihovim API-jem. /// @@ -162,21 +165,4 @@ protected async Task SendRequest(HttpMethod method, string url, object? return content; } - - /// - /// Događaj koji se pokreće nakon kreiranja HTTP klijenta unutar metode . Omogućuje dodatnu konfiguraciju klijenta prije slanja zahtjeva. - /// - protected event EventHandler? OnClientCreated; -} - -/// -/// Podaci događaja koji se prosljeđuju prilikom pokretanja događaja . Sadrži referencu na kreirani koji se koristi za slanje zahtjeva prema posredniku. -/// -/// Kreirani HTTP klijent. -public class ClientCreatedEventArgs(HttpClient client) : EventArgs -{ - /// - /// Kreirani HTTP klijent koji se koristi za komunikaciju s posrednikom. Dopušta dodatnu konfiguraciju (npr. dodavanje zaglavlja) prije slanja zahtjeva. - /// - public HttpClient Client { get; } = client; } \ No newline at end of file From 7c6804d054a5faf688d5fe4bfbd715d92433bd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 07:50:41 +0200 Subject: [PATCH 5/8] popravljen readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 974b496..637ad1c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ U `Posrednici/` direktoriju nalaze se konkretne implementacije | Evidentiranje UBL dokumenta | ✅ | ✅ | ⚠️ | ⚠️ | 🚧 | 🚧 | ⚠️ | | Evidentiranje uplate | ✅ | ✅ | ❌ | 🚧 | 🚧 | 🚧 | ⚠️ | | Odbijanje računa | ✅ | ✅ | ❌ | 🚧 | 🚧 | 🚧 | ⚠️ | +| Prihvaćanje računa | ❌ | 🚧 | ❌ | 🚧 | 🚧 | 🚧 | 🚧 | * ✅ — Implementirano * ⚠️ — Nije testirano/Ne prolazi testove @@ -175,7 +176,11 @@ Abstraktna klasa `Posrednik` nudi sljedeće metode: - `Task OdbijRacunAsync(string id, RazlogOdbijana razlog, string opis)` - Odbija račun + Odbija ulazni eRačun + +- `Task PrihvatiRacunAsync(string id)` + + Prihvaća ulazni eRačun ## Izgradnja i pakiranje From d36c9405f41dfe3e51dd263258dede1179fe6f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 07:59:29 +0200 Subject: [PATCH 6/8] popravljen yml --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 614229d..d2e2182 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,9 @@ jobs: SUPER_BUSINESS_GUID: ${{ secrets.SUPER_BUSINESS_GUID }} SUPER_USERNAME: ${{ secrets.SUPER_USERNAME }} SUPER_PASSWORD: ${{ secrets.SUPER_PASSWORD }} + EPOSLOVANJE_USERNAME: ${{ secrets.EPOSLOVANJE_USERNAME }} + EPOSLOVANJE_PASSWORD: ${{ secrets.EPOSLOVANJE_PASSWORD }} + DOKU_API_KEY: ${{ secrets.DOKU_API_KEY }} steps: - name: Checkout repository From 7ce9a89104a8315b61862ced1eac1d054bf5154e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 08:07:00 +0200 Subject: [PATCH 7/8] popravljen output --- MAES.Fiskal2.Tests/PosredniciTests.cs | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/MAES.Fiskal2.Tests/PosredniciTests.cs b/MAES.Fiskal2.Tests/PosredniciTests.cs index a698fb1..bf9124e 100644 --- a/MAES.Fiskal2.Tests/PosredniciTests.cs +++ b/MAES.Fiskal2.Tests/PosredniciTests.cs @@ -4,7 +4,7 @@ namespace MAES.Fiskal2.Tests; -public class PosredniciTests(ITestOutputHelper output) +public class PosredniciTests { readonly List posrednici = [ @@ -37,12 +37,12 @@ public async Task EvidentirajUBL() try { await posrednik.EvidentirajUBLAsync(File.ReadAllText("ubl.xml")); - output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUBL OK"); + Console.WriteLine($"{posrednik.GetType().Name}: EvidentirajUBL OK"); } catch (Exception ex) { - output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUBL FAIL"); - output.WriteLine(ex.ToString()); + Console.WriteLine($"{posrednik.GetType().Name}: EvidentirajUBL FAIL"); + Console.WriteLine(ex.ToString()); } } } @@ -58,12 +58,12 @@ public async Task DohvatiIzlazneRacune() DateTime.UtcNow.AddDays(-30), DateTime.UtcNow); - output.WriteLine($"{posrednik.GetType().Name}: IzlazniList OK ({izlazni.Count()})"); + Console.WriteLine($"{posrednik.GetType().Name}: IzlazniList OK ({izlazni.Count()})"); } catch (Exception ex) { - output.WriteLine($"{posrednik.GetType().Name}: IzlazniList FAIL"); - output.WriteLine(ex.ToString()); + Console.WriteLine($"{posrednik.GetType().Name}: IzlazniList FAIL"); + Console.WriteLine(ex.ToString()); } } } @@ -83,19 +83,19 @@ public async Task DohvatiPrviIzlazniPdfIUBL() if (first == null) { - output.WriteLine($"{posrednik.GetType().Name}: nema izlaznih računa"); + Console.WriteLine($"{posrednik.GetType().Name}: nema izlaznih računa"); continue; } await posrednik.IzlazniPdfAsync(first.Id); await posrednik.IzlazniUBLAsync(first.Id); - output.WriteLine($"{posrednik.GetType().Name}: Izlazni PDF + UBL OK"); + Console.WriteLine($"{posrednik.GetType().Name}: Izlazni PDF + UBL OK"); } catch (Exception ex) { - output.WriteLine($"{posrednik.GetType().Name}: Izlazni PDF + UBL FAIL"); - output.WriteLine(ex.ToString()); + Console.WriteLine($"{posrednik.GetType().Name}: Izlazni PDF + UBL FAIL"); + Console.WriteLine(ex.ToString()); } } } @@ -115,7 +115,7 @@ public async Task EvidentirajUplatu() if (first == null) { - output.WriteLine($"{posrednik.GetType().Name}: nema izlaznih računa za uplatu"); + Console.WriteLine($"{posrednik.GetType().Name}: nema izlaznih računa za uplatu"); continue; } @@ -125,12 +125,12 @@ await posrednik.EvidentirajUplatuAsync( 100, NacinPlacanja.TransakcijskiRaCun); - output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUplatu OK"); + Console.WriteLine($"{posrednik.GetType().Name}: EvidentirajUplatu OK"); } catch (Exception ex) { - output.WriteLine($"{posrednik.GetType().Name}: EvidentirajUplatu FAIL"); - output.WriteLine(ex.ToString()); + Console.WriteLine($"{posrednik.GetType().Name}: EvidentirajUplatu FAIL"); + Console.WriteLine(ex.ToString()); } } } @@ -150,19 +150,19 @@ public async Task UlazniRacuni() if (first == null) { - output.WriteLine($"{posrednik.GetType().Name}: nema ulaznih računa"); + Console.WriteLine($"{posrednik.GetType().Name}: nema ulaznih računa"); continue; } await posrednik.UlazniPdfAsync(first.Id); await posrednik.UlazniUBLAsync(first.Id); - output.WriteLine($"{posrednik.GetType().Name}: Ulazni PDF + UBL OK"); + Console.WriteLine($"{posrednik.GetType().Name}: Ulazni PDF + UBL OK"); } catch (Exception ex) { - output.WriteLine($"{posrednik.GetType().Name}: Ulazni FAIL"); - output.WriteLine(ex.ToString()); + Console.WriteLine($"{posrednik.GetType().Name}: Ulazni FAIL"); + Console.WriteLine(ex.ToString()); } } } @@ -182,7 +182,7 @@ public async Task OdbijPrviUlazniRacun() if (first == null) { - output.WriteLine($"{posrednik.GetType().Name}: nema ulaznih za odbijanje"); + Console.WriteLine($"{posrednik.GetType().Name}: nema ulaznih za odbijanje"); continue; } @@ -191,12 +191,12 @@ await posrednik.OdbijRacunAsync( RazlogOdbijanja.NeusklađenostKojaNeUtjeceNaObracunPoreza, "Nedostaje OIB"); - output.WriteLine($"{posrednik.GetType().Name}: OdbijRacun OK"); + Console.WriteLine($"{posrednik.GetType().Name}: OdbijRacun OK"); } catch (Exception ex) { - output.WriteLine($"{posrednik.GetType().Name}: OdbijRacun FAIL"); - output.WriteLine(ex.ToString()); + Console.WriteLine($"{posrednik.GetType().Name}: OdbijRacun FAIL"); + Console.WriteLine(ex.ToString()); } } } From 76e4e2c7a5510daf9df294d042bd1e39fba08e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roko=20Tomovi=C4=87?= Date: Thu, 28 May 2026 08:13:25 +0200 Subject: [PATCH 8/8] popravljen cicd --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d2e2182..dd07148 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,7 @@ jobs: run: dotnet restore MAES.Fiskal2.Tests/MAES.Fiskal2.Tests.csproj - name: Run tests - run: dotnet test MAES.Fiskal2.Tests/MAES.Fiskal2.Tests.csproj --configuration Release --no-restore + run: dotnet test MAES.Fiskal2.Tests/MAES.Fiskal2.Tests.csproj --configuration Release --no-restore -v normal --logger "console;verbosity=detailed" pack-and-publish: name: Pack and publish package