From d9e3c59d69930fa9debe712eb810129adf985579 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:21:37 +0200 Subject: [PATCH] more flex and filters --- Client/ListModifiers.cs | 248 ++++++++++++++++++++++ Client/SdkRequestExtensions.cs | 7 +- Controllers/ContactsController.cs | 20 +- Controllers/IssuedInvoicesController.cs | 20 +- Controllers/MailController.cs | 47 ++++ Controllers/ReceivedInvoicesController.cs | 17 +- Controllers/RegistersController.cs | 36 +++- Controllers/ReportsController.cs | 38 ++++ Program.cs | 2 + Services/ContactsService.cs | 4 +- Services/IssuedInvoicesService.cs | 4 +- Services/MailService.cs | 30 +++ Services/ReceivedInvoicesService.cs | 4 +- Services/RegistersService.cs | 12 +- Services/ReportsService.cs | 31 +++ 15 files changed, 488 insertions(+), 32 deletions(-) create mode 100644 Client/ListModifiers.cs create mode 100644 Controllers/MailController.cs create mode 100644 Controllers/ReportsController.cs create mode 100644 Services/MailService.cs create mode 100644 Services/ReportsService.cs diff --git a/Client/ListModifiers.cs b/Client/ListModifiers.cs new file mode 100644 index 0000000..50dd063 --- /dev/null +++ b/Client/ListModifiers.cs @@ -0,0 +1,248 @@ +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Text.RegularExpressions; +using IdokladSdk.Clients; +using IdokladSdk.Requests.Core; +using IdokladSdk.Requests.Core.Modifiers.Filters.Common; +using IdokladSdk.Requests.Core.Modifiers.Sort.Common; +using IdokladSdk.Response; + +namespace Idoklad.Client; + +/// +/// Translates iDoklad-style filter/sort query strings into the SDK's strongly-typed +/// / +/// modifiers, so the REST +/// list endpoints expose the full server-side filtering the SDK supports (operators +/// eq/neq/gt/gte/lt/lte/ct/nct, AND/OR combining, and multi-column sort) instead of only paging. +/// +/// Filter format (identical to the public iDoklad API): one or more +/// (Property~operator~value) conditions joined by , — e.g. +/// (DateOfTaxing~gte~2024-01-01),(IsPaid~eq~false). Parentheses are optional for a single +/// condition. The filtertype query parameter (and | or, default and) +/// decides how multiple conditions combine. +/// +/// Operators: eq (=), neq (≠), gt (>), gte (≥), +/// lt (<), lte (≤), ct (contains), nct (not contains). The available +/// operators per field follow the SDK filter type (e.g. dates support the compare operators, +/// text fields support contains). +/// +/// Sort format: Property~asc|desc, multiple joined by , — e.g. +/// DateOfIssue~desc,Id~asc. Direction defaults to ascending. +/// +public static class ListModifiers +{ + private static readonly Regex GroupRegex = new(@"\(([^()]*)\)", RegexOptions.Compiled); + + private static readonly Dictionary OperatorMethods = new(StringComparer.OrdinalIgnoreCase) + { + ["eq"] = "IsEqual", + ["neq"] = "IsNotEqual", + ["gt"] = "IsGreaterThan", + ["gte"] = "IsGreaterThanOrEqual", + ["lt"] = "IsLowerThan", + ["lte"] = "IsLowerThanOrEqual", + ["ct"] = "Contains", + ["nct"] = "NotContains", + }; + + /// + /// Applies paging plus optional / to a list + /// request and returns the unwrapped page. Shared by every list endpoint. + /// + public static async Task> GetPageAsync( + this BaseList list, + int page, + int pageSize, + string? filter, + string? filterType, + string? sort, + CancellationToken ct) + where TList : BaseList + where TClient : BaseClient + where TFilter : new() + where TSort : new() + where TGetModel : new() + { + BaseList built = list; + + if (!string.IsNullOrWhiteSpace(filter)) + { + var useOr = IsOr(filterType); + built = built.Filter(f => BuildFilter(f!, filter, useOr)); + } + + if (!string.IsNullOrWhiteSpace(sort)) + { + built = built.Sort(BuildSort(sort)); + } + + return (await built.Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + } + + /// True when the filtertype query value requests OR combining (default AND). + public static bool IsOr(string? filterType) + => string.Equals(filterType, "or", StringComparison.OrdinalIgnoreCase); + + /// + /// Builds a combined from an iDoklad-style filter string + /// against the SDK filter object (passed in by the SDK at call time). + /// Conditions are combined with OR when is true, otherwise AND. + /// + public static FilterExpressionBase BuildFilter(object filter, string filterString, bool useOr) + { + FilterExpressionBase? combined = null; + + foreach (var group in ExtractConditions(filterString)) + { + var expression = BuildCondition(filter, group); + combined = combined is null + ? expression + : (useOr ? combined | expression : combined & expression); + } + + if (combined is null) + { + throw new ArgumentException($"Filtr '{filterString}' neobsahuje žádnou platnou podmínku."); + } + + return combined; + } + + /// Builds the SDK sort selectors from a Field~asc|desc,... string. + public static Func[] BuildSort(string sort) + { + var parts = sort.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var selectors = new List>(); + + foreach (var part in parts) + { + var segments = part.Split('~', StringSplitOptions.TrimEntries); + var field = segments[0]; + var descending = segments.Length > 1 && segments[1].Equals("desc", StringComparison.OrdinalIgnoreCase); + + selectors.Add(sortObj => + { + var property = typeof(TSort).GetProperty(field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) + ?? throw new ArgumentException($"Řazení podle pole '{field}' není u této agendy podporováno."); + var item = (SortItem)property.GetValue(sortObj)!; + return descending ? item.Desc() : item.Asc(); + }); + } + + return selectors.ToArray(); + } + + /// Splits the filter string into individual Name~op~value conditions. + private static IEnumerable ExtractConditions(string filterString) + { + filterString = filterString.Trim(); + + if (filterString.Contains('(')) + { + return GroupRegex.Matches(filterString) + .Select(m => m.Groups[1].Value.Trim()) + .Where(s => s.Length > 0); + } + + return filterString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + private static FilterExpressionBase BuildCondition(object filter, string condition) + { + var firstTilde = condition.IndexOf('~'); + var secondTilde = firstTilde < 0 ? -1 : condition.IndexOf('~', firstTilde + 1); + if (firstTilde < 0 || secondTilde < 0) + { + throw new ArgumentException($"Neplatná filtrační podmínka '{condition}'. Očekává se tvar 'Pole~operátor~hodnota'."); + } + + var name = condition[..firstTilde].Trim(); + var op = condition[(firstTilde + 1)..secondTilde].Trim(); + var rawValue = condition[(secondTilde + 1)..].Trim(); + + if (!OperatorMethods.TryGetValue(op, out var methodName)) + { + throw new ArgumentException($"Neznámý filtrační operátor '{op}'. Povolené: eq, neq, gt, gte, lt, lte, ct, nct."); + } + + var property = filter.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) + ?? throw new ArgumentException($"Filtrování podle pole '{name}' není u této agendy podporováno."); + + var item = property.GetValue(filter) + ?? throw new ArgumentException($"Filtrační pole '{name}' není dostupné."); + + var method = FindOperatorMethod(item.GetType(), methodName) + ?? throw new ArgumentException($"Operátor '{op}' není pro pole '{name}' podporován."); + + var targetType = method.GetParameters()[0].ParameterType; + var value = ConvertValue(rawValue, targetType, name); + + try + { + return (FilterExpressionBase)method.Invoke(item, new[] { value })!; + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + throw new ArgumentException($"Filtr '{name}~{op}~{rawValue}' se nepodařilo sestavit: {ex.InnerException.Message}"); + } + } + + private static MethodInfo? FindOperatorMethod(Type itemType, string methodName) + { + var candidates = itemType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.Name == methodName && m.GetParameters().Length == 1) + .ToList(); + + if (candidates.Count <= 1) + { + return candidates.FirstOrDefault(); + } + + // Some filter items (e.g. the Id filter) overload Contains with a scalar and a collection — + // prefer the scalar overload for a single query value. + return candidates.FirstOrDefault(m => + { + var pt = m.GetParameters()[0].ParameterType; + return pt == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(pt); + }) ?? candidates[0]; + } + + private static object ConvertValue(string raw, Type target, string fieldName) + { + var type = Nullable.GetUnderlyingType(target) ?? target; + + try + { + if (type == typeof(string)) + { + return raw; + } + if (type.IsEnum) + { + return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric) + ? Enum.ToObject(type, numeric) + : Enum.Parse(type, raw, ignoreCase: true); + } + if (type == typeof(DateTime)) + { + return DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.None); + } + if (type == typeof(bool)) + { + return bool.Parse(raw); + } + if (type == typeof(Guid)) + { + return Guid.Parse(raw); + } + return Convert.ChangeType(raw, type, CultureInfo.InvariantCulture); + } + catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException or ArgumentException) + { + throw new ArgumentException($"Hodnotu '{raw}' filtru '{fieldName}' nelze převést na typ {type.Name}."); + } + } +} diff --git a/Client/SdkRequestExtensions.cs b/Client/SdkRequestExtensions.cs index 279e350..0a3156b 100644 --- a/Client/SdkRequestExtensions.cs +++ b/Client/SdkRequestExtensions.cs @@ -14,13 +14,16 @@ public static class SdkRequestExtensions this BaseList list, int page, int pageSize, - CancellationToken ct) + CancellationToken ct, + string? filter = null, + string? filterType = null, + string? sort = null) where TList : BaseList where TClient : BaseClient where TFilter : new() where TSort : new() where TGetModel : new() - => (await list.Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + => await list.GetPageAsync(page, pageSize, filter, filterType, sort, ct); public static async Task ToDetailAsync( this BaseDetail detail, diff --git a/Controllers/ContactsController.cs b/Controllers/ContactsController.cs index 8c480cf..c41b54b 100644 --- a/Controllers/ContactsController.cs +++ b/Controllers/ContactsController.cs @@ -21,10 +21,24 @@ public sealed class ContactsController : ControllerBase public ContactsController(ContactsService service) => _service = service; - /// List contacts (paged). + /// + /// List contacts (paged, optionally filtered and sorted). + /// + /// + /// Optional iDoklad-style filter, e.g. (IdentificationNumber~eq~12345678) or + /// (CompanyName~ct~s.r.o.). Operators: eq, neq, gt, gte, lt, lte, ct, nct. + /// + /// How multiple conditions combine: and (default) or or. + /// Optional sort, e.g. CompanyName~asc. [HttpGet] - public async Task List([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) - => Ok(await _service.ListAsync(page, pageSize, ct)); + public async Task List( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? filter = null, + [FromQuery] string? filtertype = null, + [FromQuery] string? sort = null, + CancellationToken ct = default) + => Ok(await _service.ListAsync(page, pageSize, filter, filtertype, sort, ct)); /// Get a contact detail by id. [HttpGet("{id:int}")] diff --git a/Controllers/IssuedInvoicesController.cs b/Controllers/IssuedInvoicesController.cs index 61d7d9c..708ad17 100644 --- a/Controllers/IssuedInvoicesController.cs +++ b/Controllers/IssuedInvoicesController.cs @@ -18,10 +18,24 @@ public sealed class IssuedInvoicesController : ControllerBase public IssuedInvoicesController(IssuedInvoicesService service) => _service = service; - /// List issued invoices (paged). + /// + /// List issued invoices (paged, optionally filtered and sorted). + /// + /// + /// Optional iDoklad-style filter, e.g. (DateOfTaxing~gte~2024-01-01),(IsPaid~eq~false). + /// Operators: eq, neq, gt, gte, lt, lte, ct (contains), nct (not contains). + /// + /// How multiple conditions combine: and (default) or or. + /// Optional sort, e.g. DateOfIssue~desc,Id~asc. [HttpGet] - public async Task List([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) - => Ok(await _service.ListAsync(page, pageSize, ct)); + public async Task List( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? filter = null, + [FromQuery] string? filtertype = null, + [FromQuery] string? sort = null, + CancellationToken ct = default) + => Ok(await _service.ListAsync(page, pageSize, filter, filtertype, sort, ct)); /// Get an issued invoice detail by id. [HttpGet("{id:int}")] diff --git a/Controllers/MailController.cs b/Controllers/MailController.cs new file mode 100644 index 0000000..1ebb11b --- /dev/null +++ b/Controllers/MailController.cs @@ -0,0 +1,47 @@ +using IdokladSdk.Models.Email; +using Microsoft.AspNetCore.Mvc; +using Idoklad.Services; + +namespace Idoklad.Controllers; + +/// +/// Sends document e-mails through iDoklad. The request body carries the settings: the document id +/// (DocumentId), recipients (OtherRecipients, SendToPartner, SendToSelf, +/// SendToAccountant), optional subject/body and report language. Requires iDoklad credentials +/// (headers or environment defaults). +/// +[ApiController] +[Route("mail")] +[Produces("application/json")] +[Tags("Mail")] +public sealed class MailController : ControllerBase +{ + private readonly MailService _service; + + public MailController(MailService service) => _service = service; + + /// Send an issued invoice by e-mail. + [HttpPost("issued-invoices/send")] + public async Task SendIssuedInvoice([FromBody] IssuedInvoiceEmailSettings settings, CancellationToken ct) + => Ok(await _service.SendIssuedInvoiceAsync(settings, ct)); + + /// Send a proforma (advance) invoice by e-mail. + [HttpPost("proforma-invoices/send")] + public async Task SendProformaInvoice([FromBody] ProformaInvoiceEmailSettings settings, CancellationToken ct) + => Ok(await _service.SendProformaInvoiceAsync(settings, ct)); + + /// Send a credit note by e-mail. + [HttpPost("credit-notes/send")] + public async Task SendCreditNote([FromBody] CreditNoteEmailSettings settings, CancellationToken ct) + => Ok(await _service.SendCreditNoteAsync(settings, ct)); + + /// Send a received invoice by e-mail. + [HttpPost("received-invoices/send")] + public async Task SendReceivedInvoice([FromBody] ReceivedInvoiceEmailSettings settings, CancellationToken ct) + => Ok(await _service.SendReceivedInvoiceAsync(settings, ct)); + + /// Send payment reminders by e-mail. + [HttpPost("reminders/send")] + public async Task SendReminders([FromBody] RemindersEmailSettings settings, CancellationToken ct) + => Ok(await _service.SendRemindersAsync(settings, ct)); +} diff --git a/Controllers/ReceivedInvoicesController.cs b/Controllers/ReceivedInvoicesController.cs index d016d33..e7824a6 100644 --- a/Controllers/ReceivedInvoicesController.cs +++ b/Controllers/ReceivedInvoicesController.cs @@ -18,10 +18,21 @@ public sealed class ReceivedInvoicesController : ControllerBase public ReceivedInvoicesController(ReceivedInvoicesService service) => _service = service; - /// List received invoices (paged). + /// + /// List received invoices (paged, optionally filtered and sorted). + /// + /// Optional iDoklad-style filter (see IssuedInvoices for the syntax). + /// How multiple conditions combine: and (default) or or. + /// Optional sort, e.g. DateOfReceiving~desc. [HttpGet] - public async Task List([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) - => Ok(await _service.ListAsync(page, pageSize, ct)); + public async Task List( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? filter = null, + [FromQuery] string? filtertype = null, + [FromQuery] string? sort = null, + CancellationToken ct = default) + => Ok(await _service.ListAsync(page, pageSize, filter, filtertype, sort, ct)); /// Get a received invoice detail by id. [HttpGet("{id:int}")] diff --git a/Controllers/RegistersController.cs b/Controllers/RegistersController.cs index f71af48..d4fedcc 100644 --- a/Controllers/RegistersController.cs +++ b/Controllers/RegistersController.cs @@ -20,10 +20,16 @@ public sealed class RegistersController : ControllerBase // ---- Bank accounts ---- - /// List bank accounts (paged). + /// List bank accounts (paged, optionally filtered and sorted). [HttpGet("bank-accounts")] - public async Task ListBankAccounts([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) - => Ok(await _service.ListBankAccountsAsync(page, pageSize, ct)); + public async Task ListBankAccounts( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? filter = null, + [FromQuery] string? filtertype = null, + [FromQuery] string? sort = null, + CancellationToken ct = default) + => Ok(await _service.ListBankAccountsAsync(page, pageSize, filter, filtertype, sort, ct)); /// Get a bank account detail by id. [HttpGet("bank-accounts/{id:int}")] @@ -47,10 +53,16 @@ public sealed class RegistersController : ControllerBase // ---- VAT rates (read-only) ---- - /// List VAT rates (paged). + /// List VAT rates (paged, optionally filtered and sorted). [HttpGet("vat-rates")] - public async Task ListVatRates([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) - => Ok(await _service.ListVatRatesAsync(page, pageSize, ct)); + public async Task ListVatRates( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? filter = null, + [FromQuery] string? filtertype = null, + [FromQuery] string? sort = null, + CancellationToken ct = default) + => Ok(await _service.ListVatRatesAsync(page, pageSize, filter, filtertype, sort, ct)); /// Get a VAT rate detail by id. [HttpGet("vat-rates/{id:int}")] @@ -59,8 +71,14 @@ public sealed class RegistersController : ControllerBase // ---- Numeric sequences (read-only) ---- - /// List numeric (document) sequences (paged). + /// List numeric (document) sequences (paged, optionally filtered and sorted). [HttpGet("numeric-sequences")] - public async Task ListNumericSequences([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) - => Ok(await _service.ListNumericSequencesAsync(page, pageSize, ct)); + public async Task ListNumericSequences( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? filter = null, + [FromQuery] string? filtertype = null, + [FromQuery] string? sort = null, + CancellationToken ct = default) + => Ok(await _service.ListNumericSequencesAsync(page, pageSize, filter, filtertype, sort, ct)); } diff --git a/Controllers/ReportsController.cs b/Controllers/ReportsController.cs new file mode 100644 index 0000000..9c3f81f --- /dev/null +++ b/Controllers/ReportsController.cs @@ -0,0 +1,38 @@ +using IdokladSdk.Enums; +using Microsoft.AspNetCore.Mvc; +using Idoklad.Services; + +namespace Idoklad.Controllers; + +/// +/// Document PDF exports (iDoklad Reports). Each endpoint returns the document as a +/// Base64-encoded string (the iDoklad API's native representation). Requires iDoklad credentials +/// (headers or environment defaults). +/// +[ApiController] +[Route("reports")] +[Produces("application/json")] +[Tags("Reports")] +public sealed class ReportsController : ControllerBase +{ + private readonly ReportsService _service; + + public ReportsController(ReportsService service) => _service = service; + + /// Export an issued invoice as a Base64-encoded PDF. + /// Optional report language: Cz, Sk, En or De. + /// When true, the returned PDF is compressed. + [HttpGet("issued-invoices/{id:int}/pdf")] + public async Task IssuedInvoicePdf(int id, [FromQuery] Language? language = null, [FromQuery] bool compressed = false, CancellationToken ct = default) + => Ok(await _service.IssuedInvoicePdfAsync(id, language, compressed, ct)); + + /// Export a proforma (advance) invoice as a Base64-encoded PDF. + [HttpGet("proforma-invoices/{id:int}/pdf")] + public async Task ProformaInvoicePdf(int id, [FromQuery] Language? language = null, [FromQuery] bool compressed = false, CancellationToken ct = default) + => Ok(await _service.ProformaInvoicePdfAsync(id, language, compressed, ct)); + + /// Export a credit note as a Base64-encoded PDF. + [HttpGet("credit-notes/{id:int}/pdf")] + public async Task CreditNotePdf(int id, [FromQuery] Language? language = null, [FromQuery] bool compressed = false, CancellationToken ct = default) + => Ok(await _service.CreditNotePdfAsync(id, language, compressed, ct)); +} diff --git a/Program.cs b/Program.cs index 431b492..f4daec8 100644 --- a/Program.cs +++ b/Program.cs @@ -38,6 +38,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // Use Newtonsoft.Json so request/response binding matches the iDoklad SDK model attributes. builder.Services diff --git a/Services/ContactsService.cs b/Services/ContactsService.cs index 88c35bf..5dcf3c1 100644 --- a/Services/ContactsService.cs +++ b/Services/ContactsService.cs @@ -11,8 +11,8 @@ public sealed class ContactsService public ContactsService(IdokladApiAccessor accessor) => _accessor = accessor; - public async Task> ListAsync(int page, int pageSize, CancellationToken ct) - => (await _accessor.Api.ContactClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + public Task> ListAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct) + => _accessor.Api.ContactClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct); public async Task DetailAsync(int id, CancellationToken ct) => (await _accessor.Api.ContactClient.Detail(id).GetAsync(ct)).Unwrap(); diff --git a/Services/IssuedInvoicesService.cs b/Services/IssuedInvoicesService.cs index 939f92e..86f528a 100644 --- a/Services/IssuedInvoicesService.cs +++ b/Services/IssuedInvoicesService.cs @@ -11,8 +11,8 @@ public sealed class IssuedInvoicesService public IssuedInvoicesService(IdokladApiAccessor accessor) => _accessor = accessor; - public async Task> ListAsync(int page, int pageSize, CancellationToken ct) - => (await _accessor.Api.IssuedInvoiceClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + public Task> ListAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct) + => _accessor.Api.IssuedInvoiceClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct); public async Task DetailAsync(int id, CancellationToken ct) => (await _accessor.Api.IssuedInvoiceClient.Detail(id).GetAsync(ct)).Unwrap(); diff --git a/Services/MailService.cs b/Services/MailService.cs new file mode 100644 index 0000000..e50b993 --- /dev/null +++ b/Services/MailService.cs @@ -0,0 +1,30 @@ +using IdokladSdk.Models.Email; +using Idoklad.Client; + +namespace Idoklad.Services; + +/// +/// Sends document e-mails (invoice, proforma, credit note, received invoice, reminders) through +/// the iDoklad MailClient. The settings carry the document id, recipients and options. +/// +public sealed class MailService +{ + private readonly IdokladApiAccessor _accessor; + + public MailService(IdokladApiAccessor accessor) => _accessor = accessor; + + public async Task SendIssuedInvoiceAsync(IssuedInvoiceEmailSettings settings, CancellationToken ct) + => (await _accessor.Api.MailClient.IssuedInvoiceEmail.SendAsync(settings, ct)).Unwrap(); + + public async Task SendProformaInvoiceAsync(ProformaInvoiceEmailSettings settings, CancellationToken ct) + => (await _accessor.Api.MailClient.ProformaInvoiceEmail.SendAsync(settings, ct)).Unwrap(); + + public async Task SendCreditNoteAsync(CreditNoteEmailSettings settings, CancellationToken ct) + => (await _accessor.Api.MailClient.CreditNoteEmail.SendAsync(settings, ct)).Unwrap(); + + public async Task SendReceivedInvoiceAsync(ReceivedInvoiceEmailSettings settings, CancellationToken ct) + => (await _accessor.Api.MailClient.ReceivedInvoiceEmail.SendAsync(settings, ct)).Unwrap(); + + public async Task SendRemindersAsync(RemindersEmailSettings settings, CancellationToken ct) + => (await _accessor.Api.MailClient.RemindersEmail.SendAsync(settings, ct)).Unwrap(); +} diff --git a/Services/ReceivedInvoicesService.cs b/Services/ReceivedInvoicesService.cs index bfd8cc5..63f3143 100644 --- a/Services/ReceivedInvoicesService.cs +++ b/Services/ReceivedInvoicesService.cs @@ -11,8 +11,8 @@ public sealed class ReceivedInvoicesService public ReceivedInvoicesService(IdokladApiAccessor accessor) => _accessor = accessor; - public async Task> ListAsync(int page, int pageSize, CancellationToken ct) - => (await _accessor.Api.ReceivedInvoiceClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + public Task> ListAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct) + => _accessor.Api.ReceivedInvoiceClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct); public async Task DetailAsync(int id, CancellationToken ct) => (await _accessor.Api.ReceivedInvoiceClient.Detail(id).GetAsync(ct)).Unwrap(); diff --git a/Services/RegistersService.cs b/Services/RegistersService.cs index 6eb9ca4..c3a7727 100644 --- a/Services/RegistersService.cs +++ b/Services/RegistersService.cs @@ -16,8 +16,8 @@ public sealed class RegistersService public RegistersService(IdokladApiAccessor accessor) => _accessor = accessor; // Bank accounts - public async Task> ListBankAccountsAsync(int page, int pageSize, CancellationToken ct) - => (await _accessor.Api.BankAccountClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + public Task> ListBankAccountsAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct) + => _accessor.Api.BankAccountClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct); public async Task BankAccountDetailAsync(int id, CancellationToken ct) => (await _accessor.Api.BankAccountClient.Detail(id).GetAsync(ct)).Unwrap(); @@ -32,13 +32,13 @@ public sealed class RegistersService => (await _accessor.Api.BankAccountClient.DeleteAsync(id, ct)).Unwrap(); // VAT rates (read-only) - public async Task> ListVatRatesAsync(int page, int pageSize, CancellationToken ct) - => (await _accessor.Api.VatRateClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + public Task> ListVatRatesAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct) + => _accessor.Api.VatRateClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct); public async Task VatRateDetailAsync(int id, CancellationToken ct) => (await _accessor.Api.VatRateClient.Detail(id).GetAsync(ct)).Unwrap(); // Numeric sequences (read-only list) - public async Task> ListNumericSequencesAsync(int page, int pageSize, CancellationToken ct) - => (await _accessor.Api.NumericSequenceClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); + public Task> ListNumericSequencesAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct) + => _accessor.Api.NumericSequenceClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct); } diff --git a/Services/ReportsService.cs b/Services/ReportsService.cs new file mode 100644 index 0000000..7e0781e --- /dev/null +++ b/Services/ReportsService.cs @@ -0,0 +1,31 @@ +using IdokladSdk.Enums; +using IdokladSdk.Models.Report; +using Idoklad.Client; + +namespace Idoklad.Services; + +/// +/// Document PDF exports (iDoklad Reports). Each method returns the report as a Base64-encoded +/// string — the iDoklad API's native representation; decode it to obtain the raw PDF bytes. +/// +public sealed class ReportsService +{ + private readonly IdokladApiAccessor _accessor; + + public ReportsService(IdokladApiAccessor accessor) => _accessor = accessor; + + /// Export an issued invoice as a Base64-encoded PDF. + public async Task IssuedInvoicePdfAsync(int id, Language? language, bool compressed, CancellationToken ct) + => (await _accessor.Api.ReportClient.IssuedInvoice.Detail(id).GetAsync(Option(language, compressed), ct)).Unwrap(); + + /// Export a proforma (advance) invoice as a Base64-encoded PDF. + public async Task ProformaInvoicePdfAsync(int id, Language? language, bool compressed, CancellationToken ct) + => (await _accessor.Api.ReportClient.ProformaInvoice.Detail(id).GetAsync(Option(language, compressed), ct)).Unwrap(); + + /// Export a credit note as a Base64-encoded PDF. + public async Task CreditNotePdfAsync(int id, Language? language, bool compressed, CancellationToken ct) + => (await _accessor.Api.ReportClient.CreditNote.Detail(id).GetAsync(Option(language, compressed), ct)).Unwrap(); + + private static ExtendedReportOption Option(Language? language, bool compressed) + => new() { Language = language, Compressed = compressed }; +}