Joaquín Reñé
2026-03-27 4ee50e257b32f6ec0f72907305d1f2b1212808a4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
/*
 * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
 */
package net.curisit.securis.services;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Date;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import net.curisit.integrity.commons.Utils;
import net.curisit.securis.DefaultExceptionHandler;
import net.curisit.securis.LicenseGenerator;
import net.curisit.securis.SeCurisException;
import net.curisit.securis.beans.LicenseBean;
import net.curisit.securis.beans.RequestBean;
import net.curisit.securis.beans.SignedLicenseBean;
import net.curisit.securis.db.Application;
import net.curisit.securis.db.BlockedRequest;
import net.curisit.securis.db.License;
import net.curisit.securis.db.LicenseHistory;
import net.curisit.securis.db.LicenseStatus;
import net.curisit.securis.db.Pack;
import net.curisit.securis.db.PackStatus;
import net.curisit.securis.db.User;
import net.curisit.securis.db.User.Rol;
import net.curisit.securis.ioc.EnsureTransaction;
import net.curisit.securis.security.BasicSecurityContext;
import net.curisit.securis.security.Securable;
import net.curisit.securis.services.exception.SeCurisServiceException;
import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes;
import net.curisit.securis.services.helpers.LicenseHelper;
import net.curisit.securis.services.helpers.UserHelper;
import net.curisit.securis.utils.Config;
import net.curisit.securis.utils.EmailManager;
import net.curisit.securis.utils.JsonUtils;
import net.curisit.securis.utils.LicUtils;
/**
 * 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 UserHelper userHelper;
   @Inject private LicenseHelper licenseHelper;
   @Inject private LicenseGenerator licenseGenerator;
   @Context EntityManager em;
   /**
    * 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("/")
   @Securable
   @Produces({ MediaType.APPLICATION_JSON })
   public Response index(@QueryParam("packId") Integer packId, @Context BasicSecurityContext bsc) {
       LOG.info("Getting licenses list ");
       em.clear();
       if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
           Pack pack = em.find(Pack.class, packId);
           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();
           }
       }
       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();
   }
   /**
    * 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}")
   @Securable
   @Produces({ MediaType.APPLICATION_JSON })
   public Response get(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
       LOG.info("Getting organization data for id: {}: ", licId);
       em.clear();
       License lic = getCurrentLicense(licId, bsc, em);
       return Response.ok(lic).build();
   }
   /**
    * 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")
   @Securable
   @Produces({ MediaType.APPLICATION_OCTET_STREAM })
   @EnsureTransaction
   public Response download(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
       License lic = getCurrentLicense(licId, bsc, em);
       if (lic.getLicenseData() == null) {
           LOG.error("License with id {} has not license file generated", licId, bsc.getUserPrincipal());
           throw new SeCurisServiceException(Status.FORBIDDEN.getStatusCode(), "License has not contain data to generate license file");
       }
       if (!License.Status.isActionValid(License.Action.DOWNLOAD, lic.getStatus())) {
           LOG.error("License with id {} is not active, so It can not downloaded", licId, bsc.getUserPrincipal());
           throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License is not active, so It can not be downloaded");
       }
       em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.DOWNLOAD));
       return Response.ok(lic.getLicenseData())
               .header("Content-Disposition", String.format("attachment; filename=\"%s\"", lic.getPack().getLicenseType().getApplication().getLicenseFilename())).build();
   }
   /**
    * 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
   @Path("/{licId}/activate")
   @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
   @EnsureTransaction
   @Consumes(MediaType.APPLICATION_JSON)
   @Produces({ MediaType.APPLICATION_JSON })
   public Response activate(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
       License lic = getCurrentLicense(licId, bsc, em);
       if (!License.Status.isActionValid(License.Action.ACTIVATION, lic.getStatus())) {
           LOG.error("License with id {} can not be activated from current license status", licId);
           throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "License with id " + licId + " can not be activated from the current license status");
       }
       if (lic.getPack().getNumAvailables() == 0) {
           throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "The pack has not available licenses");
       }
       validateRequestData(lic.getPack(), lic.getRequestData(), lic.getActivationCode());
       License existingLicense = License.findActiveLicenseByRequestData(lic.getRequestData(), em);
       if (existingLicense != null && existingLicense.getStatus() == LicenseStatus.ACTIVE) {
           throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "An active license already exists for the given request data");
       }
       lic.setStatus(LicenseStatus.ACTIVE);
       lic.setModificationTimestamp(new Date());
       em.persist(lic);
       User user = userHelper.getUser(bsc.getUserPrincipal().getName(), em);
       em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.ACTIVATE));
       return Response.ok(lic).build();
   }
   /**
    * 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
   @POST
   @Path("/{licId}/send")
   @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
   @EnsureTransaction
   @Consumes(MediaType.APPLICATION_JSON)
   @Produces({ MediaType.APPLICATION_JSON })
   public Response send(@PathParam("licId") Integer licId, @DefaultValue("false") @FormParam("add_cc") Boolean addCC, @Context BasicSecurityContext bsc)
           throws SeCurisServiceException, SeCurisException {
       License lic = getCurrentLicense(licId, bsc, em);
       Application app = lic.getPack().getLicenseType().getApplication();
       File licFile = null;
       if (lic.getLicenseData() == null) {
           throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "There is no license file available");
       }
       if (lic.getFullName() == null) {
           throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "Please add an user name in license data to send it the license file");
       }
       User user = userHelper.getUser(bsc.getUserPrincipal().getName(), em);
       try {
           String subject = MessageFormat.format(Config.get(Config.KEYS.EMAIL_LIC_DEFAULT_SUBJECT), lic.getPack().getAppName());
           String email_tpl = IOUtils.toString(this.getClass().getResourceAsStream("/lic_email.template.en"));
           String body = MessageFormat.format(email_tpl, lic.getFullName(), app.getName());
           licFile = licenseHelper.createTemporaryLicenseFile(lic, app.getLicenseFilename());
           emailManager.sendEmail(subject, body, lic.getEmail(), addCC ? user.getEmail() : null, licFile);
       } catch (IOException e) {
           LOG.error("Error creating temporary license file", e);
           throw new SeCurisServiceException(Status.NOT_FOUND.getStatusCode(), "There is no license file available");
       } finally {
           if (licFile != null) {
               licFile.delete();
               licFile.getParentFile().delete();
           }
       }
       em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.SEND, "Email sent to: " + lic.getEmail()));
       return Response.ok(lic).build();
   }
   /**
    * 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
   @Path("/{licId}/cancel")
   @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
   @EnsureTransaction
   @Consumes(MediaType.APPLICATION_JSON)
   @Produces({ MediaType.APPLICATION_JSON })
   public Response cancel(@PathParam("licId") Integer licId, CancellationLicenseActionBean actionData, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
       License lic = getCurrentLicense(licId, bsc, em);
       if (!License.Status.isActionValid(License.Action.CANCEL, lic.getStatus())) {
           LOG.error("License with id {} can not be canceled from current license status", licId);
           throw new SeCurisServiceException(Status.FORBIDDEN.getStatusCode(), "License with id " + licId + " can not be canceled from the current license status");
       }
       if (actionData.reason == null) {
           LOG.error("To cancel an active License we need a reason, lic ID: {}, user: {}", lic.getId(), bsc.getUserPrincipal().getName());
           throw new SeCurisServiceException(Status.FORBIDDEN.getStatusCode(), "Active license with id " + licId + " can not be canceled without a reason");
       }
       licenseHelper.cancelLicense(lic, actionData.reason, bsc, em);
       return Response.ok(lic).build();
   }
   /**
    * 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.
    */
   @POST
   @Path("/")
   @Consumes(MediaType.APPLICATION_JSON)
   @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
   @Produces({ MediaType.APPLICATION_JSON })
   @EnsureTransaction
   public Response create(License lic, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
       if (checkIfCodeExists(lic.getCode(), em)) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The license code is already used in an existing license");
       }
       if (lic.getActivationCode() == null) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The activation code is mandatory");
       }
       License existingLic = License.findLicenseByActivationCode(lic.getActivationCode(), em);
       if (existingLic != null) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The activation code is already used in: " + existingLic.getCode());
       }
       if (!LicUtils.checkValidLicenseCodeCrc(lic.getCode())) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The license code is not valid");
       }
       if (!Utils.isValidEmail(lic.getEmail())) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The user email should be a valid email");
       }
       Pack pack = null;
       if (lic.getPackId() != null) {
           pack = em.find(Pack.class, lic.getPackId());
       }
       if (pack == null) {
           LOG.error("License pack with id {} not found in DB", lic.getPackId());
           throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "License's pack not found with ID: " + lic.getPackId());
       } else {
           if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN) && !bsc.getOrganizationsIds().contains(pack.getOrganization().getId())) {
               LOG.error("License for pack with id {} can not be created by user {}", pack.getId(), bsc.getUserPrincipal());
               throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Unathorized action on pack license");
           }
           if (pack.getStatus() != PackStatus.ACTIVE) {
               LOG.error("Current pack, {}, is not active so licenses cannot be created", pack.getId());
               throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "Current pack is not active so licenses cannot be created");
           }
       }
       User createdBy = userHelper.getUser(bsc.getUserPrincipal().getName(), em);
       if (lic.getRequestData() != null) {
           License existingLicense = License.findActiveLicenseByRequestData(lic.getRequestData(), em);
           if (existingLicense != null) {
               throw new SeCurisServiceException(ErrorCodes.DUPLICATED_REQUEST_DATA, "There is already an active license for current request data");
           }
           if (pack.getNumAvailables() > 0) {
               SignedLicenseBean signedLicense = generateLicense(lic, em);
               // Move directly to ACTIVE when request data is provided
               lic.setStatus(LicenseStatus.ACTIVE);
               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");
               }
           }
       } else {
           lic.setStatus(LicenseStatus.CREATED);
       }
       lic.setCodeSuffix(LicUtils.getLicenseCodeSuffix(lic.getCode()));
       lic.setCreatedBy(createdBy);
       lic.setCreationTimestamp(new Date());
       lic.setModificationTimestamp(lic.getCreationTimestamp());
       lic.setMetadataObsolete(false);
       em.persist(lic);
       em.persist(licenseHelper.createLicenseHistoryAction(lic, createdBy, LicenseHistory.Actions.CREATE));
       if (lic.getStatus() == LicenseStatus.ACTIVE) {
           em.persist(licenseHelper.createLicenseHistoryAction(lic, createdBy, LicenseHistory.Actions.ACTIVATE, "Activated on creation"));
       }
       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());
       RequestBean rb = validateRequestData(pack, license.getRequestData(), license.getActivationCode());
       try {
           LicenseBean lb = licenseGenerator.generateLicense(rb, licenseHelper.extractPackMetadata(pack.getMetadata()), licenseHelper.getExpirationDateFromPack(pack, false),
                   license.getCode(), pack.getAppName());
           sl = new SignedLicenseBean(lb);
       } catch (SeCurisException e) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
       }
       return sl;
   }
   /**
    * 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) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA, "Request data is empty");
       }
       RequestBean rb = null;
       try {
           rb = JsonUtils.json2object(requestData, RequestBean.class);
       } catch (SeCurisException e) {
           throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data has not a valid format");
       }
       if (rb.getActivationCode() != null && activationCode != null) {
           if (!rb.getActivationCode().equals(activationCode)) {
               throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Activation code mismatch");
           }
       } else {
           if (!rb.getCustomerCode().equals(pack.getOrganization().getCode())) {
               throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data not valid, wrong Organization code");
           }
           if (!rb.getLicenseTypeCode().equals(pack.getLicenseTypeCode())) {
               throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data not valid, wrong License type code");
           }
           if (rb.getPackCode() != null && !rb.getPackCode().equals(pack.getCode())) {
               throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data not valid, wrong Pack code");
           }
       }
       return rb;
   }
   /**
    * 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");
           throw new SeCurisServiceException(Status.NOT_FOUND.getStatusCode(), "Missing license ID");
       }
       License lic = em.find(License.class, licId);
       if (lic == null) {
           LOG.error("License with id {} not found in DB", licId);
           throw new SeCurisServiceException(Status.NOT_FOUND.getStatusCode(), "License not found for ID: " + licId);
       }
       if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN) && !bsc.getOrganizationsIds().contains(lic.getPack().getOrganization().getId())) {
           LOG.error("License with id {} is not accesible by user {}", licId, bsc.getUserPrincipal());
           throw new SeCurisServiceException(Status.UNAUTHORIZED.getStatusCode(), "Unathorized access to license data");
       }
       return lic;
   }
   // ---------------------------------------------------------------------
   // DTOs
   // ---------------------------------------------------------------------
   /**
    * DTO used to carry a cancellation reason for the cancel endpoint.
    */
   @JsonAutoDetect
   @JsonIgnoreProperties(ignoreUnknown = true)
   static class CancellationLicenseActionBean {
       @JsonProperty
       private String reason;
   }
}