Skip to main content

Slovenia — FURS Davčno Potrjevanje Računov

Research doc for the Slovenia fiscalization adapter. Consumed by the implementation bead. No code in this bead.

Status: Research — no adapter code written yet. Strategic context: Phase-1 Central/Adriatic cluster country (per docs/strategy/thesis-api-wedge.md, fi-awx). Chosen because: free FURS-issued certificate, REST/JSON over HTTPS with mTLS + JWS signing (no SOAP, no HSM), real-time submission, mandatory since 2016 so the business universe (~150K entities) is mature and tooled. Earns its slot via cluster effect with Croatia (HR) + Hungary (HU): a single Adriatic sales motion can land all three. See apps/docs-internal/docs/ops/country-prioritization.md.


1. Regulatory scope

What must be reported

Slovenia's davčno potrjevanje računov ("fiscal verification of invoices") regime, operated by FURS (Finančna uprava Republike Slovenije — the Financial Administration of the Republic of Slovenia), requires real-time verification of invoices that are paid in cash (or any cash-equivalent instrument) by the buyer.

The legal basis is ZDavPRZakon o davčnem potrjevanju računov (Act on Fiscal Verification of Invoices), Official Gazette RS 57/2015, in force from 2016-01-02. Implementing rules: Pravilnik o izvajanju Zakona o davčnem potrjevanju računov (Rules on the implementation of the ZDavPR), Official Gazette RS 60/2015 with subsequent amendments.

Two parallel regimes coexist; Slovenia uniquely allows either:

RegimeMechanismScope of our adapter
Online certification (davčno potrjevanje računov)Each invoice POSTed to FURS in real time → FURS returns a unique invoice ID (EOR) which is printed on the receiptPrimary — adapter targets this regime.
Pre-numbered invoice books (vezana knjiga računov)Manual paper books pre-issued by FURS; for taxpayers with ≤ 50 receipts/month who opt inOut of scope — handled out-of-band by the merchant.

Who must report

A taxpayer must use online certification when all three conditions are met (ZDavPR Art. 3):

  1. The taxpayer issues invoices for goods or services delivered in Slovenia.
  2. The invoice is paid in cash ("gotovina") or by an equivalent non-cash payment considered cash for fiscalization purposes — see below.
  3. The taxpayer is required to keep accounting records under ZDDV-1 (the VAT Act) or ZDoh-2 (the Personal Income Tax Act).

The wording is "plačilo z gotovino" — "cash payment" — which per ZDavPR Art. 2 pt. 7 explicitly includes:

  • Banknotes and coins.
  • Payment cards (debit/credit).
  • Cheques.
  • Other similar means of payment — i.e. anything that is not a direct bank transfer between the buyer's and seller's accounts where the seller's account is named on the invoice.

In practice this captures most B2C transactions and a meaningful share of B2B. The carve-out is direct wire / SEPA bank transfer between named accounts.

Exemptions (ZDavPR Art. 5)

  • Sales financed exclusively from public sources by special public agencies.
  • Sales of own agricultural products by registered farmers under the flat-rate scheme (pavšalni nadomestilec) up to specified thresholds.
  • Sales by personal-supplementary-work providers under ZPDZC-1.
  • Vending machines and toll-road kiosks (specific items in the rule book).
  • Diplomatic / consular sales.

Thresholds

  • No transaction-amount threshold. Every cash-paid invoice in scope is reportable, including 0.01 EUR receipts.
  • No taxpayer-size threshold for the online regime — even a single sole proprietor must use it if they issue cash invoices and choose not to opt into the (capped) pre-numbered book regime.
  • The pre-numbered book regime is capped at the rule-book limit (currently ~50 invoices/month, indexed); above that limit only online certification is permitted.

B2B?

Yes — when the invoice is paid in cash (per the broad definition above), B2B is in scope. The customer's tax number (davčna številka for Slovenian customers, VAT ID for cross-border) is a required field on the invoice when the customer is a taxpayer.


2. API surface

