autocomit

This commit is contained in:
2026-02-08 15:52:23 +01:00
parent 7bfeb83fcb
commit e3d9ea1050
26 changed files with 1227 additions and 0 deletions
@@ -0,0 +1,82 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreateContactRequest;
import com.ldpv2.dto.response.ContactResponse;
import com.ldpv2.service.ContactService;
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.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/contacts")
@Tag(name = "Contacts", description = "Contact management endpoints")
@SecurityRequirement(name = "bearerAuth")
public class ContactController {
@Autowired
private ContactService contactService;
@PostMapping
@Operation(summary = "Create contact", description = "Create a new contact")
public ResponseEntity<ContactResponse> create(@Valid @RequestBody CreateContactRequest request) {
ContactResponse response = contactService.create(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@GetMapping("/{id}")
@Operation(summary = "Get contact", description = "Get contact by ID with persons")
public ResponseEntity<ContactResponse> getById(@PathVariable UUID id) {
ContactResponse response = contactService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List contacts", description = "Get all contacts")
public ResponseEntity<List<ContactResponse>> getAll() {
List<ContactResponse> response = contactService.findAll();
return ResponseEntity.ok(response);
}
@PostMapping("/{contactId}/persons/{personId}")
@Operation(summary = "Add person to contact", description = "Add a person to a contact")
public ResponseEntity<ContactResponse> addPerson(
@PathVariable UUID contactId,
@PathVariable UUID personId,
@RequestParam(defaultValue = "false") boolean isPrimary) {
ContactResponse response = contactService.addPerson(contactId, personId, isPrimary);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{contactId}/persons/{personId}")
@Operation(summary = "Remove person from contact", description = "Remove a person from a contact")
public ResponseEntity<ContactResponse> removePerson(
@PathVariable UUID contactId,
@PathVariable UUID personId) {
ContactResponse response = contactService.removePerson(contactId, personId);
return ResponseEntity.ok(response);
}
@PatchMapping("/{contactId}/persons/{personId}/primary")
@Operation(summary = "Set primary person", description = "Set a person as primary contact")
public ResponseEntity<ContactResponse> setPrimary(
@PathVariable UUID contactId,
@PathVariable UUID personId) {
ContactResponse response = contactService.setPrimary(contactId, personId);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete contact", description = "Delete a contact")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
contactService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,41 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreateContactRoleRequest;
import com.ldpv2.dto.response.ContactRoleResponse;
import com.ldpv2.service.ContactRoleService;
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;
@RestController
@RequestMapping("/contact-roles")
@Tag(name = "Contact Roles", description = "Contact role management endpoints")
@SecurityRequirement(name = "bearerAuth")
public class ContactRoleController {
@Autowired
private ContactRoleService contactRoleService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create contact role", description = "Create a new contact role (Admin only)")
public ResponseEntity<ContactRoleResponse> create(@Valid @RequestBody CreateContactRoleRequest request) {
ContactRoleResponse response = contactRoleService.create(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@GetMapping
@Operation(summary = "List contact roles", description = "Get all contact roles")
public ResponseEntity<List<ContactRoleResponse>> getAll() {
List<ContactRoleResponse> response = contactRoleService.findAll();
return ResponseEntity.ok(response);
}
}
@@ -0,0 +1,82 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreatePersonRequest;
import com.ldpv2.dto.request.UpdatePersonRequest;
import com.ldpv2.dto.response.PersonResponse;
import com.ldpv2.service.PersonService;
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("/persons")
@Tag(name = "Persons", description = "Person management endpoints")
@SecurityRequirement(name = "bearerAuth")
public class PersonController {
@Autowired
private PersonService personService;
@PostMapping
@Operation(summary = "Create person", description = "Create a new person")
public ResponseEntity<PersonResponse> create(@Valid @RequestBody CreatePersonRequest request) {
PersonResponse response = personService.create(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@Operation(summary = "Update person", description = "Update an existing person")
public ResponseEntity<PersonResponse> update(
@PathVariable UUID id,
@Valid @RequestBody UpdatePersonRequest request) {
PersonResponse response = personService.update(id, request);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get person", description = "Get person by ID")
public ResponseEntity<PersonResponse> getById(@PathVariable UUID id) {
PersonResponse response = personService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List persons", description = "Get paginated list of persons")
public ResponseEntity<Page<PersonResponse>> getAll(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "lastName") 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<PersonResponse> response = (name != null && !name.trim().isEmpty())
? personService.search(name, pageable)
: personService.findAll(pageable);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete person", description = "Delete a person")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
personService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,41 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.HashSet;
import java.util.Set;
/**
* Contact entity representing functional roles with associated persons
*/
@Data
@Entity
@Table(name = "contact")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true, exclude = {"contactPersons"})
public class Contact extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "contact_role_id", nullable = false)
private ContactRole contactRole;
@OneToMany(mappedBy = "contact", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<ContactPerson> contactPersons = new HashSet<>();
public void addPerson(Person person, boolean isPrimary) {
ContactPerson contactPerson = new ContactPerson();
contactPerson.setContact(this);
contactPerson.setPerson(person);
contactPerson.setPrimary(isPrimary);
contactPersons.add(contactPerson);
}
public void removePerson(Person person) {
contactPersons.removeIf(cp -> cp.getPerson().equals(person));
}
}
@@ -0,0 +1,47 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* Junction entity for Contact-Person many-to-many relationship
*/
@Data
@Entity
@Table(name = "contact_person")
@NoArgsConstructor
@AllArgsConstructor
public class ContactPerson implements Serializable {
@EmbeddedId
private ContactPersonId id = new ContactPersonId();
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("contactId")
@JoinColumn(name = "contact_id")
private Contact contact;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("personId")
@JoinColumn(name = "person_id")
private Person person;
@Column(name = "is_primary", nullable = false)
private boolean isPrimary = false;
@Embeddable
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ContactPersonId implements Serializable {
@Column(name = "contact_id")
private java.util.UUID contactId;
@Column(name = "person_id")
private java.util.UUID personId;
}
}
@@ -0,0 +1,25 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Contact Role entity representing functional roles
*/
@Data
@Entity
@Table(name = "contact_role")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ContactRole extends BaseEntity {
@Column(name = "role_name", nullable = false, unique = true, length = 100)
private String roleName;
@Column(columnDefinition = "TEXT")
private String description;
}
@@ -0,0 +1,31 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Person entity representing individuals
*/
@Data
@Entity
@Table(name = "person")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Person extends BaseEntity {
@Column(name = "first_name", nullable = false, length = 100)
private String firstName;
@Column(name = "last_name", nullable = false, length = 100)
private String lastName;
@Column(nullable = false, unique = true, length = 255)
private String email;
@Column(length = 50)
private String phone;
}
@@ -0,0 +1,25 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateContactRequest {
@NotNull(message = "Contact role is required")
private UUID contactRoleId;
@NotEmpty(message = "At least one person is required")
private List<UUID> personIds;
@NotNull(message = "Primary person must be specified")
private UUID primaryPersonId;
}
@@ -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 CreateContactRoleRequest {
@NotBlank(message = "Role name is required")
@Size(max = 100, message = "Role name must not exceed 100 characters")
private String roleName;
private String description;
}
@@ -0,0 +1,29 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreatePersonRequest {
@NotBlank(message = "First name is required")
@Size(max = 100, message = "First name must not exceed 100 characters")
private String firstName;
@NotBlank(message = "Last name is required")
@Size(max = 100, message = "Last name must not exceed 100 characters")
private String lastName;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@Size(max = 50, message = "Phone must not exceed 50 characters")
private String phone;
}
@@ -0,0 +1,25 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdatePersonRequest {
@Size(max = 100, message = "First name must not exceed 100 characters")
private String firstName;
@Size(max = 100, message = "Last name must not exceed 100 characters")
private String lastName;
@Email(message = "Email must be valid")
private String email;
@Size(max = 50, message = "Phone must not exceed 50 characters")
private String phone;
}
@@ -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.List;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ContactResponse {
private UUID id;
private ContactRoleResponse contactRole;
private List<PersonInContactResponse> persons;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,19 @@
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 ContactRoleResponse {
private UUID id;
private String roleName;
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,13 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PersonInContactResponse {
private PersonResponse person;
private boolean isPrimary;
}
@@ -0,0 +1,21 @@
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 PersonResponse {
private UUID id;
private String firstName;
private String lastName;
private String email;
private String phone;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,19 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.Contact;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface ContactRepository extends JpaRepository<Contact, UUID> {
@Query("SELECT c FROM Contact c JOIN FETCH c.contactRole LEFT JOIN FETCH c.contactPersons cp LEFT JOIN FETCH cp.person")
List<Contact> findAllWithDetails();
@Query("SELECT c FROM Contact c JOIN FETCH c.contactRole LEFT JOIN FETCH c.contactPersons cp LEFT JOIN FETCH cp.person WHERE c.id = :id")
Contact findByIdWithDetails(UUID id);
}
@@ -0,0 +1,14 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.ContactRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface ContactRoleRepository extends JpaRepository<ContactRole, UUID> {
Optional<ContactRole> findByRoleName(String roleName);
boolean existsByRoleName(String roleName);
}
@@ -0,0 +1,23 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.Person;
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.Optional;
import java.util.UUID;
@Repository
public interface PersonRepository extends JpaRepository<Person, UUID> {
Optional<Person> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT p FROM Person p WHERE " +
"LOWER(p.firstName) LIKE LOWER(CONCAT('%', :name, '%')) OR " +
"LOWER(p.lastName) LIKE LOWER(CONCAT('%', :name, '%'))")
Page<Person> findByName(@Param("name") String name, Pageable pageable);
}
@@ -0,0 +1,50 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.ContactRole;
import com.ldpv2.dto.request.CreateContactRoleRequest;
import com.ldpv2.dto.response.ContactRoleResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.repository.ContactRoleRepository;
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.stream.Collectors;
@Service
public class ContactRoleService {
@Autowired
private ContactRoleRepository contactRoleRepository;
@Transactional
public ContactRoleResponse create(CreateContactRoleRequest request) {
if (contactRoleRepository.existsByRoleName(request.getRoleName())) {
throw new BadRequestException("Contact role with name '" + request.getRoleName() + "' already exists");
}
ContactRole role = new ContactRole();
role.setRoleName(request.getRoleName());
role.setDescription(request.getDescription());
role = contactRoleRepository.save(role);
return mapToResponse(role);
}
public List<ContactRoleResponse> findAll() {
return contactRoleRepository.findAll().stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
private ContactRoleResponse mapToResponse(ContactRole role) {
return new ContactRoleResponse(
role.getId(),
role.getRoleName(),
role.getDescription(),
role.getCreatedAt(),
role.getUpdatedAt()
);
}
}
@@ -0,0 +1,166 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.Contact;
import com.ldpv2.domain.entity.ContactPerson;
import com.ldpv2.domain.entity.ContactRole;
import com.ldpv2.domain.entity.Person;
import com.ldpv2.dto.request.CreateContactRequest;
import com.ldpv2.dto.response.ContactResponse;
import com.ldpv2.dto.response.ContactRoleResponse;
import com.ldpv2.dto.response.PersonInContactResponse;
import com.ldpv2.dto.response.PersonResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.ContactRepository;
import com.ldpv2.repository.ContactRoleRepository;
import com.ldpv2.repository.PersonRepository;
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 ContactService {
@Autowired
private ContactRepository contactRepository;
@Autowired
private ContactRoleRepository contactRoleRepository;
@Autowired
private PersonRepository personRepository;
@Transactional
public ContactResponse create(CreateContactRequest request) {
ContactRole role = contactRoleRepository.findById(request.getContactRoleId())
.orElseThrow(() -> new ResourceNotFoundException(
"Contact role not found with id: " + request.getContactRoleId()));
if (!request.getPersonIds().contains(request.getPrimaryPersonId())) {
throw new BadRequestException("Primary person must be in the list of persons");
}
Contact contact = new Contact();
contact.setContactRole(role);
for (UUID personId : request.getPersonIds()) {
Person person = personRepository.findById(personId)
.orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + personId));
boolean isPrimary = personId.equals(request.getPrimaryPersonId());
contact.addPerson(person, isPrimary);
}
contact = contactRepository.save(contact);
return mapToResponse(contact);
}
public ContactResponse findById(UUID id) {
Contact contact = contactRepository.findByIdWithDetails(id);
if (contact == null) {
throw new ResourceNotFoundException("Contact not found with id: " + id);
}
return mapToResponse(contact);
}
public List<ContactResponse> findAll() {
return contactRepository.findAllWithDetails().stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
@Transactional
public ContactResponse addPerson(UUID contactId, UUID personId, boolean isPrimary) {
Contact contact = contactRepository.findById(contactId)
.orElseThrow(() -> new ResourceNotFoundException("Contact not found with id: " + contactId));
Person person = personRepository.findById(personId)
.orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + personId));
contact.addPerson(person, isPrimary);
contact = contactRepository.save(contact);
return mapToResponse(contact);
}
@Transactional
public ContactResponse removePerson(UUID contactId, UUID personId) {
Contact contact = contactRepository.findById(contactId)
.orElseThrow(() -> new ResourceNotFoundException("Contact not found with id: " + contactId));
Person person = personRepository.findById(personId)
.orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + personId));
contact.removePerson(person);
contact = contactRepository.save(contact);
return mapToResponse(contact);
}
@Transactional
public ContactResponse setPrimary(UUID contactId, UUID personId) {
Contact contact = contactRepository.findByIdWithDetails(contactId);
if (contact == null) {
throw new ResourceNotFoundException("Contact not found with id: " + contactId);
}
boolean personFound = false;
for (ContactPerson cp : contact.getContactPersons()) {
if (cp.getPerson().getId().equals(personId)) {
cp.setPrimary(true);
personFound = true;
} else {
cp.setPrimary(false);
}
}
if (!personFound) {
throw new ResourceNotFoundException("Person not found in contact");
}
contact = contactRepository.save(contact);
return mapToResponse(contact);
}
@Transactional
public void delete(UUID id) {
if (!contactRepository.existsById(id)) {
throw new ResourceNotFoundException("Contact not found with id: " + id);
}
contactRepository.deleteById(id);
}
private ContactResponse mapToResponse(Contact contact) {
ContactRoleResponse roleResponse = new ContactRoleResponse(
contact.getContactRole().getId(),
contact.getContactRole().getRoleName(),
contact.getContactRole().getDescription(),
contact.getContactRole().getCreatedAt(),
contact.getContactRole().getUpdatedAt()
);
List<PersonInContactResponse> personsResponse = contact.getContactPersons().stream()
.map(cp -> {
PersonResponse personResponse = new PersonResponse(
cp.getPerson().getId(),
cp.getPerson().getFirstName(),
cp.getPerson().getLastName(),
cp.getPerson().getEmail(),
cp.getPerson().getPhone(),
cp.getPerson().getCreatedAt(),
cp.getPerson().getUpdatedAt()
);
return new PersonInContactResponse(personResponse, cp.isPrimary());
})
.collect(Collectors.toList());
return new ContactResponse(
contact.getId(),
roleResponse,
personsResponse,
contact.getCreatedAt(),
contact.getUpdatedAt()
);
}
}
@@ -0,0 +1,102 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.Person;
import com.ldpv2.dto.request.CreatePersonRequest;
import com.ldpv2.dto.request.UpdatePersonRequest;
import com.ldpv2.dto.response.PersonResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.PersonRepository;
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 PersonService {
@Autowired
private PersonRepository personRepository;
@Transactional
public PersonResponse create(CreatePersonRequest request) {
if (personRepository.existsByEmail(request.getEmail())) {
throw new BadRequestException("Person with email '" + request.getEmail() + "' already exists");
}
Person person = new Person();
person.setFirstName(request.getFirstName());
person.setLastName(request.getLastName());
person.setEmail(request.getEmail());
person.setPhone(request.getPhone());
person = personRepository.save(person);
return mapToResponse(person);
}
@Transactional
public PersonResponse update(UUID id, UpdatePersonRequest request) {
Person person = personRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + id));
if (request.getFirstName() != null) {
person.setFirstName(request.getFirstName());
}
if (request.getLastName() != null) {
person.setLastName(request.getLastName());
}
if (request.getEmail() != null) {
if (!request.getEmail().equals(person.getEmail()) &&
personRepository.existsByEmail(request.getEmail())) {
throw new BadRequestException("Person with email '" + request.getEmail() + "' already exists");
}
person.setEmail(request.getEmail());
}
if (request.getPhone() != null) {
person.setPhone(request.getPhone());
}
person = personRepository.save(person);
return mapToResponse(person);
}
public PersonResponse findById(UUID id) {
Person person = personRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + id));
return mapToResponse(person);
}
public Page<PersonResponse> findAll(Pageable pageable) {
return personRepository.findAll(pageable).map(this::mapToResponse);
}
public Page<PersonResponse> search(String name, Pageable pageable) {
return personRepository.findByName(name, pageable).map(this::mapToResponse);
}
@Transactional
public void delete(UUID id) {
if (!personRepository.existsById(id)) {
throw new ResourceNotFoundException("Person not found with id: " + id);
}
personRepository.deleteById(id);
}
private PersonResponse mapToResponse(Person person) {
return new PersonResponse(
person.getId(),
person.getFirstName(),
person.getLastName(),
person.getEmail(),
person.getPhone(),
person.getCreatedAt(),
person.getUpdatedAt()
);
}
}
@@ -16,4 +16,7 @@
<!-- Story 2: Applications --> <!-- Story 2: Applications -->
<include file="db/changelog/v1.0/004-create-application-table.xml"/> <include file="db/changelog/v1.0/004-create-application-table.xml"/>
<!-- Story 3: Contacts -->
<include file="db/changelog/v1.0/005-create-contact-tables.xml"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,172 @@
<?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="005-create-contact-tables" author="ldpv2-team">
<!-- Contact Roles Table -->
<createTable tableName="contact_role">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="role_name" type="VARCHAR(100)">
<constraints nullable="false" unique="true"/>
</column>
<column name="description" type="TEXT"/>
<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>
<!-- Person Table -->
<createTable tableName="person">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="first_name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="last_name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="email" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
<column name="phone" type="VARCHAR(50)"/>
<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>
<!-- Contact Table -->
<createTable tableName="contact">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="contact_role_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_contact_role"
references="contact_role(id)"/>
</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>
<!-- Contact-Person Junction Table -->
<createTable tableName="contact_person">
<column name="contact_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_contact_person_contact"
references="contact(id)"
deleteCascade="true"/>
</column>
<column name="person_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_contact_person_person"
references="person(id)"
deleteCascade="true"/>
</column>
<column name="is_primary" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</createTable>
<!-- Indexes -->
<createIndex tableName="person" indexName="idx_person_email">
<column name="email"/>
</createIndex>
<createIndex tableName="contact" indexName="idx_contact_role">
<column name="contact_role_id"/>
</createIndex>
<addPrimaryKey tableName="contact_person"
columnNames="contact_id, person_id"
constraintName="pk_contact_person"/>
<!-- Insert default contact roles -->
<insert tableName="contact_role">
<column name="role_name" value="Product Owner"/>
<column name="description" value="Responsible for product vision and backlog"/>
</insert>
<insert tableName="contact_role">
<column name="role_name" value="Functional Authority"/>
<column name="description" value="Business decision maker"/>
</insert>
<insert tableName="contact_role">
<column name="role_name" value="Developer"/>
<column name="description" value="Software developer"/>
</insert>
<insert tableName="contact_role">
<column name="role_name" value="Maintainer"/>
<column name="description" value="System maintainer"/>
</insert>
<insert tableName="contact_role">
<column name="role_name" value="Technical Lead"/>
<column name="description" value="Technical team leader"/>
</insert>
<insert tableName="contact_role">
<column name="role_name" value="Business Analyst"/>
<column name="description" value="Requirements analyst"/>
</insert>
<insert tableName="contact_role">
<column name="role_name" value="Director"/>
<column name="description" value="Business unit director"/>
</insert>
<insert tableName="contact_role">
<column name="role_name" value="Program Manager"/>
<column name="description" value="Program management"/>
</insert>
<!-- Insert sample persons -->
<insert tableName="person">
<column name="first_name" value="John"/>
<column name="last_name" value="Doe"/>
<column name="email" value="john.doe@example.com"/>
<column name="phone" value="+1-555-0101"/>
</insert>
<insert tableName="person">
<column name="first_name" value="Jane"/>
<column name="last_name" value="Smith"/>
<column name="email" value="jane.smith@example.com"/>
<column name="phone" value="+1-555-0102"/>
</insert>
<insert tableName="person">
<column name="first_name" value="Bob"/>
<column name="last_name" value="Johnson"/>
<column name="email" value="bob.johnson@example.com"/>
<column name="phone" value="+1-555-0103"/>
</insert>
<insert tableName="person">
<column name="first_name" value="Alice"/>
<column name="last_name" value="Williams"/>
<column name="email" value="alice.williams@example.com"/>
<column name="phone" value="+1-555-0104"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,53 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Contact, ContactRole, CreateContactRequest, CreateContactRoleRequest } from '../../shared/models/contact.model';
@Injectable({
providedIn: 'root'
})
export class ContactService {
private readonly CONTACT_URL = '/api/contacts';
private readonly ROLE_URL = '/api/contact-roles';
constructor(private http: HttpClient) {}
// Contact Roles
getContactRoles(): Observable<ContactRole[]> {
return this.http.get<ContactRole[]>(this.ROLE_URL);
}
createContactRole(data: CreateContactRoleRequest): Observable<ContactRole> {
return this.http.post<ContactRole>(this.ROLE_URL, data);
}
// Contacts
getContacts(): Observable<Contact[]> {
return this.http.get<Contact[]>(this.CONTACT_URL);
}
getContact(id: string): Observable<Contact> {
return this.http.get<Contact>(`${this.CONTACT_URL}/${id}`);
}
createContact(data: CreateContactRequest): Observable<Contact> {
return this.http.post<Contact>(this.CONTACT_URL, data);
}
addPersonToContact(contactId: string, personId: string, isPrimary: boolean = false): Observable<Contact> {
const params = new HttpParams().set('isPrimary', isPrimary.toString());
return this.http.post<Contact>(`${this.CONTACT_URL}/${contactId}/persons/${personId}`, null, { params });
}
removePersonFromContact(contactId: string, personId: string): Observable<Contact> {
return this.http.delete<Contact>(`${this.CONTACT_URL}/${contactId}/persons/${personId}`);
}
setPrimaryPerson(contactId: string, personId: string): Observable<Contact> {
return this.http.patch<Contact>(`${this.CONTACT_URL}/${contactId}/persons/${personId}/primary`, null);
}
deleteContact(id: string): Observable<void> {
return this.http.delete<void>(`${this.CONTACT_URL}/${id}`);
}
}
@@ -0,0 +1,50 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Person, CreatePersonRequest, UpdatePersonRequest } from '../../shared/models/contact.model';
import { Page } from '../../shared/models/environment.model';
@Injectable({
providedIn: 'root'
})
export class PersonService {
private readonly API_URL = '/api/persons';
constructor(private http: HttpClient) {}
getPersons(
name?: string,
page: number = 0,
size: number = 20,
sortBy: string = 'lastName',
sortDirection: string = 'asc'
): Observable<Page<Person>> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sortBy', sortBy)
.set('sortDirection', sortDirection);
if (name && name.trim()) {
params = params.set('name', name.trim());
}
return this.http.get<Page<Person>>(this.API_URL, { params });
}
getPerson(id: string): Observable<Person> {
return this.http.get<Person>(`${this.API_URL}/${id}`);
}
createPerson(data: CreatePersonRequest): Observable<Person> {
return this.http.post<Person>(this.API_URL, data);
}
updatePerson(id: string, data: UpdatePersonRequest): Observable<Person> {
return this.http.put<Person>(`${this.API_URL}/${id}`, data);
}
deletePerson(id: string): Observable<void> {
return this.http.delete<void>(`${this.API_URL}/${id}`);
}
}
@@ -0,0 +1,55 @@
export interface ContactRole {
id: string;
roleName: string;
description?: string;
createdAt: Date;
updatedAt: Date;
}
export interface Person {
id: string;
firstName: string;
lastName: string;
email: string;
phone?: string;
createdAt: Date;
updatedAt: Date;
}
export interface PersonInContact {
person: Person;
isPrimary: boolean;
}
export interface Contact {
id: string;
contactRole: ContactRole;
persons: PersonInContact[];
createdAt: Date;
updatedAt: Date;
}
export interface CreatePersonRequest {
firstName: string;
lastName: string;
email: string;
phone?: string;
}
export interface UpdatePersonRequest {
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
}
export interface CreateContactRequest {
contactRoleId: string;
personIds: string[];
primaryPersonId: string;
}
export interface CreateContactRoleRequest {
roleName: string;
description?: string;
}