Enneo

Business Logic Samples

Bank Data Agent

Automated processing of bank account changes

Tip

The Bank Data Agent is a specialized AI agent designed for efficient handling of changes to bank account details. It validates and processes customer requests for bank data updates automatically and ensures a seamless integration into existing payment systems. Through intelligent verification mechanisms and structured processes, bank data changes are executed precisely, securely, and with minimal manual effort.

Key Functions

The Bank Data Agent takes over the complete process of updating bank data, providing the following core functionalities:

  • IBAN Validation: Automatic review of the formatting and checksum conformity of new bank accounts
  • Identity Verification: Comparison of the account holder with contract information to ensure legitimacy
  • System Integration: Seamless update of bank data in all relevant systems
  • Customer Communication: Automated confirmations of successful changes or queries in case of ambiguities

Process Flow

The agent operates according to a clearly defined process, ensuring maximum security while maintaining high user-friendliness:

  1. Extraction of relevant data from the customer request (new IBAN, account holder, desired date)
  2. Validation of the new bank account based on international standards
  3. Verification of authorization to change the bank data
  4. Execution of the change in all linked systems
  5. Confirmation to the customer with the relevant information about the new bank account details

Through rule-based logic, all inputs are systematically checked and processed, which minimizes errors and significantly reduces manual processing effort.


<?php

use EnneoSDK\Api;
use EnneoSDK\Helpers;
use EnneoSDK\IntentInfo;
use EnneoSDK\IntentOption;
use EnneoSDK\Interaction;

require(getenv()['SDK'] ?? 'sdk.php');
/** @var stdClass $in */

######### Expected Input:
// date -> Date for deposit change (optional)
// channel -> ticket channel. Voice and chat can be handled synchronously (required)
// newIBAN -> IBAN of the new bank account (required)
// oldIBAN -> IBAN of the old bank account (optional)
// payoutOnly -> true if only payout should be changed (optional)


// Initialize variables
if ($in->date ?? false) {
    $in->date = Helpers::parseDateToYMD($in->date);
} else {
    $in->date = date('Y-m-d', strtotime('+1 day'));
}
$interaction = new Interaction(data: $in);
$interaction->data->oldIBAN = formatIban($in->oldIBAN ?: "");
$interaction->data->newIBAN = formatIban($in->newIBAN ?: "");

// Handle actual change of bank data in the backend system
if ($in->_action == 'enter_into_system') {
    $response = Api::call(
        method: 'POST',
        url: 'https://echo.enneo.ai',
        params: json_decode(json_encode($interaction), true)
    );
    echo json_encode($interaction);
    exit();
}

// Business logic
if ($in->oldIBAN == $in->newIBAN && $in->newIBAN) {
    $interaction->infos[] = new IntentInfo(
        type: 'warning',
        message: 'IBAN ist schon im System',
    );
    $interaction->options[] = new IntentOption(
        type: 'iban_already_in_system',
        name: 'Kunden darauf hinweisen',
        recommended: true,
    );
} elseif (!validateIbanFormatting($in->newIBAN)) {
    $interaction->infos[] = new IntentInfo(
        type: 'warning',
        message: 'IBAN nicht im korrekten Format',
    );
    $interaction->options[] = new IntentOption(
        type: 'ask_for_verification_iban_invalid_format',
        name: 'Kunden darauf hinweisen',
        recommended: true,
    );
} elseif (!validateIbanChecksum($in->newIBAN)) {
    $interaction->infos[] = new IntentInfo(
        type: 'warning',
        message: 'IBAN-Prüfsumme ungültig',
    );
    $interaction->options[] = new IntentOption(
        type: 'ask_for_verification_iban_invalid_checksum',
        name: 'Kunden darauf hinweisen',
        recommended: true,
    );
} else {
    if ($in->date < date('Y-m-d')) {
        // If the customer wants to use an IBAN effective in the past, we default to tomorrow
        $interaction->infos[] = new IntentInfo(
            type: 'warning',
            message: 'Gewünschtes Gültigkeitsdatum '.EnneoSDK\Helpers::formatDate($in->date).' liegt in der Vergangenheit. Gültigkeit auf morgen gesetzt'
        );
        $interaction->data->date = date('Y-m-d', strtotime('+1 day'));
    }

    $interaction->infos[] = new IntentInfo(
        type: 'success',
        message: 'IBAN gültig',
    );
    if ($in->payoutOnly) {
        $msg = 'Im System  nur für Guthaben hinterlegen';
    } else {
        $msg = 'Im System hinterlegen';
    }

    $interaction->options[] = new IntentOption(
        type: 'enter_into_system',
        name: $msg,
        recommended: true,
    );
}

