openapi: 3.1.0
info:
  title: tevaxia.lu API
  version: 1.1.1
  description: >-
    API de calculs indicatifs. Les hypothèses internes ne constituent ni une expertise certifiée ni une
    décision de crédit. Authentification selon chaque endpoint, par X-API-Key ou Authorization: Bearer. Les
    endpoints authentifiés appliquent des quotas et peuvent retourner 429 avec Retry-After.
    Pour les clés API, les limites sont partagées par clé sur une même instance serveur : Free 10/minute
    et 200/24 heures, Pro 60/minute et 5000/24 heures, Enterprise 600/minute et 100000/24 heures.
    Les fenêtres démarrent au premier appel ; ces compteurs en mémoire ne constituent pas un quota
    global distribué. La clé sandbox partage le quota Free entre ses utilisateurs. Une indisponibilité
    de la vérification des clés peut retourner 503.
  contact:
    name: Support tevaxia.lu
    email: contact@tevaxia.lu
    url: https://www.tevaxia.lu
  license:
    name: Proprietary
servers:
  - url: https://tevaxia.lu
    description: Production
security:
  - ApiKeyHeader: []
  - BearerAuth: []
tags:
  - name: Estimation
    description: Estimation indicative par référence communale et hypothèses internes
  - name: Agencies
    description: Endpoints B2B pour agences immobilières
  - name: PropCalc
    description: Rendement et cashflow multi-pays
  - name: Evaluation
    description: Scénarios de valorisation et sensibilité, sans certification réglementaire
  - name: Costs
    description: Frais d'acquisition et taxes
  - name: Fiscal
    description: Plus-values, impôts sur revenus locatifs
  - name: Data
    description: Données de marché (STATEC, BCL, ICV, Euribor)