Base URLs

EnvironmentBase URLPort
Sandbox (test)https://blagajne-test.fu.gov.si:90029002
Productionhttps://blagajne.fu.gov.si:90039003

Note the non-standard ports. Cited in: Tehnična dokumentacija — Postopki za izmenjavo sporočil prek REST, FURS, current version v2.4 (2024-11). Sandbox and production are separate certificate universes — a production cert cannot be used in sandbox and vice versa.

API version

Current: REST API v1 (path prefix /v1/cash_registers/). The wire format spec itself is on technical documentation v2.x (current is v2.4, dated 2024-11), which describes the message contracts that ride over the v1 path. The version pair has been stable since 2019; v2.x compatible payloads have been accepted continuously. There is no public v2 of the path prefix.

Endpoints

All endpoints are POST with Content-Type: application/json; UTF-8. Body is a JWS Compact Serialization string (header.payload.signature, all base64url) carrying the JSON request as the payload. All endpoints are rooted under /v1/cash_registers/.

PathPurposeAuth
POST /v1/cash_registers/echoHeartbeat / round-trip test (does not record)mTLS only — no JWS required
POST /v1/cash_registers/invoicesSubmit an invoice for fiscal verification — returns the EOR (unique invoice identifier)mTLS + JWS
POST /v1/cash_registers/invoices/registerRegister or update a poslovni prostor (business premise) — must be done once per premise before the first invoice from that premisemTLS + JWS

Slovenia's surface is dramatically smaller than Hungary or Italy: no token exchange, no transaction polling, no separate query API. Every write is synchronous; either FURS returns the EOR (success) or an error envelope (failure). The simplicity is a deliberate design choice in ZDavPR — the burden of correctness sits on the issuer, not on a queryable state machine.

There is also an XML/SOAP profile of the same operations, served on the same hosts at port 9007 (test) / 9008 (prod). Both formats accept the same logical message and are functionally equivalent.

JSON-vs-XML choice for our adapter

We pick JSON over REST. Rationale:

  • The JSON profile is the default in the FURS technical documentation v2.x; XML is documented as an alternative for legacy integrations.
  • JWS Compact Serialization (RFC 7515) is a smaller, better-supported surface than the SOAP/XAdES envelope used on port 9007/9008.
  • Aligns with the rest of our adapters (Hungary v3.0 over JSON-equivalent HTTP+XML, Croatia and Romania over JSON).
  • All sandbox tooling and FURS sample code in their public test repo use the JSON profile.

The XSDs in testdata/conformance/slovenia/furs/schemas/ ship for reference and validation parity; the wire format we exercise in goldens is JSON.


3. Authentication

One-time, out-of-band: certificate issuance

