openapi: 3.1.0
info:
  title: ERPIA API
  version: "1.1"
  summary: API REST de ERPIA para integrar la facturación, los contactos, los gastos y los cobros con otros sistemas.
  description: |
    Autenticación con `X-API-Key` (Mi ERPIA → API y webhooks). Las claves de **solo lectura**
    sirven para todos los GET; las de **lectura y escritura** (plan de pago) permiten crear
    facturas, presupuestos, contactos, artículos, gastos y cobros.

    Las facturas creadas por API entran por el mismo camino que las de la pantalla: nacen como
    borrador con número previsto y, al emitirlas (`emitir: true`), ERPIA asigna el correlativo
    definitivo y sella el registro Verifactu. No hay ninguna puerta trasera al encadenado.

    Para recibir avisos cuando pasa algo (factura emitida, cobro registrado…) configura un
    **webhook saliente**; la firma se verifica con `X-ERPIA-Signature`. Documentación: https://erpia.es/api
  contact:
    name: ERPIA
    email: hola@erpia.es
    url: https://erpia.es/api
servers:
  - url: https://rkjhrcujdsdhnjfbvhuf.supabase.co/functions/v1/api-v1
security:
  - ApiKey: []
tags:
  - name: Cuenta
  - name: Facturas
  - name: Presupuestos
  - name: Contactos
  - name: Artículos
  - name: Gastos
  - name: Cobros
  - name: Proyectos
