> ## Documentation Index
> Fetch the complete documentation index at: https://docs.activeviam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add custom validation

> How to add custom validation rules for sign-off definitions and adjustments in Atoti Sign-Off, with built-in and custom validator options for both creation and update operations.

## Overview

We expect users to utilize the `ISignOffDefinitionService` and `IAdjustmentService` to perform transactional CRUD operations. These services come with
built-in validation constraint handling using [Java Bean Validation](https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.).
However, you can customize the validation performed when creating or updating sign-off definitions and adjustments
by adding beans that implement the `ISignOffDefinitionValidator` and `IAdjustmentValidator` interfaces, respectively.

<Info>
  We expect Sign-Off tasks to be created or updated via the workflow, hence why they are not included in the list of services that can be customized.
</Info>

## How it works

Atoti Sign-Off uses [custom constraint annotations](https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.html#validation-beanvalidation-spring-constraints)
to validate the input of the services. For example, when creating a Sign-Off definition, the validation works as follows:

1. The input parameter of `ISignOffDefinitionService::createSignOffDefinition` is annotated with `@SignOffDefinitionConstraint(type = CREATION)`.
   This tells
   Spring: when passing an `ISignOffDefinition` to this method, use the custom `@SignOffDefinitionConstraint` annotation with the type `CREATION` to validate the `ISignOffDefinition`.
   This means that if you include your own implementation of `ISignOffDefinitionService`, the constraint will automatically be applied.
2. `@SignOffDefinitionConstraint` is annotated with `@Constraint(validatedBy = SignOffDefinitionConstraintValidator.class)`.
   This tells Spring: when you see the
   `@SignOffDefinitionConstraint` annotation, use the `SignOffDefinitionConstraintValidator` to validate the input.
3. The `SignOffDefinitionConstraintValidator` class implements Jakarta’s `ConstraintValidator` interface, which is used to validate the input. The `isValid`
   method is called when the input is passed to the method. This method calls the `ISignOffDefinitionValidator` implementation to actually validate the input.

<Note>
  You can expose your own implementation of `SignOffDefinitionConstraintValidator` to override the `isValid` method, but in order to do so,
  you must extend the concrete class `SignOffDefinitionConstraintValidator` so that your implementation can be picked up by `@SignOffDefinitionConstraint`.
  The Jakarta validation API requires a concrete class to be used as the validator, so we cannot use an interface.
</Note>

4. The `ISignOffDefinitionValidator` implementation is used to validate the input. If you want to add your own custom validation logic, expose an
   implementation of this interface.

## Default validation

We perform the following validations out-of-the-box:

### Sign-Off definitions

#### On creation

The default `ISignOffDefinitionValidator` validates the following:

* The `id` field is not populated.
* The `scope` field does not overlap with another definition’s scope. See the [overlapping scopes](../../user-ref/configure-tasks/create-task/create-task-ui#overlapping-scopes) section for more details.

#### On update

The default `ISignOffDefinitionValidator` validates the following:

* The `id` field is populated.
* The `name` field has not changed, as it is required as a key for the workflow.
* The `startDate` field has not changed, as it is required as a key for the workflow.
* The `scope` field does not overlap with another definition’s scope. See the [overlapping scopes](../../user-ref/configure-tasks/create-task/create-task-ui#overlapping-scopes) section for
  more details.

### Sign-Off adjustments

#### On creation

The default `ISignOffAdjustmentValidator` validates the following:

* The `id` field is not populated.

#### On update

The default `ISignOffAdjustmentValidator` validates the following:

* The `id` field is populated.

## Custom validation

To implement your own custom validation you must expose an implementation of `ISignOffDefinitionValidator` or `IAdjustmentValidator` as a Spring Bean. This will
then replace our default validation logic. You may extend our logic by letting your implementation extend `SignOffDefinitionValidator` or `AdjustmentValidator`
respectively.

### Sign-Off definitions

To provide custom validation for sign-off definitions, create a class that implements the `ISignOffDefinitionValidator` interface and override its methods. The
interface is as follows:

```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
/**
 * <b>ISignOffDefinitionValidator</b>
 *
 * <p> The validator used for validating Atoti Sign-Off Definitions.
 *
 * @author ActiveViam
 */
public interface ISignOffDefinitionValidator {

    /**
     * @param definitionToCreate the definition to be created
     * @return validation errors if the definition is not valid for creation, empty list otherwise
     */
    List<IValidationError> validateSignOffDefinitionCreation(ISignOffDefinition definitionToCreate);

    /**
     * @param definitionToUpdate the definition to be updated
     * @return validation errors if the definition is not valid for update, empty list otherwise
     */
    List<IValidationError> validateSignOffDefinitionUpdate(ISignOffDefinition definitionToUpdate);
}
```

A sample implementation of `ISignOffDefinitionValidator` could look like this:

```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
public class MyCustomSignOffDefinitionValidator implements ISignOffDefinitionValidator {

    List<IValidationError> validateSignOffDefinitionCreation(ISignOffDefinition definitionToCreate) {
        if (definitionToCreate.getName() == null){
            return List.of(DefaultValidationError.builder()
                    .source(definitionToCreate)
                    .reason("The definition name cannot be null")
                    .action("Please ensure the name field is populated.")
                    .build());
        }
        return Collections.emptyList();
    }

    List<IValidationError> validateSignOffDefinitionUpdate(ISignOffDefinition definitionToUpdate) {
        if (definitionToUpdate.getName() == null) {
            return List.of(DefaultValidationError.builder()
                    .source(definitionToUpdate)
                    .reason("The updated definition name cannot be null")
                    .action("Please ensure the name field is populated.")
                    .build());
        }
        return Collections.emptyList();
    }

}
```

### Sign-Off adjustments

To provide custom validation for sign-off adjustments, create a class that implements the `ISignOffAdjustmentValidator` interface and override its methods. The
interface is as follows:

```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
/**
 * <b>ISignOffAdjustmentValidator</b>
 *
 * <p> The validator used for validating Atoti Sign-Off Adjustments.
 *
 * @author ActiveViam
 */
public interface ISignOffAdjustmentValidator {

   /**
    * @param adjustmentToCreate the adjustment to be created
    * @return validation errors if the adjustment is not valid for creation, empty list otherwise
    */
   List<IValidationError> validateSignOffAdjustmentsCreation(IAdjustment adjustmentToCreate);

   /**
    * @param adjustmentsToCreate the adjustments to be created
    * @return validation errors if any of the adjustments is not valid for creation, empty list otherwise
    */
   List<IValidationError> validateSignOffAdjustmentsCreation(List<IAdjustment> adjustmentsToCreate);

   /**
    * @param adjustmentToUpdate the adjustment to be updated
    * @return validation errors if the adjustment is not valid for update, empty list otherwise
    */
   List<IValidationError> validateSignOffAdjustmentsUpdate(IAdjustment adjustmentToUpdate);

   /**
    * @param adjustmentsToUpdate the adjustments to be updated
    * @return validation errors if any of the adjustments is not valid for update, empty list otherwise
    */
   List<IValidationError> validateSignOffAdjustmentsUpdate(List<IAdjustment> adjustmentsToUpdate);
}
```

A sample implementation of `ISignOffAdjustmentValidator` could look like this:

```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
public class MyCustomSignOffAdjustmentValidator implements ISignOffAdjustmentValidator {

   List<IValidationError> validateSignOffAdjustmentCreation(IAdjustment adjustmentToCreate) {
        if (adjustmentToCreate.getSignOffTaskId() == null){
            return List.of(DefaultValidationError.builder()
                    .source(adjustmentToCreate)
                    .reason("The adjustment's task ID cannot be null")
                    .action("Please ensure the task ID field is populated.")
                    .build());
        }
        return Collections.emptyList();
    }

   List<IValidationError> validateSignOffAdjustmentsCreation(List<IAdjustment> adjustmentsToCreate){
        List<IValidationError> errors = new ArrayList<>();
        for (IAdjustment adjustment : adjustmentsToCreate) {
            errors.addAll(validateSignOffAdjustmentCreation(adjustment));
        }
        return errors;
   }

   List<IValidationError> validateSignOffAdjustmentUpdate(IAdjustment adjustmentToUpdate) {
        if (adjustmentToUpdate.getSignOffTaskId() == null) {
            return List.of(DefaultValidationError.builder()
                    .source(adjustmentToUpdate)
                    .reason("The adjustment's task ID cannot be null")
                    .action("Please ensure the task ID field is populated.")
                    .build());
        }
        return Collections.emptyList();
    }

    List<IValidationError> validateSignOffAdjustmentsUpdate(List<IAdjustment> adjustmentsToUpdate) {
        List<IValidationError> errors = new ArrayList<>();
        for (IAdjustment adjustment : adjustmentsToUpdate) {
            errors.addAll(validateSignOffAdjustmentUpdate(adjustment));
        }
        return errors;
    }

}
```
