How to Create a Custom CSM Reachability Service

This guide will provide an example of how to use CSM Reachability functionalities to create a custom implementation.

Step 1: Generate the API Endpoint

If a new API endpoint is required, generate it along with all necessary schemas and model classes. Follow the CSM Reachability structure by creating an interface that will serve as the entry point (port) to the new service.

We will use a fictional iban-reachability endpoint as an example, planning to expose this logic via an HTTP API:

openapi: 3.0.1
info:
  title: Custom CSM Reachability Service API V1
  version: ${project.version}
  description: APIs for IBAN Reachability
servers:
  - url: http://localhost:8080
    description: Local server URL
paths:
  /iban-reachability:
    get:
      tags:
        - iban-reachability
      description: Using creditor iban to discover which CSMs is creditor reachable by
      operationId: get-csm-reachability
      parameters:
        - name: processingEntity
          in: query
          description: >-
            Exact match on the processing entity on behalf of which the source
            payment instruction was being processed
          required: true
          schema:
            type: string
          example: '001'
        - name: creditorIban
          in: query
          description: IBAN for which to check which CSMs are reachable
          required: true
          schema:
            type: string
        - name: transferCurrency
          in: query
          description: Filter results by currency
          schema:
            type: string
# omitted for brevity

Step 2: Separate Model and Port Modules

It’s good practice to separate the generated model and port module, allowing them to be used independently if needed.

We use the API spec to generate the model classes:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.iconsolutions.ipf.payments.csm.reachability</groupId>
        <artifactId>iban-reachability-parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>iban-reachability-model</artifactId>

    <dependencies>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <!-- https://openapi-generator.tech/ -->
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <dependencies>
                    <!-- Depend on the api module, we're going to generate interfaces from its spec -->
                    <dependency>
                        <groupId>com.iconsolutions.ipf.payments.csm.reachability</groupId>
                        <artifactId>iban-reachability-service-api</artifactId>
                        <version>${project.version}</version>
                    </dependency>
                </dependencies>
                <executions>
                    <!-- Generate DTO classes -->
                    <execution>
                        <id>generate-openapi-model</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>static/iban-reachability.yaml</inputSpec>
                            <skipIfSpecIsUnchanged>true</skipIfSpecIsUnchanged>

                            <!-- See https://openapi-generator.tech/docs/generators/spring/ for options-->
                            <generatorName>spring</generatorName>
                            <configOptions>
                                <dateLibrary>java8</dateLibrary>
                                <generateBuilders>true</generateBuilders>
                                <openApiNullable>false</openApiNullable>
                                <useSwaggerAnnotations>false</useSwaggerAnnotations>
                                <booleanGetterPrefix>is</booleanGetterPrefix>
                                <!-- Remain backward compatible with existing code by using Lombok builders -->
                                <!-- To be strictly compatible with existing schema, do not include null properties in JSONs -->
                                <additionalModelTypeAnnotations>
                                    @lombok.NoArgsConstructor
                                    @lombok.Builder(toBuilder = true)
                                    @lombok.AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
                                    @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
                                </additionalModelTypeAnnotations>
                                <useSpringBoot3>true</useSpringBoot3>
                            </configOptions>

                            <!-- Only generate models in this module-->
                            <generateModels>true</generateModels>
                            <modelPackage>com.iconsolutions.ipf.csmreachability.dto.v1</modelPackage>

                            <!-- Interfaces/APIs will be generated later in a separate module -->
                            <generateApis>false</generateApis>
                            <generateApiTests>false</generateApiTests>
                            <generateApiDocumentation>false</generateApiDocumentation>
                            <generateModelTests>false</generateModelTests>
                            <generateModelDocumentation>false</generateModelDocumentation>

                            <!-- Skip all the extra stuff that gets generated -->
                            <generateSupportingFiles>false</generateSupportingFiles>
                        </configuration>
                    </execution>
                    <execution>
                        <id>generate-iban-reachability</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>static/iban-reachability.yaml</inputSpec>
                            <skipIfSpecIsUnchanged>true</skipIfSpecIsUnchanged>
                            <generatorName>openapi</generatorName>
                            <configOptions>
                                <outputFileName>iban-reachability-service.json</outputFileName>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