// Return result
echo json_encode($interaction);

// Format IBAN for readability
function formatIban(string $iban): string
{
    $iban = str_replace(' ', '', $iban);

    // Split the input string into 4-character chunks for readability
    $formatted_iban = '';
    $chunk_size = 4;
    $num_chunks = ceil(strlen($iban) / $chunk_size);
    for ($i = 0; $i < $num_chunks; ++$i) {
        $chunk = substr($iban, $i * $chunk_size, $chunk_size);
        $formatted_iban .= $chunk . ' ';
    }

    // Trim any trailing spaces and return the formatted IBAN
    return trim($formatted_iban);
}

// Check integrity of IBAN (modified from original source: https://stackoverflow.com/questions/20983339/validate-iban-php)
function validateIbanChecksum(string $iban): bool
{
    $iban = strtolower(str_replace(' ', '', $iban));
    $Countries = ['al' => 28, 'ad' => 24, 'at' => 20, 'az' => 28, 'bh' => 22, 'be' => 16, 'ba' => 20, 'br' => 29, 'bg' => 22, 'cr' => 21, 'hr' => 21, 'cy' => 28, 'cz' => 24, 'dk' => 18, 'do' => 28, 'ee' => 20, 'fo' => 18, 'fi' => 18, 'fr' => 27, 'ge' => 22, 'de' => 22, 'gi' => 23, 'gr' => 27, 'gl' => 18, 'gt' => 28, 'hu' => 28, 'is' => 26, 'ie' => 22, 'il' => 23, 'it' => 27, 'jo' => 30, 'kz' => 20, 'kw' => 30, 'lv' => 21, 'lb' => 28, 'li' => 21, 'lt' => 20, 'lu' => 20, 'mk' => 19, 'mt' => 31, 'mr' => 27, 'mu' => 30, 'mc' => 27, 'md' => 24, 'me' => 22, 'nl' => 18, 'no' => 15, 'pk' => 24, 'ps' => 29, 'pl' => 28, 'pt' => 25, 'qa' => 29, 'ro' => 24, 'sm' => 27, 'sa' => 24, 'rs' => 22, 'sk' => 24, 'si' => 19, 'es' => 24, 'se' => 24, 'ch' => 21, 'tn' => 24, 'tr' => 26, 'ae' => 23, 'gb' => 22, 'vg' => 24];
    $Chars = ['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17, 'i' => 18, 'j' => 19, 'k' => 20, 'l' => 21, 'm' => 22, 'n' => 23, 'o' => 24, 'p' => 25, 'q' => 26, 'r' => 27, 's' => 28, 't' => 29, 'u' => 30, 'v' => 31, 'w' => 32, 'x' => 33, 'y' => 34, 'z' => 35];

    if (!array_key_exists(substr($iban, 0, 2), $Countries)) {
        return false;
    }

    if (strlen($iban) == $Countries[substr($iban, 0, 2)]) {
        $MovedChar = substr($iban, 4) . substr($iban, 0, 4);
        $MovedCharArray = str_split($MovedChar);
        $NewString = '';

        foreach ($MovedCharArray as $key => $value) {
            if (!is_numeric($MovedCharArray[$key])) {
                $MovedCharArray[$key] = $Chars[$MovedCharArray[$key]];
            }
            $NewString .= $MovedCharArray[$key];
        }

        if (bcmod($NewString, '97') == 1) {
            return true;
        }
    }

    return false;
}

// Check formatting of IBAN
function validateIbanFormatting(string $str): bool
{
    // Note: German IBAN definition allows for slack
    // Correct IBAN definition: DE[0-9]{2}\s?([0-9]{4}\s?){4}([0-9]{2})\s?
    if (str_starts_with($str,'DE')) {
        $re = 'DE\s?[0-9]{2}\s?([0-9]{4}\s?){4}([0-9]{2})\s?';
        // Find all IBAN addresses
        if (preg_match_all('/' . $re . '/', $str, $m)) {
            return true;
        } else {
            return false;
        }
    } else {
        // International IBANs are validated in the validateIbanChecksum() function
        return true;
    }
}
import importlib.util
import os
import json
import re
from datetime import datetime, timedelta

file_path = os.getenv('SDK', 'sdk.py')
spec = importlib.util.spec_from_file_location('sdk', file_path)
sdk = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sdk)

