Developer notes
How an invoice is built
Section titled “How an invoice is built”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>).
Validation
Section titled “Validation”Every document is validated before it is uploaded (Validator::check()):
- XSD: the OASIS UBL 2.1 schemas in
app/addons/kv_ro_efactura/resources/xsd/(unmodified, source inSOURCE.txt). - Rules (
Ubl\Rules): a hand-written subset of the EN 16931 business rules with the same ids as the CEN Schematron (BR-01toBR-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 idsKV-RO-01toKV-RO-06(customization id, county codeRO-xx, Bucharest sector, VAT in RON for foreign currencies, seller and buyer identifiers). The rules accept all 15 official CEN example invoices that are intests/fixtures/en16931/(unit test). - 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.
ANAF API flow
Section titled “ANAF API flow”| 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).
Hooks used
Section titled “Hooks used”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.
Public PHP API
Section titled “Public PHP API”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.
File map
Section titled “File map”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