Skip to content

Commit

Permalink
feat: fix rollback policy for the case where consultant cannot be cre…
Browse files Browse the repository at this point in the history
…ated in appointment service
  • Loading branch information
tkuzynow committed Jan 9, 2024
1 parent d8e0794 commit a5f0fbc
Show file tree
Hide file tree
Showing 23 changed files with 220 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import de.caritas.cob.userservice.api.exception.httpresponses.ConflictException;
import de.caritas.cob.userservice.api.exception.httpresponses.CreateEnquiryMessageException;
import de.caritas.cob.userservice.api.exception.httpresponses.CustomValidationHttpStatusException;
import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException;
import de.caritas.cob.userservice.api.exception.httpresponses.ForbiddenException;
import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException;
import de.caritas.cob.userservice.api.exception.httpresponses.NoContentException;
Expand Down Expand Up @@ -55,6 +56,14 @@ public ResponseEntity<Object> handleJPAConstraintViolationException(
return handleExceptionInternal(ex, null, new HttpHeaders(), HttpStatus.CONFLICT, request);
}

@ExceptionHandler({DistributedTransactionException.class})
public ResponseEntity<Object> handleDistributedTransactionException(
final DistributedTransactionException ex, final WebRequest request) {
log.error("Distributed transaction failed to complete", ex);
return handleExceptionInternal(
ex, null, ex.getCustomHttpHeaders(), HttpStatus.FAILED_DEPENDENCY, request);
}

