A raíz de la Resolución DGT-R-48-2016 del Ministerio de Hacienda de la República de Costa Rica, se vuelve necesario realizar una integración en los sistemas para poder ser parte de la Factura Electrónica.

¿Qué es Factura Electrónica? Si tomamos la definición del Ministerio de Hacienda se dice que: “es un documento comercial con efectos tributarios, generado, expresado y transmitido en formato electrónico.”

En este artículo se mostrará cómo conectarse a esta plataforma y enviar una factura digital.

Prerequisito

  • Debe ser un contribuyente activo registrado en el Ministerio de Hacienda de la República de Costa Rica.

  • Tener acceso al Portal de Administración Tributaria Virtual (ATV.).

  • En ATV. solicitar acceso al ambiente de Staging de Factura Electrónica y obtener el certificado de autenticación. Deberá recibir un correo electrónico en donde se indica los URLs de este ambiente.

  • Leer el documento de la Resolución DGT-R-48-2016.

  • Leer el documento de Anexos y Estructuras, particularmente el Anexo [2] y [3].

Autenticación y Autorización

De acuerdo a la documentación referida previamente el primer paso para conectarse a la plataforma es obtener un Open ID Connect (OIDC.) Token que funciona sobre el estándar de OAuth 2.0.

Para ello, en ATV. se consigna que la dirección del IDP. a emplear en el entorno de pruebas es:

https://idp.comprobanteselectronicos.go.cr/auth/realms/rut-stag/protocol/openid-connect

Inicialmente se debe definir cuál es el URI. del IDP., el CLIENT ID, el usuario (que corresponde a la identificación del contribuyente) y su credencial.

  private static final String IDP_URI = "https://idp.comprobanteselectronicos.go.cr/auth/realms/rut-stag/protocol/openid-connect";
  private static final String IDP_CLIENT_ID = "api-stag";
  private String usuario = "[email protected]";
  private String password = "miClave";

Posteriormente debemos conectarnos al IDP. (que se expone como un servicio REST) para obtener un ‘access token’ y un ‘refresh token’. Esto lo podemos lograr de la siguiente manera:

  Client client = ClientBuilder.newClient();
  WebTarget target = client.target(IDP_URI + "/token");
  Form form = new Form();
  form.param("grant_type", "password")
               .param("username", usuario)
               .param("password", password)
               .param("client_id", IDP_CLIENT_ID);
  Response response = target.request().post(Entity.form(form));

  //La respuesta debe ser un código 200. Debe considerarse el caso en que retorne un valor diferente
  // Indicador que se ha enviado un atributo incorrecto
  JsonObject responseJson = response.readEntity(JsonObject.class);

  // De la respuesta procedemos a leer el access y refresh token
  String acessToken = responseJson.getString("access_token");
  String refreshToken =responseJson.getString("refresh_token");

Preparación

Una vez que tenemos la parte de autenticación completa, debemos preparar el documento que deseamos enviar al Ministerio de Hacienda. Para esto será necesario crear un objeto JSON. que debe constar de:

  • Clave: que corresponde a la clave de 50 posiciones del documento electrónico (Factura, Tiquete, Nota de Débito o Nota de Crédito).

  • Fecha: fecha de emisión del documento, codificada en formato yyyy-MM-dd’T’HH:mm:ssZ como se define en http://tools.ietf.org/html/rfc3339#section-5.6.

  • Emisor: que sería el emisor del documento y que debe ser el mismo contribuyente que realiza el proceso de autenticación. En este ejemplo se ilustra el caso de una persona jurídica. Aunque se soportan otros tipos de identificación.

  • Receptor: es un atributo opcional que sería el receptor del documento.

  • CallbackUrl: es un atributo opcional que corresponde a un URL. para que el Ministerio de Hacienda envíe la respuesta de aceptación o rechazo, por medio de un mensaje JSON.

  • ComprobanteXML : es un campo obligatorio y de acuerdo a la documentación consiste del XML. del comprobante electrónico firmado por el emisor utilizando XAdES-EPES según lo define el estándar ETSI TS 101 903 v1.3.2 o superior. El texto del XML. debe convertirse a un byte array y codificarse en Base64. El mapa de caracteres a utilizar en el XML. y en la codificación Base64 es UTF8.