paths:
  /me:
    get:
      tags: [Cuenta]
      summary: Datos de la cuenta y scopes de la clave
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  razon_social: { type: string }
                  nif: { type: string }
                  email: { type: string }
                  plan: { type: string, enum: [free, suscriptor] }
                  scopes: { type: array, items: { type: string } }
                  api_version: { type: string }
        "401": { $ref: "#/components/responses/NoAutorizado" }
  /eventos:
    get:
      tags: [Cuenta]
      summary: Catálogo de eventos que puede emitir un webhook
      responses:
        "200":
          description: OK
  /facturas:
    get:
      tags: [Facturas]
      summary: Listar facturas (facturas, tickets y rectificativas)
      parameters:
        - { name: desde, in: query, schema: { type: string, format: date } }
        - { name: hasta, in: query, schema: { type: string, format: date } }
        - { name: estado, in: query, schema: { type: string, enum: [borrador, confirmada, enviada, cobrada, anulada, rectificada] } }
        - { name: tipo, in: query, schema: { type: string, enum: [factura, rectificativa, ticket] } }
        - { name: contacto_id, in: query, schema: { type: string, format: uuid } }
        - { name: q, in: query, description: Busca en número y nombre del cliente, schema: { type: string } }
        - { name: actualizado_desde, in: query, description: Solo documentos modificados desde esta fecha-hora (sincronización incremental), schema: { type: string, format: date-time } }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lista" }
    post:
      tags: [Facturas]
      summary: Crear una factura (borrador, o emitida con `emitir`)
      description: Requiere scope `write` y plan de pago. Usa `Idempotency-Key` para que un reintento no duplique la factura.
      parameters: [ { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NuevaFactura" }
            example:
              contacto_id: "0b0e3a4e-2f5d-4a2b-9f1e-1c2d3e4f5a6b"
              lineas:
                - { descripcion: "Reforma baño", detalle: "Alicatado y fontanería", cantidad: 1, precio: 1450, iva: 21 }
                - { descripcion: "Mano de obra", cantidad: 8, precio: 35, unidad: "H" }
              fecha_vencimiento: "2026-10-15"
              emitir: true
      responses:
        "201":
          description: Creada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FacturaResumen" }
        "400": { $ref: "#/components/responses/Peticion" }
        "403": { $ref: "#/components/responses/Prohibido" }
  /facturas/{id}:
    get:
      tags: [Facturas]
      summary: Una factura con sus líneas
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "404": { $ref: "#/components/responses/NoEncontrado" }
    patch:
      tags: [Facturas]
      summary: Modificar un borrador o emitirlo
      description: Solo borradores. Una factura emitida es inalterable (Verifactu) y se corrige con una rectificativa desde la app.
      parameters: [ { $ref: "#/components/parameters/id" }, { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                fecha_vencimiento: { type: string, format: date, nullable: true }
                titulo: { type: string }
                observaciones: { type: string }
                notas_internas: { type: string }
                email_cliente: { type: string }
                metodo_pago: { type: string, enum: [efectivo, transferencia, tarjeta, bizum, cheque, otro] }
                emitir: { type: boolean, description: "true = emitir (numera y sella)" }
      responses:
        "200": { description: OK }
        "409": { description: La factura ya no es un borrador }
  /presupuestos:
    get:
      tags: [Presupuestos]
      summary: Listar presupuestos
      parameters:
        - { name: desde, in: query, schema: { type: string, format: date } }
        - { name: hasta, in: query, schema: { type: string, format: date } }
        - { name: estado, in: query, schema: { type: string, enum: [borrador, enviada, aceptado, firmada, facturado, caducado] } }
        - { name: contacto_id, in: query, schema: { type: string, format: uuid } }
        - { name: q, in: query, schema: { type: string } }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Lista" } } } }
    post:
      tags: [Presupuestos]
      summary: Crear un presupuesto
      description: Devuelve `url_publica`, el enlace donde el cliente lo ve, lo acepta y lo firma.
      parameters: [ { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NuevaFactura" }
      responses:
        "201": { description: Creado }
  /presupuestos/{id}:
    get:
      tags: [Presupuestos]
      summary: Un presupuesto con sus líneas
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "404": { $ref: "#/components/responses/NoEncontrado" }
  /contactos:
    get:
      tags: [Contactos]
      summary: Listar clientes y proveedores
      parameters:
        - { name: tipo, in: query, schema: { type: string, enum: [cliente, proveedor, ambos, acreedor, prospecto] } }
        - { name: q, in: query, description: Busca en nombre, NIF y email, schema: { type: string } }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Lista" } } } }
    post:
      tags: [Contactos]
      summary: Crear un contacto
      parameters: [ { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NuevoContacto" }
            example: { nombre: "Rita Gálvez", nif: "12345678Z", email: "rita@ejemplo.es", telefono: "600 000 000", ciudad: "Granada", tipo: "cliente", etiquetas: ["web"] }
      responses:
        "201": { description: Creado }
  /contactos/{id}:
    get:
      tags: [Contactos]
      summary: Un contacto
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "404": { $ref: "#/components/responses/NoEncontrado" }
    patch:
      tags: [Contactos]
      summary: Modificar un contacto
      parameters: [ { $ref: "#/components/parameters/id" }, { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NuevoContacto" }
      responses:
        "200": { description: OK }
  /articulos:
    get:
      tags: [Artículos]
      summary: Listar el catálogo
      parameters:
        - { name: q, in: query, description: Busca en nombre, código y referencia, schema: { type: string } }
        - { name: familia, in: query, schema: { type: string } }
        - { name: activo, in: query, schema: { type: boolean } }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Lista" } } } }
    post:
      tags: [Artículos]
      summary: Crear un artículo o servicio
      parameters: [ { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nombre, precio]
              properties:
                nombre: { type: string }
                precio: { type: number, description: Base imponible, sin IVA }
                tipo: { type: string, enum: [servicio, producto], default: servicio }
                porcentaje_iva: { type: integer, default: 21 }
                unidad: { type: string, default: ud }
                descripcion: { type: string }
                codigo: { type: string }
                referencia: { type: string }
                ean: { type: string }
                familia: { type: string }
                precio_coste: { type: number }
                stock_actual: { type: number }
                stock_minimo: { type: number }
                activo: { type: boolean, default: true }
      responses:
        "201": { description: Creado }
  /articulos/{id}:
    get:
      tags: [Artículos]
      summary: Un artículo
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
  /gastos:
    get:
      tags: [Gastos]
      summary: Listar gastos
      parameters:
        - { name: desde, in: query, schema: { type: string, format: date } }
        - { name: hasta, in: query, schema: { type: string, format: date } }
        - { name: categoria, in: query, schema: { $ref: "#/components/schemas/CategoriaGasto" } }
        - { name: proyecto_id, in: query, schema: { type: string, format: uuid } }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Lista" } } } }
    post:
      tags: [Gastos]
      summary: Registrar un gasto
      parameters: [ { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [concepto, importe]
              properties:
                concepto: { type: string }
                importe: { type: number, description: Total, IVA incluido }
                base_imponible: { type: number, description: Si no se envía, se calcula desde el importe y el IVA }
                porcentaje_iva: { type: integer, default: 21 }
                fecha: { type: string, format: date }
                proveedor: { type: string }
                nif_proveedor: { type: string, description: Sin NIF el gasto se registra como factura simplificada (ticket) }
                categoria: { $ref: "#/components/schemas/CategoriaGasto" }
                metodo_pago: { type: string, enum: [efectivo, transferencia, tarjeta, bizum, domiciliacion, otro] }
                pagado: { type: boolean, default: false }
                fecha_pago: { type: string, format: date }
                num_factura_proveedor: { type: string }
                tipo_justificante: { type: string, enum: [factura, simplificada, otros] }
                proyecto_id: { type: string, format: uuid }
                contacto_id: { type: string, format: uuid }
                notas: { type: string }
      responses:
        "201": { description: Creado }
  /gastos/{id}:
    get:
      tags: [Gastos]
      summary: Un gasto
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
  /cobros:
    get:
      tags: [Cobros]
      summary: Listar cobros
      parameters:
        - { name: desde, in: query, schema: { type: string, format: date } }
        - { name: hasta, in: query, schema: { type: string, format: date } }
        - { name: factura_id, in: query, schema: { type: string, format: uuid } }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Lista" } } } }
    post:
      tags: [Cobros]
      summary: Registrar un cobro
      description: Si los cobros cubren el total de la factura, ésta pasa a `cobrada` (mismo criterio que la pantalla de Cobros). Un cobro que supere el total se rechaza con 409.
      parameters: [ { $ref: "#/components/parameters/idempotencia" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [importe]
              properties:
                importe: { type: number }
                factura_id: { type: string, format: uuid }
                fecha: { type: string, format: date }
                metodo: { type: string, enum: [efectivo, transferencia, tarjeta, bizum, cheque, otro], default: transferencia }
                tipo: { type: string, enum: [anticipo, reserva, parcial, liquidacion], default: parcial }
                concepto: { type: string }
                proyecto_id: { type: string, format: uuid }
                persona: { type: string }
                notas: { type: string }
      responses:
        "201": { description: Creado }
        "409": { description: La factura está en borrador/anulada o el cobro supera su total }
  /proyectos:
    get:
      tags: [Proyectos]
      summary: Listar proyectos y obras
      parameters:
        - { name: estado, in: query, schema: { type: string, enum: [activo, pausado, completado, cancelado] } }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Lista" } } } }
  /proyectos/{id}:
    get:
      tags: [Proyectos]
      summary: Un proyecto
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
  parameters:
    id:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    limit:
      name: limit
      in: query
      schema: { type: integer, default: 100, maximum: 500 }
    offset:
      name: offset
      in: query
      schema: { type: integer, default: 0 }
    idempotencia:
      name: Idempotency-Key
      in: header
      description: Clave única por operación. Si se repite, ERPIA devuelve la misma respuesta sin volver a escribir (cabecera `Idempotent-Replayed`).
      schema: { type: string, maxLength: 200 }
  responses:
    NoAutorizado: { description: API key inválida o revocada }
    Prohibido: { description: Scope insuficiente o función del plan de pago }
    NoEncontrado: { description: No existe o no pertenece a esta cuenta }
    Peticion: { description: Petición inválida (el cuerpo dice qué campo) }
  schemas:
    Lista:
      type: object
      properties:
        data: { type: array, items: { type: object } }
        count: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
    CategoriaGasto:
      type: string
      enum: [material, herramienta, combustible, subcontrata, seguro, suministro, transporte, formacion, alimentacion, oficina, otro, arrendamiento, publicidad, banca]
    Linea:
      type: object
      required: [descripcion, precio]
      properties:
        descripcion: { type: string }
        detalle: { type: string }
        cantidad: { type: number, default: 1 }
        precio: { type: number, description: Precio unitario sin IVA }
        unidad: { type: string, default: UD }
        iva: { type: number, description: "% de IVA de la línea; si falta, el del documento" }
        articulo_id: { type: string, format: uuid }
    NuevaFactura:
      type: object
      required: [lineas]
      properties:
        tipo: { type: string, enum: [factura, presupuesto], default: factura }
        contacto_id: { type: string, format: uuid, description: Rellena los datos del cliente desde la ficha }
        nombre_cliente: { type: string, description: Obligatorio si no hay contacto_id }
        nif_cliente: { type: string }
        email_cliente: { type: string }
        telefono_cliente: { type: string }
        direccion_cliente: { type: string }
        cp_cliente: { type: string }
        ciudad_cliente: { type: string }
        provincia_cliente: { type: string }
        lineas: { type: array, items: { $ref: "#/components/schemas/Linea" } }
        porcentaje_iva: { type: number, default: 21 }
        porcentaje_irpf: { type: number, default: 0 }
        fecha_emision: { type: string, format: date }
        fecha_vencimiento: { type: string, format: date }
        titulo: { type: string }
        observaciones: { type: string }
        notas_internas: { type: string }
        metodo_pago: { type: string, enum: [efectivo, transferencia, tarjeta, bizum, cheque, otro] }
        proyecto_id: { type: string, format: uuid }
        serie: { type: string, description: Prefijo de la serie (por defecto F) }
        emitir: { type: boolean, default: false, description: Solo facturas. true = numerar y sellar al crearla }
    FacturaResumen:
      type: object
      properties:
        id: { type: string, format: uuid }
        numero_factura: { type: string }
        tipo_factura: { type: string }
        estado: { type: string }
        fecha_emision: { type: string }
        total: { type: number }
        inalterable: { type: boolean }
        url_publica: { type: string, nullable: true }
    NuevoContacto:
      type: object
      properties:
        nombre: { type: string }
        nif: { type: string }
        email: { type: string }
        telefono: { type: string }
        direccion: { type: string }
        codigo_postal: { type: string }
        ciudad: { type: string }
        provincia: { type: string }
        tipo: { type: string, enum: [cliente, proveedor, ambos, acreedor, prospecto], default: cliente }
        notas: { type: string }
        persona_contacto: { type: string }
        web: { type: string }
        etiquetas: { type: array, items: { type: string } }
