diff --git a/Client/SdkRequestExtensions.cs b/Client/SdkRequestExtensions.cs
new file mode 100644
index 0000000..279e350
--- /dev/null
+++ b/Client/SdkRequestExtensions.cs
@@ -0,0 +1,32 @@
+using IdokladSdk.Clients;
+using IdokladSdk.Requests.Core;
+
+namespace Idoklad.Client;
+
+///
+/// Generic helpers over the SDK's list/detail request builders so the read endpoints of the many
+/// agendas can be expressed in a single line. The concrete payload type is inferred by the SDK and
+/// returned boxed (full data is preserved at runtime; only the Swagger schema is generic).
+///
+public static class SdkRequestExtensions
+{
+ public static async Task ToPageAsync(
+ this BaseList list,
+ int page,
+ int pageSize,
+ CancellationToken ct)
+ where TList : BaseList
+ where TClient : BaseClient
+ where TFilter : new()
+ where TSort : new()
+ where TGetModel : new()
+ => (await list.Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap();
+
+ public static async Task ToDetailAsync(
+ this BaseDetail detail,
+ CancellationToken ct)
+ where TDetail : BaseDetail
+ where TClient : BaseClient
+ where TGetModel : new()
+ => (await detail.GetAsync(ct)).Unwrap();
+}
diff --git a/Controllers/CatalogController.cs b/Controllers/CatalogController.cs
new file mode 100644
index 0000000..1e0bb82
--- /dev/null
+++ b/Controllers/CatalogController.cs
@@ -0,0 +1,101 @@
+using IdokladSdk.Models.PriceListItem;
+using IdokladSdk.Models.StockMovement;
+using IdokladSdk.Models.Tag;
+using Microsoft.AspNetCore.Mvc;
+using Idoklad.Services;
+
+namespace Idoklad.Controllers;
+
+///
+/// Price list items, stock movements and tags. Requires iDoklad credentials (headers or environment defaults).
+///
+[ApiController]
+[Produces("application/json")]
+[Tags("Catalog")]
+public sealed class CatalogController : ControllerBase
+{
+ private readonly CatalogService _service;
+
+ public CatalogController(CatalogService service) => _service = service;
+
+ // ---- Price list items ----
+
+ /// List price list items (paged).
+ [HttpGet("price-list-items")]
+ public async Task ListPriceListItems(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListPriceListItemsAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new price list item.
+ [HttpGet("price-list-items/default")]
+ public async Task PriceListItemDefault(CancellationToken ct)
+ => Ok(await _service.PriceListItemDefaultAsync(ct));
+
+ /// Get a price list item detail by id.
+ [HttpGet("price-list-items/{id:int}")]
+ public async Task PriceListItemDetail(int id, CancellationToken ct)
+ => Ok(await _service.PriceListItemDetailAsync(id, ct));
+
+ /// Create a new price list item.
+ [HttpPost("price-list-items")]
+ public async Task CreatePriceListItem([FromBody] PriceListItemPostModel model, CancellationToken ct)
+ => Ok(await _service.CreatePriceListItemAsync(model, ct));
+
+ /// Update an existing price list item (the model id identifies the item).
+ [HttpPatch("price-list-items")]
+ public async Task UpdatePriceListItem([FromBody] PriceListItemPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdatePriceListItemAsync(model, ct));
+
+ // ---- Stock movements ----
+
+ /// List stock movements (paged).
+ [HttpGet("stock-movements")]
+ public async Task ListStockMovements(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListStockMovementsAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default stock movement model for the given price list item id.
+ [HttpGet("stock-movements/default/{priceListItemId:int}")]
+ public async Task StockMovementDefault(int priceListItemId, CancellationToken ct)
+ => Ok(await _service.StockMovementDefaultAsync(priceListItemId, ct));
+
+ /// Get a stock movement detail by id.
+ [HttpGet("stock-movements/{id:int}")]
+ public async Task StockMovementDetail(int id, CancellationToken ct)
+ => Ok(await _service.StockMovementDetailAsync(id, ct));
+
+ /// Create a new stock movement.
+ [HttpPost("stock-movements")]
+ public async Task CreateStockMovement([FromBody] StockMovementPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateStockMovementAsync(model, ct));
+
+ /// Update an existing stock movement (the model id identifies the movement).
+ [HttpPatch("stock-movements")]
+ public async Task UpdateStockMovement([FromBody] StockMovementPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateStockMovementAsync(model, ct));
+
+ /// Delete a stock movement by id.
+ [HttpDelete("stock-movements/{id:int}")]
+ public async Task DeleteStockMovement(int id, CancellationToken ct)
+ => Ok(await _service.DeleteStockMovementAsync(id, ct));
+
+ // ---- Tags ----
+
+ /// List tags (paged).
+ [HttpGet("tags")]
+ public async Task ListTags(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListTagsAsync(page, pageSize, ct));
+
+ /// Create a new tag.
+ [HttpPost("tags")]
+ public async Task CreateTag([FromBody] TagPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateTagAsync(model, ct));
+
+ /// Update an existing tag (the model id identifies the tag).
+ [HttpPatch("tags")]
+ public async Task UpdateTag([FromBody] TagPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateTagAsync(model, ct));
+
+ /// Delete a tag by id.
+ [HttpDelete("tags/{id:int}")]
+ public async Task DeleteTag(int id, CancellationToken ct)
+ => Ok(await _service.DeleteTagAsync(id, ct));
+}
diff --git a/Controllers/CodeListsController.cs b/Controllers/CodeListsController.cs
new file mode 100644
index 0000000..33ca7ba
--- /dev/null
+++ b/Controllers/CodeListsController.cs
@@ -0,0 +1,118 @@
+using Microsoft.AspNetCore.Mvc;
+using Idoklad.Services;
+
+namespace Idoklad.Controllers;
+
+///
+/// Read-only iDoklad code lists / registers. Requires iDoklad credentials (headers or environment
+/// defaults).
+///
+[ApiController]
+[Produces("application/json")]
+[Tags("CodeLists")]
+public sealed class CodeListsController : ControllerBase
+{
+ private readonly CodeListsService _service;
+
+ public CodeListsController(CodeListsService service) => _service = service;
+
+ /// List banks (paged).
+ [HttpGet("code-lists/banks")]
+ public async Task Banks(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListBanksAsync(page, pageSize, ct));
+
+ /// Get a bank detail by id.
+ [HttpGet("code-lists/banks/{id:int}")]
+ public async Task BankDetail(int id, CancellationToken ct)
+ => Ok(await _service.BankDetailAsync(id, ct));
+
+ /// List countries (paged).
+ [HttpGet("code-lists/countries")]
+ public async Task Countries(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListCountriesAsync(page, pageSize, ct));
+
+ /// Get a country detail by id.
+ [HttpGet("code-lists/countries/{id:int}")]
+ public async Task CountryDetail(int id, CancellationToken ct)
+ => Ok(await _service.CountryDetailAsync(id, ct));
+
+ /// List currencies (paged).
+ [HttpGet("code-lists/currencies")]
+ public async Task Currencies(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListCurrenciesAsync(page, pageSize, ct));
+
+ /// Get a currency detail by id.
+ [HttpGet("code-lists/currencies/{id:int}")]
+ public async Task CurrencyDetail(int id, CancellationToken ct)
+ => Ok(await _service.CurrencyDetailAsync(id, ct));
+
+ /// List constant symbols (paged).
+ [HttpGet("code-lists/constant-symbols")]
+ public async Task ConstantSymbols(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListConstantSymbolsAsync(page, pageSize, ct));
+
+ /// Get a constant symbol detail by id.
+ [HttpGet("code-lists/constant-symbols/{id:int}")]
+ public async Task ConstantSymbolDetail(int id, CancellationToken ct)
+ => Ok(await _service.ConstantSymbolDetailAsync(id, ct));
+
+ /// List exchange rates (paged).
+ [HttpGet("code-lists/exchange-rates")]
+ public async Task ExchangeRates(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListExchangeRatesAsync(page, pageSize, ct));
+
+ /// Get an exchange rate detail by id.
+ [HttpGet("code-lists/exchange-rates/{id:int}")]
+ public async Task ExchangeRateDetail(int id, CancellationToken ct)
+ => Ok(await _service.ExchangeRateDetailAsync(id, ct));
+
+ /// List payment options (paged).
+ [HttpGet("code-lists/payment-options")]
+ public async Task PaymentOptions(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListPaymentOptionsAsync(page, pageSize, ct));
+
+ /// Get a payment option detail by id.
+ [HttpGet("code-lists/payment-options/{id:int}")]
+ public async Task PaymentOptionDetail(int id, CancellationToken ct)
+ => Ok(await _service.PaymentOptionDetailAsync(id, ct));
+
+ /// List VAT codes (paged).
+ [HttpGet("code-lists/vat-codes")]
+ public async Task VatCodes(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListVatCodesAsync(page, pageSize, ct));
+
+ /// Get a VAT code detail by id.
+ [HttpGet("code-lists/vat-codes/{id:int}")]
+ public async Task VatCodeDetail(int id, CancellationToken ct)
+ => Ok(await _service.VatCodeDetailAsync(id, ct));
+
+ /// List VAT reverse-charge codes (paged).
+ [HttpGet("code-lists/vat-reverse-charge-codes")]
+ public async Task VatReverseChargeCodes(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListVatReverseChargeCodesAsync(page, pageSize, ct));
+
+ /// Get a VAT reverse-charge code detail by id.
+ [HttpGet("code-lists/vat-reverse-charge-codes/{id:int}")]
+ public async Task VatReverseChargeCodeDetail(int id, CancellationToken ct)
+ => Ok(await _service.VatReverseChargeCodeDetailAsync(id, ct));
+
+ /// List sales offices (paged).
+ [HttpGet("code-lists/sales-offices")]
+ public async Task SalesOffices(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListSalesOfficesAsync(page, pageSize, ct));
+
+ /// Get a sales office detail by id.
+ [HttpGet("code-lists/sales-offices/{id:int}")]
+ public async Task SalesOfficeDetail(int id, CancellationToken ct)
+ => Ok(await _service.SalesOfficeDetailAsync(id, ct));
+
+ /// List sales POS equipment (paged).
+ [HttpGet("code-lists/sales-pos-equipment")]
+ public async Task SalesPosEquipment(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListSalesPosEquipmentAsync(page, pageSize, ct));
+
+ /// Get a sales POS equipment detail by id.
+ [HttpGet("code-lists/sales-pos-equipment/{id:int}")]
+ public async Task SalesPosEquipmentDetail(int id, CancellationToken ct)
+ => Ok(await _service.SalesPosEquipmentDetailAsync(id, ct));
+}
diff --git a/Controllers/IntegrationController.cs b/Controllers/IntegrationController.cs
new file mode 100644
index 0000000..c5dcca4
--- /dev/null
+++ b/Controllers/IntegrationController.cs
@@ -0,0 +1,122 @@
+using IdokladSdk.Enums;
+using IdokladSdk.Models.Attachment;
+using IdokladSdk.Models.Webhook;
+using Microsoft.AspNetCore.Mvc;
+using Idoklad.Services;
+
+namespace Idoklad.Controllers;
+
+///
+/// Integration / utility agendas: webhooks, notifications, change log, registered sales,
+/// unpaired documents, attachments and system code books. Requires iDoklad credentials.
+///
+[ApiController]
+[Produces("application/json")]
+[Tags("Integration")]
+public sealed class IntegrationController : ControllerBase
+{
+ private readonly IntegrationService _service;
+
+ public IntegrationController(IntegrationService service) => _service = service;
+
+ // ---- Webhooks ----
+
+ /// List configured webhooks (paged).
+ [HttpGet("webhooks")]
+ public async Task ListWebhooks(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListWebhooksAsync(page, pageSize, ct));
+
+ /// Get a webhook detail by id.
+ [HttpGet("webhooks/{id:int}")]
+ public async Task WebhookDetail(int id, CancellationToken ct)
+ => Ok(await _service.WebhookDetailAsync(id, ct));
+
+ /// Create (register) a new webhook subscription.
+ [HttpPost("webhooks")]
+ public async Task CreateWebhook([FromBody] AgendaWebhookSettingsPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateWebhookAsync(model, ct));
+
+ /// Delete a webhook by id.
+ [HttpDelete("webhooks/{id:int}")]
+ public async Task DeleteWebhook(int id, CancellationToken ct)
+ => Ok(await _service.DeleteWebhookAsync(id, ct));
+
+ // ---- Notifications ----
+
+ /// List notifications (paged).
+ [HttpGet("notifications")]
+ public async Task ListNotifications(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListNotificationsAsync(page, pageSize, ct));
+
+ /// Delete (dismiss) a notification by id.
+ [HttpDelete("notifications/{id:int}")]
+ public async Task DeleteNotification(int id, CancellationToken ct)
+ => Ok(await _service.DeleteNotificationAsync(id, ct));
+
+ // ---- Change log ----
+
+ /// List the agenda change log (paged).
+ [HttpGet("logs")]
+ public async Task ListLog(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListLogAsync(page, pageSize, ct));
+
+ // ---- Registered sales (EET) ----
+
+ /// List registered sales / EET records (paged).
+ [HttpGet("registered-sales")]
+ public async Task ListRegisteredSales(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListRegisteredSalesAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new registered sale.
+ [HttpGet("registered-sales/default")]
+ public async Task RegisteredSaleDefault(CancellationToken ct)
+ => Ok(await _service.RegisteredSaleDefaultAsync(ct));
+
+ // ---- Unpaired documents ----
+
+ /// List unpaired documents for a movement type and pairing document type.
+ [HttpGet("unpaired-documents/{movementType}/{pairingDocumentType}")]
+ public async Task ListUnpairedDocuments(
+ MovementType movementType, PairingDocumentType pairingDocumentType, int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListUnpairedDocumentsAsync(movementType, pairingDocumentType, page, pageSize, ct));
+
+ // ---- Attachments ----
+
+ /// Get a single attachment by its id.
+ [HttpGet("attachments/{attachmentId:int}")]
+ public async Task GetAttachment(int attachmentId, bool compressed = false, CancellationToken ct = default)
+ => Ok(await _service.GetAttachmentAsync(attachmentId, compressed, ct));
+
+ /// Get all attachments of a document.
+ [HttpGet("attachments/document/{documentId:int}/{documentType}")]
+ public async Task GetDocumentAttachments(
+ int documentId, AttachmentDocumentType documentType, bool compressed = false, CancellationToken ct = default)
+ => Ok(await _service.GetDocumentAttachmentsAsync(documentId, documentType, compressed, ct));
+
+ /// Upload an attachment for a document.
+ [HttpPost("attachments")]
+ public async Task UploadAttachment([FromBody] AttachmentUploadModel model, CancellationToken ct)
+ => Ok(await _service.UploadAttachmentAsync(model, ct));
+
+ /// Delete a single attachment by its id.
+ [HttpDelete("attachments/{attachmentId:int}")]
+ public async Task DeleteAttachment(int attachmentId, CancellationToken ct)
+ => Ok(await _service.DeleteAttachmentAsync(attachmentId, ct));
+
+ /// Delete all attachments of a document.
+ [HttpDelete("attachments/document/{documentId:int}/{documentType}")]
+ public async Task DeleteDocumentAttachments(int documentId, AttachmentDocumentType documentType, CancellationToken ct)
+ => Ok(await _service.DeleteDocumentAttachmentsAsync(documentId, documentType, ct));
+
+ // ---- System code books ----
+
+ /// Get the iDoklad system code books.
+ [HttpGet("system/code-books")]
+ public async Task CodeBooks(CancellationToken ct)
+ => Ok(await _service.CodeBooksAsync(ct));
+
+ /// Get changes to the code books since the given timestamp.
+ [HttpGet("system/code-books/changes")]
+ public async Task CodeBooksChanges([FromQuery] DateTime lastCheck, CancellationToken ct = default)
+ => Ok(await _service.CodeBooksChangesAsync(lastCheck, ct));
+}
diff --git a/Controllers/PaymentsController.cs b/Controllers/PaymentsController.cs
new file mode 100644
index 0000000..5242ca9
--- /dev/null
+++ b/Controllers/PaymentsController.cs
@@ -0,0 +1,112 @@
+using IdokladSdk.Models.BankStatement.Patch;
+using IdokladSdk.Models.BankStatement.Post;
+using IdokladSdk.Models.IssuedDocumentPayment;
+using IdokladSdk.Models.ReceivedDocumentPayments;
+using Microsoft.AspNetCore.Mvc;
+using Idoklad.Services;
+
+namespace Idoklad.Controllers;
+
+///
+/// Bank statements and document payments. Requires iDoklad credentials (headers or environment defaults).
+///
+[ApiController]
+[Produces("application/json")]
+[Tags("Payments")]
+public sealed class PaymentsController : ControllerBase
+{
+ private readonly PaymentsService _service;
+
+ public PaymentsController(PaymentsService service) => _service = service;
+
+ // ---- Bank statements ----
+
+ /// List bank statements (paged).
+ [HttpGet("bank-statements")]
+ public async Task ListBankStatements(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListBankStatementsAsync(page, pageSize, ct));
+
+ /// Get a bank statement detail by id.
+ [HttpGet("bank-statements/{id:int}")]
+ public async Task BankStatementDetail(int id, CancellationToken ct)
+ => Ok(await _service.BankStatementDetailAsync(id, ct));
+
+ /// Create a new bank statement.
+ [HttpPost("bank-statements")]
+ public async Task CreateBankStatement([FromBody] BankStatementPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateBankStatementAsync(model, ct));
+
+ /// Update an existing bank statement (the model id identifies the statement).
+ [HttpPatch("bank-statements")]
+ public async Task UpdateBankStatement([FromBody] BankStatementPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateBankStatementAsync(model, ct));
+
+ /// Delete a bank statement by id.
+ [HttpDelete("bank-statements/{id:int}")]
+ public async Task DeleteBankStatement(int id, CancellationToken ct)
+ => Ok(await _service.DeleteBankStatementAsync(id, ct));
+
+ // ---- Issued document payments ----
+
+ /// List issued document payments (paged).
+ [HttpGet("issued-payments")]
+ public async Task ListIssuedPayments(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListIssuedPaymentsAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default issued payment model for the given issued invoice id.
+ [HttpGet("issued-payments/default/{invoiceId:int}")]
+ public async Task IssuedPaymentDefault(int invoiceId, CancellationToken ct)
+ => Ok(await _service.IssuedPaymentDefaultAsync(invoiceId, ct));
+
+ /// Get an issued document payment detail by id.
+ [HttpGet("issued-payments/{id:int}")]
+ public async Task IssuedPaymentDetail(int id, CancellationToken ct)
+ => Ok(await _service.IssuedPaymentDetailAsync(id, ct));
+
+ /// Create (register) a new payment for an issued document.
+ [HttpPost("issued-payments")]
+ public async Task CreateIssuedPayment([FromBody] IssuedDocumentPaymentPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateIssuedPaymentAsync(model, ct));
+
+ /// Delete an issued document payment by id.
+ [HttpDelete("issued-payments/{id:int}")]
+ public async Task DeleteIssuedPayment(int id, CancellationToken ct)
+ => Ok(await _service.DeleteIssuedPaymentAsync(id, ct));
+
+ /// Fully unpay an issued invoice (removes all its payments).
+ [HttpPost("issued-payments/fully-unpay/{invoiceId:int}")]
+ public async Task FullyUnpayIssued(int invoiceId, CancellationToken ct)
+ => Ok(await _service.FullyUnpayIssuedAsync(invoiceId, ct));
+
+ // ---- Received document payments ----
+
+ /// List received document payments (paged).
+ [HttpGet("received-payments")]
+ public async Task ListReceivedPayments(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListReceivedPaymentsAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default received payment model for the given received invoice id.
+ [HttpGet("received-payments/default/{invoiceId:int}")]
+ public async Task ReceivedPaymentDefault(int invoiceId, CancellationToken ct)
+ => Ok(await _service.ReceivedPaymentDefaultAsync(invoiceId, ct));
+
+ /// Get a received document payment detail by id.
+ [HttpGet("received-payments/{id:int}")]
+ public async Task ReceivedPaymentDetail(int id, CancellationToken ct)
+ => Ok(await _service.ReceivedPaymentDetailAsync(id, ct));
+
+ /// Create (register) a new payment for a received document.
+ [HttpPost("received-payments")]
+ public async Task CreateReceivedPayment([FromBody] ReceivedDocumentPaymentPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateReceivedPaymentAsync(model, ct));
+
+ /// Delete a received document payment by id.
+ [HttpDelete("received-payments/{id:int}")]
+ public async Task DeleteReceivedPayment(int id, CancellationToken ct)
+ => Ok(await _service.DeleteReceivedPaymentAsync(id, ct));
+
+ /// Fully unpay a received invoice (removes all its payments).
+ [HttpPost("received-payments/fully-unpay/{invoiceId:int}")]
+ public async Task FullyUnpayReceived(int invoiceId, CancellationToken ct)
+ => Ok(await _service.FullyUnpayReceivedAsync(invoiceId, ct));
+}
diff --git a/Controllers/PurchaseCashController.cs b/Controllers/PurchaseCashController.cs
new file mode 100644
index 0000000..fd273fd
--- /dev/null
+++ b/Controllers/PurchaseCashController.cs
@@ -0,0 +1,107 @@
+using IdokladSdk.Models.CashRegister;
+using IdokladSdk.Models.CashVoucher;
+using IdokladSdk.Models.ReceivedReceipt.Patch;
+using IdokladSdk.Models.ReceivedReceipt.Post;
+using Microsoft.AspNetCore.Mvc;
+using Idoklad.Services;
+
+namespace Idoklad.Controllers;
+
+///
+/// Received receipts and cash agendas. Requires iDoklad credentials (headers or environment defaults).
+///
+[ApiController]
+[Produces("application/json")]
+[Tags("PurchaseAndCash")]
+public sealed class PurchaseCashController : ControllerBase
+{
+ private readonly PurchaseCashService _service;
+
+ public PurchaseCashController(PurchaseCashService service) => _service = service;
+
+ // ---- Received receipts ----
+
+ /// List received receipts (paged).
+ [HttpGet("received-receipts")]
+ public async Task ListReceivedReceipts(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListReceivedReceiptsAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new received receipt.
+ [HttpGet("received-receipts/default")]
+ public async Task ReceivedReceiptDefault(CancellationToken ct)
+ => Ok(await _service.ReceivedReceiptDefaultAsync(ct));
+
+ /// Get a received receipt detail by id.
+ [HttpGet("received-receipts/{id:int}")]
+ public async Task ReceivedReceiptDetail(int id, CancellationToken ct)
+ => Ok(await _service.ReceivedReceiptDetailAsync(id, ct));
+
+ /// Create a new received receipt.
+ [HttpPost("received-receipts")]
+ public async Task CreateReceivedReceipt([FromBody] ReceivedReceiptPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateReceivedReceiptAsync(model, ct));
+
+ /// Update an existing received receipt (the model id identifies the document).
+ [HttpPatch("received-receipts")]
+ public async Task UpdateReceivedReceipt([FromBody] ReceivedReceiptPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateReceivedReceiptAsync(model, ct));
+
+ /// Delete a received receipt by id.
+ [HttpDelete("received-receipts/{id:int}")]
+ public async Task DeleteReceivedReceipt(int id, CancellationToken ct)
+ => Ok(await _service.DeleteReceivedReceiptAsync(id, ct));
+
+ // ---- Cash vouchers ----
+
+ /// List cash vouchers (paged).
+ [HttpGet("cash-vouchers")]
+ public async Task ListCashVouchers(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListCashVouchersAsync(page, pageSize, ct));
+
+ /// Get a cash voucher detail by id.
+ [HttpGet("cash-vouchers/{id:int}")]
+ public async Task CashVoucherDetail(int id, CancellationToken ct)
+ => Ok(await _service.CashVoucherDetailAsync(id, ct));
+
+ /// Create a new cash voucher.
+ [HttpPost("cash-vouchers")]
+ public async Task CreateCashVoucher([FromBody] CashVoucherPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateCashVoucherAsync(model, ct));
+
+ /// Update an existing cash voucher (the model id identifies the document).
+ [HttpPatch("cash-vouchers")]
+ public async Task UpdateCashVoucher([FromBody] CashVoucherPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateCashVoucherAsync(model, ct));
+
+ /// Delete a cash voucher by id.
+ [HttpDelete("cash-vouchers/{id:int}")]
+ public async Task DeleteCashVoucher(int id, CancellationToken ct)
+ => Ok(await _service.DeleteCashVoucherAsync(id, ct));
+
+ // ---- Cash registers ----
+
+ /// List cash registers (paged).
+ [HttpGet("cash-registers")]
+ public async Task ListCashRegisters(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListCashRegistersAsync(page, pageSize, ct));
+
+ /// Get a cash register detail by id.
+ [HttpGet("cash-registers/{id:int}")]
+ public async Task CashRegisterDetail(int id, CancellationToken ct)
+ => Ok(await _service.CashRegisterDetailAsync(id, ct));
+
+ /// Create a new cash register.
+ [HttpPost("cash-registers")]
+ public async Task CreateCashRegister([FromBody] CashRegisterPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateCashRegisterAsync(model, ct));
+
+ /// Update an existing cash register (the model id identifies the register).
+ [HttpPatch("cash-registers")]
+ public async Task UpdateCashRegister([FromBody] CashRegisterPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateCashRegisterAsync(model, ct));
+
+ /// Delete a cash register by id.
+ [HttpDelete("cash-registers/{id:int}")]
+ public async Task DeleteCashRegister(int id, CancellationToken ct)
+ => Ok(await _service.DeleteCashRegisterAsync(id, ct));
+}
diff --git a/Controllers/SalesDocumentsController.cs b/Controllers/SalesDocumentsController.cs
new file mode 100644
index 0000000..d23bebe
--- /dev/null
+++ b/Controllers/SalesDocumentsController.cs
@@ -0,0 +1,270 @@
+using IdokladSdk.Models.CreditNote;
+using IdokladSdk.Models.IssuedDocumentTemplate.Patch;
+using IdokladSdk.Models.IssuedDocumentTemplate.Post;
+using IdokladSdk.Models.IssuedTaxDocument.Patch;
+using IdokladSdk.Models.IssuedTaxDocument.Post;
+using IdokladSdk.Models.ProformaInvoice;
+using IdokladSdk.Models.RecurringInvoice;
+using IdokladSdk.Models.SalesOrder;
+using IdokladSdk.Models.SalesReceipt;
+using Microsoft.AspNetCore.Mvc;
+using Idoklad.Services;
+
+namespace Idoklad.Controllers;
+
+///
+/// Sales-side documents. Requires iDoklad credentials (headers or environment defaults).
+///
+[ApiController]
+[Produces("application/json")]
+[Tags("SalesDocuments")]
+public sealed class SalesDocumentsController : ControllerBase
+{
+ private readonly SalesDocumentsService _service;
+
+ public SalesDocumentsController(SalesDocumentsService service) => _service = service;
+
+ // ---- Proforma invoices ----
+
+ /// List proforma invoices (paged).
+ [HttpGet("proforma-invoices")]
+ public async Task ListProforma(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListProformaAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new proforma invoice.
+ [HttpGet("proforma-invoices/default")]
+ public async Task ProformaDefault(CancellationToken ct)
+ => Ok(await _service.ProformaDefaultAsync(ct));
+
+ /// Get a proforma invoice detail by id.
+ [HttpGet("proforma-invoices/{id:int}")]
+ public async Task ProformaDetail(int id, CancellationToken ct)
+ => Ok(await _service.ProformaDetailAsync(id, ct));
+
+ /// Create a new proforma invoice.
+ [HttpPost("proforma-invoices")]
+ public async Task CreateProforma([FromBody] ProformaInvoicePostModel model, CancellationToken ct)
+ => Ok(await _service.CreateProformaAsync(model, ct));
+
+ /// Update an existing proforma invoice (the model id identifies the document).
+ [HttpPatch("proforma-invoices")]
+ public async Task UpdateProforma([FromBody] ProformaInvoicePatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateProformaAsync(model, ct));
+
+ /// Create a copy (draft) of an existing proforma invoice.
+ [HttpPost("proforma-invoices/{id:int}/copy")]
+ public async Task CopyProforma(int id, CancellationToken ct)
+ => Ok(await _service.ProformaCopyAsync(id, ct));
+
+ /// Delete a proforma invoice by id.
+ [HttpDelete("proforma-invoices/{id:int}")]
+ public async Task DeleteProforma(int id, CancellationToken ct)
+ => Ok(await _service.DeleteProformaAsync(id, ct));
+
+ // ---- Credit notes ----
+
+ /// List credit notes (paged).
+ [HttpGet("credit-notes")]
+ public async Task ListCreditNotes(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListCreditNotesAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default credit note model for the given (issued) invoice id.
+ [HttpGet("credit-notes/default/{invoiceId:int}")]
+ public async Task CreditNoteDefault(int invoiceId, CancellationToken ct)
+ => Ok(await _service.CreditNoteDefaultAsync(invoiceId, ct));
+
+ /// Get a credit note detail by id.
+ [HttpGet("credit-notes/{id:int}")]
+ public async Task CreditNoteDetail(int id, CancellationToken ct)
+ => Ok(await _service.CreditNoteDetailAsync(id, ct));
+
+ /// Create a new credit note.
+ [HttpPost("credit-notes")]
+ public async Task CreateCreditNote([FromBody] CreditNotePostModel model, CancellationToken ct)
+ => Ok(await _service.CreateCreditNoteAsync(model, ct));
+
+ /// Update an existing credit note (the model id identifies the document).
+ [HttpPatch("credit-notes")]
+ public async Task UpdateCreditNote([FromBody] CreditNotePatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateCreditNoteAsync(model, ct));
+
+ /// Delete a credit note by id.
+ [HttpDelete("credit-notes/{id:int}")]
+ public async Task DeleteCreditNote(int id, CancellationToken ct)
+ => Ok(await _service.DeleteCreditNoteAsync(id, ct));
+
+ // ---- Sales receipts ----
+
+ /// List sales receipts (paged).
+ [HttpGet("sales-receipts")]
+ public async Task ListSalesReceipts(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListSalesReceiptsAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new sales receipt.
+ [HttpGet("sales-receipts/default")]
+ public async Task SalesReceiptDefault(CancellationToken ct)
+ => Ok(await _service.SalesReceiptDefaultAsync(ct));
+
+ /// Get a sales receipt detail by id.
+ [HttpGet("sales-receipts/{id:int}")]
+ public async Task SalesReceiptDetail(int id, CancellationToken ct)
+ => Ok(await _service.SalesReceiptDetailAsync(id, ct));
+
+ /// Create a new sales receipt.
+ [HttpPost("sales-receipts")]
+ public async Task CreateSalesReceipt([FromBody] SalesReceiptPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateSalesReceiptAsync(model, ct));
+
+ /// Update an existing sales receipt (the model id identifies the document).
+ [HttpPatch("sales-receipts")]
+ public async Task UpdateSalesReceipt([FromBody] SalesReceiptPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateSalesReceiptAsync(model, ct));
+
+ /// Create a copy (draft) of an existing sales receipt.
+ [HttpPost("sales-receipts/{id:int}/copy")]
+ public async Task CopySalesReceipt(int id, CancellationToken ct)
+ => Ok(await _service.SalesReceiptCopyAsync(id, ct));
+
+ /// Delete a sales receipt by id.
+ [HttpDelete("sales-receipts/{id:int}")]
+ public async Task DeleteSalesReceipt(int id, CancellationToken ct)
+ => Ok(await _service.DeleteSalesReceiptAsync(id, ct));
+
+ // ---- Sales orders ----
+
+ /// List sales orders (paged).
+ [HttpGet("sales-orders")]
+ public async Task ListSalesOrders(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListSalesOrdersAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new sales order.
+ [HttpGet("sales-orders/default")]
+ public async Task SalesOrderDefault(CancellationToken ct)
+ => Ok(await _service.SalesOrderDefaultAsync(ct));
+
+ /// Get a sales order detail by id.
+ [HttpGet("sales-orders/{id:int}")]
+ public async Task SalesOrderDetail(int id, CancellationToken ct)
+ => Ok(await _service.SalesOrderDetailAsync(id, ct));
+
+ /// Create a new sales order.
+ [HttpPost("sales-orders")]
+ public async Task CreateSalesOrder([FromBody] SalesOrderPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateSalesOrderAsync(model, ct));
+
+ /// Update an existing sales order (the model id identifies the document).
+ [HttpPatch("sales-orders")]
+ public async Task UpdateSalesOrder([FromBody] SalesOrderPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateSalesOrderAsync(model, ct));
+
+ /// Create a copy (draft) of an existing sales order.
+ [HttpPost("sales-orders/{id:int}/copy")]
+ public async Task CopySalesOrder(int id, CancellationToken ct)
+ => Ok(await _service.SalesOrderCopyAsync(id, ct));
+
+ /// Delete a sales order by id.
+ [HttpDelete("sales-orders/{id:int}")]
+ public async Task DeleteSalesOrder(int id, CancellationToken ct)
+ => Ok(await _service.DeleteSalesOrderAsync(id, ct));
+
+ // ---- Recurring invoices ----
+
+ /// List recurring invoices (paged).
+ [HttpGet("recurring-invoices")]
+ public async Task ListRecurring(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListRecurringAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new recurring invoice.
+ [HttpGet("recurring-invoices/default")]
+ public async Task RecurringDefault(CancellationToken ct)
+ => Ok(await _service.RecurringDefaultAsync(ct));
+
+ /// Get a recurring invoice detail by id.
+ [HttpGet("recurring-invoices/{id:int}")]
+ public async Task RecurringDetail(int id, CancellationToken ct)
+ => Ok(await _service.RecurringDetailAsync(id, ct));
+
+ /// Create a new recurring invoice template.
+ [HttpPost("recurring-invoices")]
+ public async Task CreateRecurring([FromBody] RecurringInvoicePostModel model, CancellationToken ct)
+ => Ok(await _service.CreateRecurringAsync(model, ct));
+
+ /// Update an existing recurring invoice (the model id identifies the template).
+ [HttpPatch("recurring-invoices")]
+ public async Task UpdateRecurring([FromBody] RecurringInvoicePatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateRecurringAsync(model, ct));
+
+ /// Create a copy (draft) of an existing recurring invoice.
+ [HttpPost("recurring-invoices/{id:int}/copy")]
+ public async Task CopyRecurring(int id, CancellationToken ct)
+ => Ok(await _service.RecurringCopyAsync(id, ct));
+
+ /// Delete a recurring invoice by id.
+ [HttpDelete("recurring-invoices/{id:int}")]
+ public async Task DeleteRecurring(int id, CancellationToken ct)
+ => Ok(await _service.DeleteRecurringAsync(id, ct));
+
+ // ---- Issued tax documents ----
+
+ /// List issued tax documents (paged).
+ [HttpGet("issued-tax-documents")]
+ public async Task ListTaxDocuments(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListTaxDocumentsAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default tax document model for the given advance (proforma) invoice id.
+ [HttpGet("issued-tax-documents/default/{advanceInvoiceId:int}")]
+ public async Task TaxDocumentDefault(int advanceInvoiceId, CancellationToken ct)
+ => Ok(await _service.TaxDocumentDefaultAsync(advanceInvoiceId, ct));
+
+ /// Get an issued tax document detail by id.
+ [HttpGet("issued-tax-documents/{id:int}")]
+ public async Task TaxDocumentDetail(int id, CancellationToken ct)
+ => Ok(await _service.TaxDocumentDetailAsync(id, ct));
+
+ /// Create a new issued tax document.
+ [HttpPost("issued-tax-documents")]
+ public async Task CreateTaxDocument([FromBody] IssuedTaxDocumentPostModel model, CancellationToken ct)
+ => Ok(await _service.CreateTaxDocumentAsync(model, ct));
+
+ /// Update an existing issued tax document (the model id identifies the document).
+ [HttpPatch("issued-tax-documents")]
+ public async Task UpdateTaxDocument([FromBody] IssuedTaxDocumentPatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateTaxDocumentAsync(model, ct));
+
+ /// Delete an issued tax document by id.
+ [HttpDelete("issued-tax-documents/{id:int}")]
+ public async Task DeleteTaxDocument(int id, CancellationToken ct)
+ => Ok(await _service.DeleteTaxDocumentAsync(id, ct));
+
+ // ---- Issued document templates ----
+
+ /// List issued document templates (paged).
+ [HttpGet("issued-document-templates")]
+ public async Task ListTemplates(int page = 1, int pageSize = 20, CancellationToken ct = default)
+ => Ok(await _service.ListTemplatesAsync(page, pageSize, ct));
+
+ /// Get a pre-filled default model for creating a new issued document template.
+ [HttpGet("issued-document-templates/default")]
+ public async Task TemplateDefault(CancellationToken ct)
+ => Ok(await _service.TemplateDefaultAsync(ct));
+
+ /// Get an issued document template detail by id.
+ [HttpGet("issued-document-templates/{id:int}")]
+ public async Task TemplateDetail(int id, CancellationToken ct)
+ => Ok(await _service.TemplateDetailAsync(id, ct));
+
+ /// Create a new issued document template.
+ [HttpPost("issued-document-templates")]
+ public async Task CreateTemplate([FromBody] IssuedDocumentTemplatePostModel model, CancellationToken ct)
+ => Ok(await _service.CreateTemplateAsync(model, ct));
+
+ /// Update an existing issued document template (the model id identifies the template).
+ [HttpPatch("issued-document-templates")]
+ public async Task UpdateTemplate([FromBody] IssuedDocumentTemplatePatchModel model, CancellationToken ct)
+ => Ok(await _service.UpdateTemplateAsync(model, ct));
+
+ /// Delete an issued document template by id.
+ [HttpDelete("issued-document-templates/{id:int}")]
+ public async Task DeleteTemplate(int id, CancellationToken ct)
+ => Ok(await _service.DeleteTemplateAsync(id, ct));
+}
diff --git a/Controllers/StatisticsController.cs b/Controllers/StatisticsController.cs
new file mode 100644
index 0000000..2d3fd47
--- /dev/null
+++ b/Controllers/StatisticsController.cs
@@ -0,0 +1,62 @@
+using IdokladSdk.Enums;
+using Microsoft.AspNetCore.Mvc;
+using Idoklad.Services;
+
+namespace Idoklad.Controllers;
+
+/// Agenda statistics and summaries. Requires iDoklad credentials.
+[ApiController]
+[Route("statistics")]
+[Produces("application/json")]
+[Tags("Statistics")]
+public sealed class StatisticsController : ControllerBase
+{
+ private readonly StatisticsService _service;
+
+ public StatisticsController(StatisticsService service) => _service = service;
+
+ /// Invoicing totals for a relative period (e.g. LastSevenDays, ThisMonth).
+ [HttpGet("invoicing-for-period/{periodType}")]
+ public async Task InvoicingForPeriod(PeriodType periodType, CancellationToken ct)
+ => Ok(await _service.InvoicingForPeriodAsync(periodType, ct));
+
+ /// Invoicing totals for a relative year (e.g. ThisYear, LastYear).
+ [HttpGet("invoicing-for-year/{yearType}")]
+ public async Task InvoicingForYear(YearType yearType, CancellationToken ct)
+ => Ok(await _service.InvoicingForYearAsync(yearType, ct));
+
+ /// Invoicing summary broken down by quarter.
+ [HttpGet("quarter-summary")]
+ public async Task QuarterSummary(CancellationToken ct)
+ => Ok(await _service.QuarterSummaryAsync(ct));
+
+ /// Top partners by turnover (optionally limited by count).
+ [HttpGet("top-partners")]
+ public async Task TopPartners([FromQuery] int? count, CancellationToken ct)
+ => Ok(await _service.TopPartnersAsync(count, ct));
+
+ /// Overall agenda summary (totals and key figures).
+ [HttpGet("agenda-summary")]
+ public async Task AgendaSummary(CancellationToken ct)
+ => Ok(await _service.AgendaSummaryAsync(ct));
+
+ /// Statistics for a single contact by id.
+ [HttpGet("contact/{id:int}")]
+ public async Task StatisticForContact(int id, CancellationToken ct)
+ => Ok(await _service.StatisticForContactAsync(id, ct));
+
+ /// Receivables broken down by debt age intervals.
+ [HttpGet("debt-intervals")]
+ public async Task DebtIntervals(CancellationToken ct)
+ => Ok(await _service.DebtIntervalsAsync(ct));
+
+ /// Top debtors by outstanding amount (optionally limited by count).
+ [HttpGet("top-debtors")]
+ public async Task TopDebtors([FromQuery] int? count, CancellationToken ct)
+ => Ok(await _service.TopDebtorsAsync(count, ct));
+
+ /// Progress of the agenda towards the VAT-payer registration threshold.
+ [HttpGet("vat-payer-progress")]
+ public async Task VatPayerProgress(CancellationToken ct)
+ => Ok(await _service.VatPayerProgressAsync(ct));
+}
diff --git a/Program.cs b/Program.cs
index 415a109..e7182f7 100644
--- a/Program.cs
+++ b/Program.cs
@@ -31,6 +31,13 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
+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
@@ -81,7 +88,10 @@ app.UseMiddleware();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
- options.SwaggerEndpoint("v1/swagger.json", $"{settings.AppName} v1");
+ // Serve the interactive docs at /docs (matching the sibling microsoft-365-service).
+ options.RoutePrefix = "docs";
+ options.SwaggerEndpoint("/swagger/v1/swagger.json", $"{settings.AppName} v1");
+ options.DocumentTitle = $"{settings.AppName} – API docs";
});
app.MapControllers();
diff --git a/README.md b/README.md
index f412079..d0996c0 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ controllery a samostatná dokumentace ve Swaggeru.
## Konfigurace (proměnné prostředí)
-Aplikace, ClientId, ClientSecret a ApplicationId získáte ve vývojářském portálu iDoklad. Tyto
+Aplikaci, ClientId, ClientSecret a ApplicationId získáte ve vývojářském portálu iDoklad. Tyto
hodnoty lze nastavit jako výchozí přes proměnné prostředí, nebo je předávat per-request v hlavičkách
(viz níže).
@@ -38,7 +38,7 @@ ROOT_PATH= # base path při běhu za reverzní proxy
Citlivé proměnné (zejména **secret**) se **nepředávají v query stringu ani v těle** požadavku –
**vyžadují se v HTTP hlavičkách**. To je zohledněno i ve Swaggeru: u každého agendového endpointu
-jsou hlavičky vtypu `X-...` zdokumentované jako parametry.
+jsou tyto hlavičky zdokumentované jako parametry.
| Hlavička | Význam | Fallback (env) |
| --- | --- | --- |
@@ -60,64 +60,56 @@ X-ClientSecret:
X-ApplicationId:
```
-## API
+## Swagger / OpenAPI
-```http
-GET /health
-GET /version
-GET /status
+Interaktivní dokumentace běží na **`/docs`**, surový OpenAPI dokument na `/swagger/v1/swagger.json`.
+Každý agendový endpoint má ve Swaggeru zdokumentované credential hlavičky i popisky operací.
-# Account
-GET /account/agenda
-GET /account/user
+## Pokryté agendy
-# Contacts
-GET /contacts?page=1&pageSize=20
-GET /contacts/default
-GET /contacts/{id}
-POST /contacts
-PATCH /contacts
-DELETE /contacts/{id}
+Propojeno je **41 ze 45 clientů** SDK 5.3.0. Standardní agendy podporují stránkovaný `list`,
+`detail`, `default` (kde to SDK umožňuje) a `create`/`update`/`delete`; číselníky jsou read-only.
-# Issued invoices (vydané faktury)
-GET /issued-invoices?page=1&pageSize=20
-GET /issued-invoices/default
-GET /issued-invoices/{id}
-POST /issued-invoices
-PATCH /issued-invoices
-POST /issued-invoices/{id}/copy
-DELETE /issued-invoices/{id}
+Meta (bez credentials): `GET /health`, `GET /version`, `GET /status`.
-# Received invoices (přijaté faktury)
-GET /received-invoices?page=1&pageSize=20
-GET /received-invoices/default
-GET /received-invoices/{id}
-POST /received-invoices
-PATCH /received-invoices
-DELETE /received-invoices/{id}
-
-# Registry
-GET /registers/bank-accounts?page=1&pageSize=20
-GET /registers/bank-accounts/{id}
-POST /registers/bank-accounts
-PATCH /registers/bank-accounts
-DELETE /registers/bank-accounts/{id}
-GET /registers/vat-rates?page=1&pageSize=20
-GET /registers/vat-rates/{id}
-GET /registers/numeric-sequences?page=1&pageSize=20
-```
+| Tag | Agendy (cesty) |
+| --- | --- |
+| Account | `/account/agenda`, `/account/user` |
+| Contacts | `/contacts` |
+| IssuedInvoices | `/issued-invoices` (+ `/default`, `/{id}/copy`) |
+| ReceivedInvoices | `/received-invoices` (+ `/default`) |
+| SalesDocuments | `/proforma-invoices`, `/credit-notes`, `/sales-receipts`, `/sales-orders`, `/recurring-invoices`, `/issued-tax-documents`, `/issued-document-templates` |
+| PurchaseAndCash | `/received-receipts`, `/cash-vouchers`, `/cash-registers` |
+| Payments | `/bank-statements`, `/issued-payments`, `/received-payments` (+ `/fully-unpay/{invoiceId}`) |
+| Catalog | `/price-list-items`, `/stock-movements`, `/tags` |
+| Registers | `/registers/bank-accounts`, `/registers/vat-rates`, `/registers/numeric-sequences` |
+| CodeLists | `/code-lists/banks`, `/countries`, `/currencies`, `/constant-symbols`, `/exchange-rates`, `/payment-options`, `/vat-codes`, `/vat-reverse-charge-codes`, `/sales-offices`, `/sales-pos-equipment` |
+| Integration | `/webhooks`, `/notifications`, `/logs`, `/registered-sales`, `/unpaired-documents`, `/attachments`, `/system/code-books` |
+| Statistics | `/statistics/*` (invoicing-for-period/year, quarter-summary, top-partners, agenda-summary, contact/{id}, debt-intervals, top-debtors, vat-payer-progress) |
Request/response těla odpovídají modelům iDoklad SDK (`*PostModel`, `*PatchModel`, `*GetModel`).
Serializace používá Newtonsoft.Json, aby se chování shodovalo s atributy modelů v SDK. Chyby z
iDoklad API se propagují jako `application/problem+json` s odpovídajícím HTTP statusem.
+### Zatím nezapojené (na vyžádání)
+
+Tyto klienty mají nestandardní/binární/facade charakter a nejsou zatím napojené:
+
+- **MailClient** – odesílání dokladů e-mailem (facade nad typy dokladů).
+- **ReportClient** – generování PDF reportů (binární výstup).
+- **DocumentPaymentClient** – facade nad platbami (překrývá se s `issued-payments` / `received-payments`).
+- **BatchClient** – dávkové operace.
+
+Mimo to nejsou napojené pokročilé operace `Recount`, `*Batch` a `RecurringInvoice/NextIssueDates`.
+`InboxClient` a `ReceivedDocumentsClient` v SDK 5.3.0 ještě nejsou (přibyly až po vydání).
+
## Lokální spuštění
```bash
dotnet run
```
-Swagger UI je na `/swagger`, OpenAPI dokument na `/swagger/v1/swagger.json`.
+Swagger UI otevřete na `http://localhost:/docs`.
## Architektura
diff --git a/Services/CatalogService.cs b/Services/CatalogService.cs
new file mode 100644
index 0000000..f7df56e
--- /dev/null
+++ b/Services/CatalogService.cs
@@ -0,0 +1,64 @@
+using IdokladSdk.Models.PriceListItem;
+using IdokladSdk.Models.StockMovement;
+using IdokladSdk.Models.Tag;
+using Idoklad.Client;
+
+namespace Idoklad.Services;
+
+///
+/// Catalog and stock agendas: price list items, stock movements and tags.
+///
+public sealed class CatalogService
+{
+ private readonly IdokladApiAccessor _accessor;
+
+ public CatalogService(IdokladApiAccessor accessor) => _accessor = accessor;
+
+ // ---- Price list items ----
+ public Task ListPriceListItemsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.PriceListItemClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task PriceListItemDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.PriceListItemClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task PriceListItemDefaultAsync(CancellationToken ct)
+ => (await _accessor.Api.PriceListItemClient.DefaultAsync(ct)).Unwrap();
+
+ public async Task CreatePriceListItemAsync(PriceListItemPostModel model, CancellationToken ct)
+ => (await _accessor.Api.PriceListItemClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdatePriceListItemAsync(PriceListItemPatchModel model, CancellationToken ct)
+ => (await _accessor.Api.PriceListItemClient.UpdateAsync(model, ct)).Unwrap();
+
+ // ---- Stock movements ----
+ public Task ListStockMovementsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.StockMovementClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task StockMovementDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.StockMovementClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task StockMovementDefaultAsync(int priceListItemId, CancellationToken ct)
+ => (await _accessor.Api.StockMovementClient.DefaultAsync(priceListItemId, ct)).Unwrap();
+
+ public async Task CreateStockMovementAsync(StockMovementPostModel model, CancellationToken ct)
+ => (await _accessor.Api.StockMovementClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdateStockMovementAsync(StockMovementPatchModel model, CancellationToken ct)
+ => (await _accessor.Api.StockMovementClient.UpdateAsync(model, ct)).Unwrap();
+
+ public async Task DeleteStockMovementAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.StockMovementClient.DeleteAsync(id, ct)).Unwrap();
+
+ // ---- Tags ----
+ public Task ListTagsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.TagClient.List().ToPageAsync(page, pageSize, ct);
+
+ public async Task CreateTagAsync(TagPostModel model, CancellationToken ct)
+ => (await _accessor.Api.TagClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdateTagAsync(TagPatchModel model, CancellationToken ct)
+ => (await _accessor.Api.TagClient.UpdateAsync(model, ct)).Unwrap();
+
+ public async Task DeleteTagAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.TagClient.DeleteAsync(id, ct)).Unwrap();
+}
diff --git a/Services/CodeListsService.cs b/Services/CodeListsService.cs
new file mode 100644
index 0000000..6bc97dc
--- /dev/null
+++ b/Services/CodeListsService.cs
@@ -0,0 +1,75 @@
+using Idoklad.Client;
+
+namespace Idoklad.Services;
+
+///
+/// Read-only iDoklad code lists (registers): banks, countries, currencies, constant symbols,
+/// exchange rates, payment options, VAT codes, VAT reverse-charge codes, sales offices and
+/// sales POS equipment. Every agenda supports list + detail.
+///
+public sealed class CodeListsService
+{
+ private readonly IdokladApiAccessor _accessor;
+
+ public CodeListsService(IdokladApiAccessor accessor) => _accessor = accessor;
+
+ public Task ListBanksAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.BankClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task BankDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.BankClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListCountriesAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.CountryClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task CountryDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.CountryClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListCurrenciesAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.CurrencyClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task CurrencyDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.CurrencyClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListConstantSymbolsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.ConstantSymbolClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task ConstantSymbolDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.ConstantSymbolClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListExchangeRatesAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.ExchangeRateClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task ExchangeRateDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.ExchangeRateClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListPaymentOptionsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.PaymentOptionClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task PaymentOptionDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.PaymentOptionClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListVatCodesAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.VatCodeClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task VatCodeDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.VatCodeClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListVatReverseChargeCodesAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.VatReverseChargeCodeClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task VatReverseChargeCodeDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.VatReverseChargeCodeClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListSalesOfficesAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.SalesOfficeClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task SalesOfficeDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.SalesOfficeClient.Detail(id).ToDetailAsync(ct);
+
+ public Task ListSalesPosEquipmentAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.SalesPosEquipmentClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task SalesPosEquipmentDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.SalesPosEquipmentClient.Detail(id).ToDetailAsync(ct);
+}
diff --git a/Services/IntegrationService.cs b/Services/IntegrationService.cs
new file mode 100644
index 0000000..78569f9
--- /dev/null
+++ b/Services/IntegrationService.cs
@@ -0,0 +1,76 @@
+using IdokladSdk.Enums;
+using IdokladSdk.Models.Attachment;
+using IdokladSdk.Models.Webhook;
+using Idoklad.Client;
+
+namespace Idoklad.Services;
+
+///
+/// Integration / utility agendas: webhooks, notifications, change log, registered sales (EET),
+/// unpaired documents, attachments and the system code books.
+///
+public sealed class IntegrationService
+{
+ private readonly IdokladApiAccessor _accessor;
+
+ public IntegrationService(IdokladApiAccessor accessor) => _accessor = accessor;
+
+ // ---- Webhooks ----
+ public Task ListWebhooksAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.WebhookClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task WebhookDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.WebhookClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task CreateWebhookAsync(AgendaWebhookSettingsPostModel model, CancellationToken ct)
+ => (await _accessor.Api.WebhookClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task DeleteWebhookAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.WebhookClient.DeleteAsync(id, ct)).Unwrap();
+
+ // ---- Notifications ----
+ public Task ListNotificationsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.NotificationClient.List().ToPageAsync(page, pageSize, ct);
+
+ public async Task DeleteNotificationAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.NotificationClient.DeleteAsync(id, ct)).Unwrap();
+
+ // ---- Change log ----
+ public Task ListLogAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.LogClient.List().ToPageAsync(page, pageSize, ct);
+
+ // ---- Registered sales (EET) ----
+ public Task ListRegisteredSalesAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.RegisteredSaleClient.List().ToPageAsync(page, pageSize, ct);
+
+ public async Task RegisteredSaleDefaultAsync(CancellationToken ct)
+ => (await _accessor.Api.RegisteredSaleClient.DefaultAsync(ct)).Unwrap();
+
+ // ---- Unpaired documents ----
+ public Task ListUnpairedDocumentsAsync(
+ MovementType movementType, PairingDocumentType pairingDocumentType, int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.UnpairedDocumentClient.List(movementType, pairingDocumentType).ToPageAsync(page, pageSize, ct);
+
+ // ---- Attachments ----
+ public async Task GetAttachmentAsync(int attachmentId, bool compressed, CancellationToken ct)
+ => (await _accessor.Api.AttachmentClient.GetAsync(attachmentId, compressed, ct)).Unwrap();
+
+ public async Task GetDocumentAttachmentsAsync(int documentId, AttachmentDocumentType documentType, bool compressed, CancellationToken ct)
+ => (await _accessor.Api.AttachmentClient.GetAsync(documentId, documentType, compressed, ct)).Unwrap();
+
+ public async Task UploadAttachmentAsync(AttachmentUploadModel model, CancellationToken ct)
+ => (await _accessor.Api.AttachmentClient.UploadAsync(model, ct)).Unwrap();
+
+ public async Task DeleteAttachmentAsync(int attachmentId, CancellationToken ct)
+ => (await _accessor.Api.AttachmentClient.DeleteAsync(attachmentId, ct)).Unwrap();
+
+ public async Task DeleteDocumentAttachmentsAsync(int documentId, AttachmentDocumentType documentType, CancellationToken ct)
+ => (await _accessor.Api.AttachmentClient.DeleteAsync(documentId, documentType, ct)).Unwrap();
+
+ // ---- System code books ----
+ public Task CodeBooksAsync(CancellationToken ct)
+ => _accessor.Api.SystemClient.CodeBooks().ToDetailAsync(ct);
+
+ public Task CodeBooksChangesAsync(DateTime lastCheck, CancellationToken ct)
+ => _accessor.Api.SystemClient.CodeBooksChanges(lastCheck).ToDetailAsync(ct);
+}
diff --git a/Services/PaymentsService.cs b/Services/PaymentsService.cs
new file mode 100644
index 0000000..af47aa5
--- /dev/null
+++ b/Services/PaymentsService.cs
@@ -0,0 +1,72 @@
+using IdokladSdk.Models.BankStatement.Patch;
+using IdokladSdk.Models.BankStatement.Post;
+using IdokladSdk.Models.IssuedDocumentPayment;
+using IdokladSdk.Models.ReceivedDocumentPayments;
+using Idoklad.Client;
+
+namespace Idoklad.Services;
+
+///
+/// Payments and bank statements: bank statements, issued document payments and received document
+/// payments.
+///
+public sealed class PaymentsService
+{
+ private readonly IdokladApiAccessor _accessor;
+
+ public PaymentsService(IdokladApiAccessor accessor) => _accessor = accessor;
+
+ // ---- Bank statements ----
+ public Task ListBankStatementsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.BankStatementClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task BankStatementDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.BankStatementClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task CreateBankStatementAsync(BankStatementPostModel model, CancellationToken ct)
+ => (await _accessor.Api.BankStatementClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdateBankStatementAsync(BankStatementPatchModel model, CancellationToken ct)
+ => (await _accessor.Api.BankStatementClient.UpdateAsync(model, ct)).Unwrap();
+
+ public async Task DeleteBankStatementAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.BankStatementClient.DeleteAsync(id, ct)).Unwrap();
+
+ // ---- Issued document payments ----
+ public Task ListIssuedPaymentsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.IssuedDocumentPaymentClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task IssuedPaymentDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.IssuedDocumentPaymentClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task IssuedPaymentDefaultAsync(int invoiceId, CancellationToken ct)
+ => (await _accessor.Api.IssuedDocumentPaymentClient.DefaultAsync(invoiceId, ct)).Unwrap();
+
+ public async Task CreateIssuedPaymentAsync(IssuedDocumentPaymentPostModel model, CancellationToken ct)
+ => (await _accessor.Api.IssuedDocumentPaymentClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task DeleteIssuedPaymentAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.IssuedDocumentPaymentClient.DeleteAsync(id, ct)).Unwrap();
+
+ public async Task FullyUnpayIssuedAsync(int invoiceId, CancellationToken ct)
+ => (await _accessor.Api.IssuedDocumentPaymentClient.FullyUnpayAsync(invoiceId, ct)).Unwrap();
+
+ // ---- Received document payments ----
+ public Task ListReceivedPaymentsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.ReceivedDocumentPaymentsClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task ReceivedPaymentDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.ReceivedDocumentPaymentsClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task ReceivedPaymentDefaultAsync(int invoiceId, CancellationToken ct)
+ => (await _accessor.Api.ReceivedDocumentPaymentsClient.DefaultAsync(invoiceId, ct)).Unwrap();
+
+ public async Task CreateReceivedPaymentAsync(ReceivedDocumentPaymentPostModel model, CancellationToken ct)
+ => (await _accessor.Api.ReceivedDocumentPaymentsClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task DeleteReceivedPaymentAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.ReceivedDocumentPaymentsClient.DeleteAsync(id, ct)).Unwrap();
+
+ public async Task FullyUnpayReceivedAsync(int invoiceId, CancellationToken ct)
+ => (await _accessor.Api.ReceivedDocumentPaymentsClient.FullyUnpayAsync(invoiceId, ct)).Unwrap();
+}
diff --git a/Services/PurchaseCashService.cs b/Services/PurchaseCashService.cs
new file mode 100644
index 0000000..6c6e3e7
--- /dev/null
+++ b/Services/PurchaseCashService.cs
@@ -0,0 +1,69 @@
+using IdokladSdk.Models.CashRegister;
+using IdokladSdk.Models.CashVoucher;
+using IdokladSdk.Models.ReceivedReceipt.Patch;
+using IdokladSdk.Models.ReceivedReceipt.Post;
+using Idoklad.Client;
+
+namespace Idoklad.Services;
+
+///
+/// Purchase-side receipts and cash agendas: received receipts, received documents (read-only),
+/// cash vouchers and cash registers.
+///
+public sealed class PurchaseCashService
+{
+ private readonly IdokladApiAccessor _accessor;
+
+ public PurchaseCashService(IdokladApiAccessor accessor) => _accessor = accessor;
+
+ // ---- Received receipts ----
+ public Task ListReceivedReceiptsAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.ReceivedReceiptClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task ReceivedReceiptDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.ReceivedReceiptClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task ReceivedReceiptDefaultAsync(CancellationToken ct)
+ => (await _accessor.Api.ReceivedReceiptClient.DefaultAsync(ct)).Unwrap();
+
+ public async Task CreateReceivedReceiptAsync(ReceivedReceiptPostModel model, CancellationToken ct)
+ => (await _accessor.Api.ReceivedReceiptClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdateReceivedReceiptAsync(ReceivedReceiptPatchModel model, CancellationToken ct)
+ => (await _accessor.Api.ReceivedReceiptClient.UpdateAsync(model, ct)).Unwrap();
+
+ public async Task DeleteReceivedReceiptAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.ReceivedReceiptClient.DeleteAsync(id, ct)).Unwrap();
+
+ // ---- Cash vouchers ----
+ public Task ListCashVouchersAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.CashVoucherClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task CashVoucherDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.CashVoucherClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task CreateCashVoucherAsync(CashVoucherPostModel model, CancellationToken ct)
+ => (await _accessor.Api.CashVoucherClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdateCashVoucherAsync(CashVoucherPatchModel model, CancellationToken ct)
+ => (await _accessor.Api.CashVoucherClient.UpdateAsync(model, ct)).Unwrap();
+
+ public async Task DeleteCashVoucherAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.CashVoucherClient.DeleteAsync(id, ct)).Unwrap();
+
+ // ---- Cash registers ----
+ public Task ListCashRegistersAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.CashRegisterClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task CashRegisterDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.CashRegisterClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task CreateCashRegisterAsync(CashRegisterPostModel model, CancellationToken ct)
+ => (await _accessor.Api.CashRegisterClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdateCashRegisterAsync(CashRegisterPatchModel model, CancellationToken ct)
+ => (await _accessor.Api.CashRegisterClient.UpdateAsync(model, ct)).Unwrap();
+
+ public async Task DeleteCashRegisterAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.CashRegisterClient.DeleteAsync(id, ct)).Unwrap();
+}
diff --git a/Services/SalesDocumentsService.cs b/Services/SalesDocumentsService.cs
new file mode 100644
index 0000000..b4a67f7
--- /dev/null
+++ b/Services/SalesDocumentsService.cs
@@ -0,0 +1,168 @@
+using IdokladSdk.Models.CreditNote;
+using IdokladSdk.Models.IssuedDocumentTemplate.Patch;
+using IdokladSdk.Models.IssuedDocumentTemplate.Post;
+using IdokladSdk.Models.IssuedTaxDocument.Patch;
+using IdokladSdk.Models.IssuedTaxDocument.Post;
+using IdokladSdk.Models.ProformaInvoice;
+using IdokladSdk.Models.RecurringInvoice;
+using IdokladSdk.Models.SalesOrder;
+using IdokladSdk.Models.SalesReceipt;
+using Idoklad.Client;
+
+namespace Idoklad.Services;
+
+///
+/// Sales-side documents: proforma invoices, credit notes, sales receipts, sales orders,
+/// recurring invoices, issued tax documents and issued document templates.
+///
+public sealed class SalesDocumentsService
+{
+ private readonly IdokladApiAccessor _accessor;
+
+ public SalesDocumentsService(IdokladApiAccessor accessor) => _accessor = accessor;
+
+ // ---- Proforma invoices ----
+ public Task ListProformaAsync(int page, int pageSize, CancellationToken ct)
+ => _accessor.Api.ProformaInvoiceClient.List().ToPageAsync(page, pageSize, ct);
+
+ public Task ProformaDetailAsync(int id, CancellationToken ct)
+ => _accessor.Api.ProformaInvoiceClient.Detail(id).ToDetailAsync(ct);
+
+ public async Task ProformaDefaultAsync(CancellationToken ct)
+ => (await _accessor.Api.ProformaInvoiceClient.DefaultAsync(ct)).Unwrap();
+
+ public async Task ProformaCopyAsync(int id, CancellationToken ct)
+ => (await _accessor.Api.ProformaInvoiceClient.CopyAsync(id, ct)).Unwrap();
+
+ public async Task CreateProformaAsync(ProformaInvoicePostModel model, CancellationToken ct)
+ => (await _accessor.Api.ProformaInvoiceClient.PostAsync(model, ct)).Unwrap();
+
+ public async Task UpdateProformaAsync(ProformaInvoicePatchModel model, CancellationToken ct)
+ => (await _accessor.Api.ProformaInvoiceClient.UpdateAsync(model, ct)).Unwrap();
+
+ public async Task