# facturi.cloud — REST API v1

REST API pentru aplicații mobile, integrări POS și scripturi externe.

- **Base URL** dev: `http://localhost/Efactura/public/api/v1`
- **Base URL** prod (exemplu): `https://facturi.cloud/api/v1`
- **Documentație HTML**: https://facturi.cloud/docs/api
- **Markdown**: https://facturi.cloud/docs/api-v1.md
- **Postman**: https://facturi.cloud/docs/api-v1.postman_collection.json
- **Versiune**: `v1`
- **Format**: JSON (`application/json; charset=utf-8`)
- **Autentificare**: `Authorization: Bearer <token>` (Personal Access Token)

> Toate scrierile sunt scoped la o singură companie — token-ul are atașată
> compania activă. Dacă utilizatorul are acces la mai multe companii, poate
> emite token-uri separate pentru fiecare.

---

## 1. Autentificare

API-ul folosește **Personal Access Tokens (PAT)** opace. Generarea se face
din panoul web la `Setări → Token-uri API`.

```
Authorization: Bearer fcl_AbCd1234_...veryLongOpaqueToken
```

### 1.1 Scope-uri suportate

| Scope               | Descriere                                                   |
|---------------------|-------------------------------------------------------------|
| `documents:read`    | Citește facturi, proforme, avize.                           |
| `documents:write`   | Creează / modifică facturi, proforme, avize.                |
| `clients:read`      | Listare/detalii clienți + lookup ANAF.                      |
| `clients:write`     | Creare/modificare clienți.                                  |
| `products:read`     | Listează produse, serii.                                    |
| `products:write`    | Creează/modifică produse.                                   |
| `pos:read`          | Listează case de marcat, bonuri, sesiuni, rapoarte Z.       |
| `pos:write`         | Emite/anulează bonuri, deschide/închide sesiuni, raport Z.  |

> Scope-ul `:write` include și `:read` pentru același domeniu.

### 1.2 Răspunsuri standard

```jsonc
// Succes
{
  "data": { ... },
  "meta": { "page": 1, "per_page": 25, "total": 142, "last_page": 6 },
  "errors": []
}

// Eroare
{
  "data": null,
  "meta": null,
  "errors": [
    { "code": "validation_error", "message": "Lipsește `id_client`.", "field": "id_client" }
  ]
}
```

### 1.3 Coduri HTTP relevante

| Cod | Înțeles                                            |
|-----|----------------------------------------------------|
| 200 | OK                                                 |
| 201 | Resursă creată                                     |
| 400 | Cerere invalidă (JSON malformat, header lipsă)     |
| 401 | Token lipsă / invalid / expirat                    |
| 403 | Token valid dar fără scope sau companie inaccesibilă |
| 404 | Resursă inexistentă                                |
| 409 | Conflict (sesiune deja deschisă etc.)              |
| 422 | Eroare validare business                           |
| 429 | Rate limit depășit                                 |
| 500 | Eroare server (jurnalizată)                        |

---

## 2. Endpoint-uri publice

### 2.1 GET `/health`

Verifică conectivitatea la baza de date și starea serviciului.

```bash
curl https://app.facturi.cloud/api/v1/health
```

```json
{
  "data": { "status": "ok", "service": "facturi.cloud-api", "version": "v1",
            "database": "ok", "time": "2026-04-19T18:00:00+03:00" },
  "meta": null,
  "errors": []
}
```

---

## 3. Profil & companii

### 3.1 GET `/me`

Detalii utilizator + listă companii la care are acces + scope-uri token.

### 3.2 GET `/companii/active`

Detaliile companiei active (CUI, adresă, regim TVA, plan abonament).

---

## 4. Facturi

### 4.1 GET `/facturi`