FURS issues a free X.509 client certificate per cash register to each taxpayer through the eDavki portal (the Slovenian tax authority's online service portal at edavki.durs.si). Steps:

  1. The taxpayer's authorised representative logs into eDavki with their personal SIGEN-CA / SI-PASS / Halcom / etc. citizen certificate.
  2. Submits the DPR-PridobitevPotrdila form, naming each cash register they plan to use (one cert per taxNumber + permanentRegisterId).
  3. eDavki returns a download link for a .p12 file (PKCS#12, password-protected by a token displayed on screen) and a 4-character one-time password to unlock it.
  4. The taxpayer extracts the cert + private key and installs them in the cash register / our adapter.

Cert subject convention:

  • CN = <taxNumber>-<cashRegisterId> (e.g. CN=10442564-CR1).
  • O = Republika Slovenija, OU = Davčna uprava Republike Slovenije.
  • Issuer: CN = sitest-ca, etc. (sandbox) or CN = sigov-ca (prod).

Sandbox cert

Available immediately in eDavki test environment: https://test.edavki.durs.si. No real KYC. The test certs are signed by the sitest-ca intermediate; we must trust this CA in the adapter's TLS-client trust store when talking to blagajne-test.

Per-request: mTLS

Every request to :9002 / :9003 is over mutual TLS:

  • The client (us) presents the FURS-issued client cert above.
  • The server (FURS) presents a cert signed by sitest-ca (test) or sigov-ca (prod). We must validate the chain.
  • TLS version: TLS 1.2 minimum, ECDHE / RSA 2048+ ciphers.
  • SNI: send the literal hostname, not the IP.

mTLS alone identifies the cash register. JWS adds non-repudiation of the message body: even if the TLS were stripped or replayed, the JWS signature anchors the payload to the same cert.

Per-request: JWS Compact Serialization

The HTTP body is a single JWS Compact string:

BASE64URL(JOSE-header) "." BASE64URL(payload-JSON) "." BASE64URL(signature)

JOSE header (the JSON object that gets base64url-encoded as part 1):

{
"alg": "RS256",
"x5c": ["<base64 DER of the issuer cert (no PEM wrapper, no newlines)>"],
"subject_name": "CN=10442564-CR1, ..."
}
  • alg MUST be RS256 (RSA-SHA256). FURS does not accept ES256 or PS256 in this surface.
  • x5c MUST contain only the leaf cert (the FURS-issued client cert), as a single-element array, base64-DER-encoded with no PEM framing and no embedded whitespace. FURS resolves the CA chain server-side from its own trust store.
  • subject_name is OPTIONAL but recommended; FURS uses it for diagnostics. Format must match the cert's actual Subject DN exactly.

Payload: the request JSON object exactly as documented (e.g. an InvoiceRequest), serialized with stable key order to keep the signature deterministic across retries.

Signature: RSA-SHA256(privateKey, ASCII(BASE64URL(header) + "." + BASE64URL(payload))), then base64url-encoded.

Content-Type header on the HTTP request: application/json; UTF-8 (FURS quirk — application/jose is not accepted; the body is "JSON-compatible" because it's a single quoted JWS string when deserialized).

Response signatures

Every FURS response that carries a successful payload (InvoiceResponse, BusinessPremiseResponse) is also a JWS Compact signed by FURS's sigov-ca-issued (or sitest-ca in sandbox) server signing cert. Our adapter must verify the response signature before treating the EOR as authoritative — this is the chain of trust that makes the EOR audit-grade.

echo responses are unsigned (plain JSON body).

Header object

Every JWS payload carries a Header sub-object:

"Header": {
"MessageID": "<UUID v4 — adapter-generated, unique per request>",
"DateTime": "2026-04-26T13:14:15Z"
}

MessageID provides request-level idempotency: re-POSTing the same JWS (same payload bytes, same MessageID) within FURS's dedup window returns the original EOR rather than minting a new one. The dedup window is documented as 48 hours. We MUST mint MessageID deterministically from the prepared record so that network-level retries hit the dedup path rather than double-submit.


4. Payload shape

Top-level: InvoiceRequest

{
"InvoiceRequest": {
"Header": {
"MessageID": "61a874c6-625a-4f60-a4b3-e0e1d3a85cc5",
"DateTime": "2026-04-26T13:14:15Z"
},
"Invoice": {
"TaxNumber": 10442564,
"IssueDateTime": "2026-04-26T13:14:15",
"NumberingStructure": "B",
"InvoiceIdentifier": {
"BusinessPremiseID": "BP101",
"ElectronicDeviceID": "BL01",
"InvoiceNumber": 12
},
"CustomerVATNumber": "SI12345678",
"InvoiceAmount": 39.06,
"PaymentAmount": 39.06,
"TaxesPerSeller": [
{
"VAT": [
{
"TaxRate": 22.00,
"TaxableAmount": 32.02,
"TaxAmount": 7.04
}
]
}
],
"OperatorTaxNumber": 12345678,
"ProtectedID": "a7e5f55e1dbb48b799268e1a6d8618a3",
"SubsequentSubmit": false
}
}
}

Field reference (selected)

FieldTypeNotes
TaxNumberint (8 digits)Slovenian taxpayer number, no SI prefix. Must match the taxpayer who owns the issuing certificate.
IssueDateTimeyyyy-MM-ddTHH:mm:ssLocal time, no timezone — this matches the Header.DateTime in wall-clock terms. Used in ZOI input.
NumberingStructure"C" or "B""C" = numbering centralized per business premise; "B" = numbering per electronic device (more common).
InvoiceIdentifier.BusinessPremiseIDstring ≤ 20Free-form, taxpayer-assigned. Must be pre-registered via BusinessPremiseRequest.
InvoiceIdentifier.ElectronicDeviceIDstring ≤ 20Free-form, taxpayer-assigned. Identifies the cash register. Not pre-registered, but must be stable per device.
InvoiceIdentifier.InvoiceNumberint ≥ 1Strictly increasing per (BusinessPremiseID, ElectronicDeviceID) for NumberingStructure="B", or per BusinessPremiseID for "C". Resets every calendar year.
CustomerVATNumberstring, optionalRequired only if invoice ≥ EUR 100 net and customer is a taxable person. Format: SI prefix for SI taxpayers; ISO country prefix for cross-border EU.
InvoiceAmountdecimal, 2 placesTotal invoice amount including VAT.
PaymentAmountdecimal, 2 placesAmount paid in cash-equivalent. Usually equals InvoiceAmount; differs only on partial-cash mixed-payment invoices (e.g. card + credit voucher).
TaxesPerSellerarrayEither one entry per seller (when this register sells on behalf of multiple legal entities — rare) or a single entry for the primary taxpayer. Each entry has VAT (per-rate breakdown), OtherTaxes, ExemptVATTaxableAmount, ReverseVATTaxableAmount, NontaxableAmount.
OperatorTaxNumberintThe cashier's personal tax number (a natural person). Required by ZDavPR Art. 11.
ProtectedIDstring (32 lowercase hex)The ZOI — see §4.1 below. Computed by the adapter, not by FURS.
SubsequentSubmitboolfalse for real-time submission; true if this was an offline-issued invoice now being uploaded after the fact (within 2 days).

4.1 ZOI (ProtectedID) — protective issuer code

ZOI = zaščitna oznaka izdajatelja računa. Fixed-output 32-char lowercase hex. Computed by the adapter, never by FURS. Drives non-repudiation: it links a printed receipt back to the cert holder even if FURS itself is offline at issue time.

Algorithm (ZDavPR Pravilnik §11, FURS tech doc v2.4 §3.2):

input = TaxNumber +
IssueDateTime (yyyy-MM-dd HH:mm:ss, NO 'T') +
InvoiceNumber +
BusinessPremiseID +
ElectronicDeviceID +
InvoiceAmount (with 2 fractional digits, '.' separator)
sig = RSA-SHA256-sign(privateKey, input.getBytes("UTF-8"))
ZOI = lowercase(hex(MD5(sig)))

Critical formatting rules:

  • IssueDateTime here uses a space separator (yyyy-MM-dd HH:mm:ss), different from the field's wire format which uses T. The two representations must denote the same wall-clock instant.
  • InvoiceAmount is rendered with exactly two decimals, period as separator, no thousands grouping (39.06, never 39,06 or 39.060).
  • All concatenation is plain string concatenation, no separators between fields. (Yes, this means the boundaries are ambiguous on inspection — but inputs are fixed-format enough that ambiguity does not produce collisions in practice.)
  • The MD5 in the last step only consumes the raw signature bytes — not a base64 of them, not a hex of them.

The 32-char ZOI is also rendered on the receipt formatted with dashes (a7e5f55e-1dbb-48b7-9926-8e1a6d8618a3) — that is a display-only transformation; the field on the wire is plain hex.

4.2 EOR (UniqueInvoiceID)

EOR = enkratna identifikacijska oznaka računa. Returned by FURS in InvoiceResponse:

{
"InvoiceResponse": {
"Header": { "MessageID": "...", "DateTime": "..." },
"UniqueInvoiceID": "f01ec7d3-d2b2-4c23-aff8-4d3aa3b3b4f7"
}
}

Format: UUID-like (32 hex chars + 4 dashes). Must be printed on the buyer's receipt alongside the ZOI per ZDavPR Art. 12. Must be persisted by the adapter — it is the audit anchor for any later FURS inspection.

4.3 BusinessPremiseRequest

{
"BusinessPremiseRequest": {
"Header": { "MessageID": "...", "DateTime": "..." },
"BusinessPremise": {
"TaxNumber": 10442564,
"BusinessPremiseID": "BP101",
"BPIdentifier": {
"RealEstateBP": {
"PropertyID": {
"CadastralNumber": 365,
"BuildingNumber": 12,
"BuildingSectionNumber": 3
},
"Address": {
"Street": "Slovenska cesta",
"HouseNumber": "1",
"Community": "Ljubljana",
"City": "Ljubljana",
"PostalCode": "1000"
}
}
},
"ValidityDate": "2026-04-26",
"SoftwareSupplier": [
{ "TaxNumber": 12345678 }
],
"SpecialNotes": ""
}
}
}

Two BPIdentifier flavours: RealEstateBP (a fixed location, identified by the cadastral triple from the Slovenian land registry) or PremiseType (a string code in {"A","B","C"} for movable / non-fixed premises: food trucks, market stalls, web shops). For an e-shop the PremiseType="B" (web shop) is used and no cadastral data is required.

SoftwareSupplier is us — Zyntem. We register our own taxpayer ID once per customer onboarding so FURS can reach us if there is a software-side incident. The supplier ID is NOT a per-tenant secret.


5. Ordering and sequencing

Per-business-premise + per-electronic-device

For NumberingStructure="B" (the default), invoice numbers are strictly monotonic within (taxNumber, BusinessPremiseID, ElectronicDeviceID, calendarYear). Resets to 1 on January 1st of each year.

For NumberingStructure="C" (centralised at the premise), the third key drops out and numbering is per (taxNumber, BusinessPremiseID, calendarYear).

Adapter rule: the Transaction.sequence_number from upstream must be globally monotonic per device (or premise, for "C") and we mint the InvoiceNumber from it. Gaps are technically permitted but auditable — FURS will not reject a gap, but a tax inspection will.

Reissue (corrective invoices)

Slovenia treats a corrective invoice as a brand-new invoice with a new InvoiceNumber and new ZOI, that references the original through ReferenceInvoice in the payload:

"ReferenceInvoice": [
{
"ReferenceInvoiceIdentifier": {
"BusinessPremiseID": "BP101",
"ElectronicDeviceID": "BL01",
"InvoiceNumber": 12
},
"ReferenceInvoiceIssueDateTime": "2026-04-26T13:14:15"
}
]

The reference is informational only in the wire protocol — FURS does not "void" the original. The new invoice's amounts may be:

  • Negative (full credit / storno), reversing the original.
  • Positive (additional invoice), settling an underbill.
  • Mixed, common in returns (-€39.06) plus replacement (+€41.50) issued as two separate invoices on the same receipt.

There is no STORNO enum; the sign of InvoiceAmount carries the intent. Negative amounts are explicitly allowed (decimal, with leading minus).

Cancellation

There is no cancellation API. Once an EOR is issued, it stands. Errors are corrected by issuing a new corrective invoice as above.


6. Submission window

ModeWindow
Online (real-time)Immediately on issuance — adapter MUST POST the JWS within the same request-response cycle in which the cashier prints the receipt. The receipt prints with the EOR already populated.
Offline (degraded)If FURS is unreachable at issue time, the cashier prints the receipt with the ZOI but no EOR, marks the receipt "Račun še ni potrjen" ("invoice not yet confirmed"), and the adapter MUST submit the invoice within 2 calendar days of issuance, with SubsequentSubmit=true.

SubmissionWindow value: Immediate (with a 48-hour fallback queue for offline mode). FURS publishes incident banners; the 2-day clock is not extended for FURS-side outages — the burden remains on the issuer to retry until success.

If the 2-day window is missed, the adapter has no recourse via the API; the merchant must self-report the late submission as a tax incident through eDavki (manual, out of band).


7. Sandbox access

How to request a test certificate

  1. Register at https://test.edavki.durs.si (Slovenian-language portal, English partial). Test users self-register; no real KYC.
  2. From the dashboard, submit the DPR-PridobitevPotrdila form listing the planned cash registers (one entry per register; field OznakaElektronskeNaprave = our ElectronicDeviceID).
  3. eDavki test instantly produces a downloadable .p12 per register plus a 4-char PIN displayed on screen. Capture both immediately — the PIN is shown only once.
  4. The cert chain root: sitest-ca. Download from https://www.fu.gov.si/fileadmin/Internet/Davcni_postopki/Posebna_podrocja/Davcne_blagajne_in_VKR/Tehnicna_dokumentacija/sitest-ca.cer and pin in the adapter's TLS trust store for sandbox.
  5. Optionally register a sandbox business premise via the same form set so that the first BusinessPremiseRequest from our adapter does not have to handle a 403.

Timeline: fully self-service. End-to-end (cert in hand, business premise registered, first echo green) in ~30 minutes — the fastest sandbox in our Phase-1 set.

What we'll need

  • A .p12 checked into our test-secrets vault for the CI sandbox tenant (SI_FURS_PFX, SI_FURS_PFX_PASSWORD).
  • The sandbox sitest-ca root pinned in our test trust store.
  • A sandbox business premise registered (one-time at suite startup).
  • An .env template in the Slovenia adapter repo with: SI_FURS_TAX_NUMBER, SI_FURS_PFX_PATH, SI_FURS_PFX_PASSWORD, SI_FURS_BUSINESS_PREMISE_ID, SI_FURS_ELECTRONIC_DEVICE_ID, SI_FURS_BASE_URL, SI_FURS_OPERATOR_TAX_NUMBER, SI_FURS_SOFTWARE_SUPPLIER_TAX_NUMBER.

8. Error model

FURS does not use HTTP status codes to disambiguate business errors: nearly every business-validation failure is returned as HTTP 200 OK with an error envelope in the JWS payload. Transport-level errors (TLS fail, 4xx/5xx, malformed JWS) come back as HTTP errors.

Error envelope (in the JWS payload)

{
"InvoiceResponse": {
"Header": { "MessageID": "...", "DateTime": "..." },
"Error": {
"ErrorCode": "S001",
"ErrorMessage": "Račun s podanim ZOI in časom izdaje že obstaja v sistemu davčnih blagajn."
}
}
}

Same envelope shape on BusinessPremiseResponse.

Error-code taxonomy (selected; full list in tech doc §6)

CodeClassMeaningAdapter action
S001duplicateAn invoice with this (ZOI, IssueDateTime) already existsTreat as success — fetch original EOR via dedup logic (we cached it)
S002business-validationTaxNumber mismatch with certBug — config error; halt + alert
S003business-validationBusinessPremiseID not registeredRe-run BusinessPremiseRequest, then retry
S004business-validationInvoiceAmount inconsistent with TaxesPerSeller breakdownBug — fix arithmetic; reissue with new InvoiceNumber
S005schemaJSON schema violationBug — fix payload; retry with same MessageID
S100signatureJWS signature invalidBug — re-sign; retry
S101signaturex5c not matched in FURS trust storeBug — wrong cert; halt + alert
S200rate / availabilityFURS overloaded, retry laterBackoff + retry
S300systemFURS internal errorRetry with same MessageID

Distinguish three retry classes:

  • Transport (TLS error, HTTP 5xx, timeout): retry with same JWS bytes (idempotent via MessageID + 48h dedup window).
  • System (S200, S300): retry with backoff (250ms, 1s, 4s, 16s, cap at 60s).
  • Validation (everything else S0**): halt and surface; do not retry.
  • Duplicate (S001): treat as success — the original EOR is in our store, return it. (This is how the dedup window helps us at all.)

Echo

POST /v1/cash_registers/echo with {"EchoRequest": "ping"} returns {"EchoResponse": "ping"} on success. Used for connectivity smoke tests before each batch (and as a CI health probe). Round-trip <200ms typical.


9. Implementation plan

Crate layout

Candidate name: slovenia-furs (matches hungary-nav / croatia-cis naming pattern in the rust/adapters/ workspace).

rust/adapters/slovenia/
├── Cargo.toml
└── src/
├── lib.rs # SloveniaAdapter (impl CountryAdapter)
├── config.rs # SloveniaConfig (cert, premise, device, …)
├── auth.rs # JWS sign + verify, x5c handling
├── zoi.rs # ProtectedID computation (RSA-SHA256 + MD5)
├── client.rs # reqwest mTLS client, retry, echo probe
├── invoice.rs # Transaction -> InvoiceRequest JSON
├── premise.rs # BusinessPremiseRequest builder + register-once cache
├── types.rs # serde models (Header, Invoice, TaxesPerSeller, …)
├── errors.rs # AdapterError variants per error class
└── tests.rs # JSON-Schema conformance against golden fixtures

Trait fit

CountryAdapter::prepare() is a clean two-phase split here:

  • prepare() — build the InvoiceRequest JSON, compute the ZOI, produce a deterministic fiscal_id = {taxNumber}:{businessPremiseId}:{electronicDeviceId}:{invoiceNumber}, cache the JSON bytes for submit(). Network-free (ZOI requires the private key but not FURS).
  • submit() — JWS-sign → POST to /invoices → verify response JWS → extract EOR. Synchronous; SubmitResult.upstream_id = EOR.

DeploymentMode::Cloud is the primary target — the cert lives in our HSM/secret store and signs server-side. Embedded works identically (the cert can live on the cash register), but Cloud is the thesis.

SubmissionWindow::Immediate (with the 48h offline fallback as a queue behaviour, not a window relaxation).

Config shape

pub struct SloveniaConfig {
/// SI taxpayer number, 8 digits, no SI prefix (e.g. 10442564).
pub tax_number: u64,
/// PKCS#12 cert + private key bundle (FURS-issued, one per register).
pub pfx_bytes: Vec<u8>,
pub pfx_password: SecretString,
/// Pre-registered business premise (must match BusinessPremiseRequest).
pub business_premise_id: String,
/// Stable identifier for this cash register / device.
pub electronic_device_id: String,
/// "B" (per-device numbering, default) or "C" (per-premise numbering).
pub numbering_structure: NumberingStructure,
/// Cashier tax number, required on every invoice.
pub default_operator_tax_number: u64,
/// Our (Zyntem) software-supplier tax number.
pub software_supplier_tax_number: u64,
/// true => :9002 sandbox, false => :9003 production.
pub sandbox: bool,
}

Dependencies (expected)

  • reqwest (mTLS via native-tls or rustls + rustls-pemfile) — already in workspace.
  • serde / serde_json — already in workspace.
  • rsa + sha2 — RSA-SHA256 for JWS sign and for ZOI input signing.
  • md-5 — MD5 of the ZOI signature.
  • base64 (URL-safe variant) — JWS Compact encoding.
  • x509-parser — to extract Subject DN from the PKCS#12 cert for the JWS subject_name field.
  • p12 or pkcs12 — read PKCS#12 to extract cert chain + private key.
  • chrono — already in workspace.
  • uuid (v4 deterministic via SHA-256) — MessageID minting.

No SOAP, no XAdES, no native fiscal libs. The cryptography is RSA-SHA256 + MD5 + AES-stream — entirely software, no HSM needed (though a Cloud-HSM-backed key is an upgrade path post-MVP).

Open questions (for implementation bead)

  1. ZOI format on receipt printing. Adapter returns the 32-char hex. The receipt-printing layer (downstream) must format it with dashes per ZDavPR. Should the adapter expose both? Recommend: expose hex only; downstream owns presentation.
  2. OperatorTaxNumber source. Required per ZDavPR Art. 11. Our Transaction schema doesn't carry a cashier ID today. Options: (a) add Transaction.operator_id and require upstream to map to tax number, (b) take a static default from config and let upstream override per-call. Recommend: (b) with a strict default_operator_tax_number and an optional override field.
  3. Numbering reset on January 1st. FURS expects InvoiceNumber=1 on the first invoice of each calendar year per (BusinessPremise, ElectronicDevice). Our Transaction.sequence_number is monotonic forever. The adapter needs a per-(premise, device, year) counter rebase. Recommend: a NumberingStrategy trait with a year-aware default impl.
  4. Multiple sellers per device. A cash register can issue invoices on behalf of multiple legal entities (consignment: e.g. a market stall renting space to multiple producers). TaxesPerSeller is an array for this reason. Our Transaction ties to a single Location → Account → TaxNumber. Recommend: out of scope for v1; document as a known limitation; only the primary taxpayer.
  5. SubsequentSubmit + offline queue. The 2-day offline window is real and needs a persistent queue. Where does it live — in the adapter or in the higher-level submission orchestrator? Recommend: orchestrator (the queue is country-agnostic infrastructure), the adapter just exposes a SubmissionMode::Subsequent variant that sets the wire-level flag.
  6. eDavki cert provisioning. Self-service for the taxpayer but not for us (we don't get the .p12; it's emailed to / downloaded by the merchant). Onboarding flow: merchant uploads .p12 + PIN via secure form. Recommend: build a one-page upload UI in the merchant onboarding portal; encrypt the PFX with KMS at rest.
  7. PremiseType="B" for cloud-only e-shops. A pure-online merchant with no physical premise registers PremiseType="B" once and uses that BusinessPremiseID for every invoice. Worth confirming whether the cadastral triple is truly optional in this case (FURS schemas mark it nillable=true only when PremiseType is set). The fixture pack proves it; double-check on first sandbox round-trip.

What could force us off Cloud-OK?

Nothing identified in the spec. No HSM requirement, no client-side hardware seal, no XAdES. RSA-2048 keys on a .p12, RS256 JWS signing done in software. The only constraint is that the private key must remain accessible at signing time — for Cloud that means it lives in our KMS / secret store, encrypted at rest.

This is a green light — Slovenia confirms the Phase-1 cluster thesis.

Testing strategy

  • Unit (in-adapter):
    • ZOI vector tests against the worked example in FURS tech doc §3.2.
    • JWS round-trip: sign → verify with same cert → bytes-equal payload.
    • JSON-Schema validation on all 6 golden triplets using the schemas in testdata/conformance/slovenia/furs/schemas/.
  • Integration (against sandbox): full BusinessPremiseRequest → echo → InvoiceRequest → response-JWS-verify round-trip. Gated behind SI_FURS_SANDBOX_CREDENTIALS env var so CI without creds still passes unit tests.
  • Conformance: every triplet must round-trip input.json → adapter → wire-bytes, and the wire-bytes must be a byte-exact match to golden.json after canonicalising key order.

Rollout sequence (for the implementation bead)

  1. Day 1: zoi.rs + worked-example vector test passing.
  2. Day 2: auth.rs JWS sign/verify; PKCS#12 cert load; subject_name extraction.
  3. Day 3: invoice.rs builder + JSON-Schema conformance against all 6 goldens.
  4. Day 4: client.rs mTLS client + echo probe + first sandbox round-trip.
  5. Day 5: premise.rs + register-once cache; sandbox premise creation.
  6. Day 6: Error model + retry classes; S001 dedup short-circuit.
  7. Day 7: SubsequentSubmit path + offline-queue interface.
  8. Day 8: Soak test against sandbox: 100 invoices over an hour, observe latencies.

10. Citations

All facts above are sourced from one of:

Per-fixture citations live in each meta.json under testdata/conformance/slovenia/furs/golden/.