{
  "clave": "50601011600310112345600100010100000000011999999999",
  "fecha": "2016-01-01T00:00:00-0600",
  "emisor": {
    "tipoIdentificacion": "02",
    "numeroIdentificacion": "3101123456"
  },
  "receptor": {
    "tipoIdentificacion": "02",
    "numeroIdentificacion": "3101123456"
  },
  "comprobanteXml": "PD94bWwgdmVyc2lvbj0iMS4wIiA/Pg0KDQo8ZG9tYWluIHhtbG5zPSJ1cm46amJvc3M6ZG9tYWluOjQuMCI+DQogICAgPGV4dGVuc2lvbnM+DQogICAgICAgIDxleHRlbnNpb24gbW9kdWxlPSJvcmcuamJvc3MuYXMuY2x1c3RlcmluZy5pbmZpbmlzcGFuIi8+DQogICAgICAgIDxleHRlbnNpb24gbW9kdWxlPSJvcmcuamJvc3MuYXMuY2x1c3RlcmluZy5qZ3JvdXBzIi8+DQogICAgICAgIDxleHRlbnNpb24gbW9kdWxlPSJvcmcuamJvc3MuYXMuY29ubmVjdG9yIi8+DQogICAgICAgIDxleHRlbnNpb24gbW..."
}

Observemos cómo se puede crear este JSON., suponiendo que se recibe un objeto con los parámetros anteriores.

  // Crea el objeto recepción que se enviará a los APIs.
  Recepcion recepcion = new Recepcion();
  recepcion.setClave(param.getClave());
  recepcion.setFecha(param.getFecha());
  // Emisor
  Recepcion.Identificacion emisor = new Recepcion.Identificacion();
  emisor.setNumeroIdentificacion(param.getNumIdentificacionEmisor());
  emisor.setTipoIdentificacion(param.getTipoIdentificacionEmisor());
  recepcion.setEmisor(emisor);

  // Receptor (condicional)
  if (param.getNumIdentificacionReceptor() != null && param.getTipoIdentificacionReceptor() != null) {
     Recepcion.Identificacion receptor = new Recepcion.Identificacion();
     receptor.setNumeroIdentificacion(param.getNumIdentificacionReceptor());
     receptor.setTipoIdentificacion(param.getTipoIdentificacionReceptor());
     recepcion.setReceptor(receptor);
  }

  // Comprobante XML, el cuál —para este ejemplo— firmaremos con nuestro producto SignumOne.
  recepcion.setComprobanteXml(SignService.sign(param.getXML(), "XAdES_EPES"));

¿Cómo firmar en XAdES_EPES?

Existen muchas formas de firmar un documento en XAdES_EPES; en nuestro caso desarrollamos una solución de firma que soporta múltiples formatos.

La herramienta se llama SignumOne y abstrae toda la complejidad relacionada a criptográfica, XAdES, PAdES, CRL, OCSP, etc. Exponiendo un simple API REST que puede ser utilizado de una manera muy expedita.

Observemos el uso seguidamente.

  // Se define el URL del servicio de SignumOne HSM y los ID pertinentes
  private static final String SIGN_API_URL = "http://SERVER/signumone-hsm-api/resources/sign";
  private static final String APPLICATION_ID = "myId";
  private static final String USER_ID = "myUserId";

  // Invocamos el API., el signLevel en nuestro ejemplo sería "XAdES_EPES"
  public String sign(String xml, String signLevel) {
        String fileBytes = Base64Util.encode(xml.getBytes(StandardCharsets.UTF_8));
        JsonObjectBuilder builder = Json.createObjectBuilder();
        JsonObject signRequest = builder
                .add("application-id", APPLICATION_ID)
                .add("user-id", USER_ID)
                .add("sign-level", signLevel)
                .add("file-bytes", fileBytes)
                .build();

       Client client = ClientBuilder.newClient();
       WebTarget target = client.target(SIGN_API_URL);
       try {
         // Hacemos la invocación a SignumOne y obtenemos el XML. enviado, firmado, y codificado en Base64.
         Response response = target.request(MediaType.APPLICATION_JSON).put(Entity.json(signRequest));
         return response.readEntity(JsonObject.class).getString("file-bytes");

       } catch (Exception ex) {
            throw new IllegalStateException("Error al comunicarse con el Sign API", ex);
      }
    }