We then add the model dependency to the port module to utilize the generated classes.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.iconsolutions.ipf.payments.csm.reachability</groupId>
        <artifactId>iban-reachability-parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>iban-reachability-service-api-port</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.iconsolutions.ipf.payments.csm.reachability</groupId>
            <artifactId>iban-reachability-model</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</project>

And finally, we define a service port:

/**
 * <p>
 * Interface for providing list of reachable csm's
 * based on input provided (iban, sort code, account number)
 * </p>
 */
public interface IbanReachabilityService {
    Mono<ReachabilityDtoWrapper> getReachableCsm(IbanReachabilityCriteria ibanReachabilityCriteria);
}

Step 3: Implement the REST Controller

Implement the REST controller from the generated API:

@RestController
@RequiredArgsConstructor
public class IbanReachabilityController implements IbanReachabilityApi {

    private final IbanReachabilityService ibanReachabilityService;

    @Override
    public Mono<ResponseEntity<ReachabilityDtoWrapper>> getCsmReachability(String processingEntity,
                                                                           String creditorIban,
                                                                           String transferCurrency,
                                                                           ServerWebExchange exchange) {
        var criteria = IbanReachabilityCriteria.builder()
                .processingEntity(processingEntity)
                .creditorIban(creditorIban)
                .transferCurrency(transferCurrency)
                .build();

        return ibanReachabilityService.getReachableCsm(criteria)
                .map(ResponseEntity::ok)
                .switchIfEmpty(Mono.just(notFound().build()));
    }
}

Step 4: Use Reachability Error Handling

Implement a specific error handler and the necessary error attributes. Here are some examples:

public class ErrorHandler extends AbstractErrorWebExceptionHandler {

    private static final RequestPredicate CSM_REACHABILITY_PATHS = Stream.of(
                    "/iban-reachability")
            .map(RequestPredicates::path)
            .reduce(RequestPredicate::or)
            .orElseThrow();

    public ErrorHandler(ErrorAttributes errorAttributes,
                        ApplicationContext applicationContext,
                        ServerCodecConfigurer serverCodecConfigurer) {
        super(errorAttributes, new WebProperties.Resources(), applicationContext);
        super.setMessageReaders(serverCodecConfigurer.getReaders());
        super.setMessageWriters(serverCodecConfigurer.getWriters());
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(org.springframework.boot.web.reactive.error.ErrorAttributes errorAttributes) {
        return RouterFunctions.route(CSM_REACHABILITY_PATHS, this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        var errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults());
        return ServerResponse.status(ErrorAttributes.httpStatusFor(getError(request)))
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(errorPropertiesMap));
    }
}
public class ErrorAttributes extends DefaultErrorAttributes {

    public static HttpStatus httpStatusFor(Throwable ex) {
        return ex instanceof ServerWebInputException
                || ex instanceof InvalidRequestException
                ? HttpStatus.BAD_REQUEST
                : HttpStatus.INTERNAL_SERVER_ERROR;
    }

    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
        var error = getError(request);
        var httpStatus = httpStatusFor(error);
        var map = super.getErrorAttributes(request, options);
        map.put("status", httpStatus);
        map.put("messages", getErrorMessage(error));
        map.put("error", httpStatus.getReasonPhrase());
        return map;
    }

    private List<String> getErrorMessage(Throwable error) {
        return error.getMessage() == null ? Collections.emptyList() : List.of(error.getMessage());
    }
}

Step 5: Implement the Service Layer

First, add the dependency for the CSM Reachability service:

<dependency>
    <groupId>com.iconsolutions.ipf.payments.csm.reachability</groupId>
    <artifactId>csm-reachability-service</artifactId>
    <version>{csm-reachability.version}</version>
</dependency>

Also, add the dependency for the previously constructed port module to implement the service.

Step 6: Define Configuration Properties

To use DPS Direct Implementation add the following dependencies:

<dependency>
    <groupId>com.iconsolutions.ipf.payments.csm.reachability</groupId>
    <artifactId>csm-reachability-api-direct</artifactId>
    <version>{csm-reachability.version}</version>
</dependency>
<dependency>
    <groupId>com.iconsolutions.ipf.core.dynamicsettings.v2</groupId>
    <artifactId>dynamic-processing-settings-service-adapter</artifactId>
    <version>{dynamic-processing-settings.version}</version>
