Skip to content

Engineering - Feedback Request and Feedback Response

Overview

In this sprint, we will develop the module where the collaborator creates their feedback questionnaire with three or more questions and selects one or more external appraisers (collaborators from CI&T clients to whom they are allocated). Once the questionnaire is created, it is sent for PDM approval. If approved, the appraisers automatically receive an email requesting them to respond to the evaluation, accompanied by a link to it. Once the questionnaire is answered, the PDM can view the responses with the data of who responded, as can the collaborator. However, the collaborator cannot see the data of who responded, thus configuring an anonymous view for the collaborator. It is important to note that the questionnaire is valid for 3 months from its approval by the PDM.

mmd

1. Domain Understanding

This module requires the creation of a slightly more robust domain than the previous one. Additionally, it has a state machine to be managed so that the status of the request can be tracked by the involved users. It is very important to remember that status fields are rarely necessary when we have other data in the entity that can be inferred to determine the current state of the request. This way, we do not break the third normal form (3NF - Do not persist fields that can be calculated from other fields), which aims to remove redundant fields that can lead to inconsistent states:

«Entity»FeedbackRequestid: UUIDrequester_id: LongcreatedAt: DateTimeapprovedAt: DateTimerejectedAt: DateTimeeditedAt: DateTimeappraisers: List<Appraiser>isApproved(): booleanisRejected(): booleanisEdited(): booleanisExpired(): boolean«ValueObject»Appraisername: stringemail: stringrespondedAt: DateTimeisResponded(): boolean

This approach avoids an inconsistent state by not using status fields with enums or booleans, and instead, defines methods that encapsulate the state logic within the class itself. Let's explore this approach with examples and explanations.

Example

class FeedbackRequest {
    public boolean isApproved() {
        return approvedAt != null && (rejectedAt == null || approvedAt.isAfter(rejectedAt));
    }

    public boolean isRejected() {
        return rejectedAt != null && (approvedAt == null || rejectedAt.isAfter(approvedAt));
    }

    public boolean isEdited() {
        return editedAt != null;
    }

    public boolean isExpired() {
        return createdAt.plusMonths(3).isBeforeNow();
    }
}

class Avaliator {
    public boolean isResponded() {
        return respondedAt != null;
    }
}

Observations

  1. Avoids State Ambiguity:

    • Instead of using a status field (like a STATUS enum with values PENDING, APPROVED, REJECTED), the approval and rejection logic is encapsulated in methods. This prevents inconsistent states, such as a feedback that could be simultaneously "approved" and "rejected" if the fields are not managed correctly.
    • For example, in the isApproved() method, we check if approvedAt is not null and ensure that if rejectedAt is also set, the approval occurred before the rejection. This ensures that only one state can be true at a time.
  2. Ease of Maintenance:

    • The business logic is centralized within the entity, making maintenance easier. If there is a need to change the validation logic, it can be done in a single place, instead of modifying status handling in various parts of the code.
  3. Complexity in HQL Queries:

    • A disadvantage of this approach is that, by not using status fields, HQL queries become more complex. To filter objects based on calculated states, you must use the fields stored in the database.
    • For example, when querying for approved feedbacks, the logic must be incorporated into the HQL query:
    List<FeedbackRequest> approvedRequests = session.createQuery(
        "FROM FeedbackRequest WHERE approvedAt IS NOT NULL AND (rejectedAt IS NULL OR approvedAt > rejectedAt)", 
        FeedbackRequest.class).list();
    

Is Appraiser a Value Object?

The Avaliator (Appraiser) is an example of a Value Object, which encapsulates a set of attributes that define an appraiser. It has no identity of its own (i.e., two appraisers with the same name and email are considered equal), and its value is based solely on its properties. However, it is important to note that it can also be an aggregate entity for performance reasons. In this case, it will have an ID only to serve as a foreign key, since the same appraiser can appear multiple times in different feedback requests. Although this approach is not optimal in relation to DDD, it brings advantages in indexing, searching, and grouping within a relational database.

Decision Between Value Object and Aggregate Entity

An aggregate entity is an entity that is only a dependency of another, meaning it does not need direct access via a repository, service, or controller; its data will always be manipulated only through the endpoints of its aggregating entity.