paths:
  /api/v1/estimation:
    options:
      summary: Pré-vol CORS
      responses:
        '204':
          description: CORS OK
    post:
      tags:
        - Estimation
      summary: Estimation indicative d’un appartement en pleine propriété
      description: >-
        Référence communale agrégée, ajustements internes non validés statistiquement et fourchette
        conventionnelle. Pas de ventes individuelles, pas de prix par quartier. Les paramètres facultatifs
        omis utilisent les valeurs par défaut du schéma ; aucune coercition de type ni aucun champ inconnu
        n’est accepté.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EstimationInput'
            examples:
              luxembourg:
                summary: Appartement existant 85 m² — hypothèses explicites
                value:
                  commune: Luxembourg
                  surface: 85
                  nbChambres: 2
                  etage: adjEtage2e3eRef
                  etat: adjEtatBonRef
                  exterieur: adjExtBalconRef
                  parking: false
                  classeEnergie: D
                  typeBien: appartement
                  estNeuf: false
      responses:
        '200':
          description: Estimation calculée
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EstimationResponse'
        '400':
          description: Paramètres invalides
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Non authentifié
        '404':
          description: Commune ambiguë ou prix indisponible pour le segment
        '429':
          description: Quota dépassé
  /api/v1/estimation/batch:
    options:
      summary: Pré-vol CORS
      responses:
        '204':
          description: CORS OK
    post:
      tags:
        - Estimation
      summary: Estimation en lot (jusqu'à 1000 biens)
      description: >-
        Même validation, valeurs par défaut et méthode que l’endpoint unitaire. Réponse 200 pour un lot
        traité, même si des éléments échouent ; consulter success et error pour chaque index. Aucun résultat
        réglementaire.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - items
              properties:
                items:
                  type: array
                  maxItems: 1000
                  items:
                    $ref: '#/components/schemas/EstimationInput'
                  minItems: 1
      responses:
        '200':
          description: Lot traité (succès partiels possibles dans results[].error)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  count:
                    type: integer
                  succeeded:
                    type: integer
                  failed:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                        success:
                          type: boolean
                        data:
                          type: object
                        error:
                          type: string
        '400':
          description: Corps invalide ou lot vide
        '413':
          description: Lot trop volumineux (> 1000 items)
  /api/v1/agences/pdf:
    post:
      tags:
        - Agencies
      summary: Rapport d'estimation co-brandé (PDF)
      description: |
        Génère un PDF d'estimation à l'image d'une agence, incluant
        estimation + frais d'acquisition + aides applicables. Auth
        API key ou session avec org_id admin.
      responses:
        '200':
          description: PDF binaire (application/pdf)
        '401':
          description: Non autorisé
  /api/v1/ai/analyze:
    post:
      tags:
        - AI
      summary: Analyse IA d'un résultat (commentaire expert)
      description: |
        Génère un commentaire professionnel par LLM (Cerebras Llama 3.3
        par défaut, ou BYOK OpenAI/Anthropic) à partir d'un contexte
        structuré (inputs + résultats d'un calcul) et d'un prompt métier.
        Rate-limit : 5/jour gratuit pour JWT, tier clé API pour X-API-Key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - context
                - prompt
              properties:
                context:
                  type: string
                  description: Inputs + résultats formatés (plain text multiligne)
                prompt:
                  type: string
                  description: Consigne métier pour le LLM
      responses:
        '200':
          description: Analyse IA générée
          content:
            application/json:
              schema:
                type: object
                properties:
                  text:
                    type: string
                  model:
                    type: string
                  provider:
                    type: string
                    enum:
                      - cerebras
                      - groq
                      - openai
                      - anthropic
                  remaining:
                    type: integer
                    description: '-1 si illimité'
        '401':
          description: Auth requise
        '429':
          description: Quota quotidien atteint
        '502':
          description: Erreur fournisseur IA
  /api/v1/ai/chat:
    post:
      tags:
        - AI
      summary: Conversation multi-tour avec l'assistant immobilier LU
      description: |
        Endpoint conversationnel spécialisé immobilier Luxembourg
        (fiscalité LIR, préparation de scénarios indicatifs, VEFA, copropriété, hôtellerie, KYC).
        Accepte jusqu'à 20 messages par requête.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - messages
              properties:
                messages:
                  type: array
                  maxItems: 20
                  items:
                    type: object
                    required:
                      - role
                      - content
                    properties:
                      role:
                        type: string
                        enum:
                          - user
                          - assistant
                      content:
                        type: string
                        maxLength: 4000
      responses:
        '200':
          description: Réponse de l'assistant
          content:
            application/json:
              schema:
                type: object
                properties:
                  text:
                    type: string
                  model:
                    type: string
                  provider:
                    type: string
                  remaining:
                    type: integer
  /api/v1/ai/extract:
    post:
      tags:
        - AI
      summary: Extraction structurée depuis PDF/image (BYOK vision)
      description: |
        Extraction vision (OCR intelligent) depuis un document (PDF ou
        image PNG/JPEG) vers un JSON structuré. 4 schémas disponibles :
        bilan_promoteur, plus_values, dpe, facture_immo.
        **Nécessite une clé BYOK Anthropic (PDF) ou OpenAI (images)** —
        les providers gratuits Cerebras/Groq ne supportent pas la vision.
        Limite 8 MB par fichier.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - schema
                - fileBase64
                - mediaType
              properties:
                schema:
                  type: string
                  enum:
                    - bilan_promoteur
                    - plus_values
                    - dpe
                    - facture_immo
                fileBase64:
                  type: string
                  description: Fichier encodé base64 (sans préfixe data:)
                mediaType:
                  type: string
                  enum:
                    - application/pdf
                    - image/png
                    - image/jpeg
      responses:
        '200':
          description: Données extraites
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Objet JSON structuré selon le schéma demandé
                  schema:
                    type: string
                  provider:
                    type: string
                    enum:
                      - openai
                      - anthropic
                  model:
                    type: string
        '400':
          description: Schéma inconnu ou mismatch provider/media
        '402':
          description: Clé BYOK vision requise
        '413':
          description: Fichier > 8 MB
  /api/v1/mlv:
    post:
      summary: Sensibilité de valeur — chemin historique MLV
      description: >-
        Décotes additives choisies par l’utilisateur, total <= 100 %. Pas de MLV certifiée, valeur CRR ni
        pondération de risque. Les noms mlv et ratioMLVsurMV sont conservés pour compatibilité ; ltvBands
        reste vide et regulatoryValue vaut false.
      tags:
        - Evaluation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - valeurMarche
                - decoteConjoncturelle
                - decoteCommercialisation
                - decoteSpecifique
              properties:
                valeurMarche:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 1000000000000
                decoteConjoncturelle:
                  type: number
                  minimum: 0
                  maximum: 100
                  description: Pourcentage, ex. 5 = 5 %
                decoteCommercialisation:
                  type: number
                  minimum: 0
                  maximum: 100
                  description: Pourcentage, ex. 5 = 5 %
                decoteSpecifique:
                  type: number
                  minimum: 0
                  maximum: 100
                  description: Pourcentage, ex. 5 = 5 %
      responses:
        '200':
          description: Scénario non réglementaire dans success/data/meta
        '400':
          description: Paramètres invalides
        '401':
          description: Non authentifié
        '429':
          description: Quota dépassé
  /api/v1/dcf:
    post:
      summary: DCF annuel simplifié
      description: >-
        Flux en fin d’année et revente en fin d’année N à partir du NOI de N+1. Loyers et charges croissent
        séparément. Le champ irr est le taux d’actualisation par construction pour les flux conventionnels,
        sinon null ; pas de rendement indépendant sur un prix d’achat réel. Hors travaux, financement,
        fiscalité et acquisition.
      tags:
        - Evaluation
      responses:
        '200':
          description: Scénario de flux actualisés dans success/data
        '400':
          description: Entrées invalides ou NOI terminal non positif
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - loyerAnnuelInitial
                - tauxIndexation
                - tauxVacance
                - chargesAnnuelles
                - tauxProgressionCharges
                - periodeAnalyse
                - tauxActualisation
                - tauxCapSortie
                - fraisCessionPct
              properties:
                loyerAnnuelInitial:
                  type: number
                  minimum: 0
                  maximum: 1000000000000
                tauxIndexation:
                  type: number
                  minimum: -1
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                tauxVacance:
                  type: number
                  minimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                chargesAnnuelles:
                  type: number
                  minimum: 0
                  maximum: 1000000000000
                tauxProgressionCharges:
                  type: number
                  minimum: -1
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                periodeAnalyse:
                  type: integer
                  minimum: 1
                  maximum: 50
                tauxActualisation:
                  type: number
                  minimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                tauxCapSortie:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                fraisCessionPct:
                  type: number
                  minimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
  /api/v1/capitalisation:
    post:
      summary: Capitalisation directe
      description: >-
        Capitalisation directe d’un NOI annuel stabilisé positif. Vacance sur le brut ; frais de gestion et
        provision sur le brut avant vacance. Taux choisi par l’utilisateur, sans taux de marché automatique.
        Hors financement, fiscalité, acquisition et programme de travaux distinct. Les rendements Initial et
        Reversionnaire historiques sont avant charges après vacance ; rendementNet est NOI / valeur.
      tags:
        - Evaluation
      responses:
        '200':
          description: Scénario de capitalisation dans success/data
        '400':
          description: Entrées invalides, NOI non positif ou dépassement numérique
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - loyerBrutAnnuel
                - chargesNonRecuperables
                - assurancePNO
                - taxeFonciere
                - tauxVacance
                - provisionGrosEntretien
                - fraisGestion
                - tauxCapitalisation
              properties:
                loyerBrutAnnuel:
                  type: number
                  minimum: 0
                  maximum: 1000000000000
                chargesNonRecuperables:
                  type: number
                  minimum: 0
                  maximum: 1000000000000
                assurancePNO:
                  type: number
                  minimum: 0
                  maximum: 1000000000000
                taxeFonciere:
                  type: number
                  minimum: 0
                  maximum: 1000000000000
                tauxVacance:
                  type: number
                  minimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                provisionGrosEntretien:
                  type: number
                  minimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                fraisGestion:
                  type: number
                  minimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                tauxCapitalisation:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 1
                  description: 'Fraction : 0.05 = 5 %'
                ervAnnuel:
                  type: number
                  minimum: 0
                  maximum: 1000000000000
                  description: Facultatif ; omis = inconnu, zéro = loyer de marché nul
  /api/v1/capital-investi:
    post:
      summary: Capital investi total
      tags:
        - PropCalc
      responses:
        '200':
          description: Total capital (apport + frais + travaux)
  /api/v1/frais-acquisition:
    post:
      summary: Frais d'acquisition LU
      description: Droits d'enregistrement (6/7/10%), frais notaire, TVA logement, Bëllegen Akt.
      tags:
        - Costs
      responses:
        '200':
          description: Frais détaillés par poste
  /api/v1/plus-values:
    post:
      summary: Plus-value immobilière LU
      description: Art. 99ter LIR, demi-taux, abattement Bëllegen Akt, exonération résidence principale.
      tags:
        - Fiscal
      responses:
        '200':
          description: Plus-value imposable + impôt
  /api/v1/market-data:
    get:
      summary: Données marché (STATEC + Euribor + OAT + ICV)
      description: Source STATEC, BCL, INSEE. Rafraîchi quotidiennement.
      tags:
        - Data
      responses:
        '200':
          description: Indicateurs macro LU
  /api/v1/propcalc/countries:
    get:
      summary: Liste pays supportés PropCalc
      description: LU/FR/DE/BE/NL/IT/ES/PT/UK/US avec macro HICP + property growth.
      tags:
        - PropCalc
      responses:
        '200':
          description: Liste pays + méta
  /api/v1/propcalc/cashflow:
    post:
      summary: Cashflow locatif multi-pays
      tags:
        - PropCalc
      responses:
        '200':
          description: Cashflow annuel + cumul 10 ans
  /api/v1/propcalc/mortgage:
    post:
      summary: Amortissement hypothèque (multi-pays)
      tags:
        - PropCalc
      responses:
        '200':
          description: Échéancier mensuel/annuel
  /api/v1/propcalc/yield:
    post:
      summary: Rendement brut + net
      tags:
        - PropCalc
      responses:
        '200':
          description: Yield calculé
  /api/v1/propcalc/fees:
    post:
      summary: Frais d'acquisition multi-pays
      tags:
        - PropCalc
      responses:
        '200':
          description: Frais détaillés par pays
components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
  schemas:
    EstimationInput:
      type: object
      required:
        - commune
        - surface
      properties:
        commune:
          type: string
          description: Nom de la commune luxembourgeoise (ex. "Luxembourg")
          minLength: 1
          maxLength: 200
        quartier:
          type: string
          description: Information facultative, sans ajustement par quartier dans le jeu de données actuel
          maxLength: 200
        surface:
          type: number
          maximum: 10000
          description: Surface habitable en m²
          exclusiveMinimum: 0
        nbChambres:
          type: integer
          minimum: 0
          maximum: 20
          default: 0
        etage:
          type: string
          enum:
            - adjEtageSousSol
            - adjEtageRDC
            - adjEtage1er
            - adjEtage2e3eRef
            - adjEtage4e5e
            - adjEtageDernier
            - adjEtageAttique
          default: adjEtage2e3eRef
        etat:
          type: string
          enum:
            - adjEtatNeuf
            - adjEtatRenove
            - adjEtatBonRef
            - adjEtatCorrect
            - adjEtatARenover
            - adjEtatGrosTravaux
          default: adjEtatBonRef
        exterieur:
          type: string
          enum:
            - adjExtPasExterieur
            - adjExtBalconRef
            - adjExtGrandBalcon
            - adjExtTerrasse
            - adjExtJardin
            - adjExtTerrasseJardin
          default: adjExtBalconRef
        parking:
          type: boolean
          default: false
        classeEnergie:
          type: string
          enum:
            - A
            - B
            - C
            - D
            - E
            - F
            - G
          default: D
          description: >-
            Hypothèse de classe utilisée par ce modèle ; A+, H, I et inconnue ne disposent pas d’ajustement
            dans cet endpoint.
        typeBien:
          type: string
          enum:
            - appartement
          default: appartement
        estNeuf:
          type: boolean
          default: false
      additionalProperties: false
    EstimationResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            estimationBasse:
              type: number
            estimationCentrale:
              type: number
            estimationHaute:
              type: number
            prixM2Ajuste:
              type: number
            confiance:
              type: string
              enum:
                - faible
              description: Limitation du modèle, pas de probabilité statistique
            ajustements:
              type: array
              items:
                type: object
                properties:
                  labelKey:
                    type: string
                  pct:
                    type: number
            prixM2Base:
              type: number
            totalAjustements:
              type: number
            sourceBase:
              type: string
            confianceNote:
              type: string
        meta:
          type: object
          properties:
            api_key_name:
              type: string
            tier:
              type: string
              enum:
                - free
                - pro
                - enterprise
                - sandbox
            method:
              type: string
            limitations:
              type: string
    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