Si alguien desea evaluar el servicio que ofrece nuestra herramienta SignumOne se ofrece una opción de evaluación gratuita. Por favor contáctenos a [email protected] para poder brindar los accesos correspondientes.

Envío

Para enviar un documento electrónico al Ministerio de Hacienda se debe hacer uso de otro servicio REST., el cuál tiene su API. disponible aquí.

  private static final String URI = "https://api.comprobanteselectronicos.go.cr/recepcion-sandbox/v1/";

  Client client = ClientBuilder.newClient();
  WebTarget target = client.target(URI + "recepcion");
  Invocation.Builder request = target.request();

  // Se deberá brindar una cabecera (header) "Authorization" con el valor del access token obtenido anteriormente.
  request.header("Authorization", "Bearer " + getAccessToken());

  // Se envía un POST. con los datos del documento que deseamos registrar, observe que colocamos como  
  // atributo el objeto que configuramos en el apartado de 'Preparación'
  response = request.post(Entity.json(recepcion));

  switch (response.getStatus()) {
    case 202:
      // Éste código de retorno se da por recibido a la plataforma el documento. Posteriormente
      // debe validarse su estado de aceptación o rechazo. Es importante hacer notar que se
      // regresa un header "Location" que corresponde a un URL. donde se puede validar el
      // estado del documento, por ejemplo:
      // https://api.comprobanteselectronicos.go.cr/recepcion-sandbox/v1/recepcion/50601011600310112345600100010100000000011999999999/
      break;
    case 400:
      // Se da si se detecta un error en las validaciones, por ejemplo: si intento enviar más de una
      // vez un documento. El encabezado "X-Error-Cause" indica la causa del problema.
      String xErrorCause = response.getHeaderString("X-Error-Cause");
      LOG.log(Level.SEVERE, xErrorCause);
      break;
    }

Validación de Estado

Una vez que se ha enviado el documento a la plataforma, el API. nos indica que debemos emplear otro recurso para determinar si el documento fue aceptado o rechazado o si aún continua en estado de recibido.

  private static final String URI = "https://api.comprobanteselectronicos.go.cr/recepcion-sandbox/v1/";

  Client client = ClientBuilder.newClient();
   // En éste caso, clave corresponde a la clave del documento a validar
  WebTarget target = client.target(URI + "recepcion/" + clave);
  Invocation.Builder request = target.request();

  // Se debe brindar un header "Authorization" con el valor del access token obtenido anteriormente.
  request.header("Authorization", "Bearer " + getAccessToken());

  // Se envía un GET. para tomar el estado
  response = request.get();

  switch (response.getStatus()) {
    case 200:
      // Acá se debe procesar la respuesta para determinar si el atributo "ind-estado"
      // del JSON. de respuesta da por aceptado o rechazado el documento. Si no está
      // en ese estado se debe reintentar posteriormente.
      break;
    case 404:
      // Se presenta si no se localiza la clave brindada
      LOG.log(Level.SEVERE, "La clave no esta registrada");
      break;
    }

Desconexión

Por último, para hacer un logout del IDP.; se realiza una invocación a otro recurso.

  Client client = ClientBuilder.newClient();
  WebTarget target = client.target(IDP_URI + "/logout");

  // Los tokens son los obtenidos durante la fase de login inicial.
  Response response = target.request().header("Authorization", "Bearer " + getAccessToken())
               .post(Entity.form(new Form("refresh_token", getRefreshToken())));

Conclusión

Esperamos que esta pequeño artículo sirva de orientación para la implemetanción sobre Factura Electrónica desde la perspectiva de Java. No dude en contactarnos si desea soporte en el tema o explorar otros productos que ofrecemos.

Referencias

Autor

Gerardo Arroyo Arce

Chief Technology Officer.

  • Ingeniero en Software. ITCR.
  • Master en Computación en Informática. UCR.
  • Oracle Certified Expert, JEE 6 Web Services Developer.
  • Oracle Certified Professional, Java SE 7 Programmer.
  • Oracle WebLogic Server 12c Certified Implementation Specialist.

Flecha Roja Technologies, 2017.