Imagine the following PUT to the feedback requests endpoint; this data should update the request and the question in question.

{
    "id": 1,
    ...
    "perguntas": [
        {
            "id": 1,
            "text": "evaluate my performance"
        }
    ]
}

Note: This approach can be poor in a context where the associated collection is very large, but in this scenario where we will have few questions, it can be handled this way without major complications.

JPA Cascade

Remember that to get the expected result in this type of situation, it is necessary to use the appropriate cascade in the JPA mapping, such as:

@OneToMany(mappedBy = "request", cascade = CascadeType.ALL)
private List<Pergunta> perguntas

More details here

User Integration with Feedback Request

The LoggedUser class is part of the security library and is used to represent an authenticated user in the system. When a request receives a JWT (JSON Web Token), a security filter extracts the data from this token and populates an instance of the LoggedUser class.

This documentation focuses on how to use the LoggedUser class to populate entities that depend on user information, especially the user ID, avoiding the need to map a complete user entity.

LoggedUser Class Structure

Attributes

  • Long id: Unique user identifier.
  • String name: User's name.
  • String email: User's email address.
  • String type: User type (e.g., role or access profile).
  • Long PDM: An additional identifier associated with the user.

Using the LoggedUser Class to Populate Entities

After the LoggedUser instance is populated with data from the JWT token, which is done automatically by the security library, you can use it to populate entities that require the authenticated user's information. Below are examples of how to use LoggedUser to populate these entities.

1. Example of an Entity Dependent on User ID

Suppose you have an entity called FeedbackRequest that stores information about feedback requests made by users.

@Entity
public class FeedbackRequest {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long userId; // ID of the collaborator making the request
    private String message;

    // Getters and Setters
}

2. Creating a Feedback Request Using LoggedUser

You can use the LoggedUser class to populate the FeedbackRequest entity when creating a new request.

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Service
public class FeedbackService {

    public FeedbackRequest createFeedbackRequest(String message) {
        // Retrieves the LoggedUser instance from the security context
        LoggedUser loggedUser = (LoggedUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        // Creates a new feedback request
        FeedbackRequest feedbackRequest = new FeedbackRequest();
        feedbackRequest.setUserId(loggedUser.getId()); // Uses the ID of the authenticated user
        feedbackRequest.setMessage(message);

        // Save feedbackRequest to the repository (not shown here)
        // feedbackRequestRepository.save(feedbackRequest);

        return feedbackRequest;
    }
}

3. Advantages of Using LoggedUser

  • Simplicity: Facilitates access to the authenticated user's ID without the need to map a complete user entity.
  • Decoupling: Keeps security logic separate from business logic, improving code organization.
  • Efficiency: Reduces the need for database calls to get user information by using the data already available in the JWT token.

The LoggedUser class is an effective tool for managing authenticated user information in applications that use JWT. By using this class, you can easily populate entities that depend on the user ID, simplifying the code and improving system efficiency. This avoids the need to map unnecessary user entities and keeps your code clean and easy to maintain. More details on integration between microservices here

Email Sending

The moment a PDM approves a request, an email must be sent to the appraisers. This email must contain the link to the form response, which will be the appraiser's "login" screen to be created in sprint 3. It will be necessary to plan the route for this screen in order to send it in the email.

Note

Use the same email system used in sprint 1

Reference Guide

Here are the most important documents from this documentation to be read for this sprint:

  1. Relationship Between Microservices
  2. Sending Emails with Spring

References

  1. 3NF (Third Normal Form)

  2. JPA Cascade

  3. Aggregate Entity

  4. Value Object

  5. CORS (Cross-Origin Resource Sharing)

  6. Spring Email

Prompt for AI

Please generate a Java code example using JPA that demonstrates the implementation of the following concepts:

1. **3NF**: Structure the entities so that they are normalized to the third normal form.
2. **JPA Cascade**: Use cascade types to manage the persistence of related entities.
3. **Aggregate Entity**: Show how to define an aggregate entity that encapsulates business rules.
4. **Value Object**: Create an example of a Value Object that represents an immutable concept.
5. **CORS**: Configure a REST endpoint to allow CORS.
6. **DB Weak External Key**: Include a weak entity that depends on a strong entity.

Make sure the code is well-commented and follows best development practices.