From 146a0fb8b0e90f9196e569152f649baf60d6cc8f Mon Sep 17 00:00:00 2001
From: Joaquín Reñé <jrene@curisit.net>
Date: Tue, 07 Oct 2025 14:52:57 +0000
Subject: [PATCH] #4410 - Comments on classes

---
 securis/src/main/java/net/curisit/securis/GzipFilter.java                                 |   21 
 securis/src/main/java/net/curisit/securis/RestServicesApplication.java                    |   17 
 securis/src/main/java/net/curisit/securis/db/common/Metadata.java                         |   68 
 securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java           |  113 
 securis/src/main/java/net/curisit/securis/db/BlockedRequest.java                          |  123 
 securis/src/main/java/net/curisit/securis/db/User.java                                    |  586 +-
 securis/src/main/java/net/curisit/securis/db/Application.java                             |  167 
 securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java                        |   18 
 securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java                     |  179 
 securis/src/main/java/net/curisit/securis/services/ApplicationResource.java               |  405 +
 securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java          |   25 
 securis/src/main/java/net/curisit/securis/utils/CacheTTL.java                             |  248 +
 securis/src/main/java/net/curisit/securis/db/LicenseStatus.java                           |   67 
 securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java                |   20 
 securis/src/main/java/net/curisit/securis/services/LicenseResource.java                   |  549 +-
 securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java                  |   29 
 securis/src/main/java/net/curisit/securis/LicenseGenerator.java                           |   68 
 securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java                      |   24 
 securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java                    |  298 +
 securis/src/main/java/net/curisit/securis/AuthFilter.java                                 |   80 
 securis/src/main/java/net/curisit/securis/services/BasicServices.java                     |  189 
 securis/src/main/java/net/curisit/securis/db/Pack.java                                    |  822 ++-
 securis/src/main/java/net/curisit/securis/services/PackResource.java                      |  407 +
 securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java      |   33 
 securis/src/main/java/net/curisit/securis/db/License.java                                 |  940 +++-
 securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java                |   34 
 securis/src/main/java/net/curisit/securis/db/Organization.java                            |  340 +
 securis/src/main/java/net/curisit/securis/services/UserResource.java                      |  643 ++-
 securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java                         |   27 
 securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java                          |  112 
 securis/src/main/java/net/curisit/securis/beans/User.java                                 |   11 
 securis/src/main/java/net/curisit/securis/utils/EmailManager.java                         |  217 
 securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java     |   24 
 securis/src/main/java/net/curisit/securis/services/ApiResource.java                       |  840 ++--
 securis/src/main/java/net/curisit/securis/db/PackStatus.java                              |   61 
 securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java            |  372 +
 securis/src/main/java/net/curisit/securis/db/common/SystemParams.java                     |  190 
 securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java                     |  215 
 securis/src/main/java/net/curisit/securis/utils/Config.java                               |  186 
 securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java               |  146 
 securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java                    |   47 
 securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java             |  287 
 securis/src/main/java/net/curisit/securis/utils/TokenHelper.java                          |  187 
 securis/src/main/java/net/curisit/securis/db/Settings.java                                |  105 
 securis/src/main/java/net/curisit/securis/security/Securable.java                         |   24 
 securis/src/main/java/net/curisit/securis/db/LicenseHistory.java                          |  158 
 securis/src/main/java/net/curisit/securis/db/LicenseType.java                             |  302 +
 securis/src/main/java/net/curisit/securis/db/PackMetadata.java                            |  246 
 securis/src/main/java/net/curisit/securis/services/OrganizationResource.java              |  186 
 securis/src/main/resources/META-INF/persistence.xml                                       |    2 
 securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java                       |   31 
 securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java |   22 
 securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java                   |   19 
 securis/src/main/java/net/curisit/securis/DevFilter.java                                  |   29 
 securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java           |  175 
 securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java              |  257 
 securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java |  111 
 57 files changed, 7,451 insertions(+), 3,651 deletions(-)

diff --git a/securis/src/main/java/net/curisit/securis/AuthFilter.java b/securis/src/main/java/net/curisit/securis/AuthFilter.java
index 48acee4..28a2c7f 100644
--- a/securis/src/main/java/net/curisit/securis/AuthFilter.java
+++ b/securis/src/main/java/net/curisit/securis/AuthFilter.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis;
 
 import java.io.IOException;
@@ -17,16 +20,63 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+/**
+* AuthFilter
+* <p>
+* Simple authentication/role wrapper for development and lightweight scenarios.
+* If a request parameter <code>user</code> or a session attribute <code>user</code>
+* is present, this filter wraps the current request with a custom {@link Principal}
+* and an ad-hoc role. The role assignment is temporary and follows the rule:
+* <ul>
+* <li>user == "advance" → role "advance"</li>
+* <li>otherwise → role "normal"</li>
+* </ul>
+* If no user is present, the request continues unmodified.
+*
+* <p><b>Security note:</b> This filter trusts a user name coming from a request parameter,
+* which must not be used in production. Replace with a proper authentication mechanism
+* (e.g., JWT, container security, SSO) and derive roles from authoritative claims.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 6, 2025.
+*/
 @ApplicationScoped
 @WebFilter(urlPatterns = "/*")
 public class AuthFilter implements Filter {
 
     private static final Logger LOG = LogManager.getLogger(AuthFilter.class);
 
+    // ---------------------------------------------------------------------
+    // Lifecycle
+    // ---------------------------------------------------------------------
+    
+    /** 
+     * init<p>
+     * Filter initialization hook (unused). 
+     */
     @Override
     public void init(FilterConfig fc) throws ServletException {
     }
 
+    // ---------------------------------------------------------------------
+    // Filtering
+    // ---------------------------------------------------------------------
+
+
+    /**
+     * doFilter
+     * <p>
+     * If a user is detected (request param or session attribute), wrap the request to:
+     * <ul>
+     * <li>Expose a {@link Principal} with the provided username.</li>
+     * <li>Report a single role through {@link HttpServletRequest#isUserInRole(String)}.</li>
+     * </ul>
+     * Otherwise, pass-through.
+     *
+     * @param sr incoming request
+     * @param sr1 outgoing response
+     * @param fc filter chain
+     */
     @Override
     public void doFilter(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
         HttpServletRequest req = (HttpServletRequest) sr;
@@ -46,21 +96,46 @@
 
     }
 
+    /** 
+     * destroy<p>
+     * Filter destruction hook (unused). 
+     */
     @Override
     public void destroy() {
     }
 
+     // ---------------------------------------------------------------------
+     // Wrapper
+     // ---------------------------------------------------------------------
+
+     /**
+      * UserRoleRequestWrapper
+      * <p>
+      * Wrapper that overrides role checks and the user principal when a synthetic user is present.
+      */
     private class UserRoleRequestWrapper extends HttpServletRequestWrapper {
 
         private String role;
         private String user;
 
+        /**
+        * Constructor
+        * <p>
+        * @param role single role to expose via {@link #isUserInRole(String)}
+        * @param user user name to expose via {@link #getUserPrincipal()}
+        * @param request original request to wrap
+        */
         public UserRoleRequestWrapper(String role, String user, HttpServletRequest request) {
             super(request);
             this.role = role;
             this.user = user;
         }
 
+        /**
+        * isUserInRole
+        * <p>
+        * Returns {@code true} if the requested role equals the configured synthetic role.
+        */
         @Override
         public boolean isUserInRole(String role) {
             LOG.info("isUserRole METHOD: {}, {}", role, this.role);
@@ -70,6 +145,11 @@
             return this.role.equals(role);
         }
 
+        /**
+        * getUserPrincipal
+        * <p>
+        * Returns a minimal {@link Principal} with the configured user name; delegates otherwise.
+        */
         @Override
         public Principal getUserPrincipal() {
             if (this.user == null) {
diff --git a/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java b/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
index 7726c7d..6605752 100644
--- a/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
+++ b/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis;
 
 import jakarta.persistence.EntityManager;
@@ -17,18 +20,49 @@
 import net.curisit.securis.services.exception.SeCurisServiceException;
 import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes;
 
+/**
+* DefaultExceptionHandler
+* <p>
+* JAX-RS {@link ExceptionMapper} that normalizes error responses across the API.
+* It also makes a best-effort to rollback and close a request-scoped {@link EntityManager}
+* if still open.
+*
+* <p>Response strategy:
+* <ul>
+* <li>{@link ForbiddenException} → 401 UNAUTHORIZED with app-specific error headers.</li>
+* <li>{@link SeCurisServiceException} → 418 (custom) with app error headers.</li>
+* <li>Other exceptions → 500 with generic message and request context logging.</li>
+* </ul>
+*
+* Headers:
+* <ul>
+* <li>{@code X-SECURIS-ERROR-MSG}</li>
+* <li>{@code X-SECURIS-ERROR-CODE}</li>
+* </ul>
+*
+* @author JRA
+* Last reviewed by JRA on Oct 6, 2025.
+*/
 @Provider
 public class DefaultExceptionHandler implements ExceptionMapper<Exception> {
+	
 	private static final Logger LOG = LogManager.getLogger(DefaultExceptionHandler.class);
-
+	
+	/** Default status code used for application-defined errors. */
 	public static final int DEFAULT_APP_ERROR_STATUS_CODE = 418;
+	
+	/** Header name carrying a human-readable error message. */
 	public static final String ERROR_MESSAGE_HEADER = "X-SECURIS-ERROR-MSG";
+	
+	/** Header name carrying a symbolic application error code. */
 	public static final String ERROR_CODE_MESSAGE_HEADER = "X-SECURIS-ERROR-CODE";
 
+	/** Default constructor (logs instantiation). */
 	public DefaultExceptionHandler() {
 		LOG.info("Creating DefaultExceptionHandler ");
 	}
 
+	// Context objects injected by the runtime
 	@Context
 	HttpServletRequest request;
 	@Context
@@ -36,6 +70,12 @@
 	@Context
 	EntityManager em;
 
+	/**
+	* toResponse
+	* <p>
+	* Map a thrown exception to an HTTP {@link Response}, releasing the {@link EntityManager}
+	* if present.
+	*/
 	@Override
 	public Response toResponse(Exception e) {
 		releaseEntityManager();
@@ -57,6 +97,11 @@
 		return Response.serverError().header(ERROR_MESSAGE_HEADER, "Unexpected error: " + e.toString()).type(MediaType.APPLICATION_JSON).build();
 	}
 
+	/**
+	* releaseEntityManager
+	* <p>
+	* Best-effort cleanup: rollback active transaction (if joined) and close the {@link EntityManager}.
+	*/
 	private void releaseEntityManager() {
 		try {
 			if (em != null && em.isOpen()) {
diff --git a/securis/src/main/java/net/curisit/securis/DevFilter.java b/securis/src/main/java/net/curisit/securis/DevFilter.java
index 2ed1e4d..fe7b3f1 100644
--- a/securis/src/main/java/net/curisit/securis/DevFilter.java
+++ b/securis/src/main/java/net/curisit/securis/DevFilter.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis;
 
 import java.io.IOException;
@@ -16,6 +19,19 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+/**
+* DevFilter
+* <p>
+* Development-time CORS helper. Adds permissive CORS headers to allow front-end
+* resources (e.g. JS served from a different origin) to call the API.
+* Short-circuits <code>OPTIONS</code> preflight requests.
+*
+* <p><b>Security note:</b> This configuration is intentionally permissive and should be
+* restricted for production.
+* 
+* @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+*/
 @ApplicationScoped
 @WebFilter(urlPatterns = "/*")
 public class DevFilter implements Filter {
@@ -23,10 +39,19 @@
 	@SuppressWarnings("unused")
 	private static final Logger log = LogManager.getLogger(DevFilter.class);
 
+	/** 
+	 * init<p>
+	 * Filter init hook (unused). 
+	 */
 	@Override
 	public void init(FilterConfig fc) throws ServletException {
 	}
 
+	/**
+	* doFilter
+	* <p>
+	* Add CORS headers and pass through non-OPTIONS methods to the next filter.
+	*/
 	@Override
 	public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain fc) throws IOException, ServletException {
 		HttpServletRequest req = (HttpServletRequest) sreq;
@@ -44,6 +69,10 @@
 		}
 	}
 
+	/** 
+	 * destroy<p>
+	 * Filter destroy hook (unused). 
+	 */
 	@Override
 	public void destroy() {
 	}
diff --git a/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java b/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java
index 30556ea..85043a9 100644
--- a/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java
+++ b/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis;
 
 import java.util.Date;
@@ -9,10 +12,31 @@
 import net.curisit.securis.beans.SignedLicenseBean;
 import net.curisit.securis.utils.JsonUtils;
 
+/**
+* FreeLicenseGenerator
+* <p>
+* Helper to generate a signed FREE license (no expiration) for a given app and code.
+* 
+* @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+*/
 public class FreeLicenseGenerator {
 
+	/** Constant license type code for FREE licenses. */
 	public static final String FREE_LICENSE_TYPE = "FREE";
 	
+	/**
+	* generateLicense
+	* <p>
+	* Build and sign a FREE license using the default generator. Uses a <code>Date(-1)</code>
+	* sentinel as "no expiration".
+	*
+	* @param appName application name
+	* @param licCode license code
+	* @param metadata additional metadata to embed
+	* @return signed license bean wrapper
+	* @throws SeCurisException on generation/signature errors
+	*/
     public static SignedLicenseBean generateLicense(String appName, String licCode, Map<String, Object> metadata) throws SeCurisException  {
         SignedLicenseBean sl = null;
         RequestBean rb = new RequestBean();
@@ -24,6 +48,13 @@
         return sl;
     }
 
+    
+    /**
+     * Demo main
+     * 
+     * @param args
+     * @throws SeCurisException
+     */
     public static void main(String[] args) throws SeCurisException {
     	Map<String, Object> metadata = new HashMap<>();
     	metadata.put("max_docs", 2000);
diff --git a/securis/src/main/java/net/curisit/securis/GzipFilter.java b/securis/src/main/java/net/curisit/securis/GzipFilter.java
index a8ceaee..9b153f2 100644
--- a/securis/src/main/java/net/curisit/securis/GzipFilter.java
+++ b/securis/src/main/java/net/curisit/securis/GzipFilter.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis;
 
 import java.io.IOException;
@@ -18,6 +21,12 @@
 
 import net.curisit.securis.utils.GZipServletResponseWrapper;
 
+/**
+* GzipFilter
+* <p>
+* Servlet filter that compresses <code>*.js</code> responses with GZIP when the client
+* advertises <code>Accept-Encoding: gzip</code>.
+*/
 @ApplicationScoped
 @WebFilter(urlPatterns = "*.js")
 public class GzipFilter implements Filter {
@@ -25,10 +34,16 @@
 	@SuppressWarnings("unused")
 	private static final Logger LOG = LogManager.getLogger(GzipFilter.class);
 
+	/** init<p>Filter init hook (unused). */
 	@Override
 	public void init(FilterConfig fc) throws ServletException {
 	}
 
+	/**
+	* doFilter
+	* <p>
+	* Wrap the response with a GZIP-compressing wrapper if supported by the client.
+	*/
 	@Override
 	public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain chain) throws IOException, ServletException {
 		HttpServletRequest httpRequest = (HttpServletRequest) sreq;
@@ -44,12 +59,18 @@
 		}
 	}
 
+	/**
+	* acceptsGZipEncoding
+	* <p>
+	* @return {@code true} when request header contains "gzip" in <code>Accept-Encoding</code>.
+	*/
 	private boolean acceptsGZipEncoding(HttpServletRequest httpRequest) {
 		String acceptEncoding = httpRequest.getHeader("Accept-Encoding");
 
 		return acceptEncoding != null && acceptEncoding.indexOf("gzip") != -1;
 	}
 
+	/** destroy<p>Filter destroy hook (unused). */
 	@Override
 	public void destroy() {
 	}
diff --git a/securis/src/main/java/net/curisit/securis/LicenseGenerator.java b/securis/src/main/java/net/curisit/securis/LicenseGenerator.java
index 558599a..419ba29 100644
--- a/securis/src/main/java/net/curisit/securis/LicenseGenerator.java
+++ b/securis/src/main/java/net/curisit/securis/LicenseGenerator.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis;
 
 import java.io.File;
@@ -24,23 +27,35 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import jakarta.inject.Singleton;
+
 /**
- * License generator and signer
- * 
- * @author roberto <roberto.sanchez@curisit.net>
- */
-@javax.inject.Singleton
+* LicenseGenerator
+* <p>
+* Factory for building and signing {@link LicenseBean} instances. Uses a process-wide
+* singleton and expects a PKCS#8 private key at:
+* <code>~/.SeCuris/keys/securis_private_key.pkcs8</code>.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
+@Singleton
 public class LicenseGenerator {
 
     private static final Logger LOG = LogManager.getLogger(LicenseGenerator.class);
 
     private static LicenseGenerator singleton = new LicenseGenerator();
 
+    /** 
+     * getInstance<p>
+     * Singleton accessor. 
+     */
     public static LicenseGenerator getInstance() {
         return singleton;
     }
 
     /**
+     * generateLicense<p>
      * Generate a license bean with the specified data
      * 
      * @param req
@@ -66,12 +81,14 @@
     }
 
     /**
-     * Generate a license file using a {@link LicenseBean}
-     * 
-     * @param license
-     * @param file
-     * @throws SeCurisException
-     */
+    * save
+    * <p>
+    * Persist a pretty-printed JSON representation of the signed license to disk.
+    *
+    * @param license source license
+    * @param file target file path
+    * @throws SeCurisException if serialization or IO fails
+    */
     public void save(LicenseBean license, File file) throws SeCurisException {
         SignedLicenseBean signedLic = new SignedLicenseBean(license);
         byte[] json;
@@ -91,15 +108,14 @@
     }
 
     /**
-     * 
-     * @param licBean
-     * @return
-     * @throws NoSuchAlgorithmException
-     * @throws IOException
-     * @throws InvalidKeySpecException
-     * @throws InvalidKeyException
-     * @throws SignatureException
-     */
+    * sign
+    * <p>
+    * Compute a Base64 signature for the given license and set it into the bean.
+    *
+    * @param licBean license to sign (in-place)
+    * @return Base64 signature string
+    * @throws SeCurisException if the signature process fails
+    */
     public String sign(LicenseBean licBean) throws SeCurisException {
         SignatureHelper sh = SignatureHelper.getInstance();
 
@@ -114,16 +130,8 @@
             byte[] signatureData = signature.sign();
             licBean.setSignature(Base64.encodeBase64String(signatureData));
             return licBean.getSignature();
-        } catch (NoSuchAlgorithmException e) {
-            LOG.error("Error signing license for " + licBean, e);
-        } catch (InvalidKeyException e) {
-            LOG.error("Error signing license for " + licBean, e);
-        } catch (InvalidKeySpecException e) {
-            LOG.error("Error signing license for " + licBean, e);
-        } catch (IOException e) {
-            LOG.error("Error signing license for " + licBean, e);
-        } catch (SignatureException e) {
-            LOG.error("Error signing license for " + licBean, e);
+        } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException | IOException | SignatureException e) {
+        	LOG.error("Error signing license for {}", licBean, e);
         }
         throw new SeCurisException("License could not be generated");
     }
diff --git a/securis/src/main/java/net/curisit/securis/RestServicesApplication.java b/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
index fc1c0b5..9c9f1e7 100644
--- a/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
+++ b/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis;
 
 import java.util.HashSet;
@@ -19,11 +22,25 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+/**
+* RestServicesApplication
+* <p>
+* JAX-RS application configuring the REST resource classes and interceptors.
+* Declares base path <code>/</code>.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @ApplicationPath("/")
 public class RestServicesApplication extends Application {
 
     private static final Logger LOG = LogManager.getLogger(RestServicesApplication.class);
 
+    /**
+    * getClasses
+    * <p>
+    * @return set of REST endpoints and filters to be registered by the runtime
+    */
     @Override
     public Set<Class<?>> getClasses() {
         Set<Class<?>> classes = new HashSet<>();
diff --git a/securis/src/main/java/net/curisit/securis/beans/User.java b/securis/src/main/java/net/curisit/securis/beans/User.java
index 3b8a4f8..92abfb9 100644
--- a/securis/src/main/java/net/curisit/securis/beans/User.java
+++ b/securis/src/main/java/net/curisit/securis/beans/User.java
@@ -1,5 +1,16 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.beans;
 
+/**
+* User
+* <p>
+* Placeholder bean for a system user. Intentionally empty in this snapshot.
+* Extend with fields (username, roles, etc.) and proper JSON/JPA annotations as needed.
+*
+* Note: Kept as-is to preserve current behavior.
+*/
 public class User {
 
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/Application.java b/securis/src/main/java/net/curisit/securis/db/Application.java
index 138481f..bc57809 100644
--- a/securis/src/main/java/net/curisit/securis/db/Application.java
+++ b/securis/src/main/java/net/curisit/securis/db/Application.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -30,9 +33,25 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- * Entity implementation class for Entity: application
- * 
- */
+* Application
+* <p>
+* JPA entity that represents an application registered in the licensing server.
+* Includes descriptive fields and relationships to <code>LicenseType</code>,
+* <code>ApplicationMetadata</code> and <code>User</code>.
+*
+* Mapping details:
+* <ul>
+* <li>Table: <code>application</code></li>
+* <li>Named queries: <code>list-applications</code>, <code>list-applications-by_ids</code></li>
+* <li>Relationships:
+* <ul>
+* <li><code>@OneToMany</code> <b>licenseTypes</b> (mappedBy="application")</li>
+* <li><code>@OneToMany</code> <b>metadata</b> with cascade PERSIST/REMOVE/REFRESH</li>
+* <li><code>@ManyToMany</code> <b>users</b> via join table <code>user_application</code></li>
+* </ul>
+* </li>
+* </ul>
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @JsonIgnoreProperties(ignoreUnknown = true)
@@ -46,32 +65,57 @@
 
 	private static final long serialVersionUID = 1L;
 
+	// ------------------------------------------------------------------
+	// Columns
+	// ------------------------------------------------------------------
+
+
+	/** Surrogate primary key. */
 	@Id
 	@GeneratedValue
 	private Integer id;
 
+	/** Unique short code for the application (business identifier). */
 	private String code;
+	
+	/** Human-friendly application name. */
 	private String name;
+	
+	/** Optional description. */
 	private String description;
 
+	/** Default license file name suggested for downloads/exports. */
 	@Column(name = "license_filename")
 	@JsonProperty("license_filename")
 	private String licenseFilename;
 
+	/** Creation timestamp (server-side). */
 	@Column(name = "creation_timestamp")
 	@JsonProperty("creation_timestamp")
 	private Date creationTimestamp;
 
-	// We don't include the referenced entities to limit the size of each row at
-	// // the listing
+	// ----------------------- Relationships ---------------------------
+
+
+	/**
+	* License types attached to this application (ignored in default JSON to keep listings small).
+	* 
+	* We don't include the referenced entities to limit the size of each row at the listing
+	*/
 	@JsonIgnore
 	@OneToMany(fetch = FetchType.LAZY, mappedBy = "application")
 	private Set<LicenseType> licenseTypes;
 
+	/**
+	* Metadata key/value entries for this application.
+	*/
 	@OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "application")
 	@JsonManagedReference
 	private Set<ApplicationMetadata> metadata;
 
+	/**
+	* Users that have access/relationship with this application (ignored in JSON listings).
+	*/
 	@JsonIgnore
 	// We don't include the users to limit the size of each row a the listing
 	@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
@@ -80,73 +124,180 @@
 			inverseJoinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") })
 	private Set<User> users;
 
+	// ------------------------------------------------------------------
+	// Getters & setters
+	// ------------------------------------------------------------------
+
+	/** 
+	 * getId<p>
+	 * Return the primary key. 
+	 * 
+	 * @return id
+	 */
 	public Integer getId() {
 		return id;
 	}
 
+	/**
+	 * setId<p>
+	 * Set the primary key
+	 * 
+	 * @param id
+	 */
 	public void setId(Integer id) {
 		this.id = id;
 	}
 
+	/**
+	 * getName<p>
+	 * Get the name
+	 * 
+	 * @return name
+	 */
 	public String getName() {
 		return name;
 	}
 
+	/**
+	 * setName<p>
+	 * Set the name
+	 * 
+	 * @param name
+	 */
 	public void setName(String name) {
 		this.name = name;
 	}
 
+	/**
+	 * getDescription<p>
+	 * Get the description
+	 * 
+	 * @return description
+	 */
 	public String getDescription() {
 		return description;
 	}
 
+	/**
+	 * setDescription<p>
+	 * Set the description
+	 * 
+	 * @param description
+	 */
 	public void setDescription(String description) {
 		this.description = description;
 	}
 
+	/**
+	 * getCreationTimestamp<p>
+	 * Get the creation timestamp
+	 * 
+	 * @return creationTimestamp
+	 */
 	public Date getCreationTimestamp() {
 		return creationTimestamp;
 	}
 
+	/**
+	 * setCreationTimestamp<p>
+	 * Set the creation timestamp
+	 * 
+	 * @param creationTimestamp
+	 */
 	public void setCreationTimestamp(Date creationTimestamp) {
 		this.creationTimestamp = creationTimestamp;
 	}
 
+	/**
+	 * getApplicationMetadata<p>
+	 * Set the application metadata
+	 * 
+	 * @return appMetadata
+	 */
 	@JsonProperty("metadata")
 	public Set<ApplicationMetadata> getApplicationMetadata() {
 		return metadata;
 	}
 
+	/**
+	 * setApplicationMetadata<p>
+	 * Set the application metadata
+	 * 
+	 * @param metadata
+	 */
 	@JsonProperty("metadata")
 	public void setApplicationMetadata(Set<ApplicationMetadata> metadata) {
 		this.metadata = metadata;
 	}
 
+	/**
+	 * getLicenseFilename<p>
+	 * Get the license file name
+	 * 
+	 * @return licenseFilename
+	 */
 	public String getLicenseFilename() {
 		return licenseFilename;
 	}
 
+	/**
+	 * setLicenseFilename<p>
+	 * Set the license file name
+	 * 
+	 * @param licenseFilename
+	 */
 	public void setLicenseFilename(String licenseFilename) {
 		this.licenseFilename = licenseFilename;
 	}
 
+	/**
+	 * getLicenseTypes<p>
+	 * Get the license types
+	 * 
+	 * @return licenseTypes
+	 */
 	public Set<LicenseType> getLicenseTypes() {
 		LOG.info("Getting list license types!!!!");
 		return licenseTypes;
 	}
 
+	/**
+	 * setLicenseTypes<p>
+	 * Set the license types
+	 * 
+	 * @param licenseTypes
+	 */
 	public void setLicenseTypes(Set<LicenseType> licenseTypes) {
 		this.licenseTypes = licenseTypes;
 	}
 
+	/**
+	 * getCode<p>
+	 * Get the application code
+	 * 
+	 * @return code
+	 */
 	public String getCode() {
 		return code;
 	}
 
+	/**
+	 * setCode<p>
+	 * Set the application code
+	 * 
+	 * @param code
+	 */
 	public void setCode(String code) {
 		this.code = code;
 	}
 
+	/**
+	 * equals<p>
+	 * Compares the current object with the given object
+	 * 
+	 * @param object
+	 * @return isEquals
+	 */
 	@Override
 	public boolean equals(Object obj) {
 		if (!(obj instanceof Application))
@@ -155,6 +306,12 @@
 		return id.equals(other.id);
 	}
 
+	/**
+	 * hashCode<p>
+	 * Get the object hashCode
+	 * 
+	 * @param hashCode
+	 */
 	@Override
 	public int hashCode() {
 
diff --git a/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java b/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java
index eafd418..5b2c982 100644
--- a/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -26,97 +29,179 @@
 import net.curisit.securis.db.common.Metadata;
 
 /**
- * Entity implementation class for Entity: application_metadata
- * 
- */
+* ApplicationMetadata
+* <p>
+* Single metadata entry (key/value/mandatory) attached to an {@link Application}.
+* Uses a composite PK: (application_id, key).
+* <p>
+* Mapping details:
+* - Table: application_metadata
+* - PK: application_id + key (two @Id fields).
+* - application: @ManyToOne with @JsonBackReference to avoid JSON cycles.
+* - creation_timestamp exposed as "creation_timestamp".
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @Entity
 @Table(name = "application_metadata")
 @JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-application-metadata", query = "SELECT a FROM ApplicationMetadata a where a.application.id = :applicationId") })
+@NamedQueries({
+    @NamedQuery(name = "list-application-metadata",
+                query = "SELECT a FROM ApplicationMetadata a where a.application.id = :applicationId")
+})
 public class ApplicationMetadata implements Serializable, Metadata {
 
-	private static final Logger LOG = LogManager.getLogger(ApplicationMetadata.class);
+    private static final Logger LOG = LogManager.getLogger(ApplicationMetadata.class);
 
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-	@Id
-	@ManyToOne
-	@JoinColumn(name = "application_id")
-	@JsonBackReference
-	private Application application;
+    /** Part of PK: owning application. */
+    @Id
+    @ManyToOne
+    @JoinColumn(name = "application_id")
+    @JsonBackReference
+    private Application application;
 
-	@Id
-	@Column(name = "\"key\"")
-	private String key;
+    /** Part of PK: metadata key (quoted column name). */
+    @Id
+    @Column(name = "\"key\"")
+    private String key;
 
-	private String value;
+    /** Arbitrary metadata value. */
+    private String value;
 
-	private boolean mandatory;
+    /** Whether this key is required for the parent application. */
+    private boolean mandatory;
 
-	@Column(name = "creation_timestamp")
-	@JsonProperty("creation_timestamp")
-	private Date creationTimestamp;
+    /** Server-side creation timestamp. */
+    @Column(name = "creation_timestamp")
+    @JsonProperty("creation_timestamp")
+    private Date creationTimestamp;
 
-	public String getKey() {
-		return key;
-	}
+    // ---------------------------------------------------------------------
+    // Getters & setters
+    // ---------------------------------------------------------------------
 
-	public void setKey(String key) {
-		this.key = key;
-	}
+    /**
+    * getKey<p>
+    * Get the metadata key (PK part).
+    *
+    * @return key
+    */
+    public String getKey() { return key; }
 
-	public Application getApplication() {
-		LOG.info("Getting application from app metadata: {}", application);
-		return application;
-	}
+    /**
+    * setKey<p>
+    * Set the metadata key (PK part).
+    *
+    * @param key
+    */
+    public void setKey(String key) { this.key = key; }
 
-	public void setApplication(Application application) {
-		this.application = application;
-	}
+    /**
+    * getApplication<p>
+    * Get the owning application.
+    *
+    * @return application
+    */
+    public Application getApplication() {
+        LOG.info("Getting application from app metadata: {}", application);
+        return application;
+    }
 
-	public Date getCreationTimestamp() {
-		return creationTimestamp;
-	}
+    /**
+    * setApplication<p>
+    * Set the owning application (PK part).
+    *
+   * @param application
+    */
+    public void setApplication(Application application) { this.application = application; }
 
-	public void setCreationTimestamp(Date creationTimestamp) {
-		this.creationTimestamp = creationTimestamp;
-	}
+    /**
+    * getCreationTimestamp<p>
+    * Get the creation timestamp.
+    *
+    * @return creationTimestamp
+    */
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
-	public String getValue() {
-		return value;
-	}
+    /**
+    * setCreationTimestamp<p>
+    * Set the creation timestamp.
+    *
+    * @param creationTimestamp
+    */
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 
-	public void setValue(String value) {
-		this.value = value;
-	}
+    /**
+    * getValue<p>
+    * Get the metadata value.
+    *
+    * @return value
+    */
+    public String getValue() { return value; }
 
-	public boolean isMandatory() {
-		return mandatory;
-	}
+    /**
+    * setValue<p>
+    * Set the metadata value.
+    *
+    * @param value
+    */
+    public void setValue(String value) { this.value = value; }
 
-	public void setMandatory(boolean mandatory) {
-		this.mandatory = mandatory;
-	}
+    /**
+    * isMandatory<p>
+    * Whether this entry is required.
+    *
+    * @return mandatory
+    */
+    public boolean isMandatory() { return mandatory; }
 
-	@Override
-	public String toString() {
+    /**
+    * setMandatory<p>
+    * Mark this entry as required or optional.
+    *
+    * @param mandatory
+    */
+    public void setMandatory(boolean mandatory) { this.mandatory = mandatory; }
 
-		return String.format("AppMd (%s: %s)", this.key, value);
-	}
+    // ---------------------------------------------------------------------
+    // Object methods
+    // ---------------------------------------------------------------------
 
-	@Override
-	public boolean equals(Object obj) {
-		if (!(obj instanceof ApplicationMetadata))
-			return false;
-		ApplicationMetadata other = (ApplicationMetadata) obj;
-		return Objects.equals(key, other.key) && Objects.equals(application, other.application);
-	}
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
+    @Override
+    public String toString() { return String.format("AppMd (%s: %s)", this.key, value); }
 
-	@Override
-	public int hashCode() {
-		return Objects.hash(key, application);
-	}
+    /**
+     * equals<p>
+     * Compare the current object with the given object
+     * 
+     * @param object
+     * @return isEquals
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof ApplicationMetadata)) return false;
+        ApplicationMetadata other = (ApplicationMetadata) obj;
+        return Objects.equals(key, other.key) && Objects.equals(application, other.application);
+    }
 
+    /**
+     * hashCode<p>
+     * Get the object hashCode
+     * 
+     * @return hashCode
+     */
+    @Override
+    public int hashCode() { return Objects.hash(key, application); }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java b/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java
index 74f562b..c2fc300 100644
--- a/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java
+++ b/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -21,9 +24,20 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- * Entity implementation class for Entity: pack
- * 
- */
+* BlockedRequest
+* <p>
+* Persistent record marking a request (by hash) as blocked.
+* Primary key is the SHA-256 of the original request_data.
+* Useful to avoid replay/duplicate processing.
+*
+* Mapping details:
+* - Table: blocked_request
+* - PK: hash (SHA-256(request_data))
+* - Optional relation 'blockedBy' for auditing.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @Entity
@@ -33,69 +47,126 @@
 
     private static final long serialVersionUID = 1L;
 
+    /** Unique SHA-256 hash of {@link #requestData}. */
     @Id
     private String hash;
 
+    /** Original request payload. */
     @Column(name = "request_data")
     @JsonProperty("request_data")
     private String requestData;
 
+    /** Server-side creation timestamp. */
     @Column(name = "creation_timestamp")
     @JsonProperty("creation_timestamp")
     private Date creationTimestamp;
 
+    /** User who blocked this request (optional, auditing). */
     @JsonIgnore
     @ManyToOne
     @JoinColumn(name = "blocked_by")
     private User blockedBy;
 
-    public Date getCreationTimestamp() {
-        return creationTimestamp;
-    }
+    // ---------------------------------------------------------------------
+    // Getters & setters
+    // ---------------------------------------------------------------------
 
-    public void setCreationTimestamp(Date creationTimestamp) {
-        this.creationTimestamp = creationTimestamp;
-    }
+    /**
+    * getCreationTimestamp<p>
+    * Get the creation timestamp.
+    *
+    * @return creationTimestamp
+    */
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
+    /**
+    * setCreationTimestamp<p>
+    * Set the creation timestamp.
+    *
+    * @param creationTimestamp
+    */
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
+
+    /**
+    * equals<p>
+    * Identity based on primary key (hash).
+    */
     @Override
     public boolean equals(Object obj) {
-        if (!(obj instanceof BlockedRequest))
-            return false;
+        if (!(obj instanceof BlockedRequest)) return false;
         BlockedRequest other = (BlockedRequest) obj;
-        return (hash == null && other.hash == null) || hash.equals(other.hash);
+        return (hash == null && other.hash == null) || (hash != null && hash.equals(other.hash));
     }
 
+    /**
+    * hashCode<p>
+    * Hash based on primary key (hash).
+    */
     @Override
-    public int hashCode() {
+    public int hashCode() { return (hash == null ? 0 : hash.hashCode()); }
 
-        return (hash == null ? 0 : hash.hashCode());
-    }
+    /**
+    * getRequestData<p>
+    * Get the original serialized request data.
+    *
+    * @return requestData
+    */
+    public String getRequestData() { return requestData; }
 
-    public String getRequestData() {
-        return requestData;
-    }
-
+    /**
+    * setRequestData<p>
+    * Set the original request data and recompute the PK hash immediately.
+    * Hash is computed as SHA-256 over the request string.
+    *
+    * @param requestData
+    */
     public void setRequestData(String requestData) {
         this.requestData = requestData;
         this.hash = generateHash(this.requestData);
     }
 
-    public User getBlockedBy() {
-        return blockedBy;
-    }
+    /**
+    * getBlockedBy<p>
+    * Return the user who blocked this request (if any).
+    *
+    * @return blockedBy
+    */
+    public User getBlockedBy() { return blockedBy; }
 
-    public void setBlockedBy(User blockedBy) {
-        this.blockedBy = blockedBy;
-    }
+    /**
+    * setBlockedBy<p>
+    * Set the user who blocked this request.
+    *
+    * @param blockedBy
+    */
+    public void setBlockedBy(User blockedBy) { this.blockedBy = blockedBy; }
 
+    // ---------------------------------------------------------------------
+    // Static helpers
+    // ---------------------------------------------------------------------
+
+    /**
+    * generateHash<p>
+    * Compute the SHA-256 hex string for the given request data.
+    *
+    * @param reqData
+    * @return sha256(reqData) or null if reqData is null
+    */
     public static String generateHash(String reqData) {
         return (reqData != null ? Utils.sha256(reqData) : null);
     }
 
+    /**
+    * isRequestBlocked<p>
+    * Check if a request payload is blocked by looking up its hash as the PK.
+    *
+    * @param requestData original payload
+    * @param em JPA entity manager
+    * @return true if an entry exists with the same hash
+    */
     public static boolean isRequestBlocked(String requestData, EntityManager em) {
         String hash = generateHash(requestData);
         BlockedRequest br = em.find(BlockedRequest.class, hash);
         return br != null;
     }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/License.java b/securis/src/main/java/net/curisit/securis/db/License.java
index 8b36055..d71759d 100644
--- a/securis/src/main/java/net/curisit/securis/db/License.java
+++ b/securis/src/main/java/net/curisit/securis/db/License.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -41,438 +44,677 @@
 import net.curisit.securis.services.exception.SeCurisServiceException;
 
 /**
- * Entity implementation class for Entity: license
- * 
- */
+* License
+* <p>
+* Main license entity. Contains identity, ownership, timestamps and payload fields.
+* Includes convenience JSON properties for related IDs/names.
+*
+* Mapping details:
+* - Table: license
+* - Listeners: CreationTimestampListener, ModificationTimestampListener
+* - Named queries: license-by-code, license-by-activation-code, last-code-suffix-used-in-pack, ...
+* - Status column uses custom Hibernate type: net.curisit.securis.db.common.LicenseStatusType
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @Entity
 @EntityListeners({ CreationTimestampListener.class, ModificationTimestampListener.class })
 @Table(name = "license")
 @JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "license-by-code", query = "SELECT l FROM License l where l.code = :code"),
-		@NamedQuery(name = "license-by-activation-code", query = "SELECT l FROM License l where l.activationCode = :activationCode"),
-		@NamedQuery(name = "last-code-suffix-used-in-pack", query = "SELECT max(l.codeSuffix) FROM License l where l.pack.id = :packId"),
-		@NamedQuery(name = "list-licenses-by-pack", query = "SELECT l FROM License l where l.pack.id = :packId"),
-		@NamedQuery(name = "list-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash"),
-		@NamedQuery(name = "list-active-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash and l.status in ('AC', 'PA')"),
-		@NamedQuery(name = "list-valid-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash and l.status in ('RE', 'AC', 'PA')")
-
+@NamedQueries({
+    @NamedQuery(name = "license-by-code", query = "SELECT l FROM License l where l.code = :code"),
+    @NamedQuery(name = "license-by-activation-code", query = "SELECT l FROM License l where l.activationCode = :activationCode"),
+    @NamedQuery(name = "last-code-suffix-used-in-pack", query = "SELECT max(l.codeSuffix) FROM License l where l.pack.id = :packId"),
+    @NamedQuery(name = "list-licenses-by-pack", query = "SELECT l FROM License l where l.pack.id = :packId"),
+    @NamedQuery(name = "list-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash"),
+    @NamedQuery(name = "list-active-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash and l.status in ('AC', 'PA')"),
+    @NamedQuery(name = "list-valid-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash and l.status in ('RE', 'AC', 'PA')")
 })
 public class License implements CreationTimestampEntity, ModificationTimestampEntity, Serializable {
 
-	private static final long serialVersionUID = 2700310404904877227L;
+    private static final long serialVersionUID = 2700310404904877227L;
 
-	private static final Logger LOG = LogManager.getLogger(License.class);
+    private static final Logger LOG = LogManager.getLogger(License.class);
 
-	@Id
-	@GeneratedValue
-	private Integer id;
+    // ------------------------------------------------------------------
+    // Columns & relations
+    // ------------------------------------------------------------------
 
-	private String code;
+    @Id
+    @GeneratedValue
+    private Integer id;
 
-	@Column(name = "metadata_obsolete")
-	@JsonProperty("metadata_obsolete")
-	private Boolean metadataObsolete;
+    private String code;
 
-	@Column(name = "activation_code")
-	@JsonProperty("activation_code")
-	private String activationCode;
+    @Column(name = "metadata_obsolete")
+    @JsonProperty("metadata_obsolete")
+    private Boolean metadataObsolete;
 
-	@Column(name = "code_suffix")
-	@JsonProperty("code_suffix")
-	private Integer codeSuffix;
+    @Column(name = "activation_code")
+    @JsonProperty("activation_code")
+    private String activationCode;
 
-	@JsonIgnore
-	@ManyToOne
-	@JoinColumn(name = "pack_id")
-	private Pack pack;
+    @Column(name = "code_suffix")
+    @JsonProperty("code_suffix")
+    private Integer codeSuffix;
 
-	@JsonIgnore
-	@ManyToOne
-	@JoinColumn(name = "created_by")
-	private User createdBy;
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "pack_id")
+    private Pack pack;
 
-	@JsonIgnore
-	@ManyToOne
-	@JoinColumn(name = "cancelled_by")
-	private User cancelledBy;
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "created_by")
+    private User createdBy;
 
-	@Type(type = "net.curisit.securis.db.common.LicenseStatusType")
-	private LicenseStatus status;
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "cancelled_by")
+    private User cancelledBy;
 
-	@Column(name = "full_name")
-	@JsonProperty("full_name")
-	private String fullName;
+    @Type(type = "net.curisit.securis.db.common.LicenseStatusType")
+    private LicenseStatus status;
 
-	private String email;
+    @Column(name = "full_name")
+    @JsonProperty("full_name")
+    private String fullName;
 
-	@Column(name = "request_data")
-	@JsonProperty("request_data")
-	private String requestData;
+    private String email;
 
-	/**
-	 * request data hash is automatically set when we use
-	 * {@link License#setRequestData(String)} method
-	 */
-	@Column(name = "request_data_hash")
-	@JsonIgnore
-	private String reqDataHash;
+    @Column(name = "request_data")
+    @JsonProperty("request_data")
+    private String requestData;
 
-	@Column(name = "license_data")
-	@JsonProperty("license_data")
-	@JsonIgnore
-	// The license data is sent to user as a separate file, It doesn't need to
-	// be included as License attribute on browser
-	private String licenseData;
+    /**
+    * Request data hash (not serialized). Automatically updated by setRequestData().
+    */
+    @Column(name = "request_data_hash")
+    @JsonIgnore
+    private String reqDataHash;
 
-	@Column(name = "creation_timestamp")
-	@JsonProperty("creation_timestamp")
-	private Date creationTimestamp;
+    @Column(name = "license_data")
+    @JsonProperty("license_data")
+    @JsonIgnore
+    // License data is delivered separately (e.g., file download). Not sent in list views.
+    private String licenseData;
 
-	@Column(name = "modification_timestamp")
-	@JsonProperty("modification_timestamp")
-	private Date modificationTimestamp;
+    @Column(name = "creation_timestamp")
+    @JsonProperty("creation_timestamp")
+    private Date creationTimestamp;
 
-	@Column(name = "last_access_timestamp")
-	@JsonProperty("last_access_timestamp")
-	private Date lastAccessTimestamp;
+    @Column(name = "modification_timestamp")
+    @JsonProperty("modification_timestamp")
+    private Date modificationTimestamp;
 
-	@Column(name = "expiration_date")
-	@JsonProperty("expiration_date")
-	private Date expirationDate;
+    @Column(name = "last_access_timestamp")
+    @JsonProperty("last_access_timestamp")
+    private Date lastAccessTimestamp;
 
-	private String comments;
+    @Column(name = "expiration_date")
+    @JsonProperty("expiration_date")
+    private Date expirationDate;
 
-	@OneToMany(fetch = FetchType.LAZY, mappedBy = "license")
-	@JsonIgnore
-	private List<LicenseHistory> history;
+    private String comments;
 
-	public Integer getId() {
-		return id;
-	}
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "license")
+    @JsonIgnore
+    private List<LicenseHistory> history;
 
-	public String getCode() {
-		return code;
-	}
+    // ------------------------------------------------------------------
+    // Basic accessors
+    // ------------------------------------------------------------------
 
-	public void setCode(String code) {
-		this.code = code;
-	}
+    /** 
+     * getId<p>
+     * Return primary key. 
+     * 
+     * @return id
+     */
+    public Integer getId() { return id; }
 
-	@Override
-	public Date getCreationTimestamp() {
-		return creationTimestamp;
-	}
+    /** 
+     * getCode<p>
+     * Return human-readable license code. 
+     * 
+     * @return code
+     */
+    public String getCode() { return code; }
 
-	@Override
-	public void setCreationTimestamp(Date creationTimestamp) {
-		this.creationTimestamp = creationTimestamp;
-	}
+    /** 
+     * setCode<p>
+     * Set human-readable license code. 
+     * 
+     * @param code
+     */
+    public void setCode(String code) { this.code = code; }
 
-	public User getCreatedBy() {
-		return createdBy;
-	}
+    /** 
+     * getCreationTimestamp<p>
+     * Required by CreationTimestampEntity. 
+     * 
+     * @return creationTimestamp
+     */
+    @Override
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
-	public void setCreatedBy(User createdBy) {
-		this.createdBy = createdBy;
-	}
+    /** 
+     * setCreationTimestamp<p>
+     * Set creation timestamp. 
+     * 
+     * @param creationTimestamp
+     */
+    @Override
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 
-	public Pack getPack() {
-		return pack;
-	}
+    /** 
+     * getCreatedBy<p>
+     * Return creator user (entity). 
+     * 
+     * @return user
+     */
+    public User getCreatedBy() { return createdBy; }
 
-	public void setPack(Pack pack) {
-		this.pack = pack;
-	}
+    /** 
+     * setCreatedBy<p>
+     * Set creator user (entity). 
+     * 
+     * @param user
+     */
+    public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
 
-	@JsonProperty("created_by_id")
-	public String getCreatedById() {
-		return createdBy == null ? null : createdBy.getUsername();
-	}
+    /** 
+     * getPack<p>
+     * Return owning pack. 
+     * 
+     * @return pack
+     */
+    public Pack getPack() { return pack; }
 
-	@JsonProperty("created_by_id")
-	public void setCreatedById(String username) {
-		if (username == null) {
-			createdBy = null;
-		} else {
-			createdBy = new User();
-			createdBy.setUsername(username);
-		}
-	}
+    /** 
+     * setPack<p>
+     * Set owning pack.
+     * 
+     * @param pack
+     */
+    public void setPack(Pack pack) { this.pack = pack; }
 
-	@JsonProperty("cancelled_by_id")
-	public String getCancelledById() {
-		return cancelledBy == null ? null : cancelledBy.getUsername();
-	}
+    /** 
+     * getCreatedById<p>
+     * Expose creator username as JSON. 
+     * 
+     * @return username
+     */
+    @JsonProperty("created_by_id")
+    public String getCreatedById() { return createdBy == null ? null : createdBy.getUsername(); }
 
-	@JsonProperty("cancelled_by_id")
-	public void setCancelledById(String username) {
-		if (username == null) {
-			cancelledBy = null;
-		} else {
-			cancelledBy = new User();
-			cancelledBy.setUsername(username);
-		}
-	}
+    /** 
+     * setCreatedById<p>
+     * Setter by username for JSON binding. 
+     * 
+     * @param username
+     */
+    @JsonProperty("created_by_id")
+    public void setCreatedById(String username) {
+        if (username == null) {
+            createdBy = null;
+        } else {
+            createdBy = new User();
+            createdBy.setUsername(username);
+        }
+    }
 
-	@JsonProperty("pack_code")
-	public String getPackCode() {
-		return pack == null ? null : pack.getCode();
-	}
+    /** 
+     * getCancelledById<p>
+     * Expose cancelling user username as JSON. 
+     * 
+     * @return username
+     */
+    @JsonProperty("cancelled_by_id")
+    public String getCancelledById() { return cancelledBy == null ? null : cancelledBy.getUsername(); }
 
-	@JsonProperty("pack_id")
-	public Integer getPackId() {
-		return pack == null ? null : pack.getId();
-	}
+    /** 
+     * setCancelledById<p>
+     * Setter by username for JSON binding. 
+     * 
+     * @param username
+     */
+    @JsonProperty("cancelled_by_id")
+    public void setCancelledById(String username) {
+        if (username == null) {
+            cancelledBy = null;
+        } else {
+            cancelledBy = new User();
+            cancelledBy.setUsername(username);
+        }
+    }
 
-	@JsonProperty("pack_id")
-	public void setPackId(Integer idPack) {
-		if (idPack == null) {
-			pack = null;
-		} else {
-			pack = new Pack();
-			pack.setId(idPack);
-		}
-	}
+    /** 
+     * getPackCode<p>
+     * Expose pack code for convenience. 
+     * 
+     * @return packCode
+     */
+    @JsonProperty("pack_code")
+    public String getPackCode() { return pack == null ? null : pack.getCode(); }
 
-	public LicenseStatus getStatus() {
-		return status;
-	}
+    /** 
+     * getPackId<p>
+     * Expose pack id for convenience. 
+     * 
+     * @return packId
+     */
+    @JsonProperty("pack_id")
+    public Integer getPackId() { return pack == null ? null : pack.getId(); }
 
-	public void setStatus(LicenseStatus status) {
-		this.status = status;
-	}
+    /** 
+     * setPackId<p>
+     * Setter by id for JSON binding (creates a shallow Pack).
+     * 
+     * @param packId
+     */
+    @JsonProperty("pack_id")
+    public void setPackId(Integer idPack) {
+        if (idPack == null) {
+            pack = null;
+        } else {
+            pack = new Pack();
+            pack.setId(idPack);
+        }
+    }
 
-	@Override
-	public Date getModificationTimestamp() {
-		return modificationTimestamp;
-	}
+    /** 
+     * getStatus<p>
+     * Return license status. 
+     * 
+     * @return licenseStatus
+     */
+    public LicenseStatus getStatus() { return status; }
 
-	@Override
-	public void setModificationTimestamp(Date modificationTimestamp) {
-		this.modificationTimestamp = modificationTimestamp;
-	}
+    /** 
+     * setStatus<p>
+     * Set license status. 
+     * 
+     * @param status
+     */
+    public void setStatus(LicenseStatus status) { this.status = status; }
 
-	public String getFullName() {
-		return fullName;
-	}
+    /** 
+     * getModificationTimestamp<p>
+     * Required by ModificationTimestampEntity. 
+     * 
+     * @return modificationTimestamp
+     */
+    @Override
+    public Date getModificationTimestamp() { return modificationTimestamp; }
 
-	public void setFullName(String fullName) {
-		this.fullName = fullName;
-	}
+    /** 
+     * setModificationTimestamp<p>
+     * Set modification timestamp. 
+     * 
+     * @param modificationTimestamp
+     */
+    @Override
+    public void setModificationTimestamp(Date modificationTimestamp) { this.modificationTimestamp = modificationTimestamp; }
 
-	public String getEmail() {
-		return email;
-	}
+    /** 
+     * getFullName<p>
+     * Return license holder full name. 
+     * 
+     * @return name
+     */
+    public String getFullName() { return fullName; }
 
-	public void setEmail(String email) {
-		this.email = email;
-	}
+    /** 
+     * setFullName<p>
+     * Set license holder full name. 
+     * 
+     * @param name
+     */
+    public void setFullName(String fullName) { this.fullName = fullName; }
 
-	public void setId(Integer id) {
-		this.id = id;
-	}
+    /** 
+     * getEmail<p>
+     * Return email address. 
+     * 
+     * @return email
+     */
+    public String getEmail() { return email; }
 
-	public User getCancelledBy() {
-		return cancelledBy;
-	}
+    /** 
+     * setEmail<p>
+     * Set email address. 
+     * 
+     * @param email
+     */
+    public void setEmail(String email) { this.email = email; }
+
+    /** 
+     * setId<p>
+     * Set primary key (rarely used). 
+     * 
+     * @param id
+     */
+    public void setId(Integer id) { this.id = id; }
+
+    /** 
+     * getCancelledBy<p>
+     * Return cancelling user (entity). 
+     * 
+     * @param user
+     */
+    public User getCancelledBy() { return cancelledBy; }
+
+    /** 
+     * setCancelledBy<p>
+     * Set cancelling user (entity). 
+     * 
+     * @param cancelledBy
+     */
+    public void setCancelledBy(User cancelledBy) { this.cancelledBy = cancelledBy; }
 
-	public void setCancelledBy(User cancelledBy) {
-		this.cancelledBy = cancelledBy;
-	}
+    /** 
+     * getLastAccessTimestamp<p>
+     * Return last access timestamp. 
+     * 
+     * @return lastAccessTimestamp
+     */
+    public Date getLastAccessTimestamp() { return lastAccessTimestamp; }
 
-	public Date getLastAccessTimestamp() {
-		return lastAccessTimestamp;
-	}
+    /** 
+     * setLastAccessTimestamp<p>
+     * Set last access timestamp. 
+     * 
+     * @param lastAccessTimestamp
+     */
+    public void setLastAccessTimestamp(Date lastAccessTimestamp) { this.lastAccessTimestamp = lastAccessTimestamp; }
 
-	public void setLastAccessTimestamp(Date lastAccessTimestamp) {
-		this.lastAccessTimestamp = lastAccessTimestamp;
-	}
+    /** 
+     * getRequestData<p>
+     * Return raw request data. 
+     * 
+     * @return requestData
+     */
+    public String getRequestData() { return requestData; }
 
-	public String getRequestData() {
-		return requestData;
-	}
+    /**
+    * setRequestData<p>
+    * Set raw request data and recompute {@link #reqDataHash} immediately using
+    * the same hashing strategy as BlockedRequest (SHA-256).
+    *
+    * @param requestData
+    */
+    public void setRequestData(String requestData) {
+        this.requestData = requestData;
+        this.reqDataHash = BlockedRequest.generateHash(this.requestData);
+    }
 
-	public void setRequestData(String requestData) {
-		this.requestData = requestData;
-		this.reqDataHash = BlockedRequest.generateHash(this.requestData);
-	}
+    /** 
+     * getLicenseData<p>
+     * Return opaque license data (not serialized in lists). 
+     * 
+     * @return licenseData
+     */
+    public String getLicenseData() { return licenseData; }
 
-	public String getLicenseData() {
-		return licenseData;
-	}
+    /** 
+     * setLicenseData<p>
+     * Set opaque license data (large content kept server-side). 
+     * 
+     * @param licenseDate
+     */
+    public void setLicenseData(String licenseData) { this.licenseData = licenseData; }
 
-	public void setLicenseData(String licenseData) {
-		this.licenseData = licenseData;
-	}
+    /** 
+     * getComments<p>
+     * Return optional comments. 
+     * 
+     * @return comments
+     */
+    public String getComments() { return comments; }
 
-	public String getComments() {
-		return comments;
-	}
+    /** 
+     * setComments<p>
+     * Set optional comments. 
+     * 
+     * @param comments
+     */
+    public void setComments(String comments) { this.comments = comments; }
 
-	public void setComments(String comments) {
-		this.comments = comments;
-	}
+    /** 
+     * getHistory<p>
+     * Return change history entries (lazy). 
+     * 
+     * @return history
+     */
+    public List<LicenseHistory> getHistory() { return history; }
 
-	public List<LicenseHistory> getHistory() {
-		return history;
-	}
+    /** 
+     * setHistory<p>
+     * Set change history entries. 
+     * 
+     * @param history
+     */
+    public void setHistory(List<LicenseHistory> history) { this.history = history; }
 
-	public void setHistory(List<LicenseHistory> history) {
-		this.history = history;
-	}
+    /** 
+     * getExpirationDate<p>
+     * Return expiration date (nullable). 
+     * 
+     * @return expirationDate
+     */
+    public Date getExpirationDate() { return expirationDate; }
 
-	public Date getExpirationDate() {
-		return expirationDate;
-	}
+    /** 
+     * setExpirationDate<p>
+     * Set expiration date (nullable). 
+     * 
+     * @param expirationDate
+     */
+    public void setExpirationDate(Date expirationDate) { this.expirationDate = expirationDate; }
 
-	public void setExpirationDate(Date expirationDate) {
-		this.expirationDate = expirationDate;
-	}
+    /** 
+     * getReqDataHash<p>
+     * Return cached hash of request data (not exposed in JSON). 
+     * 
+     * @return reqDataHash
+     */
+    public String getReqDataHash() { return reqDataHash; }
 
-	public String getReqDataHash() {
-		return reqDataHash;
-	}
+    /** 
+     * getCodeSuffix<p>
+     * Return numeric suffix of the code. 
+     * 
+     * @return codeSuffix
+     */
+    public Integer getCodeSuffix() { return codeSuffix; }
 
-	public static class Action {
-		public static final int CREATE = 1;
-		public static final int REQUEST = 2;
-		public static final int ACTIVATION = 3;
-		public static final int SEND = 4;
-		public static final int DOWNLOAD = 5;
-		public static final int CANCEL = 6;
-		public static final int DELETE = 7;
-		public static final int BLOCK = 8;
-		public static final int UNBLOCK = 9;
-	}
+    /** 
+     * setCodeSuffix<p>
+     * Set numeric suffix of the code. 
+     * 
+     * @param codeSuffix
+     */
+    public void setCodeSuffix(Integer codeSuffix) { this.codeSuffix = codeSuffix; }
 
-	public static class Status {
+    /** 
+     * getActivationCode<p>
+     * Return activation code. 
+     * 
+     * @return activationCode
+     */
+    public String getActivationCode() { return activationCode; }
 
-		private static final Map<Integer, List<LicenseStatus>> transitions = Utils.createMap( //
-				Action.REQUEST, Arrays.asList(LicenseStatus.CREATED, LicenseStatus.REQUESTED), //
-				Action.ACTIVATION, Arrays.asList(LicenseStatus.CREATED, LicenseStatus.REQUESTED, LicenseStatus.PRE_ACTIVE, LicenseStatus.EXPIRED), //
-				Action.SEND, Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE), //
-				Action.DOWNLOAD, Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE), //
-				Action.CANCEL, Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE, LicenseStatus.REQUESTED, LicenseStatus.EXPIRED), //
-				Action.DELETE, Arrays.asList(LicenseStatus.CANCELLED, LicenseStatus.CREATED, LicenseStatus.BLOCKED), //
-				Action.UNBLOCK, Arrays.asList(LicenseStatus.BLOCKED), //
-				Action.BLOCK, Arrays.asList(LicenseStatus.CANCELLED) //
-		);
+    /** 
+     * setActivationCode<p>
+     * Set activation code. 
+     * 
+     * @param activationCode
+     */
+    public void setActivationCode(String activationCode) { this.activationCode = activationCode; }
 
-		/**
-		 * It checks if a given action is valid for the License, passing the
-		 * action and the current license status
-		 * 
-		 * @param oldStatus
-		 * @param newStatus
-		 * @return
-		 */
-		public static boolean isActionValid(Integer action, LicenseStatus currentStatus) {
-			List<LicenseStatus> validStatuses = transitions.get(action);
-			LOG.info("Action {} is valid ? => {} current: {} OK? {}", action, validStatuses, currentStatus, validStatuses.contains(currentStatus));
-			return validStatuses != null && validStatuses.contains(currentStatus);
-		}
-	}
+    /** 
+     * isMetadataObsolete<p>
+     * Convenience Boolean → primitive with null-safe false. 
+     * 
+     * @return isMetadataObsolete
+     */
+    public boolean isMetadataObsolete() { return metadataObsolete != null && metadataObsolete; }
 
-	/**
-	 * Return licenses with status: REquested, ACtive, Pre-Active for a given
-	 * request data
-	 * 
-	 * @param requestData
-	 * @param em
-	 * @return
-	 * @throws SeCurisServiceException
-	 */
-	public static License findValidLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException {
-		TypedQuery<License> query = em.createNamedQuery("list-valid-licenses-by-req-data", License.class);
-		query.setParameter("hash", BlockedRequest.generateHash(requestData));
-		try {
-			List<License> list = query.getResultList();
-			if (list.size() == 0) {
-				return null;
-			}
-			if (list.size() > 1) {
-				LOG.error("There are more than 1 active or requested license for request data: {}\nHash: {}", requestData, BlockedRequest.generateHash(requestData));
-			}
-			return list.get(0);
-		} catch (NoResultException e) {
-			// There is no license for request data
-			return null;
-		}
-	}
+    /** 
+     * setMetadataObsolete<p>
+     * Set metadata obsolete flag (nullable wrapper). 
+     * 
+     * @param obsolete
+     */
+    public void setMetadataObsolete(Boolean obsolete) { this.metadataObsolete = obsolete; }
 
-	/**
-	 * Return licenses with status: REquested, ACtive, Pre-Active for a given
-	 * request data
-	 * 
-	 * @param requestData
-	 * @param em
-	 * @return
-	 * @throws SeCurisServiceException
-	 */
-	public static License findLicenseByActivationCode(String activationCode, EntityManager em) throws SeCurisServiceException {
-		TypedQuery<License> query = em.createNamedQuery("license-by-activation-code", License.class);
-		query.setParameter("activationCode", activationCode);
-		try {
-			return query.getSingleResult();
-		} catch (NoResultException e) {
-			// There is no license for request data
-			return null;
-		}
-	}
+    // ------------------------------------------------------------------
+    // Status transitions helpers
+    // ------------------------------------------------------------------
 
-	public static License findActiveLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException {
-		TypedQuery<License> query = em.createNamedQuery("list-active-licenses-by-req-data", License.class);
-		query.setParameter("hash", BlockedRequest.generateHash(requestData));
-		try {
-			List<License> list = query.getResultList();
-			if (list.size() == 0) {
-				return null;
-			}
-			if (list.size() > 1) {
-				LOG.error("There are more than 1 active license for request data: {}\nHash: {}", requestData, BlockedRequest.generateHash(requestData));
-			}
-			return list.get(0);
-		} catch (NoResultException e) {
-			// There is no license for request data
-			return null;
-		}
-	}
+    /**
+     * Action<p>
+     * Actions to take with the license
+     */
+    public static class Action {
+        public static final int CREATE = 1;
+        public static final int REQUEST = 2;
+        public static final int ACTIVATION = 3;
+        public static final int SEND = 4;
+        public static final int DOWNLOAD = 5;
+        public static final int CANCEL = 6;
+        public static final int DELETE = 7;
+        public static final int BLOCK = 8;
+        public static final int UNBLOCK = 9;
+    }
 
-	public static License findLicenseByCode(String code, EntityManager em) throws SeCurisServiceException {
-		TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class);
-		query.setParameter("code", code);
-		try {
-			return query.getSingleResult();
-		} catch (NoResultException e) {
-			// There is no license for request data
-			return null;
-		}
-	}
+    /**
+     * Status<p>
+     * Status of the requested license
+     */
+    public static class Status {
 
-	public Integer getCodeSuffix() {
-		return codeSuffix;
-	}
+        private static final Map<Integer, List<LicenseStatus>> transitions = Utils.createMap(
+            Action.REQUEST,   Arrays.asList(LicenseStatus.CREATED, LicenseStatus.REQUESTED),
+            Action.ACTIVATION,Arrays.asList(LicenseStatus.CREATED, LicenseStatus.REQUESTED, LicenseStatus.PRE_ACTIVE, LicenseStatus.EXPIRED),
+            Action.SEND,      Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE),
+            Action.DOWNLOAD,  Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE),
+            Action.CANCEL,    Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE, LicenseStatus.REQUESTED, LicenseStatus.EXPIRED),
+            Action.DELETE,    Arrays.asList(LicenseStatus.CANCELLED, LicenseStatus.CREATED, LicenseStatus.BLOCKED),
+            Action.UNBLOCK,   Arrays.asList(LicenseStatus.BLOCKED),
+            Action.BLOCK,     Arrays.asList(LicenseStatus.CANCELLED)
+        );
 
-	public void setCodeSuffix(Integer codeSuffix) {
-		this.codeSuffix = codeSuffix;
-	}
+        /**
+        * isActionValid<p>
+        * Check whether an action is valid given the current license status.
+        *
+        * @param action action constant from {@link Action}
+        * @param currentStatus current license status
+        * @return true if allowed
+        */
+        public static boolean isActionValid(Integer action, LicenseStatus currentStatus) {
+            List<LicenseStatus> validStatuses = transitions.get(action);
+            LOG.info("Action {} is valid ? => {} current: {} OK? {}", action, validStatuses, currentStatus,
+                     validStatuses != null && validStatuses.contains(currentStatus));
+            return validStatuses != null && validStatuses.contains(currentStatus);
+        }
+    }
 
-	public String getActivationCode() {
-		return activationCode;
-	}
+    // ------------------------------------------------------------------
+    // Repository helpers (static queries)
+    // ------------------------------------------------------------------
 
-	public void setActivationCode(String activationCode) {
-		this.activationCode = activationCode;
-	}
+    /**
+    * findValidLicenseByRequestData<p>
+    * Return the first license in statuses RE/AC/PA for the given request data hash.
+    *
+    * @param requestData raw request data
+    * @param em entity manager
+    * @return matching license or null
+    * @throws SeCurisServiceException
+    */
+    public static License findValidLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException {
+        TypedQuery<License> query = em.createNamedQuery("list-valid-licenses-by-req-data", License.class);
+        query.setParameter("hash", BlockedRequest.generateHash(requestData));
+        try {
+            List<License> list = query.getResultList();
+            if (list.size() == 0) return null;
+            if (list.size() > 1) {
+                LOG.error("There are more than 1 active or requested license for request data: {}\nHash: {}",
+                          requestData, BlockedRequest.generateHash(requestData));
+            }
+            return list.get(0);
+        } catch (NoResultException e) {
+            return null;
+        }
+    }
 
-	public boolean isMetadataObsolete() {
-		return metadataObsolete != null && metadataObsolete;
-	}
+    /**
+    * findLicenseByActivationCode<p>
+    * Retrieve a license by its activation code.
+    * 
+    * @param activationCode
+    * @param entityManager
+    * @return license
+    * @throws SeCurisServiceException
+    */
+    public static License findLicenseByActivationCode(String activationCode, EntityManager em) throws SeCurisServiceException {
+        TypedQuery<License> query = em.createNamedQuery("license-by-activation-code", License.class);
+        query.setParameter("activationCode", activationCode);
+        try {
+            return query.getSingleResult();
+        } catch (NoResultException e) {
+            return null;
+        }
+    }
 
-	public void setMetadataObsolete(Boolean obsolete) {
-		this.metadataObsolete = obsolete;
-	}
+    /**
+    * findActiveLicenseByRequestData<p>
+    * Return the first AC/PA license for a given requestData.
+    * 
+    * @param requestData
+    * @param entityManager
+    * @return license
+    * @throws SeCurisServiceException
+    */
+    public static License findActiveLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException {
+        TypedQuery<License> query = em.createNamedQuery("list-active-licenses-by-req-data", License.class);
+        query.setParameter("hash", BlockedRequest.generateHash(requestData));
+        try {
+            List<License> list = query.getResultList();
+            if (list.size() == 0) return null;
+            if (list.size() > 1) {
+                LOG.error("There are more than 1 active license for request data: {}\nHash: {}",
+                          requestData, BlockedRequest.generateHash(requestData));
+            }
+            return list.get(0);
+        } catch (NoResultException e) {
+            return null;
+        }
+    }
 
+    /**
+    * findLicenseByCode<p>
+    * Retrieve a license by human-readable code.
+    * 
+    * @param code
+    * @param entityManager
+    * @return license
+    * @throws SeCurisServiceException
+    */
+    public static License findLicenseByCode(String code, EntityManager em) throws SeCurisServiceException {
+        TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class);
+        query.setParameter("code", code);
+        try {
+            return query.getSingleResult();
+        } catch (NoResultException e) {
+            return null;
+        }
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java b/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
index 7fd0b03..78f8cbd 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -21,16 +24,26 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- * Entity implementation class for Entity: license
- * 
- */
+* LicenseHistory
+* <p>
+* Audit trail entries for a given license (who/what/when).
+*
+* Mapping details:
+* - Table: license_history
+* - Many-to-one to License and User (ignored in JSON).
+* - NamedQuery: list-license-history by license id.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @Entity
 @Table(name = "license_history")
 @JsonIgnoreProperties(ignoreUnknown = true)
 @NamedQueries({
-    @NamedQuery(name = "list-license-history", query = "SELECT lh FROM LicenseHistory lh where lh.license.id = :licId")
+    @NamedQuery(name = "list-license-history",
+                query = "SELECT lh FROM LicenseHistory lh where lh.license.id = :licId")
 })
 public class LicenseHistory implements Serializable {
 
@@ -57,59 +70,117 @@
     @JsonProperty("creation_timestamp")
     private Date creationTimestamp;
 
-    public int getId() {
-        return id;
-    }
+    // ---------------- Getters & setters ----------------
 
-    public License getLicense() {
-        return license;
-    }
+    /** 
+     * getId<p>
+     * Return primary key. 
+     * 
+     * @return id
+     */
+    public int getId() { return id; }
 
-    public void setLicense(License license) {
-        this.license = license;
-    }
+    /** 
+     * getLicense<p>
+     * Return parent license (entity). 
+     * 
+     * @return license
+     */
+    public License getLicense() { return license; }
 
-    public User getUser() {
-        return user;
-    }
+    /** 
+     * setLicense<p>
+     * Set parent license (entity). 
+     * 
+     * @return license
+     */
+    public void setLicense(License license) { this.license = license; }
 
+    /** 
+     * getUser<p>
+     * Return actor user (entity). 
+     * 
+     * @return user
+     */
+    public User getUser() { return user; }
+
+    /** 
+     * getUsername<p>
+     * Expose username for JSON. 
+     * 
+     * @return username
+     */
     @JsonProperty("username")
-    public String getUsername() {
-        return user == null ? null : user.getUsername();
-    }
+    public String getUsername() { return user == null ? null : user.getUsername(); }
 
-    public void setUser(User user) {
-        this.user = user;
-    }
+    /** 
+     * setUser<p>
+     * Set actor user (entity). 
+     * 
+     * @param user
+     */
+    public void setUser(User user) { this.user = user; }
 
-    public String getAction() {
-        return action;
-    }
+    /** 
+     * getAction<p>
+     * Return action key (e.g., "activate"). 
+     * 
+     * @return action
+     */
+    public String getAction() { return action; }
 
-    public void setAction(String action) {
-        this.action = action;
-    }
+    /** 
+     * setAction<p>
+     * Set action key. 
+     * 
+     * @param action
+     */
+    public void setAction(String action) { this.action = action; }
 
-    public String getComments() {
-        return comments;
-    }
+    /** 
+     * getComments<p>
+     * Return optional comments. 
+     * 
+     * @return comments
+     */
+    public String getComments() { return comments; }
 
-    public void setComments(String comments) {
-        this.comments = comments;
-    }
+    /** 
+     * setComments<p>
+     * Set optional comments. 
+     * 
+     * @param comments
+     */
+    public void setComments(String comments) { this.comments = comments; }
 
-    public void setId(int id) {
-        this.id = id;
-    }
+    /** 
+     * setId<p>
+     * Set primary key (normally framework-managed)
+     * 
+     * @param id. 
+     */
+    public void setId(int id) { this.id = id; }
 
-    public Date getCreationTimestamp() {
-        return creationTimestamp;
-    }
+    /** 
+     * getCreationTimestamp<p>
+     * Return timestamp. 
+     * 
+     * @return creationTimestamp
+     */
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
-    public void setCreationTimestamp(Date creationTimestamp) {
-        this.creationTimestamp = creationTimestamp;
-    }
+    /** 
+     * setCreationTimestamp<p>
+     * Set timestamp. 
+     * 
+     * @param creationTimestamp
+     */
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 
+    /** 
+     * Actions<p>
+     * Catalog of action names. 
+     */
     public static class Actions {
         public static final String CREATE = "creation";
         public static final String ADD_REQUEST = "request";
@@ -125,3 +196,4 @@
         public static final String DELETE = "delete";
     }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java b/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java
index df94ccb..9b60df9 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import net.curisit.securis.db.common.CodedEnum;
@@ -6,42 +9,64 @@
 import com.fasterxml.jackson.annotation.JsonValue;
 
 /**
- * Contains the possible license statuses. For further details:
- * https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
- * 
- * @author rob
- */
+* LicenseStatus
+* <p>
+* Enumerates the possible license states. JSON code/value is the short code (CR, RE, AC, ...).
+* See: https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 public enum LicenseStatus implements CodedEnum {
     CREATED("CR"), REQUESTED("RE"), ACTIVE("AC"), PRE_ACTIVE("PA"), EXPIRED("EX"), CANCELLED("CA"), BLOCKED("BL");
 
     private final String code;
 
-    LicenseStatus(String code) {
-        this.code = code;
-    }
+    /**
+     * LicenseStatus<p>
+     * Constructor
+     * 
+     * @param code
+     */
+    LicenseStatus(String code) { this.code = code; }
 
-    @Override
-    public String getCode() {
-        return code;
-    }
+    /** 
+     * getCode<p>
+     * Return the short code used in DB/JSON. 
+     * 
+     * @return code
+     */
+    @Override public String getCode() { return code; }
 
+    /** 
+     * valueFromCode<p>
+     * Factory from code string (null on unknown). 
+     * 
+     * @param code
+     */
     @JsonCreator
     public static LicenseStatus valueFromCode(String code) {
         for (LicenseStatus ps : LicenseStatus.values()) {
-            if (ps.code.equals(code)) {
-                return ps;
-            }
+            if (ps.code.equals(code)) return ps;
         }
         return null;
     }
 
+    /** 
+     * getName<p>
+     * Expose the code as JSON value. 
+     * 
+     * @return name
+     */
     @JsonValue
-    public String getName() {
-        return this.code;
-    }
+    public String getName() { return this.code; }
 
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
     @Override
-    public String toString() {
-        return code;
-    }
+    public String toString() { return code; }
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseType.java b/securis/src/main/java/net/curisit/securis/db/LicenseType.java
index 1733787..b2553cd 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseType.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseType.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -29,135 +32,236 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- * Entity implementation class for Entity: license_type
- * 
- */
+* LicenseType
+* <p>
+* Describes a license category within an application. Owns metadata entries.
+*
+* Mapping details:
+* - Table: license_type
+* - Many-to-one to Application (lazy).
+* - One-to-many metadata with cascade PERSIST/REMOVE/REFRESH.
+* - Named queries for listing and filtering by application(s).
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @JsonIgnoreProperties(ignoreUnknown = true)
 @Entity
 @Table(name = "license_type")
-@NamedQueries({ @NamedQuery(name = "list-license_types", query = "SELECT lt FROM LicenseType lt"),
-		@NamedQuery(name = "list-license_types-by_apps-id", query = "SELECT lt FROM LicenseType lt where lt.application.id in :list_ids"),
-		@NamedQuery(name = "list-application-license_types", query = "SELECT lt FROM LicenseType lt where lt.application.id = :appId") })
+@NamedQueries({
+    @NamedQuery(name = "list-license_types", query = "SELECT lt FROM LicenseType lt"),
+    @NamedQuery(name = "list-license_types-by_apps-id", query = "SELECT lt FROM LicenseType lt where lt.application.id in :list_ids"),
+    @NamedQuery(name = "list-application-license_types", query = "SELECT lt FROM LicenseType lt where lt.application.id = :appId")
+})
 public class LicenseType implements Serializable {
 
-	@SuppressWarnings("unused")
-	private static final Logger LOG = LogManager.getLogger(LicenseType.class);
-	private static final long serialVersionUID = 1L;
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LogManager.getLogger(LicenseType.class);
+    private static final long serialVersionUID = 1L;
 
-	@Id
-	@GeneratedValue
-	private Integer id;
+    @Id
+    @GeneratedValue
+    private Integer id;
 
-	private String code;
-	private String name;
-	private String description;
+    private String code;
+    private String name;
+    private String description;
 
-	@Column(name = "creation_timestamp")
-	@JsonProperty("creation_timestamp")
-	private Date creationTimestamp;
+    @Column(name = "creation_timestamp")
+    @JsonProperty("creation_timestamp")
+    private Date creationTimestamp;
 
-	@JsonIgnore
-	@ManyToOne(fetch = FetchType.LAZY)
-	@JoinColumn(name = "application_id")
-	private Application application;
+    @JsonIgnore
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "application_id")
+    private Application application;
 
-	@OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "licenseType")
-	@JsonManagedReference
-	private Set<LicenseTypeMetadata> metadata;
+    @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "licenseType")
+    @JsonManagedReference
+    private Set<LicenseTypeMetadata> metadata;
 
-	public Set<LicenseTypeMetadata> getMetadata() {
-		return metadata;
-	}
+    // ---------------- Getters & setters ----------------
 
-	public void setMetadata(Set<LicenseTypeMetadata> metadata) {
-		this.metadata = metadata;
-	}
+    /** 
+     * getMetadata<p>
+     * Return associated metadata entries.
+     * 
+     * @return metadata
+     */
+    public Set<LicenseTypeMetadata> getMetadata() { return metadata; }
 
-	public Integer getId() {
-		return id;
-	}
+    /** 
+     * setMetadata<p>
+     * Set associated metadata entries. 
+     * 
+     * @param metadata
+     */
+    public void setMetadata(Set<LicenseTypeMetadata> metadata) { this.metadata = metadata; }
 
-	public void setId(Integer id) {
-		this.id = id;
-	}
+    /** 
+     * getId<p>
+     * Return primary key. 
+     * 
+     * @return id
+     */
+    public Integer getId() { return id; }
 
-	public String getName() {
-		return name;
-	}
+    /** 
+     * setId<p>
+     * Set primary key. 
+     * 
+     * @param id
+     */
+    public void setId(Integer id) { this.id = id; }
 
-	public void setName(String name) {
-		this.name = name;
-	}
+    /** 
+     * getName<p>
+     * Return display name. 
+     * 
+     * @return name
+     */
+    public String getName() { return name; }
 
-	public String getDescription() {
-		return description;
-	}
+    /** 
+     * setName<p>
+     * Set display name. 
+     * 
+     * @param name
+     */
+    public void setName(String name) { this.name = name; }
 
-	public void setDescription(String description) {
-		this.description = description;
-	}
+    /** 
+     * getDescription<p>
+     * Return description. 
+     * 
+     * @return description
+     */
+    public String getDescription() { return description; }
 
-	public String getCode() {
-		return code;
-	}
+    /** 
+     * setDescription<p>
+     * Set description. 
+     * 
+     * @param description
+     */
+    public void setDescription(String description) { this.description = description; }
 
-	public void setCode(String code) {
-		this.code = code;
-	}
+    /** 
+     * getCode<p>
+     * Return short code. 
+     * 
+     * @return code
+     */
+    public String getCode() { return code; }
 
-	public Application getApplication() {
-		return application;
-	}
+    /** 
+     * setCode<p>
+     * Set short code. 
+     * 
+     * @param code
+     */
+    public void setCode(String code) { this.code = code; }
 
-	@JsonProperty("application_name")
-	public String getApplicationName() {
-		return application == null ? null : application.getName();
-	}
+    /** 
+     * getApplication<p>
+     * Return owning application (entity). 
+     * 
+     * @return application
+     */
+    public Application getApplication() { return application; }
 
-	@JsonProperty("application_id")
-	public Integer getApplicationId() {
-		return application == null ? null : application.getId();
-	}
+    /** 
+     * getApplicationName<p>
+     * Expose app name for JSON. 
+     * 
+     * @return appName
+     */
+    @JsonProperty("application_name")
+    public String getApplicationName() { return application == null ? null : application.getName(); }
 
-	@JsonProperty("application_id")
-	public void setApplicationId(Integer appId) {
-		if (appId == null) {
-			application = null;
-		} else {
-			application = new Application();
-			application.setId(appId);
-		}
-	}
+    /** 
+     * getApplicationId<p>
+     * Expose app id for JSON. 
+     * 
+     * @return appId
+     */
+    @JsonProperty("application_id")
+    public Integer getApplicationId() { return application == null ? null : application.getId(); }
 
-	public void setApplication(Application application) {
-		this.application = application;
-	}
+    /** 
+     * setApplicationId<p>
+     * Setter by id for JSON binding (creates shallow Application). 
+     * 
+     * @param appId
+     */
+    @JsonProperty("application_id")
+    public void setApplicationId(Integer appId) {
+        if (appId == null) {
+            application = null;
+        } else {
+            application = new Application();
+            application.setId(appId);
+        }
+    }
 
-	public Date getCreationTimestamp() {
-		return creationTimestamp;
-	}
+    /** 
+     * setApplication<p>
+     * Set owning application (entity). 
+     * 
+     * @param application
+     */
+    public void setApplication(Application application) { this.application = application; }
 
-	public void setCreationTimestamp(Date creationTimestamp) {
-		this.creationTimestamp = creationTimestamp;
-	}
+    /** 
+     * getCreationTimestamp<p>
+     * Return creation timestamp. 
+     * 
+     * @return creationTimestamp
+     */
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
-	@Override
-	public boolean equals(Object obj) {
-		if (!(obj instanceof LicenseType))
-			return false;
-		LicenseType other = (LicenseType) obj;
-		return id.equals(other.id);
-	}
+    /** 
+     * setCreationTimestamp<p>
+     * Set creation timestamp. 
+     * 
+     * @param creationTimestamp
+     */
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 
-	@Override
-	public int hashCode() {
-		return (id == null ? 0 : id.hashCode());
-	}
+    // ---------------- Object methods ----------------
 
-	@Override
-	public String toString() {
-		return String.format("LT: ID: %d, code: %s", id, code);
-	}
+    /**
+     * equals<p>
+     * Compare the current object with the given object
+     * 
+     * @param object
+     * @return isEquals
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LicenseType)) return false;
+        LicenseType other = (LicenseType) obj;
+        return id != null && id.equals(other.id);
+    }
+
+    /**
+     * hashCode<p>
+     * Get the object hashCode
+     * 
+     * @return hashCode
+     */
+    @Override
+    public int hashCode() { return (id == null ? 0 : id.hashCode()); }
+
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
+    @Override
+    public String toString() { return String.format("LT: ID: %d, code: %s", id, code); }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java b/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java
index e17ae8c..a004085 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -21,80 +24,148 @@
 import net.curisit.securis.db.common.Metadata;
 
 /**
- * Entity implementation class for Entity: licensetype_metadata
- * 
- */
+* LicenseTypeMetadata
+* <p>
+* Single metadata entry attached to a {@link LicenseType}.
+* Composite PK: (license_type_id, key).
+*
+* Mapping details:
+* - Table: licensetype_metadata
+* - @JsonBackReference to avoid recursion with LicenseType.metadata.
+* - NamedQuery: list-licensetype-metadata by license type id.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @Entity
 @Table(name = "licensetype_metadata")
 @JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-licensetype-metadata", query = "SELECT a FROM LicenseTypeMetadata a where a.licenseType.id = :licenseTypeId") })
+@NamedQueries({
+    @NamedQuery(name = "list-licensetype-metadata",
+                query = "SELECT a FROM LicenseTypeMetadata a where a.licenseType.id = :licenseTypeId")
+})
 public class LicenseTypeMetadata implements Serializable, Metadata {
 
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-	@Id
-	@ManyToOne
-	@JsonBackReference
-	@JoinColumn(name = "license_type_id")
-	private LicenseType licenseType;
+    /** PK part: owning license type. */
+    @Id
+    @ManyToOne
+    @JsonBackReference
+    @JoinColumn(name = "license_type_id")
+    private LicenseType licenseType;
 
-	@Id
-	@Column(name = "\"key\"")
-	private String key;
+    /** PK part: metadata key (quoted). */
+    @Id
+    @Column(name = "\"key\"")
+    private String key;
 
-	private String value;
+    /** Metadata value. */
+    private String value;
 
-	private boolean mandatory;
+    /** Whether this key is mandatory for the license type. */
+    private boolean mandatory;
 
-	public LicenseType getLicenseType() {
-		return licenseType;
-	}
+    // ---------------- Getters & setters ----------------
 
-	public void setLicenseType(LicenseType licenseType) {
-		this.licenseType = licenseType;
-	}
+    /** 
+     * getLicenseType<p>
+     * Return owning license type. 
+     * 
+     * @return licenseType
+     */
+    public LicenseType getLicenseType() { return licenseType; }
 
-	public String getValue() {
-		return value;
-	}
+    /** 
+     * setLicenseType<p>
+     * Set owning license type. 
+     * 
+     * @param licenseType
+     */
+    public void setLicenseType(LicenseType licenseType) { this.licenseType = licenseType; }
 
-	public void setValue(String value) {
-		this.value = value;
-	}
+    /** 
+     * getValue<p>
+     * Return metadata value. 
+     * 
+     * @return value
+     */
+    public String getValue() { return value; }
 
-	public String getKey() {
-		return key;
-	}
+    /** 
+     * setValue<p>
+     * Set metadata value. 
+     * 
+     * @param value
+     */
+    public void setValue(String value) { this.value = value; }
 
-	public void setKey(String key) {
-		this.key = key;
-	}
+    /** 
+     * getKey<p>
+     * Return metadata key (PK part). 
+     * 
+     * @return key
+     */
+    public String getKey() { return key; }
 
-	public boolean isMandatory() {
-		return mandatory;
-	}
+    /** 
+     * setKey<p>
+     * Set metadata key (PK part). 
+     * 
+     * @param key
+     */
+    public void setKey(String key) { this.key = key; }
 
-	public void setMandatory(boolean mandatory) {
-		this.mandatory = mandatory;
-	}
+    /** 
+     * isMandatory<p>
+     * Return whether this entry is required. 
+     * 
+     * @return isMandatory
+     */
+    public boolean isMandatory() { return mandatory; }
 
-	@Override
-	public boolean equals(Object obj) {
-		if (!(obj instanceof LicenseTypeMetadata))
-			return false;
-		LicenseTypeMetadata other = (LicenseTypeMetadata) obj;
-		return Objects.equals(key, other.key) && Objects.equals(licenseType, other.licenseType);
-	}
+    /** 
+     * setMandatory<p>
+     * Set whether this entry is required. 
+     * 
+     * @param mandatory
+     */
+    public void setMandatory(boolean mandatory) { this.mandatory = mandatory; }
 
-	@Override
-	public int hashCode() {
-		return Objects.hash(key, licenseType);
-	}
+    // ---------------- Object methods ----------------
 
-	@Override
-	public String toString() {
-		return String.format("LTMD (%s: %s)", key, value);
-	}
+    /**
+     * equals<p>
+     * Compare the current object with the given object
+     * 
+     * @param object
+     * @return isEquals
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LicenseTypeMetadata)) return false;
+        LicenseTypeMetadata other = (LicenseTypeMetadata) obj;
+        return Objects.equals(key, other.key) && Objects.equals(licenseType, other.licenseType);
+    }
+
+    /**
+     * hashCode<p>
+     * Get the object hashCode
+     * 
+     * @return hashCode
+     */
+    @Override
+    public int hashCode() { return Objects.hash(key, licenseType); }
+
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
+    @Override
+    public String toString() { return String.format("LTMD (%s: %s)", key, value); }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/Organization.java b/securis/src/main/java/net/curisit/securis/db/Organization.java
index 6b8b217..b70b8b8 100644
--- a/securis/src/main/java/net/curisit/securis/db/Organization.java
+++ b/securis/src/main/java/net/curisit/securis/db/Organization.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -32,164 +35,253 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- * Entity implementation class for Entity: organization
- * 
- */
+* Organization
+* <p>
+* Represents a customer/tenant organization. Manages parent/children hierarchy
+* and user membership.
+*
+* Mapping details:
+* - Table: organization
+* - ManyToMany users via user_organization (ignored in default JSON).
+* - Self-referencing parent/children relation.
+* - Named queries for listing, filtering by ids, and children discovery.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @JsonIgnoreProperties(ignoreUnknown = true)
 @Entity
 @Table(name = "organization")
-@NamedQueries({ @NamedQuery(name = "list-organizations", query = "SELECT o FROM Organization o"),
-		@NamedQuery(name = "list-organizations-by-ids", query = "SELECT o FROM Organization o where id in :list_ids"),
-		@NamedQuery(name = "find-children-org", query = "SELECT o FROM Organization o where o.parentOrganization = :parentOrganization") })
+@NamedQueries({
+    @NamedQuery(name = "list-organizations", query = "SELECT o FROM Organization o"),
+    @NamedQuery(name = "list-organizations-by-ids", query = "SELECT o FROM Organization o where id in :list_ids"),
+    @NamedQuery(name = "find-children-org", query = "SELECT o FROM Organization o where o.parentOrganization = :parentOrganization")
+})
 public class Organization implements Serializable {
 
-	@SuppressWarnings("unused")
-	private static final Logger LOG = LogManager.getLogger(Organization.class);
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LogManager.getLogger(Organization.class);
 
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-	@Id
-	@GeneratedValue
-	private Integer id;
+    @Id
+    @GeneratedValue
+    private Integer id;
 
-	private String code;
-	private String name;
-	private String description;
+    private String code;
+    private String name;
+    private String description;
 
-	@Column(name = "creation_timestamp")
-	@JsonProperty("creation_timestamp")
-	private Date creationTimestamp;
+    @Column(name = "creation_timestamp")
+    @JsonProperty("creation_timestamp")
+    private Date creationTimestamp;
 
-	@JsonIgnore
-	// We don't include the users to limit the size of each row a the listing
-	@ManyToMany(cascade = CascadeType.REMOVE)
-	@JoinTable(name = "user_organization", //
-			joinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") }, //
-			inverseJoinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") })
-	private Set<User> users;
+    @JsonIgnore
+    @ManyToMany(cascade = CascadeType.REMOVE)
+    @JoinTable(name = "user_organization",
+        joinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") },
+        inverseJoinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") })
+    private Set<User> users;
 
-	@JsonIgnore
-	// We don't include the users to limit the size of each row a the listing
-	@ManyToOne
-	@JoinColumn(name = "org_parent_id")
-	private Organization parentOrganization;
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "org_parent_id")
+    private Organization parentOrganization;
 
-	@JsonIgnore
-	// We don't include the users to limit the size of each row a the listing
-	@OneToMany(fetch = FetchType.LAZY, mappedBy = "parentOrganization")
-	private Set<Organization> childOrganizations;
+    @JsonIgnore
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "parentOrganization")
+    private Set<Organization> childOrganizations;
 
-	public Integer getId() {
-		return id;
-	}
+    // ---------------- Getters & setters ----------------
 
-	public void setId(Integer id) {
-		this.id = id;
-	}
+    /** 
+     * getId<p>
+     * Return primary key.
+     * 
+     * @return id
+     */
+    public Integer getId() { return id; }
 
-	public String getName() {
-		return name;
-	}
+    /** 
+     * setId<p>
+     * Set primary key. 
+     * 
+     * @param id
+     */
+    public void setId(Integer id) { this.id = id; }
 
-	public void setName(String name) {
-		this.name = name;
-	}
+    /** 
+     * getName<p>
+     * Return display name. 
+     * 
+     * @return name
+     */
+    public String getName() { return name; }
 
-	public String getDescription() {
-		return description;
-	}
+    /** 
+     * setName<p>
+     * Set display name. 
+     * 
+     * @param name
+     */
+    public void setName(String name) { this.name = name; }
 
-	public void setDescription(String description) {
-		this.description = description;
-	}
+    /** 
+     * getDescription<p>
+     * Return optional description. 
+     * 
+     * @return description
+     */
+    public String getDescription() { return description; }
 
-	public String getCode() {
-		return code;
-	}
+    /** 
+     * setDescription<p>
+     * Set optional description. 
+     * 
+     * @param description
+     */
+    public void setDescription(String description) { this.description = description; }
 
-	public void setCode(String code) {
-		this.code = code;
-	}
+    /** 
+     * getCode<p>
+     * Return short code. 
+     * 
+     * @return code
+     */
+    public String getCode() { return code; }
 
-	public Date getCreationTimestamp() {
-		return creationTimestamp;
-	}
+    /** 
+     * setCode<p>
+     * Set short code. 
+     * 
+     * @param code
+     */
+    public void setCode(String code) { this.code = code; }
 
-	public void setCreationTimestamp(Date creationTimestamp) {
-		this.creationTimestamp = creationTimestamp;
-	}
+    /** 
+     * getCreationTimestamp<p>
+     * Return creation timestamp. 
+     * 
+     * @return creationTimeStamp
+     */
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
-	public Set<User> getUsers() {
-		return users;
-	}
+    /** 
+     * setCreationTimestamp<p>
+     * Set creation timestamp. 
+     * 
+     * @param creationTimestamp
+     */
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 
-	public void setUsers(Set<User> users) {
-		this.users = users;
-	}
+    /** 
+     * getUsers<p>
+     * Return member users (entity set). 
+     * 
+     * @return users
+     */
+    public Set<User> getUsers() { return users; }
 
-	public Organization getParentOrganization() {
-		return parentOrganization;
-	}
+    /** 
+     * setUsers<p>
+     * Set member users. 
+     * 
+     * @param users
+     */
+    public void setUsers(Set<User> users) { this.users = users; }
 
-	public void setParentOrganization(Organization parentOrganization) {
-		this.parentOrganization = parentOrganization;
-	}
+    /** 
+     * getParentOrganization<p>
+     * Return parent org (entity). 
+     * 
+     * @return parentOrganization
+     */
+    public Organization getParentOrganization() { return parentOrganization; }
 
-	// Roberto: Following methods are necessary to include in the REST list
-	// response
-	// information about the referenced entities.
-	@JsonProperty("org_parent_id")
-	public void setParentOrgId(Integer orgId) {
-		if (orgId != null) {
-			parentOrganization = new Organization();
-			parentOrganization.setId(orgId);
-		} else {
-			parentOrganization = null;
-		}
-	}
+    /** 
+     * setParentOrganization<p>
+     * Set parent org (entity). 
+     * 
+     * @param parentOrganization
+     */
+    public void setParentOrganization(Organization parentOrganization) { this.parentOrganization = parentOrganization; }
 
-	@JsonProperty("org_parent_id")
-	public Integer getParentOrgId() {
-		return parentOrganization == null ? null : parentOrganization.getId();
-	}
+    // JSON helpers for parent organization
 
-	@JsonProperty("org_parent_name")
-	public String getParentOrgName() {
-		return parentOrganization == null ? null : parentOrganization.getName();
-	}
+    /** 
+     * setParentOrgId<p>
+     * Setter by id (creates shallow Organization). 
+     * 
+     * @param orgId
+     */
+    @JsonProperty("org_parent_id")
+    public void setParentOrgId(Integer orgId) {
+        if (orgId != null) {
+            parentOrganization = new Organization();
+            parentOrganization.setId(orgId);
+        } else {
+            parentOrganization = null;
+        }
+    }
 
-	@JsonProperty("users_ids")
-	public void setUsersIds(List<String> usersIds) {
-		users = new HashSet<>();
-		if (usersIds != null) {
-			for (String userid : usersIds) {
-				User u = new User();
-				u.setUsername(userid);
-				users.add(u);
-			}
-		}
-	}
+    /** 
+     * getParentOrgId<p>
+     * Expose parent org id. 
+     * 
+     * @return parentOrgId
+     */
+    @JsonProperty("org_parent_id")
+    public Integer getParentOrgId() { return parentOrganization == null ? null : parentOrganization.getId(); }
 
-	@JsonProperty("users_ids")
-	public Set<String> getUsersIds() {
-		if (users == null) {
-			return null;
-		}
-		Set<String> ids = new HashSet<>();
-		for (User user : users) {
-			ids.add(user.getUsername());
-		}
-		return ids;
-	}
+    /** 
+     * getParentOrgName<p>
+     * Expose parent org name. 
+     * 
+     * @return parentOrgName
+     */
+    @JsonProperty("org_parent_name")
+    public String getParentOrgName() { return parentOrganization == null ? null : parentOrganization.getName(); }
 
-	public Set<Organization> getChildOrganizations() {
-		return childOrganizations;
-	}
+    // JSON helpers for users
 
-	public void setChildOrganizations(Set<Organization> childOrganizations) {
-		this.childOrganizations = childOrganizations;
-	}
+    /** 
+     * setUsersIds<p>
+     * Replace users set from a list of usernames. 
+     * 
+     * @param userId list
+     */
+    @JsonProperty("users_ids")
+    public void setUsersIds(List<String> usersIds) {
+        users = new HashSet<>();
+        if (usersIds != null) {
+            for (String userid : usersIds) {
+                User u = new User();
+                u.setUsername(userid);
+                users.add(u);
+            }
+        }
+    }
 
+    /** 
+     * getUsersIds<p>
+     * Expose member usernames. 
+     * 
+     * @return userId list
+     */
+    @JsonProperty("users_ids")
+    public Set<String> getUsersIds() {
+        if (users == null) return null;
+        Set<String> ids = new HashSet<>();
+        for (User user : users) { ids.add(user.getUsername()); }
+        return ids;
+    }
+
+    /** getChildOrganizations<p>Return children (entity set). */
+    public Set<Organization> getChildOrganizations() { return childOrganizations; }
+
+    /** setChildOrganizations<p>Set children (entity set). */
+    public void setChildOrganizations(Set<Organization> childOrganizations) { this.childOrganizations = childOrganizations; }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/Pack.java b/securis/src/main/java/net/curisit/securis/db/Pack.java
index aee90fa..94dd5dd 100644
--- a/securis/src/main/java/net/curisit/securis/db/Pack.java
+++ b/securis/src/main/java/net/curisit/securis/db/Pack.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -32,379 +35,606 @@
 import net.curisit.integrity.commons.Utils;
 
 /**
- * Entity implementation class for Entity: pack
- * 
- */
+* Pack
+* <p>
+* Group/bundle of licenses for an organization and application (via LicenseType).
+* Tracks capacity, availability, status, and validity windows.
+*
+* Mapping details:
+* - Table: pack
+* - ManyToOne to Organization, LicenseType, User (createdBy)
+* - OneToMany licenses (lazy)
+* - Custom type: net.curisit.securis.db.common.PackStatusType
+* - Named queries for listing and filtering by org/app.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @Entity
 @Table(name = "pack")
 @JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-packs", query = "SELECT pa FROM Pack pa"), //
-		@NamedQuery(name = "pack-by-code", query = "SELECT pa FROM Pack pa where pa.code = :code"), //
-		@NamedQuery(name = "list-packs-by-lic-type", query = "SELECT pa FROM Pack pa where pa.licenseType.id = :lt_id"), //
-		@NamedQuery(name = "list-packs-by-orgs-apps", query = "SELECT pa FROM Pack pa where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app "), //
-		@NamedQuery(name = "list-packs-by-apps", query = "SELECT pa FROM Pack pa where pa.licenseType.application.id in :list_ids_app ") })
+@NamedQueries({
+    @NamedQuery(name = "list-packs", query = "SELECT pa FROM Pack pa"),
+    @NamedQuery(name = "pack-by-code", query = "SELECT pa FROM Pack pa where pa.code = :code"),
+    @NamedQuery(name = "list-packs-by-lic-type", query = "SELECT pa FROM Pack pa where pa.licenseType.id = :lt_id"),
+    @NamedQuery(name = "list-packs-by-orgs-apps", query = "SELECT pa FROM Pack pa where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app "),
+    @NamedQuery(name = "list-packs-by-apps", query = "SELECT pa FROM Pack pa where pa.licenseType.application.id in :list_ids_app ")
+})
 public class Pack implements Serializable {
 
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-	@Id
-	@GeneratedValue
-	private Integer id;
+    @Id
+    @GeneratedValue
+    private Integer id;
 
-	private String code;
+    private String code;
+    private String comments;
+    private Boolean frozen;
 
-	private String comments;
+    @Column(name = "creation_timestamp")
+    @JsonProperty("creation_timestamp")
+    private Date creationTimestamp;
 
-	private Boolean frozen;
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "organization_id")
+    private Organization organization;
 
-	@Column(name = "creation_timestamp")
-	@JsonProperty("creation_timestamp")
-	private Date creationTimestamp;
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "license_type_id")
+    private LicenseType licenseType;
 
-	@JsonIgnore
-	@ManyToOne
-	@JoinColumn(name = "organization_id")
-	private Organization organization;
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "created_by")
+    private User createdBy;
 
-	@JsonIgnore
-	@ManyToOne
-	@JoinColumn(name = "license_type_id")
-	private LicenseType licenseType;
+    @JsonIgnore
+    @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
+    private Set<License> licenses;
 
-	@JsonIgnore
-	@ManyToOne
-	@JoinColumn(name = "created_by")
-	private User createdBy;
+    @Column(name = "num_licenses")
+    @JsonProperty("num_licenses")
+    private int numLicenses;
 
-	@JsonIgnore
-	@OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
-	private Set<License> licenses;
+    @Column(name = "init_valid_date")
+    @JsonProperty("init_valid_date")
+    private Date initValidDate;
 
-	@Column(name = "num_licenses")
-	@JsonProperty("num_licenses")
-	private int numLicenses;
+    @Column(name = "end_valid_date")
+    @JsonProperty("end_valid_date")
+    private Date endValidDate;
 
-	@Column(name = "init_valid_date")
-	@JsonProperty("init_valid_date")
-	private Date initValidDate;
+    @Type(type = "net.curisit.securis.db.common.PackStatusType")
+    private PackStatus status;
 
-	@Column(name = "end_valid_date")
-	@JsonProperty("end_valid_date")
-	private Date endValidDate;
+    @Column(name = "license_preactivation")
+    @JsonProperty("license_preactivation")
+    private boolean licensePreactivation;
 
-	@Type(type = "net.curisit.securis.db.common.PackStatusType")
-	private PackStatus status;
+    @Column(name = "preactivation_valid_period")
+    @JsonProperty("preactivation_valid_period")
+    private Integer preactivationValidPeriod;
 
-	@Column(name = "license_preactivation")
-	@JsonProperty("license_preactivation")
-	private boolean licensePreactivation;
+    @Column(name = "renew_valid_period")
+    @JsonProperty("renew_valid_period")
+    private Integer renewValidPeriod;
 
-	@Column(name = "preactivation_valid_period")
-	@JsonProperty("preactivation_valid_period")
-	private Integer preactivationValidPeriod;
+    @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
+    private Set<PackMetadata> metadata;
 
-	@Column(name = "renew_valid_period")
-	@JsonProperty("renew_valid_period")
-	private Integer renewValidPeriod;
+    // ---------------- Getters & setters ----------------
 
-	@OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
-	private Set<PackMetadata> metadata;
+    /** 
+     * getId<p>
+     * Return primary key. 
+     * 
+     * @return id
+     */
+    public Integer getId() { return id; }
 
-	public Integer getId() {
-		return id;
-	}
+    /** 
+     * setId<p>
+     * Set primary key. 
+     * 
+     * @param id
+     */
+    public void setId(Integer id) { this.id = id; }
 
-	public void setId(Integer id) {
-		this.id = id;
-	}
+    /** 
+     * getCode<p>
+     * Return pack code. 
+     * 
+     * @return packCode
+     */
+    public String getCode() { return code; }
 
-	public String getCode() {
-		return code;
-	}
+    /** 
+     * setCode<p>
+     * Set pack code. 
+     * 
+     * @param packCode
+     */
+    public void setCode(String code) { this.code = code; }
 
-	public void setCode(String code) {
-		this.code = code;
-	}
+    /** 
+     * getCreationTimestamp<p>
+     * Return creation timestamp. 
+     * 
+     * @return creationTimestamp
+     */
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
-	public Date getCreationTimestamp() {
-		return creationTimestamp;
-	}
+    /** 
+     * setCreationTimestamp<p>
+     * Set creation timestamp.
+     * 
+     *  @param creationTimestamp
+     */
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 
-	public void setCreationTimestamp(Date creationTimestamp) {
-		this.creationTimestamp = creationTimestamp;
-	}
+    /** 
+     * getOrganization<p>
+     * Return owning organization (entity). 
+     * 
+     * @return organization
+     */
+    public Organization getOrganization() { return organization; }
 
-	public Organization getOrganization() {
-		return organization;
-	}
+    /** 
+     * setOrganization<p>
+     * Set owning organization (entity). 
+     * 
+     * @param organization
+     */
+    public void setOrganization(Organization organization) { this.organization = organization; }
 
-	public void setOrganization(Organization organization) {
-		this.organization = organization;
-	}
+    /** 
+     * getLicenseType<p>
+     * Return license type (entity). 
+     * 
+     * @return licenseType
+     */
+    public LicenseType getLicenseType() { return licenseType; }
 
-	public LicenseType getLicenseType() {
-		return licenseType;
-	}
+    /** 
+     * setLicenseType<p>
+     * Set license type (entity). 
+     * 
+     * @param licenseType
+     */
+    public void setLicenseType(LicenseType licenseType) { this.licenseType = licenseType; }
 
-	public void setLicenseType(LicenseType licenseType) {
-		this.licenseType = licenseType;
-	}
+    /** 
+     * getCreatedBy<p>
+     * Return creator (entity). 
+     * 
+     * @return user
+     */
+    public User getCreatedBy() { return createdBy; }
 
-	public User getCreatedBy() {
-		return createdBy;
-	}
+    /** 
+     * setCreatedBy<p>
+     * Set creator (entity). 
+     * 
+     * @param user
+     */
+    public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
 
-	public void setCreatedBy(User createdBy) {
-		this.createdBy = createdBy;
-	}
+    /** 
+     * getNumLicenses<p>
+     * Return capacity (licenses). 
+     * 
+     * @return numLicenses
+     *            Number of licenses
+     */
+    public int getNumLicenses() { return numLicenses; }
 
-	public int getNumLicenses() {
-		return numLicenses;
-	}
+    /** 
+     * setNumLicenses<p>
+     * Set capacity (licenses). 
+     * 
+     * @param numLicenses
+     *           Number of licenses
+     */
+    public void setNumLicenses(int numLicenses) { this.numLicenses = numLicenses; }
 
-	public void setNumLicenses(int numLicenses) {
-		this.numLicenses = numLicenses;
-	}
+    /** 
+     * getNumActivations<p>
+     * Count ACTIVE/PRE_ACTIVE licenses in this pack. 
+     * 
+     * @return numActivations
+     *             number of activated licenses
+     */
+    @JsonProperty("num_activations")
+    public int getNumActivations() {
+        if (licenses == null) return 0;
+        int num = 0;
+        for (License lic : licenses) {
+            if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) num++;
+        }
+        return num;
+    }
 
-	@JsonProperty("num_activations")
-	public int getNumActivations() {
-		if (licenses == null) {
-			return 0;
-		}
-		int num = 0;
-		for (License lic : licenses) {
-			if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) {
-				num++;
-			}
-		}
-		return num;
-	}
+    /**
+    * getNumCreations<p>
+    * Count all created licenses (including waiting for activation). Ignores CANCELLED.
+    *
+    * @return numCreations
+    *            number of created licenses
+    */
+    @JsonProperty("num_creations")
+    public int getNumCreations() {
+        if (licenses == null) return 0;
+        int num = 0;
+        for (License lic : licenses) {
+            if (lic.getStatus() != LicenseStatus.CANCELLED) num++;
+        }
+        return num;
+    }
 
-	/**
-	 * Counts all created licenses, It counts active licenses and licenses
-	 * waiting for activation This number will be used to control the max number
-	 * of licenses created. Ignore canceled licenses.
-	 * 
-	 * @return
-	 */
-	@JsonProperty("num_creations")
-	public int getNumCreations() {
-		if (licenses == null) {
-			return 0;
-		}
-		int num = 0;
-		for (License lic : licenses) {
-			if (lic.getStatus() != LicenseStatus.CANCELLED) {
-				num++;
-			}
-		}
-		return num;
-	}
+    /**
+    * getNumAvailables<p>
+    * Number of available licenses in this pack: capacity - activations.
+    * 
+    * @return numAvailable 
+    * 				Number of available licenses
+    */
+    @JsonProperty("num_available")
+    public int getNumAvailables() { return numLicenses - getNumActivations(); }
 
-	/**
-	 * Number of available licenses in this pack
-	 * 
-	 * @return
-	 */
-	@JsonProperty("num_available")
-	public int getNumAvailables() {
-		return numLicenses - getNumActivations();
-	}
+    /** 
+     * getOrgName<p>
+     * Expose organization name. 
+     * 
+     * @return orgName
+     */
+    @JsonProperty("organization_name")
+    public String getOrgName() { return organization == null ? null : organization.getName(); }
 
-	@JsonProperty("organization_name")
-	public String getOrgName() {
-		return organization == null ? null : organization.getName();
-	}
+    /** 
+     * getAppName<p>
+     * Expose application name via license type. 
+     * 
+     * @return appName
+     */
+    @JsonProperty("application_name")
+    public String getAppName() {
+        if (licenseType == null) return null;
+        Application app = licenseType.getApplication();
+        return app == null ? null : app.getName();
+    }
 
-	@JsonProperty("application_name")
-	public String getAppName() {
-		if (licenseType == null) {
-			return null;
-		}
-		Application app = licenseType.getApplication();
-		return app == null ? null : app.getName();
-	}
+    /** 
+     * getOrgId<p>
+     * Expose organization id. 
+     * 
+     * @return orgId
+     */
+    @JsonProperty("organization_id")
+    public Integer getOrgId() { return organization == null ? null : organization.getId(); }
 
-	@JsonProperty("organization_id")
-	public Integer getOrgId() {
-		return organization == null ? null : organization.getId();
-	}
+    /** 
+     * setOrgId<p>
+     * Setter by id for JSON binding (creates shallow Organization). 
+     * 
+     * @param orgId
+     */
+    @JsonProperty("organization_id")
+    public void setOrgId(Integer idOrg) {
+        if (idOrg == null) {
+            organization = null;
+        } else {
+            organization = new Organization();
+            organization.setId(idOrg);
+        }
+    }
 
-	@JsonProperty("organization_id")
-	public void setOrgId(Integer idOrg) {
-		if (idOrg == null) {
-			organization = null;
-		} else {
-			organization = new Organization();
-			organization.setId(idOrg);
-		}
-	}
+    /** 
+     * setLicTypeId<p>
+     * Setter by id for JSON binding (creates shallow LicenseType). 
+     * 
+     * @param licTypeId
+     */
+    @JsonProperty("license_type_id")
+    public void setLicTypeId(Integer idLT) {
+        if (idLT == null) {
+            licenseType = null;
+        } else {
+            licenseType = new LicenseType();
+            licenseType.setId(idLT);
+        }
+    }
 
-	@JsonProperty("license_type_id")
-	public void setLicTypeId(Integer idLT) {
-		if (idLT == null) {
-			licenseType = null;
-		} else {
-			licenseType = new LicenseType();
-			licenseType.setId(idLT);
-		}
-	}
+    /** 
+     * getLicTypeId<p>
+     * Expose license type id. 
+     * 
+     * @return licTypeId
+     */
+    @JsonProperty("license_type_id")
+    public Integer getLicTypeId() { return licenseType == null ? null : licenseType.getId(); }
 
-	@JsonProperty("license_type_id")
-	public Integer getLicTypeId() {
-		return licenseType == null ? null : licenseType.getId();
-	}
+    /** 
+     * getCreatedById<p>
+     * Expose creator username. 
+     * 
+     * @return username
+     */
+    @JsonProperty("created_by_id")
+    public String getCreatedById() { return createdBy == null ? null : createdBy.getUsername(); }
 
-	@JsonProperty("created_by_id")
-	public String getCreatedById() {
-		return createdBy == null ? null : createdBy.getUsername();
-	}
+    /** 
+     * setCreatedById<p>
+     * Setter by username (creates shallow User). 
+     * 
+     * @param username
+     */
+    @JsonProperty("created_by_id")
+    public void setCreatedById(String username) {
+        createdBy = new User();
+        createdBy.setUsername(username);
+    }
 
-	@JsonProperty("created_by_id")
-	public void setCreatedById(String username) {
-		createdBy = new User();
-		createdBy.setUsername(username);
-	}
+    /** 
+     * getCreatedByname<p>
+     * Expose creator full display name. 
+     * 
+     * @return userName
+     */
+    @JsonProperty("created_by_name")
+    public String getCreatedByname() {
+        return createdBy == null ? null
+            : String.format("%s %s (%s)", createdBy.getFirstName(),
+                createdBy.getLastName() != null ? createdBy.getLastName() : "",
+                createdBy.getUsername());
+    }
 
-	@JsonProperty("created_by_name")
-	public String getCreatedByname() {
-		return createdBy == null ? null
-				: String.format("%s %s (%s)", createdBy.getFirstName(), createdBy.getLastName() != null ? createdBy.getLastName() : "", createdBy.getUsername());
-	}
+    /** 
+     * getLicenseTypeCode<p>
+     * Expose license type code. 
+     * 
+     * @return licenseTypeCode
+     */
+    @JsonProperty("licensetype_code")
+    public String getLicenseTypeCode() { return licenseType == null ? null : licenseType.getCode(); }
 
-	@JsonProperty("licensetype_code")
-	public String getLicenseTypeCode() {
-		return licenseType == null ? null : licenseType.getCode();
-	}
+    /** 
+     * getComments<p>
+     * Return comments. 
+     * 
+     * @return comments
+     */
+    public String getComments() { return comments; }
 
-	public String getComments() {
-		return comments;
-	}
+    /** 
+     * setComments<p>
+     * Set comments. 
+     * 
+     * @param comments
+     */
+    public void setComments(String comments) { this.comments = comments; }
 
-	public void setComments(String comments) {
-		this.comments = comments;
-	}
+    /** 
+     * isLicensePreactivation<p>
+     * Whether licenses are pre-activated. 
+     * 
+     * @return isLicensePreactivation
+     */
+    public boolean isLicensePreactivation() { return licensePreactivation; }
 
-	public boolean isLicensePreactivation() {
-		return licensePreactivation;
-	}
+    /** 
+     * setLicensePreactivation<p>
+     * Set pre-activation flag. 
+     * 
+     * @param licensePreactivation
+     */
+    public void setLicensePreactivation(boolean licensePreactivation) { this.licensePreactivation = licensePreactivation; }
 
-	public void setLicensePreactivation(boolean licensePreactivation) {
-		this.licensePreactivation = licensePreactivation;
-	}
+    /** 
+     * getMetadata<p>
+     * Return pack metadata entries. 
+     * 
+     * @return metadata
+     */
+    public Set<PackMetadata> getMetadata() { return metadata; }
 
-	public Set<PackMetadata> getMetadata() {
-		return metadata;
-	}
+    /** 
+     * setMetadata<p>
+     * Set pack metadata entries. 
+     * 
+     * @param metadata
+     */
+    public void setMetadata(Set<PackMetadata> metadata) { this.metadata = metadata; }
 
-	public void setMetadata(Set<PackMetadata> metadata) {
-		this.metadata = metadata;
-	}
+    /** 
+     * getStatus<p>
+     * Return pack status. 
+     * 
+     * @return packStatus
+     */
+    public PackStatus getStatus() { return status; }
 
-	public PackStatus getStatus() {
-		return status;
-	}
+    /** 
+     * setStatus<p>
+     * Set pack status. 
+     * 
+     * @param packStatus
+     */
+    public void setStatus(PackStatus status) { this.status = status; }
 
-	public void setStatus(PackStatus status) {
-		this.status = status;
-	}
+    /** 
+     * getInitValidDate<p>
+     * Return start of validity window. 
+     * 
+     * @return initValidDate
+     */
+    public Date getInitValidDate() { return initValidDate; }
 
-	public Date getInitValidDate() {
-		return initValidDate;
-	}
+    /** 
+     * setInitValidDate<p>
+     * Set start of validity window. 
+     * 
+     * @param initValidDate
+     */
+    public void setInitValidDate(Date initValidDate) { this.initValidDate = initValidDate; }
 
-	public void setInitValidDate(Date initValidDate) {
-		this.initValidDate = initValidDate;
-	}
+    /** 
+     * getEndValidDate<p>
+     * Return end of validity window. 
+     * 
+     * @return endValidDate
+     */
+    public Date getEndValidDate() { return endValidDate; }
 
-	public Date getEndValidDate() {
-		return endValidDate;
-	}
+    /** 
+     * setEndValidDate<p>
+     * Set end of validity window. 
+     * 
+     * @param endValidDate
+     */
+    public void setEndValidDate(Date endValidDate) { this.endValidDate = endValidDate; }
 
-	public void setEndValidDate(Date endValidDate) {
-		this.endValidDate = endValidDate;
-	}
+    /** 
+     * getLicenses<p>
+     * Return contained licenses (entity set). 
+     * 
+     * @return licenses
+     */
+    public Set<License> getLicenses() { return licenses; }
 
-	public Set<License> getLicenses() {
-		return licenses;
-	}
+    /** 
+     * setLicenses<p>
+     * Set contained licenses (entity set). 
+     * 
+     * @param licenses
+     */
+    public void setLicenses(Set<License> licenses) { this.licenses = licenses; }
 
-	public void setLicenses(Set<License> licenses) {
-		this.licenses = licenses;
-	}
+    /** 
+     * getPreactivationValidPeriod<p>
+     * Return preactivation validity (days). 
+     * 
+     * @return preactivationValidPeriod
+     */
+    public Integer getPreactivationValidPeriod() { return preactivationValidPeriod; }
 
-	public Integer getPreactivationValidPeriod() {
-		return preactivationValidPeriod;
-	}
+    /** 
+     * setPreactivationValidPeriod<p>
+     * Set preactivation validity (days). 
+     * 
+     * @param preactivationValidPeriod
+     */
+    public void setPreactivationValidPeriod(Integer preactivationValidPeriod) { this.preactivationValidPeriod = preactivationValidPeriod; }
 
-	public void setPreactivationValidPeriod(Integer preactivationValidPeriod) {
-		this.preactivationValidPeriod = preactivationValidPeriod;
-	}
+    /** 
+     * getRenewValidPeriod<p>
+     * Return renewal validity (days). 
+     * 
+     * @return renewValidPeriod
+     */
+    public Integer getRenewValidPeriod() { return renewValidPeriod; }
 
-	public Integer getRenewValidPeriod() {
-		return renewValidPeriod;
-	}
+    /** 
+     * setRenewValidPeriod<p>
+     * Set renewal validity (days). 
+     * 
+     * @param renewValidPeriod
+     */
+    public void setRenewValidPeriod(Integer renewValidPeriod) { this.renewValidPeriod = renewValidPeriod; }
 
-	public void setRenewValidPeriod(Integer renewValidPeriod) {
-		this.renewValidPeriod = renewValidPeriod;
-	}
+    // ---------------- Object methods ----------------
 
-	@Override
-	public boolean equals(Object obj) {
-		if (!(obj instanceof Application))
-			return false;
-		return id.equals(Pack.class.cast(obj).id);
-	}
+    /**
+     * equals<p>
+     * Compare the current object with the given object
+     * 
+     * @param object
+     * @return isEquals
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof Application)) return false;
+        return id != null && id.equals(Pack.class.cast(obj).id);
+    }
 
-	@Override
-	public int hashCode() {
-		return (id == null ? 0 : id.hashCode());
-	}
+    /**
+     * hashCode<p>
+     * Get the object hashCode
+     * 
+     * @return hashCode
+     */
+    @Override
+    public int hashCode() { return (id == null ? 0 : id.hashCode()); }
 
-	@Override
-	public String toString() {
-		return String.format("Pack: ID: %d, code: %s", id, code);
-	}
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
+    @Override
+    public String toString() { return String.format("Pack: ID: %d, code: %s", id, code); }
 
-	public boolean isFrozen() {
-		return frozen != null && frozen;
-	}
+    /** 
+     * isFrozen<p>
+     * Null-safe boolean getter. 
+     * 
+     * @return isFrozen
+     */
+    public boolean isFrozen() { return frozen != null && frozen; }
 
-	public void setFrozen(Boolean frozen) {
-		this.frozen = frozen;
-	}
+    /** 
+     * setFrozen<p>
+     * Set frozen flag (nullable wrapper). 
+     * 
+     * @param frozen
+     */
+    public void setFrozen(Boolean frozen) { this.frozen = frozen; }
 
-	public static class Action {
-		public static final int CREATE = 1;
-		public static final int ACTIVATION = 2;
-		public static final int PUT_ONHOLD = 3;
-		public static final int CANCEL = 4;
-		public static final int DELETE = 5;
-	}
+    // ---------------- Status transitions ----------------
 
-	public static class Status {
+    /**
+     * Action<p>
+     * Available actions for the Pack
+     */
+    public static class Action {
+        public static final int CREATE = 1;
+        public static final int ACTIVATION = 2;
+        public static final int PUT_ONHOLD = 3;
+        public static final int CANCEL = 4;
+        public static final int DELETE = 5;
+    }
 
-		private static final Map<Integer, List<PackStatus>> transitions = Utils.createMap( //
-				Action.ACTIVATION, Arrays.asList(PackStatus.CREATED, PackStatus.ON_HOLD, PackStatus.EXPIRED), //
-				Action.PUT_ONHOLD, Arrays.asList(PackStatus.ACTIVE), //
-				Action.CANCEL, Arrays.asList(PackStatus.ACTIVE, PackStatus.ON_HOLD, PackStatus.EXPIRED), //
-				Action.DELETE, Arrays.asList(PackStatus.CANCELLED, PackStatus.CREATED) //
-		);
+    /**
+     * Status<p>
+     * Pack status
+     */
+    public static class Status {
 
-		/**
-		 * It checks if a given action is valid for the License, passing the
-		 * action and the current license status
-		 * 
-		 * @param oldStatus
-		 * @param newStatus
-		 * @return
-		 */
-		public static boolean isActionValid(Integer action, PackStatus currentStatus) {
-			List<PackStatus> validStatuses = transitions.get(action);
+        private static final Map<Integer, List<PackStatus>> transitions = Utils.createMap(
+            Action.ACTIVATION, Arrays.asList(PackStatus.CREATED, PackStatus.ON_HOLD, PackStatus.EXPIRED),
+            Action.PUT_ONHOLD, Arrays.asList(PackStatus.ACTIVE),
+            Action.CANCEL,    Arrays.asList(PackStatus.ACTIVE, PackStatus.ON_HOLD, PackStatus.EXPIRED),
+            Action.DELETE,    Arrays.asList(PackStatus.CANCELLED, PackStatus.CREATED)
+        );
 
-			return validStatuses != null && validStatuses.contains(currentStatus);
-		}
-	}
+        /**
+        * isActionValid<p>
+        * Validate whether an action is allowed given the current pack status.
+        *
+        * @param action action constant
+        * @param currentStatus current pack status
+        * @return true if allowed
+        */
+        public static boolean isActionValid(Integer action, PackStatus currentStatus) {
+            List<PackStatus> validStatuses = transitions.get(action);
+            return validStatuses != null && validStatuses.contains(currentStatus);
+        }
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/PackMetadata.java b/securis/src/main/java/net/curisit/securis/db/PackMetadata.java
index d35b22c..e690db5 100644
--- a/securis/src/main/java/net/curisit/securis/db/PackMetadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/PackMetadata.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -22,106 +25,201 @@
 import net.curisit.securis.db.common.Metadata;
 
 /**
- * Entity implementation class for Entity: pack_metadata
- * 
- */
+* PackMetadata
+* <p>
+* Single metadata entry (key/value/flags) attached to a {@link Pack}.
+* Uses composite PK: (pack_id, key).
+*
+* Mapping details:
+* - Table: pack_metadata
+* - PK: pack_id + key (two @Id fields)
+* - 'pack' is @JsonIgnore to reduce payload size in list views
+* - NamedQuery: list-pack-metadata by pack id
+*
+* Flags:
+* - readonly: UI hint to prevent edits
+* - mandatory: requires value on pack creation/updates
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @Entity
 @Table(name = "pack_metadata")
 @JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-pack-metadata", query = "SELECT a FROM PackMetadata a where a.pack.id = :packId") })
+@NamedQueries({
+    @NamedQuery(name = "list-pack-metadata",
+                query = "SELECT a FROM PackMetadata a where a.pack.id = :packId")
+})
 public class PackMetadata implements Serializable, Metadata {
 
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-	@Id
-	@JsonIgnore
-	@ManyToOne
-	@JoinColumn(name = "pack_id")
-	private Pack pack;
+    /** PK part: owning pack (ignored in JSON). */
+    @Id
+    @JsonIgnore
+    @ManyToOne
+    @JoinColumn(name = "pack_id")
+    private Pack pack;
 
-	@Id
-	@Column(name = "\"key\"")
-	private String key;
+    /** PK part: metadata key (quoted column name). */
+    @Id
+    @Column(name = "\"key\"")
+    private String key;
 
-	private String value;
+    /** Metadata value. */
+    private String value;
 
-	private boolean readonly;
+    /** Whether this field can be edited by clients. */
+    private boolean readonly;
 
-	private boolean mandatory;
+    /** Whether this field is required. */
+    private boolean mandatory;
 
-	@JsonProperty("pack_id")
-	public Integer getPackId() {
-		return pack == null ? null : pack.getId();
-	}
+    // -------- JSON helpers to expose pack id --------
 
-	@JsonProperty("pack_id")
-	public void setLicenseTypeId(Integer idPack) {
-		if (idPack == null) {
-			pack = null;
-		} else {
-			pack = new Pack();
-			pack.setId(idPack);
-		}
-	}
+    /** 
+     * getPackId<p>
+     * Expose pack id as JSON scalar. 
+     * 
+     * @return packId
+     */
+    @JsonProperty("pack_id")
+    public Integer getPackId() {
+        return pack == null ? null : pack.getId();
+    }
 
-	public Pack getPack() {
-		return pack;
-	}
+    /** 
+     * setLicenseTypeId<p>
+     * Setter by id (creates shallow Pack).
+     * 
+     * @param packId
+     */
+    @JsonProperty("pack_id")
+    public void setLicenseTypeId(Integer idPack) {
+        if (idPack == null) {
+            pack = null;
+        } else {
+            pack = new Pack();
+            pack.setId(idPack);
+        }
+    }
 
-	public void setPack(Pack pack) {
-		this.pack = pack;
-	}
+    // -------- Getters & setters --------
 
-	public String getValue() {
-		return value;
-	}
+    /** 
+     * getPack<p>
+     * Return owning pack (entity). 
+     * 
+     * @return pack
+     */
+    public Pack getPack() { return pack; }
 
-	public void setValue(String value) {
-		this.value = value;
-	}
+    /** 
+     * setPack<p>
+     * Set owning pack (entity). 
+     * 
+     * @param pack
+     */
+    public void setPack(Pack pack) { this.pack = pack; }
 
-	public String getKey() {
-		return key;
-	}
+    /** 
+     * getValue<p>
+     * Return metadata value. 
+     * 
+     * @return metadataValue
+     */
+    public String getValue() { return value; }
 
-	public void setKey(String key) {
-		this.key = key;
-	}
+    /** 
+     * setValue<p>
+     * Set the metadata value. 
+     * 
+     * @param metadataValue
+     */
+    public void setValue(String value) { this.value = value; }
 
-	public boolean isReadonly() {
-		return readonly;
-	}
+    /** 
+     * getKey<p>
+     * Return metadata key (PK part). 
+     * 
+     * @return key
+     */
+    public String getKey() { return key; }
 
-	public void setReadonly(boolean readonly) {
-		this.readonly = readonly;
-	}
+    /** 
+     * setKey<p>
+     * Set metadata key (PK part). 
+     * 
+     * @param key
+     */
+    public void setKey(String key) { this.key = key; }
 
-	public boolean isMandatory() {
-		return mandatory;
-	}
+    /** 
+     * isReadonly<p>
+     * Return read-only flag. 
+     * 
+     * @return isReadonly
+     */
+    public boolean isReadonly() { return readonly; }
 
-	public void setMandatory(boolean mandatory) {
-		this.mandatory = mandatory;
-	}
+    /** 
+     * setReadonly<p>
+     * Set read-only flag.
+     * 
+     * @param readonly
+     */
+    public void setReadonly(boolean readonly) { this.readonly = readonly; }
 
-	@Override
-	public String toString() {
-		return String.format("PackMD (%s: %s)", key, value);
-	}
+    /** 
+     * isMandatory<p>
+     * Return mandatory flag. 
+     * 
+     * @return isMandatory
+     */
+    public boolean isMandatory() { return mandatory; }
 
-	@Override
-	public boolean equals(Object obj) {
-		if (!(obj instanceof PackMetadata))
-			return false;
-		PackMetadata other = (PackMetadata) obj;
-		return Objects.equals(key, other.key) && Objects.equals(pack, other.pack);
-	}
+    /** 
+     * setMandatory<p>
+     * Set mandatory flag.
+     * 
+     * @param mandatory
+     */
+    public void setMandatory(boolean mandatory) { this.mandatory = mandatory; }
 
-	@Override
-	public int hashCode() {
-		return Objects.hash(key, pack);
-	}
+    // -------- Object methods --------
 
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
+    @Override
+    public String toString() { return String.format("PackMD (%s: %s)", key, value); }
+
+    /**
+     * equals<p>
+     * Compare the current object with the given object
+     * 
+     * @param object
+     * @return isEquals
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof PackMetadata)) return false;
+        PackMetadata other = (PackMetadata) obj;
+        return Objects.equals(key, other.key) && Objects.equals(pack, other.pack);
+    }
+
+    /**
+     * hashCode<p>
+     * Get the object hashCode
+     * 
+     * @return hashCode
+     */
+    @Override
+    public int hashCode() { return Objects.hash(key, pack); }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/PackStatus.java b/securis/src/main/java/net/curisit/securis/db/PackStatus.java
index 2588fe2..aa1a841 100644
--- a/securis/src/main/java/net/curisit/securis/db/PackStatus.java
+++ b/securis/src/main/java/net/curisit/securis/db/PackStatus.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import net.curisit.securis.db.common.CodedEnum;
@@ -6,38 +9,58 @@
 import com.fasterxml.jackson.annotation.JsonValue;
 
 /**
- * Contains the possible pack statuses. For further details:
- * https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
- * 
- * @author rob
- */
+* PackStatus
+* <p>
+* Enumerates possible pack lifecycle statuses. JSON representation is the short code.
+* See: https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 public enum PackStatus implements CodedEnum {
+	
+	// Available status for the pack
     CREATED("CR"), ACTIVE("AC"), ON_HOLD("OH"), EXPIRED("EX"), CANCELLED("CA");
 
     private final String code;
 
-    PackStatus(String code) {
-        this.code = code;
-    }
+    /**
+     * PackStatus<p>
+     * Constructor
+     * 
+     * @param code
+     */
+    PackStatus(String code) { this.code = code; }
 
-    @Override
-    public String getCode() {
-        return code;
-    }
+    /** 
+     * getCode<p>
+     * Short code stored in DB / used in JSON. 
+     * 
+     * @return packCode
+     */
+    @Override public String getCode() { return code; }
 
+    /** 
+     * valueFromCode<p>
+     * Factory method from short code. 
+     * 
+     * @param packCode
+     * @return packStatus
+     */
     @JsonCreator
     public static PackStatus valueFromCode(String code) {
         for (PackStatus ps : PackStatus.values()) {
-            if (ps.code.equals(code)) {
-                return ps;
-            }
+            if (ps.code.equals(code)) return ps;
         }
         return null;
     }
 
+    /** 
+     * getName<p>
+     * Expose short code as JSON value. 
+     * 
+     * @return name
+     */
     @JsonValue
-    public String getName() {
-        return this.code;
-    }
-
+    public String getName() { return this.code; }
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/Settings.java b/securis/src/main/java/net/curisit/securis/db/Settings.java
index 74336e1..ed28e2c 100644
--- a/securis/src/main/java/net/curisit/securis/db/Settings.java
+++ b/securis/src/main/java/net/curisit/securis/db/Settings.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -20,63 +23,103 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- * Entity implementation class for Entity: settings settings is a table that has
- * rows with 3 columns: "key", "value", "timestamp"
- *
- */
-@Entity()
-@EntityListeners({
-    ModificationTimestampListener.class
-})
+* Settings
+* <p>
+* Simple key/value store with last modification timestamp.
+* Table rows have columns: "key", "value", "modification_timestamp".
+*
+* Mapping details:
+* - Table: settings
+* - Listeners: {@link ModificationTimestampListener}
+* - NamedQuery: get-param by key
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
+@Entity
+@EntityListeners({ ModificationTimestampListener.class })
 @Table(name = "settings")
 @NamedQueries({
     @NamedQuery(name = "get-param", query = "SELECT p FROM Settings p where p.key = :key")
 })
 public class Settings implements ModificationTimestampEntity, Serializable {
+
     @SuppressWarnings("unused")
     private static final Logger LOG = LogManager.getLogger(Settings.class);
 
     private static final long serialVersionUID = 1L;
 
+    /** Primary key: setting key. */
     @Id
     String key;
 
+    /** Setting value as string. */
     String value;
 
+    /** Last modification timestamp. */
     @Column(name = "modification_timestamp")
     @JsonProperty("modification_timestamp")
     private Date modificationTimestamp;
 
-    public String getKey() {
-        return key;
-    }
+    // -------- Getters/setters --------
 
-    public void setKey(String key) {
-        this.key = key;
-    }
+    /** 
+     * getKey<p>
+     * Return setting key. 
+     * 
+     * @return key
+     */
+    public String getKey() { return key; }
 
-    public String getValue() {
-        return value;
-    }
+    /** 
+     * setKey<p>
+     * Set setting key.
+     * 
+     * @param key
+     */
+    public void setKey(String key) { this.key = key; }
 
-    public void setValue(String value) {
-        this.value = value;
-    }
+    /** 
+     * getValue<p>
+     * Return value.
+     * 
+     * @return value
+     */
+    public String getValue() { return value; }
 
+    /** 
+     * setValue<p>
+     * Set value. 
+     * 
+     * @param value
+     */
+    public void setValue(String value) { this.value = value; }
+
+    /** 
+     * getModificationTimestamp<p>
+     * Required by ModificationTimestampEntity. 
+     * 
+     * @return modificationTimestamp
+     */
     @Override
-    public Date getModificationTimestamp() {
-        return modificationTimestamp;
-    }
+    public Date getModificationTimestamp() { return modificationTimestamp; }
 
+    /** 
+     * setModificationTimestamp<p>
+     * Required by ModificationTimestampEntity. 
+     * 
+     * @param modificationTimestamp
+     */
     @Override
-    public void setModificationTimestamp(Date modificationTimestamp) {
-        this.modificationTimestamp = modificationTimestamp;
-    }
+    public void setModificationTimestamp(Date modificationTimestamp) { this.modificationTimestamp = modificationTimestamp; }
 
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
     @Override
-    public String toString() {
-
-        return String.format("{key: %s, value: %s, ts: %s}", key, value, modificationTimestamp);
-    }
-
+    public String toString() { return String.format("{key: %s, value: %s, ts: %s}", key, value, modificationTimestamp); }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/User.java b/securis/src/main/java/net/curisit/securis/db/User.java
index 052082f..13a0f51 100644
--- a/securis/src/main/java/net/curisit/securis/db/User.java
+++ b/securis/src/main/java/net/curisit/securis/db/User.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db;
 
 import java.io.Serializable;
@@ -27,275 +30,426 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- * Entity implementation class for Entity: Users
- * 
- */
+* User
+* <p>
+* Application user with bitmask-based roles and membership in organizations
+* and applications. Exposes convenience JSON properties to fetch/set related
+* entity IDs without fetching full entities.
+*
+* Mapping details:
+* - Table: user
+* - ManyToMany organizations via user_organization
+* - ManyToMany applications via user_application
+* - Named queries: list-users, get-user, auth-user, delete-all-users
+*
+* Roles:
+* - Stored as integer bitmask; see {@link Rol}.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @JsonAutoDetect
 @JsonInclude(Include.NON_NULL)
 @JsonIgnoreProperties(ignoreUnknown = true)
 @Entity
 @Table(name = "user")
-@NamedQueries({ @NamedQuery(name = "list-users", query = "SELECT u FROM User u"), @NamedQuery(name = "get-user", query = "SELECT u FROM User u where u.username = :username"),
-		@NamedQuery(name = "auth-user", query = "SELECT u FROM User u where u.username = :username and u.password = :password"),
-		@NamedQuery(name = "delete-all-users", query = "delete FROM User u") })
+@NamedQueries({
+    @NamedQuery(name = "list-users", query = "SELECT u FROM User u"),
+    @NamedQuery(name = "get-user", query = "SELECT u FROM User u where u.username = :username"),
+    @NamedQuery(name = "auth-user", query = "SELECT u FROM User u where u.username = :username and u.password = :password"),
+    @NamedQuery(name = "delete-all-users", query = "delete FROM User u")
+})
 public class User implements Serializable {
 
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-	@Id
-	private String username;
+    /** Username (PK). */
+    @Id
+    private String username;
 
-	private String password;
+    /** Password hash/string (not exposed in JSON). */
+    private String password;
 
-	@JsonProperty(value = "first_name")
-	@Column(name = "first_name")
-	private String firstName;
+    @JsonProperty("first_name")
+    @Column(name = "first_name")
+    private String firstName;
 
-	@JsonProperty(value = "last_name")
-	@Column(name = "last_name")
-	private String lastName;
+    @JsonProperty("last_name")
+    @Column(name = "last_name")
+    private String lastName;
 
-	private int roles;
+    /** Roles bitmask (see Rol constants). */
+    private int roles;
 
-	@Column(name = "last_login")
-	private Date lastLogin;
+    @Column(name = "last_login")
+    private Date lastLogin;
 
-	@Column(name = "modification_timestamp")
-	private Date modificationTimestamp;
+    @Column(name = "modification_timestamp")
+    private Date modificationTimestamp;
 
-	@Column(name = "creation_timestamp")
-	@JsonProperty("creation_timestamp")
-	private Date creationTimestamp;
+    @Column(name = "creation_timestamp")
+    @JsonProperty("creation_timestamp")
+    private Date creationTimestamp;
 
-	private String lang;
+    private String lang;
+    private String email;
 
-	private String email;
+    @JsonIgnore
+    @ManyToMany
+    @JoinTable(name = "user_organization",
+        joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") },
+        inverseJoinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") })
+    private Set<Organization> organizations;
 
-	@JsonIgnore
-	@ManyToMany
-	@JoinTable(name = "user_organization", //
-			joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") }, //
-			inverseJoinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") } //
-	)
-	private Set<Organization> organizations;
+    @JsonIgnore
+    @ManyToMany
+    @JoinTable(name = "user_application",
+        joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") },
+        inverseJoinColumns = { @JoinColumn(name = "application_id", referencedColumnName = "id") })
+    private Set<Application> applications;
 
-	@JsonIgnore
-	@ManyToMany
-	@JoinTable(name = "user_application", //
-			joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") }, //
-			inverseJoinColumns = { @JoinColumn(name = "application_id", referencedColumnName = "id") } //
-	)
-	private Set<Application> applications;
+    // -------- Getters & setters --------
 
-	public String getUsername() {
-		return username;
-	}
+    /** 
+     * getUsername<p>
+     * Return username (PK). 
+     * 
+     * @return username
+     */
+    public String getUsername() { return username; }
 
-	public void setUsername(String username) {
-		this.username = username;
-	}
+    /** 
+     * setUsername<p>
+     * Set username (PK). 
+     * 
+     * @param username
+     */
+    public void setUsername(String username) { this.username = username; }
 
-	@JsonProperty("password")
-	public String getDummyPassword() {
-		return null;
-	}
+    /**
+    * getDummyPassword<p>
+    * Forces password to be omitted in JSON responses.
+    *
+    * @return always null
+    */
+    @JsonProperty("password")
+    public String getDummyPassword() { return null; }
 
-	public String getPassword() {
-		return password;
-	}
+    /** 
+     * getPassword<p>
+     * Return raw/hashed password (internal use). 
+     * 
+     * @return password
+     */
+    public String getPassword() { return password; }
 
-	public void setPassword(String password) {
-		this.password = password;
-	}
+    /** 
+     * setPassword<p>
+     * Set raw/hashed password (internal use).
+     * 
+     * @param password
+     */
+    public void setPassword(String password) { this.password = password; }
 
-	public List<Integer> getRoles() {
-		if (roles == 0) {
-			return null;
-		}
-		List<Integer> aux = new ArrayList<>();
-		for (int rol : Rol.ALL) {
-			if ((roles & rol) != 0) { // Each rol is a number with only 1 bit ==
-											// 1 in binary representation
-				aux.add(rol);
-			}
-		}
-		return aux;
-	}
+    /**
+    * getRoles<p>
+    * Return list of individual role flags contained in the bitmask.
+    *
+    * @return list of role integers or null if no roles
+    */
+    public List<Integer> getRoles() {
+        if (roles == 0) return null;
+        List<Integer> aux = new ArrayList<>();
+        for (int rol : Rol.ALL) {
+            if ((roles & rol) != 0) aux.add(rol);
+        }
+        return aux;
+    }
 
-	public void setRoles(List<Integer> roles) {
-		this.roles = 0;
-		if (roles != null) {
-			for (Integer rol : roles) {
-				this.roles |= rol;
-			}
-		}
-	}
+    /**
+    * setRoles<p>
+    * Set the roles bitmask from a list of role flags.
+    *
+    * @param roles list of flags
+    */
+    public void setRoles(List<Integer> roles) {
+        this.roles = 0;
+        if (roles != null) {
+            for (Integer rol : roles) this.roles |= rol;
+        }
+    }
 
-	public String getFirstName() {
-		return firstName;
-	}
+    /** 
+     * getFirstName<p>
+     * Return first name. 
+     * 
+     * @return firstName
+     */
+    public String getFirstName() { return firstName; }
 
-	public void setFirstName(String firstName) {
-		this.firstName = firstName;
-	}
+    /** 
+     * setFirstName<p>
+     * Set first name. 
+     * 
+     * @param firstName
+     */
+    public void setFirstName(String firstName) { this.firstName = firstName; }
 
-	public String getLastName() {
-		return lastName;
-	}
+    /** 
+     * getLastName<p>
+     * Return last name. 
+     * 
+     * @return lastName
+     */
+    public String getLastName() { return lastName; }
 
-	public void setLastName(String lastName) {
-		this.lastName = lastName;
-	}
+    /** 
+     * setLastName<p>
+     * Set last name. 
+     * 
+     * @param lastName
+     */
+    public void setLastName(String lastName) { this.lastName = lastName; }
 
-	public Date getLastLogin() {
-		return lastLogin;
-	}
+    /** 
+     * getLastLogin<p>
+     * Return last login timestamp. 
+     * 
+     * @return lastLogin
+     */
+    public Date getLastLogin() { return lastLogin; }
 
-	public void setLastLogin(Date lastLogin) {
-		this.lastLogin = lastLogin;
-	}
+    /** 
+     * setLastLogin<p>
+     * Set last login timestamp. 
+     * 
+     * @param lastLogin
+     */
+    public void setLastLogin(Date lastLogin) { this.lastLogin = lastLogin; }
 
-	public Date getModificationTimestamp() {
-		return modificationTimestamp;
-	}
+    /** 
+     * getModificationTimestamp<p>
+     * Return modification timestamp. 
+     * 
+     * @return modificationTimestamp
+     */
+    public Date getModificationTimestamp() { return modificationTimestamp; }
 
-	public void setModificationTimestamp(Date modificationTimestamp) {
-		this.modificationTimestamp = modificationTimestamp;
-	}
+    /** 
+     * setModificationTimestamp<p>
+     * Set modification timestamp. 
+     * 
+     * @param modificationTimestamp
+     */
+    public void setModificationTimestamp(Date modificationTimestamp) { this.modificationTimestamp = modificationTimestamp; }
 
-	public Date getCreationTimestamp() {
-		return creationTimestamp;
-	}
+    /** 
+     * getCreationTimestamp<p>
+     * Return creation timestamp. 
+     * 
+     * @return creationTimestamp
+     */
+    public Date getCreationTimestamp() { return creationTimestamp; }
 
-	public void setCreationTimestamp(Date creationTimestamp) {
-		this.creationTimestamp = creationTimestamp;
-	}
+    /** 
+     * setCreationTimestamp<p>
+     * Set creation timestamp. 
+     * 
+     * @param creationTimestamp
+     */
+    public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 
-	@Override
-	public String toString() {
-		return "{User: " + username + " Name: " + firstName + " " + lastName + ", last login: " + lastLogin + "}";
-	}
+    /** 
+     * getLang<p>
+     * Return preferred language. 
+     * 
+     * @return lang
+     */
+    public String getLang() { return lang; }
 
-	public String getLang() {
-		return lang;
-	}
+    /** 
+     * setLang<p>
+     * Set preferred language. 
+     * 
+     * @param lang
+     */
+    public void setLang(String lang) { this.lang = lang; }
 
-	public void setLang(String lang) {
-		this.lang = lang;
-	}
+    /** 
+     * getEmail<p>
+     * Return email address. 
+     * 
+     * @return email
+     */
+    public String getEmail() { return email; }
 
-	public Set<Organization> getOrganizations() {
-		return organizations;
-	}
+    /** 
+     * setEmail<p>
+     * Set email address. 
+     * 
+     * @param email
+     */
+    public void setEmail(String email) { this.email = email; }
 
-	public void setOrganizations(Set<Organization> organizations) {
-		this.organizations = organizations;
-	}
+    /** 
+     * getOrganizations<p>
+     * Return organizations (entity set). 
+     * 
+     * @return organizations
+     */
+    public Set<Organization> getOrganizations() { return organizations; }
 
-	public Set<Application> getApplications() {
-		return applications;
-	}
+    /** 
+     * setOrganizations<p>
+     * Set organizations (entity set). 
+     * 
+     * @param organizations
+     */
+    public void setOrganizations(Set<Organization> organizations) { this.organizations = organizations; }
 
-	public void setApplications(Set<Application> applications) {
-		this.applications = applications;
-	}
+    /** 
+     * getApplications<p>
+     * Return applications (entity set). 
+     * 
+     * @return applications
+     */
+    public Set<Application> getApplications() { return applications; }
 
-	@JsonProperty("organizations_ids")
-	public void setOrgsIds(List<Integer> orgsIds) {
-		organizations = new HashSet<>();
-		for (Integer orgid : orgsIds) {
-			Organization o = new Organization();
-			o.setId(orgid);
-			organizations.add(o);
-		}
-	}
+    /** 
+     * setApplications<p>
+     * Set applications (entity set). 
+     * 
+     * @param applications
+     */
+    public void setApplications(Set<Application> applications) { this.applications = applications; }
 
-	@JsonProperty("organizations_ids")
-	public Set<Integer> getOrgsIds() {
-		if (organizations == null) {
-			return null;
-		}
-		Set<Integer> ids = new HashSet<>();
-		for (Organization org : organizations) {
-			ids.add(org.getId());
-		}
-		return ids;
-	}
+    // -------- JSON helpers for related IDs --------
 
-	@JsonProperty("applications_ids")
-	public void setAppsIds(Collection<Integer> appIds) {
-		applications = new HashSet<>();
-		for (Integer appid : appIds) {
-			Application a = new Application();
-			a.setId(appid);
-			applications.add(a);
-		}
-	}
+    /** 
+     * setOrgsIds<p>
+     * Replace organizations from a list of org IDs. 
+     * 
+     * @param orgsIds
+     */
+    @JsonProperty("organizations_ids")
+    public void setOrgsIds(List<Integer> orgsIds) {
+        organizations = new HashSet<>();
+        for (Integer orgid : orgsIds) {
+            Organization o = new Organization();
+            o.setId(orgid);
+            organizations.add(o);
+        }
+    }
 
-	@JsonProperty("applications_ids")
-	public Set<Integer> getAppsIds() {
-		if (applications == null) {
-			return null;
-		}
-		Set<Integer> ids = new HashSet<>();
-		for (Application app : applications) {
-			ids.add(app.getId());
-		}
-		return ids;
-	}
+    /** 
+     * getOrgsIds<p>
+     * Expose organization IDs.
+     * 
+     * @return orgsIds
+     */
+    @JsonProperty("organizations_ids")
+    public Set<Integer> getOrgsIds() {
+        if (organizations == null) return null;
+        Set<Integer> ids = new HashSet<>();
+        for (Organization org : organizations) ids.add(org.getId());
+        return ids;
+    }
 
-	@JsonIgnore
-	public Set<Integer> getAllOrgsIds() {
-		if (organizations == null) {
-			return null;
-		}
-		Set<Integer> ids = new HashSet<>();
-		includeAllOrgs(this.organizations, ids);
-		return ids;
-	}
+    /** 
+     * setAppsIds<p>
+     * Replace applications from a collection of app IDs. 
+     * 
+     * @param appIds
+     */
+    @JsonProperty("applications_ids")
+    public void setAppsIds(Collection<Integer> appIds) {
+        applications = new HashSet<>();
+        for (Integer appid : appIds) {
+            Application a = new Application();
+            a.setId(appid);
+            applications.add(a);
+        }
+    }
 
-	@JsonIgnore
-	public Set<Integer> getAllAppsIds() {
-		if (applications == null) {
-			return null;
-		}
-		Set<Integer> ids = this.applications.parallelStream().map(app -> app.getId()).collect(Collectors.toSet());
+    /** 
+     * getAppsIds<p>
+     * Expose application IDs. 
+     * 
+     * @return appsIds
+     */
+    @JsonProperty("applications_ids")
+    public Set<Integer> getAppsIds() {
+        if (applications == null) return null;
+        Set<Integer> ids = new HashSet<>();
+        for (Application app : applications) ids.add(app.getId());
+        return ids;
+    }
 
-		return ids;
-	}
+    // -------- Derived scopes --------
 
-	/**
-	 * Walk into the organization hierarchy to include all descendants
-	 * 
-	 * @param list
-	 * @param orgIds
-	 */
-	private void includeAllOrgs(Set<Organization> list, Set<Integer> orgIds) {
-		for (Organization org : list) {
-			orgIds.add(org.getId());
-			includeAllOrgs(org.getChildOrganizations(), orgIds);
-		}
-	}
+    /**
+    * getAllOrgsIds<p>
+    * Compute full organization scope including descendants.
+    *
+    * @return set of org IDs (may be null when no organizations)
+    */
+    @JsonIgnore
+    public Set<Integer> getAllOrgsIds() {
+        if (organizations == null) return null;
+        Set<Integer> ids = new HashSet<>();
+        includeAllOrgs(this.organizations, ids);
+        return ids;
+    }
 
-	public String getEmail() {
-		return email;
-	}
+    /**
+    * getAllAppsIds<p>
+    * Compute application scope (direct associations only).
+    *
+    * @return set of application IDs (may be null when no applications)
+    */
+    @JsonIgnore
+    public Set<Integer> getAllAppsIds() {
+        if (applications == null) return null;
+        return this.applications.parallelStream().map(Application::getId).collect(Collectors.toSet());
+    }
 
-	public void setEmail(String email) {
-		this.email = email;
-	}
+    /**
+    * includeAllOrgs<p>
+    * Walk organization hierarchy to include all descendants.
+    *
+    * @param list current level orgs
+    * @param orgIds accumulator of ids
+    */
+    private void includeAllOrgs(Set<Organization> list, Set<Integer> orgIds) {
+        for (Organization org : list) {
+            orgIds.add(org.getId());
+            includeAllOrgs(org.getChildOrganizations(), orgIds);
+        }
+    }
 
-	/**
-	 * Numeric rol mask. Be aware to use different bit position for each role
-	 * 
-	 * @author rob
-	 */
-	public static class Rol {
-		public static final int ADVANCE = 0x01;
-		public static final int ADMIN = 0x02;
-		public static final int BASIC = 0x04;
-		public static final int API_CLIENT = 0x80;
-		public static final int[] ALL = new int[] { ADVANCE, ADMIN, BASIC, API_CLIENT };
-	}
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
+    @Override
+    public String toString() {
+        return "{User: " + username + " Name: " + firstName + " " + lastName + ", last login: " + lastLogin + "}";
+    }
 
+
+    /**
+    * Rol
+    * <p>
+    * Bitmask constants for user roles. Each constant must occupy a distinct bit.
+    */
+    public static class Rol {
+        public static final int ADVANCE   = 0x01;
+        public static final int ADMIN     = 0x02;
+        public static final int BASIC     = 0x04;
+        public static final int API_CLIENT= 0x80;
+        public static final int[] ALL = new int[] { ADVANCE, ADMIN, BASIC, API_CLIENT };
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java b/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java
index 9d9f996..8d38250 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java
@@ -1,7 +1,23 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
+/**
+* CodedEnum
+* <p>
+* Small contract for enums persisted as short codes
+*/
 public interface CodedEnum {
 
-    public String getCode();
+    /** 
+     * getCode<p>
+     * Return the short string code for persistence/JSON.
+     * 
+     * @return codeEnum
+     */
+    String getCode();
 
 }
+
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java b/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java
index e473ea0..f01b366 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java
@@ -1,10 +1,31 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
 import java.util.Date;
 
+/**
+* CreationTimestampEntity
+* <p>
+* Contract for entities that expose a creation timestamp property.
+*/
 public interface CreationTimestampEntity {
 
-    public Date getCreationTimestamp();
+    /** 
+     * getCreationTimestamp<p>
+     * Return creation timestamp. 
+     * 
+     * @return creationTimeStamp
+     */
+    Date getCreationTimestamp();
 
-    public void setCreationTimestamp(Date creationTimestamp);
+    /** 
+     * setCreationTimestamp<p>
+     * Set creation timestamp. 
+     * 
+     * @param creationTimestamp
+     */
+    void setCreationTimestamp(Date creationTimestamp);
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java b/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java
index 8310069..e93ea56 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java
@@ -1,12 +1,30 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
 import net.curisit.securis.db.LicenseStatus;
 
+/**
+* LicenseStatusType
+* <p>
+* Hibernate user type that persists {@link LicenseStatus} using its code.
+* Delegates specifics to {@link PersistentEnumUserType}.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 public class LicenseStatusType extends PersistentEnumUserType<LicenseStatus> {
 
+    /** 
+     * returnedClass<p>
+     * Return the enum class handled by this type. 
+     * 
+     * @return licenseStatus
+     */
     @Override
     public Class<LicenseStatus> returnedClass() {
         return LicenseStatus.class;
     }
-
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/Metadata.java b/securis/src/main/java/net/curisit/securis/db/common/Metadata.java
index 8706e36..6018d5e 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/Metadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/Metadata.java
@@ -1,16 +1,64 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
+/**
+* Metadata
+* <p>
+* Contract for metadata entries (key/value/mandatory).
+* Implemented by ApplicationMetadata, LicenseTypeMetadata, PackMetadata, etc.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 public interface Metadata {
-	public String getKey();
+	
+    /** 
+     * getKey<p>
+     * Return entry key.
+     * 
+     * @return key
+     */
+    String getKey();
+    
+    /** 
+     * setKey<p>
+     * Set entry key. 
+     * 
+     * @param key
+     */
+    void setKey(String key);
 
-	public void setKey(String key);
+    /** 
+     * getValue<p>
+     * Return entry value. 
+     * 
+     * @return value
+     */
+    String getValue();
+    
+    /** 
+     * setValue<p>
+     * Set entry value. 
+     * 
+     * @param value
+     */
+    void setValue(String value);
 
-	public String getValue();
-
-	public void setValue(String value);
-
-	public boolean isMandatory();
-
-	public void setMandatory(boolean mandatory);
-
+    /** 
+     * isMandatory<p>
+     * Whether this metadata is required. 
+     * 
+     * @return isMandatory
+     */
+    boolean isMandatory();
+    
+    /** 
+     * setMandatory<p>
+     * Set required flag. 
+     * 
+     * @param mandatory
+     */
+    void setMandatory(boolean mandatory);
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java b/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java
index a59b56c..17084f8 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java
@@ -1,10 +1,39 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
 import java.util.Date;
 
+/**
+* ModificationTimestampEntity
+* <p>
+* Contract for entities that track a <b>modification timestamp</b>.
+* Typical usage: attach {@code @EntityListeners(ModificationTimestampListener.class)}
+* so the value is updated automatically on persist/update.
+*
+* Methods:
+* - {@link #getModificationTimestamp()} exposes the timestamp.
+* - {@link #setModificationTimestamp(Date)} sets the timestamp.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 public interface ModificationTimestampEntity {
 
-    public Date getModificationTimestamp();
+    /** 
+     * getModificationTimestamp<p>
+     * Return the last modification timestamp. 
+     * 
+     * @return modificationTimestamp
+     */
+    Date getModificationTimestamp();
 
-    public void setModificationTimestamp(Date modificationTimestamp);
+    /** 
+     * setModificationTimestamp<p>
+     * Set/update the last modification timestamp.
+     * 
+     * @param modificationTimestamp
+     */
+    void setModificationTimestamp(Date modificationTimestamp);
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java b/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java
index e562933..f3b81b9 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java
@@ -1,12 +1,29 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
 import net.curisit.securis.db.PackStatus;
 
+/**
+* PackStatusType
+* <p>
+* Hibernate {@code UserType} for persisting {@link PackStatus} as its short code.
+* Delegates common enum-code mapping behavior to {@link PersistentEnumUserType}.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 public class PackStatusType extends PersistentEnumUserType<PackStatus> {
 
+    /** 
+     * returnedClass<p>
+     * Return enum class handled by this type. 
+     * 
+     * @return packStatus
+     */
     @Override
     public Class<PackStatus> returnedClass() {
         return PackStatus.class;
     }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java b/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java
index 19879f2..e11a355 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
 import java.io.Serializable;
@@ -10,57 +13,150 @@
 import org.hibernate.engine.spi.SharedSessionContractImplementor;
 import org.hibernate.usertype.UserType;
 
+/**
+* PersistentEnumUserType
+* <p>
+* Base Hibernate {@link UserType} for enums implementing {@link CodedEnum}.
+* Stores the enum's <b>short code</b> (VARCHAR) and reconstructs the enum
+* from that code on load.
+*
+* Notes:
+* - SQL type is {@code VARCHAR}.
+* - Immutable by default ({@link #isMutable()} returns false).
+* - {@link #equals(Object, Object)} compares by reference (adequate for enums).
+*
+* @param <T> enum type implementing {@link CodedEnum}
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 public abstract class PersistentEnumUserType<T extends CodedEnum> implements UserType {
 
+    /** 
+     * assemble<p>
+     * Return cached value as-is (immutable semantics). 
+     * 
+     * @param cached
+     * @param owner
+     * @return assembleObject
+     * @throws HibernateException
+     */
     @Override
     public Object assemble(Serializable cached, Object owner) throws HibernateException {
         return cached;
     }
 
+    /** 
+     * deepCopy<p>
+     * Enums are immutable; return value as-is. 
+     * 
+     * @param value
+     * @return deepCopy
+     */
     @Override
     public Object deepCopy(Object value) throws HibernateException {
         return value;
     }
 
+    /** 
+     * disassemble<p>
+     * Return value for 2nd-level cache.
+     * 
+     * @param value
+     * @return disassembleObject
+     * @throw HibernateException
+     */
     @Override
     public Serializable disassemble(Object value) throws HibernateException {
         return (Serializable) value;
     }
 
+    /** 
+     * equals<p>
+     * Reference equality is fine for enums.
+     * 
+     * @param x
+     * @param y
+     * @param isEqual
+     * @throws HibernateException
+     */
     @Override
     public boolean equals(Object x, Object y) throws HibernateException {
         return x == y;
     }
 
+    /** 
+     * hashCode<p>
+     * Delegate to value hashCode; 0 for null. 
+     * 
+     * @param object
+     * @return hashCode
+     * @throws HibernateException
+     */
     @Override
     public int hashCode(Object x) throws HibernateException {
         return x == null ? 0 : x.hashCode();
     }
 
+    /** 
+     * isMutable<p>
+     * Enums are immutable. 
+     * 
+     * @return isMutable
+     */
     @Override
     public boolean isMutable() {
         return false;
     }
 
+    /** 
+     * replace<p>
+     * Immutable; return original. 
+     * 
+     * @param original
+     * @param target
+     * @param owner
+     * @return object
+     * @throws HibernateException
+     */
     @Override
     public Object replace(Object original, Object target, Object owner) throws HibernateException {
         return original;
     }
 
+    /** 
+     * returnedClass<p>
+     * Concrete types must provide the enum class. 
+     */
     @Override
     public abstract Class<T> returnedClass();
 
+    /** 
+     * sqlTypes<p>
+     * Persist as single VARCHAR column
+     * 
+     * @return sqlTypes
+     */
     @Override
     public int[] sqlTypes() {
-        return new int[] {
-            Types.VARCHAR
-        };
+        return new int[] { Types.VARCHAR };
     }
 
+    /**
+    * nullSafeGet<p>
+    * Read code from result set and map to the corresponding enum constant.
+    *
+    * @param resultSet
+    * @param names
+    * @param session
+    * @param owner
+    * @return enum instance or null if DB value is null or code not matched
+    */
     @Override
     public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
             throws HibernateException, SQLException {
         String code = rs.getString(names[0]);
+        if (code == null) return null;
         for (CodedEnum en : returnedClass().getEnumConstants()) {
             if (en.getCode().equals(code)) {
                 return en;
@@ -69,6 +165,15 @@
         return null;
     }
 
+    /**
+    * nullSafeSet<p>
+    * Write enum code as VARCHAR or set NULL if value is null.
+    * 
+    * @param statement
+    * @param value
+    * @param index
+    * @param session
+    */
     @Override
     public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
             throws HibernateException, SQLException {
@@ -78,5 +183,5 @@
             st.setString(index, ((CodedEnum) value).getCode());
         }
     }
-
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java b/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java
index 0e745f2..a21a5ca 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.common;
 
 import java.util.Date;
@@ -13,120 +16,148 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+/**
+* SystemParams
+* <p>
+* Simple façade to read/write application-wide parameters stored in the
+* {@code settings} table (key/value + timestamps).
+*
+* Features:
+* - Typed getters: {@code String}, {@code Integer}, {@code Boolean}, {@code Double}, {@code Date}.
+* - Default value overloads.
+* - Upsert semantics in {@link #setParam(String, String)} and typed variants.
+* - Removal with {@link #removeParam(String)}.
+*
+* Transaction note:
+* - Each write method starts and commits its own transaction. Rollback is invoked
+*   only on exceptions.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 @ApplicationScoped
 public class SystemParams {
 
     @SuppressWarnings("unused")
     private static final Logger LOG = LogManager.getLogger(SystemParams.class);
 
-    @Inject
-    private EntityManagerProvider emProvider;
+    @Inject private EntityManagerProvider emProvider;
+
+    // -------------------- Read API --------------------
 
     /**
-     * Returns the system parameter value for given key
-     * 
-     * @param key
-     * @return the value of the param or null if it doesn't exist
-     */
+    * getParam<p>
+    * Get raw string value or {@code null} when absent.
+    *
+    * @param key setting key
+    * @return string value or null
+    */
     public String getParam(String key) {
         return getParam(key, null);
     }
 
     /**
-     * Returns the system parameter as int value for given key
-     * 
-     * @param key
-     * @return the value of the param or null if it doesn't exist
-     */
+    * getParamAsInt<p>
+    * Get value as Integer or null when absent.
+    *
+    * @param key setting key
+    * @return integer value or null
+    */
     public Integer getParamAsInt(String key) {
         String value = getParam(key, null);
         return value == null ? null : Integer.parseInt(value);
     }
 
     /**
-     * 
-     * @param key
-     * @param defaulValue
-     *            returned if key doesn't exist in params table
-     * @return
-     */
+    * getParamAsInt<p>
+    * Get value as Integer with default.
+    *
+    * @param key setting key
+    * @param defaulValue returned if key is missing
+    * @return integer value or default
+    */
     public Integer getParamAsInt(String key, Integer defaulValue) {
         String value = getParam(key, null);
         return value == null ? defaulValue : Integer.parseInt(value);
     }
 
     /**
-     * Returns the system parameter as Date value for given key
-     * 
-     * @param key
-     * @return the value of the param or null if it doesn't exist
-     */
+    * getParamAsDate<p>
+    * Get value parsed from ISO-8601.
+    *
+    * @param key setting key
+    * @return date value or null
+    */
     public Date getParamAsDate(String key) {
         String value = getParam(key, null);
         return value == null ? null : Utils.toDateFromIso(value);
     }
 
     /**
-     * Returns the system parameter as boolean value for given key
-     * 
-     * @param key
-     * @return the value of the param or null if it doesn't exist
-     */
+    * getParamAsBool<p>
+    * Get value parsed as boolean.
+    *
+    * @param key setting key
+    * @return boolean value or null
+    */
     public Boolean getParamAsBool(String key) {
         String value = getParam(key, null);
         return value == null ? null : Boolean.parseBoolean(value);
     }
 
     /**
-     * 
-     * @param key
-     * @param defaulValue
-     *            returned if key doesn't exist in params table
-     * @return
-     */
+    * getParamAsBool<p>
+    * Get value parsed as boolean with default.
+    *
+    * @param key setting key
+    * @param defaulValue default when missing
+    * @return boolean value or default
+    */
     public Boolean getParamAsBool(String key, boolean defaulValue) {
         String value = getParam(key, null);
         return value == null ? defaulValue : Boolean.parseBoolean(value);
     }
 
     /**
-     * Returns the system parameter as boolean value for given key
-     * 
-     * @param key
-     * @return the value of the param or null if it doesn't exist
-     */
+    * getParamAsDouble<p>
+    * Get value parsed as double.
+    *
+    * @param key setting key
+    * @return double value or null
+    */
     public Double getParamAsDouble(String key) {
         String value = getParam(key, null);
         return value == null ? null : Double.parseDouble(value);
     }
 
     /**
-     * Returns the system parameter value for given key
-     * 
-     * @param key
-     * @param defaultValue
-     *            returned if key doesn't exist in params table
-     * @return
-     */
+    * getParam<p>
+    * Get raw string value or a default when missing.
+    *
+    * @param key setting key
+    * @param defaultValue default when missing
+    * @return value or default
+    */
     public String getParam(String key, String defaultValue) {
         EntityManager em = emProvider.getEntityManager();
         Settings p = em.find(Settings.class, key);
         return p == null ? defaultValue : p.getValue();
     }
 
+    // -------------------- Write API --------------------
+
     /**
-     * Returns the system parameter value passed as parameter to method
-     * 
-     * @param key
-     * @param defaultValue
-     * @return
-     */
+    * setParam<p>
+    * Upsert a parameter as string.
+    *
+    * @param key setting key
+    * @param value string value
+    */
     public void setParam(String key, String value) {
         EntityManager em = emProvider.getEntityManager();
         em.getTransaction().begin();
         try {
             Settings p = em.find(Settings.class, key);
-
             if (p == null) {
                 p = new Settings();
                 p.setKey(key);
@@ -136,15 +167,16 @@
                 p.setValue(value);
                 em.merge(p);
             }
-            em.flush();
             em.getTransaction().commit();
-        } finally {
+        } catch (Exception ex) {
             em.getTransaction().rollback();
+            throw ex;
         }
     }
 
-    /**
-     * Save a parameter as a Date
+    /** 
+     * setParam<p>
+     * Save parameter as ISO date string. 
      * 
      * @param key
      * @param value
@@ -153,8 +185,9 @@
         setParam(key, Utils.toIsoFormat(value));
     }
 
-    /**
-     * Save a parameter as a integer
+    /** 
+     * setParam<p>
+     * Save parameter as integer. 
      * 
      * @param key
      * @param value
@@ -163,8 +196,9 @@
         setParam(key, String.valueOf(value));
     }
 
-    /**
-     * Save a parameter as a boolean
+    /** 
+     * setParam<p>
+     * Save parameter as boolean. 
      * 
      * @param key
      * @param value
@@ -173,8 +207,9 @@
         setParam(key, String.valueOf(value));
     }
 
-    /**
-     * Save a parameter as a double
+    /** 
+     * setParam<p>
+     * Save parameter as double. 
      * 
      * @param key
      * @param value
@@ -184,11 +219,11 @@
     }
 
     /**
-     * Remove a parameter from params table
-     * 
-     * @param key
-     * @return
-     */
+    * removeParam<p>
+    * Delete a parameter by key (no-op if missing).
+    *
+    * @param key setting key
+    */
     public void removeParam(String key) {
         EntityManager em = emProvider.getEntityManager();
         em.getTransaction().begin();
@@ -198,13 +233,19 @@
                 em.remove(p);
             }
             em.getTransaction().commit();
-        } finally {
+        } catch (Exception ex) {
             em.getTransaction().rollback();
+            throw ex;
         }
     }
 
+    /**
+    * Keys
+    * <p>
+    * Centralized constants for parameter keys (client/common/server).
+    */
     public static class Keys {
-        // Keys used in basic app
+        // Client app keys
         public static final String CONFIG_CLIENT_HOST = "config.client.host";
         public static final String CONFIG_CLIENT_PORT = "config.client.port";
         public static final String CONFIG_CLIENT_LAST_UPDATE = "config.client.last_update";
@@ -217,9 +258,9 @@
         public static final String CONFIG_CLIENT_GS_HOST = "config.client.gs_host";
         public static final String CONFIG_CLIENT_GS_PORT = "config.client.gs_port";
 
-        // Keys used in both app
-        public static final String CONFIG_COMMON_CUSTOMER_CODE = "config.common.customer_code"; // BP
-        public static final String CONFIG_COMMON_CS_CODE = "config.common.cs_code"; // 0000
+        // Shared keys
+        public static final String CONFIG_COMMON_CUSTOMER_CODE = "config.common.customer_code";
+        public static final String CONFIG_COMMON_CS_CODE = "config.common.cs_code";
         public static final String CONFIG_COMMON_USERS_VERSION = "config.common.user_version";
         public static final String CONFIG_COMMON_SETTINGS_VERSION = "config.common.settings_version";
         public static final String CONFIG_COMMON_DATASET_VERSION = "config.common.dataset_version";
@@ -227,7 +268,7 @@
         public static final String CONFIG_COMMON_TIMEOUT_SESSION_BA = "config.common.timeout_session_ba";
         public static final String CONFIG_COMMON_TIMEOUT_SESSION_CS = "config.common.timeout_session_cs";
 
-        // Keys used in server app
+        // Server app keys
         public static final String CONFIG_SERVER_LICENSE_EXPIRATION = "config.server.license.expiation";
         public static final String CONFIG_SERVER_MAX_INSTANCES = "config.server.max_instances";
         public static final String CONFIG_SERVER_MAX_USERS = "config.server.max_users";
@@ -240,5 +281,4 @@
         public static final String CONFIG_SERVER_CREATE_DATASET = "config.server.create_dataset_in_next_startup";
         public static final String CONFIG_SERVER_PORT = "config.server.port";
     }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java b/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java
index 0b2c88a..0e858e3 100644
--- a/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java
+++ b/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.listeners;
 
 import java.util.Date;
@@ -9,14 +12,31 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+/**
+* CreationTimestampListener
+* <p>
+* JPA entity listener that sets the <b>creation timestamp</b> right before persisting.
+* Entities must implement {@link CreationTimestampEntity} to be compatible.
+*
+* Usage:
+* {@code @EntityListeners(CreationTimestampListener.class)}
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 public class CreationTimestampListener {
 
     private static final Logger log = LogManager.getLogger(CreationTimestampListener.class);
 
+    /** 
+     * updateTimestamp<p>
+     * Set creation timestamp during @PrePersist. 
+     * 
+     * @param creationTimeStampEntity
+     */
     @PrePersist
     public void updateTimestamp(CreationTimestampEntity p) {
-        log.info("Settings creation timestmap date");
+        log.info("Settings creation timestamp date");
         p.setCreationTimestamp(new Date());
     }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java b/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java
index df53613..937269f 100644
--- a/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java
+++ b/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.db.listeners;
 
 import java.util.Date;
@@ -10,15 +13,30 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+/**
+* ModificationTimestampListener
+* <p>
+* JPA entity listener that updates the <b>modification timestamp</b> on both
+* persist and update events. Entities must implement
+* {@link ModificationTimestampEntity}.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 public class ModificationTimestampListener {
 
     private static final Logger log = LogManager.getLogger(ModificationTimestampListener.class);
 
+    /** 
+     * updateTimestamp<p>
+     * Set modification timestamp during @PrePersist/@PreUpdate. 
+     * 
+     * @param modificationTimestampEntity
+     */
     @PreUpdate
     @PrePersist
     public void updateTimestamp(ModificationTimestampEntity p) {
-        log.info("Settings modification timestmap date");
+        log.info("Settings modification timestamp date");
         p.setModificationTimestamp(new Date());
     }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java b/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java
index f2e9008..8c8d23d 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.ioc;
 
 import java.lang.annotation.ElementType;
@@ -7,11 +10,20 @@
 
 import jakarta.interceptor.InterceptorBinding;
 
-@Target({
-        ElementType.METHOD, ElementType.TYPE
-})
+/**
+* EnsureTransaction
+* <p>
+* CDI interceptor binding to mark resource methods that require a
+* transaction boundary. Interceptors (e.g., in a request filter / writer
+* interceptor) can check this annotation to begin/commit/rollback.
+*
+* Usage:
+* {@code @EnsureTransaction} on JAX-RS methods.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
+@Target({ ElementType.METHOD, ElementType.TYPE })
 @Retention(RetentionPolicy.RUNTIME)
 @InterceptorBinding
-public @interface EnsureTransaction {
-
-}
+public @interface EnsureTransaction { }
diff --git a/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java b/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java
index be020f5..f702c97 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.ioc;
 
 import jakarta.enterprise.context.ApplicationScoped;
@@ -8,16 +11,38 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+/**
+* EntityManagerProvider
+* <p>
+* Simple provider for JPA {@link EntityManager} instances using the
+* persistence unit "localdb". Creates an {@link EntityManagerFactory}
+* once per application and returns a fresh {@link EntityManager} per call.
+*
+* Note:
+* - Callers are responsible for closing the obtained EntityManager.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @ApplicationScoped
 public class EntityManagerProvider {
 
     @SuppressWarnings("unused")
-	private static final Logger log = LogManager.getLogger(EntityManagerProvider.class);
+    private static final Logger log = LogManager.getLogger(EntityManagerProvider.class);
 
+    /** 
+     * entityManagerFactory<p>
+     * Application-wide EMF built from persistence.xml PU "localdb". 
+     */
     private final EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("localdb");
 
+    /**
+    * getEntityManager<p>
+    * Create a new {@link EntityManager}.
+    *
+    * @return a new EntityManager; caller must close it
+    */
     public EntityManager getEntityManager() {
         return entityManagerFactory.createEntityManager();
     }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java b/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
index fd61433..2417954 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.ioc;
 
 import java.io.IOException;
@@ -31,143 +34,214 @@
 import net.curisit.securis.utils.CacheTTL;
 import net.curisit.securis.utils.TokenHelper;
 
+/**
+* RequestsInterceptor
+* <p>
+* Authentication/authorization interceptor that:
+* <ul>
+*   <li>Loads and stores the {@link EntityManager} in the request context.</li>
+*   <li>Validates tokens for methods annotated with {@link Securable}.</li>
+*   <li>Builds a {@link BasicSecurityContext} with roles and scoped organization/application IDs.</li>
+*   <li>Manages transactions when {@code @EnsureTransaction} is present.</li>
+* </ul>
+*
+* <p><b>Cache usage:</b> Uses {@link CacheTTL} to cache roles and scope sets.
+* The new {@link CacheTTL#getSet(String, Class)} helper removes unchecked
+* conversion warnings when retrieving {@code Set<Integer>} from the cache.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @Provider
 @Priority(Priorities.AUTHENTICATION)
 public class RequestsInterceptor implements ContainerRequestFilter, WriterInterceptor {
 
-	private static final Logger LOG = LogManager.getLogger(RequestsInterceptor.class);
+    private static final Logger LOG = LogManager.getLogger(RequestsInterceptor.class);
 
-	@Context
-	private HttpServletResponse servletResponse;
+    @Inject private CacheTTL cache;
+    @Inject private TokenHelper tokenHelper;
+    @Inject private EntityManagerProvider emProvider;
 
-	@Context
-	private HttpServletRequest servletRequest;
+    @Context private HttpServletResponse servletResponse;
+    @Context private HttpServletRequest servletRequest;
+    @Context private ResourceInfo resourceInfo;
 
-	@Context
-	private ResourceInfo resourceInfo;
+    private static final String EM_CONTEXT_PROPERTY = "curisit.entitymanager";
 
-	@Inject
-	private CacheTTL cache;
+    // -------------------------------------------------------------
+    // Request filter (authN/authZ + EM handling)
+    // -------------------------------------------------------------
 
-	@Inject
-	private TokenHelper tokenHelper;
+    /** 
+     * filter<p>
+     * Entry point before resource method invocation. 
+     * 
+     * @param requestContext
+     * @throws IOException
+     */
+    @Override
+    public void filter(ContainerRequestContext requestContext) throws IOException {
+        EntityManager em = emProvider.getEntityManager();
+        LOG.debug("GETTING EM: {}", em);
 
-	@Inject
-	private EntityManagerProvider emProvider;
+        // Store EntityManager for later retrieval (writer interceptor)
+        requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
 
-	private static final String EM_CONTEXT_PROPERTY = "curisit.entitymanager";
+        Method method = resourceInfo.getResourceMethod();
 
-	@Override
-	public void filter(ContainerRequestContext requestContext) throws IOException {
-		EntityManager em = emProvider.getEntityManager();
-		LOG.debug("GETTING EM: {}", em);
+        if (checkSecurableMethods(requestContext, method)) {
+            if (method.isAnnotationPresent(EnsureTransaction.class)) {
+                LOG.debug("Beginning transaction");
+                em.getTransaction().begin();
+            }
+        }
+    }
 
-		// Guardamos el EntityManager en el contexto para recuperación posterior
-		requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
+    /**
+    * checkSecurableMethods<p>
+    * Enforce security checks for methods annotated with {@link Securable}.
+    * Builds {@link BasicSecurityContext} when authorized.
+    *
+    * @param ctx
+    * @param method
+    * @return true if request can proceed; false when aborted
+    */
+    private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) {
+        if (!method.isAnnotationPresent(Securable.class)) return true;
 
-		Method method = resourceInfo.getResourceMethod();
+        String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
+        if (token == null || !tokenHelper.isTokenValid(token)) {
+            LOG.warn("Access denied, invalid token");
+            ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
+            return false;
+        }
 
-		if (checkSecurableMethods(requestContext, method)) {
-			if (method.isAnnotationPresent(EnsureTransaction.class)) {
-				LOG.debug("Beginning transaction");
-				em.getTransaction().begin();
-			}
-		}
-	}
+        String username = tokenHelper.extractUserFromToken(token);
+        int roles = getUserRoles(username);
+        Securable securable = method.getAnnotation(Securable.class);
 
-	private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) {
-		if (!method.isAnnotationPresent(Securable.class)) return true;
+        if (securable.roles() != 0 && (securable.roles() & roles) == 0) {
+            LOG.warn("User {} lacks required roles for method {}", username, method.getName());
+            ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
+            return false;
+        }
 
-		String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
-		if (token == null || !tokenHelper.isTokenValid(token)) {
-			LOG.warn("Access denied, invalid token");
-			ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
-			return false;
-		}
+        BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure());
+        sc.setOrganizationsIds(getUserOrganizations(username));
+        sc.setApplicationsIds(getUserApplications(username));
+        ctx.setSecurityContext(sc);
+        return true;
+    }
 
-		String username = tokenHelper.extractUserFromToken(token);
-		int roles = getUserRoles(username);
-		Securable securable = method.getAnnotation(Securable.class);
+    // -------------------------------------------------------------
+    // Cached lookups (roles/orgs/apps)
+    // -------------------------------------------------------------
 
-		if (securable.roles() != 0 && (securable.roles() & roles) == 0) {
-			LOG.warn("User {} lacks required roles for method {}", username, method.getName());
-			ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
-			return false;
-		}
+    /**
+    * getUserRoles<p>
+    * Retrieve roles bitmask for the given user (cached).
+    * 
+    * @param username
+    * @return userRoles
+    */
+    private int getUserRoles(String username) {
+        if (username == null) return 0;
+        Integer cached = cache.get("roles_" + username, Integer.class);
+        if (cached != null) return cached;
 
-		BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure());
-		sc.setOrganizationsIds(getUserOrganizations(username));
-		sc.setApplicationsIds(getUserApplications(username));
-		ctx.setSecurityContext(sc);
-		return true;
-	}
+        EntityManager em = emProvider.getEntityManager();
+        User user = em.find(User.class, username);
+        int roles = 0;
+        if (user != null) {
+            List<Integer> r = user.getRoles();
+            if (r != null) for (Integer role : r) roles += role;
+            cache.set("roles_" + username, roles, 3600);
+            // also warm some caches
+            cache.set("orgs_" + username, user.getOrgsIds(), 3600);
+        }
+        return roles;
+    }
 
-	private int getUserRoles(String username) {
-		if (username == null) return 0;
-		Integer cached = cache.get("roles_" + username, Integer.class);
-		if (cached != null) return cached;
+    /**
+    * getUserOrganizations<p>
+    * Retrieve organization scope for the user as a typed {@code Set<Integer>}
+    * using the cache helper that validates element types.
+    * 
+    * @param username
+    * @return userOrganizations
+    */
+    private Set<Integer> getUserOrganizations(String username) {
+        Set<Integer> cached = cache.getSet("orgs_" + username, Integer.class);
+        if (cached != null) return cached;
 
-		EntityManager em = emProvider.getEntityManager();
-		User user = em.find(User.class, username);
-		int roles = 0;
-		if (user != null) {
-			List<Integer> r = user.getRoles();
-			if (r != null) for (Integer role : r) roles += role;
-			cache.set("roles_" + username, roles, 3600);
-			cache.set("orgs_" + username, user.getOrgsIds(), 3600);
-		}
-		return roles;
-	}
+        User user = emProvider.getEntityManager().find(User.class, username);
+        if (user != null) {
+            Set<Integer> result = user.getAllOrgsIds();
+            cache.set("orgs_" + username, result, 3600);
+            return result;
+        }
+        return Set.of();
+    }
 
-	private Set<Integer> getUserOrganizations(String username) {
-		Set<Integer> cached = cache.get("orgs_" + username, Set.class);
-		if (cached != null) return cached;
-		User user = emProvider.getEntityManager().find(User.class, username);
-		if (user != null) {
-			Set<Integer> result = user.getAllOrgsIds();
-			cache.set("orgs_" + username, result, 3600);
-			return result;
-		}
-		return Set.of();
-	}
+    /**
+    * getUserApplications<p>
+    * Retrieve application scope for the user as a typed {@code Set<Integer>}
+    * using the cache helper that validates element types.
+    * 
+    * @param username
+    * @return userApplications
+    */
+    private Set<Integer> getUserApplications(String username) {
+        Set<Integer> cached = cache.getSet("apps_" + username, Integer.class);
+        if (cached != null) return cached;
 
-	private Set<Integer> getUserApplications(String username) {
-		Set<Integer> cached = cache.get("apps_" + username, Set.class);
-		if (cached != null) return cached;
-		User user = emProvider.getEntityManager().find(User.class, username);
-		if (user != null) {
-			Set<Integer> result = user.getAllAppsIds();
-			cache.set("apps_" + username, result, 3600);
-			return result;
-		}
-		return Set.of();
-	}
+        User user = emProvider.getEntityManager().find(User.class, username);
+        if (user != null) {
+            Set<Integer> result = user.getAllAppsIds();
+            cache.set("apps_" + username, result, 3600);
+            return result;
+        }
+        return Set.of();
+    }
 
-	@Override
-	public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
-		context.proceed();
+    // -------------------------------------------------------------
+    // Writer interceptor (transaction finalize)
+    // -------------------------------------------------------------
 
-		EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY);
-		if (em == null) return;
+    /** 
+     * aroundWriteTo<p>
+     * Commit/rollback and close EM after response writing. 
+     * 
+     * @param context
+     * @throws IOException
+     * @throws WebApplicationException
+     */
+    @Override
+    public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+        context.proceed();
 
-		try {
-			if (em.getTransaction().isActive()) {
-				if (servletResponse.getStatus() == Status.OK.getStatusCode()) {
-					em.getTransaction().commit();
-					LOG.debug("Transaction committed");
-				} else {
-					em.getTransaction().rollback();
-					LOG.debug("Transaction rolled back");
-				}
-			}
-		} finally {
-			if (em.isOpen()) {
-				try {
-					em.close();
-				} catch (Exception e) {
-					LOG.error("Error closing EntityManager", e);
-				}
-			}
-		}
-	}
+        EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY);
+        if (em == null) return;
+
+        try {
+            if (em.getTransaction().isActive()) {
+                if (servletResponse.getStatus() == Status.OK.getStatusCode()) {
+                    em.getTransaction().commit();
+                    LOG.debug("Transaction committed");
+                } else {
+                    em.getTransaction().rollback();
+                    LOG.debug("Transaction rolled back");
+                }
+            }
+        } finally {
+            if (em.isOpen()) {
+                try {
+                    em.close();
+                } catch (Exception e) {
+                    LOG.error("Error closing EntityManager", e);
+                }
+            }
+        }
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java b/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java
index af144d4..66f4ad3 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.ioc;
 
 import net.curisit.securis.services.ApiResource;
@@ -11,28 +14,42 @@
 
 import com.google.inject.AbstractModule;
 
+/**
+* RequestsModule
+* <p>
+* Guice module that binds JAX-RS resource classes so they can be
+* injected and discovered by the DI container.
+* <p>
+* Notes:
+* - Currently binds resources explicitly. TODO indicates a future
+*   improvement to bind dynamically via reflection / classpath scanning.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 public class RequestsModule extends AbstractModule {
 
+    /**
+    * configure<p>
+    * Register resource types in the injector.
+    */
     @Override
     protected void configure() {
         // TODO Securis: Make the bind using reflection dynamically
-
         bind(BasicServices.class);
         bind(UserResource.class);
-
         bind(ApplicationResource.class);
         bind(LicenseTypeResource.class);
         bind(OrganizationResource.class);
         bind(ApiResource.class);
         bind(LicenseResource.class);
         bind(PackResource.class);
-
     }
 
+    // Example provider (kept commented for reference)
     // @Provides
     // @RequestScoped
     // public User provideUser() {
-    // return ResteasyProviderFactory.getContextData(User.class);
+    //   return ResteasyProviderFactory.getContextData(User.class);
     // }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java b/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java
index 8cdfa22..5bc1ef4 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.ioc;
 
 import java.io.File;
@@ -18,6 +21,25 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 
+/**
+* SecurisModule
+* <p>
+* Guice module that provides application-level infrastructural dependencies
+* (base URI, app directories, DB files list, support email/hash, etc.).
+* <p>
+* Configuration:
+* - Reads server port from /securis-server.properties (key: "port").
+* - Defaults to port 9997 when not present or on read errors.
+* - Constructs base URI as http://0.0.0.0:{port}/ with UriBuilder.
+* - Creates working directories under ${user.home}/.SeCuris on demand.
+*
+* Security note:
+* - getPassword/getFilePassword are simple helpers; secrets should be
+*   managed via a secure vault/env vars in production.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
 public class SecurisModule extends AbstractModule {
 
     private static final int DEFAULT_PORT = 9997;
@@ -25,28 +47,51 @@
 
     private static final Logger LOG = LogManager.getLogger(SecurisModule.class);
 
+    /** configure<p>Currently no explicit bindings; providers below supply instances. */
     @Override
-    protected void configure() {
+    protected void configure() { }
 
-    }
-
+    /**
+    * getPassword<p>
+    * Composite password (example use with encrypted H2 URL).
+    *
+    * @return concatenated password string
+    */
     public String getPassword() {
         return getFilePassword() + " " + "53curi5";
     }
 
+    /**
+    * getFilePassword<p>
+    * Standalone file password (for H2 CIPHER).
+    *
+    * @return file password string
+    */
     public String getFilePassword() {
         return "cur151T";
     }
 
+    /**
+    * getUrl<p>
+    * H2 JDBC URL with AES cipher pointing to {appDir}/db/securis.
+    *
+    * @param appDir application working directory
+    * @return JDBC URL (H2)
+    */
     public String getUrl(File appDir) {
         return String.format("jdbc:h2:%s/db/securis;CIPHER=AES", appDir.getAbsolutePath());
     }
 
+    /**
+    * getBaseURI<p>
+    * Provide the base URI for the HTTP server using configured or default port.
+    *
+    * @return base URI (http://0.0.0.0:{port}/)
+    */
     @Named("base-uri")
     @Provides
     @ApplicationScoped
     public URI getBaseURI() {
-        // Read from configuration, where?
         try {
             String url = MessageFormat.format("http://{0}/", "0.0.0.0");
             LOG.debug("Server url{}", url);
@@ -56,33 +101,46 @@
         }
     }
 
+    /**
+    * getPort<p>
+    * Read port from properties file or return default.
+    *
+    * @return HTTP port
+    */
     private int getPort() {
         Integer port;
         Properties prop = new Properties();
         try {
             prop.load(getClass().getResourceAsStream(PROPERTIES_FILE_NAME));
             port = Integer.valueOf(prop.getProperty("port"));
-            if (port == null) {
-                return DEFAULT_PORT;
-            } else {
-                return port;
-            }
+            return (port == null ? DEFAULT_PORT : port);
         } catch (Exception ex) {
             return DEFAULT_PORT;
         }
     }
 
+    /**
+    * getAppDbFiles<p>
+    * List of SQL files to initialize the application DB.
+    *
+    * @return list of classpath resource paths
+    */
     protected List<String> getAppDbFiles() {
-
         return Arrays.asList("/db/schema.sql");
     }
 
+    /**
+    * getTemporaryDir<p>
+    * Provide a temp directory inside the app working dir (.TEMP).
+    * Creates it if missing and marks for deletion on exit.
+    *
+    * @return temp directory or null if creation failed
+    */
     @Named("temporary-dir")
     @Provides
     @ApplicationScoped
     public File getTemporaryDir() {
-        String tmp = getAppDir().getAbsolutePath();
-        tmp += File.separator + ".TEMP";
+        String tmp = getAppDir().getAbsolutePath() + File.separator + ".TEMP";
         File ftmp = new File(tmp);
         if (!ftmp.exists()) {
             if (!ftmp.mkdirs()) {
@@ -94,14 +152,18 @@
         return ftmp;
     }
 
+    /**
+    * getAppDir<p>
+    * Provide the app working directory under ${user.home}/.SeCuris (creates if missing).
+    *
+    * @return working directory or null if creation failed
+    */
     @Named("app-dir")
     @Provides
     @ApplicationScoped
     public File getAppDir() {
         String appDir = System.getProperty("user.home", System.getProperty("user.dir"));
-        if (appDir == null) {
-            appDir = ".";
-        }
+        if (appDir == null) appDir = ".";
         appDir += File.separator + ".SeCuris";
         File fAppDir = new File(appDir);
         if (!fAppDir.exists()) {
@@ -113,6 +175,12 @@
         return fAppDir;
     }
 
+    /**
+    * getSupportEmail<p>
+    * Provide support email address.
+    *
+    * @return email
+    */
     @Named("support-email")
     @Provides
     @ApplicationScoped
@@ -120,6 +188,12 @@
         return "support@curisit.net";
     }
 
+    /**
+    * getHashLogo<p>
+    * Provide a static content hash for the logo (cache-busting or integrity).
+    *
+    * @return hex SHA-256
+    */
     @Named("hash-logo")
     @Provides
     @ApplicationScoped
@@ -127,11 +201,17 @@
         return "1b42616809d4cd8ccf109e3c30d0ab25067f160b30b7354a08ddd563de0096ba";
     }
 
+    /**
+    * getDbFiles<p>
+    * Provide DB initialization files list (delegates to {@link #getAppDbFiles()}).
+    *
+    * @return list of SQL resource paths
+    */
     @Named("db-files")
     @Provides
     @ApplicationScoped
     public List<String> getDbFiles() {
         return getAppDbFiles();
     }
-
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java b/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java
index 1c831b5..9d5fa0e 100644
--- a/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java
+++ b/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.security;
 
 import java.security.Principal;
@@ -9,103 +12,193 @@
 import net.curisit.integrity.commons.Utils;
 import net.curisit.securis.db.User;
 
+/**
+* BasicSecurityContext
+* <p>
+* Lightweight implementation of JAX-RS {@link SecurityContext} based on:
+* - A {@link Principal} holding the username.
+* - An integer bitmask of roles (see {@link User.Rol}).
+* - Optional scope restrictions (organization/application IDs).
+*
+* Role checks:
+* - {@link #isUserInRole(String)} maps string names to bit constants via {@link #ROLES}.
+*
+* Scope helpers:
+* - {@link #isOrgAccesible(Integer)} and {@link #isAppAccesible(Integer)}.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 public class BasicSecurityContext implements SecurityContext {
 
-	final public static String ROL_ADVANCE = "advance";
-	final public static String ROL_ADMIN = "admin";
-	final public static String ROL_BASIC = "basic";
+    /** String role names mapped to bit flags. */
+    public static final String ROL_ADVANCE = "advance";
+    public static final String ROL_ADMIN   = "admin";
+    public static final String ROL_BASIC   = "basic";
 
-	final static Map<String, Integer> ROLES = Utils.<String, Integer> createMap(ROL_BASIC, User.Rol.BASIC, ROL_ADVANCE, User.Rol.ADVANCE, ROL_ADMIN, User.Rol.ADMIN);
+    /** Mapping from role name to bit flag. */
+    static final Map<String, Integer> ROLES =
+        Utils.<String, Integer>createMap(ROL_BASIC, User.Rol.BASIC,
+                                         ROL_ADVANCE, User.Rol.ADVANCE,
+                                         ROL_ADMIN,   User.Rol.ADMIN);
 
-	Principal user = null;
-	int roles = 0;
-	boolean secure = false;
-	Set<Integer> organizationsIds = null;
-	Set<Integer> applicationsIds = null;
-	double ran = 0;
+    Principal user = null;
+    int roles = 0;
+    boolean secure = false;
+    Set<Integer> organizationsIds = null;
+    Set<Integer> applicationsIds = null;
+    double ran = 0; // small unique marker for debugging instances
 
-	public BasicSecurityContext(String username, int roles, boolean secure) {
-		user = new UserPrincipal(username);
-		this.roles = roles;
-		this.secure = secure;
-		ran = Math.random();
-	}
+    /**
+    * BasicSecurityContext<p>
+    * Construct a context for given user, roles and transport security flag.
+    *
+    * @param username principal name
+    * @param roles bitmask of roles
+    * @param secure whether the request is HTTPS
+    */
+    public BasicSecurityContext(String username, int roles, boolean secure) {
+        user = new UserPrincipal(username);
+        this.roles = roles;
+        this.secure = secure;
+        ran = Math.random();
+    }
 
-	@Override
-	public Principal getUserPrincipal() {
-		return user;
-	}
+    /** 
+     * getUserPrincipal<p>
+     * Return the user principal. 
+     * 
+     * @return mainUser
+     */
+    @Override
+    public Principal getUserPrincipal() { return user; }
 
-	@Override
-	public boolean isUserInRole(String role) {
-		Integer introle = ROLES.get(role);
-		return introle != null && (introle & roles) != 0;
-	}
+    /**
+    * isUserInRole<p>
+    * Check role membership by name (mapped to bitmask).
+    * 
+    * @param role
+    * @return isUserInRole
+    */
+    @Override
+    public boolean isUserInRole(String role) {
+        Integer introle = ROLES.get(role);
+        return introle != null && (introle & roles) != 0;
+    }
 
-	@Override
-	public boolean isSecure() {
-		return secure;
-	}
+    /** 
+     * isSecure<p>
+     * Return whether transport is secure (HTTPS). 
+     * 
+     * @return isSecure
+     */
+    @Override
+    public boolean isSecure() { return secure; }
 
-	@Override
-	public String getAuthenticationScheme() {
-		return null;
-	}
+    /** 
+     * getAuthenticationScheme<p>
+     * Not used; returns null. 
+     * 
+     * @return authenticationsScheme
+     */
+    @Override
+    public String getAuthenticationScheme() { return null; }
 
-	@Override
-	public String toString() {
+    /**
+     * toString<p>
+     * Get the string describing the current object
+     * 
+     * @return object string
+     */
+    @Override
+    public String toString() { return String.format("SecurityContextWrapper(%f) %s", ran, user); }
 
-		return String.format("SecurityContextWrapper(%f) %s", ran, user);
-	}
+    /** 
+     * setOrganizationsIds<p>
+     * Set org scope (IDs allowed). 
+     * 
+     * @param organizationsIds
+     */
+    public void setOrganizationsIds(Set<Integer> orgs) { this.organizationsIds = orgs; }
 
-	public void setOrganizationsIds(Set<Integer> orgs) {
-		this.organizationsIds = orgs;
-	}
+    /** 
+     * getOrganizationsIds<p>
+     * Return org scope. 
+     * 
+     * @return organizationsIds
+     */
+    public Set<Integer> getOrganizationsIds() { return this.organizationsIds; }
 
-	public Set<Integer> getOrganizationsIds() {
-		return this.organizationsIds;
-	}
+    /** 
+     * getApplicationsIds<p>
+     * Return app scope. 
+     * 
+     * @return applicationIds
+     */
+    public Set<Integer> getApplicationsIds() { return applicationsIds; }
 
-	public Set<Integer> getApplicationsIds() {
-		return applicationsIds;
-	}
+    /** 
+     * setApplicationsIds<p>
+     * Set app scope. 
+     * 
+     * @param applicationIds
+     */
+    public void setApplicationsIds(Set<Integer> applicationsIds) { this.applicationsIds = applicationsIds; }
 
-	public void setApplicationsIds(Set<Integer> applicationsIds) {
-		this.applicationsIds = applicationsIds;
-	}
+    /** 
+     * UserPrincipal<p>
+     * Inner Principal holding only the username. 
+     */
+    private class UserPrincipal implements Principal {
+        final String name;
+        
+        /**
+         * UserPrincipal<p>
+         * Main user
+         * 
+         * @param username
+         */
+        public UserPrincipal(String name) { this.name = name; }
+        
+        /**
+         * getName<p>
+         * Get the username
+         * 
+         * @return userName
+         */
+        @Override public String getName() { return this.name; }
+        
+        /**
+         * toString<p>
+         * Get the string describing the current object
+         * 
+         * @return object string
+         */
+        @Override public String toString() { return String.format("[%s]", name); }
+    }
 
-	private class UserPrincipal implements Principal {
+    /** 
+     * isOrgAccesible<p>
+     * Check if org id is within scope. 
+     * 
+     * @param orgId
+     * @return isOrgAccesible
+     */
+    public boolean isOrgAccesible(Integer orgid) {
+        if (organizationsIds == null || orgid == null) return false;
+        return organizationsIds.contains(orgid);
+    }
 
-		final String name;
-
-		public UserPrincipal(String name) {
-			this.name = name;
-		}
-
-		@Override
-		public String getName() {
-			return this.name;
-		}
-
-		@Override
-		public String toString() {
-			return String.format("[%s]", name);
-		}
-
-	}
-
-	public boolean isOrgAccesible(Integer orgid) {
-		if (organizationsIds == null || orgid == null) {
-			return false;
-		}
-		return organizationsIds.contains(orgid);
-	}
-
-	public boolean isAppAccesible(Integer appid) {
-		if (applicationsIds == null || appid == null) {
-			return false;
-		}
-		return applicationsIds.contains(appid);
-	}
-
+    /** 
+     * isAppAccesible<p>
+     * Check if app id is within scope. 
+     * 
+     * @param appId
+     * @return isAppAccesible
+     */
+    public boolean isAppAccesible(Integer appid) {
+        if (applicationsIds == null || appid == null) return false;
+        return applicationsIds.contains(appid);
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/security/Securable.java b/securis/src/main/java/net/curisit/securis/security/Securable.java
index 5580b5f..1ab3cd9 100644
--- a/securis/src/main/java/net/curisit/securis/security/Securable.java
+++ b/securis/src/main/java/net/curisit/securis/security/Securable.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.security;
 
 import java.lang.annotation.ElementType;
@@ -7,16 +10,25 @@
 
 import net.curisit.securis.utils.TokenHelper;
 
+/**
+* Securable
+* <p>
+* Method-level annotation to declare security requirements:
+* - {@link #header()} name containing the auth token (defaults to {@link TokenHelper#TOKEN_HEADER_PÀRAM}).
+* - {@link #roles()} required role bitmask; {@code 0} means no role restriction.
+*
+* Intended to be enforced by request filters/interceptors (e.g., RequestsInterceptor).
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.METHOD)
 public @interface Securable {
-    /**
-     * Name of header parameter with the auth token to validate
-     */
+
+    /** Header name carrying the token to validate. */
     String header() default TokenHelper.TOKEN_HEADER_PÀRAM;
 
-    /**
-     * Bit mask with the rol or roles necessary to access the method
-     */
+    /** Bitmask of required roles; set 0 for public endpoints (token still may be required). */
     int roles() default 0;
 }
diff --git a/securis/src/main/java/net/curisit/securis/services/ApiResource.java b/securis/src/main/java/net/curisit/securis/services/ApiResource.java
index 2718e51..07da3d2 100644
--- a/securis/src/main/java/net/curisit/securis/services/ApiResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/ApiResource.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.services;
 
 import java.io.IOException;
@@ -49,446 +52,491 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * External API to be accessed by third parties
- * 
- * @author roberto <roberto.sanchez@curisit.net>
- */
+* ApiResource
+* <p>
+* External API for license operations, intended for third-party clients.
+*
+* Endpoints:
+* - GET  /api/            -> Plain-text status with date (health check).
+* - GET  /api/ping        -> JSON status (message + date).
+* - POST /api/request     -> Create license from RequestBean (JSON).
+* - POST /api/request     -> Create license from request file (multipart).
+* - POST /api/renew       -> Renew from previous LicenseBean (JSON).
+* - POST /api/renew       -> Renew from previous license file (multipart).
+* - POST /api/validate    -> Server-side validation of a license.
+*
+* Security:
+* - Methods that mutate/inspect licenses require {@link Securable} with role {@link Rol#API_CLIENT}.
+* - {@link EnsureTransaction} ensures transaction handling at the filter/interceptor layer.
+*
+* Errors:
+* - Business errors are mapped to {@link SeCurisServiceException} with {@link ErrorCodes}.
+* 
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @Path("/api")
 public class ApiResource {
 
-	private static final Logger LOG = LogManager.getLogger(ApiResource.class);
+    private static final Logger LOG = LogManager.getLogger(ApiResource.class);
 
-	@Inject
-	TokenHelper tokenHelper;
+    @Inject TokenHelper tokenHelper;
+    @Inject private LicenseHelper licenseHelper;
+    @Context EntityManager em;
+    @Inject LicenseGenerator licenseGenerator;
 
-	@Inject
-	private LicenseHelper licenseHelper;
+    /** Fixed username representing API client actor for audit trails. */
+    public static final String API_CLIENT_USERNAME = "_client";
 
-	@Context
-	EntityManager em;
+    /** Default constructor (required by JAX-RS). */
+    public ApiResource() { }
 
-	@Inject
-	LicenseGenerator licenseGenerator;
+    // -------------------- Health checks --------------------
 
-	public static final String API_CLIENT_USERNAME = "_client";
+    /**
+    * index<p>
+    * Plain text endpoint to verify API is reachable.
+    *
+    * @return 200 OK with simple message
+    */
+    @GET
+    @Path("/")
+    @Produces({ MediaType.TEXT_PLAIN })
+    public Response index() {
+        return Response.ok("SeCuris API. Date: " + new Date()).build();
+    }
 
-	public ApiResource() {
-	}
+    /**
+    * ping<p>
+    * JSON endpoint for health checks.
+    *
+    * @return 200 OK with {@link StatusBean}
+    */
+    @GET
+    @Path("/ping")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response ping() {
+        StatusBean status = new StatusBean();
+        status.setDate(new Date());
+        status.setMessage(LicenseManager.PING_MESSAGE);
+        return Response.ok(status).build();
+    }
 
-	/**
-	 * 
-	 * @return Simple text message to check API status
-	 */
-	@GET
-	@Path("/")
-	@Produces({ MediaType.TEXT_PLAIN })
-	public Response index() {
-		return Response.ok("SeCuris API. Date: " + new Date()).build();
-	}
+    // -------------------- License creation --------------------
 
-	/**
-	 * 
-	 * @return Simple text message to check API status
-	 */
-	@GET
-	@Path("/ping")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response ping() {
-		StatusBean status = new StatusBean();
-		status.setDate(new Date());
-		status.setMessage(LicenseManager.PING_MESSAGE);
-		return Response.ok(status).build();
-	}
+    /**
+    * createFromRequest<p>
+    * Create a new license from JSON request data.
+    *
+    * @param request        RequestBean payload
+    * @param nameOrReference Holder name or external reference (header)
+    * @param userEmail      Email (header)
+    * @return {@link SignedLicenseBean} JSON
+    */
+    @POST
+    @Path("/request")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Securable(roles = Rol.API_CLIENT)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    public Response createFromRequest(RequestBean request,
+                                      @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
+                                      @HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail)
+            throws IOException, SeCurisServiceException, SeCurisException {
+        LOG.info("Request to get license: {}", request);
+        SignedLicenseBean lic = createLicense(request, em, nameOrReference, userEmail);
+        return Response.ok(lic).build();
+    }
 
-	/**
-	 * Request a new license file based in a RequestBean object sent as
-	 * parameter
-	 * 
-	 * @param mpfdi
-	 * @param bsc
-	 * @return
-	 * @throws IOException
-	 * @throws SeCurisServiceException
-	 */
-	@POST
-	@Path("/request")
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Securable(roles = Rol.API_CLIENT)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	public Response createFromRequest(RequestBean request, @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
-			@HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail) throws IOException, SeCurisServiceException, SeCurisException {
-		LOG.info("Request to get license: {}", request);
-		SignedLicenseBean lic = createLicense(request, em, nameOrReference, userEmail);
+    /**
+    * createFromRequestFile<p>
+    * Create a new license from a multipart form (uploaded request fields).
+    *
+    * @param mpfdi          multipart input
+    * @param nameOrReference holder name/reference (header)
+    * @param userEmail      email (header)
+    * @return {@link SignedLicenseBean} JSON
+    */
+    @POST
+    @Path("/request")
+    @Consumes(MediaType.MULTIPART_FORM_DATA)
+    @Securable(roles = Rol.API_CLIENT)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    @SuppressWarnings("unchecked")
+    public Response createFromRequestFile(MultipartFormDataInput mpfdi,
+                                          @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
+                                          @HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail)
+            throws IOException, SeCurisServiceException, SeCurisException {
+        RequestBean req = new RequestBean();
+        req.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
+        req.setActivationCode(mpfdi.getFormDataPart("activationCode", String.class, null));
+        req.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
+        req.setLicenseTypeCode(mpfdi.getFormDataPart("licenseTypeCode", String.class, null));
+        req.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
+        req.setArch(mpfdi.getFormDataPart("arch", String.class, null));
+        req.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
+        req.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
+        req.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
 
-		return Response.ok(lic).build();
-	}
+        return createFromRequest(req, nameOrReference, userEmail);
+    }
 
-	/**
-	 * Returns a License file in JSON format from an uploaded Request file
-	 * 
-	 * @param mpfdi
-	 * @param bsc
-	 * @return
-	 * @throws IOException
-	 * @throws SeCurisServiceException
-	 * @throws SeCurisException
-	 */
-	@POST
-	@Path("/request")
-	@Consumes(MediaType.MULTIPART_FORM_DATA)
-	@Securable(roles = Rol.API_CLIENT)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	@SuppressWarnings("unchecked")
-	public Response createFromRequestFile(MultipartFormDataInput mpfdi, @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
-			@HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail) throws IOException, SeCurisServiceException, SeCurisException {
-		RequestBean req = new RequestBean();
-		req.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
-		req.setActivationCode(mpfdi.getFormDataPart("activationCode", String.class, null));
-		req.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
-		req.setLicenseTypeCode(mpfdi.getFormDataPart("licenseTypeCode", String.class, null));
-		req.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
-		req.setArch(mpfdi.getFormDataPart("arch", String.class, null));
-		req.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
-		req.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
-		req.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
+    // -------------------- License renew --------------------
 
-		return createFromRequest(req, nameOrReference, userEmail);
-	}
+    /**
+    * renewFromPreviousLicense<p>
+    * Renew a license from an existing {@link LicenseBean} JSON payload.
+    * Only <b>Active</b> licenses within one month of expiration are eligible.
+    *
+    * @param previousLic current license bean
+    * @param bsc         security context
+    * @return new {@link SignedLicenseBean}
+    */
+    @POST
+    @Path("/renew")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Securable(roles = Rol.API_CLIENT)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    public Response renewFromPreviousLicense(LicenseBean previousLic, @Context BasicSecurityContext bsc)
+            throws IOException, SeCurisServiceException, SeCurisException {
+        LOG.info("Renew license: {}", previousLic);
 
-	/**
-	 * Create a new License file based in a previous one
-	 * 
-	 * @param request
-	 * @param bsc
-	 * @return
-	 * @throws IOException
-	 * @throws SeCurisServiceException
-	 * @throws SeCurisException
-	 */
-	@POST
-	@Path("/renew")
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Securable(roles = Rol.API_CLIENT)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	public Response renewFromPreviousLicense(LicenseBean previousLic, @Context BasicSecurityContext bsc) throws IOException, SeCurisServiceException, SeCurisException {
-		LOG.info("Renew license: {}", previousLic);
+        if (previousLic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
+            throw new SeCurisServiceException(ErrorCodes.UNNECESSARY_RENEW, "The license is still valid, not ready for renew");
+        }
 
-		if (previousLic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
-			throw new SeCurisServiceException(ErrorCodes.UNNECESSARY_RENEW, "The license is still valid, not ready for renew");
-		}
+        License lic = License.findLicenseByCode(previousLic.getLicenseCode(), em);
+        if (lic == null) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license is missing in DB");
+        }
+        if (lic.getStatus() != LicenseStatus.ACTIVE) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "Only licenses with status 'Active' can be renew");
+        }
 
-		// EntityManager em = emProvider.get();
-		License lic = License.findLicenseByCode(previousLic.getLicenseCode(), em);
-		if (lic == null) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license is missing in DB");
-		}
+        SignedLicenseBean signedLic = renewLicense(previousLic, em);
+        LOG.info("Renewed license code: {}, until: {}", signedLic.getLicenseCode(), signedLic.getExpirationDate());
 
-		if (lic.getStatus() != LicenseStatus.ACTIVE) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "Only licenses with status 'Active' can be renew");
-		}
+        return Response.ok(signedLic).build();
+    }
 
-		SignedLicenseBean signedLic = renewLicense(previousLic, em);
-		LOG.info("Renewed license code: {}, until: {}", signedLic.getLicenseCode(), signedLic.getExpirationDate());
+    /**
+    * renewFromLicenseFile<p>
+    * Renew a license from multipart (uploaded prior license fields).
+    *
+    * @param mpfdi multipart input
+    * @param bsc   security context
+    * @return new {@link SignedLicenseBean}
+    */
+    @POST
+    @Path("/renew")
+    @Consumes(MediaType.MULTIPART_FORM_DATA)
+    @Securable(roles = Rol.API_CLIENT)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    @SuppressWarnings("unchecked")
+    public Response renewFromLicenseFile(MultipartFormDataInput mpfdi, @Context BasicSecurityContext bsc)
+            throws IOException, SeCurisServiceException, SeCurisException {
+        LicenseBean lic = new LicenseBean();
 
-		return Response.ok(signedLic).build();
-	}
+        lic.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
+        lic.setActivationCode(mpfdi.getFormDataPart("activationName", String.class, null));
+        lic.setAppName(mpfdi.getFormDataPart("appName", String.class, null));
+        lic.setArch(mpfdi.getFormDataPart("arch", String.class, null));
+        lic.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
+        lic.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
+        lic.setLicenseTypeCode(mpfdi.getFormDataPart("licenseCode", String.class, null));
+        lic.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
+        lic.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
+        lic.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
+        lic.setExpirationDate(mpfdi.getFormDataPart("expirationDate", Date.class, null));
 
-	/**
-	 * License validation on server side, in this case we validate that the
-	 * current licenses has not been cancelled and they are still in valid
-	 * period. If the pack has reached the end valid period, the license is no
-	 * longer valid.
-	 * 
-	 * @param currentLic
-	 * @param bsc
-	 * @return
-	 * @throws IOException
-	 * @throws SeCurisServiceException
-	 * @throws SeCurisException
-	 */
-	@POST
-	@Path("/validate")
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Securable(roles = Rol.API_CLIENT)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	public Response validate(LicenseBean currentLic, @Context BasicSecurityContext bsc) throws IOException, SeCurisServiceException, SeCurisException {
-		LOG.info("Validate license: {}", currentLic);
+        LOG.info("Lic expires at: {}", lic.getExpirationDate());
+        if (lic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "The license is still valid, not ready for renew");
+        }
 
-		if (currentLic.getExpirationDate().before(new Date())) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_IS_EXPIRED, "The license is expired");
-		}
+        return renewFromPreviousLicense(lic, bsc);
+    }
 
-		License existingLic = licenseHelper.getActiveLicenseFromDB(currentLic, em);
+    // -------------------- Validation --------------------
 
-		Pack pack = existingLic.getPack();
-		if (pack.getEndValidDate().before(new Date())) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack end valid date has been reached");
-		}
-		if (pack.getStatus() != PackStatus.ACTIVE) {
-			LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack status is not Active");
-		}
+    /**
+    * validate<p>
+    * Server-side validation of a license:
+    * - Not expired
+    * - Pack still valid and active
+    * - Signature valid
+    *
+    * @param currentLic license to validate
+    * @param bsc        security context
+    * @return same license if valid
+    */
+    @POST
+    @Path("/validate")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Securable(roles = Rol.API_CLIENT)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    public Response validate(LicenseBean currentLic, @Context BasicSecurityContext bsc)
+            throws IOException, SeCurisServiceException, SeCurisException {
+        LOG.info("Validate license: {}", currentLic);
 
-		try {
-			SignatureHelper.getInstance().validateSignature(currentLic);
-		} catch (SeCurisException ex) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The license signature is not valid");
-		}
+        if (currentLic.getExpirationDate().before(new Date())) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_IS_EXPIRED, "The license is expired");
+        }
 
-		return Response.ok(currentLic).build();
-	}
+        License existingLic = licenseHelper.getActiveLicenseFromDB(currentLic, em);
 
-	/**
-	 * Returns a new License file in JSON format based in a previous license
-	 * There is 2 /renew services with json input and with upload file
-	 * 
-	 * @param mpfdi
-	 * @param bsc
-	 * @return
-	 * @throws IOException
-	 * @throws SeCurisServiceException
-	 * @throws SeCurisException
-	 */
-	@POST
-	@Path("/renew")
-	@Consumes(MediaType.MULTIPART_FORM_DATA)
-	@Securable(roles = Rol.API_CLIENT)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	@SuppressWarnings("unchecked")
-	public Response renewFromLicenseFile(MultipartFormDataInput mpfdi, @Context BasicSecurityContext bsc) throws IOException, SeCurisServiceException, SeCurisException {
-		LicenseBean lic = new LicenseBean();
+        Pack pack = existingLic.getPack();
+        if (pack.getEndValidDate().before(new Date())) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack end valid date has been reached");
+        }
+        if (pack.getStatus() != PackStatus.ACTIVE) {
+            LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack status is not Active");
+        }
 
-		lic.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
-		lic.setActivationCode(mpfdi.getFormDataPart("activationName", String.class, null));
-		lic.setAppName(mpfdi.getFormDataPart("appName", String.class, null));
-		lic.setArch(mpfdi.getFormDataPart("arch", String.class, null));
-		lic.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
-		lic.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
-		lic.setLicenseTypeCode(mpfdi.getFormDataPart("licenseCode", String.class, null));
-		lic.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
-		lic.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
-		lic.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
-		lic.setExpirationDate(mpfdi.getFormDataPart("expirationDate", Date.class, null));
-		LOG.info("Lic expires at: {}", lic.getExpirationDate());
-		if (lic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "The license is still valid, not ready for renew");
-		}
+        try {
+            SignatureHelper.getInstance().validateSignature(currentLic);
+        } catch (SeCurisException ex) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The license signature is not valid");
+        }
 
-		return renewFromPreviousLicense(lic, bsc);
-	}
+        return Response.ok(currentLic).build();
+    }
 
-	/**
-	 * Creates a new signed license from request data or from previous license
-	 * if It's a renew
-	 * 
-	 * @param req
-	 * @param em
-	 * @param renew
-	 * @return
-	 * @throws SeCurisServiceException
-	 */
-	private SignedLicenseBean createLicense(RequestBean req, EntityManager em, String nameOrReference, String email) throws SeCurisServiceException {
-		License lic = null;
+    // -------------------- Internal helpers --------------------
 
-		if (req.getActivationCode() != null) {
-			lic = License.findLicenseByActivationCode(req.getActivationCode(), em);
-			if (lic == null) {
-				throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The given activation code is invalid: " + req.getActivationCode());
-			}
-			if (lic.getStatus() == LicenseStatus.ACTIVE) {
-				RequestBean initialRequest;
-				try {
-					initialRequest = JsonUtils.json2object(lic.getRequestData(), RequestBean.class);
-					if (!req.match(initialRequest)) {
-						throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "There is already an active license for given activation code: " + req.getActivationCode());
-					} else {
-						return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
-					}
-				} catch (SeCurisException e) {
-					LOG.error("Error getting existing license", e);
-					throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Original request is wrong");
-				}
-			} else {
-				if (req.getAppCode() != null && !req.getAppCode().equals(lic.getPack().getLicenseType().getApplication().getCode())) {
-					LOG.error("Activation code {} belongs to app: {} but was sent by: {}", req.getActivationCode(), lic.getPack().getLicenseType().getApplication().getCode(),
-							req.getAppCode());
-					throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The given activation code belongs to a different application: " + req.getActivationCode());
-				}
-			}
-			// We validate if the HW is the same, otherwise an error is
-			// thrown
-		} else {
-			try {
-				lic = License.findValidLicenseByRequestData(JsonUtils.toJSON(req), em);
-			} catch (SeCurisException e1) {
-				throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Request sent is not valid");
-			}
-			if (lic != null) {
-				try {
-					if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) {
-						return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
-					}
-				} catch (SeCurisException e) {
-					throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error trying to get the license bean from license code: " + lic.getCode());
-				}
-			} else {
-				lic = new License();
-			}
-		}
+    /**
+    * createLicense<p>
+    * Creates a new signed license from request data or reuses an existing
+    * pre-active/active one when allowed by business rules.
+    *
+    * @param req request bean
+    * @param em  entity manager
+    * @param nameOrReference license holder name/reference (header)
+    * @param email email (header)
+    * @return signed license bean
+    */
+    private SignedLicenseBean createLicense(RequestBean req, EntityManager em, String nameOrReference, String email)
+            throws SeCurisServiceException {
 
-		Pack pack;
-		if (lic.getActivationCode() == null) {
-			try {
-				pack = em.createNamedQuery("pack-by-code", Pack.class).setParameter("code", req.getPackCode()).getSingleResult();
-			} catch (NoResultException e) {
-				throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "No pack found for code: " + req.getPackCode());
-			}
+        License lic = null;
 
-			if (pack.getNumAvailables() <= 0) {
-				throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "The current pack has no licenses availables");
-			}
-			if (lic.getStatus() == LicenseStatus.REQUESTED && !pack.isLicensePreactivation()) {
-				throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
-			}
+        // (1) Activation-code flow
+        if (req.getActivationCode() != null) {
+            lic = License.findLicenseByActivationCode(req.getActivationCode(), em);
+            if (lic == null) {
+                throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The given activation code is invalid: " + req.getActivationCode());
+            }
+            if (lic.getStatus() == LicenseStatus.ACTIVE) {
+                try {
+                    RequestBean initialRequest = JsonUtils.json2object(lic.getRequestData(), RequestBean.class);
+                    if (!req.match(initialRequest)) {
+                        throw new SeCurisServiceException(ErrorCodes.INVALID_DATA,
+                                "There is already an active license for given activation code: " + req.getActivationCode());
+                    } else {
+                        return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
+                    }
+                } catch (SeCurisException e) {
+                    LOG.error("Error getting existing license", e);
+                    throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Original request is wrong");
+                }
+            } else {
+                if (req.getAppCode() != null &&
+                    !req.getAppCode().equals(lic.getPack().getLicenseType().getApplication().getCode())) {
+                    LOG.error("Activation code {} belongs to app: {} but was sent by: {}",
+                              req.getActivationCode(), lic.getPack().getLicenseType().getApplication().getCode(), req.getAppCode());
+                    throw new SeCurisServiceException(ErrorCodes.INVALID_DATA,
+                            "The given activation code belongs to a different application: " + req.getActivationCode());
+                }
+            }
+        } else {
+            // (2) Request-data flow (idempotent check)
+            try {
+                lic = License.findValidLicenseByRequestData(JsonUtils.toJSON(req), em);
+            } catch (SeCurisException e1) {
+                throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Request sent is not valid");
+            }
+            if (lic != null) {
+                try {
+                    if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) {
+                        return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
+                    }
+                } catch (SeCurisException e) {
+                    throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT,
+                            "Error trying to get the license bean from license code: " + lic.getCode());
+                }
+            } else {
+                lic = new License();
+            }
+        }
 
-			if (!req.getCustomerCode().equals(pack.getOrganization().getCode())) {
-				throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Customer code is not valid: " + req.getCustomerCode());
-			}
+        // (3) Pack validation & constraints
+        Pack pack;
+        if (lic.getActivationCode() == null) {
+            try {
+                pack = em.createNamedQuery("pack-by-code", Pack.class)
+                         .setParameter("code", req.getPackCode())
+                         .getSingleResult();
+            } catch (NoResultException e) {
+                throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "No pack found for code: " + req.getPackCode());
+            }
 
-			if (!req.getLicenseTypeCode().equals(pack.getLicenseTypeCode())) {
-				throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "License type code is not valid: " + req.getLicenseTypeCode());
-			}
-		} else {
-			pack = lic.getPack();
-		}
-		if (pack.getStatus() != PackStatus.ACTIVE) {
-			LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The pack status is not Active");
-		}
-		SignedLicenseBean signedLicense;
-		try {
-			String licCode;
-			if (lic.getCode() == null) {
-				licCode = LicUtils.getLicenseCode(pack.getCode(), licenseHelper.getNextCodeSuffix(pack.getId(), em));
-			} else {
-				licCode = lic.getCode();
-			}
-			Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, lic.getActivationCode() == null);
+            if (pack.getNumAvailables() <= 0) {
+                throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "The current pack has no licenses availables");
+            }
+            if (lic.getStatus() == LicenseStatus.REQUESTED && !pack.isLicensePreactivation()) {
+                throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
+            }
+            if (!req.getCustomerCode().equals(pack.getOrganization().getCode())) {
+                throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Customer code is not valid: " + req.getCustomerCode());
+            }
+            if (!req.getLicenseTypeCode().equals(pack.getLicenseTypeCode())) {
+                throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "License type code is not valid: " + req.getLicenseTypeCode());
+            }
+        } else {
+            pack = lic.getPack();
+        }
 
-			LicenseBean lb = licenseGenerator.generateLicense(req, licenseHelper.extractPackMetadata(pack.getMetadata()), expirationDate, licCode, pack.getAppName());
-			signedLicense = new SignedLicenseBean(lb);
-		} catch (SeCurisException e) {
-			throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
-		}
-		try {
-			lic.setRequestData(JsonUtils.toJSON(req));
-			if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
-				throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
-			}
-			lic.setLicenseData(JsonUtils.toJSON(signedLicense));
-		} catch (SeCurisException e) {
-			LOG.error("Error generating license JSON", e);
-			throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
-		}
+        if (pack.getStatus() != PackStatus.ACTIVE) {
+            LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The pack status is not Active");
+        }
 
-		lic.setModificationTimestamp(new Date());
-		lic.setExpirationDate(signedLicense.getExpirationDate());
-		User user = em.find(User.class, API_CLIENT_USERNAME);
-		if (lic.getStatus() != LicenseStatus.REQUESTED) {
-			lic.setPack(pack);
-			lic.setCreatedBy(user);
-			lic.setCreationTimestamp(new Date());
-			if (lic.getActivationCode() != null) {
-				lic.setStatus(LicenseStatus.ACTIVE);
-			} else {
-				lic.setStatus(pack.isLicensePreactivation() ? LicenseStatus.PRE_ACTIVE : LicenseStatus.REQUESTED);
-			}
-			lic.setCode(signedLicense.getLicenseCode());
-			lic.setCodeSuffix(LicUtils.getLicenseCodeSuffix(signedLicense.getLicenseCode()));
-			if (lic.getEmail() == null || "".equals(lic.getEmail())) {
-				lic.setEmail(email);
-			}
-			if (lic.getFullName() == null || "".equals(lic.getFullName())) {
-				lic.setFullName(nameOrReference);
-			}
-			if (lic.getId() != null) {
-				em.merge(lic);
-			} else {
-				em.persist(lic);
-			}
-			em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.CREATE));
-			if (lic.getActivationCode() != null) {
-				em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.ACTIVATE, "Activated by code on creation"));
-			} else {
-				if (pack.isLicensePreactivation()) {
-					em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated on creation"));
-				} else {
-					LOG.warn("License ({}) created, but the pack doesn't allow preactivation", lic.getCode());
-					throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
-				}
-			}
-		} else {
-			lic.setStatus(LicenseStatus.PRE_ACTIVE);
-			em.merge(lic);
-			em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated after request"));
-		}
+        // (4) License generation
+        SignedLicenseBean signedLicense;
+        try {
+            String licCode = (lic.getCode() == null)
+                    ? LicUtils.getLicenseCode(pack.getCode(), licenseHelper.getNextCodeSuffix(pack.getId(), em))
+                    : lic.getCode();
 
-		return signedLicense;
-	}
+            Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, lic.getActivationCode() == null);
+            LicenseBean lb = licenseGenerator.generateLicense(
+                    req,
+                    licenseHelper.extractPackMetadata(pack.getMetadata()),
+                    expirationDate,
+                    licCode,
+                    pack.getAppName()
+            );
+            signedLicense = new SignedLicenseBean(lb);
+        } catch (SeCurisException e) {
+            throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
+        }
 
-	/**
-	 * Creates a new signed license from request data or from previous license
-	 * if It's a renew
-	 * 
-	 * @param req
-	 * @param em
-	 * @param renew
-	 * @return
-	 * @throws SeCurisServiceException
-	 */
-	private SignedLicenseBean renewLicense(LicenseBean previousLicenseBean, EntityManager em) throws SeCurisServiceException {
+        // (5) Persist/merge license + history
+        try {
+            lic.setRequestData(JsonUtils.toJSON(req));
+            if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+                throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
+            }
+            lic.setLicenseData(JsonUtils.toJSON(signedLicense));
+        } catch (SeCurisException e) {
+            LOG.error("Error generating license JSON", e);
+            throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
+        }
 
-		License lic = License.findLicenseByCode(previousLicenseBean.getLicenseCode(), em);
-		if (lic.getStatus() != LicenseStatus.ACTIVE && lic.getStatus() != LicenseStatus.PRE_ACTIVE) {
-			throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The current license has been cancelled");
-		}
+        lic.setModificationTimestamp(new Date());
+        lic.setExpirationDate(signedLicense.getExpirationDate());
+        User user = em.find(User.class, API_CLIENT_USERNAME);
 
-		Pack pack = lic.getPack();
-		SignedLicenseBean signedLicense;
-		try {
-			String licCode = lic.getCode();
-			Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, false);
+        if (lic.getStatus() != LicenseStatus.REQUESTED) {
+            lic.setPack(pack);
+            lic.setCreatedBy(user);
+            lic.setCreationTimestamp(new Date());
+            if (lic.getActivationCode() != null) {
+                lic.setStatus(LicenseStatus.ACTIVE);
+            } else {
+                lic.setStatus(pack.isLicensePreactivation() ? LicenseStatus.PRE_ACTIVE : LicenseStatus.REQUESTED);
+            }
+            lic.setCode(signedLicense.getLicenseCode());
+            lic.setCodeSuffix(LicUtils.getLicenseCodeSuffix(signedLicense.getLicenseCode()));
+            if (lic.getEmail() == null || "".equals(lic.getEmail())) {
+                lic.setEmail(email);
+            }
+            if (lic.getFullName() == null || "".equals(lic.getFullName())) {
+                lic.setFullName(nameOrReference);
+            }
+            if (lic.getId() != null) {
+                em.merge(lic);
+            } else {
+                em.persist(lic);
+            }
+            em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.CREATE));
+            if (lic.getActivationCode() != null) {
+                em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.ACTIVATE, "Activated by code on creation"));
+            } else {
+                if (pack.isLicensePreactivation()) {
+                    em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated on creation"));
+                } else {
+                    LOG.warn("License ({}) created, but the pack doesn't allow preactivation", lic.getCode());
+                    throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
+                }
+            }
+        } else {
+            lic.setStatus(LicenseStatus.PRE_ACTIVE);
+            em.merge(lic);
+            em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated after request"));
+        }
 
-			LicenseBean lb = licenseGenerator.generateLicense(previousLicenseBean, licenseHelper.extractPackMetadata(pack.getMetadata()), expirationDate, licCode,
-					pack.getAppName());
-			signedLicense = new SignedLicenseBean(lb);
-		} catch (SeCurisException e) {
-			throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
-		}
-		try {
-			lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
-			if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
-				throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
-			}
-			lic.setLicenseData(JsonUtils.toJSON(signedLicense));
-		} catch (SeCurisException e) {
-			LOG.error("Error generating license JSON", e);
-			throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
-		}
+        return signedLicense;
+    }
 
-		lic.setModificationTimestamp(new Date());
-		lic.setExpirationDate(signedLicense.getExpirationDate());
-		User user = em.find(User.class, API_CLIENT_USERNAME);
+    /**
+    * renewLicense<p>
+    * Internal renew logic used by JSON and multipart variants.
+    *
+    * @param previousLicenseBean previous license data
+    * @param em entity manager
+    * @return new signed license bean
+    */
+    private SignedLicenseBean renewLicense(LicenseBean previousLicenseBean, EntityManager em)
+            throws SeCurisServiceException {
 
-		lic.setStatus(LicenseStatus.ACTIVE);
-		em.merge(lic);
-		em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.RENEW));
+        License lic = License.findLicenseByCode(previousLicenseBean.getLicenseCode(), em);
+        if (lic.getStatus() != LicenseStatus.ACTIVE && lic.getStatus() != LicenseStatus.PRE_ACTIVE) {
+            throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The current license has been cancelled");
+        }
 
-		return signedLicense;
-	}
+        Pack pack = lic.getPack();
+        SignedLicenseBean signedLicense;
+        try {
+            String licCode = lic.getCode();
+            Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, false);
+
+            LicenseBean lb = licenseGenerator.generateLicense(
+                    previousLicenseBean,
+                    licenseHelper.extractPackMetadata(pack.getMetadata()),
+                    expirationDate,
+                    licCode,
+                    pack.getAppName()
+            );
+            signedLicense = new SignedLicenseBean(lb);
+        } catch (SeCurisException e) {
+            throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
+        }
+        try {
+            lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
+            if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+                throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
+            }
+            lic.setLicenseData(JsonUtils.toJSON(signedLicense));
+        } catch (SeCurisException e) {
+            LOG.error("Error generating license JSON", e);
+            throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
+        }
+
+        lic.setModificationTimestamp(new Date());
+        lic.setExpirationDate(signedLicense.getExpirationDate());
+        User user = em.find(User.class, API_CLIENT_USERNAME);
+
+        lic.setStatus(LicenseStatus.ACTIVE);
+        em.merge(lic);
+        em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.RENEW));
+
+        return signedLicense;
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java b/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java
index e9b2776..f004acd 100644
--- a/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.util.Date;
@@ -42,204 +45,246 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * Application resource, this service will provide methods to create, modify and
- * delete applications
- * 
- * @author roberto <roberto.sanchez@curisit.net>
+ * ApplicationResource
+ * <p>
+ * REST endpoints to list, fetch, create, update and delete {@link Application}s.
+ * Security:
+ * <ul>
+ *   <li>Listing filters by user's accessible application IDs unless ADMIN.</li>
+ *   <li>Create/Modify/Delete restricted to ADMIN.</li>
+ * </ul>
+ * Side-effects:
+ * <ul>
+ *   <li>Manages {@link ApplicationMetadata} lifecycle on create/update.</li>
+ *   <li>Propagates metadata changes via {@link MetadataHelper}.</li>
+ * </ul>
+ *
+ * Author: roberto &lt;roberto.sanchez@curisit.net&gt;<br>
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/application")
 public class ApplicationResource {
 
-	@Inject
-	TokenHelper tokenHelper;
+    @Inject TokenHelper tokenHelper;
+    @Inject MetadataHelper metadataHelper;
 
-	@Inject
-	MetadataHelper metadataHelper;
+    @Context EntityManager em;
 
-	@Context
-	EntityManager em;
+    private static final Logger LOG = LogManager.getLogger(ApplicationResource.class);
 
-	private static final Logger LOG = LogManager.getLogger(ApplicationResource.class);
+    /**
+     * ApplicationResource<p>
+     * Constructor
+     */
+    public ApplicationResource() {}
 
-	public ApplicationResource() {
-	}
+    /**
+     * index<p>
+     * List applications visible to the current user.
+     *
+     * @param bsc security context
+     * @return 200 with list (possibly empty) or 200 empty if user has no app scope
+     */
+    @GET
+    @Path("/")
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable
+    public Response index(@Context BasicSecurityContext bsc) {
+        LOG.info("Getting applications list ");
+        em.clear();
 
-	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
-	 */
-	@GET
-	@Path("/")
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable
-	public Response index(@Context BasicSecurityContext bsc) {
-		LOG.info("Getting applications list ");
+        TypedQuery<Application> q;
+        if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
+            q = em.createNamedQuery("list-applications", Application.class);
+        } else {
+            if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
+                return Response.ok().build();
+            }
+            q = em.createNamedQuery("list-applications-by_ids", Application.class);
+            q.setParameter("list_ids", bsc.getApplicationsIds());
+        }
+        List<Application> list = q.getResultList();
+        return Response.ok(list).build();
+    }
 
-		// EntityManager em = emProvider.get();
-		em.clear();
+    /**
+     * get<p>
+     * Fetch a single application by ID.
+     *
+     * @param appid string ID
+     * @return 200 + entity or 404 if not found
+     * @throws SeCurisServiceException when ID is invalid or not found
+     */
+    @GET
+    @Path("/{appid}")
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable
+    public Response get(@PathParam("appid") String appid) throws SeCurisServiceException {
+        LOG.info("Getting application data for id: {}: ", appid);
+        if (appid == null || "".equals(appid)) {
+            LOG.error("Application ID is mandatory");
+            return Response.status(Status.NOT_FOUND).build();
+        }
 
-		TypedQuery<Application> q;
-		if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
-			q = em.createNamedQuery("list-applications", Application.class);
-		} else {
-			if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
-				return Response.ok().build();
-			}
-			q = em.createNamedQuery("list-applications-by_ids", Application.class);
+        em.clear();
+        Application app = null;
+        try {
+            LOG.info("READY to GET app: {}", appid);
+            app = em.find(Application.class, Integer.parseInt(appid));
+        } catch (Exception e) {
+            LOG.info("ERROR GETTING app: {}", e);
+        }
+        if (app == null) {
+            LOG.error("Application with id {} not found in DB", appid);
+            throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "Application not found with ID: " + appid);
+        }
+        return Response.ok(app).build();
+    }
 
-			q.setParameter("list_ids", bsc.getApplicationsIds());
-		}
-		List<Application> list = q.getResultList();
+    /**
+     * create<p>
+     * Create a new application with optional metadata entries.
+     *
+     * @param app application payload
+     * @param token auth token (audited externally)
+     * @return 200 + persisted entity
+     */
+    @POST
+    @Path("/")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response create(Application app, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Creating new application");
+        app.setCreationTimestamp(new Date());
+        em.persist(app);
 
-		return Response.ok(list).build();
-	}
+        if (app.getApplicationMetadata() != null) {
+            for (ApplicationMetadata md : app.getApplicationMetadata()) {
+                md.setApplication(app);
+                md.setCreationTimestamp(new Date());
+                em.persist(md);
+            }
+        }
+        LOG.info("Creating application ({}) with date: {}", app.getId(), app.getCreationTimestamp());
+        return Response.ok(app).build();
+    }
 
-	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
-	 * @throws SeCurisServiceException
-	 */
-	@GET
-	@Path("/{appid}")
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable
-	public Response get(@PathParam("appid") String appid) throws SeCurisServiceException {
-		LOG.info("Getting application data for id: {}: ", appid);
-		if (appid == null || "".equals(appid)) {
-			LOG.error("Application ID is mandatory");
-			return Response.status(Status.NOT_FOUND).build();
-		}
+    /**
+     * modify<p>
+     * Update core fields and reconcile metadata set:
+     * <ul>
+     *   <li>Removes missing keys, merges existing, persists new.</li>
+     *   <li>Propagates metadata if there were changes.</li>
+     * </ul>
+     *
+     * @param appid path ID
+     * @param app   new state
+     */
+    @PUT
+    @POST
+    @Path("/{appid}")
+    @EnsureTransaction
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response modify(Application app, @PathParam("appid") String appid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Modifying application with id: {}", appid);
+        Application currentapp = em.find(Application.class, Integer.parseInt(appid));
+        if (currentapp == null) {
+            LOG.error("Application with id {} not found in DB", appid);
+            return Response.status(Status.NOT_FOUND)
+                    .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid)
+                    .build();
+        }
 
-		em.clear();
+        currentapp.setCode(app.getCode());
+        currentapp.setName(app.getName());
+        currentapp.setLicenseFilename(app.getLicenseFilename());
+        currentapp.setDescription(app.getDescription());
 
-		Application app = null;
-		try {
-			LOG.info("READY to GET app: {}", appid);
-			app = em.find(Application.class, Integer.parseInt(appid));
-		} catch (Exception e) {
-			LOG.info("ERROR GETTING app: {}", e);
-		}
-		if (app == null) {
-			LOG.error("Application with id {} not found in DB", appid);
-			throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "Application not found with ID: " + appid);
-		}
+        Set<ApplicationMetadata> newMD = app.getApplicationMetadata();
+        Set<ApplicationMetadata> oldMD = currentapp.getApplicationMetadata();
+        boolean metadataChanges = !metadataHelper.match(newMD, oldMD);
+        if (metadataChanges) {
+            Map<String, ApplicationMetadata> directOldMD = getMapMD(oldMD);
+            Map<String, ApplicationMetadata> directNewMD = getMapMD(newMD);
 
-		return Response.ok(app).build();
-	}
+            // Remove deleted MD
+            for (ApplicationMetadata currentMd : oldMD) {
+                if (newMD == null || !directNewMD.containsKey(currentMd.getKey())) {
+                    em.remove(currentMd);
+                }
+            }
+            // Merge or persist
+            if (newMD != null) {
+                for (ApplicationMetadata md : newMD) {
+                    if (directOldMD.containsKey(md.getKey())) {
+                        em.merge(md);
+                    } else {
+                        md.setApplication(currentapp);
+                        if (md.getCreationTimestamp() == null) {
+                            md.setCreationTimestamp(app.getCreationTimestamp());
+                        }
+                        em.persist(md);
+                    }
+                }
+            }
+            currentapp.setApplicationMetadata(app.getApplicationMetadata());
+        }
 
-	@POST
-	@Path("/")
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response create(Application app, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Creating new application");
-		// EntityManager em = emProvider.get();
-		app.setCreationTimestamp(new Date());
-		em.persist(app);
+        em.merge(currentapp);
+        if (metadataChanges) {
+            metadataHelper.propagateMetadata(em, currentapp);
+        }
+        return Response.ok(currentapp).build();
+    }
 
-		if (app.getApplicationMetadata() != null) {
-			for (ApplicationMetadata md : app.getApplicationMetadata()) {
-				md.setApplication(app);
-				md.setCreationTimestamp(new Date());
-				em.persist(md);
-			}
-		}
-		LOG.info("Creating application ({}) with date: {}", app.getId(), app.getCreationTimestamp());
+    /**
+     * getMapMD<p> 
+     * Build a map from metadata key → entity for fast reconciliation. 
+     * 
+     * @param applicationMetadata
+     * @return mapMD
+     */
+    private Map<String, ApplicationMetadata> getMapMD(Set<ApplicationMetadata> amd) {
+        Map<String, ApplicationMetadata> map = new HashMap<>();
+        if (amd != null) {
+            for (ApplicationMetadata m : amd) {
+                map.put(m.getKey(), m);
+            }
+        }
+        return map;
+    }
 
-		return Response.ok(app).build();
-	}
-
-	@PUT
-	@POST
-	@Path("/{appid}")
-	@EnsureTransaction
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response modify(Application app, @PathParam("appid") String appid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Modifying application with id: {}", appid);
-		// EntityManager em = emProvider.get();
-		Application currentapp = em.find(Application.class, Integer.parseInt(appid));
-		if (currentapp == null) {
-			LOG.error("Application with id {} not found in DB", appid);
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid).build();
-		}
-		currentapp.setCode(app.getCode());
-		currentapp.setName(app.getName());
-		currentapp.setLicenseFilename(app.getLicenseFilename());
-		currentapp.setDescription(app.getDescription());
-
-		Set<ApplicationMetadata> newMD = app.getApplicationMetadata();
-		Set<ApplicationMetadata> oldMD = currentapp.getApplicationMetadata();
-		boolean metadataChanges = !metadataHelper.match(newMD, oldMD);
-		if (metadataChanges) {
-			Map<String, ApplicationMetadata> directOldMD = getMapMD(oldMD);
-			Map<String, ApplicationMetadata> directNewMD = getMapMD(newMD);
-			for (ApplicationMetadata currentMd : oldMD) {
-				if (newMD == null || !directNewMD.containsKey(currentMd.getKey())) {
-					em.remove(currentMd);
-				}
-			}
-
-			if (newMD != null) {
-				for (ApplicationMetadata md : newMD) {
-					if (directOldMD.containsKey(md.getKey())) {
-						em.merge(md);
-					} else {
-						md.setApplication(currentapp);
-						if (md.getCreationTimestamp() == null) {
-							md.setCreationTimestamp(app.getCreationTimestamp());
-						}
-						em.persist(md);
-					}
-				}
-			}
-			currentapp.setApplicationMetadata(app.getApplicationMetadata());
-		}
-		em.merge(currentapp);
-		if (metadataChanges) {
-			metadataHelper.propagateMetadata(em, currentapp);
-		}
-		return Response.ok(currentapp).build();
-	}
-
-	private Map<String, ApplicationMetadata> getMapMD(Set<ApplicationMetadata> amd) {
-		Map<String, ApplicationMetadata> map = new HashMap<String, ApplicationMetadata>();
-		if (amd != null) {
-			for (ApplicationMetadata applicationMetadata : amd) {
-				map.put(applicationMetadata.getKey(), applicationMetadata);
-			}
-		}
-		return map;
-	}
-
-	@DELETE
-	@Path("/{appid}")
-	@EnsureTransaction
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response delete(@PathParam("appid") String appid, @Context HttpServletRequest request) {
-		LOG.info("Deleting app with id: {}", appid);
-		// EntityManager em = emProvider.get();
-		Application app = em.find(Application.class, Integer.parseInt(appid));
-		if (app == null) {
-			LOG.error("Application with id {} can not be deleted, It was not found in DB", appid);
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid).build();
-		}
-		/*
-		 * if (app.getLicenseTypes() != null &&
-		 * !app.getLicenseTypes().isEmpty()) { throw new
-		 * SeCurisServiceException(ErrorCodes.NOT_FOUND,
-		 * "Application can not be deleted becasue has assigned one or more License types, ID: "
-		 * + appid); }
-		 */
-		em.remove(app);
-		return Response.ok(Utils.createMap("success", true, "id", appid)).build();
-	}
-
+    /**
+     * delete<p>
+     * Delete an application by ID.
+     * <p>Note: deletion is not allowed if there are dependent entities (enforced by DB/cascade).</p>
+     * 
+     * @param appId
+     * @param request
+     */
+    @DELETE
+    @Path("/{appid}")
+    @EnsureTransaction
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response delete(@PathParam("appid") String appid, @Context HttpServletRequest request) {
+        LOG.info("Deleting app with id: {}", appid);
+        Application app = em.find(Application.class, Integer.parseInt(appid));
+        if (app == null) {
+            LOG.error("Application with id {} can not be deleted, It was not found in DB", appid);
+            return Response.status(Status.NOT_FOUND)
+                    .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid)
+                    .build();
+        }
+        em.remove(app);
+        return Response.ok(Utils.createMap("success", true, "id", appid)).build();
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/BasicServices.java b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
index f025795..2c989cc 100644
--- a/securis/src/main/java/net/curisit/securis/services/BasicServices.java
+++ b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.net.URI;
@@ -32,96 +35,124 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * Basic services for login and basic app wrkflow
- * 
- * @author roberto <roberto.sanchez@curisit.net>
+ * BasicServices
+ * <p>
+ * Minimal public endpoints for service liveness, version info and token checks.
+ * Also provides entry routing to SPA (admin/login/licenses) via /index.jsp.
+ *
+ * Security:
+ * <ul>
+ *   <li>/check requires a valid bearer token (via {@link Securable}).</li>
+ *   <li>/logout just logs intention; token invalidation is outside this class.</li>
+ * </ul>
+ *
+ * Author: roberto &lt;roberto.sanchez@curisit.net&gt;
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/")
 @ApplicationScoped
 public class BasicServices {
 
-	private static final Logger LOG = LogManager.getLogger(BasicServices.class);
+    private static final Logger LOG = LogManager.getLogger(BasicServices.class);
 
-	@Inject
-	TokenHelper tokenHelper;
+    @Inject TokenHelper tokenHelper;
+    @Context EntityManager em;
 
-	@Context
-	EntityManager em;
+    @Inject  public BasicServices() {}
 
-	@Inject
-	public BasicServices() {
-	}
+    /** 
+     * info<p>
+     * Simple liveness text endpoint. 
+     * 
+     * @param request
+     * @return response
+     */
+    @GET
+    @Path("/info")
+    @Produces({ MediaType.TEXT_PLAIN })
+    public Response info(@Context HttpServletRequest request) {
+        return Response.ok().entity("License server running OK. Date: " + new Date()).build();
+    }
 
-	@GET
-	@Path("/info")
-	@Produces({ MediaType.TEXT_PLAIN })
-	public Response info(@Context HttpServletRequest request) {
-		return Response.ok().entity("License server running OK. Date: " + new Date()).build();
-	}
+    /** 
+     * version<p>
+     * Returns semantic app version as JSON. 
+     * 
+     * @param request
+     * @return version
+     */
+    @GET
+    @Path("/version")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Map<String, String> version(@Context HttpServletRequest request) {
+        Map<String, String> resp = new HashMap<>();
+        resp.put("version", AppVersion.getInstance().getCompleteVersion());
+        return resp;
+    }
 
-	@GET
-	@Path("/version")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Map<String, String> version(@Context HttpServletRequest request) {
-		Map<String, String> resp = new HashMap<>();
-		
-		// Get the real version
-		String version = AppVersion.getInstance().getCompleteVersion();
-		resp.put("version", version);
-		return resp;
-	}
+    /**
+     * init<p>
+     * Redirects SPA modules to the main index page.
+     * 
+     * @param module
+     * @param request
+     * @return response
+     */
+    @GET
+    @Path("/{module:(admin)|(login)|(licenses)}")
+    @Produces({ MediaType.TEXT_HTML })
+    public Response init(@PathParam("module") String module, @Context HttpServletRequest request) {
+        LOG.info("App index main.html");
+        URI uri = UriBuilder.fromUri("/index.jsp").build();
+        return Response.seeOther(uri).build();
+    }
 
-	@GET
-	@Path("/{module:(admin)|(login)|(licenses)}")
-	@Produces({ MediaType.TEXT_HTML })
-	public Response init(@PathParam("module") String module, @Context HttpServletRequest request) {
-		LOG.info("App index main.html");
-		String page = "/index.jsp";
-		URI uri = UriBuilder.fromUri(page).build();
-		return Response.seeOther(uri).build();
-	}
+    /**
+     * check<p>
+     * Validates a token (from header or query param).
+     *
+     * @param token X-Token header
+     * @param token2 token query param fallback
+     * @return 200 with user/date if valid, 401/403 otherwise
+     */
+    @GET
+    @Securable()
+    @Path("/check")
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+        if (token == null) token = token2;
+        if (token == null) {
+            return Response.status(Status.FORBIDDEN).build();
+        }
+        boolean valid = tokenHelper.isTokenValid(token);
+        if (!valid) {
+            return Response.status(Status.UNAUTHORIZED).build();
+        }
 
-	/**
-	 * Check if current token is valid
-	 * 
-	 * @param user
-	 * @param password
-	 * @param request
-	 * @return
-	 */
-	@GET
-	@Securable()
-	@Path("/check")
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
-		if (token == null) {
-			token = token2;
-		}
-		if (token == null) {
-			return Response.status(Status.FORBIDDEN).build();
-		}
-		boolean valid = tokenHelper.isTokenValid(token);
-		if (!valid) {
-			return Response.status(Status.UNAUTHORIZED).build();
-		}
+        String user = tokenHelper.extractUserFromToken(token);
+        Date date = tokenHelper.extractDateCreationFromToken(token);
+        return Response.ok(Utils.createMap("valid", true, "user", user, "date", date)).build();
+    }
 
-		String user = tokenHelper.extractUserFromToken(token);
-		Date date = tokenHelper.extractDateCreationFromToken(token);
-
-		return Response.ok(Utils.createMap("valid", true, "user", user, "date", date)).build();
-	}
-
-	@GET
-	@POST
-	@Path("/logout")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response logout(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		if (token == null) {
-			Response.status(Status.BAD_REQUEST).build();
-		}
-		String user = tokenHelper.extractUserFromToken(token);
-		LOG.info("User {} has logged out", user);
-		return Response.ok().build();
-	}
+    /**
+     * logout<p>
+     * Logs logout event. (Token invalidation is handled elsewhere.)
+     * 
+     * @param token
+     * @return response
+     */
+    @GET
+    @POST
+    @Path("/logout")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response logout(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        if (token == null) {
+            Response.status(Status.BAD_REQUEST).build();
+        }
+        String user = tokenHelper.extractUserFromToken(token);
+        LOG.info("User {} has logged out", user);
+        return Response.ok().build();
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/LicenseResource.java b/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
index ed337ce..0885630 100644
--- a/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.io.File;
@@ -63,34 +66,45 @@
 import net.curisit.securis.utils.LicUtils;
 
 /**
- * License resource, this service will provide methods to create, modify and
- * delete licenses
- * 
- * @author roberto <roberto.sanchez@curisit.net>
+ * LicenseResource
+ * <p>
+ * REST resource in charge of managing licenses: list, fetch, create, activate,
+ * email delivery, cancel, block/unblock, modify and delete. It relies on
+ * {@link BasicSecurityContext} to scope access (organizations/apps) and
+ * on {@link EnsureTransaction} for mutating endpoints that need a TX.
+ * <p>
+ * Key rules:
+ * <ul>
+ *   <li>Non-admin users must belong to the license's organization.</li>
+ *   <li>License creation validates code CRC, activation code and email.</li>
+ *   <li>Request payload must match Pack constraints (org/type/pack codes).</li>
+ *   <li>History is recorded for key actions (CREATE/ACTIVATE/DOWNLOAD/etc.).</li>
+ * </ul>
+ *
+ * @author roberto
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/license")
 public class LicenseResource {
 
 	private static final Logger LOG = LogManager.getLogger(LicenseResource.class);
 
-	@Inject
-	private EmailManager emailManager;
+	@Inject private EmailManager emailManager;
+	@Inject private UserHelper userHelper;
+	@Inject private LicenseHelper licenseHelper;
+	@Inject private LicenseGenerator licenseGenerator;
 
-	@Inject
-	private UserHelper userHelper;
-
-	@Inject
-	private LicenseHelper licenseHelper;
-
-	@Context
-	EntityManager em;
-
-	@Inject
-	private LicenseGenerator licenseGenerator;
+	@Context EntityManager em;
 
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
+	 * index
+	 * <p>
+	 * List all licenses for a given pack. If the caller is not admin,
+	 * verifies the pack belongs to an accessible organization.
+	 *
+	 * @param packId Pack identifier to filter licenses (required).
+	 * @param bsc    Security context to evaluate roles and scoping.
+	 * @return 200 OK with a list (possibly empty), or 401 if unauthorized.
 	 */
 	@GET
 	@Path("/")
@@ -98,31 +112,33 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response index(@QueryParam("packId") Integer packId, @Context BasicSecurityContext bsc) {
 		LOG.info("Getting licenses list ");
-
-		// EntityManager em = emProvider.get();
 		em.clear();
 
 		if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
 			Pack pack = em.find(Pack.class, packId);
-			if (pack == null) {
-				return Response.ok().build();
-			}
+			if (pack == null) return Response.ok().build();
 			if (!bsc.getOrganizationsIds().contains(pack.getOrganization().getId())) {
 				LOG.error("Pack with id {} not accesible by user {}", pack, bsc.getUserPrincipal());
-				return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack licenses").build();
+				return Response.status(Status.UNAUTHORIZED)
+						.header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack licenses")
+						.build();
 			}
 		}
 		TypedQuery<License> q = em.createNamedQuery("list-licenses-by-pack", License.class);
 		q.setParameter("packId", packId);
 		List<License> list = q.getResultList();
-
 		return Response.ok(list).build();
 	}
 
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
-	 * @throws SeCurisServiceException
+	 * get
+	 * <p>
+	 * Fetch a single license by id, enforcing access scope for non-admin users.
+	 *
+	 * @param licId License id (required).
+	 * @param bsc   Security context.
+	 * @return 200 OK with the license.
+	 * @throws SeCurisServiceException 404 if not found, 401 if out of scope.
 	 */
 	@GET
 	@Path("/{licId}")
@@ -130,17 +146,22 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response get(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
 		LOG.info("Getting organization data for id: {}: ", licId);
-
-		// EntityManager em = emProvider.get();
 		em.clear();
 		License lic = getCurrentLicense(licId, bsc, em);
 		return Response.ok(lic).build();
 	}
 
 	/**
-	 * 
-	 * @return The license file, only of license is active
-	 * @throws SeCurisServiceException
+	 * download
+	 * <p>
+	 * Download the license file. Only allowed when the license is ACTIVE
+	 * and license data exists. Adds a DOWNLOAD entry in history.
+	 *
+	 * @param licId License id.
+	 * @param bsc   Security context.
+	 * @return 200 OK with the binary as application/octet-stream and a
+	 *         Content-Disposition header; otherwise specific error codes.
+	 * @throws SeCurisServiceException if state or data is invalid.
 	 */
 	@GET
 	@Path("/{licId}/download")
@@ -149,7 +170,6 @@
 	@EnsureTransaction
 	public Response download(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
 
-		// EntityManager em = emProvider.get();
 		License lic = getCurrentLicense(licId, bsc, em);
 
 		if (lic.getLicenseData() == null) {
@@ -166,12 +186,16 @@
 	}
 
 	/**
-	 * Activate the given license
-	 * 
-	 * @param licId
-	 * @param bsc
-	 * @return
-	 * @throws SeCurisServiceException
+	 * activate
+	 * <p>
+	 * Set license to ACTIVE provided status transition is valid, pack has
+	 * available units and request data passes validation/uniqueness.
+	 * Adds an ACTIVATE entry in history.
+	 *
+	 * @param licId License id.
+	 * @param bsc   Security context.
+	 * @return 200 OK with updated license.
+	 * @throws SeCurisServiceException if invalid transition, no availability or invalid request data.
 	 */
 	@PUT
 	@POST
@@ -182,7 +206,6 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response activate(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
 
-		// EntityManager em = emProvider.get();
 		License lic = getCurrentLicense(licId, bsc, em);
 
 		if (!License.Status.isActionValid(License.Action.ACTIVATION, lic.getStatus())) {
@@ -211,12 +234,18 @@
 	}
 
 	/**
-	 * Send license file by email to the organization
-	 * 
-	 * @param licId
-	 * @param bsc
-	 * @return
-	 * @throws SeCurisServiceException
+	 * send
+	 * <p>
+	 * Email the license file to the license owner. Builds a temporary file
+	 * using the application license filename and cleans it afterwards.
+	 * Adds a SEND entry in history.
+	 *
+	 * @param licId License id.
+	 * @param addCC whether to CC the current operator.
+	 * @param bsc   Security context.
+	 * @return 200 OK with the license (no state change).
+	 * @throws SeCurisServiceException when no license file exists or user full name is missing.
+	 * @throws SeCurisException        if JSON/signature process fails.
 	 */
 	@SuppressWarnings("deprecation")
 	@PUT
@@ -229,7 +258,6 @@
 	public Response send(@PathParam("licId") Integer licId, @DefaultValue("false") @FormParam("add_cc") Boolean addCC, @Context BasicSecurityContext bsc)
 			throws SeCurisServiceException, SeCurisException {
 
-		// EntityManager em = emProvider.get();
 		License lic = getCurrentLicense(licId, bsc, em);
 		Application app = lic.getPack().getLicenseType().getApplication();
 		File licFile = null;
@@ -259,19 +287,21 @@
 			}
 		}
 
-		// lic.setModificationTimestamp(new Date());
-		// em.merge(lic);
 		em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.SEND, "Email sent to: " + lic.getEmail()));
 		return Response.ok(lic).build();
 	}
 
 	/**
-	 * Cancel given license
-	 * 
-	 * @param licId
-	 * @param bsc
-	 * @return
-	 * @throws SeCurisServiceException
+	 * cancel
+	 * <p>
+	 * Cancel a license (requires valid state transition and a non-null reason).
+	 * Delegates to {@link LicenseHelper#cancelLicense}.
+	 *
+	 * @param licId      License id.
+	 * @param actionData DTO carrying the cancellation reason.
+	 * @param bsc        Security context.
+	 * @return 200 OK with updated license.
+	 * @throws SeCurisServiceException when state is invalid or reason is missing.
 	 */
 	@PUT
 	@POST
@@ -282,7 +312,6 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response cancel(@PathParam("licId") Integer licId, CancellationLicenseActionBean actionData, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
 
-		// EntityManager em = emProvider.get();
 		License lic = getCurrentLicense(licId, bsc, em);
 
 		if (!License.Status.isActionValid(License.Action.CANCEL, lic.getStatus())) {
@@ -300,22 +329,24 @@
 	}
 
 	/**
-	 * Check if there is some pack with the same code
-	 * 
-	 * @param code
-	 *            Pack code
-	 * @param em
-	 *            DB session object
-	 * @return <code>true</code> if code is already used, <code>false</code>
-	 *         otherwise
+	 * create
+	 * <p>
+	 * Create a license. Validates:
+	 * <ul>
+	 *   <li>Unique license code and valid CRC.</li>
+	 *   <li>Activation code presence and uniqueness.</li>
+	 *   <li>Valid user email.</li>
+	 *   <li>Pack existence, ACTIVE status and scope authorization.</li>
+	 *   <li>Request data consistency and unblock status (if provided).</li>
+	 * </ul>
+	 * If request data is provided and the Pack has availability, the license is
+	 * generated and set to ACTIVE immediately.
+	 *
+	 * @param lic License payload.
+	 * @param bsc Security context.
+	 * @return 200 OK with created license.
+	 * @throws SeCurisServiceException on validation failures.
 	 */
-	private boolean checkIfCodeExists(String code, EntityManager em) {
-		TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class);
-		query.setParameter("code", code);
-		int lics = query.getResultList().size();
-		return lics > 0;
-	}
-
 	@POST
 	@Path("/")
 	@Consumes(MediaType.APPLICATION_JSON)
@@ -323,8 +354,6 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	@EnsureTransaction
 	public Response create(License lic, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
-		// EntityManager em = emProvider.get();
-
 		if (checkIfCodeExists(lic.getCode(), em)) {
 			throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The license code is already used in an existing license");
 		}
@@ -370,10 +399,8 @@
 			}
 
 			if (pack.getNumAvailables() > 0) {
-
 				SignedLicenseBean signedLicense = generateLicense(lic, em);
-				// If user provide a request data the license status is passed
-				// directly to ACTIVE
+				// Move directly to ACTIVE when request data is provided
 				lic.setStatus(LicenseStatus.ACTIVE);
 				try {
 					lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
@@ -404,6 +431,203 @@
 		return Response.ok(lic).build();
 	}
 
+	/**
+	 * modify
+	 * <p>
+	 * Update license basic fields (comments, fullName, email) and, when
+	 * status is CREATED and request payload changes, re-normalize/validate and
+	 * regenerate the signed license data. Adds a MODIFY history entry.
+	 *
+	 * @param lic   New values.
+	 * @param licId License id.
+	 * @param bsc   Security context.
+	 * @return 200 OK with updated license.
+	 * @throws SeCurisServiceException if validation fails.
+	 */
+	@SuppressWarnings("deprecation")
+	@PUT
+	@POST
+	@Path("/{licId}")
+	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+	@EnsureTransaction
+	@Consumes(MediaType.APPLICATION_JSON)
+	@Produces({ MediaType.APPLICATION_JSON })
+	public Response modify(License lic, @PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+		LOG.info("Modifying license with id: {}", licId);
+
+		License currentLicense = getCurrentLicense(licId, bsc, em);
+		currentLicense.setComments(lic.getComments());
+		currentLicense.setFullName(lic.getFullName());
+		currentLicense.setEmail(lic.getEmail());
+		if (currentLicense.getActivationCode() == null) {
+			currentLicense.setActivationCode(lic.getActivationCode());
+		}
+
+		if (currentLicense.getStatus() == LicenseStatus.CREATED && !ObjectUtils.equals(currentLicense.getReqDataHash(), lic.getReqDataHash())) {
+			if (lic.getRequestData() != null) {
+				SignedLicenseBean signedLicense = generateLicense(lic, em);
+				try {
+					// Normalize the request JSON and update signed license JSON
+					currentLicense.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
+					LOG.info("JSON generated for request: \n{}", currentLicense.getRequestData());
+					if (BlockedRequest.isRequestBlocked(currentLicense.getRequestData(), em)) {
+						throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be used again");
+					}
+					currentLicense.setLicenseData(JsonUtils.toJSON(signedLicense));
+				} catch (SeCurisException e) {
+					LOG.error("Error generaing license JSON", e);
+					throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generaing license JSON");
+				}
+			} else {
+				currentLicense.setRequestData(null);
+			}
+		}
+
+		currentLicense.setModificationTimestamp(new Date());
+		em.persist(currentLicense);
+		em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.MODIFY));
+
+		return Response.ok(currentLicense).build();
+	}
+
+	/**
+	 * delete
+	 * <p>
+	 * Delete the license when the current status allows it. If the license
+	 * was BLOCKED, removes the BlockedRequest entry to unblock the request.
+	 *
+	 * @param licId License id.
+	 * @param bsc   Security context.
+	 * @return 200 OK with a success payload.
+	 * @throws SeCurisServiceException if status does not allow deletion.
+	 */
+	@DELETE
+	@Path("/{licId}")
+	@EnsureTransaction
+	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+	@Produces({ MediaType.APPLICATION_JSON })
+	public Response delete(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+		LOG.info("Deleting license with id: {}", licId);
+		License lic = getCurrentLicense(licId, bsc, em);
+
+		if (!License.Status.isActionValid(License.Action.DELETE, lic.getStatus())) {
+			LOG.error("License {} can not be deleted with status {}", lic.getCode(), lic.getStatus());
+			throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can not be deleted in current status: " + lic.getStatus().name());
+		}
+		if (lic.getStatus() == LicenseStatus.BLOCKED) {
+			BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
+			if (blockedReq != null) {
+				em.remove(blockedReq);
+			}
+		}
+
+		em.remove(lic);
+		return Response.ok(Utils.createMap("success", true, "id", licId)).build();
+	}
+
+	/**
+	 * block
+	 * <p>
+	 * Block the license request data (allowed only from CANCELLED state).
+	 * Persists a {@link BlockedRequest} and transitions the license to BLOCKED.
+	 *
+	 * @param licId License id.
+	 * @param bsc   Security context.
+	 * @return 200 OK with a success payload.
+	 * @throws SeCurisServiceException if state is not CANCELLED or already blocked.
+	 */
+	@POST
+	@Path("/{licId}/block")
+	@EnsureTransaction
+	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+	@Produces({ MediaType.APPLICATION_JSON })
+	public Response block(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+		LOG.info("Blocking license with id: {}", licId);
+		License lic = getCurrentLicense(licId, bsc, em);
+
+		if (!License.Status.isActionValid(License.Action.BLOCK, lic.getStatus())) {
+			LOG.error("License can only be blocked in CANCELLED status, current: {}", lic.getStatus().name());
+			throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can only be blocked in CANCELLED status");
+		}
+		if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+			throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is already blocked");
+		}
+		BlockedRequest blockedReq = new BlockedRequest();
+		blockedReq.setCreationTimestamp(new Date());
+		blockedReq.setBlockedBy(userHelper.getUser(bsc, em));
+		blockedReq.setRequestData(lic.getRequestData());
+
+		em.persist(blockedReq);
+		lic.setStatus(LicenseStatus.BLOCKED);
+		lic.setModificationTimestamp(new Date());
+		em.merge(lic);
+
+		em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.BLOCK));
+		return Response.ok(Utils.createMap("success", true, "id", licId)).build();
+	}
+
+	/**
+	 * unblock
+	 * <p>
+	 * Remove the block for the license request data (if present) and move
+	 * license back to CANCELLED. Adds an UNBLOCK history entry.
+	 *
+	 * @param licId License id.
+	 * @param bsc   Security context.
+	 * @return 200 OK with a success payload.
+	 * @throws SeCurisServiceException never if not blocked (returns success anyway).
+	 */
+	@POST
+	@Path("/{licId}/unblock")
+	@EnsureTransaction
+	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+	@Produces({ MediaType.APPLICATION_JSON })
+	public Response unblock(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+		LOG.info("Unblocking license with id: {}", licId);
+		License lic = getCurrentLicense(licId, bsc, em);
+
+		if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+			BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
+			em.remove(blockedReq);
+
+			lic.setStatus(LicenseStatus.CANCELLED);
+			lic.setModificationTimestamp(new Date());
+			em.merge(lic);
+			em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.UNBLOCK));
+		} else {
+			LOG.info("Request data for license {} is NOT blocked", licId);
+		}
+
+		return Response.ok(Utils.createMap("success", true, "id", licId)).build();
+	}
+
+	// ---------------------------------------------------------------------
+	// Helpers
+	// ---------------------------------------------------------------------
+
+	/** 
+	 * checkIfCodeExists<p>
+	 * Check if there is an existing license with the same code. 
+	 * 
+	 * @param code
+	 * @param entityManager
+	 */
+	private boolean checkIfCodeExists(String code, EntityManager em) {
+		TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class);
+		query.setParameter("code", code);
+		int lics = query.getResultList().size();
+		return lics > 0;
+	}
+
+	/**
+	 * generateLicense<p>
+	 * Generate a signed license from request data and pack metadata/expiration.
+	 *
+	 * @param license License with requestData and packId populated.
+	 * @param em      Entity manager.
+	 * @return Signed license bean.
+	 * @throws SeCurisServiceException if validation/generation fails.
+	 */
 	private SignedLicenseBean generateLicense(License license, EntityManager em) throws SeCurisServiceException {
 		SignedLicenseBean sl = null;
 		Pack pack = em.find(Pack.class, license.getPackId());
@@ -419,12 +643,14 @@
 	}
 
 	/**
-	 * We check if the given request data is valid for the current Pack and has
-	 * a valid format
-	 * 
-	 * @param pack
-	 * @param requestData
-	 * @throws SeCurisServiceException
+	 * validateRequestData<p>
+	 * Validate that requestData matches the Pack and is well-formed.
+	 *
+	 * @param pack           Target pack (org/type constraints).
+	 * @param requestData    Raw JSON string with the license request.
+	 * @param activationCode Activation code from the license payload.
+	 * @return Parsed {@link RequestBean}.
+	 * @throws SeCurisServiceException on format mismatch or wrong codes.
 	 */
 	private RequestBean validateRequestData(Pack pack, String requestData, String activationCode) throws SeCurisServiceException {
 		if (requestData == null) {
@@ -456,143 +682,16 @@
 		return rb;
 	}
 
-	@SuppressWarnings("deprecation")
-	@PUT
-	@POST
-	@Path("/{licId}")
-	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
-	@EnsureTransaction
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response modify(License lic, @PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
-		LOG.info("Modifying license with id: {}", licId);
-
-		// EntityManager em = emProvider.get();
-
-		License currentLicense = getCurrentLicense(licId, bsc, em);
-		currentLicense.setComments(lic.getComments());
-		currentLicense.setFullName(lic.getFullName());
-		currentLicense.setEmail(lic.getEmail());
-		if (currentLicense.getActivationCode() == null) {
-			currentLicense.setActivationCode(lic.getActivationCode());
-		}
-
-		if (currentLicense.getStatus() == LicenseStatus.CREATED && !ObjectUtils.equals(currentLicense.getReqDataHash(), lic.getReqDataHash())) {
-			if (lic.getRequestData() != null) {
-				SignedLicenseBean signedLicense = generateLicense(lic, em);
-				try {
-					// Next 2 lines are necessary to normalize the String that
-					// contains
-					// the request.
-					currentLicense.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
-					LOG.info("JSON generated for request: \n{}", currentLicense.getRequestData());
-					if (BlockedRequest.isRequestBlocked(currentLicense.getRequestData(), em)) {
-						throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be used again");
-					}
-					currentLicense.setLicenseData(JsonUtils.toJSON(signedLicense));
-				} catch (SeCurisException e) {
-					LOG.error("Error generaing license JSON", e);
-					throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generaing license JSON");
-				}
-			} else {
-				// This set method could pass a null value
-				currentLicense.setRequestData(null);
-			}
-		}
-
-		currentLicense.setModificationTimestamp(new Date());
-		em.persist(currentLicense);
-		em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.MODIFY));
-
-		return Response.ok(currentLicense).build();
-	}
-
-	@DELETE
-	@Path("/{licId}")
-	@EnsureTransaction
-	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response delete(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
-		LOG.info("Deleting license with id: {}", licId);
-		// EntityManager em = emProvider.get();
-		License lic = getCurrentLicense(licId, bsc, em);
-
-		if (!License.Status.isActionValid(License.Action.DELETE, lic.getStatus())) {
-			LOG.error("License {} can not be deleted with status {}", lic.getCode(), lic.getStatus());
-			throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can not be deleted in current status: " + lic.getStatus().name());
-		}
-		if (lic.getStatus() == LicenseStatus.BLOCKED) {
-			// If license is removed and it's blocked then the blocked request
-			// should be removed, that is,
-			// the license deletion will unblock the request data
-			BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
-			if (blockedReq != null) {
-				// This if is to avoid some race condition or if the request has
-				// been already removed manually
-				em.remove(blockedReq);
-			}
-		}
-
-		em.remove(lic);
-		return Response.ok(Utils.createMap("success", true, "id", licId)).build();
-	}
-
-	@POST
-	@Path("/{licId}/block")
-	@EnsureTransaction
-	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response block(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
-		LOG.info("Blocking license with id: {}", licId);
-		// EntityManager em = emProvider.get();
-		License lic = getCurrentLicense(licId, bsc, em);
-
-		if (!License.Status.isActionValid(License.Action.BLOCK, lic.getStatus())) {
-			LOG.error("License can only be blocked in CANCELLED status, current: {}", lic.getStatus().name());
-			throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can only be blocked in CANCELLED status");
-		}
-		if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
-			throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is already blocked");
-		}
-		BlockedRequest blockedReq = new BlockedRequest();
-		blockedReq.setCreationTimestamp(new Date());
-		blockedReq.setBlockedBy(userHelper.getUser(bsc, em));
-		blockedReq.setRequestData(lic.getRequestData());
-
-		em.persist(blockedReq);
-		lic.setStatus(LicenseStatus.BLOCKED);
-		lic.setModificationTimestamp(new Date());
-		em.merge(lic);
-
-		em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.BLOCK));
-		return Response.ok(Utils.createMap("success", true, "id", licId)).build();
-	}
-
-	@POST
-	@Path("/{licId}/unblock")
-	@EnsureTransaction
-	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response unblock(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
-		LOG.info("Unblocking license with id: {}", licId);
-		// EntityManager em = emProvider.get();
-		License lic = getCurrentLicense(licId, bsc, em);
-
-		if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
-			BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
-			em.remove(blockedReq);
-
-			lic.setStatus(LicenseStatus.CANCELLED);
-			lic.setModificationTimestamp(new Date());
-			em.merge(lic);
-			em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.UNBLOCK));
-		} else {
-			LOG.info("Request data for license {} is NOT blocked", licId);
-		}
-
-		return Response.ok(Utils.createMap("success", true, "id", licId)).build();
-	}
-
+	/**
+	 * getCurrentLicense<p>
+	 * Load a license and verify scope for non-admin users.
+	 *
+	 * @param licId License id.
+	 * @param bsc   Security context.
+	 * @param em    Entity manager.
+	 * @return License entity.
+	 * @throws SeCurisServiceException if id is missing, not found or unauthorized.
+	 */
 	private License getCurrentLicense(Integer licId, BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException {
 		if (licId == null || "".equals(Integer.toString(licId))) {
 			LOG.error("License ID is mandatory");
@@ -611,6 +710,13 @@
 		return lic;
 	}
 
+	// ---------------------------------------------------------------------
+	// DTOs
+	// ---------------------------------------------------------------------
+
+	/**
+	 * DTO used to carry a cancellation reason for the cancel endpoint.
+	 */
 	@JsonAutoDetect
 	@JsonIgnoreProperties(ignoreUnknown = true)
 	static class CancellationLicenseActionBean {
@@ -618,3 +724,4 @@
 		private String reason;
 	}
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java b/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java
index 28b5c7d..04c15a2 100644
--- a/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.util.Date;
@@ -44,31 +47,34 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * LicenseType resource, this service will provide methods to create, modify and
- * delete license types
- * 
- * @author roberto <roberto.sanchez@curisit.net>
+ * LicenseTypeResource
+ * <p>
+ * CRUD for license types. Non-admin queries are scoped to the applications
+ * accessible by the caller. Metadata changes are reconciled and, when keys
+ * change, can be propagated to dependent entities via {@link MetadataHelper}.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/licensetype")
 public class LicenseTypeResource {
 
 	private static final Logger LOG = LogManager.getLogger(LicenseTypeResource.class);
 
-	@Inject
-	TokenHelper tokenHelper;
+	@Inject TokenHelper tokenHelper;
+	@Inject MetadataHelper metadataHelper;
 
-	@Inject
-	MetadataHelper metadataHelper;
+	@Context EntityManager em;
 
-	@Context
-	EntityManager em;
-
-	public LicenseTypeResource() {
-	}
+	public LicenseTypeResource() { }
 
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
+	 * index
+	 * <p>
+	 * List license types. Non-admin users get only types for their allowed apps.
+	 *
+	 * @param bsc security context.
+	 * @return 200 OK with list (possibly empty).
 	 */
 	@GET
 	@Path("/")
@@ -76,8 +82,6 @@
 	@Securable
 	public Response index(@Context BasicSecurityContext bsc) {
 		LOG.info("Getting license types list ");
-
-		// EntityManager em = emProvider.get();
 		em.clear();
 		TypedQuery<LicenseType> q;
 		if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
@@ -87,18 +91,21 @@
 				return Response.ok().build();
 			}
 			q = em.createNamedQuery("list-license_types-by_apps-id", LicenseType.class);
-
 			q.setParameter("list_ids", bsc.getApplicationsIds());
 		}
 		List<LicenseType> list = q.getResultList();
-
 		return Response.ok(list).build();
 	}
 
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
-	 * @throws SeCurisServiceException
+	 * get
+	 * <p>
+	 * Fetch a license type by id.
+	 *
+	 * @param ltid  LicenseType id (string form).
+	 * @param token (unused) header token.
+	 * @return 200 OK with the entity.
+	 * @throws SeCurisServiceException 404 if not found.
 	 */
 	@GET
 	@Path("/{ltid}")
@@ -111,7 +118,6 @@
 			return Response.status(Status.NOT_FOUND).build();
 		}
 
-		// EntityManager em = emProvider.get();
 		em.clear();
 		LicenseType lt = em.find(LicenseType.class, Integer.parseInt(ltid));
 		if (lt == null) {
@@ -121,6 +127,16 @@
 		return Response.ok(lt).build();
 	}
 
+	/**
+	 * create
+	 * <p>
+	 * Create a new license type. Requires ADMIN. Sets application reference,
+	 * persists metadata entries and stamps creation time.
+	 *
+	 * @param lt    Payload.
+	 * @param token (unused) token header.
+	 * @return 200 OK with created entity, or 404 if app missing.
+	 */
 	@POST
 	@Path("/")
 	@Consumes(MediaType.APPLICATION_JSON)
@@ -130,7 +146,6 @@
 	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
 	public Response create(LicenseType lt, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
 		LOG.info("Creating new license type");
-		// EntityManager em = emProvider.get();
 
 		try {
 			setApplication(lt, lt.getApplicationId(), em);
@@ -158,16 +173,18 @@
 		return Response.ok(lt).build();
 	}
 
-	private Set<String> getMdKeys(Set<LicenseTypeMetadata> mds) {
-		Set<String> ids = new HashSet<String>();
-		if (mds != null) {
-			for (LicenseTypeMetadata md : mds) {
-				ids.add(md.getKey());
-			}
-		}
-		return ids;
-	}
-
+	/**
+	 * modify
+	 * <p>
+	 * Update an existing license type. Reconciles metadata:
+	 * removes keys not present in the new set; merges existing; persists new ones.
+	 * If keys changed, {@link MetadataHelper#propagateMetadata} is invoked.
+	 *
+	 * @param lt    New values.
+	 * @param ltid  LicenseType id.
+	 * @param token (unused) token.
+	 * @return 200 OK with updated entity; 404 if not found or app missing.
+	 */
 	@PUT
 	@POST
 	@Path("/{ltid}")
@@ -178,7 +195,6 @@
 	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
 	public Response modify(LicenseType lt, @PathParam("ltid") String ltid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
 		LOG.info("Modifying license type with id: {}", ltid);
-		// EntityManager em = emProvider.get();
 		LicenseType currentlt = em.find(LicenseType.class, Integer.parseInt(ltid));
 		if (currentlt == null) {
 			LOG.error("LicenseType with id {} not found in DB", ltid);
@@ -230,28 +246,23 @@
 		return Response.ok(currentlt).build();
 	}
 
-	private void setApplication(LicenseType licType, Integer applicationId, EntityManager em) throws SeCurisException {
-		Application app = null;
-		if (applicationId != null) {
-			app = em.find(Application.class, applicationId);
-			if (app == null) {
-				LOG.error("LicenseType application with id {} not found in DB", applicationId);
-
-				throw new SecurityException("License type's app not found with ID: " + applicationId);
-			}
-		}
-		licType.setApplication(app);
-	}
-
+	/**
+	 * delete
+	 * <p>
+	 * Delete a license type by id. Requires ADMIN.
+	 *
+	 * @param ltid LicenseType id.
+	 * @param req  request (unused).
+	 * @return 200 OK on success; 404 if not found.
+	 */
 	@DELETE
 	@Path("/{ltid}")
 	@EnsureTransaction
 	@Produces({ MediaType.APPLICATION_JSON })
 	@Securable(roles = Rol.ADMIN)
 	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response delete(@PathParam("ltid") String ltid, @Context HttpServletRequest request) {
+	public Response delete(@PathParam("ltid") String ltid, @Context HttpServletRequest req) {
 		LOG.info("Deleting app with id: {}", ltid);
-		// EntityManager em = emProvider.get();
 		LicenseType app = em.find(LicenseType.class, Integer.parseInt(ltid));
 		if (app == null) {
 			LOG.error("LicenseType with id {} can not be deleted, It was not found in DB", ltid);
@@ -262,4 +273,39 @@
 		return Response.ok(Utils.createMap("success", true, "id", ltid)).build();
 	}
 
+	// ---------------------------------------------------------------------
+	// Helpers
+	// ---------------------------------------------------------------------
+
+	private Set<String> getMdKeys(Set<LicenseTypeMetadata> mds) {
+		Set<String> ids = new HashSet<String>();
+		if (mds != null) {
+			for (LicenseTypeMetadata md : mds) {
+				ids.add(md.getKey());
+			}
+		}
+		return ids;
+	}
+
+	/**
+	 * setApplication<p>
+	 * Resolve and set the application of a license type.
+	 *
+	 * @param licType       target LicenseType.
+	 * @param applicationId id of the application (nullable).
+	 * @param em            entity manager.
+	 * @throws SeCurisException if id provided but not found.
+	 */
+	private void setApplication(LicenseType licType, Integer applicationId, EntityManager em) throws SeCurisException {
+		Application app = null;
+		if (applicationId != null) {
+			app = em.find(Application.class, applicationId);
+			if (app == null) {
+				LOG.error("LicenseType application with id {} not found in DB", applicationId);
+				throw new SecurityException("License type's app not found with ID: " + applicationId);
+			}
+		}
+		licType.setApplication(app);
+	}
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java b/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java
index ee23619..30d5940 100644
--- a/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.util.Date;
@@ -39,10 +42,12 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * Organization resource, this service will provide methods to create, modify
- * and delete organizations
- * 
- * @author roberto <roberto.sanchez@curisit.net>
+ * OrganizationResource
+ * <p>
+ * CRUD and listing of organizations. Non-admin users are scoped by their
+ * accessible organization ids when listing.
+ *
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/organization")
 @RequestScoped
@@ -50,18 +55,18 @@
 
 	private static final Logger LOG = LogManager.getLogger(OrganizationResource.class);
 
-	@Context
-	EntityManager em;
+	@Context EntityManager em;
+	@Context BasicSecurityContext bsc;
 
-	@Context
-	BasicSecurityContext bsc;
-
-	public OrganizationResource() {
-	}
+	public OrganizationResource() { }
 
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
+	 * index
+	 * <p>
+	 * List organizations. For admins returns all; for non-admins filters
+	 * by the ids in {@link BasicSecurityContext#getOrganizationsIds()}.
+	 *
+	 * @return 200 OK with the list.
 	 */
 	@GET
 	@Path("/")
@@ -69,8 +74,6 @@
 	@Securable
 	public Response index() {
 		LOG.info("Getting organizations list ");
-
-		// EntityManager em = emProvider.get();
 		em.clear();
 		TypedQuery<Organization> q;
 		if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
@@ -84,15 +87,18 @@
 				q.setParameter("list_ids", bsc.getOrganizationsIds());
 			}
 		}
-
 		List<Organization> list = q.getResultList();
-
 		return Response.ok(list).build();
 	}
 
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
+	 * get
+	 * <p>
+	 * Fetch an organization by id.
+	 *
+	 * @param orgid organization id (string form).
+	 * @param token header token (unused).
+	 * @return 200 OK with entity or 404 if not found.
 	 */
 	@GET
 	@Path("/{orgid}")
@@ -104,8 +110,6 @@
 			LOG.error("Organization ID is mandatory");
 			return Response.status(Status.NOT_FOUND).build();
 		}
-
-		// EntityManager em = emProvider.get();
 		em.clear();
 		Organization org = em.find(Organization.class, Integer.parseInt(orgid));
 		if (org == null) {
@@ -115,16 +119,15 @@
 		return Response.ok(org).build();
 	}
 
-	private boolean isCyclicalRelationship(int currentId, Organization parent) {
-		while (parent != null) {
-			if (parent.getId() == currentId) {
-				return true;
-			}
-			parent = parent.getParentOrganization();
-		}
-		return false;
-	}
-
+	/**
+	 * create
+	 * <p>
+	 * Create a new organization, setting optional parent and user members.
+	 * Requires ADMIN.
+	 *
+	 * @param org payload with code/name/etc., optional parentOrgId and usersIds.
+	 * @return 200 OK with created organization or 404 when parent/user not found.
+	 */
 	@POST
 	@Path("/")
 	@Consumes(MediaType.APPLICATION_JSON)
@@ -134,7 +137,6 @@
 	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
 	public Response create(Organization org) {
 		LOG.info("Creating new organization");
-		// EntityManager em = emProvider.get();
 
 		try {
 			this.setParentOrg(org, org.getParentOrgId(), em);
@@ -162,36 +164,17 @@
 		return Response.ok(org).build();
 	}
 
-	private void setParentOrg(Organization org, Integer parentOrgId, EntityManager em) throws SeCurisException {
-		Organization parentOrg = null;
-		if (parentOrgId != null) {
-			parentOrg = em.find(Organization.class, parentOrgId);
-			if (parentOrg == null) {
-				LOG.error("Organization parent with id {} not found in DB", org.getParentOrgId());
-				throw new SecurityException("Organization's parent not found with ID: " + org.getParentOrgId());
-			}
-		}
-
-		org.setParentOrganization(parentOrg);
-	}
-
-	private void setOrgUsers(Organization org, Set<String> usersIds, EntityManager em) throws SeCurisException {
-		Set<User> users = null;
-		if (usersIds != null && !usersIds.isEmpty()) {
-			users = new HashSet<>();
-			for (String username : usersIds) {
-				User user = em.find(User.class, username);
-				if (user == null) {
-					LOG.error("Organization user with id '{}' not found in DB", username);
-					throw new SecurityException("Organization's user not found with ID: " + username);
-				}
-				users.add(user);
-			}
-		}
-
-		org.setUsers(users);
-	}
-
+	/**
+	 * modify
+	 * <p>
+	 * Update an organization. Validates no cyclic parent relationship,
+	 * updates parent and user set. Requires ADMIN.
+	 *
+	 * @param org   new values (including optional parent/usersIds).
+	 * @param orgid target id.
+	 * @param token (unused) header token.
+	 * @return 200 OK with updated organization, or specific error status.
+	 */
 	@PUT
 	@POST
 	@Path("/{orgid}")
@@ -202,7 +185,6 @@
 	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
 	public Response modify(Organization org, @PathParam("orgid") String orgid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
 		LOG.info("Modifying organization with id: {}", orgid);
-		// EntityManager em = emProvider.get();
 		Organization currentOrg = em.find(Organization.class, Integer.parseInt(orgid));
 		if (currentOrg == null) {
 			LOG.error("Organization with id {} not found in DB", orgid);
@@ -233,15 +215,23 @@
 		return Response.ok(currentOrg).build();
 	}
 
+	/**
+	 * delete
+	 * <p>
+	 * Delete an organization if it has no children. Requires ADMIN.
+	 *
+	 * @param orgid target id.
+	 * @param req   request (unused).
+	 * @return 200 OK with success map, or 404/403 on constraints.
+	 */
 	@DELETE
 	@Path("/{orgid}")
 	@EnsureTransaction
 	@Produces({ MediaType.APPLICATION_JSON })
 	@Securable(roles = Rol.ADMIN)
 	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response delete(@PathParam("orgid") String orgid, @Context HttpServletRequest request) {
+	public Response delete(@PathParam("orgid") String orgid, @Context HttpServletRequest req) {
 		LOG.info("Deleting organization with id: {}", orgid);
-		// EntityManager em = emProvider.get();
 		Organization org = em.find(Organization.class, Integer.parseInt(orgid));
 		if (org == null) {
 			LOG.error("Organization with id {} can not be deleted, It was not found in DB", orgid);
@@ -256,4 +246,70 @@
 		return Response.ok(Utils.createMap("success", true, "id", orgid)).build();
 	}
 
+	// ---------------------------------------------------------------------
+	// Helpers
+	// ---------------------------------------------------------------------
+
+	/** 
+	 * isCyclicalRelationship<p>
+	 * Detects cycles by walking up the parent chain. 
+	 * 
+	 * @param currentId
+	 * @param parent
+	 * @return isCyclicalRelationship
+	 */
+	private boolean isCyclicalRelationship(int currentId, Organization parent) {
+		while (parent != null) {
+			if (parent.getId() == currentId) return true;
+			parent = parent.getParentOrganization();
+		}
+		return false;
+	}
+
+	/** 
+	 * setParentOrg<p>
+	 * Resolve and set parent organization (nullable). 
+	 * 
+	 * @param org
+	 * @param parentOrgId
+	 * @param entitymanager
+	 * @throws SeCurisException
+	 */
+	private void setParentOrg(Organization org, Integer parentOrgId, EntityManager em) throws SeCurisException {
+		Organization parentOrg = null;
+		if (parentOrgId != null) {
+			parentOrg = em.find(Organization.class, parentOrgId);
+			if (parentOrg == null) {
+				LOG.error("Organization parent with id {} not found in DB", org.getParentOrgId());
+				throw new SecurityException("Organization's parent not found with ID: " + org.getParentOrgId());
+			}
+		}
+		org.setParentOrganization(parentOrg);
+	}
+
+	/** 
+	 * setOrgUsers<p>
+	 * Replace organization users from the provided usernames set. 
+	 * 
+	 * @param org
+	 * @param usersIds
+	 * @param entityManager
+	 * @throws SeCurisException
+	 */
+	private void setOrgUsers(Organization org, Set<String> usersIds, EntityManager em) throws SeCurisException {
+		Set<User> users = null;
+		if (usersIds != null && !usersIds.isEmpty()) {
+			users = new HashSet<>();
+			for (String username : usersIds) {
+				User user = em.find(User.class, username);
+				if (user == null) {
+					LOG.error("Organization user with id '{}' not found in DB", username);
+					throw new SecurityException("Organization's user not found with ID: " + username);
+				}
+				users.add(user);
+			}
+		}
+		org.setUsers(users);
+	}
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/PackResource.java b/securis/src/main/java/net/curisit/securis/services/PackResource.java
index 4623314..85b784f 100644
--- a/securis/src/main/java/net/curisit/securis/services/PackResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/PackResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.security.Principal;
@@ -53,31 +56,35 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * Pack resource, this service will provide methods to create, modify and delete
- * packs
- * 
- * @author roberto <roberto.sanchez@curisit.net>
+ * PackResource
+ * <p>
+ * Manages Packs (group of licenses bound to an organization, application/type,
+ * and configuration/metadata). Provides list/filter, get, create, modify,
+ * state transitions (activate/hold/cancel) and deletion.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/pack")
 public class PackResource {
 
 	private static final Logger LOG = LogManager.getLogger(PackResource.class);
 
-	@Inject
-	TokenHelper tokenHelper;
+	@Inject TokenHelper tokenHelper;
+	@Inject MetadataHelper metadataHelper;
+	@Inject private LicenseHelper licenseHelper;
 
-	@Inject
-	MetadataHelper metadataHelper;
-
-	@Context
-	EntityManager em;
-
-	@Inject
-	private LicenseHelper licenseHelper;
+	@Context EntityManager em;
 
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
+	 * index
+	 * <p>
+	 * List packs with optional filters (organizationId, applicationId, licenseTypeId).
+	 * For non-admins, results are scoped by both apps and orgs from {@link BasicSecurityContext}.
+	 *
+	 * @param uriInfo supplies query parameters.
+	 * @param bsc     security scope/roles.
+	 * @return 200 OK with the list (possibly empty).
 	 */
 	@GET
 	@Path("/")
@@ -86,70 +93,25 @@
 	public Response index(@Context UriInfo uriInfo, @Context BasicSecurityContext bsc) {
 		LOG.info("Getting packs list ");
 		MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
-
-		// EntityManager em = emProvider.get();
 		em.clear();
 
 		TypedQuery<Pack> q = createQuery(queryParams, bsc);
 		if (q == null) {
 			return Response.ok().build();
 		}
-
 		List<Pack> list = q.getResultList();
-
 		return Response.ok(list).build();
 	}
 
-	private String generateWhereFromParams(boolean addWhere, MultivaluedMap<String, String> queryParams) {
-		List<String> conditions = new ArrayList<>();
-		if (queryParams.containsKey("organizationId")) {
-			conditions.add(String.format("pa.organization.id = %s", queryParams.getFirst("organizationId")));
-		}
-		if (queryParams.containsKey("applicationId")) {
-			conditions.add(String.format("pa.licenseType.application.id = %s", queryParams.getFirst("applicationId")));
-		}
-		if (queryParams.containsKey("licenseTypeId")) {
-			conditions.add(String.format("pa.licenseType.id = %s", queryParams.getFirst("licenseTypeId")));
-		}
-		String connector = addWhere ? " where " : " and ";
-		return (conditions.isEmpty() ? "" : connector) + String.join(" and ", conditions);
-	}
-
-	private TypedQuery<Pack> createQuery(MultivaluedMap<String, String> queryParams, BasicSecurityContext bsc) {
-		TypedQuery<Pack> q;
-		String hql = "SELECT pa FROM Pack pa";
-		if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
-			hql += generateWhereFromParams(true, queryParams);
-			q = em.createQuery(hql, Pack.class);
-		} else {
-			if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
-				return null;
-			}
-			if (bsc.getOrganizationsIds() == null || bsc.getOrganizationsIds().isEmpty()) {
-				hql += " where pa.licenseType.application.id in :list_ids_app ";
-			} else {
-				hql += " where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app ";
-			}
-			hql += generateWhereFromParams(false, queryParams);
-			q = em.createQuery(hql, Pack.class);
-			if (hql.contains("list_ids_org")) {
-				q.setParameter("list_ids_org", bsc.getOrganizationsIds());
-			}
-			q.setParameter("list_ids_app", bsc.getApplicationsIds());
-			LOG.info("Getting packs from orgs: {} and apps: {}", bsc.getOrganizationsIds(), bsc.getApplicationsIds());
-		}
-
-		return q;
-	}
-
-	private Response generateErrorUnathorizedAccess(Pack pack, Principal user) {
-		LOG.error("Pack with id {} not accesible by user {}", pack, user);
-		return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack").build();
-	}
-
 	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
+	 * get
+	 * <p>
+	 * Fetch a pack by id. If the caller is an ADVANCE user, validates
+	 * the organization scope.
+	 *
+	 * @param packId pack id.
+	 * @param bsc    security context.
+	 * @return 200 OK with entity, or 404/401 accordingly.
 	 */
 	@GET
 	@Path("/{packId}")
@@ -162,7 +124,6 @@
 			return Response.status(Status.NOT_FOUND).build();
 		}
 
-		// EntityManager em = emProvider.get();
 		em.clear();
 		Pack pack = em.find(Pack.class, packId);
 		if (pack == null) {
@@ -175,6 +136,18 @@
 		return Response.ok(pack).build();
 	}
 
+	/**
+	 * create
+	 * <p>
+	 * Create a new pack. Validates code uniqueness, sets organization and
+	 * license type references, stamps creator and timestamps, and persists
+	 * metadata entries.
+	 *
+	 * @param pack payload.
+	 * @param bsc  security context (for createdBy).
+	 * @return 200 OK with created pack or 404 when references not found.
+	 * @throws SeCurisServiceException on duplicated code.
+	 */
 	@POST
 	@Path("/")
 	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@@ -184,7 +157,6 @@
 	@EnsureTransaction
 	public Response create(Pack pack, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
 		LOG.info("Creating new pack");
-		// EntityManager em = emProvider.get();
 
 		if (checkIfCodeExists(pack.getCode(), em)) {
 			throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is already used in an existing pack");
@@ -221,67 +193,16 @@
 	}
 
 	/**
-	 * Check if there is some pack with the same code
-	 * 
-	 * @param code
-	 *            Pack code
-	 * @param em
-	 *            DB session object
-	 * @return <code>true</code> if code is already used, <code>false</code>
-	 *         otherwise
+	 * modify
+	 * <p>
+	 * Update a pack basic fields and reconcile metadata (remove/merge/persist).
+	 * If metadata keys changed, marks dependent licenses metadata as obsolete via
+	 * {@link MetadataHelper#markObsoleteMetadata}.
+	 *
+	 * @param pack   payload values.
+	 * @param packId target id.
+	 * @return 200 OK with updated pack or 404 on ref errors.
 	 */
-	private boolean checkIfCodeExists(String code, EntityManager em) {
-		TypedQuery<Pack> query = em.createNamedQuery("pack-by-code", Pack.class);
-		query.setParameter("code", code);
-		int packs = query.getResultList().size();
-		return packs > 0;
-	}
-
-	/**
-	 * 
-	 * @return The next available code suffix in pack for license code
-	 * @throws SeCurisServiceException
-	 */
-	@GET
-	@Path("/{packId}/next_license_code")
-	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
-	@Produces({ MediaType.TEXT_PLAIN })
-	public Response getCodeSuffix(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
-		// EntityManager em = emProvider.get();
-
-		if (packId == null) {
-			throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is mandatory");
-		}
-		Integer codeSuffix = licenseHelper.getNextCodeSuffix(packId, em);
-		Pack pack = em.find(Pack.class, packId);
-		;
-
-		String licCode = LicUtils.getLicenseCode(pack.getCode(), codeSuffix);
-		return Response.ok(licCode).build();
-	}
-
-	private void setPackLicenseType(Pack pack, Integer licTypeId, EntityManager em) throws SeCurisException {
-		LicenseType lt = null;
-		if (licTypeId != null) {
-			lt = em.find(LicenseType.class, pack.getLicTypeId());
-			if (lt == null) {
-				LOG.error("Pack license type with id {} not found in DB", licTypeId);
-				throw new SeCurisException("Pack license type not found with ID: " + licTypeId);
-			}
-		}
-		pack.setLicenseType(lt);
-	}
-
-	private Set<String> getMdKeys(Set<PackMetadata> mds) {
-		Set<String> ids = new HashSet<String>();
-		if (mds != null) {
-			for (PackMetadata md : mds) {
-				ids.add(md.getKey());
-			}
-		}
-		return ids;
-	}
-
 	@PUT
 	@POST
 	@Path("/{packId}")
@@ -292,7 +213,6 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response modify(Pack pack, @PathParam("packId") Integer packId) {
 		LOG.info("Modifying pack with id: {}", packId);
-		// EntityManager em = emProvider.get();
 		Pack currentPack = em.find(Pack.class, packId);
 
 		try {
@@ -348,6 +268,15 @@
 		return Response.ok(currentPack).build();
 	}
 
+	/**
+	 * activate
+	 * <p>
+	 * Move a pack to ACTIVE (only from allowed states).
+	 *
+	 * @param packId target pack id.
+	 * @return 200 OK with updated pack or error when invalid transition.
+	 * @throws SeCurisServiceException when invalid state transition.
+	 */
 	@POST
 	@Path("/{packId}/activate")
 	@EnsureTransaction
@@ -357,7 +286,6 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response activate(@PathParam("packId") Integer packId) throws SeCurisServiceException {
 		LOG.info("Activating pack with id: {}", packId);
-		// EntityManager em = emProvider.get();
 
 		Pack currentPack = em.find(Pack.class, packId);
 
@@ -372,8 +300,17 @@
 		return Response.ok(currentPack).build();
 	}
 
+	/**
+	 * onhold
+	 * <p>
+	 * Put a pack ON_HOLD from allowed states.
+	 *
+	 * @param packId id.
+	 * @return 200 OK with updated pack or error on invalid state.
+	 * @throws SeCurisServiceException on invalid state.
+	 */
 	@POST
-	@Path("/{packId}/putonhold")
+	@Path("/{packId}/putonhold}")
 	@EnsureTransaction
 	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
 	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@@ -381,7 +318,6 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response onhold(@PathParam("packId") Integer packId) throws SeCurisServiceException {
 		LOG.info("Putting On hold pack with id: {}", packId);
-		// EntityManager em = emProvider.get();
 
 		Pack currentPack = em.find(Pack.class, packId);
 
@@ -396,6 +332,18 @@
 		return Response.ok(currentPack).build();
 	}
 
+	/**
+	 * cancel
+	 * <p>
+	 * Cancel a pack. Cascades cancel to ACTIVE/PRE_ACTIVE licenses in the pack
+	 * via {@link LicenseHelper#cancelLicense}.
+	 *
+	 * @param packId id.
+	 * @param reason cancellation reason.
+	 * @param bsc    actor for history entries.
+	 * @return 200 OK with updated pack.
+	 * @throws SeCurisServiceException on invalid state.
+	 */
 	@POST
 	@Path("/{packId}/cancel")
 	@EnsureTransaction
@@ -405,7 +353,6 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response cancel(@PathParam("packId") Integer packId, @FormParam("reason") String reason, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
 		LOG.info("Cancelling pack with id: {}", packId);
-		// EntityManager em = emProvider.get();
 
 		Pack currentPack = em.find(Pack.class, packId);
 
@@ -426,18 +373,41 @@
 		return Response.ok(currentPack).build();
 	}
 
-	private void setPackOrganization(Pack currentPack, Integer orgId, EntityManager em) throws SeCurisException {
-		Organization org = null;
-		if (orgId != null) {
-			org = em.find(Organization.class, orgId);
-			if (org == null) {
-				LOG.error("Organization pack with id {} not found in DB", orgId);
-				throw new SeCurisException("Pack organization not found with ID: " + orgId);
-			}
+	/**
+	 * getCodeSuffix
+	 * <p>
+	 * Compute the next available license code for a pack, by asking the helper
+	 * for the next numeric suffix and composing with {@link LicUtils}.
+	 *
+	 * @param packId id.
+	 * @param bsc    (unused) security context.
+	 * @return 200 OK with the full code text.
+	 * @throws SeCurisServiceException if packId missing.
+	 */
+	@GET
+	@Path("/{packId}/next_license_code")
+	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+	@Produces({ MediaType.TEXT_PLAIN })
+	public Response getCodeSuffix(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+		if (packId == null) {
+			throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is mandatory");
 		}
-		currentPack.setOrganization(org);
+		Integer codeSuffix = licenseHelper.getNextCodeSuffix(packId, em);
+		Pack pack = em.find(Pack.class, packId);
+		String licCode = LicUtils.getLicenseCode(pack.getCode(), codeSuffix);
+		return Response.ok(licCode).build();
 	}
 
+	/**
+	 * delete
+	 * <p>
+	 * Delete a pack after ensuring there are no ACTIVE/PRE_ACTIVE licenses.
+	 * Removes remaining licenses then the pack itself.
+	 *
+	 * @param packId String id.
+	 * @return 200 OK with success map, 404 if missing, or 409 if active license exists.
+	 * @throws SeCurisServiceException on constraint errors.
+	 */
 	@DELETE
 	@Path("/{packId}")
 	@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@@ -446,13 +416,11 @@
 	@Produces({ MediaType.APPLICATION_JSON })
 	public Response delete(@PathParam("packId") String packId) throws SeCurisServiceException {
 		LOG.info("Deleting pack with id: {}", packId);
-		// EntityManager em = emProvider.get();
 		Pack pack = em.find(Pack.class, Integer.parseInt(packId));
 		if (pack == null) {
 			LOG.error("Pack with id {} can not be deleted, It was not found in DB", packId);
 			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Pack was not found, ID: " + packId).build();
 		}
-		// Pack metadata is removed in cascade automatically.
 
 		Set<License> licenses = pack.getLicenses();
 		for (License license : licenses) {
@@ -466,4 +434,149 @@
 		return Response.ok(Utils.createMap("success", true, "id", packId)).build();
 	}
 
+	// ---------------------------------------------------------------------
+	// Helpers
+	// ---------------------------------------------------------------------
+
+	/**
+	 * generateWhereFromParams<p>
+	 * Generate where clause to include to a query
+	 * 
+	 * @param addWhere
+	 * @param queryParams
+	 * @return whereClause
+	 */
+	private String generateWhereFromParams(boolean addWhere, MultivaluedMap<String, String> queryParams) {
+		List<String> conditions = new ArrayList<>();
+		if (queryParams.containsKey("organizationId")) {
+			conditions.add(String.format("pa.organization.id = %s", queryParams.getFirst("organizationId")));
+		}
+		if (queryParams.containsKey("applicationId")) {
+			conditions.add(String.format("pa.licenseType.application.id = %s", queryParams.getFirst("applicationId")));
+		}
+		if (queryParams.containsKey("licenseTypeId")) {
+			conditions.add(String.format("pa.licenseType.id = %s", queryParams.getFirst("licenseTypeId")));
+		}
+		String connector = addWhere ? " where " : " and ";
+		return (conditions.isEmpty() ? "" : connector) + String.join(" and ", conditions);
+	}
+
+	/** 
+	 * createQuery<p>
+	 * Build a typed query considering role-based scopes and filters. 
+	 * 
+	 * @param queryParams
+	 * @param basicSecurityContext
+	 */
+	private TypedQuery<Pack> createQuery(MultivaluedMap<String, String> queryParams, BasicSecurityContext bsc) {
+		TypedQuery<Pack> q;
+		String hql = "SELECT pa FROM Pack pa";
+		if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
+			hql += generateWhereFromParams(true, queryParams);
+			q = em.createQuery(hql, Pack.class);
+		} else {
+			if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
+				return null;
+			}
+			if (bsc.getOrganizationsIds() == null || bsc.getOrganizationsIds().isEmpty()) {
+				hql += " where pa.licenseType.application.id in :list_ids_app ";
+			} else {
+				hql += " where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app ";
+			}
+			hql += generateWhereFromParams(false, queryParams);
+			q = em.createQuery(hql, Pack.class);
+			if (hql.contains("list_ids_org")) {
+				q.setParameter("list_ids_org", bsc.getOrganizationsIds());
+			}
+			q.setParameter("list_ids_app", bsc.getApplicationsIds());
+			LOG.info("Getting packs from orgs: {} and apps: {}", bsc.getOrganizationsIds(), bsc.getApplicationsIds());
+		}
+		return q;
+	}
+
+	/** 
+	 * generateErrorUnathorizedAccess<p>
+	 * Convenience 401 generator with log.
+	 * 
+	 *  @param pack
+	 *  @param user
+	 */
+	private Response generateErrorUnathorizedAccess(Pack pack, Principal user) {
+		LOG.error("Pack with id {} not accesible by user {}", pack, user);
+		return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack").build();
+	}
+
+	/**
+	 * setPackLicenseType<p>
+	 * Set the pack type
+	 * 
+	 * @param pack
+	 * @param licTypeId
+	 * @param em
+	 * @throws SeCurisException
+	 */
+	private void setPackLicenseType(Pack pack, Integer licTypeId, EntityManager em) throws SeCurisException {
+		LicenseType lt = null;
+		if (licTypeId != null) {
+			lt = em.find(LicenseType.class, pack.getLicTypeId());
+			if (lt == null) {
+				LOG.error("Pack license type with id {} not found in DB", licTypeId);
+				throw new SeCurisException("Pack license type not found with ID: " + licTypeId);
+			}
+		}
+		pack.setLicenseType(lt);
+	}
+
+	/**
+	 * getMdKeys<p>
+	 * Get the MD keys
+	 * 
+	 * @param mds
+	 * @return mdKeys
+	 */
+	private Set<String> getMdKeys(Set<PackMetadata> mds) {
+		Set<String> ids = new HashSet<String>();
+		if (mds != null) {
+			for (PackMetadata md : mds) {
+				ids.add(md.getKey());
+			}
+		}
+		return ids;
+	}
+
+	/**
+	 * checkIfCodeExists<p>
+	 * Check if the code already exist
+	 * 
+	 * @param code
+	 * @param em
+	 * @return codeExist
+	 */
+	private boolean checkIfCodeExists(String code, EntityManager em) {
+		TypedQuery<Pack> query = em.createNamedQuery("pack-by-code", Pack.class);
+		query.setParameter("code", code);
+		int packs = query.getResultList().size();
+		return packs > 0;
+	}
+
+	/**
+	 * setPackOrganization<p>
+	 * Set the organization of the pack
+	 * 
+	 * @param currentPack
+	 * @param orgId
+	 * @param em
+	 * @throws SeCurisException
+	 */
+	private void setPackOrganization(Pack currentPack, Integer orgId, EntityManager em) throws SeCurisException {
+		Organization org = null;
+		if (orgId != null) {
+			org = em.find(Organization.class, orgId);
+			if (org == null) {
+				LOG.error("Organization pack with id {} not found in DB", orgId);
+				throw new SeCurisException("Pack organization not found with ID: " + orgId);
+			}
+		}
+		currentPack.setOrganization(org);
+	}
 }
diff --git a/securis/src/main/java/net/curisit/securis/services/UserResource.java b/securis/src/main/java/net/curisit/securis/services/UserResource.java
index d5a9690..7c4681a 100644
--- a/securis/src/main/java/net/curisit/securis/services/UserResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/UserResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.util.Date;
@@ -47,291 +50,437 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * User resource
- * 
+ * UserResource
+ * <p>
+ * REST resource that manages users (CRUD + authentication helpers).
+ * All endpoints are guarded and ADMIN-only unless otherwise stated.
+ * <p>
+ * Notes:
+ * - Uses {@link BasicSecurityContext} authorization via @Securable and @RolesAllowed.
+ * - Uses JPA {@link EntityManager} injected through @Context.
+ * - Mutating endpoints are wrapped in @EnsureTransaction to guarantee commit/rollback.
+ * - Passwords are stored as SHA-256 hashes (see {@link Utils#sha256(String)}).
+ *
+ * Endpoints:
+ *  GET  /user/              -> list users
+ *  GET  /user/{uid}         -> get user by username
+ *  POST /user/              -> create user (idempotent: upsert semantics)
+ *  PUT  /user/{uid}         -> update user (creates if not exists)
+ *  POST /user/login         -> password authentication; returns token and basic identity
+ *  POST /user/check         -> validates a token and returns token metadata
+ *  GET  /user/logout        -> invalidates HTTP session (non-token based)
+ *
+ * Thread-safety: RequestScoped. No shared mutable state.
+ *
  * @author roberto <roberto.sanchez@curisit.net>
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/user")
 @RequestScoped
 public class UserResource {
 
-	@Inject
-	TokenHelper tokenHelper;
+    /** Token encoder/decoder & validator. */
+    @Inject TokenHelper tokenHelper;
 
-	@Inject
-	private CacheTTL cache;
+    /** Small cache to invalidate role/org derived data after user mutations. */
+    @Inject private CacheTTL cache;
 
-	@Context
-	EntityManager em;
+    /** JPA entity manager bound to the current request context. */
+    @Context EntityManager em;
 
-	private static final Logger LOG = LogManager.getLogger(UserResource.class);
+    private static final Logger LOG = LogManager.getLogger(UserResource.class);
 
-	public UserResource() {
-	}
+    /**
+     * UserResource 
+     * Default constructor for CDI. 
+     */
+    public UserResource() {
+    }
 
-	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
-	 */
-	@GET
-	@Path("/")
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response index() {
-		LOG.info("Getting users list ");
+    // ---------------------------------------------------------------------
+    // Read operations
+    // ---------------------------------------------------------------------
 
-		// EntityManager em = emProvider.get();
-		em.clear();
-		TypedQuery<User> q = em.createNamedQuery("list-users", User.class);
+    /**
+     * index
+     * <p>
+     * List all users.
+     *
+     * Security: ADMIN only.
+     *
+     * @return 200 OK with JSON array of {@link User}, or 200 OK with empty list.
+     */
+    @GET
+    @Path("/")
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response index() {
+        LOG.info("Getting users list ");
 
-		List<User> list = q.getResultList();
+        em.clear();
+        TypedQuery<User> q = em.createNamedQuery("list-users", User.class);
+        List<User> list = q.getResultList();
 
-		return Response.ok(list).build();
-	}
+        return Response.ok(list).build();
+    }
 
-	/**
-	 * 
-	 * @return The user
-	 */
-	@GET
-	@Path("/{uid}")
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response get(@PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Getting user data for id: {}: ", uid);
-		if (uid == null || "".equals(uid)) {
-			LOG.error("User ID is mandatory");
-			return Response.status(Status.NOT_FOUND).build();
-		}
+    /**
+     * get
+     * <p>
+     * Retrieve a single user by username.
+     *
+     * Security: ADMIN only.
+     *
+     * @param uid Username (primary key).
+     * @param token Optional token header (unused here, enforced by filters).
+     * @return 200 OK with user payload or 404 if not found/invalid uid.
+     */
+    @GET
+    @Path("/{uid}")
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response get(@PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Getting user data for id: {}: ", uid);
+        if (uid == null || "".equals(uid)) {
+            LOG.error("User ID is mandatory");
+            return Response.status(Status.NOT_FOUND).build();
+        }
 
-		// EntityManager em = emProvider.get();
-		em.clear();
-		User lt = em.find(User.class, uid);
-		if (lt == null) {
-			LOG.error("User with id {} not found in DB", uid);
-			return Response.status(Status.NOT_FOUND).build();
-		}
-		return Response.ok(lt).build();
-	}
+        em.clear();
+        User lt = em.find(User.class, uid);
+        if (lt == null) {
+            LOG.error("User with id {} not found in DB", uid);
+            return Response.status(Status.NOT_FOUND).build();
+        }
+        return Response.ok(lt).build();
+    }
 
-	@POST
-	@Path("/")
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response create(User user, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Creating new user");
-		// EntityManager em = emProvider.get();
-		User currentUser = em.find(User.class, user.getUsername());
-		if (currentUser != null) {
-			LOG.info("User with id {} was found in DB, we'll try to modify it", user.getUsername());
-			return modify(user, user.getUsername(), token);
-		}
+    // ---------------------------------------------------------------------
+    // Create / Update / Delete
+    // ---------------------------------------------------------------------
 
-		try {
-			this.setUserOrgs(user, user.getOrgsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		try {
-			this.setUserApps(user, user.getAppsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		if (user.getPassword() != null && !"".equals(user.getPassword())) {
-			user.setPassword(Utils.sha256(user.getPassword()));
-		} else {
-			return Response.status(DefaultExceptionHandler.DEFAULT_APP_ERROR_STATUS_CODE).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "User password is mandatory")
-					.build();
-		}
-		user.setModificationTimestamp(new Date());
-		user.setLastLogin(null);
-		user.setCreationTimestamp(new Date());
-		em.persist(user);
+    /**
+     * create
+     * <p>
+     * Create a new user. If the username already exists, delegates to {@link #modify(User, String, String)}
+     * to behave like an upsert.
+     *
+     * Security: ADMIN only.
+     * Transaction: yes (via @EnsureTransaction).
+     *
+     * @param user  Incoming user payload. Password must be non-empty (plain text).
+     *              Password is SHA-256 hashed before persist.
+     * @param token Security token header (unused here; enforced by filters).
+     * @return 200 OK with created/updated user; 4xx on validation errors.
+     */
+    @POST
+    @Path("/")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response create(User user, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Creating new user");
 
-		return Response.ok(user).build();
-	}
+        User currentUser = em.find(User.class, user.getUsername());
+        if (currentUser != null) {
+            LOG.info("User with id {} was found in DB, we'll try to modify it", user.getUsername());
+            return modify(user, user.getUsername(), token);
+        }
 
-	private void setUserOrgs(User user, Set<Integer> orgsIds, EntityManager em) throws SeCurisException {
-		Set<Organization> orgs = null;
-		if (orgsIds != null && !orgsIds.isEmpty()) {
-			orgs = new HashSet<>();
-			for (Integer orgId : orgsIds) {
-				Organization o = em.find(Organization.class, orgId);
-				if (o == null) {
-					LOG.error("User organization with id {} not found in DB", orgId);
-					throw new SeCurisException("User's organization not found with ID: " + orgId);
-				}
-				orgs.add(o);
-			}
-		}
+        try {
+            this.setUserOrgs(user, user.getOrgsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
+        try {
+            this.setUserApps(user, user.getAppsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
 
-		user.setOrganizations(orgs);
+        // Password must be provided on create
+        if (user.getPassword() != null && !"".equals(user.getPassword())) {
+            user.setPassword(Utils.sha256(user.getPassword()));
+        } else {
+            return Response.status(DefaultExceptionHandler.DEFAULT_APP_ERROR_STATUS_CODE)
+                    .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "User password is mandatory")
+                    .build();
+        }
 
-	}
+        user.setModificationTimestamp(new Date());
+        user.setLastLogin(null);
+        user.setCreationTimestamp(new Date());
+        em.persist(user);
 
-	private void setUserApps(User user, Set<Integer> appsIds, EntityManager em) throws SeCurisException {
-		Set<Application> apps = null;
-		if (appsIds != null && !appsIds.isEmpty()) {
-			apps = new HashSet<>();
-			for (Integer appId : appsIds) {
-				Application o = em.find(Application.class, appId);
-				if (o == null) {
-					LOG.error("User application with id {} not found in DB", appId);
-					throw new SeCurisException("User's application not found with ID: " + appId);
-				}
-				apps.add(o);
-			}
-		}
+        return Response.ok(user).build();
+    }
 
-		user.setApplications(apps);
-	}
+    /**
+     * setUserOrgs
+     * <p>
+     * Resolve and set the organizations for a user from a set of IDs.
+     * Validates each id exists in DB.
+     *
+     * @param user     Target user entity.
+     * @param orgsIds  Organization ids to assign (nullable/empty allowed).
+     * @param em       EntityManager.
+     * @throws SeCurisException if any of the referenced organizations does not exist.
+     */
+    private void setUserOrgs(User user, Set<Integer> orgsIds, EntityManager em) throws SeCurisException {
+        Set<Organization> orgs = null;
+        if (orgsIds != null && !orgsIds.isEmpty()) {
+            orgs = new HashSet<>();
+            for (Integer orgId : orgsIds) {
+                Organization o = em.find(Organization.class, orgId);
+                if (o == null) {
+                    LOG.error("User organization with id {} not found in DB", orgId);
+                    throw new SeCurisException("User's organization not found with ID: " + orgId);
+                }
+                orgs.add(o);
+            }
+        }
+        user.setOrganizations(orgs);
+    }
 
-	@PUT
-	@POST
-	@Path("/{uid}")
-	@EnsureTransaction
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response modify(User user, @PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Modifying user with id: {}", uid);
-		// EntityManager em = emProvider.get();
-		User currentUser = em.find(User.class, uid);
-		if (currentUser == null) {
-			LOG.info("User with id {} not found in DB, we'll try to create it", uid);
-			return create(user, token);
-		}
+    /**
+     * setUserApps
+     * <p>
+     * Resolve and set the applications for a user from a set of IDs.
+     * Validates each id exists in DB.
+     *
+     * @param user     Target user entity.
+     * @param appsIds  Application ids to assign (nullable/empty allowed).
+     * @param em       EntityManager.
+     * @throws SeCurisException if any of the referenced applications does not exist.
+     */
+    private void setUserApps(User user, Set<Integer> appsIds, EntityManager em) throws SeCurisException {
+        Set<Application> apps = null;
+        if (appsIds != null && !appsIds.isEmpty()) {
+            apps = new HashSet<>();
+            for (Integer appId : appsIds) {
+                Application o = em.find(Application.class, appId);
+                if (o == null) {
+                    LOG.error("User application with id {} not found in DB", appId);
+                    throw new SeCurisException("User's application not found with ID: " + appId);
+                }
+                apps.add(o);
+            }
+        }
+        user.setApplications(apps);
+    }
 
-		try {
-			this.setUserOrgs(currentUser, user.getOrgsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		try {
-			this.setUserApps(currentUser, user.getAppsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		currentUser.setFirstName(user.getFirstName());
-		currentUser.setLastName(user.getLastName());
-		currentUser.setRoles(user.getRoles());
-		currentUser.setLang(user.getLang());
-		currentUser.setModificationTimestamp(new Date());
-		if (user.getPassword() != null && !"".equals(user.getPassword())) {
-			currentUser.setPassword(Utils.sha256(user.getPassword()));
-		} else {
-			// Password has not been modified
-			// return
-		}
+    /**
+     * modify
+     * <p>
+     * Update an existing user. If the user does not exist, delegates to {@link #create(User, String)}.
+     * Password is updated only if a non-empty password is provided.
+     * Organizations & applications are fully replaced with the given ids.
+     *
+     * Security: ADMIN only.
+     * Transaction: yes (via @EnsureTransaction).
+     *
+     * @param user  Incoming user payload.
+     * @param uid   Username (path param) to update.
+     * @param token Security token header (unused here).
+     * @return 200 OK with updated user; 404 if reference entities are missing.
+     */
+    @PUT
+    @POST
+    @Path("/{uid}")
+    @EnsureTransaction
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response modify(User user, @PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Modifying user with id: {}", uid);
 
-		currentUser.setLastLogin(user.getLastLogin());
+        User currentUser = em.find(User.class, uid);
+        if (currentUser == null) {
+            LOG.info("User with id {} not found in DB, we'll try to create it", uid);
+            return create(user, token);
+        }
 
-		em.persist(currentUser);
-		clearUserCache(currentUser.getUsername());
+        try {
+            this.setUserOrgs(currentUser, user.getOrgsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
+        try {
+            this.setUserApps(currentUser, user.getAppsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
 
-		return Response.ok(currentUser).build();
-	}
+        currentUser.setFirstName(user.getFirstName());
+        currentUser.setLastName(user.getLastName());
+        currentUser.setRoles(user.getRoles());
+        currentUser.setLang(user.getLang());
+        currentUser.setModificationTimestamp(new Date());
 
-	@DELETE
-	@Path("/{uid}")
-	@EnsureTransaction
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response delete(@PathParam("uid") String uid, @Context HttpServletRequest request) {
-		LOG.info("Deleting app with id: {}", uid);
-		// EntityManager em = emProvider.get();
-		User user = em.find(User.class, uid);
-		if (user == null) {
-			LOG.error("User with id {} can not be deleted, It was not found in DB", uid);
-			return Response.status(Status.NOT_FOUND).build();
-		}
+        // Optional password update
+        if (user.getPassword() != null && !"".equals(user.getPassword())) {
+            currentUser.setPassword(Utils.sha256(user.getPassword()));
+        }
 
-		em.remove(user);
-		clearUserCache(user.getUsername());
-		return Response.ok(Utils.createMap("success", true, "id", uid)).build();
-	}
+        // lastLogin can be set through API (rare), otherwise managed at login
+        currentUser.setLastLogin(user.getLastLogin());
 
-	private void clearUserCache(String username) {
-		cache.remove("roles_" + username);
-		cache.remove("orgs_" + username);
-	}
+        em.persist(currentUser);
+        clearUserCache(currentUser.getUsername());
 
-	@POST
-	@Path("/login")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response login(@FormParam("username") String username, @FormParam("password") String password, @Context HttpServletRequest request) throws SeCurisServiceException {
-		LOG.info("index session: " + request.getSession());
+        return Response.ok(currentUser).build();
+    }
 
-		// EntityManager em = emProvider.get();
-		User user = em.find(User.class, username);
-		if (user == null) {
-			LOG.error("Unknown username {} used in login service", username);
-			throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
-		}
-		String securedPassword = Utils.sha256(password);
+    /**
+     * delete
+     * <p>
+     * Delete a user by username.
+     *
+     * Security: ADMIN only.
+     * Transaction: yes (via @EnsureTransaction).
+     *
+     * @param uid Username to delete.
+     * @param request Http servlet request (unused).
+     * @return 200 OK on success; 404 if user does not exist.
+     */
+    @DELETE
+    @Path("/{uid}")
+    @EnsureTransaction
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response delete(@PathParam("uid") String uid, @Context HttpServletRequest request) {
+        LOG.info("Deleting app with id: {}", uid);
 
-		if (securedPassword == null || !securedPassword.equals(user.getPassword())) {
-			throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
-		}
-		user.setLastLogin(new Date());
-		em.getTransaction().begin();
-		try {
-			em.persist(user);
-			em.getTransaction().commit();
-		} catch (PersistenceException ex) {
-			LOG.error("Error updating last login date for user: {}", username);
-			LOG.error(ex);
-			em.getTransaction().rollback();
-		}
-		clearUserCache(username);
-		String userFullName = String.format("%s %s", user.getFirstName(), user.getLastName() == null ? "" : user.getLastName()).trim();
-		String tokenAuth = tokenHelper.generateToken(username);
-		return Response.ok(Utils.createMap("success", true, "token", tokenAuth, "username", username, "full_name", userFullName)).build();
-	}
+        User user = em.find(User.class, uid);
+        if (user == null) {
+            LOG.error("User with id {} can not be deleted, It was not found in DB", uid);
+            return Response.status(Status.NOT_FOUND).build();
+        }
 
-	/**
-	 * Check if current token is valid
-	 * 
-	 * @param user
-	 * @param password
-	 * @param request
-	 * @return
-	 */
-	@POST
-	@Path("/check")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
-		if (token == null) {
-			token = token2;
-		}
-		if (token == null) {
-			return Response.status(Status.FORBIDDEN).build();
-		}
+        em.remove(user);
+        clearUserCache(user.getUsername());
+        return Response.ok(Utils.createMap("success", true, "id", uid)).build();
+    }
 
-		LOG.info("Token : " + token);
-		String user = tokenHelper.extractUserFromToken(token);
-		LOG.info("Token user: " + user);
-		Date date = tokenHelper.extractDateCreationFromToken(token);
-		LOG.info("Token date: " + date);
-		boolean valid = tokenHelper.isTokenValid(token);
+    /**
+     * clearUserCache
+     * <p>
+     * Helper to invalidate cached role/org projections after changes.
+     *
+     * @param username The user whose cache entries must be cleared.
+     */
+    private void clearUserCache(String username) {
+        cache.remove("roles_" + username);
+        cache.remove("orgs_" + username);
+    }
 
-		LOG.info("Is Token valid: " + valid);
+    // ---------------------------------------------------------------------
+    // Auth helpers
+    // ---------------------------------------------------------------------
 
-		return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
-	}
+    /**
+     * login
+     * <p>
+     * Validates username & password against stored SHA-256 hash. On success,
+     * updates lastLogin timestamp, clears cache and returns an auth token.
+     *
+     * Token format: Base64("<secret> <user> <ISO8601-date>")
+     * where secret = SHA-256(seed + user + date).
+     *
+     * @param username Plain username.
+     * @param password Plain password (SHA-256 will be computed server-side).
+     * @param request  Http request, used to log underlying session (not required for token flow).
+     * @return 200 OK with {token, username, full_name}; 401 on invalid credentials.
+     * @throws SeCurisServiceException if user is missing or password mismatch.
+     */
+    @POST
+    @Path("/login")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response login(@FormParam("username") String username, @FormParam("password") String password, @Context HttpServletRequest request) throws SeCurisServiceException {
+        LOG.info("index session: " + request.getSession());
 
-	@GET
-	@Path("/logout")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response logout(@Context HttpServletRequest request) {
-		request.getSession().invalidate();
-		return Response.ok().build();
-	}
+        User user = em.find(User.class, username);
+        if (user == null) {
+            LOG.error("Unknown username {} used in login service", username);
+            throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
+        }
+        String securedPassword = Utils.sha256(password);
+
+        if (securedPassword == null || !securedPassword.equals(user.getPassword())) {
+            throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
+        }
+
+        user.setLastLogin(new Date());
+        em.getTransaction().begin();
+        try {
+            em.persist(user);
+            em.getTransaction().commit();
+        } catch (PersistenceException ex) {
+            LOG.error("Error updating last login date for user: {}", username);
+            LOG.error(ex);
+            em.getTransaction().rollback();
+        }
+
+        clearUserCache(username);
+        String userFullName = String.format("%s %s", user.getFirstName(), user.getLastName() == null ? "" : user.getLastName()).trim();
+        String tokenAuth = tokenHelper.generateToken(username);
+        return Response.ok(Utils.createMap("success", true, "token", tokenAuth, "username", username, "full_name", userFullName)).build();
+    }
+
+    /**
+     * check
+     * <p>
+     * Validates a token and echoes token claims (user, creation date, token string).
+     * Accepts header or query param for convenience.
+     *
+     * @param token  Token in header {@link TokenHelper#TOKEN_HEADER_PÀRAM}, may be null.
+     * @param token2 Token in query param 'token', used if header is null.
+     * @return 200 OK with {valid, user, date, token} or 403 if token missing.
+     */
+    @POST
+    @Path("/check")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+        if (token == null) {
+            token = token2;
+        }
+        if (token == null) {
+            return Response.status(Status.FORBIDDEN).build();
+        }
+
+        LOG.info("Token : " + token);
+        String user = tokenHelper.extractUserFromToken(token);
+        LOG.info("Token user: " + user);
+        Date date = tokenHelper.extractDateCreationFromToken(token);
+        LOG.info("Token date: " + date);
+        boolean valid = tokenHelper.isTokenValid(token);
+
+        LOG.info("Is Token valid: " + valid);
+
+        return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
+    }
+
+    /**
+     * logout
+     * <p>
+     * Invalidates the HTTP session (useful if the UI also tracks session).
+     * Note: token-based auth is stateless; tokens are not revoked here.
+     *
+     * @param request HttpServletRequest to invalidate session.
+     * @return 200 OK always.
+     */
+    @GET
+    @Path("/logout")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response logout(@Context HttpServletRequest request) {
+        request.getSession().invalidate();
+        return Response.ok().build();
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java b/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java
index bfe790c..e44e944 100644
--- a/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java
+++ b/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java
@@ -1,51 +1,90 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services.exception;
 
 import net.curisit.integrity.exception.CurisException;
 
+/**
+ * SeCurisServiceException
+ * <p>
+ * Checked exception for service-layer errors with an attached numeric error code.
+ * Extends {@link CurisException} and is intended to be translated to HTTP responses
+ * by upstream exception mappers/handlers.
+ *
+ * Usage:
+ *  - Prefer specific {@code ErrorCodes.*} when throwing.
+ *  - Use the single-arg constructor to default to UNEXPECTED_ERROR.
+ *  
+ *  @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
 public class SeCurisServiceException extends CurisException {
 
-	private int errorCode = 0;
+    /** Numeric error code associated with this exception. */
+    private int errorCode = 0;
 
-	public SeCurisServiceException(int errorCode, String msg) {
-		super(msg);
-		this.errorCode = errorCode;
-	}
+    /**
+     * Constructor with explicit error code.
+     *
+     * @param errorCode See {@link ErrorCodes}.
+     * @param msg       Human-readable message (safe to expose).
+     */
+    public SeCurisServiceException(int errorCode, String msg) {
+        super(msg);
+        this.errorCode = errorCode;
+    }
 
-	public SeCurisServiceException(String msg) {
-		super(msg);
-		this.errorCode = ErrorCodes.UNEXPECTED_ERROR;
-	}
+    /**
+     * Constructor defaulting to {@link ErrorCodes#UNEXPECTED_ERROR}.
+     *
+     * @param msg Human-readable message (safe to expose).
+     */
+    public SeCurisServiceException(String msg) {
+        super(msg);
+        this.errorCode = ErrorCodes.UNEXPECTED_ERROR;
+    }
 
-	public int getStatus() {
-		return errorCode;
-	}
+    /**
+     * getStatus
+     * <p>
+     * Returns the stored numeric error code.
+     *
+     * @return integer error code.
+     */
+    public int getStatus() {
+        return errorCode;
+    }
 
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-	public static class ErrorCodes {
-		public static int UNEXPECTED_ERROR = 1000;
-		public static int INVALID_CREDENTIALS = 1001;
-		public static int UNAUTHORIZED_ACCESS = 1002;
-		public static int NOT_FOUND = 1003;
-		public static int INVALID_FORMAT = 1004;
-		public static int WRONG_STATUS = 1005;
-		public static int UNNECESSARY_RENEW = 1006;
+    /**
+     * ErrorCodes
+     * <p>
+     * Canonical set of service-layer error codes.
+     * Grouped by feature areas (1000 generic, 11xx license, 12xx request data, 13xx validation).
+     */
+    public static class ErrorCodes {
+        public static int UNEXPECTED_ERROR = 1000;
+        public static int INVALID_CREDENTIALS = 1001;
+        public static int UNAUTHORIZED_ACCESS = 1002;
+        public static int NOT_FOUND = 1003;
+        public static int INVALID_FORMAT = 1004;
+        public static int WRONG_STATUS = 1005;
+        public static int UNNECESSARY_RENEW = 1006;
 
-		public static int INVALID_LICENSE_REQUEST_DATA = 1100;
-		public static int LICENSE_NOT_READY_FOR_RENEW = 1101;
-		public static int LICENSE_DATA_IS_NOT_VALID = 1102;
-		public static int LICENSE_IS_EXPIRED = 1103;
-		public static int LICENSE_PACK_IS_NOT_VALID = 1104;
+        public static int INVALID_LICENSE_REQUEST_DATA = 1100;
+        public static int LICENSE_NOT_READY_FOR_RENEW = 1101;
+        public static int LICENSE_DATA_IS_NOT_VALID = 1102;
+        public static int LICENSE_IS_EXPIRED = 1103;
+        public static int LICENSE_PACK_IS_NOT_VALID = 1104;
 
-		public static int INVALID_REQUEST_DATA = 1201;
-		public static int INVALID_REQUEST_DATA_FORMAT = 1202;
-		public static int BLOCKED_REQUEST_DATA = 1203;
-		public static int DUPLICATED_REQUEST_DATA = 1204;
-		public static int NO_AVAILABLE_LICENSES = 1205;
+        public static int INVALID_REQUEST_DATA = 1201;
+        public static int INVALID_REQUEST_DATA_FORMAT = 1202;
+        public static int BLOCKED_REQUEST_DATA = 1203;
+        public static int DUPLICATED_REQUEST_DATA = 1204;
+        public static int NO_AVAILABLE_LICENSES = 1205;
 
-		public static int INVALID_DATA = 1301;
-	}
+        public static int INVALID_DATA = 1301;
+    }
 }
diff --git a/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java b/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java
index 7159ddc..a2115d6 100644
--- a/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java
+++ b/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services.helpers;
 
 import java.io.File;
@@ -30,128 +33,192 @@
 import net.curisit.securis.services.exception.SeCurisServiceException;
 import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes;
 
+/**
+ * LicenseHelper
+ * <p>
+ * Stateless utility component for license lifecycle operations and helpers:
+ * - cancelation with history auditing
+ * - license resolution and validity checks
+ * - license file generation in a temp directory
+ * - metadata extraction and expiration date computation from packs
+ * - sequential code suffix allocation per pack
+ *
+ * Thread-safety: ApplicationScoped, stateless.
+ * 
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
 @ApplicationScoped
 public class LicenseHelper {
 
-	@SuppressWarnings("unused")
-	private static final Logger LOG = LogManager.getLogger(LicenseHelper.class);
-	private static final long MS_PER_DAY = 24L * 3600L * 1000L;
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LogManager.getLogger(LicenseHelper.class);
 
-	@Inject
-	private UserHelper userHelper;
+    /** Milliseconds per day (used to derive expiration dates). */
+    private static final long MS_PER_DAY = 24L * 3600L * 1000L;
 
-	/**
-	 * Cancel the license
-	 * 
-	 * @param lic
-	 * @param em
-	 */
-	public void cancelLicense(License lic, String reason, BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException {
-		lic.setStatus(LicenseStatus.CANCELLED);
-		lic.setCancelledById(bsc.getUserPrincipal().getName());
-		lic.setModificationTimestamp(new Date());
-		em.persist(lic);
+    @Inject private UserHelper userHelper;
 
-		em.persist(createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.CANCEL, "Cancellation reason: " + reason));
-	}
+    /**
+     * cancelLicense
+     * <p>
+     * Transitions a license to CANCELLED, records who canceled it and why,
+     * and appends a {@link LicenseHistory} entry.
+     *
+     * @param lic  Target license (managed).
+     * @param reason Human-readable cancellation reason (auditable).
+     * @param bsc Current security context (used to identify the user).
+     * @param em  Entity manager to persist changes.
+     * @throws SeCurisServiceException never thrown here, declared for symmetry with callers.
+     */
+    public void cancelLicense(License lic, String reason, BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException {
+        lic.setStatus(LicenseStatus.CANCELLED);
+        lic.setCancelledById(bsc.getUserPrincipal().getName());
+        lic.setModificationTimestamp(new Date());
+        em.persist(lic);
 
-	/**
-	 * Validates that the passed license exists and is still valid
-	 * 
-	 * @param licBean
-	 * @param em
-	 * @return The License instance in DB
-	 * @throws SeCurisServiceException
-	 */
-	public License getActiveLicenseFromDB(LicenseBean licBean, EntityManager em) throws SeCurisServiceException {
-		License lic = License.findLicenseByCode(licBean.getLicenseCode(), em);
-		if (lic == null) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license code doesn't exist");
-		}
-		if (lic.getStatus() != LicenseStatus.ACTIVE && lic.getStatus() != LicenseStatus.PRE_ACTIVE) {
-			throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license in not active");
-		}
-		return lic;
-	}
+        em.persist(createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.CANCEL, "Cancellation reason: " + reason));
+    }
 
-	public LicenseHistory createLicenseHistoryAction(License lic, User user, String action, String comments) {
-		LicenseHistory lh = new LicenseHistory();
-		lh.setLicense(lic);
-		lh.setUser(user);
-		lh.setCreationTimestamp(new Date());
-		lh.setAction(action);
-		lh.setComments(comments);
-		return lh;
-	}
+    /**
+     * getActiveLicenseFromDB
+     * <p>
+     * Resolve license by code and verify that it's ACTIVE or PRE_ACTIVE.
+     *
+     * @param licBean License bean containing the code to check.
+     * @param em      EntityManager for DB access.
+     * @return The managed {@link License} instance.
+     * @throws SeCurisServiceException if code not found or license not in an active-ish state.
+     */
+    public License getActiveLicenseFromDB(LicenseBean licBean, EntityManager em) throws SeCurisServiceException {
+        License lic = License.findLicenseByCode(licBean.getLicenseCode(), em);
+        if (lic == null) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license code doesn't exist");
+        }
+        if (lic.getStatus() != LicenseStatus.ACTIVE && lic.getStatus() != LicenseStatus.PRE_ACTIVE) {
+            throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license in not active");
+        }
+        return lic;
+    }
 
-	public LicenseHistory createLicenseHistoryAction(License lic, User user, String action) {
-		return createLicenseHistoryAction(lic, user, action, null);
-	}
+    /**
+     * createLicenseHistoryAction
+     * <p>
+     * Helper to build a {@link LicenseHistory} entry.
+     *
+     * @param lic      License affected.
+     * @param user     User performing the action.
+     * @param action   Action code (see {@link LicenseHistory.Actions}).
+     * @param comments Optional comments, can be null.
+     * @return transient {@link LicenseHistory} ready to persist.
+     */
+    public LicenseHistory createLicenseHistoryAction(License lic, User user, String action, String comments) {
+        LicenseHistory lh = new LicenseHistory();
+        lh.setLicense(lic);
+        lh.setUser(user);
+        lh.setCreationTimestamp(new Date());
+        lh.setAction(action);
+        lh.setComments(comments);
+        return lh;
+    }
 
-	/**
-	 * Create a license file in a temporary directory
-	 * 
-	 * @param lic
-	 * @param licFileName
-	 * @return
-	 * @throws IOException
-	 */
-	public File createTemporaryLicenseFile(License lic, String licFileName) throws IOException {
-		File f = Files.createTempDirectory("securis-server").toFile();
-		f = new File(f, licFileName);
-		FileUtils.writeStringToFile(f, lic.getLicenseData(), StandardCharsets.UTF_8);
-		return f;
-	}
+    /**
+     * createLicenseHistoryAction
+     * <p>
+     * Overload without comments.
+     *
+     * @param lic    License affected.
+     * @param user   User performing the action.
+     * @param action Action code.
+     * @return transient {@link LicenseHistory}.
+     */
+    public LicenseHistory createLicenseHistoryAction(License lic, User user, String action) {
+        return createLicenseHistoryAction(lic, user, action, null);
+    }
 
-	public Map<String, Object> extractPackMetadata(Set<PackMetadata> packMetadata) {
-		Map<String, Object> metadata = new HashMap<>();
-		for (PackMetadata md : packMetadata) {
-			metadata.put(md.getKey(), md.getValue());
-		}
+    /**
+     * createTemporaryLicenseFile
+     * <p>
+     * Materializes the license payload into a temporary file for emailing/download.
+     * The file is created under a unique temporary directory.
+     *
+     * Caller is responsible for deleting the file and its parent directory.
+     *
+     * @param lic         License whose JSON/XML/text payload is in {@code getLicenseData()}.
+     * @param licFileName Desired file name (e.g. "license.lic").
+     * @return A {@link File} pointing to the newly created file.
+     * @throws IOException If the temporary directory or file cannot be created/written.
+     */
+    public File createTemporaryLicenseFile(License lic, String licFileName) throws IOException {
+        File f = Files.createTempDirectory("securis-server").toFile();
+        f = new File(f, licFileName);
+        FileUtils.writeStringToFile(f, lic.getLicenseData(), StandardCharsets.UTF_8);
+        return f;
+    }
 
-		return metadata;
-	}
+    /**
+     * extractPackMetadata
+     * <p>
+     * Converts pack metadata set to a map for license generation.
+     *
+     * @param packMetadata Set of {@link PackMetadata}.
+     * @return Map with keys/values copied from metadata entries.
+     */
+    public Map<String, Object> extractPackMetadata(Set<PackMetadata> packMetadata) {
+        Map<String, Object> metadata = new HashMap<>();
+        for (PackMetadata md : packMetadata) {
+            metadata.put(md.getKey(), md.getValue());
+        }
+        return metadata;
+    }
 
-	/**
-	 * If the action is a renew the expiration date is got form pack end valid
-	 * date, if the action is a pre-activation the expiration date is calculated
-	 * using the pack default valid period
-	 * 
-	 * @param pack
-	 * @param isPreActivation
-	 * @return
-	 */
-	public Date getExpirationDateFromPack(Pack pack, boolean isPreActivation) {
-		Long validPeriod;
-		if (pack.getEndValidDate().before(new Date())) {
-			throw new CurisRuntimeException("Pack end valid period is reached, no new licenses can be activated.");
-		}
-		if (isPreActivation) {
-			validPeriod = pack.getPreactivationValidPeriod() * MS_PER_DAY;
-		} else {
-			if (pack.getRenewValidPeriod() <= 0) {
-				return pack.getEndValidDate();
-			}
-			long renewPeriod = pack.getRenewValidPeriod() * MS_PER_DAY;
-			long expirationPeriod = pack.getEndValidDate().getTime() - new Date().getTime();
-			validPeriod = renewPeriod < expirationPeriod ? renewPeriod : expirationPeriod;
-		}
-		Date expirationDate = new Date(new Date().getTime() + validPeriod);
-		return expirationDate;
-	}
+    /**
+     * getExpirationDateFromPack
+     * <p>
+     * Computes license expiration date depending on action type:
+     * - Pre-activation: {@code preactivationValidPeriod} days from now.
+     * - Renew/Activation: min(renewValidPeriod days, pack end date - now).
+     * Fails fast if pack end date is already in the past.
+     *
+     * @param pack            Pack with policy data.
+     * @param isPreActivation Whether the operation is a pre-activation.
+     * @return Calculated expiration {@link Date}.
+     * @throws CurisRuntimeException if the pack's end date is in the past.
+     */
+    public Date getExpirationDateFromPack(Pack pack, boolean isPreActivation) {
+        Long validPeriod;
+        if (pack.getEndValidDate().before(new Date())) {
+            throw new CurisRuntimeException("Pack end valid period is reached, no new licenses can be activated.");
+        }
+        if (isPreActivation) {
+            validPeriod = pack.getPreactivationValidPeriod() * MS_PER_DAY;
+        } else {
+            if (pack.getRenewValidPeriod() <= 0) {
+                return pack.getEndValidDate();
+            }
+            long renewPeriod = pack.getRenewValidPeriod() * MS_PER_DAY;
+            long expirationPeriod = pack.getEndValidDate().getTime() - new Date().getTime();
+            validPeriod = renewPeriod < expirationPeriod ? renewPeriod : expirationPeriod;
+        }
+        Date expirationDate = new Date(new Date().getTime() + validPeriod);
+        return expirationDate;
+    }
 
-	/**
-	 * Get the next free code suffis for a given Pack
-	 * 
-	 * @param packId
-	 * @param em
-	 * @return
-	 */
-	public int getNextCodeSuffix(int packId, EntityManager em) {
-		TypedQuery<Integer> query = em.createNamedQuery("last-code-suffix-used-in-pack", Integer.class);
-		query.setParameter("packId", packId);
-		Integer lastCodeSuffix = query.getSingleResult();
-		return lastCodeSuffix == null ? 1 : lastCodeSuffix + 1;
-	}
-
+    /**
+     * getNextCodeSuffix
+     * <p>
+     * Retrieves the last used code suffix for a given pack and returns the next one.
+     * If none found, returns 1.
+     *
+     * @param packId Pack identifier.
+     * @param em     EntityManager to query the DB.
+     * @return Next sequential suffix (>= 1).
+     */
+    public int getNextCodeSuffix(int packId, EntityManager em) {
+        TypedQuery<Integer> query = em.createNamedQuery("last-code-suffix-used-in-pack", Integer.class);
+        query.setParameter("packId", packId);
+        Integer lastCodeSuffix = query.getSingleResult();
+        return lastCodeSuffix == null ? 1 : lastCodeSuffix + 1;
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java b/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java
index ac70711..4fb1e3e 100644
--- a/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java
+++ b/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services.helpers;
 
 import java.util.Collection;
@@ -24,147 +27,258 @@
 import net.curisit.securis.db.PackMetadata;
 import net.curisit.securis.db.common.Metadata;
 
+/**
+ * MetadataHelper
+ * <p>
+ * Utilities to compare, merge and propagate metadata across the hierarchy:
+ *   Application -> LicenseType -> Pack -> (marks License as metadata-obsolete)
+ * <p>
+ * Provides:
+ * - Equality checks on metadata sets.
+ * - Merge semantics: remove keys not present, update changed values/mandatory flags.
+ * - Propagation from Application down to LicenseType and from LicenseType down to Packs.
+ * - Marking existing licenses as "metadataObsolete" when pack metadata changes and
+ *   the license is in a state where consumers could depend on metadata snapshot.
+ *
+ * Thread-safety: ApplicationScoped, stateless.
+ * 
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
 @ApplicationScoped
 public class MetadataHelper {
 
-	private static final Logger log = LogManager.getLogger(MetadataHelper.class);
+    private static final Logger log = LogManager.getLogger(MetadataHelper.class);
 
-	public <T extends Metadata> boolean match(T m1, T m2) {
-		if (m1 == null || m2 == null) {
-			return false;
-		}
-		return Objects.equals(m1.getKey(), m2.getKey()) && Objects.equals(m1.getValue(), m2.getValue()) && m1.isMandatory() == m2.isMandatory();
-	}
+    /**
+     * match
+     * <p>
+     * Compare two metadata entries (key, value, mandatory).
+     *
+     * @param m1 First metadata.
+     * @param m2 Second metadata.
+     * @param <T> Metadata subtype.
+     * @return true if equal in key/value/mandatory, false otherwise or if any is null.
+     */
+    public <T extends Metadata> boolean match(T m1, T m2) {
+        if (m1 == null || m2 == null) {
+            return false;
+        }
+        return Objects.equals(m1.getKey(), m2.getKey()) && Objects.equals(m1.getValue(), m2.getValue()) && m1.isMandatory() == m2.isMandatory();
+    }
 
-	public <T extends Metadata> Metadata findByKey(String key, Collection<T> listMd) {
-		return listMd.parallelStream().filter(m -> Objects.equals(key, m.getKey())).findAny().orElse(null);
-	}
+    /**
+     * findByKey
+     * <p>
+     * Find a metadata by key in a collection.
+     *
+     * @param key    Metadata key to search.
+     * @param listMd Collection of metadata.
+     * @param <T>    Metadata subtype.
+     * @return The first matching metadata or null.
+     */
+    public <T extends Metadata> Metadata findByKey(String key, Collection<T> listMd) {
+        return listMd.parallelStream().filter(m -> Objects.equals(key, m.getKey())).findAny().orElse(null);
+    }
 
-	public <T extends Metadata> boolean match(Set<T> listMd1, Set<T> listMd2) {
-		if (listMd1.size() != listMd2.size()) {
-			return false;
-		}
-		return listMd1.parallelStream().allMatch(m -> this.match(m, findByKey(m.getKey(), listMd2)));
-	}
+    /**
+     * match
+     * <p>
+     * Compare two sets of metadata for equality (size + all entries match).
+     *
+     * @param listMd1 First set.
+     * @param listMd2 Second set.
+     * @param <T>     Metadata subtype.
+     * @return true if both sets match element-wise, false otherwise.
+     */
+    public <T extends Metadata> boolean match(Set<T> listMd1, Set<T> listMd2) {
+        if (listMd1.size() != listMd2.size()) {
+            return false;
+        }
+        return listMd1.parallelStream().allMatch(m -> this.match(m, findByKey(m.getKey(), listMd2)));
+    }
 
-	public <T extends Metadata, K extends Metadata> void mergeMetadata(EntityManager em, Set<T> srcListMd, Set<K> tgtListMd, Set<String> keys) {
+    /**
+     * mergeMetadata
+     * <p>
+     * Merge metadata from a source set (truth) into a target set.
+     * - Removes entries in target whose keys are not in {@code keys}.
+     * - Updates entries in target whose value/mandatory differ from source.
+     * - Does NOT create new entries; caller is expected to persist new ones separately.
+     *
+     * @param em         EntityManager to remove/merge.
+     * @param srcListMd  Source metadata set (truth).
+     * @param tgtListMd  Target metadata set to update.
+     * @param keys       Keys present in source.
+     * @param <T>        Source metadata type.
+     * @param <K>        Target metadata type.
+     */
+    public <T extends Metadata, K extends Metadata> void mergeMetadata(EntityManager em, Set<T> srcListMd, Set<K> tgtListMd, Set<String> keys) {
 
-		Set<K> mdToRemove = tgtListMd.parallelStream() // 
-				.filter(md -> !keys.contains(md.getKey())) //
-				.collect(Collectors.toSet());
-		for (K tgtMd : mdToRemove) {
-			log.info("MD key to remove: {} - {}", tgtMd.getKey(), tgtMd);
-			if (tgtMd instanceof LicenseTypeMetadata) {
-				log.info("LT: {}, tx: {}, contans: {}", LicenseTypeMetadata.class.cast(tgtMd).getLicenseType(), em.isJoinedToTransaction(), em.contains(tgtMd));
-			}
-			em.remove(tgtMd);
-		}
-		Set<K> keysToUpdate = tgtListMd.parallelStream() // 
-				.filter(md -> keys.contains(md.getKey())) //
-				.collect(Collectors.toSet());
-		for (K tgtMd : keysToUpdate) {
-			Metadata md = this.findByKey(tgtMd.getKey(), srcListMd);
-			if (md.isMandatory() != tgtMd.isMandatory() || !Objects.equals(md.getValue(), tgtMd.getValue())) {
-				tgtMd.setMandatory(md.isMandatory());
-				tgtMd.setValue(md.getValue());
-				log.info("MD key to update: {}", tgtMd.getKey());
-				em.merge(tgtMd);
-			}
-		}
-	}
+        // Remove missing keys
+        Set<K> mdToRemove = tgtListMd.parallelStream()
+                .filter(md -> !keys.contains(md.getKey()))
+                .collect(Collectors.toSet());
+        for (K tgtMd : mdToRemove) {
+            log.info("MD key to remove: {} - {}", tgtMd.getKey(), tgtMd);
+            if (tgtMd instanceof LicenseTypeMetadata) {
+                log.info("LT: {}, tx: {}, contans: {}", LicenseTypeMetadata.class.cast(tgtMd).getLicenseType(), em.isJoinedToTransaction(), em.contains(tgtMd));
+            }
+            em.remove(tgtMd);
+        }
 
-	private Set<LicenseTypeMetadata> createNewMetadata(Set<ApplicationMetadata> appMd, Set<LicenseTypeMetadata> existingMd, LicenseType licenseType) {
-		Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
-		return appMd.parallelStream() // 
-				.filter(md -> !oldKeys.contains(md.getKey())) //
-				.map(appmd -> {
-					LicenseTypeMetadata ltmd = new LicenseTypeMetadata();
-					ltmd.setLicenseType(licenseType);
-					ltmd.setKey(appmd.getKey());
-					ltmd.setValue(appmd.getValue());
-					ltmd.setMandatory(appmd.isMandatory());
-					return ltmd;
-				}).collect(Collectors.toSet());
-	}
+        // Update changed keys
+        Set<K> keysToUpdate = tgtListMd.parallelStream()
+                .filter(md -> keys.contains(md.getKey()))
+                .collect(Collectors.toSet());
+        for (K tgtMd : keysToUpdate) {
+            Metadata md = this.findByKey(tgtMd.getKey(), srcListMd);
+            if (md.isMandatory() != tgtMd.isMandatory() || !Objects.equals(md.getValue(), tgtMd.getValue())) {
+                tgtMd.setMandatory(md.isMandatory());
+                tgtMd.setValue(md.getValue());
+                log.info("MD key to update: {}", tgtMd.getKey());
+                em.merge(tgtMd);
+            }
+        }
+    }
 
-	private Set<PackMetadata> createNewMetadata(Set<LicenseTypeMetadata> ltMd, Set<PackMetadata> existingMd, Pack pack) {
-		Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
-		return ltMd.parallelStream() // 
-				.filter(md -> !oldKeys.contains(md.getKey())) //
-				.map(md -> {
-					PackMetadata pmd = new PackMetadata();
-					pmd.setPack(pack);
-					pmd.setKey(md.getKey());
-					pmd.setValue(md.getValue());
-					pmd.setMandatory(md.isMandatory());
-					return pmd;
-				}).collect(Collectors.toSet());
-	}
+    // -- Internal helpers to create new metadata rows when propagating
 
-	/**
-	 * Copy the modified app metadata to LicenseTypes and Packs
-	 * 
-	 * @param em
-	 * @param app
-	 */
-	public void propagateMetadata(EntityManager em, Application app) {
-		Set<ApplicationMetadata> appMd = app.getApplicationMetadata();
-		Set<String> keys = appMd.parallelStream().map(md -> md.getKey()).collect(Collectors.toSet());
-		for (LicenseType lt : app.getLicenseTypes()) {
-			log.info("Lic type to update: {}", lt.getCode());
-			this.mergeMetadata(em, appMd, lt.getMetadata(), keys);
-			Set<LicenseTypeMetadata> newMdList = createNewMetadata(appMd, lt.getMetadata(), lt);
-			for (LicenseTypeMetadata newMetadata : newMdList) {
-				em.persist(newMetadata);
-			}
-			em.detach(lt);
-			// Probably there is a better way to get the final metadata from JPA...
-			TypedQuery<LicenseTypeMetadata> updatedMdQuery = em.createNamedQuery("list-licensetype-metadata", LicenseTypeMetadata.class);
-			updatedMdQuery.setParameter("licenseTypeId", lt.getId());
-			Set<LicenseTypeMetadata> updatedMd = new HashSet<>(updatedMdQuery.getResultList());
+    /**
+     * createNewMetadata<p>
+     * Create new metadata
+     * 
+     * @param appMd
+     * @param existingMd
+     * @param licenseType
+     * @return newMetadata
+     */
+    private Set<LicenseTypeMetadata> createNewMetadata(Set<ApplicationMetadata> appMd, Set<LicenseTypeMetadata> existingMd, LicenseType licenseType) {
+        Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
+        return appMd.parallelStream()
+                .filter(md -> !oldKeys.contains(md.getKey()))
+                .map(appmd -> {
+                    LicenseTypeMetadata ltmd = new LicenseTypeMetadata();
+                    ltmd.setLicenseType(licenseType);
+                    ltmd.setKey(appmd.getKey());
+                    ltmd.setValue(appmd.getValue());
+                    ltmd.setMandatory(appmd.isMandatory());
+                    return ltmd;
+                }).collect(Collectors.toSet());
+    }
 
-			lt.setMetadata(updatedMd);
-			propagateMetadata(em, lt, keys);
-		}
-	}
+    /**
+     * createNewMetadata<p>
+     * Create the new metadata
+     * 
+     * @param ltMd
+     * @param existingMd
+     * @param pack
+     * @return newMetadata
+     */
+    private Set<PackMetadata> createNewMetadata(Set<LicenseTypeMetadata> ltMd, Set<PackMetadata> existingMd, Pack pack) {
+        Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
+        return ltMd.parallelStream()
+                .filter(md -> !oldKeys.contains(md.getKey()))
+                .map(md -> {
+                    PackMetadata pmd = new PackMetadata();
+                    pmd.setPack(pack);
+                    pmd.setKey(md.getKey());
+                    pmd.setValue(md.getValue());
+                    pmd.setMandatory(md.isMandatory());
+                    return pmd;
+                }).collect(Collectors.toSet());
+    }
 
-	/**
-	 * Copy the modified licenseType metadata to Packs
-	 * 
-	 * @param em
-	 * @param lt
-	 * @param keys
-	 */
-	public void propagateMetadata(EntityManager em, LicenseType lt, Set<String> keys) {
-		Set<LicenseTypeMetadata> ltMd = lt.getMetadata();
-		TypedQuery<Pack> packsQuery = em.createNamedQuery("list-packs-by-lic-type", Pack.class);
-		packsQuery.setParameter("lt_id", lt.getId());
-		List<Pack> packs = packsQuery.getResultList();
-		log.info("Packs to update the metadata: {}", packs.size());
-		for (Pack pack : packs) {
-			if (pack.isFrozen()) {
-				log.warn("Metadata in LicenseType {} has changed but the Pack {} is frozen and won't be updated.", lt.getCode(), pack.getCode());
-				continue;
-			}
-			this.mergeMetadata(em, ltMd, pack.getMetadata(), keys);
-			Set<PackMetadata> newMdList = createNewMetadata(ltMd, pack.getMetadata(), pack);
-			for (PackMetadata newMetadata : newMdList) {
-				em.persist(newMetadata);
-			}
-			markObsoleteMetadata(em, pack);
-			em.detach(pack);
-		}
-	}
+    /**
+     * propagateMetadata (Application -> LicenseTypes -> Packs)
+     * <p>
+     * Propagates application metadata changes down to all its license types and packs:
+     * - mergeMetadata on LicenseType
+     * - create new LicenseTypeMetadata for new keys
+     * - re-fetch LT metadata (detached/merged semantics)
+     * - propagateMetadata(LicenseType) to packs
+     *
+     * @param em  EntityManager.
+     * @param app Application with updated metadata loaded.
+     */
+    public void propagateMetadata(EntityManager em, Application app) {
+        Set<ApplicationMetadata> appMd = app.getApplicationMetadata();
+        Set<String> keys = appMd.parallelStream().map(md -> md.getKey()).collect(Collectors.toSet());
+        for (LicenseType lt : app.getLicenseTypes()) {
+            log.info("Lic type to update: {}", lt.getCode());
+            this.mergeMetadata(em, appMd, lt.getMetadata(), keys);
+            Set<LicenseTypeMetadata> newMdList = createNewMetadata(appMd, lt.getMetadata(), lt);
+            for (LicenseTypeMetadata newMetadata : newMdList) {
+                em.persist(newMetadata);
+            }
+            em.detach(lt);
 
-	public void markObsoleteMetadata(EntityManager em, Pack pack) {
-		TypedQuery<License> existingPackLicenses = em.createNamedQuery("list-licenses-by-pack", License.class);
-		existingPackLicenses.setParameter("packId", pack.getId());
-		for (License lic : existingPackLicenses.getResultList()) {
-			log.info("License from pack: {}, status: {}", lic.getCode(), lic.getStatus());
-			if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE || lic.getStatus() == LicenseStatus.CANCELLED) {
-				lic.setMetadataObsolete(true);
-				em.merge(lic);
-			}
-		}
-	}
+            // Re-read updated metadata
+            TypedQuery<LicenseTypeMetadata> updatedMdQuery = em.createNamedQuery("list-licensetype-metadata", LicenseTypeMetadata.class);
+            updatedMdQuery.setParameter("licenseTypeId", lt.getId());
+            Set<LicenseTypeMetadata> updatedMd = new HashSet<>(updatedMdQuery.getResultList());
+
+            lt.setMetadata(updatedMd);
+            propagateMetadata(em, lt, keys);
+        }
+    }
+
+    /**
+     * propagateMetadata (LicenseType -> Packs)
+     * <p>
+     * Propagates license type metadata changes to all its packs:
+     * - mergeMetadata on Pack
+     * - create new PackMetadata for new keys
+     * - markObsoleteMetadata on packs to flag their licenses
+     *
+     * Frozen packs are skipped.
+     *
+     * @param em   EntityManager.
+     * @param lt   LicenseType with updated metadata set.
+     * @param keys Set of keys present in the source.
+     */
+    public void propagateMetadata(EntityManager em, LicenseType lt, Set<String> keys) {
+        Set<LicenseTypeMetadata> ltMd = lt.getMetadata();
+        TypedQuery<Pack> packsQuery = em.createNamedQuery("list-packs-by-lic-type", Pack.class);
+        packsQuery.setParameter("lt_id", lt.getId());
+        List<Pack> packs = packsQuery.getResultList();
+        log.info("Packs to update the metadata: {}", packs.size());
+        for (Pack pack : packs) {
+            if (pack.isFrozen()) {
+                log.warn("Metadata in LicenseType {} has changed but the Pack {} is frozen and won't be updated.", lt.getCode(), pack.getCode());
+                continue;
+            }
+            this.mergeMetadata(em, ltMd, pack.getMetadata(), keys);
+            Set<PackMetadata> newMdList = createNewMetadata(ltMd, pack.getMetadata(), pack);
+            for (PackMetadata newMetadata : newMdList) {
+                em.persist(newMetadata);
+            }
+            markObsoleteMetadata(em, pack);
+            em.detach(pack);
+        }
+    }
+
+    /**
+     * markObsoleteMetadata
+     * <p>
+     * For all licenses within the given pack, mark {@code metadataObsolete = true}
+     * if the license is in a relevant state (ACTIVE, PRE_ACTIVE, CANCELLED).
+     * This lets clients know that metadata-dependent artifacts might need refresh.
+     *
+     * @param em   EntityManager.
+     * @param pack Pack whose licenses to mark.
+     */
+    public void markObsoleteMetadata(EntityManager em, Pack pack) {
+        TypedQuery<License> existingPackLicenses = em.createNamedQuery("list-licenses-by-pack", License.class);
+        existingPackLicenses.setParameter("packId", pack.getId());
+        for (License lic : existingPackLicenses.getResultList()) {
+            log.info("License from pack: {}, status: {}", lic.getCode(), lic.getStatus());
+            if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE || lic.getStatus() == LicenseStatus.CANCELLED) {
+                lic.setMetadataObsolete(true);
+                em.merge(lic);
+            }
+        }
+    }
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java b/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java
index 4c561dc..b9bff32 100644
--- a/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java
+++ b/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services.helpers;
 
 import jakarta.enterprise.context.ApplicationScoped;
@@ -8,14 +11,45 @@
 import net.curisit.securis.security.BasicSecurityContext;
 import net.curisit.securis.services.exception.SeCurisServiceException;
 
+/**
+ * UserHelper
+ * <p>
+ * Small helper to resolve the current user (from security context) or by username.
+ * Throws a typed {@link SeCurisServiceException} if the user cannot be found.
+ *
+ * Thread-safety: ApplicationScoped, stateless.
+ * 
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
 @ApplicationScoped
 public class UserHelper {
 
+    /**
+     * getUser
+     * <p>
+     * Resolve the current authenticated user from {@link BasicSecurityContext}.
+     *
+     * @param bsc Security context containing a principal.
+     * @param em  EntityManager to fetch the user.
+     * @return Managed {@link User}.
+     * @throws SeCurisServiceException if the principal is null or not found in DB.
+     */
     public User getUser(BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException {
         String username = bsc.getUserPrincipal().getName();
         return getUser(username, em);
     }
 
+    /**
+     * getUser
+     * <p>
+     * Resolve a user by username.
+     *
+     * @param username Username to look up (nullable allowed; returns null).
+     * @param em       EntityManager to fetch the user.
+     * @return Managed {@link User} or null if username is null.
+     * @throws SeCurisServiceException if a non-null username does not exist.
+     */
     public User getUser(String username, EntityManager em) throws SeCurisServiceException {
         User user = null;
         if (username != null) {
diff --git a/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
index 2b0146a..8376449 100644
--- a/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
+++ b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
 package net.curisit.securis.utils;
 
 import java.util.ArrayList;
@@ -5,6 +8,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
@@ -13,122 +17,258 @@
 import org.apache.logging.log4j.Logger;
 
 /**
- * Cache implementation with TTL (time To Live) The objects are removed from
- * cache when TTL is reached.
- * 
- * @author roberto <roberto.sanchez@curisit.net>
- */
+* CacheTTL
+* <p>
+* Simple in-memory cache with per-entry TTL (time-to-live). A background
+* cleaning thread periodically removes expired entries.
+*
+* <p><b>Type-safety note:</b> Besides generic getters, this cache provides
+* {@link #getSet(String, Class)} to safely retrieve {@code Set<E>} values
+* without unchecked warnings at call sites. The method validates that all
+* elements match the requested {@code elementType}.
+*
+* <p><b>Threading:</b> This implementation is lightweight and uses a single
+* cleaner thread. The internal map is not synchronized beyond the remove loop,
+* which is acceptable for low-concurrency scenarios. For heavier usage,
+* consider switching to a {@code ConcurrentHashMap} and/or a scheduled executor.
+*
+* @author roberto
+* Last reviewed by JRA on Oct 5, 2025.
+*/
 @ApplicationScoped
 public class CacheTTL {
 
     private static final Logger LOG = LogManager.getLogger(CacheTTL.class);
 
+    /** Default TTL (seconds) for entries when not specified. */
+    private static final int DEFAULT_CACHE_DURATION = 24 * 60 * 60;
+
+    /** Backing store: key → cached object + expiration. */
+    private final Map<String, CachedObject> data = new HashMap<>();
+
+    /** Background cleaner thread. */
+    private final Thread cleaningThread;
+
     /**
-     * Period before token expires, set in seconds.
-     */
-    private static int DEFAULT_CACHE_DURATION = 24 * 60 * 60;
-
-    private Map<String, CachedObject> data = new HashMap<>();
-
-    private Thread cleaningThread = null;
-
+    * CacheTTL<p>
+    * Construct a cache and start the background cleaner that removes expired
+    * entries every 60 seconds.
+    */
     @Inject
     public CacheTTL() {
-        cleaningThread = new Thread(new Runnable() {
-
-            @Override
-            public void run() {
-                while (CacheTTL.this.data != null) {
-                    try {
-                        // We check for expired object every 60 seconds
-                        Thread.sleep(60 * 1000);
-                    } catch (InterruptedException e) {
-                        LOG.error("Exiting from Cache Thread");
-                        data.clear();
-                        return;
-                    }
-                    Date now = new Date();
-                    List<String> keysToRemove = new ArrayList<>();
-                    for (String key : CacheTTL.this.data.keySet()) {
-                        CachedObject co = CacheTTL.this.data.get(key);
-                        if (now.after(co.getExpireAt())) {
-                            keysToRemove.add(key);
-                        }
-                    }
-                    for (String key : keysToRemove) {
-                        // If we try to remove directly in the previous loop an
-                        // exception is thrown
-                        // java.util.ConcurrentModificationException
-                        CacheTTL.this.data.remove(key);
+        cleaningThread = new Thread(() -> {
+            while (true) {
+                try {
+                    // Check for expired objects every 60 seconds
+                    Thread.sleep(60_000);
+                } catch (InterruptedException e) {
+                    LOG.warn("Cache cleaner interrupted. Clearing cache and stopping.");
+                    data.clear();
+                    return;
+                }
+                Date now = new Date();
+                List<String> keysToRemove = new ArrayList<>();
+                for (String key : data.keySet()) {
+                    CachedObject co = data.get(key);
+                    if (co != null && now.after(co.getExpireAt())) {
+                        keysToRemove.add(key);
                     }
                 }
+                for (String key : keysToRemove) {
+                    // Avoid ConcurrentModificationException by removing after iteration
+                    data.remove(key);
+                }
             }
-        });
+        }, "CacheTTL-Cleaner");
+        cleaningThread.setDaemon(true);
         cleaningThread.start();
     }
 
+    // ---------------------------------------------------------------------
+    // Putters
+    // ---------------------------------------------------------------------
+
     /**
-     * 
-     * @param key
-     * @param obj
-     * @param ttl
-     *            Time To Live in seconds
-     */
+    * set<p>
+    * Store a value with an explicit TTL.
+    *
+    * @param key cache key
+    * @param obj value to store (may be any object, including collections)
+    * @param ttl TTL in seconds
+    */
     public void set(String key, Object obj, int ttl) {
-        Date expirationDate = new Date(new Date().getTime() + ttl * 1000);
+        Date expirationDate = new Date(System.currentTimeMillis() + (long) ttl * 1000L);
         data.put(key, new CachedObject(expirationDate, obj));
     }
 
+    /**
+    * set<p>
+    * Store a value with the default TTL.
+    *
+    * @param key cache key
+    * @param obj value to store
+    */
     public void set(String key, Object obj) {
         set(key, obj, DEFAULT_CACHE_DURATION);
     }
 
+    // ---------------------------------------------------------------------
+    // Getters
+    // ---------------------------------------------------------------------
+
+    /**
+    * get<p>
+    * Retrieve a value as {@code Object}. Returns {@code null} if not present
+    * or expired (expired entries are eagerly removed by the cleaner).
+    *
+    * @param key cache key
+    * @return cached value or null
+    */
     public Object get(String key) {
         CachedObject co = data.get(key);
         return co == null ? null : co.getObject();
     }
 
+    /**
+    * get<p>
+    * Retrieve a value and cast it to the requested type. The cast is unchecked
+    * due to type erasure, but localized within the cache implementation.
+    *
+    * @param key cache key
+    * @param type expected value type
+    * @param <T> generic type
+    * @return cached value typed or null
+    */
     public <T> T get(String key, Class<T> type) {
         CachedObject co = data.get(key);
         return co == null ? null : co.getObject(type);
     }
 
+    /**
+    * getSet<p>
+    * Retrieve a {@code Set<E>} in a type-safe way without unchecked warnings
+    * at the call site. The method validates that the cached value is a
+    * {@code Set} and that <b>all</b> elements are instances of {@code elementType}.
+    * If any element does not match, the method returns {@code null} and logs a warning.
+    *
+    * @param key cache key
+    * @param elementType class of the set elements (e.g., {@code Integer.class})
+    * @return typed set or null if missing/type-mismatch
+    */
+    @SuppressWarnings("unchecked")
+    public <E> Set<E> getSet(String key, Class<E> elementType) {
+        Object obj = get(key);
+        if (obj == null) return null;
+        if (!(obj instanceof Set<?> raw)) {
+            LOG.warn("Cache key '{}' expected Set<{}> but found {}", key, elementType.getSimpleName(), obj.getClass().getName());
+            return null;
+        }
+        // Validate element types to avoid ClassCastException later
+        for (Object el : raw) {
+            if (el != null && !elementType.isInstance(el)) {
+                LOG.warn("Cache key '{}' contains element of type {}, expected {}", key,
+                         el.getClass().getName(), elementType.getName());
+                return null;
+            }
+        }
+        // Safe due to element-wise validation
+        return (Set<E>) raw;
+    }
+
+    // ---------------------------------------------------------------------
+    // Removers & maintenance
+    // ---------------------------------------------------------------------
+
+    /**
+    * remove<p>
+    * Remove and return a value typed.
+    *
+    * @param key cache key
+    * @param type expected type
+    * @return removed value or null
+    */
     public <T> T remove(String key, Class<T> type) {
         CachedObject co = data.remove(key);
         return co == null ? null : co.getObject(type);
     }
 
+    /**
+    * remove<p>
+    * Remove and return a value as {@code Object}.
+    *
+    * @param key cache key
+    * @return removed value or null
+    */
     public Object remove(String key) {
         CachedObject co = data.remove(key);
         return co == null ? null : co.getObject();
     }
 
+    /**
+    * clear<p>
+    * Remove all entries from the cache.
+    */
     public void clear() {
         data.clear();
     }
 
-    private class CachedObject {
-        Date expireAt;
-        Object object;
+    // ---------------------------------------------------------------------
+    // Internal structure
+    // ---------------------------------------------------------------------
 
+    /**
+    * CachedObject
+    * <p>
+    * Internal wrapper that pairs an arbitrary object with its expiration date.
+    */
+    private static class CachedObject {
+        private final Date expireAt;
+        private final Object object;
+
+        /** 
+         * Constructor<p>
+         * Set expiration and payload. 
+         * 
+         * @param date
+         * @param object
+         */
         public CachedObject(Date date, Object obj) {
-            expireAt = date;
-            object = obj;
+            this.expireAt = date;
+            this.object = obj;
         }
 
+        /** 
+         * getExpireAt<p>
+         * Return expiration date. 
+         * 
+         * @return date
+         */
         public Date getExpireAt() {
             return expireAt;
         }
 
+        /** 
+         * getObject<p>
+         * Return payload as {@code Object}. 
+         * 
+         * @return object
+         */
         public Object getObject() {
             return object;
         }
 
+        /**
+        * getObject<p>
+        * Return payload cast to the requested type. Cast is localized here.
+        *
+        * @param type requested type
+        * @param <T>  generic type
+        * @return typed payload
+        */
         @SuppressWarnings("unchecked")
         public <T> T getObject(Class<T> type) {
             return (T) object;
         }
-
     }
-
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/utils/Config.java b/securis/src/main/java/net/curisit/securis/utils/Config.java
index 1d43249..a2e4c35 100644
--- a/securis/src/main/java/net/curisit/securis/utils/Config.java
+++ b/securis/src/main/java/net/curisit/securis/utils/Config.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.utils;
 
 import java.io.IOException;
@@ -11,17 +14,31 @@
 import org.apache.logging.log4j.Logger;
 
 /**
- * Class that loads and serves global config parameters.
+ * Config
+ * <p>
+ * Class that loads and serves global config parameters from a classpath properties file
+ * and, as a fallback, from environment variables.
+ *
+ * Initialization:
+ *  - Static initializer loads {@link #KEY_CONFIG_FILE} from classpath (fails hard if missing).
+ *
+ * Accessors:
+ *  - {@link #get(String)} / {@link #get(String, String)}
+ *  - Integer variants: {@link #getInt(String)} / {@link #getInt(String, int)}
+ *  - Namespaced helpers: by prefix/domain, and sequential lists via {@link #getListByPrefix(String)}.
+ *
+ * Thread-safety: static-only utility; internal state is read-only after init.
  * 
- * @author rsanchez
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 public class Config {
 
     private static final Logger LOG = LogManager.getLogger(Config.class);
 
     /**
-     * Key used to store config file resource location. In a web application,
-     * can be set as initial parameter in a servlet loaded on startup
+     * Resource path of the application properties file (in classpath).
+     * E.g. "/securis-server.properties".
      */
     public static final String KEY_CONFIG_FILE = "/securis-server.properties";
 
@@ -37,12 +54,12 @@
     }
 
     /**
-     * Loads application global parameters from a classpath resource
-     * 
-     * @param resource
-     *            : Resource location in classpath, i.e:
-     *            "/resource/cp-securis.conf"
-     * @throws IOException
+     * loadParameters
+     * <p>
+     * Loads application global parameters from a classpath resource.
+     *
+     * @param resource Classpath location (e.g. "/resource/cp-securis.conf").
+     * @throws IOException If the resource cannot be found or read.
      */
     public static void loadParameters(String resource) throws IOException {
 
@@ -51,7 +68,6 @@
 
         params = new Properties();
         try {
-
             params.load(fileis);
             LOG.debug("Params loaded OK from {}", resource);
         } catch (IOException e) {
@@ -59,84 +75,112 @@
             params = null;
             throw e;
         }
-
     }
 
+    /**
+     * getByDomain
+     * <p>
+     * Convenience accessor for domain-suffixed parameters (param.domain).
+     *
+     * @param domain     Domain suffix.
+     * @param paramname  Base parameter name.
+     * @return String value or null if not present.
+     */
     public static String getByDomain(String domain, String paramname) {
         return getByDomain(domain, paramname, null);
     }
 
+    /**
+     * getByPrefix
+     * <p>
+     * Returns parameter value from "{prefix}.{param}" or falls back to the plain "{param}".
+     *
+     * @param prefix    Namespace prefix.
+     * @param paramname Parameter name.
+     * @return Resolved value or null.
+     */
     public static String getByPrefix(String prefix, String paramname) {
         return get(prefix + "." + paramname, get(paramname));
     }
 
+    /**
+     * getByPrefix
+     * <p>
+     * Returns parameter value from "{prefix}.{param}" or provided default (which itself
+     * falls back to "{param}" or its default).
+     *
+     * @param prefix     Namespace prefix.
+     * @param paramname  Parameter name.
+     * @param defaultVal Default value if none resolved.
+     * @return Resolved value.
+     */
     public static String getByPrefix(String prefix, String paramname, String defaultVal) {
         return get(prefix + "." + paramname, get(paramname, defaultVal));
     }
 
+    /**
+     * getByDomain
+     * <p>
+     * Returns value from "{param}.{domain}" or provided default.
+     *
+     * @param domain     domain suffix.
+     * @param paramname  base name.
+     * @param defaultval fallback if not found.
+     * @return resolved string.
+     */
     public static String getByDomain(String domain, String paramname, String defaultval) {
         return get(paramname + "." + domain, defaultval);
     }
 
+    /**
+     * getIntByDomain
+     * <p>
+     * Integer variant of {@link #getByDomain(String, String)} with fallback to plain param.
+     */
     public static int getIntByDomain(String domain, String paramname) {
         return getInt(paramname + "." + domain, getInt(paramname));
     }
 
+    /**
+     * getIntByDomain
+     * <p>
+     * Integer variant returning provided default when missing.
+     */
     public static int getIntByDomain(String domain, String paramname, int defaultval) {
         return getInt(paramname + "." + domain, defaultval);
     }
 
     /**
-     * Gets a List with all values of properties that begins with
-     * <code>prefix</code> It reads sequentially. For example:
-     * 
-     * <pre>
-     * 	securis.sort.comparator.0: net.cp.securis.comparators.ComparePttidVsPtn
-     * 	securis.sort.comparator.1: net.cp.securis.comparators.CompareFrequency
-     * 	securis.sort.comparator.2: net.cp.securis.comparators.CompareOutgoingVsIncomming
-     * 	securis.sort.comparator.3: net.cp.securis.comparators.CompareDuration 
-     * 	securis.sort.comparator.4: net.cp.securis.comparators.CompareCallVsSms
-     * </pre>
-     * 
-     * That config (for prefix: "securis.sort.comparator" ) will return a
-     * List<String> with values:
-     * 
-     * <pre>
-     * 	"net.cp.securis.comparators.ComparePttidVsPtn", 
-     * 	"net.cp.securis.comparators.CompareFrequency", 
-     * 	"net.cp.securis.comparators.CompareOutgoingVsIncomming", 
-     * 	"net.cp.securis.comparators.CompareDuration", 
-     * 	"net.cp.securis.comparators.CompareCallVsSms"
-     * </pre>
-     * 
-     * Note: If there is a gap between suffixes process will stop, that is, only
-     * will be returned properties found before gap.
-     * 
-     * @param prefix
-     * @return
+     * getListByPrefix
+     * <p>
+     * Reads sequential properties using numeric suffixes starting from 0 and stops on first gap.
+     * Example:
+     *  securis.sort.comparator.0=...
+     *  securis.sort.comparator.1=...
+     *  ...
+     *
+     * @param prefix Base prefix (e.g. "securis.sort.comparator").
+     * @return Ordered list of values until first missing index.
      */
     public static List<String> getListByPrefix(String prefix) {
         List<String> list = new ArrayList<String>();
-
         String tpl = prefix + ".{0}";
-
         int i = 0;
         String value = get(MessageFormat.format(tpl, i++));
         while (value != null) {
             list.add(value);
             value = get(MessageFormat.format(tpl, i++));
         }
-
         return list;
     }
 
     /**
-     * Gets param value in config file or environment variables
-     * 
-     * @param paramname
-     *            Global parameter's name
-     * @return Value of paramname or null if paramname is not found neither in
-     *         config file nor in environment variables
+     * get
+     * <p>
+     * Get a parameter value from the loaded properties or environment variables.
+     *
+     * @param paramname Parameter key.
+     * @return Value or null if not found anywhere.
      */
     public static String get(String paramname) {
 
@@ -149,12 +193,13 @@
     }
 
     /**
-     * Gets param value from config file or environment variables
-     * 
-     * @param paramname
-     *            Global parameter's name
-     * @param defaultval
-     * @return Value of paramname or defaultval if paramname is not found
+     * get
+     * <p>
+     * Returns parameter value or default if missing.
+     *
+     * @param paramname Key.
+     * @param defaultval Default fallback.
+     * @return value or default.
      */
     public static String get(String paramname, String defaultval) {
         String value = get(paramname);
@@ -162,12 +207,12 @@
     }
 
     /**
-     * Gets param value in config file or environment variables
-     * 
-     * @param paramname
-     *            Global parameter's name
-     * @return Integer value of paramname or -1 if paramname is not found
-     *         neither in config file nor in environment variables
+     * getInt
+     * <p>
+     * Integer accessor, returns -1 if missing.
+     *
+     * @param paramname Key.
+     * @return Parsed integer or -1.
      */
     public static int getInt(String paramname) {
         String value = get(paramname);
@@ -175,19 +220,24 @@
     }
 
     /**
-     * Gets param value from config file or environment variables
-     * 
-     * @param paramname
-     *            Global parameter's name
-     * @param defaultval
-     * @return Integer value of paramname or defaultval if paramname is not
-     *         found
+     * getInt
+     * <p>
+     * Integer accessor, returns provided default when missing.
+     *
+     * @param paramname Key.
+     * @param defaultval Default fallback.
+     * @return Parsed integer or default.
      */
     public static int getInt(String paramname, int defaultval) {
         String value = get(paramname);
         return (value == null ? defaultval : Integer.parseInt(value));
     }
 
+    /**
+     * KEYS
+     * <p>
+     * Strongly-typed keys used across the application.
+     */
     public static class KEYS {
 
         public static final String SERVER_HOSTNAME = "license.server.hostname";
@@ -205,5 +255,5 @@
         public static final String EMAIL_FROM_ADDRESS = "email.from.address";
         public static final String EMAIL_LIC_DEFAULT_SUBJECT = "email.lic.default.subject";
     }
-
 }
+
diff --git a/securis/src/main/java/net/curisit/securis/utils/EmailManager.java b/securis/src/main/java/net/curisit/securis/utils/EmailManager.java
index 5b3ad1f..4d427f5 100644
--- a/securis/src/main/java/net/curisit/securis/utils/EmailManager.java
+++ b/securis/src/main/java/net/curisit/securis/utils/EmailManager.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.utils;
 
 import java.io.File;
@@ -40,22 +43,61 @@
 import org.apache.logging.log4j.Logger;
 
 /**
- * Component that send emails using Mailgun API:
- * http://documentation.mailgun.com/user_manual.html#sending-messages
- * 
- * @author roberto <roberto.sanchez@curisit.net>
+ * EmailManager
+ * <p>
+ * Small utility to send plain-text emails (optionally with one attachment)
+ * using the <b>Mailgun</b> API over HTTPS.
+ * <p>
+ * Design notes:
+ * <ul>
+ *   <li>Reads Mailgun credentials and the "from" address from {@link Config}.</li>
+ *   <li>Builds a preconfigured {@link HttpClientBuilder} with basic auth and a permissive SSL socket factory.</li>
+ *   <li>Exposes synchronous (blocking) and asynchronous (non-blocking) send methods.</li>
+ *   <li>Scope is {@code @ApplicationScoped}; the underlying builder is created once per container.</li>
+ * </ul>
+ *
+ * Thread-safety:
+ * <p>
+ * The class is effectively stateless after construction; using a shared {@link HttpClientBuilder}
+ * is safe as a new {@link HttpClient} is built per request.
+ *
+ * Configuration keys (see {@link Config.KEYS}):
+ * <ul>
+ *   <li>{@code mailgun.domain}</li>
+ *   <li>{@code mailgun.api.key}</li>
+ *   <li>{@code email.from.address}</li>
+ * </ul>
+ *
+ * Failure handling:
+ * <p>
+ * Network and HTTP errors are surfaced as {@link SeCurisServiceException} with appropriate error codes.
+ *
+ * @author roberto &lt;roberto.sanchez@curisit.net&gt;
+ * Last reviewed by JRA on Oct 6, 2025.
  */
 @ApplicationScoped
 public class EmailManager {
 
+    /** Class logger. */
     private static final Logger LOG = LogManager.getLogger(EmailManager.class);
 
+    /** Mailgun endpoint composed from configured domain. */
     private final String serverUrl;
+
+    /** Preconfigured builder that carries SSL and basic-auth configuration. */
     private final HttpClientBuilder httpClientBuilder;
 
+    // ---------------------------------------------------------------------
+    // Constructors
+    // ---------------------------------------------------------------------
+
     /**
-     * 
-     * @throws SeCurisException
+     * EmailManager
+     * <p>
+     * Default constructor that validates required configuration and prepares an
+     * HTTP client builder with Mailgun credentials and SSL settings.
+     *
+     * @throws SeCurisException if mandatory configuration is missing or the SSL builder cannot be created
      */
     public EmailManager() throws SeCurisException {
         String domain = Config.get(Config.KEYS.MAILGUN_DOMAIN);
@@ -64,13 +106,29 @@
         }
         serverUrl = String.format("https://api.mailgun.net/v2/%s/messages", domain);
         httpClientBuilder = createHttpClient();
-
     }
 
+    // ---------------------------------------------------------------------
+    // Internal helpers
+    // ---------------------------------------------------------------------
+
+    /**
+     * createHttpClient
+     * <p>
+     * Builds a {@link HttpClientBuilder} that:
+     * <ul>
+     *   <li>Accepts any server certificate (permissive trust strategy).</li>
+     *   <li>Applies HTTP Basic Auth using Mailgun's API key as the password and user "api".</li>
+     * </ul>
+     *
+     * @return a preconfigured {@link HttpClientBuilder} ready to build clients
+     * @throws SeCurisException if SSL initialization fails
+     */
     private HttpClientBuilder createHttpClient() throws SeCurisException {
         SSLContextBuilder builder = new SSLContextBuilder();
         SSLConnectionSocketFactory sslsf = null;
         try {
+            // Trust all X509 certificates (relies on HTTPS + Basic Auth; consider hardening in production).
             builder.loadTrustMaterial((KeyStore) null, new TrustStrategy() {
                 @Override
                 public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
@@ -82,30 +140,45 @@
             LOG.error(e1);
             throw new SeCurisException("Error creating SSL socket factory");
         }
+
+        // Configure Basic Auth with Mailgun API key
         CredentialsProvider provider = new BasicCredentialsProvider();
-        UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("api", Config.get(Config.KEYS.MAILGUN_API_KEY));
+        UsernamePasswordCredentials credentials =
+            new UsernamePasswordCredentials("api", Config.get(Config.KEYS.MAILGUN_API_KEY));
         provider.setCredentials(AuthScope.ANY, credentials);
 
-        return HttpClientBuilder.create().setDefaultCredentialsProvider(provider).setSSLSocketFactory(sslsf);
+        return HttpClientBuilder.create()
+                .setDefaultCredentialsProvider(provider)
+                .setSSLSocketFactory(sslsf);
     }
 
+    // ---------------------------------------------------------------------
+    // Email sending API
+    // ---------------------------------------------------------------------
+
     /**
-     * Basic method to send emails in text mode with attachment. The method is
-     * synchronous, It waits until server responses.
-     * 
-     * @param subject
-     * @param body
-     * @param to
-     * @param file
-     * @throws SeCurisException
-     * @throws UnsupportedEncodingException
+     * sendEmail
+     * <p>
+     * Sends a plain-text email (UTF-8) via Mailgun. Optionally attaches a single file.
+     * This call is <b>synchronous</b> (blocking) and only returns once the HTTP response is received.
+     *
+     * @param subject Email subject (will be sent as UTF-8).
+     * @param body    Email body in plain text (UTF-8).
+     * @param to      Recipient address (required).
+     * @param cc      Optional CC address, may be {@code null}.
+     * @param file    Optional file to attach, may be {@code null}.
+     *
+     * @throws SeCurisServiceException   if the HTTP call fails or Mailgun responds with a non-200 status
+     * @throws UnsupportedEncodingException kept for API compatibility (body/subject are forced to UTF-8)
      */
     @SuppressWarnings("deprecation")
-	public void sendEmail(String subject, String body, String to, String cc, File file) throws SeCurisServiceException, UnsupportedEncodingException {
+    public void sendEmail(String subject, String body, String to, String cc, File file)
+            throws SeCurisServiceException, UnsupportedEncodingException {
+
         HttpPost postRequest = new HttpPost(serverUrl);
 
+        // Build multipart form body compatible with Mailgun
         MultipartEntityBuilder builder = MultipartEntityBuilder.create();
-
         builder.setCharset(Charset.forName("utf-8"));
         builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
         builder.addTextBody("from", Config.get(Config.KEYS.EMAIL_FROM_ADDRESS));
@@ -113,28 +186,34 @@
         if (cc != null) {
             builder.addTextBody("cc", cc);
         }
-        builder.addTextBody("subject", subject, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
-        builder.addTextBody("text", body, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
-        if (file != null) {
+        builder.addTextBody("subject",
+                subject, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
+        builder.addTextBody("text",
+                body, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
 
+        if (file != null) {
             LOG.info("File to attach: {}", file.getAbsoluteFile());
             builder.addPart("attachment", new FileBody(file));
         }
 
         postRequest.setEntity(builder.build());
+
+        // Execute HTTP request
         HttpResponse response;
         HttpClient httpClient = httpClientBuilder.build();
         try {
             response = httpClient.execute(postRequest);
 
+            // Mailgun returns JSON. We parse it to a Map for logging/validation.
             String jsonLic = IOUtils.toString(response.getEntity().getContent());
             if (response.getStatusLine().getStatusCode() == 200) {
                 LOG.debug("Response content read OK: {}", jsonLic);
                 Map<String, Object> responseBean = JsonUtils.json2map(jsonLic);
-
                 LOG.debug("Response mail read OK: {}", responseBean);
             } else {
-                throw new SeCurisServiceException(ErrorCodes.UNEXPECTED_ERROR, "Error sending email, response estatus: " + response.getStatusLine());
+                throw new SeCurisServiceException(
+                        ErrorCodes.UNEXPECTED_ERROR,
+                        "Error sending email, response estatus: " + response.getStatusLine());
             }
         } catch (IOException e) {
             LOG.error("Error sending email", e);
@@ -143,21 +222,27 @@
     }
 
     /**
-     * Basic method to send emails in text mode with attachment. The method is
-     * asynchronous, It returns immediately
-     * 
-     * @param subject
-     * @param body
-     * @param to
-     * @param file
-     * @throws SeCurisException
-     * @throws UnsupportedEncodingException
+     * sendEmailAsync
+     * <p>
+     * Asynchronous variant of {@link #sendEmail(String, String, String, String, File)}.
+     * The call returns immediately and performs the HTTP request in a single-thread executor.
+     *
+     * @param subject  Email subject (UTF-8).
+     * @param body     Email body in plain text (UTF-8).
+     * @param to       Recipient address.
+     * @param cc       Optional CC address, may be {@code null}.
+     * @param file     Optional attachment, may be {@code null}.
+     * @param callback Non-null callback to be notified on success or failure.
+     *
+     * @throws SeCurisException              if there is a configuration or environment problem before dispatch
+     * @throws UnsupportedEncodingException  for API compatibility (subject/body are encoded as UTF-8)
      */
-    public void sendEmailAsync(String subject, String body, String to, String cc, File file, EmailCallback callback) throws SeCurisException,
-            UnsupportedEncodingException {
+    public void sendEmailAsync(
+            String subject, String body, String to, String cc, File file, EmailCallback callback)
+            throws SeCurisException, UnsupportedEncodingException {
+
         Executor ex = Executors.newSingleThreadExecutor();
         ex.execute(new Runnable() {
-
             @Override
             public void run() {
                 try {
@@ -168,37 +253,61 @@
                 } catch (SeCurisServiceException e) {
                     callback.error(e);
                 }
-
             }
         });
-
     }
 
+    // ---------------------------------------------------------------------
+    // Callback contract
+    // ---------------------------------------------------------------------
+
+    /**
+     * EmailCallback
+     * <p>
+     * Functional contract to be notified when an async send finishes.
+     */
     public static interface EmailCallback {
+        /**
+         * success<p>
+         * Called when the email was sent and Mailgun returned HTTP 200.
+         */
         public void success();
 
+        /**
+         * error<p>
+         * Called when there was a problem sending the email.
+         *
+         * @param e encapsulates the reason of failure
+         */
         public void error(SeCurisServiceException e);
     }
 
+    // ---------------------------------------------------------------------
+    // Manual test harness
+    // ---------------------------------------------------------------------
+
+    /**
+     * main<p>
+     * Simple manual test for the async email flow. Adjust addresses and file path before use.
+     *
+     * @param args program arguments (unused)
+     * @throws SeCurisException             if configuration is invalid
+     * @throws UnsupportedEncodingException if UTF-8 encoding fails (unlikely)
+     */
     public static void main(String[] args) throws SeCurisException, UnsupportedEncodingException {
-        // new EmailManager().sendEmail("España así de bien",
-        // "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response",
-        // "info@r75.es", new File(
-        // "/Users/rob/Downloads/test.req"));
-        new EmailManager().sendEmailAsync("España así de bien", "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response", "info@r75.es",
-                "dev@r75.es", new File("/Users/rob/Downloads/test.req"), new EmailCallback() {
+        // Example async call (subject/body contain non-ASCII content to validate UTF-8 handling).
+        new EmailManager().sendEmailAsync("España así de bien",
+                "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response",
+                "info@r75.es",
+                "dev@r75.es",
+                new File("/Users/rob/Downloads/test.req"),
+                new EmailCallback() {
+                    @Override
+                    public void success() { LOG.info("Success!!!"); }
 
                     @Override
-                    public void success() {
-                        LOG.info("Success!!!");
-                    }
-
-                    @Override
-                    public void error(SeCurisServiceException e) {
-                        LOG.error("Error: {} !!!", e);
-                    }
+                    public void error(SeCurisServiceException e) { LOG.error("Error: {} !!!", e); }
                 });
         LOG.info("Waiting for email to be sent...");
     }
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java b/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java
index 82ba7f6..e252ebb 100644
--- a/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java
+++ b/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.utils;
 
 import java.io.IOException;
@@ -10,38 +13,92 @@
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponseWrapper;
 
+/**
+ * GZipServletResponseWrapper
+ * <p>
+ * {@link HttpServletResponseWrapper} that transparently compresses the response
+ * body using GZIP. Intended for use in filters/servlets where the caller wants
+ * to wrap the original response and write compressed bytes to the client.
+ * <p>
+ * How it works:
+ * <ul>
+ *   <li>Overrides {@link #getOutputStream()} and {@link #getWriter()} to route output through a {@link GZIPOutputStream}.</li>
+ *   <li>Ensures mutual exclusivity between OutputStream and Writer access per Servlet API requirements.</li>
+ *   <li>Ignores {@link #setContentLength(int)} since compressed size differs from uncompressed.</li>
+ * </ul>
+ *
+ * Usage:
+ * <pre>
+ *   GZipServletResponseWrapper gz = new GZipServletResponseWrapper(resp);
+ *   chain.doFilter(request, gz);
+ *   gz.close(); // important: finish compression and flush buffers
+ * </pre>
+ *
+ * Thread-safety:
+ * <p>
+ * Instances are per-request and not shared.
+ *
+ * Limitations:
+ * <p>
+ * Caller is responsible for setting "Content-Encoding: gzip" and for avoiding
+ * double-compression scenarios.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 6, 2025.
+ */
 public class GZipServletResponseWrapper extends HttpServletResponseWrapper {
 
 	private GZIPServletOutputStream gzipOutputStream = null;
 	private PrintWriter printWriter = null;
 
+	// ---------------------------------------------------------------------
+	// Constructors
+	// ---------------------------------------------------------------------
+
+	/**
+	 * GZipServletResponseWrapper
+	 * <p>
+	 * Wraps the given response. Actual GZIP streams are lazily created on first write.
+	 *
+	 * @param response the original {@link HttpServletResponse} to wrap
+	 * @throws IOException if the underlying response streams cannot be accessed
+	 */
 	public GZipServletResponseWrapper(HttpServletResponse response) throws IOException {
 		super(response);
 	}
 
-	public void close() throws IOException {
+	// ---------------------------------------------------------------------
+	// Lifecycle
+	// ---------------------------------------------------------------------
 
-		//PrintWriter.close does not throw exceptions.
-		//Hence no try-catch block.
+	/**
+	 * close<p>
+	 * Closes any open writer or output stream and finalizes the GZIP stream.
+	 * Must be called once all response content has been written.
+	 *
+	 * @throws IOException if closing the underlying streams fails
+	 */
+	public void close() throws IOException {
+		// PrintWriter.close does not throw exceptions. Hence no try-catch block.
 		if (this.printWriter != null) {
 			this.printWriter.close();
 		}
-
 		if (this.gzipOutputStream != null) {
 			this.gzipOutputStream.close();
 		}
 	}
 
 	/**
-	 * Flush OutputStream or PrintWriter
+	 * flushBuffer<p>
+	 * Flushes the writer and the GZIP output stream, then delegates to the wrapped response.
+	 * If multiple exceptions occur, the first encountered is thrown (typical servlet practice).
 	 *
-	 * @throws IOException
+	 * @throws IOException if flushing any of the streams fails
 	 */
-
 	@Override
 	public void flushBuffer() throws IOException {
 
-		//PrintWriter.flush() does not throw exception
+		// PrintWriter.flush() does not throw exception
 		if (this.printWriter != null) {
 			this.printWriter.flush();
 		}
@@ -62,12 +119,23 @@
 			exception2 = e;
 		}
 
-		if (exception1 != null)
-			throw exception1;
-		if (exception2 != null)
-			throw exception2;
+		if (exception1 != null) throw exception1;
+		if (exception2 != null) throw exception2;
 	}
 
+	// ---------------------------------------------------------------------
+	// Output acquisition
+	// ---------------------------------------------------------------------
+
+	/**
+	 * getOutputStream<p>
+	 * Returns a {@link ServletOutputStream} that writes compressed data.
+	 * Mutually exclusive with {@link #getWriter()} as per Servlet API.
+	 *
+	 * @return compressed {@link ServletOutputStream}
+	 * @throws IOException if the underlying output stream cannot be obtained
+	 * @throws IllegalStateException if the writer has been already acquired
+	 */
 	@Override
 	public ServletOutputStream getOutputStream() throws IOException {
 		if (this.printWriter != null) {
@@ -79,6 +147,15 @@
 		return this.gzipOutputStream;
 	}
 
+	/**
+	 * getWriter<p>
+	 * Returns a {@link PrintWriter} that writes compressed data (UTF-8 by default, inherited from response).
+	 * Mutually exclusive with {@link #getOutputStream()} as per Servlet API.
+	 *
+	 * @return compressed {@link PrintWriter}
+	 * @throws IOException if streams cannot be allocated
+	 * @throws IllegalStateException if the output stream has been already acquired
+	 */
 	@Override
 	public PrintWriter getWriter() throws IOException {
 		if (this.printWriter == null && this.gzipOutputStream != null) {
@@ -91,50 +168,118 @@
 		return this.printWriter;
 	}
 
+	/**
+	 * setContentLength<p>
+	 * No-op. The content length of the zipped content is not known a priori and
+	 * will not match the uncompressed length; therefore we do not set it here.
+	 *
+	 * @param len ignored
+	 */
 	@Override
 	public void setContentLength(int len) {
-		//ignore, since content length of zipped content
-		//does not match content length of unzipped content.
+		// ignore, since content length of zipped content does not match content length of unzipped content.
 	}
 
+	// ---------------------------------------------------------------------
+	// Inner compressed stream
+	// ---------------------------------------------------------------------
+
+	/**
+	 * GZIPServletOutputStream
+	 * <p>
+	 * Decorates the original {@link ServletOutputStream} with a {@link GZIPOutputStream}.
+	 * Delegates readiness and listener to the underlying (container) stream.
+	 * 
+	 * @author JRA
+     * Last reviewed by JRA on Oct 5, 2025.
+	 */
 	private static class GZIPServletOutputStream extends ServletOutputStream {
 		private final ServletOutputStream servletOutputStream;
 		private final GZIPOutputStream gzipStream;
 
+		/**
+		 * GZIPServletOutputStream<p>
+		 * Creates a new compressed stream wrapper.
+		 *
+		 * @param servletOutputStream underlying (container-provided) output stream
+		 * @throws IOException if the GZIP stream cannot be created
+		 */
 		public GZIPServletOutputStream(ServletOutputStream servletOutputStream) throws IOException {
 			this.servletOutputStream = servletOutputStream;
 			this.gzipStream = new GZIPOutputStream(servletOutputStream);
 		}
 
+		/** 
+		 * isReady<p>
+		 * Check if the output stream is ready
+		 * {@inheritDoc} 
+		 * 
+		 * @return isReady
+		 */
 		@Override
 		public boolean isReady() {
 			return this.servletOutputStream.isReady();
 		}
 
+		/** 
+		 * setWriteListener<p>
+		 * Set the write listener for the output stream
+		 * {@inheritDoc}
+		 * 
+		 * @param writeListener
+		 */
 		@Override
 		public void setWriteListener(WriteListener writeListener) {
 			this.servletOutputStream.setWriteListener(writeListener);
 		}
 
+		/** 
+		 * write<p>
+		 * Write on the gzip stream
+		 * {@inheritDoc} 
+		 * 
+		 * @param b
+		 * @throws IOException
+		 */
 		@Override
 		public void write(int b) throws IOException {
 			this.gzipStream.write(b);
 		}
 
+		/** 
+		 * close<p>
+		 * Close the gzip stream
+		 * {@inheritDoc} 
+		 * 
+		 * @throws IOException
+		 */
 		@Override
 		public void close() throws IOException {
 			this.gzipStream.close();
 		}
 
+		/**
+		 * flush<p>
+		 * Flush the gzip stream
+		 * {@inheritDoc} 
+		 * 
+		 * @throws IOException
+		 */
 		@Override
 		public void flush() throws IOException {
 			this.gzipStream.flush();
 		}
 
+		/**
+		 * finish<p>
+		 * Explicitly finishes writing of the GZIP stream, without closing the underlying stream.
+		 * Not used by the wrapper but available for completeness.
+		 *
+		 * @throws IOException if finishing fails
+		 */
 		@SuppressWarnings("unused")
 		public void finish() throws IOException {
 			this.gzipStream.finish();
 		}
 	}
-
 }
diff --git a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
index ff567f7..74455a8 100644
--- a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
+++ b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.utils;
 
 import java.io.IOException;
@@ -20,37 +23,108 @@
 import java.util.Base64;
 import java.nio.charset.StandardCharsets;
 
+/**
+ * TokenHelper
+ * <p>
+ * Utility component to generate and validate short-lived authentication tokens
+ * for SeCuris services. Tokens are:
+ * </p>
+ * <ul>
+ *   <li>Base64-encoded UTF-8 strings.</li>
+ *   <li>Composed as: {@code <secret> <username> <iso8601-timestamp>} (space-separated).</li>
+ *   <li>Where {@code secret} is a deterministic SHA-256 HMAC-like hash built from a static seed,
+ *       the username, and the issuance timestamp.</li>
+ * </ul>
+ *
+ * <p><b>Lifecycle & scope:</b> {@code @ApplicationScoped}. Stateless and thread-safe.</p>
+ *
+ * <p><b>Security notes:</b>
+ *   The {@code seed} acts like a shared secret. Keep it private and rotate if compromised.
+ *   Tokens expire after {@link #VALID_TOKEN_PERIOD} hours except a special client token
+ *   defined by {@link ApiResource#API_CLIENT_USERNAME} issued at epoch-1 (see {@link #isTokenValid(String)}).
+ * </p>
+ *
+ * <p><b>Format details:</b>
+ *   <pre>
+ *   token = Base64( secret + " " + user + " " + Utils.toIsoFormat(date) )
+ *   secret = hex(SHA-256(seed || user || isoDate))
+ *   </pre>
+ * </p>
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 6, 2025.
+ */
 @ApplicationScoped
 public class TokenHelper {
 
     private static final Logger LOG = LogManager.getLogger(TokenHelper.class);
 
     /**
-     * Period before token expires, set in hours.
+     * Validity window for standard tokens, in hours.
+     * <p>
+     * Any token with a creation date older than this window will be rejected
+     * (unless it matches the special API client rule documented in
+     * {@link #isTokenValid(String)}).
+     * </p>
      */
     private static int VALID_TOKEN_PERIOD = 24;
+
+    /** Standard HTTP header used by SeCuris clients to carry the token. */
     public static final String TOKEN_HEADER_PÀRAM = "X-SECURIS-TOKEN";
 
+    /**
+     * TokenHelper<p>
+     * CDI no-arg constructor.
+     * <p>
+     * Kept for dependency injection. No initialization logic is required.
+     * </p>
+     */
     @Inject
     public TokenHelper() {
     }
 
+    /**
+     * Static secret seed used to derive the token {@code secret} portion.
+     * <p>
+     * Treat this as confidential. Changing it invalidates all outstanding tokens.
+     * </p>
+     */
     private static byte[] seed = "S3Cur15S33dForT0k3nG3n3r@tion".getBytes();
 
+    // ---------------------------------------------------------------------
+    // Token generation
+    // ---------------------------------------------------------------------
+
     /**
-     * Generate a token encoded in Base64 for user passed as parameter and
-     * taking the current moment as token timestamp
-     * 
-     * @param user
-     * @return
+     * generateToken
+     * <p>
+     * Convenience overload that generates a token for {@code user} using the current
+     * system time as the issuance timestamp.
+     * </p>
+     *
+     * @param user Username to embed in the token (must be non-null/non-empty).
+     * @return Base64-encoded token string, or {@code null} if a cryptographic error occurs.
      */
     public String generateToken(String user) {
-
         return generateToken(user, new Date());
     }
 
-    ;
-
+    /**
+     * generateToken
+     * <p>
+     * Builds a token for a given user and issuance date. The token body is:
+     * </p>
+     * <pre>
+     * secret + " " + user + " " + Utils.toIsoFormat(date)
+     * </pre>
+     * <p>
+     * and then Base64-encoded in UTF-8.
+     * </p>
+     *
+     * @param user Username to embed.
+     * @param date Issuance date to include in the token (affects expiry and secret derivation).
+     * @return Base64 token, or {@code null} upon failure.
+     */
     public String generateToken(String user, Date date) {
         try {
             String secret = generateSecret(user, date);
@@ -61,7 +135,7 @@
             sb.append(' ');
             sb.append(Utils.toIsoFormat(date));
 
-            // Codificación estándar con UTF-8
+            // Standard UTF-8 encoding before Base64
             return Base64.getEncoder().encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8));
 
         } catch (NoSuchAlgorithmException e) {
@@ -72,42 +146,86 @@
         return null;
     }
 
+    /**
+     * generateSecret
+     * <p>
+     * Derives the deterministic secret (a 64-hex-character SHA-256 digest) used to
+     * authenticate a token. Inputs are concatenated in the following order:
+     * </p>
+     * <ol>
+     *   <li>{@link #seed}</li>
+     *   <li>{@code user} (UTF-8 bytes)</li>
+     *   <li>{@code Utils.toIsoFormat(date)}</li>
+     * </ol>
+     *
+     * @param user Username to mix in the digest.
+     * @param date Token issuance date to mix in the digest.
+     * @return 64-char hex string.
+     * @throws UnsupportedEncodingException If UTF-8 is unavailable (unexpected).
+     * @throws NoSuchAlgorithmException     If SHA-256 is unavailable (unexpected).
+     */
     private String generateSecret(String user, Date date) throws UnsupportedEncodingException, NoSuchAlgorithmException {
         MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
         mDigest.update(seed, 0, seed.length);
+
         byte[] userbytes = user.getBytes("utf-8");
         mDigest.update(userbytes, 0, userbytes.length);
+
         byte[] isodate = Utils.toIsoFormat(date).getBytes();
         mDigest.update(isodate, 0, isodate.length);
+
         BigInteger i = new BigInteger(1, mDigest.digest());
         String secret = String.format("%1$064x", i);
         return secret;
     }
 
+    // ---------------------------------------------------------------------
+    // Token validation & parsing
+    // ---------------------------------------------------------------------
+
     /**
-     * Check if passed token is still valid, It use to check if token is expired
-     * the attribute VALID_TOKEN_PERIOD (in hours)
-     * 
-     * @param token
-     * @return
+     * isTokenValid
+     * <p>
+     * Validates the structure, signature and expiry of the given token.
+     * Steps performed:
+     * </p>
+     * <ol>
+     *   <li>Base64-decode the token into {@code "secret user isoDate"}.</li>
+     *   <li>Parse {@code user} and {@code isoDate}; recompute the expected secret via
+     *       {@link #generateSecret(String, Date)} and compare with the provided one.</li>
+     *   <li>Check expiry: if the token's timestamp is older than
+     *       {@link #VALID_TOKEN_PERIOD} hours, it's invalid.</li>
+     *   <li>Special-case: if {@code user} equals {@link ApiResource#API_CLIENT_USERNAME}
+     *       and the date returns a non-positive epoch time (e.g., created with {@code new Date(-1)}),
+     *       the expiry check is skipped (client integration token).</li>
+     * </ol>
+     *
+     * @param token Base64 token string.
+     * @return {@code true} if valid and not expired; {@code false} otherwise.
      */
     public boolean isTokenValid(String token) {
         try {
-        	String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
+            String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
             String[] parts = StringUtils.split(tokenDecoded, ' ');
             if (parts == null || parts.length < 3) {
                 return false;
             }
+
             String secret = parts[0];
             String user = parts[1];
             Date date = Utils.toDateFromIso(parts[2]);
+
+            // Expiry check (unless special client token rule applies)
             if (date.getTime() > 0 || !user.equals(ApiResource.API_CLIENT_USERNAME)) {
                 if (new Date().after(new Date(date.getTime() + VALID_TOKEN_PERIOD * 60 * 60 * 1000))) {
                     return false;
                 }
-            } // else: It's a securis-client API call
+            }
+
+            // Signature check
             String newSecret = generateSecret(user, date);
             return newSecret.equals(secret);
+
         } catch (IOException e) {
             LOG.error("Error decoding Base64 token", e);
         } catch (NoSuchAlgorithmException e) {
@@ -116,6 +234,16 @@
         return false;
     }
 
+    /**
+     * extractUserFromToken
+     * <p>
+     * Extracts the username portion from a validly structured token.
+     * </p>
+     *
+     * @param token Base64 token string (may be {@code null}).
+     * @return Username if the token has at least three space-separated fields after decoding;
+     *         {@code null} on error or malformed input.
+     */
     public String extractUserFromToken(String token) {
         try {
             if (token == null) {
@@ -134,6 +262,16 @@
         return null;
     }
 
+    /**
+     * extractDateCreationFromToken
+     * <p>
+     * Parses and returns the issuance {@link Date} embedded in the token, without
+     * performing validation or expiry checks.
+     * </p>
+     *
+     * @param token Base64 token string.
+     * @return Issuance {@link Date}, or {@code null} if the token is malformed or cannot be decoded.
+     */
     public Date extractDateCreationFromToken(String token) {
         try {
             String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
@@ -149,6 +287,20 @@
         return null;
     }
 
+    // ---------------------------------------------------------------------
+    // Demo / manual test
+    // ---------------------------------------------------------------------
+
+    /**
+     * main
+     * <p>
+     * Simple manual test demonstrating generation and validation of a special
+     * "_client" token that bypasses expiry via a negative epoch date.
+     * </p>
+     *
+     * @param args CLI args (unused).
+     * @throws IOException If something goes wrong while encoding/decoding Base64 (unlikely).
+     */
     public static void main(String[] args) throws IOException {
         // client token:
         // OTk3ODRiMzY5NzQ5MWI5NmYyZGQyODRiYjY2ZTU2YzdmMTZjYzM3YTY3N2ExM2M3ODI2MjU5ZTMzOTIyYjUzNSBfY2xpZW50IDE5NzAtMDEtMDFUMDA6NTk6NTkuOTk5KzAxMDA=
@@ -160,3 +312,4 @@
         System.out.println("is valid client token: " + new TokenHelper().isTokenValid(t));
     }
 }
+
diff --git a/securis/src/main/resources/META-INF/persistence.xml b/securis/src/main/resources/META-INF/persistence.xml
index 638909f..62abb5f 100644
--- a/securis/src/main/resources/META-INF/persistence.xml
+++ b/securis/src/main/resources/META-INF/persistence.xml
@@ -19,4 +19,4 @@
 		</properties>
 
 	</persistence-unit>
-</persistence>
+</persistence>
\ No newline at end of file

--
Gitblit v1.3.2