Appendix

Faktor Zehn JCo Library

Package de.faktorzehn.fscdadapter.service.jco of module fscd-adapter-business contains classes to make calling RFCs easier.

To make a call, the JcoFunctionExecutorFactory bean can be autowired to create a JcoFunctionExecutor using JcoFunctionExecutorFactory.create(String functionName).

To map a domain object into a JCoFunction consider using JcoRecordWriter. To map from a JCoFunction to a domain object consider using BaseTableResultMapper, JcoFunctionToObjectMapper, JcoTableIterable and JcoRecordReader. To inspect an RFC’s parameter signature during development, use JcoFunctionMetadataPrinter.

In Java, the above FM call encapsulated in a service would look like this:

@Service
public class JcoUserDetailsService {

    private final JcoFunctionExecutorFactory functionExecutorFactory;
    private final DomainValueSource domainValueSource;

    public JcoUserDetailsService(JcoFunctionExecutorFactory functionExecutorFactory,
                                    DomainValueSource domainValueSource) {
        this.functionExecutorFactory = functionExecutorFactory;
        this.domainValueSource = domainValueSource;
    }

    public List<Address> readAddresses(String username) {
        return functionExecutorFactory.create("BAPI_USER_GET_DETAIL")
                .parameters(function -> function.getImportParameterList().setValue("USERNAME", username))
                .execute(() -> "Unable to read addresses for username '%s' ".formatted(username))
                .to(this::extractAddresses);
    }

    private List<Address> extractAddresses(JCoFunction function) {
        var table = function.getTableParameterList().getTable("ADDRESS");
        var addresses = new ArrayList<Address>();
        for (var row : JcoTableIterable.iterate(table)) {
            addresses.add(mapAddress(row));
        }
        return addresses;
    }

    private Address mapAddress(JCoRecord record) {
        Objects.requireNonNull(record);
        var read = new JcoRecordReader(record);

        return Address.builder()
                    .street(read.string("STREET"))
                    .city(read.string("CITY"))
                    .country(read.string("COUNTRY"))
                    .build();
    }

}
See the classes' javadoc and usages for further details and examples.
Inspecting RFC Signatures

When implementing or debugging RFC calls it can be helpful to inspect an RFC’s full parameter signature, including all nested structures and table types. JcoFunctionMetadataPrinter.print(JCoFunctionTemplate) produces a human-readable overview of all IMPORT, EXPORT, CHANGING, and TABLE parameters with their types, lengths, and descriptions.

Example usage:

var template = destination.getRepository().getFunctionTemplate("BAPI_BUPA_SEPA_MANDATES_ADD");
System.out.println(JcoFunctionMetadataPrinter.print(template));

Example output (truncated):

════════════════════════════════════════════════════════════════════════════════════════════
Function: BAPI_BUPA_SEPA_MANDATES_ADD
════════════════════════════════════════════════════════════════════════════════════════════

┌─ IMPORT (9) ─────────────────────────────────────────────────────────────────────────────
│ Name                             Type                                Len  Opt?  Description
│ ──────────────────────────────────────────────────────────────────────────────────────────
│ IV_AUTHORITY_CHECK               STRUCTURE                             -  opt
│   ├─ BAPIBUS1006_X (1 fields) ───────────────────────────────
│   │   MARK                       CHAR                                  1        Datenelement zur Domäne BOOLE: TRUE (='X') und FALSE (=' ')
│ IV_ENQUEUE                       STRUCTURE                             -  opt
│   └─ ↑ see BAPIBUS1006_X above
│ IV_PARTNER                       CHAR                                 10  opt
│ IV_STR_MANDATES_CREATE           STRUCTURE                             -  opt
│   ├─ BAPI_B1006_S_SEPA_MANDATE_DATA (58 fields) ─────────────
│   │   APPLICATION                CHAR                                  1        Mandatsverwaltung:Anwendung, für die das Mandat relevant ist
│   │   SEPA_CREDITOR_ID           CHAR                                 35        Gläubiger-Identifikationsnummer
│   │   ...
...
Write to JCo Records

To write data to an RFC, use JcoRecordWriter:

private void writeAddress(JCoRecord record, Address address) {
    var writer = new JcoRecordWriter(record);
    writer.string("STREET", address.getStreet());
    writer.string("CITY", address.getCity());
    writer.string("COUNTRY", address.getCountry());
}

public void updateUserAddress(String username, Address newAddress) {
    functionExecutorFactory.create("BAPI_USER_CHANGE")
        .parameters(function -> {
            function.getImportParameterList().setValue("USERNAME", username);
            var addressRecord = function.getImportParameterList().getStructure("ADDRESS");
            writeAddress(addressRecord, newAddress);
        })
        .execute(() -> "Unable to update address for username '%s'".formatted(username))
        .to(this::handleResult);
}
JCo Type Conversion