Listare paginată. Suportă filtre: `status`, `id_client`, `from`, `to`, `q` (text).

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://.../api/v1/facturi?status=emisa&page=1&per_page=25"
```

### 4.2 GET `/facturi/{id}`

Bundle complet: header + linii + plăți + e-Factura status.

### 4.3 POST `/facturi`

Creează factură (draft sau emite imediat). Poate folosi produse din catalog,
crea produse noi pe linie, sau liniile pot fi text liber.

**Scope:** `documents:write` (crearea produselor din payload-ul facturii nu cere separat `products:write`).

```jsonc
{
  "id_serie": 12,                  // optional — fallback pe seria default
  "id_client": 345,                // sau bloc `client` (vezi mai jos)
  "client": {                      // alternativă: creează / reutilizează client după CUI
    "denumire": "ACME SRL",
    "cui": "RO12345678",
    "adresa": "Str. Test 1",
    "judet": "B",
    "localitate": "București"
  },
  "moneda": "RON",
  "tip_factura": "380",            // 380 = comercială (default), 751 = în baza bonului
  "data_emitere": "2026-04-19",
  "data_scadenta": "2026-05-04",
  "modalitate_plata": "OP / Card / Cash",
  "observatii": "Plata în 15 zile",
  "linii": [
    // 1) Produs existent din catalog — denumire/preț/TVA se completează automat dacă lipsesc
    { "id_produs": 5, "cantitate": 2 },

    // 2) Linie text liber (nu salvează în catalog)
    { "descriere": "Servicii consultanță", "cantitate": 1, "pret_unitar": 350.00, "cota_tva": 21 },

    // 3) Produs nou în catalog (sau reutilizat dacă există același cod_produs)
    {
      "creeaza_produs": true,
      "denumire": "Mouse wireless",
      "cod_produs": "MW-01",
      "unitate_masura": "buc",
      "pret_unitar": 89.00,
      "cota_tva": 21,
      "cantitate": 3
    },

    // 4) Echivalent: bloc nested `produs`
    {
      "produs": {
        "denumire": "Licență software",
        "tip": "serviciu",
        "pret_fara_tva": 500.00,
        "cota_tva": 21,
        "unitate_masura": "buc"
      },
      "cantitate": 1
    }
  ],
  "scade_stoc": false,
  "status": "emisa"                // sau "draft" — implicit: "emisa"
}
```

**Câmpuri pe linie**

| Câmp | Note |
|------|------|
| `id_produs` | Opțional. Produs din catalog. |
| `descriere` / `denumire` | Obligatoriu dacă nu există `id_produs` / `produs`. |
| `cantitate` | Implicit 1 |
| `pret_unitar` / `pret` | Dacă lipsește și există produs, se ia prețul din catalog |
| `cota_tva` / `tva` | Implicit 21% (0% dacă firma e neplatitor de TVA) |
| `discount_procent` / `discount_pct` | |
| `discount_valoric` / `discount_val` | |
| `creeaza_produs` | `true` → salvează în catalog (sau găsește după `cod_produs`) |
| `produs` | Obiect cu `denumire`, `cod_produs`, `tip`, `unitate_masura`/`um`, `pret_fara_tva`, `cota_tva` |

> Pentru catalog separat: `GET/POST /produse` (scope `products:read` / `products:write`).

### 4.4 GET `/facturi/{id}/pdf`

Returnează PDF-ul (Content-Type: `application/pdf`).

### 4.5 POST `/facturi/{id}/anaf`

Trimite factura în SPV (e-Factura).

### 4.6 POST `/facturi/{id}/incasare`

Înregistrează o încasare pe factură.

```jsonc
{ "data": "2026-04-20", "suma": 238.00, "metoda": "card", "observatii": "" }
```

### 4.7 POST `/facturi/{id}/storno`

Emite factură de storno (negativă) pentru anularea facturii curente. Funcționează și pentru facturi deja trimise în SPV: se creează un document nou, se emite și se încearcă trimiterea automată în e-Factura (dacă nu e exclusă din SPV). Răspunsul include eventual `spv` cu rezultatul încărcării.

---

## 5. Proforme

CRUD similar cu facturile, **fără** integrare ANAF/scădere stoc.

- `GET /proforme` — listare paginată
- `GET /proforme/{id}` — detalii
- `POST /proforme` — creare
- `GET /proforme/{id}/pdf` — PDF
- `POST /proforme/{id}/factura` — convertește proforma în factură (păstrează liniile)

---

## 6. Avize de însoțire a mărfii

- `GET /avize` — listare
- `GET /avize/{id}` — detalii (header + linii)
- `POST /avize` — creare
- `GET /avize/{id}/pdf` — PDF
- `POST /avize/{id}/factura` — convertește avizul în factură

> Avizele NU se trimit în SPV (formularul e doar tipizat fiscal pe transport).

---

## 7. Clienți

- `GET /clienti?q=...&page=1` — listare cu căutare după denumire/CUI
- `GET /clienti/{id}` — detalii
- `POST /clienti` — creare
- `POST /clienti/{id}` — actualizare
- `GET /clienti/lookup-anaf?cui=RO12345678` — lookup în registrul ANAF (rate-limited 30/min)

---

## 8. Catalog produse

**Scope:** `products:read` / `products:write`

- `GET /produse?q=...&tip=produs|serviciu&activ=1` — listare paginată
- `GET /produse/{id}` — detalii
- `POST /produse` — creare (`denumire` obligatoriu; `unitate_masura`, `pret_fara_tva`, `cota_tva` default 21, `tip`)
- `POST /produse/{id}` — actualizare
- `GET /serii?tip=factura` — listare serii (factura/proforma/aviz/chitanta)

Exemplu creare produs:

```jsonc
{
  "denumire": "Cablu HDMI 2m",
  "cod_produs": "HDMI-2",
  "tip": "produs",
  "unitate_masura": "buc",
  "pret_fara_tva": 45.00,
  "cota_tva": 21
}
```

> Alternativ, poți crea produse direct pe `POST /facturi` cu `creeaza_produs` / `produs` pe linie.

---

## 9. POS — case de marcat & bonuri fiscale

> **Status v1**: schema DB + driver mock funcțional + endpoint-uri complete.
> Driverele AMEF reale (Datecs, Tremol, Custom) sunt stub-uri ce vor fi
> implementate într-o versiune ulterioară. Pentru integrări proprii, casa
> de marcat poate fi setată cu `driver = 'mock'` în dev.

### 9.0 Împerechere din aplicație (recomandat)

Fără token pre-creat în web: utilizatorul se autentifică în app cu email/parolă,
primește un PAT cu scope-uri POS și alege / creează casa de marcat.

- `POST /pos/pair` — **public** (fără Bearer)

  ```jsonc
  {
    "email": "casier@firma.ro",
    "password": "••••••••",
    "id_companie": 5,           // obligatoriu dacă userul are mai multe firme
    "id_casa": 2,               // SAU selectează o casă existentă
    "casa_nume": "Casa magazin", // SAU creează / refolosește după nume
    "driver": "mock",           // opțional la creare: mock | datecs | tremol | custom
    "device_name": "Tablet POS Magazin 1"
  }
  ```

  Scope-uri emise automat: `pos:read`, `pos:write`, `products:read`.

  Răspuns `200`:

  ```jsonc
  {
    "data": {
      "token": "fcl_........",
      "scopes": ["pos:read", "pos:write", "products:read"],
      "user": { "id": 6, "email": "...", "nume_complet": "..." },
      "company": { "id": 5, "denumire": "...", "cui": "..." },
      "casa": { "id": 2, "nume": "Casa magazin", "driver": "mock", "activ": true },
      "case_marcat": [ /* lista firmă */ ],
      "gestiuni": [ /* pentru sync stoc */ ]
    }
  }
  ```

  Coduri utile:
  - `409` + `multiple_companies` → alegeți `id_companie` din `data.companies`
  - `409` + `casa_required` → alegeți `id_casa` din `data.case_marcat` sau trimiteți `casa_nume`

### 9.1 Case de marcat

- `GET /pos/case-marcat` — listare case configurate (filtru opțional `?activ=1`)
- `POST /pos/case-marcat` — creează casă (`nume` obligatoriu; `driver` opțional) — scope `pos:write`
- `GET /pos/case-marcat/{id}` — detalii
- `GET /pos/case-marcat/{id}/status` — query online la AMEF (driver) + cache status

### 9.2 Sesiuni de casă (deschidere/închidere zi de lucru)

- `GET /pos/sesiuni/curenta?id_casa=1` — sesiunea deschisă curentă (sau null)
- `POST /pos/sesiuni/deschide` — deschide o sesiune

  ```jsonc
  { "id_casa": 1, "sold_initial": 100.00, "observatii": "Schimb tura 1" }
  ```

- `POST /pos/sesiuni/inchide` — închide sesiunea

  ```jsonc
  { "id_sesiune": 42, "sold_final": 1234.50 }
  // sau
  { "id_casa": 1 } // închide sesiunea deschisă curentă
  ```

### 9.3 Bonuri fiscale

- `GET /pos/bonuri?id_casa=&id_sesiune=&status=&from=&to=&page=`
- `GET /pos/bonuri/{id}` — bundle (header + linii + plăți)
- `POST /pos/bonuri` — emite bon (auto_emit=true implicit)

  ```jsonc
  {
    "id_casa": 1,                  // optional — fallback pe prima casă activă
    "id_sesiune": 42,              // optional — fallback pe sesiunea deschisă
    "id_client": null,             // optional — pentru bon nominal
    "moneda": "RON",
    "linii": [
      { "descriere": "Cafea espresso", "cantitate": 1, "pret_unitar": 8.00, "cota_tva": 9, "departament": "Bauturi" },
      { "id_produs": 22, "cantitate": 2, "pret_unitar": 5.00, "cota_tva": 19 }
    ],
    "plati": { "cash": 20.00, "card": 0, "alt": 0 },
    "auto_emit": true              // dacă false, bonul rămâne în status "pregatit"
  }
  ```

  Răspuns:

  ```jsonc
  {
    "data": {
      "id": 101,
      "status": "emis",
      "numar_bon": 5,
      "serie_fiscala": "MOCK-000005",
      "subtotal": 18.00, "total_tva": 1.72, "total": 19.72,
      "plati": { "cash": 20.00, "card": 0, "alt": 0 },
      "rest": 0.28,
      "linii": [ ... ],
      "emit": { "numar_bon": 5, "serie_fiscala": "MOCK-000005", "message": "Bon mock emis." }
    }
  }
  ```

- `POST /pos/bonuri/{id}/anulare` — anulează bonul (driver: cancel sau bon negativ)

  ```jsonc
  { "motiv": "Greșeală operator" }
  ```

### 9.4 Rapoarte Z

- `GET /pos/rapoarte-z?id_casa=&from=&to=` — listare paginată
- `GET /pos/rapoarte-z/{id}` — detalii raport
- `POST /pos/rapoarte-z` — închide ziua fiscală

  ```jsonc
  { "id_casa": 1, "id_sesiune": 42 }
  ```

---

## 10. Best practices client

- Trimite mereu `Accept: application/json`.
- Pentru POST/PUT folosește `Content-Type: application/json`.
- Reîncearcă pe 5xx și 429 cu backoff exponențial (max 5 încercări, jitter 250ms-2s).
- Tratează 401 ca semn de token invalidat → forțează re-auth în UI.
- Verifică `meta.last_page` pentru paginare; nu presupune o singură pagină.
- Pentru integrare POS: emiterea de bonuri trebuie să fie idempotentă în UI
  (debounce buton de "Emite" 1500 ms) — server-ul nu are deduplicare automată.

---

## 10b. Preluare token ANAF (aplicații externe / WPF)

Autentificare: `Authorization: Bearer {ANAF_PICKUP_API_SECRET}` (din `.env` platformă).
Nu folosește personal access token-urile mobile.

### `POST /anaf/pickup-link`

Generează link public OAuth pentru un CUI.

Body JSON: `{ "cui": "RO12345678" }`

Răspuns `201`:
```json
{
  "data": {
    "id": 1,
    "cui": "RO12345678",
    "public_token": "...",
    "url": "https://facturi.cloud/anaf/preluare/...",
    "status": "pending",
    "link_expires_at": "2026-08-30 14:00:00",
    "created_at": "2026-08-28 14:00:00"
  },
  "meta": null,
  "errors": []
}
```

Flux: afișați / trimiteți `url` utilizatorului → el apasă „Preluare token” (OAuth) →
apoi aplicația apelează `GET /anaf/pickup-token`.

### `GET /anaf/pickup-status?cui=RO12345678`

Statusul ultimului link (fără a consuma tokenul). `has_token: true` când e gata.

### `GET /anaf/pickup-token?cui=RO12345678`

One-shot: returnează `access_token` / `refresh_token` / `expira_la` și **șterge** rândul.
`404` dacă nu e `ready`. Nu reîncercați după un `200`.

---

## 11. Rate limiting

- Per token: bucket-uri pe minut/operație. Răspuns 429 cu `Retry-After` recomandat.
- Endpoint-uri specifice cu limite mai stricte:
  - `GET /clienti/lookup-anaf` → 30 cereri/minut/token

---

## 12. Smoke test rapid

Vezi `tools/api_smoke_test.php`. Setează `API_BASE_URL` și `API_TOKEN`:

```bash
$env:API_BASE_URL = "http://localhost/Efactura/public/api/v1"
$env:API_TOKEN    = "fc_pat_..."
php tools/api_smoke_test.php
```

Scriptul rulează: `/health`, `/me`, `/companii/active`, `/serii`, `/clienti?per_page=1`,
`/facturi?per_page=1`, `/proforme?per_page=1`, `/avize?per_page=1`, `/produse?per_page=1`,
`/pos/case-marcat`. Output: tabel cu status / latency.

---

## 13. Postman collection

Vezi `docs/api-v1.postman_collection.json`. Importă în Postman, setează variabilele de
collection: `baseUrl` și `token`.
