{
  "_nota": "Esquema de campos del asistente: FUENTE ÚNICA de las reglas de validación (RNF-21). Lo consumen el wizard (validación en línea) y el backend (validación autoritativa). Cambiar una regla aquí la cambia en las dos capas. Lo visual (iconos, ayudas, ejemplos) vive en webapp/src/lib/wizard/fields.js. `unico: true` marca el campo con el que Saeplus resuelve esa entidad (mismo criterio que services/faltantes.js): dos filas con ese valor repetido colisionan al crear la empresa, así que se impide donde se escriben. `unicoCon: [...]` acota ese ámbito cuando Saeplus resuelve por varias columnas a la vez: el barrio no es único en la empresa, sino dentro de su departamento y su ciudad, porque es así como lo busca migracion_onboarding.php:74. Sin el ámbito, dos «CENTRO» en municipios distintos —que son dos sitios reales— se rechazaban como si fueran el mismo.",
  "_actualizado": "2026-08-13",
  "campos": {
    "company": [
      {
        "id": "country",
        "label": "País",
        "req": true
      },
      {
        "id": "document",
        "label": "Documento empresa",
        "req": true,
        "max": 15
      },
      {
        "id": "legalName",
        "label": "Razón social",
        "req": true,
        "max": 100,
        "patron": "^[A-Za-z0-9ÁÉÍÓÚÜÑáéíóúüñ @.,\\-/'()#+*&]+$",
        "patronAyuda": "Letras, números y . , - / ( ) @ # + * &"
      },
      {
        "id": "commercialName",
        "label": "Nombre comercial",
        "req": true,
        "max": 100,
        "patron": "^[A-Za-z0-9ÁÉÍÓÚÜÑáéíóúüñ @.,\\-/'()#+*&]+$",
        "patronAyuda": "Letras, números y . , - / ( ) @ # + * &"
      },
      {
        "id": "taxRate",
        "label": "Impuesto (%)",
        "req": true,
        "numeric": true
      },
      {
        "id": "currency",
        "label": "Moneda",
        "req": true,
        "max": 40
      },
      {
        "id": "currencySymbol",
        "label": "Abreviatura de moneda",
        "req": true,
        "max": 10
      },
      {
        "id": "phone",
        "label": "Teléfono",
        "req": true,
        "phone": true
      },
      {
        "id": "email",
        "label": "Correo electrónico",
        "req": true,
        "email": true
      },
      {
        "id": "extraInfo",
        "label": "Información adicional",
        "max": 100
      },
      {
        "id": "fiscalAddress",
        "label": "Dirección fiscal",
        "req": true,
        "max": 255
      },
      {
        "id": "logo",
        "label": "Logo de la empresa"
      }
    ],
    "franchises": [
      {
        "id": "name",
        "label": "Nombre de la franquicia",
        "req": true,
        "max": 50,
        "unico": true
      },
      {
        "id": "subscriberInitial",
        "label": "Inicial de abonados",
        "req": true,
        "exactLen": 2,
        "patron": "^[A-Za-z]{2}$",
        "patronAyuda": "dos letras (sin números, espacios ni tildes)",
        "unico": true
      },
      {
        "id": "orderInitial",
        "label": "Inicial de órdenes",
        "req": true,
        "exactLen": 2,
        "patron": "^[A-Za-z]{2}$",
        "patronAyuda": "dos letras (sin números, espacios ni tildes)",
        "unico": true
      },
      {
        "id": "address",
        "label": "Dirección",
        "max": 100
      },
      {
        "id": "extraInfo",
        "label": "Información adicional",
        "max": 255
      }
    ],
    "locations": [
      {
        "id": "franchise",
        "req": true
      },
      {
        "id": "department",
        "req": true
      },
      {
        "id": "zone",
        "req": true,
        "max": 50
      },
      {
        "id": "city",
        "req": true
      },
      {
        "id": "neighborhood",
        "req": true,
        "max": 50,
        "unico": true,
        "unicoCon": ["department", "city"],
        "label": "Sector o barrio"
      }
    ],
    "technologies": [
      {
        "id": "technology",
        "req": true,
        "max": 25,
        "unico": true,
        "label": "Nombre del tipo de servicio"
      },
      {
        "id": "protocol",
        "req": true
      }
    ],
    "services": [
      {
        "id": "serviceName",
        "label": "Nombre del plan",
        "req": true,
        "max": 300,
        "unico": true
      },
      {
        "id": "serviceType",
        "label": "Tipo de servicio",
        "req": true
      },
      {
        "id": "totalRate",
        "label": "Precio mensual",
        "req": true,
        "numeric": true
      },
      {
        "id": "taxStatus",
        "label": "Impuesto",
        "req": true
      },
      {
        "id": "classification",
        "label": "Clasificación"
      },
      {
        "id": "franchises",
        "label": "Franquicia donde aplica",
        "req": true,
        "arrayOf": true
      }
    ],
    "users": [
      {
        "id": "document",
        "label": "Documento",
        "req": true,
        "max": 20,
        "_nota": "general.persona.cedula es varchar(20). El tope de 11 era colombiano y rechazaba un RIF J-12345678-9.",
        "unico": true
      },
      {
        "id": "firstName",
        "label": "Nombre",
        "req": true,
        "max": 100
      },
      {
        "id": "lastName",
        "label": "Apellidos",
        "req": true,
        "max": 50
      },
      {
        "id": "username",
        "label": "Nombre de usuario",
        "req": true,
        "max": 10,
        "unico": true
      },
      {
        "id": "email",
        "label": "Correo",
        "email": true,
        "req": true,
        "unico": true
      },
      {
        "id": "phone",
        "label": "Teléfono",
        "phone": true,
        "req": true
      },
      {
        "id": "franchiseAccess",
        "label": "Acceso a franquicia",
        "arrayOf": true
      },
      {
        "id": "accessRoles",
        "label": "Roles de acceso",
        "choice": true,
        "req": true
      },
      {
        "id": "isSeller",
        "label": "¿Es vendedor?",
        "choice": true,
        "single": true,
        "bool": true
      },
      {
        "id": "isCollector",
        "label": "¿Es cobrador?",
        "choice": true,
        "single": true,
        "bool": true
      },
      {
        "id": "isTechnician",
        "label": "¿Es técnico?",
        "choice": true,
        "single": true,
        "bool": true
      },
      {
        "id": "isMobileUser",
        "label": "¿Usa la app móvil?",
        "choice": true,
        "single": true,
        "bool": true
      }
    ],
    "collectionsData": [
      {
        "id": "collector",
        "label": "Cobrador",
        "req": true
      },
      {
        "id": "office",
        "label": "Oficina de cobro",
        "req": true,
        "max": 30
      },
      {
        "id": "cashRegister",
        "label": "Nombre de caja",
        "req": true,
        "max": 30,
        "unico": true
      },
      {
        "id": "franchise",
        "label": "Franquicia",
        "req": true
      },
      {
        "id": "paymentMethodsAllowed",
        "label": "¿Cómo te pagan?",
        "req": true,
        "choice": true
      }
    ],
    "materials": [
      {
        "id": "name",
        "label": "Nombre del almacén",
        "req": true,
        "max": 30,
        "unico": true
      },
      {
        "id": "manager",
        "label": "Encargado",
        "req": true
      },
      {
        "id": "type",
        "label": "Tipo de almacén",
        "req": true,
        "choice": true,
        "single": true
      },
      {
        "id": "workGroup",
        "label": "Técnico o grupo de trabajo",
        "unico": true
      },
      {
        "id": "movementResponsible",
        "label": "Responsable de movimientos",
        "req": true,
        "arrayOf": true
      }
    ],
    "equipment": [
      {
        "id": "name",
        "label": "Nombre del almacén",
        "req": true,
        "max": 30,
        "unico": true
      },
      {
        "id": "franchises",
        "label": "Franquicia asociada",
        "req": true,
        "arrayOf": true
      },
      {
        "id": "movementResponsible",
        "label": "Responsable de movimientos",
        "req": true,
        "arrayOf": true
      }
    ],
    "parameters": [
      {
        "id": "TIPO_FACTURACION",
        "label": "Tipo de Facturación"
      },
      {
        "id": "PRO_I",
        "label": "Prorrateo de Instalación"
      },
      {
        "id": "PRO_C",
        "label": "Prorrateo de Corte"
      },
      {
        "id": "PRO_R",
        "label": "Prorrateo de Reconexión"
      },
      {
        "id": "CORREO_OBLIGATORIO",
        "label": "Correo electrónico obligatorio"
      },
      {
        "id": "TELEFONO_VACIO",
        "label": "Permitir Teléfono vacío"
      },
      {
        "id": "CREA_OR_INST_SIN_PAG",
        "label": "Crear Orden sin pago"
      },
      {
        "id": "HAB_DETENER_ORD_APP",
        "label": "Detener Orden"
      },
      {
        "id": "HAB_FIRMA_DIGITAL",
        "label": "Firma en Contrato Digital"
      },
      {
        "id": "HAB_ARCHIVO_ADJUNTO",
        "label": "Habilitar archivo adjunto"
      }
    ],
    "migration": [
      {
        "id": "archivo",
        "label": "Archivo de abonados"
      },
      {
        "id": "cuantos",
        "label": "¿Cuántos abonados tienes hoy?",
        "numeric": true
      },
      {
        "id": "origen",
        "label": "¿De dónde vienen tus datos?"
      },
      {
        "id": "legales"
      },
      {
        "id": "planes"
      },
      {
        "id": "zonas"
      },
      {
        "id": "equipo"
      },
      {
        "id": "abonados"
      }
    ]
  },
  "migracion": {
    "_nota": "Las columnas del archivo de abonados que Saeplus NO perdona: si vienen vacías o no casan, `migracion_onboarding.php` hace `return add_error(...)` y rechaza el ARCHIVO ENTERO, no esa fila. Por eso se exigen en las dos capas. El asistente ya las marcaba como críticas (`revision.js`), pero el portón solo las miraba si traían valor: un archivo con una de estas vacía pasaba nuestra validación y moría en el paso 17 de 17, con la empresa ya creada. Salió creando una empresa venezolana de verdad, primero con ESTRATO SOCIAL y después con GRUPO DE AFINIDAD.",
    "criticas": [
      "GRUPO DE AFINIDAD",
      "DEPARTAMENTO",
      "MUNICIPIO / CIUDAD",
      "BARRIO"
    ],
    "_notaEstrato": "El estrato es crítico igual, pero solo donde el país lo usa: en Venezuela no existe y el asistente no lo pregunta. Ahí lo rellena el servidor con el neutro de la ficha antes de comprobar, así que llega siempre puesto.",
    "criticasConEstratos": [
      "ESTRATO SOCIAL"
    ]
  }
}