@ExceptionHandler({CreateEnquiryMessageException.class})
public ResponseEntity<Object> handleCreateEnquiryMessageException(
final CreateEnquiryMessageException ex, final WebRequest request) {
Expand Down Expand Up @@ -88,7 +97,8 @@ public ResponseEntity<Object> handleCustomBadRequest(
final CustomValidationHttpStatusException ex, final WebRequest request) {
ex.executeLogging();

return handleExceptionInternal(ex, null, ex.getCustomHttpHeader(), ex.getHttpStatus(), request);
return handleExceptionInternal(
ex, null, ex.getCustomHttpHeaders(), ex.getHttpStatus(), request);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import de.caritas.cob.userservice.api.admin.service.consultant.create.ConsultantCreatorService;
import de.caritas.cob.userservice.api.admin.service.consultant.delete.ConsultantPreDeletionService;
import de.caritas.cob.userservice.api.admin.service.consultant.update.ConsultantUpdateService;
import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionException;
import de.caritas.cob.userservice.api.exception.httpresponses.DistributedTransactionInfo;
import de.caritas.cob.userservice.api.exception.httpresponses.NoContentException;
import de.caritas.cob.userservice.api.exception.httpresponses.NotFoundException;
import de.caritas.cob.userservice.api.helper.AuthenticatedUser;
Expand All @@ -23,10 +25,12 @@
import de.caritas.cob.userservice.api.port.out.ConsultantRepository;
import de.caritas.cob.userservice.api.port.out.SessionRepository;
import de.caritas.cob.userservice.api.service.appointment.AppointmentService;
import java.util.List;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;

/** Service class for admin operations on {@link Consultant} objects. */
@Service
Expand Down Expand Up @@ -69,14 +73,36 @@ public ConsultantAdminResponseDTO findConsultantById(String consultantId) {
* @return the generated and persisted {@link Consultant} representation as {@link
* ConsultantAdminResponseDTO}
*/
public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO createConsultantDTO) {
public ConsultantAdminResponseDTO createNewConsultant(CreateConsultantDTO createConsultantDTO)
throws DistributedTransactionException {
Consultant newConsultant =
this.consultantCreatorService.createNewConsultant(createConsultantDTO);
List<TransactionalStep> completedSteps =
Lists.newArrayList(
TransactionalStep.CREATE_ACCOUNT_IN_KEYCLOAK,
TransactionalStep.CREATE_ACCOUNT_IN_ROCKETCHAT,
TransactionalStep.CREATE_CONSULTANT_IN_MARIADB);

ConsultantAdminResponseDTO consultantAdminResponseDTO =
ConsultantResponseDTOBuilder.getInstance(newConsultant).buildResponseDTO();

this.appointmentService.createConsultant(consultantAdminResponseDTO);
try {
this.appointmentService.createConsultant(consultantAdminResponseDTO);
} catch (RestClientException e) {
log.error(
"User with id {}, who has roles {}, has created a consultant with id {} but the appointment service returned an error: {}",
authenticatedUser.getUserId(),
authenticatedUser.getRoles(),
newConsultant.getId(),
e.getMessage());
this.consultantCreatorService.rollbackCreateNewConsultant(newConsultant);
throw new DistributedTransactionException(
e,
new DistributedTransactionInfo(
"createNewConsultant",
completedSteps,
TransactionalStep.CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE));
}

return consultantAdminResponseDTO;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.caritas.cob.userservice.api.admin.service.consultant;

public enum TransactionalStep {
CREATE_ACCOUNT_IN_KEYCLOAK,
CREATE_CONSULTANT_IN_MARIADB,

CREATE_ACCOUNT_IN_ROCKETCHAT,

CREATE_ACCOUNT_IN_CALCOM_OR_APPOINTMENTSERVICE
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import de.caritas.cob.userservice.api.exception.httpresponses.InternalServerErrorException;
import de.caritas.cob.userservice.api.exception.httpresponses.customheader.HttpStatusExceptionReason;
import de.caritas.cob.userservice.api.exception.rocketchat.RocketChatLoginException;
import de.caritas.cob.userservice.api.facade.rollback.RollbackFacade;
import de.caritas.cob.userservice.api.helper.AuthenticatedUser;
import de.caritas.cob.userservice.api.helper.UserHelper;
import de.caritas.cob.userservice.api.helper.UsernameTranscoder;
Expand Down Expand Up @@ -52,6 +53,8 @@ public class ConsultantCreatorService {
private final @NonNull UserAccountInputValidator userAccountInputValidator;
private final @NonNull TenantAdminService tenantAdminService;

private final @NonNull RollbackFacade rollbackFacade;

@Value("${multitenancy.enabled}")
private boolean multiTenancyEnabled;

Expand Down Expand Up @@ -235,4 +238,9 @@ private void addGroupChatConsultantRole(
roles.add(GROUP_CHAT_CONSULTANT.getValue());
}
}

public void rollbackCreateNewConsultant(Consultant newConsultant) {
log.info("Rollback creation of consultant with id {}", newConsultant.getId());
rollbackFacade.rollbackConsultantAccount(newConsultant);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@Getter
public class CustomValidationHttpStatusException extends CustomHttpStatusException {

private final HttpHeaders customHttpHeader;
private final HttpHeaders customHttpHeaders;
private final HttpStatus httpStatus;

/**
Expand All @@ -21,7 +21,7 @@ public class CustomValidationHttpStatusException extends CustomHttpStatusExcepti
*/
public CustomValidationHttpStatusException(HttpStatusExceptionReason httpStatusExceptionReason) {
super();
this.customHttpHeader = new CustomHttpHeader(httpStatusExceptionReason).buildHeader();
this.customHttpHeaders = new CustomHttpHeader(httpStatusExceptionReason).buildHeader();
this.httpStatus = HttpStatus.BAD_REQUEST;
}

Expand All @@ -35,7 +35,7 @@ public CustomValidationHttpStatusException(HttpStatusExceptionReason httpStatusE
public CustomValidationHttpStatusException(
HttpStatusExceptionReason reason, HttpStatus httpStatus) {
super();
this.customHttpHeader = new CustomHttpHeader(reason).buildHeader();
this.customHttpHeaders = new CustomHttpHeader(reason).buildHeader();
this.httpStatus = httpStatus;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.caritas.cob.userservice.api.exception.httpresponses;

import de.caritas.cob.userservice.api.service.LogService;
import org.springframework.http.HttpHeaders;

public class DistributedTransactionException extends CustomHttpStatusException {

private final HttpHeaders customHttpHeaders;

public DistributedTransactionException(
Exception e, DistributedTransactionInfo distributedTransactionInfo) {
super(
getFormattedMessageWithDistributedTransactionInfo(distributedTransactionInfo),
e,
LogService::logError);
this.customHttpHeaders =
buildCustomHeaders(
"DISTRIBUTED_TRANSACTION_FAILED_ON_STEP_"
+ distributedTransactionInfo.getFailedStep().name());
}

private HttpHeaders buildCustomHeaders(String message) {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Reason", message);
return headers;
}

private static String getFormattedMessageWithDistributedTransactionInfo(
DistributedTransactionInfo distributedTransactionInfo) {
return String.format(
"Distributed transaction %s failed. Completed transactional operations: %s. Failed step: %s",
distributedTransactionInfo.getName(),
distributedTransactionInfo.getCompletedTransactionalOperations(),
distributedTransactionInfo.getFailedStep());
}

public HttpHeaders getCustomHttpHeaders() {
return customHttpHeaders;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.caritas.cob.userservice.api.exception.httpresponses;

import de.caritas.cob.userservice.api.admin.service.consultant.TransactionalStep;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Data
@Builder
@AllArgsConstructor
public class DistributedTransactionInfo {

String name;
List<TransactionalStep> completedTransactionalOperations;
TransactionalStep failedStep;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,46 @@

import static java.util.Objects.nonNull;

import de.caritas.cob.userservice.api.model.Consultant;
import de.caritas.cob.userservice.api.port.out.IdentityClient;
import de.caritas.cob.userservice.api.service.UserAgencyService;
import de.caritas.cob.userservice.api.service.session.SessionService;
import de.caritas.cob.userservice.api.service.user.UserService;
import de.caritas.cob.userservice.api.workflow.delete.model.DeletionWorkflowError;
import de.caritas.cob.userservice.api.workflow.delete.service.DeleteUserAccountService;
import java.time.LocalDateTime;
import java.util.List;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/*
* Facade for capsuling the steps to roll back an user account.
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RollbackFacade {

private final @NonNull IdentityClient identityClient;
private final @NonNull UserAgencyService userAgencyService;
private final @NonNull SessionService sessionService;
private final @NonNull UserService userService;

private final @NonNull DeleteUserAccountService deleteUserAccountService;

public void rollbackConsultantAccount(Consultant consultant) {
log.info("Rollback consultant account: {}", consultant);
consultant.setDeleteDate(LocalDateTime.now());
List<DeletionWorkflowError> deletionWorkflowErrors =
deleteUserAccountService.performConsultantDeletion(consultant);
if (nonNull(deletionWorkflowErrors) && !deletionWorkflowErrors.isEmpty()) {

deletionWorkflowErrors.stream()
.forEach(e -> log.error("Consultant delete workflow error: ", e));
}
}
/**
* Deletes the provided user in Keycloak, MariaDB and its related session or user-chat/agency
* relations depending on the provided {@link RollbackUserAccountInformation}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,18 @@ public static void logInfo(Exception exception) {
LOGGER.info(getStackTrace(exception));
}

public static void logError(Exception exception) {
LOGGER.error(getStackTrace(exception));
}

/**
* Logs an warning message.
*
* @param exception The exception
*/
public static void logWarn(Exception exception) {
LOGGER.warn(getStackTrace(exception));
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(getStackTrace(exception));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.caritas.cob.userservice.api.service.appointment;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.caritas.cob.userservice.api.adapters.web.dto.ConsultantAdminResponseDTO;
Expand Down Expand Up @@ -28,6 +29,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClientException;

/** Service class to communicate with the AppointmentService. */
@Component
Expand All @@ -52,7 +54,8 @@ public class AppointmentService {
@Value("${feature.appointment.enabled}")
private boolean appointmentFeatureEnabled;

public void createConsultant(ConsultantAdminResponseDTO consultantAdminResponseDTO) {
public void createConsultant(ConsultantAdminResponseDTO consultantAdminResponseDTO)
throws RestClientException {
if (!appointmentFeatureEnabled) {
return;
}
Expand All @@ -62,29 +65,38 @@ public void createConsultant(ConsultantAdminResponseDTO consultantAdminResponseD
ConsultantApi appointmentConsultantApi =
this.appointmentConsultantServiceApiControllerFactory.createControllerApi();
addTechnicalUserHeaders(appointmentConsultantApi.getApiClient());
try {
de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO consultant =
mapper.readValue(
mapper.writeValueAsString(consultantAdminResponseDTO.getEmbedded()),
de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO
.class);
appointmentConsultantApi.createConsultant(consultant);
} catch (Exception e) {
log.error(e.getMessage());
}
de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO consultant =
getConsultantDTO(consultantAdminResponseDTO, mapper);
appointmentConsultantApi.createConsultant(consultant);
}
}

private de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO
getConsultantDTO(ConsultantAdminResponseDTO consultantAdminResponseDTO, ObjectMapper mapper) {
de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO consultant =
null;
try {
consultant =
mapper.readValue(
mapper.writeValueAsString(consultantAdminResponseDTO.getEmbedded()),
de.caritas.cob.userservice.appointmentservice.generated.web.model.ConsultantDTO
.class);
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
return consultant;
}

public void syncConsultantData(Consultant consultant) {
ConsultantAdminResponseDTO ConsultantAdminResponseDTO = new ConsultantAdminResponseDTO();
ConsultantAdminResponseDTO consultantAdminResponseDTO = new ConsultantAdminResponseDTO();
ConsultantDTO consultantEmbeded = new ConsultantDTO();
consultantEmbeded.setId(consultant.getId());
consultantEmbeded.setFirstname(consultant.getFirstName());
consultantEmbeded.setLastname(consultant.getLastName());
consultantEmbeded.setEmail(consultant.getEmail());
consultantEmbeded.setAbsent(consultant.isAbsent());
ConsultantAdminResponseDTO.setEmbedded(consultantEmbeded);
updateConsultant(ConsultantAdminResponseDTO);
consultantAdminResponseDTO.setEmbedded(consultantEmbeded);
updateConsultant(consultantAdminResponseDTO);
}

public void updateConsultant(ConsultantAdminResponseDTO consultantAdminResponseDTO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;

@Data
@Builder
@ToString
public class DeletionWorkflowError {

private DeletionSourceType deletionSourceType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private List<DeletionWorkflowError> deleteConsultantsAndCollectPossibleErrors()
.collect(Collectors.toList());
}

private List<DeletionWorkflowError> performConsultantDeletion(Consultant consultant) {
public List<DeletionWorkflowError> performConsultantDeletion(Consultant consultant) {

var deletionWorkflowDTO = new ConsultantDeletionWorkflowDTO(consultant, new ArrayList<>());

Expand Down
Loading

0 comments on commit a5f0fbc

Please sign in to comment.