Documentation for a newer release is available. View Latest

How to Create a Custom CSM Reachability Service

Esta guía proporciona un ejemplo de cómo usar las funcionalidades de CSM Reachability para crear una implementación personalizada.

Step 1: Generate the API Endpoint

Si se requiere un nuevo endpoint de API, genéralo junto con todos los esquemas y clases de modelo necesarios. Sigue la estructura de CSM Reachability creando una interfaz que actuará como punto de entrada (port) al nuevo servicio.

Usaremos un endpoint ficticio iban-reachability como ejemplo, planeando exponer esta lógica mediante una API HTTP:

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

Es buena práctica separar el módulo del modelo generado del módulo port, permitiendo que se usen de forma independiente si es necesario.

Usamos la especificación de API para generar las clases de modelo:

<?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>

Luego añadimos la dependencia del modelo al módulo port para utilizar las clases generadas.

<?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>

Y finalmente, definimos un 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

Implementa el REST controller a partir de la API generada:

@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

Implementa un manejador de errores específico y los error attributes necesarios. Aquí hay algunos ejemplos:

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

Primero, añade la dependencia para el servicio de CSM Reachability:

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

Además, añade la dependencia al módulo port construido previamente para implementar el servicio.

Step 6: Define Configuration Properties

Para usar la implementación DPS Direct añade las siguientes dependencias:

<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 las siguientes propiedades en la configuración:

actor-system-name = csm-reachability-service

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

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

Para usar la implementación con conectores DPS, establece la segunda propiedad a http y consulta DPS Connectors.

Step 7: Build Validation Checks

Con la lógica de CSM Reachability disponible como dependencias, construye una validación que use datos de AgentClearingSettings:

@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));
    }

Conecta todas las validaciones relacionadas en un servicio:

@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

Finalmente, construye tu propio servicio personalizado:

@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();
    }
}

Esto completa la implementación de un nuevo endpoint que requiere algunas de las funcionalidades de reachability.