wmd / craft-commerce-eracuni
Turn Craft Commerce orders into e-računi.hr invoices: fiscalisation (F1), KPD codes, AS4 and FINA e-invoice delivery.
Package info
github.com/WMD-hosting/craft-commerce-eracuni
Type:craft-plugin
pkg:composer/wmd/craft-commerce-eracuni
Requires
- php: ^8.2
- ext-json: *
- craftcms/cms: ^5.3.0
- craftcms/commerce: ^5.0.0
- guzzlehttp/guzzle: ^7.0
- verbb/base: ^3.0
Requires (Dev)
- craftcms/ecs: dev-main
- craftcms/phpstan: dev-main
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Craft Commerce 5 plugin that sends completed orders to e-računi.hr as invoices: fiscalisation (F1), VAT treatment, KPD codes, and AS4/FINA e-invoice delivery.
What it does
When a Commerce order reaches a configured status (and is optionally fully paid), the plugin builds an e-računi invoice from the order — buyer, line items, VAT treatment, payment method — and sends it through the e-računi REST API. The resulting document (invoice number, PDF, VAT treatment, delivery status) is stored locally and surfaced:
- on the order edit page, as a read-only panel with a manual "Send to e-računi" / "Preview payload" action;
- under e-Računi → Documents in the control panel, a filterable list of every document with a retry action for failed sends;
- to Twig, via
craft.commerceEracuni; - to the console, via
craft commerce-eracuni/sync/*commands for status checks, manual sends, backfilling, retries, and delivery-status polling.
The plugin decides, per order, whether the invoice is B2C, domestic B2B, EU B2B (reverse charge), EU B2C, or third-country B2B/B2C, and whether it needs fiscalisation, based on the payment method and the buyer's billing country/organisation/tax ID.
Requirements
- Craft CMS
^5.3 - Craft Commerce
^5.0 - PHP
^8.2 - An e-računi.hr account with API access enabled
Installation
composer require wmd/craft-commerce-eracuni craft plugin/install commerce-eracuni
Configuration
Connection (.env)
The Connection settings tab is built with suggestEnvVars, so every field can hold an environment
variable reference ($VAR_NAME) instead of a literal value. Add these to your .env:
ERACUNI_API_URL=https://e-racuni.com/H7i/API-CLI
ERACUNI_USERNAME=info@example.hr
ERACUNI_TOKEN=your-token-secretkey
ERACUNI_BUSINESS_UNIT=your-poslovna-jedinica-code
Then, in Settings → e-Računi → Connection, set:
| Field | Value |
|---|---|
| API URL | $ERACUNI_API_URL |
| Username | $ERACUNI_USERNAME |
| Auth token | $ERACUNI_TOKEN |
| Business unit | $ERACUNI_BUSINESS_UNIT |
Also set the numeric cash register code (naplatni uređaj) used for fiscalised invoices, an
optional partner-code prefix for B2C partners (e.g. WEB → WEB-123), and the seller country
(only HR is validated in this version). Use Test connection on the same tab to verify the
credentials against the e-računi API before enabling auto-send.
Settings tabs
- Connection — API URL, username, auth token, business unit, cash register code, partner code prefix, seller country.
- Documents — auto-send on/off, which order statuses trigger a send, whether the order must be fully paid first, invoice date source (date of sending vs. order date), payment due days, and whether to record a payment on the e-računi invoice for orders already paid.
- VAT — the known VAT rates, the tax rate map and the default VAT rate (below), plus a read-only explanation of how VAT treatment is decided.
- Payments — the payment-method mapping table (below), per Commerce gateway.
- KPD — the KPD field handle and defaults (below).
- Delivery — AS4/FINA delivery toggles, the B2G buyer OIB list, and the cron reminder for delivery-status polling.
VAT rates
Line VAT rates are derived from Commerce's own tax adjustments: the plugin divides the line's tax
by its net amount and snaps the result to the nearest of the Known VAT rates (comma-separated,
default 25,13,5,0). The shipping line takes its rate from the order-level tax adjustment, not
from the first line, so a mixed-rate basket does not deliver at the reduced rate.
Two settings cover the case where Commerce reports no tax at all for a line — no tax engine configured, or a tax rule that does not match:
- Tax rate map — Commerce tax category handle to VAT percentage, e.g.
booksto5. Consulted only when the line carries no tax adjustment. - Default VAT rate — used when the line's tax category is not in the map (default
25).
In both cases the line's Commerce price is read as VAT-inclusive (gross) and the net is derived from it. A domestic invoice line that still ends up at 0% is flagged as a warning in the preview and on the document record, because that is nearly always a missing tax rule rather than a genuine exemption.
Payment method mapping
Each Commerce gateway is mapped to an e-računi payment method, whether it fiscalises the invoice,
and the payment method recorded against the invoice. New gateways get a suggested mapping (based
on their handle/class name) that you confirm and save on the Payments settings tab; the
mapping itself lives in src/core/PaymentMethodMap.php.
| e-računi method | Fiscalised | Payment record method |
|---|---|---|
| Cash | Yes | Cash |
| Visa | Yes | Card |
| EurocardMastercard | Yes | Card |
| Diners | Yes | Card |
| Amex | Yes | Card |
| Stripe | Yes | Stripe |
| PayPal | Yes | PayPal |
| CorvusPay | Yes | CorvusPay |
| KeksPay | Yes | KeksPay |
| BankTransfer | No | BankPaymentOrder |
| Compensation | No | Compensation |
| Other | No | BankPaymentOrder |
Fiscalised methods (cash, cards, online wallets) produce an F1 fiscal invoice; bank transfer does
not. Zero-total orders (e.g. fully discounted) use "Other". A gateway with no explicit mapping
falls back to BankTransfer.
KPD field setup
e-računi line items carry a KPD (Klasifikacija proizvoda po djelatnostima) code. The plugin reads it from:
- a Plain Text field on the variant or product — the handle is configurable on the KPD
settings tab, defaulting to
kpd; - falling back to a per-product-type default, also set on that tab;
- falling back to a single global default KPD.
Create the kpd Plain Text field (or whatever handle you configure) on your product/variant field
layouts before enabling auto-send, and fill in per-product-type or global defaults for products
that don't carry their own code.
Shipping lines use a separate Shipping KPD setting, defaulting to 532000 (postal and courier
activities). That default is a suggestion only — confirm the correct code with your accountant
before relying on it.
Delivery (AS4 / FINA)
- Deliver B2B invoices over AS4 — sends B2B e-invoices through the AS4 network (on by default).
- Deliver B2G invoices over FINA — sends invoices to public-sector buyers through FINA.
- B2G buyers (OIB) — list of buyer OIBs treated as public-sector (FINA) regardless of the organisation field.
Delivery status (AS4/FINA) isn't known synchronously at send time — poll for it with the
sync/delivery-status console command. Add it to cron:
*/15 * * * * php craft commerce-eracuni/sync/delivery-status
(adjust the php/craft paths for your server; see the polling rate-limit note in
src/services/DeliverySync.php — one status read per second per batch.)
Console commands
All commands are under commerce-eracuni/sync/:
craft commerce-eracuni/sync/status— connection check, a count of documents by status, and a warning for trigger statuses that no longer exist in Commerce.craft commerce-eracuni/sync/order <idOrNumber> [--verified-attempt=<n>]— send one order synchronously, under the send mutex; accepts an order ID, number, or reference.craft commerce-eracuni/sync/backfill [--dry-run] [--limit=50] [--since=YYYY-MM-DD]— queues every completed order matching the trigger conditions that has no document yet (e.g. orders placed before the plugin was installed). When none of the trigger statuses exists it queues nothing and exits with code 78.craft commerce-eracuni/sync/retry [--all] [--limit=50]— re-queues failed documents, except those whose outcome is unknown (see below).craft commerce-eracuni/sync/delivery-status [--limit=100]— refreshes pending AS4/FINA delivery statuses; this is the one to run from cron.
Unknown outcomes
When the invoice create call breaks off without an answer (a timeout, a dropped connection, a
reply that is not an explicit error) or the worker dies while it is in flight, e-računi may have
booked and fiscalised the invoice anyway. The document is flagged outcome unknown, and nothing
sends that order again: not auto-send, not Retry, not the order panel, not sync/retry. Look the
order up in e-računi first. If it holds the invoice, leave the order alone. If it does not, use
Verified not booked, send again on the documents list or the order panel, or
sync/order <id> --verified-attempt=<n> (the refusal message and sync/retry print the attempt
number). The confirmation covers only the attempt that was checked: if that send goes unknown
again, it needs a new check. A row still pending is offered the
override only after the job's time limit (15 minutes), when no worker can still be sending it. The
CP override is logged with the user who confirmed it.
Twig usage
{% set doc = craft.commerceEracuni.documentForOrder(order) %}
{% if doc and doc.isSent() %}
Račun {{ doc.number }} — <a href="{{ doc.pdfUrl }}">PDF</a>
{% endif %}
documentForOrder() accepts either a craft\commerce\elements\Order or a raw order ID, and
returns a wmd\commerceeracuni\models\Document (or null if nothing has been sent yet) with
number, status, treatment, deliveryStatus, outcomeUnknown, pdfUrl, and isSent().
Documents::isOutcomeUnknown($orderId) answers the same question from PHP.
PDF links are CP-only in 1.0. Document::getPdfUrl() returns a control-panel action URL
(commerce-eracuni/documents/pdf) gated by the plugin's manage-documents permission — it is not a
signed or tokenised URL, and the PDF itself never lives under the web root. The link only works for
a logged-in CP user with the "Send, preview and retry e-računi documents" permission; there is no
public front-end download route in this version.
Legal notes
- Fiscalisation (the F1 fiscal invoice, cash register code, etc.) applies only to cash-like payment methods (cash, cards, online wallets) per Croatian fiscalisation law — not to bank transfers or other non-cash settlement.
- B2B e-invoicing over the AS4 network is mandatory in Croatia from 1 January 2026 for business-to-business transactions. The Deliver B2B invoices over AS4 setting is on by default for this reason.
- B2G (business-to-government) e-invoicing goes through FINA, not AS4. Configure buyer OIBs that should be treated as public-sector on the Delivery settings tab.
None of the above is legal or tax advice — confirm applicability (especially KPD codes and the fiscalisation/VAT treatment split) with your accountant before relying on it in production.
Lineage
The VAT-treatment, fiscalisation, payment-method mapping, and KPD rules in this plugin are ported
from WMD's production e-računi integration (invoice_api_eracuni_rest.php in mojwmd), which has
issued 3000+ invoices through the same e-računi API.
Development
From plugins/commerce-eracuni:
composer install vendor/bin/ecs check --fix # coding standards vendor/bin/phpstan # static analysis vendor/bin/phpunit # test suite




