UserServiceSupport.java
package com.dmasone.identity.service;
import com.dmasone.identity.domain.model.User;
import com.dmasone.identity.domain.model.UserStatus;
import com.dmasone.identity.domain.repository.UserRepository;
import com.dmasone.identity.infrastructure.exception.EmailAlreadyExistsException;
import com.dmasone.identity.infrastructure.exception.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
/**
* Shared persistence-aware rules used by both API versions.
*/
@Component
@RequiredArgsConstructor
class UserServiceSupport {
private static final String EMAIL_EXISTS_MESSAGE = "Email already exists";
private static final String UNIQUE_VIOLATION_SQL_STATE = "23505";
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
User create(User user, String rawPassword) {
if (userRepository.existsByEmail(user.getEmail())) {
throw new EmailAlreadyExistsException(EMAIL_EXISTS_MESSAGE);
}
Instant now = currentTimestamp();
user.setPasswordHash(passwordEncoder.encode(rawPassword));
user.setStatus(UserStatus.ACTIVE);
user.setCreatedAt(now);
user.setUpdatedAt(now);
try {
return userRepository.saveAndFlush(user);
} catch (DataIntegrityViolationException exception) {
if (isUniqueConstraintViolation(exception)) {
throw new EmailAlreadyExistsException(EMAIL_EXISTS_MESSAGE, exception);
}
throw exception;
}
}
User getRequired(UUID id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
User saveChanged(User user) {
user.setUpdatedAt(currentTimestamp());
return userRepository.save(user);
}
private Instant currentTimestamp() {
// PostgreSQL stores timestamps at microsecond precision; matching it keeps
// write responses stable when the same entity is read back.
return Instant.now().truncatedTo(ChronoUnit.MICROS);
}
private boolean isUniqueConstraintViolation(Throwable exception) {
Throwable current = exception;
while (current != null) {
if (current instanceof SQLException sqlException
&& UNIQUE_VIOLATION_SQL_STATE.equals(sqlException.getSQLState())) {
return true;
}
Throwable cause = current.getCause();
current = cause == current ? null : cause;
}
return false;
}
}