Skip to content

Developer notes

Builder::invoice() turns fn_get_order_info() into a neutral invoice model; Ubl\Writer writes it as UBL 2.1 Invoice (or CreditNote) in the CIUS-RO customization. The model is stored with the document when it is created, so the invoice never changes when the order is edited later. Everything is computed in integer cents, so the totals add up exactly.

Order data UBL element
Seller (Connection page) AccountingSupplierParty: PartyName, PostalAddress (county as RO-xx), PartyTaxScheme (VAT id with RO, VAT payers only), PartyLegalEntity (RegistrationName, CompanyID = CIF, CompanyLegalForm = trade register no.)
Buyer company with CUI or VAT id AccountingCustomerParty: legal id = CUI digits, VAT id when the value has the RO prefix; EU buyers: their VAT id
Buyer without a tax id (B2C) name of the person, PartyLegalEntity/CompanyID = the 13-digit placeholder setting (default 0000000000000)
Buyer county and city CountrySubentity RO-xx from the state code or county name; Bucharest city = SECTOR1 to SECTOR6
Prices always invoiced net: prices that include tax are divided by 1 + rate; the discount of the order is spread over the product lines
Shipping, payment surcharge their own lines with the tax of the order
Rate above 0 VAT category S with the rate
0 % to a buyer in Romania Z, E or O (setting; E and O carry the exemption reason text)
0 % to an EU buyer with VAT id / outside the EU K (VATEX-EU-IC) / G (VATEX-EU-G), only when foreign buyers are reported
Seller not a VAT payer category O (VATEX-EU-O), no VAT id
Totals TaxTotal/TaxSubtotal per category and rate, LegalMonetaryTotal; the difference to the order total (up to 0.10) is PayableRoundingAmount
Paid orders PrepaidAmount = total, payable 0 (setting “Mark invoices as paid”)
Currency other than RON TaxCurrencyCode RON and a second TaxTotal in RON, converted with the store’s RON rate
Credit notes CreditNote, type code 381, BillingReference to the invoice, positive amounts, lines of the credited quantity

Numbering: SERIES000001 for invoices and SERIESC000001 for credit notes, gapless per series (?:kv_ro_efactura_counters). The document row is inserted first; the counter increment and the number are written in one transaction under a per-order (credit note: per-invoice) lock, so racing status changes make one invoice and no hole.

Length limits: item name 100 characters (BT-153), note 300 (BT-22), party names, street and city 200, cut by characters (not bytes). The official CIUS-RO Schematron (BR-RO rules) could not be obtained; only KV-RO-01..06 are implemented, so keep “Check with the ANAF validator before upload” on in the Test environment.

Upload safety: the document goes to status U before the POST. After a timeout or 5xx, Transport\Provider::find() lists the last days (listaMesajeFactura, filter T), downloads messages not yet assigned to a document and matches the invoice number in the XML; only when nothing matches 20 minutes after the attempt is the upload repeated. Credit notes carry src (invoice line index) per line and are created with a request token (credit:<invoice>:<token>).

Every document is validated before it is uploaded (Validator::check()):

  1. XSD: the OASIS UBL 2.1 schemas in app/addons/kv_ro_efactura/resources/xsd/ (unmodified, source in SOURCE.txt).
  2. Rules (Ubl\Rules): a hand-written subset of the EN 16931 business rules with the same ids as the CEN Schematron (BR-01 to BR-11, BR-16, BR-25, BR-27, BR-CO-04/09/10/13/14/15/16/17/18/26, BR-S-01/05, BR-E/AE/K/G/O-09/10, BR-Z-05, BR-50) and a few Romanian checks with our own ids KV-RO-01 to KV-RO-06 (customization id, county code RO-xx, Bucharest sector, VAT in RON for foreign currencies, seller and buyer identifiers). The rules accept all 15 official CEN example invoices that are in tests/fixtures/en16931/ (unit test).
  3. ANAF validator (optional setting): Api::validate() posts the document to ANAF’s own validation service before upload.

This is not the official CIUS-RO Schematron and not a certification. The CIUS-RO rule files are published by the Ministry of Finance together with the e-Factura technical documentation; they could not be downloaded when this version was built (the server closed the connection), so they are not shipped and none of the BR-RO- rule numbers are claimed. ANAF validates every upload with the full rule set, and the add-on shows its messages. Use the optional ANAF validator setting while you test.

