autocomit

This commit is contained in:
2026-02-08 10:32:13 +01:00
parent b69ff3965b
commit cc79baeb09
28 changed files with 4163 additions and 4 deletions
@@ -0,0 +1,121 @@
package com.ldpv2.controller;
import com.ldpv2.domain.enums.ApplicationStatus;
import com.ldpv2.dto.request.CreateApplicationRequest;
import com.ldpv2.dto.request.UpdateApplicationRequest;
import com.ldpv2.dto.response.ApplicationResponse;
import com.ldpv2.service.ApplicationService;
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.UUID;
@RestController
@RequestMapping("/applications")
@Tag(name = "Applications", description = "Application management endpoints")
@SecurityRequirement(name = "bearerAuth")
public class ApplicationController {
@Autowired
private ApplicationService applicationService;
@PostMapping
@Operation(summary = "Create application", description = "Create a new application")
public ResponseEntity<ApplicationResponse> create(@Valid @RequestBody CreateApplicationRequest request) {
ApplicationResponse response = applicationService.create(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@Operation(summary = "Update application", description = "Update an existing application")
public ResponseEntity<ApplicationResponse> update(
@PathVariable UUID id,
@Valid @RequestBody UpdateApplicationRequest request) {
ApplicationResponse response = applicationService.update(id, request);
return ResponseEntity.ok(response);
}
@PatchMapping("/{id}/status")
@Operation(summary = "Update status", description = "Update application status only")
public ResponseEntity<ApplicationResponse> updateStatus(
@PathVariable UUID id,
@RequestParam ApplicationStatus status) {
ApplicationResponse response = applicationService.updateStatus(id, status);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get application", description = "Get application by ID")
public ResponseEntity<ApplicationResponse> getById(@PathVariable UUID id) {
ApplicationResponse response = applicationService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List applications", description = "Get paginated list of applications")
public ResponseEntity<Page<ApplicationResponse>> getAll(
@RequestParam(required = false) ApplicationStatus status,
@RequestParam(required = false) UUID businessUnitId,
@RequestParam(required = false) String name,
@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<ApplicationResponse> response;
if (status != null || businessUnitId != null || name != null) {
response = applicationService.search(status, businessUnitId, name, pageable);
} else {
response = applicationService.findAll(pageable);
}
return ResponseEntity.ok(response);
}
@GetMapping("/by-status/{status}")
@Operation(summary = "Filter by status", description = "Get applications by status")
public ResponseEntity<Page<ApplicationResponse>> getByStatus(
@PathVariable ApplicationStatus status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<ApplicationResponse> response = applicationService.findByStatus(status, pageable);
return ResponseEntity.ok(response);
}
@GetMapping("/by-business-unit/{businessUnitId}")
@Operation(summary = "Filter by business unit", description = "Get applications by business unit")
public ResponseEntity<Page<ApplicationResponse>> getByBusinessUnit(
@PathVariable UUID businessUnitId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<ApplicationResponse> response = applicationService.findByBusinessUnit(businessUnitId, pageable);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete application", description = "Delete an application")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
applicationService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,42 @@
package com.ldpv2.domain.entity;
import com.ldpv2.domain.enums.ApplicationStatus;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
* Application entity representing software systems
*/
@Data
@Entity
@Table(name = "application")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Application extends BaseEntity {
@Column(nullable = false, length = 255)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 50)
private ApplicationStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "business_unit_id", nullable = false)
private BusinessUnit businessUnit;
@Column(name = "end_of_life_date")
private LocalDate endOfLifeDate;
@Column(name = "end_of_support_date")
private LocalDate endOfSupportDate;
}
@@ -0,0 +1,19 @@
package com.ldpv2.domain.enums;
public enum ApplicationStatus {
IDEA("Idea"),
IN_DEVELOPMENT("In Development"),
IN_SERVICE("In Service"),
MAINTENANCE("Maintenance"),
DECOMMISSIONED("Decommissioned");
private final String displayName;
ApplicationStatus(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,34 @@
package com.ldpv2.dto.request;
import com.ldpv2.domain.enums.ApplicationStatus;
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 CreateApplicationRequest {
@NotBlank(message = "Name is required")
@Size(max = 255, message = "Name must not exceed 255 characters")
private String name;
private String description;
@NotNull(message = "Status is required")
private ApplicationStatus status;
@NotNull(message = "Business unit is required")
private UUID businessUnitId;
private LocalDate endOfLifeDate;
private LocalDate endOfSupportDate;
}
@@ -0,0 +1,29 @@
package com.ldpv2.dto.request;
import com.ldpv2.domain.enums.ApplicationStatus;
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 UpdateApplicationRequest {
@Size(max = 255, message = "Name must not exceed 255 characters")
private String name;
private String description;
private ApplicationStatus status;
private UUID businessUnitId;
private LocalDate endOfLifeDate;
private LocalDate endOfSupportDate;
}
@@ -0,0 +1,25 @@
package com.ldpv2.dto.response;
import com.ldpv2.domain.enums.ApplicationStatus;
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 ApplicationResponse {
private UUID id;
private String name;
private String description;
private ApplicationStatus status;
private BusinessUnitSummaryResponse businessUnit;
private LocalDate endOfLifeDate;
private LocalDate endOfSupportDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,18 @@
package com.ldpv2.dto.response;
import com.ldpv2.domain.enums.ApplicationStatus;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationSummaryResponse {
private UUID id;
private String name;
private ApplicationStatus status;
private String businessUnitName;
}
@@ -0,0 +1,31 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.Application;
import com.ldpv2.domain.enums.ApplicationStatus;
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.util.UUID;
@Repository
public interface ApplicationRepository extends JpaRepository<Application, UUID> {
Page<Application> findByStatus(ApplicationStatus status, Pageable pageable);
Page<Application> findByBusinessUnitId(UUID businessUnitId, Pageable pageable);
Page<Application> findByNameContainingIgnoreCase(String name, Pageable pageable);
Page<Application> findByStatusAndBusinessUnitId(ApplicationStatus status, UUID businessUnitId, Pageable pageable);
@Query("SELECT a FROM Application a WHERE " +
"(:status IS NULL OR a.status = :status) AND " +
"(:businessUnitId IS NULL OR a.businessUnit.id = :businessUnitId) AND " +
"(:name IS NULL OR LOWER(a.name) LIKE LOWER(CONCAT('%', :name, '%')))")
Page<Application> search(
@Param("status") ApplicationStatus status,
@Param("businessUnitId") UUID businessUnitId,
@Param("name") String name,
Pageable pageable
);
}
@@ -0,0 +1,166 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.Application;
import com.ldpv2.domain.entity.BusinessUnit;
import com.ldpv2.domain.enums.ApplicationStatus;
import com.ldpv2.dto.request.CreateApplicationRequest;
import com.ldpv2.dto.request.UpdateApplicationRequest;
import com.ldpv2.dto.response.ApplicationResponse;
import com.ldpv2.dto.response.BusinessUnitSummaryResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.ApplicationRepository;
import com.ldpv2.repository.BusinessUnitRepository;
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.util.UUID;
@Service
public class ApplicationService {
@Autowired
private ApplicationRepository applicationRepository;
@Autowired
private BusinessUnitRepository businessUnitRepository;
@Transactional
public ApplicationResponse create(CreateApplicationRequest request) {
// Validate business unit exists
BusinessUnit businessUnit = businessUnitRepository.findById(request.getBusinessUnitId())
.orElseThrow(() -> new ResourceNotFoundException(
"Business unit not found with id: " + request.getBusinessUnitId()));
// Validate dates if both are provided
if (request.getEndOfSupportDate() != null && request.getEndOfLifeDate() != null) {
if (request.getEndOfSupportDate().isAfter(request.getEndOfLifeDate())) {
throw new BadRequestException(
"End of support date must be before end of life date");
}
}
Application application = new Application();
application.setName(request.getName());
application.setDescription(request.getDescription());
application.setStatus(request.getStatus());
application.setBusinessUnit(businessUnit);
application.setEndOfLifeDate(request.getEndOfLifeDate());
application.setEndOfSupportDate(request.getEndOfSupportDate());
application = applicationRepository.save(application);
return mapToResponse(application);
}
@Transactional
public ApplicationResponse update(UUID id, UpdateApplicationRequest request) {
Application application = applicationRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Application not found with id: " + id));
if (request.getName() != null) {
application.setName(request.getName());
}
if (request.getDescription() != null) {
application.setDescription(request.getDescription());
}
if (request.getStatus() != null) {
application.setStatus(request.getStatus());
}
if (request.getBusinessUnitId() != null) {
BusinessUnit businessUnit = businessUnitRepository.findById(request.getBusinessUnitId())
.orElseThrow(() -> new ResourceNotFoundException(
"Business unit not found with id: " + request.getBusinessUnitId()));
application.setBusinessUnit(businessUnit);
}
if (request.getEndOfLifeDate() != null) {
application.setEndOfLifeDate(request.getEndOfLifeDate());
}
if (request.getEndOfSupportDate() != null) {
application.setEndOfSupportDate(request.getEndOfSupportDate());
}
// Validate dates if both are set
if (application.getEndOfSupportDate() != null && application.getEndOfLifeDate() != null) {
if (application.getEndOfSupportDate().isAfter(application.getEndOfLifeDate())) {
throw new BadRequestException(
"End of support date must be before end of life date");
}
}
application = applicationRepository.save(application);
return mapToResponse(application);
}
@Transactional
public ApplicationResponse updateStatus(UUID id, ApplicationStatus newStatus) {
Application application = applicationRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Application not found with id: " + id));
application.setStatus(newStatus);
application = applicationRepository.save(application);
return mapToResponse(application);
}
public ApplicationResponse findById(UUID id) {
Application application = applicationRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Application not found with id: " + id));
return mapToResponse(application);
}
public Page<ApplicationResponse> findAll(Pageable pageable) {
return applicationRepository.findAll(pageable).map(this::mapToResponse);
}
public Page<ApplicationResponse> findByStatus(ApplicationStatus status, Pageable pageable) {
return applicationRepository.findByStatus(status, pageable).map(this::mapToResponse);
}
public Page<ApplicationResponse> findByBusinessUnit(UUID businessUnitId, Pageable pageable) {
return applicationRepository.findByBusinessUnitId(businessUnitId, pageable)
.map(this::mapToResponse);
}
public Page<ApplicationResponse> search(ApplicationStatus status, UUID businessUnitId,
String name, Pageable pageable) {
return applicationRepository.search(status, businessUnitId, name, pageable)
.map(this::mapToResponse);
}
@Transactional
public void delete(UUID id) {
if (!applicationRepository.existsById(id)) {
throw new ResourceNotFoundException("Application not found with id: " + id);
}
applicationRepository.deleteById(id);
}
private ApplicationResponse mapToResponse(Application application) {
BusinessUnitSummaryResponse buSummary = new BusinessUnitSummaryResponse(
application.getBusinessUnit().getId(),
application.getBusinessUnit().getName()
);
return new ApplicationResponse(
application.getId(),
application.getName(),
application.getDescription(),
application.getStatus(),
buSummary,
application.getEndOfLifeDate(),
application.getEndOfSupportDate(),
application.getCreatedAt(),
application.getUpdatedAt()
);
}
}
@@ -8,11 +8,11 @@
<changeSet id="initial-data" author="ldpv2-team">
<!-- Insert default admin user -->
<!-- Password: admin123 (hashed with BCrypt) -->
<!-- Password: admin (hashed with BCrypt) -->
<insert tableName="users">
<column name="username" value="admin"/>
<!-- <column name="password" value="$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"/>-->
<column name="password" value="$2a$12$mCGWGeNM3r11.yFhPFi22e./YQl2pRTIpJBVUwydScZioE3y6xm3m"/>
<!-- BCrypt hash for "admin" -->
<column name="password" value="$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cyhQQiNpz5ZeaQ/o6HIYTgYhqCL6e"/>
<column name="email" value="admin@ldpv2.com"/>
<column name="role" value="ADMIN"/>
</insert>
@@ -13,4 +13,7 @@
<!-- Story 1: Business Units -->
<include file="db/changelog/v1.0/003-create-business-unit-table.xml"/>
<!-- Story 2: Applications -->
<include file="db/changelog/v1.0/004-create-application-table.xml"/>
</databaseChangeLog>
@@ -0,0 +1,143 @@
<?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="004-create-application-table" author="ldpv2-team">
<!-- Create ApplicationStatus enum type -->
<sql>
CREATE TYPE application_status AS ENUM (
'IDEA',
'IN_DEVELOPMENT',
'IN_SERVICE',
'MAINTENANCE',
'DECOMMISSIONED'
);
</sql>
<!-- Create application table -->
<createTable tableName="application">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="description" type="TEXT"/>
<column name="status" type="application_status">
<constraints nullable="false"/>
</column>
<column name="business_unit_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_application_business_unit"
references="business_unit(id)"/>
</column>
<column name="end_of_life_date" type="DATE"/>
<column name="end_of_support_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>
<!-- Create indexes -->
<createIndex tableName="application" indexName="idx_application_status">
<column name="status"/>
</createIndex>
<createIndex tableName="application" indexName="idx_application_business_unit">
<column name="business_unit_id"/>
</createIndex>
<createIndex tableName="application" indexName="idx_application_name">
<column name="name"/>
</createIndex>
</changeSet>
<!-- Insert sample applications in a separate changeset -->
<changeSet id="004-insert-sample-applications" author="ldpv2-team">
<!-- Insert sample applications -->
<sql>
-- Customer Portal (Digital Services)
INSERT INTO application (name, description, status, business_unit_id, end_of_support_date, end_of_life_date)
SELECT
'Customer Portal',
'External customer-facing portal for self-service',
'IN_SERVICE'::application_status,
id,
'2028-12-31'::DATE,
'2030-12-31'::DATE
FROM business_unit WHERE name = 'Digital Services';
-- Internal CRM (Digital Services)
INSERT INTO application (name, description, status, business_unit_id, end_of_support_date, end_of_life_date)
SELECT
'Internal CRM',
'Customer relationship management system',
'IN_SERVICE'::application_status,
id,
'2027-06-30'::DATE,
'2029-06-30'::DATE
FROM business_unit WHERE name = 'Digital Services';
-- HR Management System (Human Resources)
INSERT INTO application (name, description, status, business_unit_id, end_of_support_date, end_of_life_date)
SELECT
'HR Management System',
'Employee data and payroll management',
'IN_SERVICE'::application_status,
id,
'2029-12-31'::DATE,
'2031-12-31'::DATE
FROM business_unit WHERE name = 'Human Resources';
-- Financial Reporting Tool (Finance)
INSERT INTO application (name, description, status, business_unit_id, end_of_support_date, end_of_life_date)
SELECT
'Financial Reporting Tool',
'Automated financial reporting and analytics',
'IN_SERVICE'::application_status,
id,
'2026-12-31'::DATE,
'2028-12-31'::DATE
FROM business_unit WHERE name = 'Finance';
-- Mobile App (Digital Services)
INSERT INTO application (name, description, status, business_unit_id)
SELECT
'Mobile App',
'Customer mobile application',
'IN_DEVELOPMENT'::application_status,
id
FROM business_unit WHERE name = 'Digital Services';
-- Legacy System (Operations)
INSERT INTO application (name, description, status, business_unit_id, end_of_life_date)
SELECT
'Legacy Inventory System',
'Old inventory management system - to be decommissioned',
'MAINTENANCE'::application_status,
id,
'2026-06-30'::DATE
FROM business_unit WHERE name = 'Operations';
-- AI Analytics Platform (Digital Services)
INSERT INTO application (name, description, status, business_unit_id)
SELECT
'AI Analytics Platform',
'Machine learning based analytics platform',
'IDEA'::application_status,
id
FROM business_unit WHERE name = 'Digital Services';
</sql>
</changeSet>
</databaseChangeLog>