######### Expected Input:
# date -> Date for deposit change (optional)
# channel -> ticket channel. Voice and chat can be handled synchronously (required)
# newIBAN -> IBAN of the new bank account (required)
# oldIBAN -> IBAN of the old bank account (optional)
# payoutOnly -> true if only payout should be changed (optional)

def parse_date_to_ymd(date_str):
    """
    Parse a textual date into yyyy-mm-dd format.
    Matches the PHP parseDateToYMD function.
    """
    if not date_str:
        return None

    # Try standard date parsing first
    try:
        parsed_date = datetime.strptime(date_str, '%Y-%m-%d')
        return parsed_date.strftime('%Y-%m-%d')
    except ValueError:
        pass

    # Try other common formats
    formats = ['%d.%m.%Y', '%d.%m.%y', '%d.%m.%Y', '%d.%m.%y']
    for fmt in formats:
        try:
            parsed_date = datetime.strptime(date_str, fmt)
            return parsed_date.strftime('%Y-%m-%d')
        except ValueError:
            continue

    return None

# Format IBAN for readability
def format_iban(iban):
    if not iban:
        return ''

    iban = str(iban).replace(' ', '')

    # Split the input string into 4-character chunks for readability
    formatted_iban = ''
    chunk_size = 4
    num_chunks = (len(iban) + chunk_size - 1) // chunk_size
    for i in range(num_chunks):
        chunk = iban[i * chunk_size:(i + 1) * chunk_size]
        formatted_iban += chunk + ' '

    # Trim any trailing spaces and return the formatted IBAN
    return formatted_iban.strip()

# Check integrity of IBAN (modified from original source: https://stackoverflow.com/questions/20983339/validate-iban-php)
def validate_iban_checksum(iban):
    if not iban:
        return False

    iban = str(iban).lower().replace(' ', '')
    countries = {
        'al': 28, 'ad': 24, 'at': 20, 'az': 28, 'bh': 22, 'be': 16, 'ba': 20, 'br': 29, 'bg': 22, 'cr': 21,
        'hr': 21, 'cy': 28, 'cz': 24, 'dk': 18, 'do': 28, 'ee': 20, 'fo': 18, 'fi': 18, 'fr': 27, 'ge': 22,
        'de': 22, 'gi': 23, 'gr': 27, 'gl': 18, 'gt': 28, 'hu': 28, 'is': 26, 'ie': 22, 'il': 23, 'it': 27,
        'jo': 30, 'kz': 20, 'kw': 30, 'lv': 21, 'lb': 28, 'li': 21, 'lt': 20, 'lu': 20, 'mk': 19, 'mt': 31,
        'mr': 27, 'mu': 30, 'mc': 27, 'md': 24, 'me': 22, 'nl': 18, 'no': 15, 'pk': 24, 'ps': 29, 'pl': 28,
        'pt': 25, 'qa': 29, 'ro': 24, 'sm': 27, 'sa': 24, 'rs': 22, 'sk': 24, 'si': 19, 'es': 24, 'se': 24,
        'ch': 21, 'tn': 24, 'tr': 26, 'ae': 23, 'gb': 22, 'vg': 24
    }
    chars = {
        'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15, 'g': 16, 'h': 17, 'i': 18, 'j': 19,
        'k': 20, 'l': 21, 'm': 22, 'n': 23, 'o': 24, 'p': 25, 'q': 26, 'r': 27, 's': 28, 't': 29,
        'u': 30, 'v': 31, 'w': 32, 'x': 33, 'y': 34, 'z': 35
    }

    if iban[:2] not in countries:
        return False

    if len(iban) == countries[iban[:2]]:
        moved_char = iban[4:] + iban[:4]
        new_string = ''

        for char in moved_char:
            if char.isdigit():
                new_string += char
            else:
                new_string += str(chars[char])

        if int(new_string) % 97 == 1:
            return True

    return False