Step Call Source
Authorise (browser, qualified certificate) GET https://logincert.anaf.ro/anaf-oauth2/v1/authorize?response_type=code&client_id=&redirect_uri=&token_content_type=jwt official: ANAF “Procedura de inregistrare a aplicatiilor in portalul ANAF” (PDF, updated 12.04.2024)
Exchange the code POST https://logincert.anaf.ro/anaf-oauth2/v1/token, Basic auth (client id : client secret), form grant_type=authorization_code, code, redirect_uri, token_content_type=jwt official, same PDF (code lifetime: the PDF says the connection is reset after about 60 seconds)
Refresh same endpoint, Basic auth, form grant_type=refresh_token, refresh_token official, same PDF. Access token 129600 min (90 days), refresh token 525600 min (365 days); both are replaced on every refresh
Token check GET https://api.anaf.ro/TestOauth/jaxrs/hello?name= official, same PDF
Upload POST https://api.anaf.ro/{prod|test}/FCTEL/rest/upload?standard=UBL&cif=<seller CIF> (credit note: standard=CN; extern=DA for buyers outside Romania), body = XML, Content-Type: text/plain; answer <header ExecutionStatus="0" index_incarcare="..."/> or ExecutionStatus="1" with <Errors errorMessage=.../> URLs: official page Servicii_web/url_eFactura.html (host webserviceapl.anaf.ro); parameters and answer: secondary (third-party description, to be confirmed against the e-Factura API presentation on mfinante.gov.ro)
Upload for individuals POST .../uploadb2c with the same parameters, used for documents with a buyer without a company tax id secondary; verify whether it is required for the current B2C rules
Status GET .../stareMesaj?id_incarcare=<index> -> stare = ok, nok, in prelucrare, XML cu erori nepreluat de sistem; id_descarcare when processed URL official; states secondary
Messages GET .../listaMesajeFactura?zile=1..60&cif= (used by “Test connection”) URL official; parameters secondary
Download GET .../descarcare?id=<id_descarcare> -> ZIP with the invoice (or the error report) and the electronic signature of the Ministry of Finance URL official; content secondary
Validate POST https://webservicesp.anaf.ro/prod/FCTEL/rest/validare/FACT1 (credit note FCN), no login, production host only secondary
XML to PDF POST .../transformare/FACT1 (credit note FCN), no login secondary; the service sits behind a web firewall that can refuse some documents

Rate limits (official PDF): 1000 requests per minute on api.anaf.ro. The add-on makes one upload and a few status requests per invoice. Messages are only available for download for a limited time (secondary source, not confirmed on an official ANAF page), which is why the signed answer is stored with the order.

Base URLs: https://api.anaf.ro/test/FCTEL/rest/ (Test mode), https://api.anaf.ro/prod/FCTEL/rest/ (Live mode).

Constants for config.local.php (development only):

  • kv_ro_efactura_base_url ($config[...]): a local mock server (honoured only for tests; replaces the ANAF hosts).

send_document builds the document if needed, validates it, uploads it and stores the upload index. poll_status reads stareMesaj after 2, 5, 15, 60 and 180 minutes; on ok or nok it downloads the ZIP, stores it with the document (receipt, base64) and sets the status. Network errors, 5xx and 429 are retried with the standard back-off (5 min, 15 min, 1 h, 3 h, 12 h); other errors stop the job and show the reason on the document. The cron job also queues uploads that ANAF has not answered for an hour.

Table Contents
?:kv_ro_efactura_accounts OAuth application (client id, secrets sealed), seller data and options (extra JSON), token pair (sealed) per account (0 = store / marketplace, vendor id)
?:kv_ro_efactura_documents Invoices (kind I) and credit notes (C): number, status, upload index, frozen model, XML, ANAF answer
?:kv_ro_efactura_counters Gapless numbers per series
?:kv_ro_efactura_jobs, ?:kv_ro_efactura_log Queue and log (secrets masked)

Secrets are sealed with AES-256-GCM using a key derived from the store’s crypt key (Secret).

change_order_status_post (create the invoice at the chosen status, optional automatic credit note), delete_order (documents are kept). Template hooks: orders:manage_header, orders:manage_data, orders:tabs_extra. Identical in 4.17.2 and 4.21.2.

use Tygh\Addons\KvRoEfactura\Documents;
$doc = Documents::createForOrder($order_id, 'manual'); // null + Documents::$skipped when the order is skipped
$res = Documents::createCredit($doc['doc_id'], null, 'Cancelled'); // null = credit everything, or [line index => quantity that stays invoiced]

Nothing is printed on storefront pages. The authorisation callback and all downloads are admin pages; downloads are private, no-store.

app/addons/kv_ro_efactura/
addon.xml init.php func.php
resources/xsd/ OASIS UBL 2.1 schemas (Invoice, CreditNote, common)
controllers/backend/kv_ro_efactura.php documents, connection, OAuth start / callback, credit notes, downloads, log, jobs
controllers/backend/orders.post.php order tab and list column data
controllers/frontend/kv_ro_efactura.php cron URL
Tygh/Addons/KvRoEfactura/Ubl/ Writer, Rules
Tygh/Addons/KvRoEfactura/Client/ Api (ANAF), BaseClient, ApiException
Tygh/Addons/KvRoEfactura/ Builder, Buyer, Documents, Jobs, Queue, Validator, Accounts, Secret, ...
Tygh/Addons/KvRoEfactura/License/ license client
Tygh/Addons/KvRoEfactura/Upgrade/Synchronizer.php