autocomit

This commit is contained in:
2026-02-09 19:33:55 +01:00
parent 16b8ae0b8e
commit fd356019d7
22 changed files with 1595 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Story 8: External Dependencies - Deployment Notes
## Files Created
### Backend
- Database migration: `009-create-external-dependency-tables.xml`
- Entities: `DependencyType.java`, `ExternalDependency.java`
- Repositories: `DependencyTypeRepository.java`, `ExternalDependencyRepository.java`
- Services: `DependencyTypeService.java`, `ExternalDependencyService.java`
- Controllers: `DependencyTypeController.java`, `ExternalDependencyController.java`
- DTOs: Request and Response classes for both entities
- Updated: `db.changelog-master.xml`
### Frontend
- Models: `dependency.model.ts`
- Service: `dependency.service.ts`
- Components:
- `application-dependencies` (tab in application detail)
- `dependency-list` (full list page)
- `dependency-form` (create/edit)
- `dependency-detail` (view details)
- `dependency-type-list` (admin catalog management)
## Deployment Steps
1. Copy all backend files to their respective locations
2. Run Liquibase migration: `mvn liquibase:update`
3. Build backend: `mvn clean package`
4. Copy frontend files
5. Install dependencies: `npm install` (if needed)
6. Build frontend: `ng build`
## Testing Checklist
- [ ] Default dependency types seeded
- [ ] Create custom dependency type (admin)
- [ ] Create external dependency
- [ ] Validate date logic
- [ ] Filter by type and status
- [ ] View expiring dependencies
- [ ] Update dependency
- [ ] Delete dependency
- [ ] Cannot delete type with dependencies
## API Endpoints
See Swagger UI at: http://localhost:8080/api/swagger-ui.html
Key endpoints:
- GET /api/dependency-types
- POST /api/dependency-types (admin)
- GET /api/dependencies
- POST /api/dependencies/for-application/{id}
- GET /api/dependencies/expiring?days=30
- GET /api/dependencies/expired
@@ -0,0 +1,68 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreateDependencyTypeRequest;
import com.ldpv2.dto.request.UpdateDependencyTypeRequest;
import com.ldpv2.dto.response.DependencyTypeResponse;
import com.ldpv2.service.DependencyTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/dependency-types")
@Tag(name = "Dependency Types", description = "Dependency type catalog management")
@SecurityRequirement(name = "bearerAuth")
public class DependencyTypeController {
@Autowired
private DependencyTypeService dependencyTypeService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create dependency type", description = "Create custom dependency type (Admin only)")
public ResponseEntity<DependencyTypeResponse> create(@Valid @RequestBody CreateDependencyTypeRequest request) {
DependencyTypeResponse response = dependencyTypeService.create(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Update dependency type", description = "Update dependency type (Admin only)")
public ResponseEntity<DependencyTypeResponse> update(
@PathVariable UUID id,
@Valid @RequestBody UpdateDependencyTypeRequest request) {
DependencyTypeResponse response = dependencyTypeService.update(id, request);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get dependency type", description = "Get dependency type by ID")
public ResponseEntity<DependencyTypeResponse> getById(@PathVariable UUID id) {
DependencyTypeResponse response = dependencyTypeService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List dependency types", description = "Get all dependency types")
public ResponseEntity<List<DependencyTypeResponse>> getAll() {
List<DependencyTypeResponse> response = dependencyTypeService.findAll();
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete dependency type", description = "Delete dependency type (Admin only)")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
dependencyTypeService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,117 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreateExternalDependencyRequest;
import com.ldpv2.dto.request.UpdateExternalDependencyRequest;
import com.ldpv2.dto.response.ExternalDependencyResponse;
import com.ldpv2.service.ExternalDependencyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/dependencies")
@Tag(name = "External Dependencies", description = "External dependency management")
@SecurityRequirement(name = "bearerAuth")
public class ExternalDependencyController {
@Autowired
private ExternalDependencyService externalDependencyService;
@PostMapping("/for-application/{applicationId}")
@Operation(summary = "Create dependency", description = "Create external dependency for application")
public ResponseEntity<ExternalDependencyResponse> create(
@PathVariable UUID applicationId,
@Valid @RequestBody CreateExternalDependencyRequest request) {
ExternalDependencyResponse response = externalDependencyService.create(applicationId, request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@Operation(summary = "Update dependency", description = "Update external dependency")
public ResponseEntity<ExternalDependencyResponse> update(
@PathVariable UUID id,
@Valid @RequestBody UpdateExternalDependencyRequest request) {
ExternalDependencyResponse response = externalDependencyService.update(id, request);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get dependency", description = "Get external dependency by ID")
public ResponseEntity<ExternalDependencyResponse> getById(@PathVariable UUID id) {
ExternalDependencyResponse response = externalDependencyService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List dependencies", description = "Get all dependencies with filters")
public ResponseEntity<Page<ExternalDependencyResponse>> getAll(
@RequestParam(required = false) UUID applicationId,
@RequestParam(required = false) UUID dependencyTypeId,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "asc") String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<ExternalDependencyResponse> response;
if (applicationId != null || dependencyTypeId != null || status != null) {
response = externalDependencyService.search(applicationId, dependencyTypeId, status, pageable);
} else {
response = externalDependencyService.findAll(pageable);
}
return ResponseEntity.ok(response);
}
@GetMapping("/by-application/{applicationId}")
@Operation(summary = "Get dependencies by application", description = "Get all dependencies for an application")
public ResponseEntity<Page<ExternalDependencyResponse>> getByApplication(
@PathVariable UUID applicationId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("name").ascending());
Page<ExternalDependencyResponse> response = externalDependencyService.findByApplication(applicationId, pageable);
return ResponseEntity.ok(response);
}
@GetMapping("/expiring")
@Operation(summary = "Get expiring dependencies", description = "Get dependencies expiring within specified days")
public ResponseEntity<List<ExternalDependencyResponse>> getExpiring(
@RequestParam(defaultValue = "30") int days) {
List<ExternalDependencyResponse> response = externalDependencyService.findExpiring(days);
return ResponseEntity.ok(response);
}
@GetMapping("/expired")
@Operation(summary = "Get expired dependencies", description = "Get all expired dependencies")
public ResponseEntity<List<ExternalDependencyResponse>> getExpired() {
List<ExternalDependencyResponse> response = externalDependencyService.findExpired();
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete dependency", description = "Delete external dependency")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
externalDependencyService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,28 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Dependency Type entity - catalog of dependency types
*/
@Data
@Entity
@Table(name = "dependency_type")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class DependencyType extends BaseEntity {
@Column(name = "type_name", nullable = false, unique = true, length = 100)
private String typeName;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "is_custom", nullable = false)
private Boolean isCustom = false;
}
@@ -0,0 +1,44 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
* External Dependency entity
*/
@Data
@Entity
@Table(name = "external_dependency")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ExternalDependency extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "application_id", nullable = false)
private Application application;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "dependency_type_id", nullable = false)
private DependencyType dependencyType;
@Column(nullable = false, length = 255)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "technical_documentation", columnDefinition = "TEXT")
private String technicalDocumentation;
@Column(name = "validity_start_date")
private LocalDate validityStartDate;
@Column(name = "validity_end_date")
private LocalDate validityEndDate;
}
@@ -0,0 +1,19 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateDependencyTypeRequest {
@NotBlank(message = "Type name is required")
@Size(max = 100, message = "Type name must not exceed 100 characters")
private String typeName;
private String description;
}
@@ -0,0 +1,32 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateExternalDependencyRequest {
@NotNull(message = "Dependency type is required")
private UUID dependencyTypeId;
@NotBlank(message = "Name is required")
@Size(max = 255, message = "Name must not exceed 255 characters")
private String name;
private String description;
private String technicalDocumentation;
private LocalDate validityStartDate;
private LocalDate validityEndDate;
}
@@ -0,0 +1,17 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateDependencyTypeRequest {
@Size(max = 100, message = "Type name must not exceed 100 characters")
private String typeName;
private String description;
}
@@ -0,0 +1,28 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateExternalDependencyRequest {
private UUID dependencyTypeId;
@Size(max = 255, message = "Name must not exceed 255 characters")
private String name;
private String description;
private String technicalDocumentation;
private LocalDate validityStartDate;
private LocalDate validityEndDate;
}
@@ -0,0 +1,20 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DependencyTypeResponse {
private UUID id;
private String typeName;
private String description;
private Boolean isCustom;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,28 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExternalDependencyResponse {
private UUID id;
private ApplicationSummaryResponse application;
private DependencyTypeResponse dependencyType;
private String name;
private String description;
private String technicalDocumentation;
private LocalDate validityStartDate;
private LocalDate validityEndDate;
private Boolean isActive;
private Integer daysUntilExpiration;
private String status; // ACTIVE, EXPIRING, EXPIRED, NOT_YET_VALID
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,14 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.DependencyType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface DependencyTypeRepository extends JpaRepository<DependencyType, UUID> {
Optional<DependencyType> findByTypeName(String typeName);
boolean existsByTypeName(String typeName);
}
@@ -0,0 +1,55 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.ExternalDependency;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
@Repository
public interface ExternalDependencyRepository extends JpaRepository<ExternalDependency, UUID> {
Page<ExternalDependency> findByApplicationId(UUID applicationId, Pageable pageable);
Page<ExternalDependency> findByDependencyTypeId(UUID dependencyTypeId, Pageable pageable);
@Query("SELECT d FROM ExternalDependency d WHERE " +
"d.validityEndDate IS NOT NULL AND " +
"d.validityEndDate >= :now AND " +
"d.validityEndDate <= :expirationDate")
List<ExternalDependency> findExpiring(
@Param("now") LocalDate now,
@Param("expirationDate") LocalDate expirationDate
);
@Query("SELECT d FROM ExternalDependency d WHERE " +
"d.validityEndDate IS NOT NULL AND " +
"d.validityEndDate < :now")
List<ExternalDependency> findExpired(@Param("now") LocalDate now);
@Query("SELECT d FROM ExternalDependency d WHERE " +
"(:applicationId IS NULL OR d.application.id = :applicationId) AND " +
"(:dependencyTypeId IS NULL OR d.dependencyType.id = :dependencyTypeId) AND " +
"(:status IS NULL OR " +
" (:status = 'ACTIVE' AND (d.validityEndDate IS NULL OR d.validityEndDate >= :now) AND (d.validityStartDate IS NULL OR d.validityStartDate <= :now)) OR " +
" (:status = 'EXPIRING' AND d.validityEndDate IS NOT NULL AND d.validityEndDate >= :now AND d.validityEndDate <= :expiringDate) OR " +
" (:status = 'EXPIRED' AND d.validityEndDate IS NOT NULL AND d.validityEndDate < :now) OR " +
" (:status = 'NOT_YET_VALID' AND d.validityStartDate IS NOT NULL AND d.validityStartDate > :now)" +
")")
Page<ExternalDependency> search(
@Param("applicationId") UUID applicationId,
@Param("dependencyTypeId") UUID dependencyTypeId,
@Param("status") String status,
@Param("now") LocalDate now,
@Param("expiringDate") LocalDate expiringDate,
Pageable pageable
);
long countByDependencyTypeId(UUID dependencyTypeId);
}
@@ -0,0 +1,99 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.DependencyType;
import com.ldpv2.dto.request.CreateDependencyTypeRequest;
import com.ldpv2.dto.request.UpdateDependencyTypeRequest;
import com.ldpv2.dto.response.DependencyTypeResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.DependencyTypeRepository;
import com.ldpv2.repository.ExternalDependencyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class DependencyTypeService {
@Autowired
private DependencyTypeRepository dependencyTypeRepository;
@Autowired
private ExternalDependencyRepository externalDependencyRepository;
@Transactional
public DependencyTypeResponse create(CreateDependencyTypeRequest request) {
if (dependencyTypeRepository.existsByTypeName(request.getTypeName())) {
throw new BadRequestException("Dependency type '" + request.getTypeName() + "' already exists");
}
DependencyType type = new DependencyType();
type.setTypeName(request.getTypeName());
type.setDescription(request.getDescription());
type.setIsCustom(true); // User-created types are custom
type = dependencyTypeRepository.save(type);
return mapToResponse(type);
}
@Transactional
public DependencyTypeResponse update(UUID id, UpdateDependencyTypeRequest request) {
DependencyType type = dependencyTypeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Dependency type not found with id: " + id));
if (request.getTypeName() != null && !request.getTypeName().equals(type.getTypeName())) {
if (dependencyTypeRepository.existsByTypeName(request.getTypeName())) {
throw new BadRequestException("Dependency type '" + request.getTypeName() + "' already exists");
}
type.setTypeName(request.getTypeName());
}
if (request.getDescription() != null) {
type.setDescription(request.getDescription());
}
type = dependencyTypeRepository.save(type);
return mapToResponse(type);
}
public DependencyTypeResponse findById(UUID id) {
DependencyType type = dependencyTypeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Dependency type not found with id: " + id));
return mapToResponse(type);
}
public List<DependencyTypeResponse> findAll() {
return dependencyTypeRepository.findAll().stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
@Transactional
public void delete(UUID id) {
DependencyType type = dependencyTypeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Dependency type not found with id: " + id));
// Check if type is being used
long count = externalDependencyRepository.countByDependencyTypeId(id);
if (count > 0) {
throw new BadRequestException("Cannot delete dependency type with " + count + " existing dependencies");
}
dependencyTypeRepository.delete(type);
}
private DependencyTypeResponse mapToResponse(DependencyType type) {
return new DependencyTypeResponse(
type.getId(),
type.getTypeName(),
type.getDescription(),
type.getIsCustom(),
type.getCreatedAt(),
type.getUpdatedAt()
);
}
}
@@ -0,0 +1,241 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.Application;
import com.ldpv2.domain.entity.DependencyType;
import com.ldpv2.domain.entity.ExternalDependency;
import com.ldpv2.dto.request.CreateExternalDependencyRequest;
import com.ldpv2.dto.request.UpdateExternalDependencyRequest;
import com.ldpv2.dto.response.ApplicationSummaryResponse;
import com.ldpv2.dto.response.DependencyTypeResponse;
import com.ldpv2.dto.response.ExternalDependencyResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.ApplicationRepository;
import com.ldpv2.repository.DependencyTypeRepository;
import com.ldpv2.repository.ExternalDependencyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class ExternalDependencyService {
@Autowired
private ExternalDependencyRepository externalDependencyRepository;
@Autowired
private ApplicationRepository applicationRepository;
@Autowired
private DependencyTypeRepository dependencyTypeRepository;
@Transactional
public ExternalDependencyResponse create(UUID applicationId, CreateExternalDependencyRequest request) {
Application application = applicationRepository.findById(applicationId)
.orElseThrow(() -> new ResourceNotFoundException("Application not found with id: " + applicationId));
DependencyType dependencyType = dependencyTypeRepository.findById(request.getDependencyTypeId())
.orElseThrow(() -> new ResourceNotFoundException(
"Dependency type not found with id: " + request.getDependencyTypeId()));
// Validate dates
if (request.getValidityStartDate() != null && request.getValidityEndDate() != null) {
if (request.getValidityEndDate().isBefore(request.getValidityStartDate())) {
throw new BadRequestException("End date must be after or equal to start date");
}
}
ExternalDependency dependency = new ExternalDependency();
dependency.setApplication(application);
dependency.setDependencyType(dependencyType);
dependency.setName(request.getName());
dependency.setDescription(request.getDescription());
dependency.setTechnicalDocumentation(request.getTechnicalDocumentation());
dependency.setValidityStartDate(request.getValidityStartDate());
dependency.setValidityEndDate(request.getValidityEndDate());
dependency = externalDependencyRepository.save(dependency);
return mapToResponse(dependency);
}
@Transactional
public ExternalDependencyResponse update(UUID id, UpdateExternalDependencyRequest request) {
ExternalDependency dependency = externalDependencyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("External dependency not found with id: " + id));
if (request.getDependencyTypeId() != null) {
DependencyType dependencyType = dependencyTypeRepository.findById(request.getDependencyTypeId())
.orElseThrow(() -> new ResourceNotFoundException(
"Dependency type not found with id: " + request.getDependencyTypeId()));
dependency.setDependencyType(dependencyType);
}
if (request.getName() != null) {
dependency.setName(request.getName());
}
if (request.getDescription() != null) {
dependency.setDescription(request.getDescription());
}
if (request.getTechnicalDocumentation() != null) {
dependency.setTechnicalDocumentation(request.getTechnicalDocumentation());
}
if (request.getValidityStartDate() != null) {
dependency.setValidityStartDate(request.getValidityStartDate());
}
if (request.getValidityEndDate() != null) {
dependency.setValidityEndDate(request.getValidityEndDate());
}
// Validate dates after updates
if (dependency.getValidityStartDate() != null && dependency.getValidityEndDate() != null) {
if (dependency.getValidityEndDate().isBefore(dependency.getValidityStartDate())) {
throw new BadRequestException("End date must be after or equal to start date");
}
}
dependency = externalDependencyRepository.save(dependency);
return mapToResponse(dependency);
}
public ExternalDependencyResponse findById(UUID id) {
ExternalDependency dependency = externalDependencyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("External dependency not found with id: " + id));
return mapToResponse(dependency);
}
public Page<ExternalDependencyResponse> findByApplication(UUID applicationId, Pageable pageable) {
if (!applicationRepository.existsById(applicationId)) {
throw new ResourceNotFoundException("Application not found with id: " + applicationId);
}
return externalDependencyRepository.findByApplicationId(applicationId, pageable)
.map(this::mapToResponse);
}
public Page<ExternalDependencyResponse> findAll(Pageable pageable) {
return externalDependencyRepository.findAll(pageable).map(this::mapToResponse);
}
public Page<ExternalDependencyResponse> search(
UUID applicationId,
UUID dependencyTypeId,
String status,
Pageable pageable) {
LocalDate now = LocalDate.now();
LocalDate expiringDate = now.plusDays(30);
return externalDependencyRepository.search(
applicationId, dependencyTypeId, status, now, expiringDate, pageable)
.map(this::mapToResponse);
}
public List<ExternalDependencyResponse> findExpiring(int days) {
LocalDate now = LocalDate.now();
LocalDate expirationDate = now.plusDays(days);
return externalDependencyRepository.findExpiring(now, expirationDate).stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
public List<ExternalDependencyResponse> findExpired() {
LocalDate now = LocalDate.now();
return externalDependencyRepository.findExpired(now).stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
@Transactional
public void delete(UUID id) {
if (!externalDependencyRepository.existsById(id)) {
throw new ResourceNotFoundException("External dependency not found with id: " + id);
}
externalDependencyRepository.deleteById(id);
}
private ExternalDependencyResponse mapToResponse(ExternalDependency dependency) {
ApplicationSummaryResponse appSummary = new ApplicationSummaryResponse(
dependency.getApplication().getId(),
dependency.getApplication().getName(),
dependency.getApplication().getStatus(),
dependency.getApplication().getBusinessUnit().getName()
);
DependencyTypeResponse typeResponse = new DependencyTypeResponse(
dependency.getDependencyType().getId(),
dependency.getDependencyType().getTypeName(),
dependency.getDependencyType().getDescription(),
dependency.getDependencyType().getIsCustom(),
dependency.getDependencyType().getCreatedAt(),
dependency.getDependencyType().getUpdatedAt()
);
// Compute status
String status = computeStatus(dependency);
Boolean isActive = "ACTIVE".equals(status) || "EXPIRING".equals(status);
Integer daysUntilExpiration = computeDaysUntilExpiration(dependency);
return new ExternalDependencyResponse(
dependency.getId(),
appSummary,
typeResponse,
dependency.getName(),
dependency.getDescription(),
dependency.getTechnicalDocumentation(),
dependency.getValidityStartDate(),
dependency.getValidityEndDate(),
isActive,
daysUntilExpiration,
status,
dependency.getCreatedAt(),
dependency.getUpdatedAt()
);
}
private String computeStatus(ExternalDependency dependency) {
LocalDate now = LocalDate.now();
if (dependency.getValidityStartDate() != null && now.isBefore(dependency.getValidityStartDate())) {
return "NOT_YET_VALID";
}
if (dependency.getValidityEndDate() == null) {
return "ACTIVE"; // No end date = indefinite
}
if (now.isAfter(dependency.getValidityEndDate())) {
return "EXPIRED";
}
long daysUntilExpiration = ChronoUnit.DAYS.between(now, dependency.getValidityEndDate());
if (daysUntilExpiration <= 30) {
return "EXPIRING";
}
return "ACTIVE";
}
private Integer computeDaysUntilExpiration(ExternalDependency dependency) {
if (dependency.getValidityEndDate() == null) {
return null;
}
LocalDate now = LocalDate.now();
if (now.isAfter(dependency.getValidityEndDate())) {
return null; // Already expired
}
return (int) ChronoUnit.DAYS.between(now, dependency.getValidityEndDate());
}
}
@@ -14,5 +14,6 @@
<include file="db/changelog/v1.0/006-create-version-table.xml"/>
<include file="db/changelog/v1.0/007-create-deployment-table.xml"/>
<include file="db/changelog/v1.0/008-create-application-contact-table.xml"/>
<include file="db/changelog/v1.0/009-create-external-dependency-tables.xml"/>
</databaseChangeLog>
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="009-create-external-dependency-tables" author="ldpv2-team">
<!-- Dependency Types Catalog -->
<createTable tableName="dependency_type">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="type_name" type="VARCHAR(100)">
<constraints nullable="false" unique="true"/>
</column>
<column name="description" type="TEXT"/>
<column name="is_custom" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<!-- External Dependencies -->
<createTable tableName="external_dependency">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="application_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_ext_dep_application"
references="application(id)"
deleteCascade="true"/>
</column>
<column name="dependency_type_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_ext_dep_type"
references="dependency_type(id)"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="description" type="TEXT"/>
<column name="technical_documentation" type="TEXT"/>
<column name="validity_start_date" type="DATE"/>
<column name="validity_end_date" type="DATE"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<!-- Add check constraint for validity dates -->
<sql>
ALTER TABLE external_dependency
ADD CONSTRAINT check_validity_dates
CHECK (validity_end_date IS NULL OR validity_start_date IS NULL OR validity_end_date >= validity_start_date);
</sql>
<!-- Indexes -->
<createIndex tableName="dependency_type" indexName="idx_dep_type_name">
<column name="type_name"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_application">
<column name="application_id"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_type">
<column name="dependency_type_id"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_validity_end">
<column name="validity_end_date"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_name">
<column name="name"/>
</createIndex>
<!-- Insert default dependency types -->
<insert tableName="dependency_type">
<column name="type_name" value="WEB_SERVICE"/>
<column name="description" value="REST APIs, SOAP services, microservices"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<insert tableName="dependency_type">
<column name="type_name" value="DATABASE"/>
<column name="description" value="External database connections"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<insert tableName="dependency_type">
<column name="type_name" value="CERTIFICATE"/>
<column name="description" value="SSL/TLS certificates, authentication certificates"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<insert tableName="dependency_type">
<column name="type_name" value="NETWORK_FLOW"/>
<column name="description" value="Network connections, firewall rules, VPN tunnels"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<!-- Sample data -->
<insert tableName="external_dependency">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Customer Portal' LIMIT 1)"/>
<column name="dependency_type_id" valueComputed="(SELECT id FROM dependency_type WHERE type_name = 'WEB_SERVICE' LIMIT 1)"/>
<column name="name" value="Payment Gateway API"/>
<column name="description" value="External payment processing service"/>
<column name="technical_documentation" value="Endpoint: https://api.payment.example.com/v2"/>
<column name="validity_start_date" value="2024-01-01"/>
<column name="validity_end_date" value="2027-12-31"/>
</insert>
<insert tableName="external_dependency">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Internal CRM' LIMIT 1)"/>
<column name="dependency_type_id" valueComputed="(SELECT id FROM dependency_type WHERE type_name = 'DATABASE' LIMIT 1)"/>
<column name="name" value="Legacy Customer Database"/>
<column name="description" value="Read-only connection to legacy system"/>
<column name="technical_documentation" value="Server: legacy-db.internal:5432"/>
<column name="validity_start_date" value="2020-01-01"/>
<column name="validity_end_date" value="2026-06-30"/>
</insert>
</changeSet>
</databaseChangeLog>