more flex and filters

This commit is contained in:
JiriUhlir
2026-07-14 06:21:37 +02:00
parent 1a898b95f9
commit d9e3c59d69
15 changed files with 488 additions and 32 deletions
+248
View File
@@ -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;
/// <summary>
/// Translates iDoklad-style <c>filter</c>/<c>sort</c> query strings into the SDK's strongly-typed
/// <see cref="BaseListCore{TList,TClient,TGetModel,TFilter,TSort}.Filter"/> /
/// <see cref="BaseListCore{TList,TClient,TGetModel,TFilter,TSort}.Sort"/> 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.
///
/// <para><b>Filter format</b> (identical to the public iDoklad API): one or more
/// <c>(Property~operator~value)</c> conditions joined by <c>,</c> — e.g.
/// <c>(DateOfTaxing~gte~2024-01-01),(IsPaid~eq~false)</c>. Parentheses are optional for a single
/// condition. The <c>filtertype</c> query parameter (<c>and</c> | <c>or</c>, default <c>and</c>)
/// decides how multiple conditions combine.</para>
///
/// <para><b>Operators:</b> <c>eq</c> (=), <c>neq</c> (≠), <c>gt</c> (&gt;), <c>gte</c> (≥),
/// <c>lt</c> (&lt;), <c>lte</c> (≤), <c>ct</c> (contains), <c>nct</c> (not contains). The available
/// operators per field follow the SDK filter type (e.g. dates support the compare operators,
/// text fields support contains).</para>
///
/// <para><b>Sort format:</b> <c>Property~asc|desc</c>, multiple joined by <c>,</c> — e.g.
/// <c>DateOfIssue~desc,Id~asc</c>. Direction defaults to ascending.</para>
/// </summary>
public static class ListModifiers
{
private static readonly Regex GroupRegex = new(@"\(([^()]*)\)", RegexOptions.Compiled);
private static readonly Dictionary<string, string> OperatorMethods = new(StringComparer.OrdinalIgnoreCase)
{
["eq"] = "IsEqual",
["neq"] = "IsNotEqual",
["gt"] = "IsGreaterThan",
["gte"] = "IsGreaterThanOrEqual",
["lt"] = "IsLowerThan",
["lte"] = "IsLowerThanOrEqual",
["ct"] = "Contains",
["nct"] = "NotContains",
};
/// <summary>
/// Applies paging plus optional <paramref name="filter"/>/<paramref name="sort"/> to a list
/// request and returns the unwrapped page. Shared by every list endpoint.
/// </summary>
public static async Task<Page<TGetModel>> GetPageAsync<TList, TClient, TGetModel, TFilter, TSort>(
this BaseList<TList, TClient, TGetModel, TFilter, TSort> list,
int page,
int pageSize,
string? filter,
string? filterType,
string? sort,
CancellationToken ct)
where TList : BaseList<TList, TClient, TGetModel, TFilter, TSort>
where TClient : BaseClient
where TFilter : new()
where TSort : new()
where TGetModel : new()
{
BaseList<TList, TClient, TGetModel, TFilter, TSort> 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<TSort>(sort));
}
return (await built.Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap();
}
/// <summary>True when the <c>filtertype</c> query value requests OR combining (default AND).</summary>
public static bool IsOr(string? filterType)
=> string.Equals(filterType, "or", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Builds a combined <see cref="FilterExpressionBase"/> from an iDoklad-style filter string
/// against the SDK filter object <paramref name="filter"/> (passed in by the SDK at call time).
/// Conditions are combined with OR when <paramref name="useOr"/> is true, otherwise AND.
/// </summary>
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;
}
/// <summary>Builds the SDK sort selectors from a <c>Field~asc|desc,...</c> string.</summary>
public static Func<TSort, SortExpression>[] BuildSort<TSort>(string sort)
{
var parts = sort.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var selectors = new List<Func<TSort, SortExpression>>();
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();
}
/// <summary>Splits the filter string into individual <c>Name~op~value</c> conditions.</summary>
private static IEnumerable<string> 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}.");
}
}
}
+5 -2
View File
@@ -14,13 +14,16 @@ public static class SdkRequestExtensions
this BaseList<TList, TClient, TGetModel, TFilter, TSort> list, this BaseList<TList, TClient, TGetModel, TFilter, TSort> list,
int page, int page,
int pageSize, int pageSize,
CancellationToken ct) CancellationToken ct,
string? filter = null,
string? filterType = null,
string? sort = null)
where TList : BaseList<TList, TClient, TGetModel, TFilter, TSort> where TList : BaseList<TList, TClient, TGetModel, TFilter, TSort>
where TClient : BaseClient where TClient : BaseClient
where TFilter : new() where TFilter : new()
where TSort : new() where TSort : new()
where TGetModel : 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<object?> ToDetailAsync<TDetail, TClient, TGetModel>( public static async Task<object?> ToDetailAsync<TDetail, TClient, TGetModel>(
this BaseDetail<TDetail, TClient, TGetModel> detail, this BaseDetail<TDetail, TClient, TGetModel> detail,
+17 -3
View File
@@ -21,10 +21,24 @@ public sealed class ContactsController : ControllerBase
public ContactsController(ContactsService service) => _service = service; public ContactsController(ContactsService service) => _service = service;
/// <summary>List contacts (paged).</summary> /// <summary>
/// List contacts (paged, optionally filtered and sorted).
/// </summary>
/// <param name="filter">
/// Optional iDoklad-style filter, e.g. <c>(IdentificationNumber~eq~12345678)</c> or
/// <c>(CompanyName~ct~s.r.o.)</c>. Operators: eq, neq, gt, gte, lt, lte, ct, nct.
/// </param>
/// <param name="filtertype">How multiple conditions combine: <c>and</c> (default) or <c>or</c>.</param>
/// <param name="sort">Optional sort, e.g. <c>CompanyName~asc</c>.</param>
[HttpGet] [HttpGet]
public async Task<IActionResult> List([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) public async Task<IActionResult> List(
=> Ok(await _service.ListAsync(page, pageSize, ct)); [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));
/// <summary>Get a contact detail by id.</summary> /// <summary>Get a contact detail by id.</summary>
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
+17 -3
View File
@@ -18,10 +18,24 @@ public sealed class IssuedInvoicesController : ControllerBase
public IssuedInvoicesController(IssuedInvoicesService service) => _service = service; public IssuedInvoicesController(IssuedInvoicesService service) => _service = service;
/// <summary>List issued invoices (paged).</summary> /// <summary>
/// List issued invoices (paged, optionally filtered and sorted).
/// </summary>
/// <param name="filter">
/// Optional iDoklad-style filter, e.g. <c>(DateOfTaxing~gte~2024-01-01),(IsPaid~eq~false)</c>.
/// Operators: eq, neq, gt, gte, lt, lte, ct (contains), nct (not contains).
/// </param>
/// <param name="filtertype">How multiple conditions combine: <c>and</c> (default) or <c>or</c>.</param>
/// <param name="sort">Optional sort, e.g. <c>DateOfIssue~desc,Id~asc</c>.</param>
[HttpGet] [HttpGet]
public async Task<IActionResult> List([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) public async Task<IActionResult> List(
=> Ok(await _service.ListAsync(page, pageSize, ct)); [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));
/// <summary>Get an issued invoice detail by id.</summary> /// <summary>Get an issued invoice detail by id.</summary>
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
+47
View File
@@ -0,0 +1,47 @@
using IdokladSdk.Models.Email;
using Microsoft.AspNetCore.Mvc;
using Idoklad.Services;
namespace Idoklad.Controllers;
/// <summary>
/// Sends document e-mails through iDoklad. The request body carries the settings: the document id
/// (<c>DocumentId</c>), recipients (<c>OtherRecipients</c>, <c>SendToPartner</c>, <c>SendToSelf</c>,
/// <c>SendToAccountant</c>), optional subject/body and report language. Requires iDoklad credentials
/// (headers or environment defaults).
/// </summary>
[ApiController]
[Route("mail")]
[Produces("application/json")]
[Tags("Mail")]
public sealed class MailController : ControllerBase
{
private readonly MailService _service;
public MailController(MailService service) => _service = service;
/// <summary>Send an issued invoice by e-mail.</summary>
[HttpPost("issued-invoices/send")]
public async Task<IActionResult> SendIssuedInvoice([FromBody] IssuedInvoiceEmailSettings settings, CancellationToken ct)
=> Ok(await _service.SendIssuedInvoiceAsync(settings, ct));
/// <summary>Send a proforma (advance) invoice by e-mail.</summary>
[HttpPost("proforma-invoices/send")]
public async Task<IActionResult> SendProformaInvoice([FromBody] ProformaInvoiceEmailSettings settings, CancellationToken ct)
=> Ok(await _service.SendProformaInvoiceAsync(settings, ct));
/// <summary>Send a credit note by e-mail.</summary>
[HttpPost("credit-notes/send")]
public async Task<IActionResult> SendCreditNote([FromBody] CreditNoteEmailSettings settings, CancellationToken ct)
=> Ok(await _service.SendCreditNoteAsync(settings, ct));
/// <summary>Send a received invoice by e-mail.</summary>
[HttpPost("received-invoices/send")]
public async Task<IActionResult> SendReceivedInvoice([FromBody] ReceivedInvoiceEmailSettings settings, CancellationToken ct)
=> Ok(await _service.SendReceivedInvoiceAsync(settings, ct));
/// <summary>Send payment reminders by e-mail.</summary>
[HttpPost("reminders/send")]
public async Task<IActionResult> SendReminders([FromBody] RemindersEmailSettings settings, CancellationToken ct)
=> Ok(await _service.SendRemindersAsync(settings, ct));
}
+14 -3
View File
@@ -18,10 +18,21 @@ public sealed class ReceivedInvoicesController : ControllerBase
public ReceivedInvoicesController(ReceivedInvoicesService service) => _service = service; public ReceivedInvoicesController(ReceivedInvoicesService service) => _service = service;
/// <summary>List received invoices (paged).</summary> /// <summary>
/// List received invoices (paged, optionally filtered and sorted).
/// </summary>
/// <param name="filter">Optional iDoklad-style filter (see IssuedInvoices for the syntax).</param>
/// <param name="filtertype">How multiple conditions combine: <c>and</c> (default) or <c>or</c>.</param>
/// <param name="sort">Optional sort, e.g. <c>DateOfReceiving~desc</c>.</param>
[HttpGet] [HttpGet]
public async Task<IActionResult> List([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) public async Task<IActionResult> List(
=> Ok(await _service.ListAsync(page, pageSize, ct)); [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));
/// <summary>Get a received invoice detail by id.</summary> /// <summary>Get a received invoice detail by id.</summary>
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
+27 -9
View File
@@ -20,10 +20,16 @@ public sealed class RegistersController : ControllerBase
// ---- Bank accounts ---- // ---- Bank accounts ----
/// <summary>List bank accounts (paged).</summary> /// <summary>List bank accounts (paged, optionally filtered and sorted).</summary>
[HttpGet("bank-accounts")] [HttpGet("bank-accounts")]
public async Task<IActionResult> ListBankAccounts([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) public async Task<IActionResult> ListBankAccounts(
=> Ok(await _service.ListBankAccountsAsync(page, pageSize, ct)); [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));
/// <summary>Get a bank account detail by id.</summary> /// <summary>Get a bank account detail by id.</summary>
[HttpGet("bank-accounts/{id:int}")] [HttpGet("bank-accounts/{id:int}")]
@@ -47,10 +53,16 @@ public sealed class RegistersController : ControllerBase
// ---- VAT rates (read-only) ---- // ---- VAT rates (read-only) ----
/// <summary>List VAT rates (paged).</summary> /// <summary>List VAT rates (paged, optionally filtered and sorted).</summary>
[HttpGet("vat-rates")] [HttpGet("vat-rates")]
public async Task<IActionResult> ListVatRates([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) public async Task<IActionResult> ListVatRates(
=> Ok(await _service.ListVatRatesAsync(page, pageSize, ct)); [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));
/// <summary>Get a VAT rate detail by id.</summary> /// <summary>Get a VAT rate detail by id.</summary>
[HttpGet("vat-rates/{id:int}")] [HttpGet("vat-rates/{id:int}")]
@@ -59,8 +71,14 @@ public sealed class RegistersController : ControllerBase
// ---- Numeric sequences (read-only) ---- // ---- Numeric sequences (read-only) ----
/// <summary>List numeric (document) sequences (paged).</summary> /// <summary>List numeric (document) sequences (paged, optionally filtered and sorted).</summary>
[HttpGet("numeric-sequences")] [HttpGet("numeric-sequences")]
public async Task<IActionResult> ListNumericSequences([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) public async Task<IActionResult> ListNumericSequences(
=> Ok(await _service.ListNumericSequencesAsync(page, pageSize, ct)); [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));
} }
+38
View File
@@ -0,0 +1,38 @@
using IdokladSdk.Enums;
using Microsoft.AspNetCore.Mvc;
using Idoklad.Services;
namespace Idoklad.Controllers;
/// <summary>
/// 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).
/// </summary>
[ApiController]
[Route("reports")]
[Produces("application/json")]
[Tags("Reports")]
public sealed class ReportsController : ControllerBase
{
private readonly ReportsService _service;
public ReportsController(ReportsService service) => _service = service;
/// <summary>Export an issued invoice as a Base64-encoded PDF.</summary>
/// <param name="language">Optional report language: Cz, Sk, En or De.</param>
/// <param name="compressed">When true, the returned PDF is compressed.</param>
[HttpGet("issued-invoices/{id:int}/pdf")]
public async Task<IActionResult> IssuedInvoicePdf(int id, [FromQuery] Language? language = null, [FromQuery] bool compressed = false, CancellationToken ct = default)
=> Ok(await _service.IssuedInvoicePdfAsync(id, language, compressed, ct));
/// <summary>Export a proforma (advance) invoice as a Base64-encoded PDF.</summary>
[HttpGet("proforma-invoices/{id:int}/pdf")]
public async Task<IActionResult> ProformaInvoicePdf(int id, [FromQuery] Language? language = null, [FromQuery] bool compressed = false, CancellationToken ct = default)
=> Ok(await _service.ProformaInvoicePdfAsync(id, language, compressed, ct));
/// <summary>Export a credit note as a Base64-encoded PDF.</summary>
[HttpGet("credit-notes/{id:int}/pdf")]
public async Task<IActionResult> CreditNotePdf(int id, [FromQuery] Language? language = null, [FromQuery] bool compressed = false, CancellationToken ct = default)
=> Ok(await _service.CreditNotePdfAsync(id, language, compressed, ct));
}
+2
View File
@@ -38,6 +38,8 @@ builder.Services.AddScoped<PaymentsService>();
builder.Services.AddScoped<CatalogService>(); builder.Services.AddScoped<CatalogService>();
builder.Services.AddScoped<IntegrationService>(); builder.Services.AddScoped<IntegrationService>();
builder.Services.AddScoped<StatisticsService>(); builder.Services.AddScoped<StatisticsService>();
builder.Services.AddScoped<ReportsService>();
builder.Services.AddScoped<MailService>();
// Use Newtonsoft.Json so request/response binding matches the iDoklad SDK model attributes. // Use Newtonsoft.Json so request/response binding matches the iDoklad SDK model attributes.
builder.Services builder.Services
+2 -2
View File
@@ -11,8 +11,8 @@ public sealed class ContactsService
public ContactsService(IdokladApiAccessor accessor) => _accessor = accessor; public ContactsService(IdokladApiAccessor accessor) => _accessor = accessor;
public async Task<Page<ContactListGetModel>> ListAsync(int page, int pageSize, CancellationToken ct) public Task<Page<ContactListGetModel>> ListAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct)
=> (await _accessor.Api.ContactClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); => _accessor.Api.ContactClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct);
public async Task<ContactGetModel> DetailAsync(int id, CancellationToken ct) public async Task<ContactGetModel> DetailAsync(int id, CancellationToken ct)
=> (await _accessor.Api.ContactClient.Detail(id).GetAsync(ct)).Unwrap(); => (await _accessor.Api.ContactClient.Detail(id).GetAsync(ct)).Unwrap();
+2 -2
View File
@@ -11,8 +11,8 @@ public sealed class IssuedInvoicesService
public IssuedInvoicesService(IdokladApiAccessor accessor) => _accessor = accessor; public IssuedInvoicesService(IdokladApiAccessor accessor) => _accessor = accessor;
public async Task<Page<IssuedInvoiceListGetModel>> ListAsync(int page, int pageSize, CancellationToken ct) public Task<Page<IssuedInvoiceListGetModel>> ListAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct)
=> (await _accessor.Api.IssuedInvoiceClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); => _accessor.Api.IssuedInvoiceClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct);
public async Task<IssuedInvoiceGetModel> DetailAsync(int id, CancellationToken ct) public async Task<IssuedInvoiceGetModel> DetailAsync(int id, CancellationToken ct)
=> (await _accessor.Api.IssuedInvoiceClient.Detail(id).GetAsync(ct)).Unwrap(); => (await _accessor.Api.IssuedInvoiceClient.Detail(id).GetAsync(ct)).Unwrap();
+30
View File
@@ -0,0 +1,30 @@
using IdokladSdk.Models.Email;
using Idoklad.Client;
namespace Idoklad.Services;
/// <summary>
/// Sends document e-mails (invoice, proforma, credit note, received invoice, reminders) through
/// the iDoklad MailClient. The settings carry the document id, recipients and options.
/// </summary>
public sealed class MailService
{
private readonly IdokladApiAccessor _accessor;
public MailService(IdokladApiAccessor accessor) => _accessor = accessor;
public async Task<EmailSendResult> SendIssuedInvoiceAsync(IssuedInvoiceEmailSettings settings, CancellationToken ct)
=> (await _accessor.Api.MailClient.IssuedInvoiceEmail.SendAsync(settings, ct)).Unwrap();
public async Task<EmailSendResult> SendProformaInvoiceAsync(ProformaInvoiceEmailSettings settings, CancellationToken ct)
=> (await _accessor.Api.MailClient.ProformaInvoiceEmail.SendAsync(settings, ct)).Unwrap();
public async Task<EmailSendResult> SendCreditNoteAsync(CreditNoteEmailSettings settings, CancellationToken ct)
=> (await _accessor.Api.MailClient.CreditNoteEmail.SendAsync(settings, ct)).Unwrap();
public async Task<EmailSendResult> SendReceivedInvoiceAsync(ReceivedInvoiceEmailSettings settings, CancellationToken ct)
=> (await _accessor.Api.MailClient.ReceivedInvoiceEmail.SendAsync(settings, ct)).Unwrap();
public async Task<EmailSendResult> SendRemindersAsync(RemindersEmailSettings settings, CancellationToken ct)
=> (await _accessor.Api.MailClient.RemindersEmail.SendAsync(settings, ct)).Unwrap();
}
+2 -2
View File
@@ -11,8 +11,8 @@ public sealed class ReceivedInvoicesService
public ReceivedInvoicesService(IdokladApiAccessor accessor) => _accessor = accessor; public ReceivedInvoicesService(IdokladApiAccessor accessor) => _accessor = accessor;
public async Task<Page<ReceivedInvoiceListGetModel>> ListAsync(int page, int pageSize, CancellationToken ct) public Task<Page<ReceivedInvoiceListGetModel>> ListAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct)
=> (await _accessor.Api.ReceivedInvoiceClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); => _accessor.Api.ReceivedInvoiceClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct);
public async Task<ReceivedInvoiceGetModel> DetailAsync(int id, CancellationToken ct) public async Task<ReceivedInvoiceGetModel> DetailAsync(int id, CancellationToken ct)
=> (await _accessor.Api.ReceivedInvoiceClient.Detail(id).GetAsync(ct)).Unwrap(); => (await _accessor.Api.ReceivedInvoiceClient.Detail(id).GetAsync(ct)).Unwrap();
+6 -6
View File
@@ -16,8 +16,8 @@ public sealed class RegistersService
public RegistersService(IdokladApiAccessor accessor) => _accessor = accessor; public RegistersService(IdokladApiAccessor accessor) => _accessor = accessor;
// Bank accounts // Bank accounts
public async Task<Page<BankAccountListGetModel>> ListBankAccountsAsync(int page, int pageSize, CancellationToken ct) public Task<Page<BankAccountListGetModel>> ListBankAccountsAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct)
=> (await _accessor.Api.BankAccountClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); => _accessor.Api.BankAccountClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct);
public async Task<BankAccountGetModel> BankAccountDetailAsync(int id, CancellationToken ct) public async Task<BankAccountGetModel> BankAccountDetailAsync(int id, CancellationToken ct)
=> (await _accessor.Api.BankAccountClient.Detail(id).GetAsync(ct)).Unwrap(); => (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(); => (await _accessor.Api.BankAccountClient.DeleteAsync(id, ct)).Unwrap();
// VAT rates (read-only) // VAT rates (read-only)
public async Task<Page<VatRateListGetModel>> ListVatRatesAsync(int page, int pageSize, CancellationToken ct) public Task<Page<VatRateListGetModel>> ListVatRatesAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct)
=> (await _accessor.Api.VatRateClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); => _accessor.Api.VatRateClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct);
public async Task<VatRateGetModel> VatRateDetailAsync(int id, CancellationToken ct) public async Task<VatRateGetModel> VatRateDetailAsync(int id, CancellationToken ct)
=> (await _accessor.Api.VatRateClient.Detail(id).GetAsync(ct)).Unwrap(); => (await _accessor.Api.VatRateClient.Detail(id).GetAsync(ct)).Unwrap();
// Numeric sequences (read-only list) // Numeric sequences (read-only list)
public async Task<Page<NumericSequenceGetModel>> ListNumericSequencesAsync(int page, int pageSize, CancellationToken ct) public Task<Page<NumericSequenceGetModel>> ListNumericSequencesAsync(int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct)
=> (await _accessor.Api.NumericSequenceClient.List().Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); => _accessor.Api.NumericSequenceClient.List().GetPageAsync(page, pageSize, filter, filterType, sort, ct);
} }
+31
View File
@@ -0,0 +1,31 @@
using IdokladSdk.Enums;
using IdokladSdk.Models.Report;
using Idoklad.Client;
namespace Idoklad.Services;
/// <summary>
/// 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.
/// </summary>
public sealed class ReportsService
{
private readonly IdokladApiAccessor _accessor;
public ReportsService(IdokladApiAccessor accessor) => _accessor = accessor;
/// <summary>Export an issued invoice as a Base64-encoded PDF.</summary>
public async Task<string> IssuedInvoicePdfAsync(int id, Language? language, bool compressed, CancellationToken ct)
=> (await _accessor.Api.ReportClient.IssuedInvoice.Detail(id).GetAsync(Option(language, compressed), ct)).Unwrap();
/// <summary>Export a proforma (advance) invoice as a Base64-encoded PDF.</summary>
public async Task<string> ProformaInvoicePdfAsync(int id, Language? language, bool compressed, CancellationToken ct)
=> (await _accessor.Api.ReportClient.ProformaInvoice.Detail(id).GetAsync(Option(language, compressed), ct)).Unwrap();
/// <summary>Export a credit note as a Base64-encoded PDF.</summary>
public async Task<string> 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 };
}