For date and time, as well as boolean we chose not to use the JCo types. We use LocalDate and LocalTime instead of Date. We use boolean instead of char.

JcoRecordReader and JcoRecordWriter provide methods taking care of the conversion when reading from or writing to JcoRecord. Internally, they use the static methods of JcoHelper, which can be used directly when not interacting with JCoRecords.

Leading Zero Handling

Data elements can have both internal and external representations. For instance, while '1001' may be the external representation, e.g., in the UI, of a BP number, a CHAR10, its internal representation is '0000001001' with leading zeros.

While some RFCs make this conversion implicitly, others do not. Unfortunately, neither we nor JCo can easily determine from the RFC’s signature whether leading zeros will be managed automatically. Therefore, it’s prudent to always add leading zeros to such fields.

Additionally, there’s also no straightforward way to identify which fields require conversion without accessing the DDIC in the SAP system and checking the conversion routines associated with the data elements. Please ask an SAP expert for help.

Known fields that require leading zeros:

  • BP number

  • Insurance Object number (e.g., policy number, claim number)

Add Leading Zeros

JcoRecordWriter.alz(String, String, int) (add leading zeros) can be used to pad strings with leading zeros before calling an RFC. It uses the static method of the same name from JcoHelper.

private void writeBpNumber(JCoRecord record, String bpNumber) {
    var writer = new JcoRecordWriter(record);
    writer.alz("BP_NUMBER", bpNumber, 10);
}
A common way to avoid issues related to leading zeros is to use the full length of the field. For instance, ensuring all BP numbers start with a 1, such as 1000001001, eliminates the need for adding zeros. However, this approach is not the standard configuration in SAP systems for most fields. Moreover, values could be assigned externally, for instance by Faktor-IBP. In both cases there is no guarantee that leading zeros are present. Thus, the recommendation is to always convert and add leading zeros before calling an RFC.
Remove Leading Zeros

We do not want to display leading zeros in the FS-CD Adapter’s UI. Neither do we use leading zeros in the Faktor Zehn modules. The FS-CD Adapter serves as the single point to convert between the representations.

JcoRecordReader.rlz(String) (remove leading zeros) can be used to strip leading zeros from strings. It uses the static method of the same name from JcoHelper.

private String readBpNumber(JCoRecord record) {
    var reader = new JcoRecordReader(record);
    return reader.rlz("BP_NUMBER");
}

Transactional Behavior

The class JcoTransactionManager is an implementation of Spring’s PlatformTransactionManager. It allows making multiple RFCs within the same transaction instead of one transaction per RFC, as this can lead to complex error handling and compensating transactions etc.

How it works:

  1. A transaction is started for the connection in SAP

  2. The RFCs are called requesting not to commit changes

  3. The transaction is committed or rolled back via RFC

For an RFC to support transactional behavior, it has to provide an importing parameter to control the commit on SAP side. This parameter is often named I_COMMIT or similar. To enable transactional behavior call JcoFunctionExecutor.transactional(parameterName) when creating the JCoFunction. Annotate a method or class with @Transactional("jcoTransactionManager") to make it transactional.

@Service
public class UserManagementService {

    private final JcoFunctionExecutorFactory factory;

    @Transactional("jcoTransactionManager")
    public void updateUserDetailsTransactionally(String username, Address newAddress, String newEmail) {
        updateAddress(username, newAddress);
        updateEmail(username, newEmail);
        logChange(username);
    }

    private void updateAddress(String username, Address address) {
        factory.create("BAPI_USER_CHANGE_ADDRESS")
            .transactional("I_COMMIT")
            .parameters(fn -> {
                fn.getImportParameterList().setValue("USERNAME", username);
            })
            .execute(() -> "Failed to update address for user '%s'".formatted(username))
            .to(this::handleResult);
    }

    private void updateEmail(String username, String email) {
        factory.create("BAPI_USER_CHANGE_EMAIL")
            .transactional("I_COMMIT")
            .parameters(fn -> {
                fn.getImportParameterList().setValue("USERNAME", username);
                fn.getImportParameterList().setValue("EMAIL", email);
            })
            .execute(() -> "Failed to update email for user '%s'".formatted(username))
            .to(this::handleResult);
    }

    private void logChange(String username) {
        factory.create("Z_LOG_USER_CHANGE")
            .transactional("I_COMMIT")
            .parameters(fn -> fn.getImportParameterList().setValue("USERNAME", username))
            .execute(() -> "Failed to log change for user '%s'".formatted(username))
            .to(this::handleResult);
    }
}

All three RFC calls will be committed together, or rolled back if any of them fails.

If a technology other than JCo is used for RFC calls, a PlatformTransactionManager must be implemented using that technology’s transaction management capabilities.