</dependency>

Define the following properties in the configuration:

actor-system-name = csm-reachability-service

ipf.csm-reachability.settings-api.connection = direct

ipf.dps-api.client-type = "direct"

To use the DPS connectors implementation, set the second property to http and refer to DPS Connectors.

Step 7: Build Validation Checks

With the CSM Reachability logic available as dependencies, build a validation check that uses AgentClearingSettings data:

@RequiredArgsConstructor
public class AgentClearingSettingsCheck implements ValidationCheck<IbanReachabilityByCurrenciesValidationData> {

    // Inject an interface provided by `csm-reachability-api` and implemented in
    // `csm-reachability-api-direct` and `csm-reachability-api-connector`
    private final AgentClearingSettingsQuery agentClearingSettingsQuery;

    @Override
    public Mono<IbanReachabilityByCurrenciesValidationData> checkAndEnrich(IbanReachabilityByCurrenciesValidationData validationData) {
        var criteria = Map.<String, Object>of("processingEntity", validationData.getProcessingEntity());
        // use the query interface to build validation validation logic
        return Mono.fromCompletionStage(agentClearingSettingsQuery.getAgentClearingSettings(criteria))
                .map(Response::getValue)
                .filter(SettingsDTO::hasSettings)
                .flatMapIterable(SettingsDTO::getSettings)
                .filter(SettingsValidationUtil::isNotInactiveApprovalPendingSetting)
                .map(SettingDTO::getPayload)
                .collectList()
                .map(itemsList -> itemsList.stream().collect(toMap(AgentClearingSettings::getAgentUniqueId, Function.identity())))
                .map(agentClearingSettingsCache -> enrichValidationData(validationData, agentClearingSettingsCache));
    }

Wire all related validation checks into a service:

@RequiredArgsConstructor
public class IbanReachabilityByCurrencyServiceImpl {

    // validation checks built using existing `csm-reachability` logic
    private final AgentSettlementSettingsCheck agentSettlementSettingsCheck;
    private final ValidateCsmReachabilityCheck validateCsmReachabilityCheck;
    private final AgentClearingSettingsCheck agentClearingSettingsCheck;

Step 8: Build the Custom Service

Finally, build your own custom service:

@RequiredArgsConstructor
public class IbanReachabilityServiceImpl implements IbanReachabilityService {

    private final Validator validator;
    private final IbanReachabilityByOnUsServiceImpl ibanReachabilityByOnUsService;
    private final IbanReachabilityByCurrencyServiceImpl ibanReachabilityByCurrencyService;

    @Override
    public Mono<ReachabilityDtoWrapper> getReachableCsm(IbanReachabilityCriteria criteria) {
        return validate(criteria)
                .flatMap(validatedCriteria -> {
                    CounterPartyIdentifier cpi = toCounterPartyIdentifier(validatedCriteria);
                    return ibanReachabilityByOnUsService.getReachableCsm(cpi, validatedCriteria.getProcessingEntity(), validatedCriteria.getTransferCurrency())
                            .zipWith(ibanReachabilityByCurrencyService.getReachableCsms(cpi, validatedCriteria.getProcessingEntity(), validatedCriteria.getTransferCurrency()));
                })
                .map(IbanReachabilityServiceImpl::buildResult);
    }

    private Mono<IbanReachabilityCriteria> validate(IbanReachabilityCriteria criteria) {
        var violations = validator.validate(criteria);
        if (!violations.isEmpty()) {
            return Mono.error(new ConstraintViolationException(violations));
        }
        return Mono.just(criteria);
    }

    private static CounterPartyIdentifier toCounterPartyIdentifier(IbanReachabilityCriteria criteria) {
        return CounterPartyIdentifier.builder()
                .identifier(criteria.getCreditorIban())
                .identifierType(IDENTIFIER_TYPE_IBAN)
                .build();
    }

    private static ReachabilityDtoWrapper buildResult(Tuple2<Optional<ReachabilityOnUsDto>, Optional<List<ReachableByCurrencyDto>>> result) {
        return ReachabilityDtoWrapper.builder()
                .reachableByOnUS(result.getT1().orElse(null))
                .reachableByCurrencies(result.getT2().orElse(null))
                .build();
    }
}

This completes the implementation of a new endpoint that requires some of the reachability functionalities.