# Check formatting of IBAN
def validate_iban_formatting(iban_str):
    if not iban_str:
        return False

    iban_str = str(iban_str)
    # Note: German IBAN definition allows for slack
    # Correct IBAN definition: DE[0-9]{2}\s?([0-9]{4}\s?){4}([0-9]{2})\s?
    if iban_str.startswith('DE'):
        pattern = r'DE\s?[0-9]{2}\s?([0-9]{4}\s?){4}([0-9]{2})\s?'
        if re.match(pattern, iban_str):
            return True
        else:
            return False
    else:
        # International IBANs are validated in the validate_iban_checksum() function
        return True

input_data = sdk.load_input_data()

# Initialize variables
if input_data.get('date'):
    input_data['date'] = parse_date_to_ymd(input_data['date'])
else:
    input_data['date'] = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')

interaction = sdk.Interaction(data=input_data)
interaction.data['oldIBAN'] = format_iban(input_data.get('oldIBAN', ''))
interaction.data['newIBAN'] = format_iban(input_data.get('newIBAN', ''))

# Handle actual change of bank data in the backend system
if input_data.get('_action') == 'enter_into_system':
    response = sdk.Api.call(
        method='POST',
        url='https://echo.enneo.ai',
        params=json.loads(json.dumps(interaction.model_dump()))
    )
    print(json.dumps(interaction.model_dump()))
    exit()

# Business logic
if input_data.get('oldIBAN') == input_data.get('newIBAN') and input_data.get('newIBAN'):
    interaction.infos.append(
        sdk.IntentInfo(
            type='warning',
            message='IBAN ist schon im System'
        )
    )
    interaction.options.append(
        sdk.IntentOption(
            type='iban_already_in_system',
            name='Kunden darauf hinweisen',
            recommended=True
        )
    )
elif not validate_iban_formatting(input_data.get('newIBAN', '')):
    interaction.infos.append(
        sdk.IntentInfo(
            type='warning',
            message='IBAN nicht im korrekten Format'
        )
    )
    interaction.options.append(
        sdk.IntentOption(
            type='ask_for_verification_iban_invalid_format',
            name='Kunden darauf hinweisen',
            recommended=True
        )
    )
elif not validate_iban_checksum(input_data.get('newIBAN', '')):
    interaction.infos.append(
        sdk.IntentInfo(
            type='warning',
            message='IBAN-Prüfsumme ungültig'
        )
    )
    interaction.options.append(
        sdk.IntentOption(
            type='ask_for_verification_iban_invalid_checksum',
            name='Kunden darauf hinweisen',
            recommended=True
        )
    )
else:
    if input_data['date'] < datetime.now().strftime('%Y-%m-%d'):
        # If the customer wants to use an IBAN effective in the past, we default to tomorrow
        interaction.infos.append(
            sdk.IntentInfo(
                type='warning',
                message=f'Gewünschtes Gültigkeitsdatum {sdk.Helpers.format_date(input_data["date"])} liegt in der Vergangenheit. Gültigkeit auf morgen gesetzt'
            )
        )
        interaction.data['date'] = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')

    interaction.infos.append(
        sdk.IntentInfo(
            type='success',
            message='IBAN gültig'
        )
    )
    if input_data.get('payoutOnly'):
        msg = 'Im System  nur für Guthaben hinterlegen'
    else:
        msg = 'Im System hinterlegen'

    interaction.options.append(
        sdk.IntentOption(
            type='enter_into_system',
            name=msg,
            recommended=True
        )
    )

# Return result
print(json.dumps(interaction.model_dump()))