You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

725 lines
34 KiB

<?php
declare(strict_types=1);
namespace App\Services\Camt\Conversion;
use App\Exceptions\ImporterErrorException;
use App\Services\CSV\Mapper\GetAccounts;
use App\Services\Shared\Configuration\Configuration;
use App\Services\Shared\Conversion\ProgressInformation;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
/**
* Class TransactionMapper
*/
class TransactionMapper
{
use GetAccounts;
use ProgressInformation;
private array $accountIdentificationSuffixes;
private array $allAccounts;
/**
* @throws ImporterErrorException
*/
public function __construct(private Configuration $configuration)
{
Log::debug('Constructed TransactionMapper.');
$this->allAccounts = $this->getAllAccounts();
$this->accountIdentificationSuffixes = ['id', 'iban', 'number', 'name'];
}
public function map(array $transactions): array
{
Log::debug(sprintf('Now mapping %d transaction(s)', count($transactions)));
$result = [];
$total = count($transactions);
/** @var array $transaction */
foreach ($transactions as $index => $transaction) {
Log::debug(sprintf('[%d/%d] Now mapping.', $index + 1, $total));
$result[] = $this->mapTransactionGroup($transaction);
Log::debug(sprintf('[%d/%d] Now done with mapping', $index + 1, $total));
}
Log::debug(sprintf('Mapped %d transaction(s)', count($result)));
return $result;
}
private function mapTransactionGroup(array $transaction): array
{
// make a new transaction:
$result = [
'group_title' => null,
'error_if_duplicate_hash' => $this->configuration->isIgnoreDuplicateTransactions(),
'transactions' => [],
];
$splits = $transaction['splits'] ?? 1;
$groupHandling = $this->configuration->getGroupedTransactionHandling();
Log::debug(sprintf('Transaction has %d split(s)', $splits));
for ($i = 0; $i < $splits; ++$i) {
/** @var array|bool $split */
$split = $transaction['transactions'][$i] ?? false;
if (is_bool($split) && false === $split) {
Log::warning(sprintf('No split #%d found, break.', $i));
continue;
}
$rawJournal = $this->mapTransactionJournal($groupHandling, $split);
$polishedJournal = null;
if (null !== $rawJournal) {
$polishedJournal = $this->sanityCheck($rawJournal);
}
if (null === $polishedJournal) {
// give warning, skip transaction.
}
// TODO loop over $current and clean up if necessary.
$result['transactions'][] = $polishedJournal;
}
Log::debug('Firefly III transaction is', $result);
return $result;
}
private function mapTransactionJournal(string $groupHandling, array $split): ?array
{
$current = [
'type' => 'withdrawal', // perhaps to be overruled later.
];
/**
* @var string $role
* @var array $data
*/
foreach ($split as $role => $data) {
// actual content of the field is in $data['data'], which is an array
if ('single' === $groupHandling || 'group' === $groupHandling) {
if (array_key_exists('entryDetailAccounterServiceReference', $data['data'])) {
// we'll use this one, no exception. so the one from level-c can be dropped (if available)
if (array_key_exists('entryAccounterServiceReference', $data['data'])) {
unset($data['data']['entryAccounterServiceReference']);
Log::debug('Dropped entryAccounterServiceReference');
}
}
if (array_key_exists('entryDetailBtcDomainCode', $data['data'])) {
// we'll use this one, no exception. so the one from level-c can be dropped (if available)
if (array_key_exists('entryBtcDomainCode', $data['data'])) {
unset($data['data']['entryBtcDomainCode']);
Log::debug('Dropped entryBtcDomainCode');
}
}
if (array_key_exists('entryDetailBtcFamilyCode', $data['data'])) {
// we'll use this one, no exception. so the one from level-c can be dropped (if available)
if (array_key_exists('entryBtcFamilyCode', $data['data'])) {
unset($data['data']['entryBtcFamilyCode']);
Log::debug('Dropped entryBtcFamilyCode');
}
}
if (array_key_exists('entryDetailBtcSubFamilyCode', $data['data'])) {
// we'll use this one, no exception. so the one from level-c can be dropped (if available)
if (array_key_exists('entryBtcSubFamilyCode', $data['data'])) {
unset($data['data']['entryBtcSubFamilyCode']);
Log::debug('Dropped entryBtcSubFamilyCode');
}
}
if (array_key_exists('entryDetailAmount', $data['data'])) {
// we'll use this one, no exception. so the one from level-c can be dropped (if available)
if (array_key_exists('entryAmount', $data['data'])) {
unset($data['data']['entryAmount']);
Log::debug('Dropped entryAmount');
}
}
}
switch ($role) {
default:
Log::error(sprintf('Cannot handle role "%s" yet.', $role));
break;
case '_ignore':
break;
case 'note':
// TODO perhaps lift into separate method?
$current['notes'] ??= '';
$addition = " \n".implode(" \n", $data['data']);
$current['notes'] .= $addition;
$current['notes'] = trim($current['notes']);
break;
case 'date_process':
// TODO perhaps lift into separate method?
$carbon = Carbon::createFromFormat('Y-m-d H:i:s', reset($data['data']));
$current['process_date'] = $carbon->toIso8601String();
break;
case 'date_transaction':
// TODO perhaps lift into separate method?
$carbon = Carbon::createFromFormat('Y-m-d H:i:s', reset($data['data']));
$current['date'] = $carbon->toIso8601String();
break;
case 'date_payment':
// TODO perhaps lift into separate method?
$carbon = Carbon::createFromFormat('Y-m-d H:i:s', reset($data['data']));
$current['payment_date'] = $carbon->toIso8601String();
break;
case 'date_book':
// TODO perhaps lift into separate method?
$carbon = Carbon::createFromFormat('Y-m-d H:i:s', reset($data['data']));
$current['book_date'] = $carbon->toIso8601String();
$current['date'] = $carbon->toIso8601String();
break;
case 'account-iban':
// could be multiple, could be mapped.
$current = $this->mapAccount($current, 'iban', 'source', $data);
break;
case 'opposing-iban':
// could be multiple, could be mapped.
$current = $this->mapAccount($current, 'iban', 'destination', $data);
break;
case 'opposing-name':
// could be multiple, could be mapped.
$current = $this->mapAccount($current, 'name', 'destination', $data);
break;
case 'external-id':
$addition = implode(' ', $data['data']);
$current['external_id'] = $addition;
break;
case 'description': // TODO think about a config value to use both values from level C and D
$current['description'] ??= '';
$addition = '';
if ('group' === $groupHandling || 'split' === $groupHandling) {
// use first description
// TODO use named field?
$addition = reset($data['data']);
}
if ('single' === $groupHandling) {
// just use the last description
// TODO use named field?
$addition = end($data['data']);
}
$current['description'] .= $addition;
Log::debug(sprintf('Description is "%s"', $current['description']));
break;
case 'amount':
// #8367 default amount = 0
$current['amount'] = 0;
Log::debug(sprintf('Start with amount at zero ("%s")', $current['amount']));
if ('group' === $groupHandling || 'split' === $groupHandling) {
Log::debug(sprintf('Group handling is "%s"', $groupHandling));
// if multiple values, use biggest (... at index 0?)
// TODO this will never work because $current['amount'] is NULL the first time and abs() can't handle that.
foreach ($data['data'] as $amount) {
if (null === $amount) {
// #8367 skip null values
continue;
}
if (is_string($amount)) {
$amount = (float) $amount;
}
if (abs($current['amount']) < abs($amount)) {
Log::debug(sprintf('Amount is now "%s" instead of "%s"', $amount, $current['amount']));
$current['amount'] = $amount;
}
}
}
if ('single' === $groupHandling) {
Log::debug(sprintf('Group handling is "%s"', $groupHandling));
// if multiple values, use smallest (... at index 1?)
foreach ($data['data'] as $amount) {
// #8367 skip null values
if (null === $amount) {
Log::debug('Amount is NULL, continue.');
continue;
}
if (is_string($amount)) {
$amount = (float) $amount;
}
Log::debug(sprintf('Amount is %s.', var_export($amount, true)));
// check for null first, should prevent null pointers in abs()
if (abs($current['amount'])
< abs($amount)) {
Log::debug(sprintf('Amount is now "%s" instead of "%s"', $amount, $current['amount']));
$current['amount'] = $amount;
}
}
}
if (0 === bccomp('0', (string) $current['amount'])) {
Log::debug('Amount is ZERO, set to NULL');
$current['amount'] = null;
}
if (null !== $current['amount'] && 0 !== bccomp('0', (string) $current['amount']) && !is_string($current['amount'])) {
Log::debug(sprintf('Amount is %s, turn into string', var_export($current['amount'], true)));
$current['amount'] = (string) $current['amount'];
}
Log::debug(sprintf('Final amount is "%s"', $current['amount']));
break;
case 'currency-code':
$current = $this->mapCurrency($current, 'currency', $data);
break;
}
}
return $current;
}
/**
* This function takes the value in $data['data'], which is for example the account
* name or the account IBAN. It will check if there is a mapping for this value, mapping for example
* the value "ShrtBankName" to "Short Bank Name".
*
* If there is a mapping the value will be replaced. If there is not, no replacement will take place.
* Either way, the new value will be placed in the correct place in $current.
*
* Example results:
* source_iban = something
* destination_number = 12345
* source_id = 5
*/
private function mapAccount(array $current, string $fieldName, string $direction, array $data): array
{
// bravely assume there's just one value in the array:
$fieldValue = implode('', $data['data']);
// replace with mapping, if mapping exists.
if (array_key_exists($fieldValue, $data['mapping'])) {
$key = sprintf('%s_id', $direction);
$current[$key] = $data['mapping'][$fieldValue];
}
// leave original value if no mapping exists.
if (!array_key_exists($fieldValue, $data['mapping'])) {
// $direction is either 'source' or 'destination'
// $fieldName is 'id', 'iban','name' or 'number'
$key = sprintf('%s_%s', $direction, $fieldName);
$current[$key] = $fieldValue;
}
return $current;
}
private function mapCurrency(mixed $current, string $type, array $data): array
{
$code = implode('', $data['data']);
// replace with mapping
if (array_key_exists($code, $data['mapping'])) {
$key = sprintf('%s_id', $type);
$current[$key] = $data['mapping'][$code];
}
// leave original IBAN
if (!array_key_exists($code, $data['mapping'])) {
$key = sprintf('%s_code', $type);
$current[$key] = $code;
}
return $current;
}
/**
* A transaction has a bunch of minimal requirements. This method checks if they are met.
*
* It will also correct the transaction type (if possible).
*/
private function sanityCheck(array $current): ?array
{
Log::debug('Start of sanityCheck');
// no amount?
if (!array_key_exists('amount', $current)) {
Log::error('Array has no amount information, cannot fix.');
return null;
}
if ('' === $current['amount']) {
Log::error('Array has empty amount information, cannot fix.');
return null;
}
if (null === $current['amount']) {
Log::error('Array has NULL amount information, cannot fix.');
return null;
}
// if there is no source information, add the default account now:
if ($this->accountDetailsEmpty('source', $current)) {
Log::debug('Array has no source information, added default info.');
$current['source_id'] = $this->configuration->getDefaultAccount();
}
// if there is no destination information, add an empty account now:
$current['destination_is_empty'] = false;
if ($this->accountDetailsEmpty('destination', $current)) {
Log::debug('Array has no destination information, added default info.');
$current['destination_name'] = '(no name)';
$current['destination_is_empty'] = true;
}
// if is positive
if (1 === bccomp((string) $current['amount'], '0')) {
Log::debug('Swap accounts because amount is positive');
// positive account is deposit (or transfer), so swap accounts.
$current = $this->swapAccounts($current);
}
$current = $this->determineTransactionType($current);
Log::debug(sprintf('Transaction type is %s', $current['type']));
// the type is a withdrawal, but we did not recognize the type of the source account.
// if that did not succeed we did not FIND the source account, and must fall back
// on the default account.
$overruleAccount = false;
if ('withdrawal' === $current['type'] && '' === (string) $current['source_type']) {
$current['source_id'] = $this->configuration->getDefaultAccount();
unset($current['source_name'], $current['source_iban']);
Log::warning(sprintf('Withdrawal, but did not recognize the type of the source account. It will be replaced with the default account (#%d).', $current['source_id']));
$overruleAccount = true;
}
// same for deposit:
if ('deposit' === $current['type'] && '' === (string) $current['destination_type']) {
$current['destination_id'] = $this->configuration->getDefaultAccount();
unset($current['destination_name'], $current['destination_iban']);
Log::warning(sprintf('Deposit, but did not recognize the destination account. It will be replaced with the default account (#%d).', $current['destination_id']));
$overruleAccount = true;
}
// at this point it is possible that either of the two actions above have accidentally
// set BOTH accounts to be the same one. For example, source_id = 1 and destination_iban = ABC
// (and they point to the same account). This sanity check must be done again. But not right now.
// amount must be positive
if (-1 === bccomp((string) $current['amount'], '0')) {
// negative amount is debit (or transfer)
$current['amount'] = bcmul((string) $current['amount'], '-1');
}
// no description?
if (!array_key_exists('description', $current)) {
Log::warning('Did not find a description in the transaction, added "(no description)"');
$current['description'] = '(no description)';
}
if (array_key_exists('description', $current) && '' === (string) $current['description']) {
Log::warning('Did not find a description in the transaction, added "(no description)"');
$current['description'] = '(no description)';
}
// no date?
if (!array_key_exists('date', $current)) {
Log::warning(sprintf('Did not find a date in the transaction, added "%s"', Carbon::now()->format('Y-m-d')));
$current['date'] = Carbon::now()->format('Y-m-d');
}
if (array_key_exists('date', $current) && '' === (string) $current['date']) {
Log::warning(sprintf('Did not find a date in the transaction, added "%s"', Carbon::now()->format('Y-m-d')));
$current['date'] = Carbon::now()->format('Y-m-d');
}
// unset var
unset($current['destination_is_empty'], $current['source_type'], $current['destination_type']);
return $current;
}
private function accountDetailsEmpty(string $direction, array $current): bool
{
$noId = '' === ($current[sprintf('%s_id', $direction)] ?? '');
$noIban = '' === ($current[sprintf('%s_iban', $direction)] ?? '');
$noNumber = '' === ($current[sprintf('%s_number', $direction)] ?? '');
$noName = '' === ($current[sprintf('%s_name', $direction)] ?? '');
return $noId && $noIban && $noNumber && $noName;
}
private function swapAccounts(array $currentTransaction): array
{
Log::debug('swapAccounts');
$return = $currentTransaction;
foreach ($this->accountIdentificationSuffixes as $suffix) {
$sourceKey = sprintf('source_%s', $suffix);
$destKey = sprintf('destination_%s', $suffix);
// if source value exists, save it.
$sourceValue = array_key_exists($sourceKey, $currentTransaction) ? $currentTransaction[$sourceKey] : null;
// if destination value exists, save it.
$destValue = array_key_exists($destKey, $currentTransaction) ? $currentTransaction[$destKey] : null;
// always unset source value
Log::debug(sprintf('[1] Unset "%s" with value "%s"', $sourceKey, $sourceValue));
Log::debug(sprintf('[2] Unset "%s" with value "%s"', $destKey, $destValue));
unset($return[$sourceKey], $return[$destKey]);
// set opposite values
if (null !== $sourceValue) {
Log::debug(sprintf('[1] Set "%s" to "%s"', $destKey, $sourceValue));
$return[$destKey] = $sourceValue;
}
// a small exception. Do not set the destination name to "no name" if it's set to "no name" because it was empty.
if (true === $currentTransaction['destination_is_empty']) {
Log::debug(sprintf('Will not set "%s" to "%s" because destination is empty.', $sourceKey, $destValue));
}
if (null !== $destValue && false === $currentTransaction['destination_is_empty']) {
Log::debug(sprintf('[2] Set "%s" to "%s"', $sourceKey, $destValue));
$return[$sourceKey] = $destValue;
}
}
return $return;
}
private function determineTransactionType(array $current): array
{
Log::debug('Determine transaction type.');
$directions = ['source', 'destination'];
$accountType = [];
$lessThanZero = 1 === bccomp('0', (string) $current['amount']);
Log::debug(sprintf('Amount is "%s", so lessThanZero is %s', $current['amount'], var_export($lessThanZero, true)));
foreach ($directions as $direction) {
Log::debug(sprintf('Now working on direction "%s".', $direction));
$accountType[$direction] = null;
$accountTypeKey = sprintf('%s_type', $direction);
$current[$accountTypeKey] = '';
foreach ($this->accountIdentificationSuffixes as $suffix) {
$key = sprintf('%s_%s', $direction, $suffix);
Log::debug(sprintf('Now working on key "%s".', $key));
// try to find the account
if (array_key_exists($key, $current) && '' !== (string) $current[$key]) {
$foundDirection = $this->getAccountType($suffix, (string) $current[$key], $lessThanZero);
Log::debug(
sprintf('Transaction array has a "%s"-field with value "%s", and its type is "%s".', $key, $current[$key], $foundDirection)
);
// should this overrule any existing account type? Since we work down from ID,
// if it's already known it should not be overruled.
if (null === $foundDirection && null !== $accountType[$direction]) {
Log::debug(sprintf('Found direction is null, but accountType[%s] is not null, so we skip.', $direction));
}
if (null !== $foundDirection && null !== $accountType[$direction] && $foundDirection !== $accountType[$direction]) {
Log::debug(sprintf('Found direction "%s" overrules accountType[%s] "%s".', $foundDirection, $direction, $accountType[$direction]));
$accountType[$direction] = $foundDirection;
$current[$accountTypeKey] = $foundDirection;
Log::debug(sprintf('"%s" = "%s"', $accountTypeKey, $foundDirection));
}
if (null === $accountType[$direction]) {
Log::debug(sprintf('accountType[%s] is set to found direction "%s"', $direction, $foundDirection));
$accountType[$direction] = $foundDirection;
$current[$accountTypeKey] = $foundDirection;
Log::debug(sprintf('"%s" = "%s"', $accountTypeKey, $foundDirection));
}
}
}
}
// TODO catch all cases according lines 281 - 285 and https://docs.firefly-iii.org/fxirefly-iii/financial-concepts/transactions/#:~:text=In%20Firefly%20III%2C%20a%20transaction,slightly%20different%20from%20one%20another.
$sourceIsNull = null === $accountType['source'];
$sourceIsAsset = 'asset' === $accountType['source'];
$sourceIsRevenue = 'revenue' === $accountType['source'];
$destIsAsset = 'asset' === $accountType['destination'];
$destIsExpense = 'expense' === $accountType['destination'];
$sourceIsExpense = 'expense' === $accountType['source'];
$destIsRevenue = 'revenue' === $accountType['destination'];
$destIsNull = null === $accountType['destination'];
switch (true) {
case $sourceIsAsset && $destIsExpense && $lessThanZero:
case $sourceIsAsset && $destIsNull && $lessThanZero:
// there is no expense account, but the account was found under revenue, so we assume this is a withdrawal with a non-existing expense account
case $sourceIsAsset && $destIsRevenue && $lessThanZero:
Log::debug('Based on types, this is a withdrawal');
$current['type'] = 'withdrawal';
break;
case $sourceIsAsset && $destIsRevenue && !$lessThanZero:
case $sourceIsAsset && $destIsNull && !$lessThanZero:
case $sourceIsNull && $destIsAsset:
case $sourceIsRevenue && $destIsAsset:
case $sourceIsAsset && $destIsExpense: // there is no revenue account, but the account was found under expense, so we assume this is a deposit with an non-existing revenue account
Log::debug('Based on types, this is a deposit');
$current['type'] = 'deposit';
break;
case $sourceIsAsset && $destIsAsset:
Log::debug('Based on types, this is a transfer');
$current['type'] = 'transfer'; // line 382 / 383
break;
case $sourceIsExpense && $destIsExpense:
Log::warning('Both types are expense. Impossible. Lets make source_type = "" and type="withdrawal"');
$current['source_type'] = '';
$current['type'] = 'withdrawal';
break;
case $sourceIsRevenue && $destIsRevenue:
Log::warning('Both types are revenue. Impossible. Lets make destination_type = "" and type="deposit"');
$current['destination_type'] = '';
$current['type'] = 'deposit';
break;
case $sourceIsExpense && $destIsNull:
Log::warning('The source is "expense" but the destination is a new account. Weird! Lets make source_type = "" and type="withdrawal"');
$current['source_type'] = '';
$current['type'] = 'withdrawal';
break;
case $sourceIsExpense && $destIsAsset:
Log::warning('The source is "expense" but the destination is an asset. Weird! Lets make source_type = "" and type="deposit"');
$current['source_type'] = '';
$current['type'] = 'deposit';
break;
default:
Log::error(
sprintf(
'Unknown transaction type: source = "%s", destination = "%s". Fall back to "withdrawal"',
null !== $accountType['source'] && '' !== $accountType['source'] && '0' !== $accountType['source'] ? $accountType['source'] : null,
null !== $accountType['destination'] && '' !== $accountType['destination'] && '0' !== $accountType['destination'] ? $accountType['destination'] : null
)
); // 285
$current['type'] = 'withdrawal'; // line 382 / 383
break;
}
// default back to withdrawal.
return $current;
}
private function getAccountType(string $field, string $value, bool $lessThanZero): ?string
{
$count = 0;
$result = null;
$hitField = null; // the field on which we found a match.
foreach ($this->allAccounts as $account) {
// we have a match!
if ((string) $account->{$field} === (string) $value) {
// never found a match before!
if (0 === $count) {
Log::debug(sprintf('Recognized "%s" as a "%s"-account by its "%s".', $value, $account->type, $field));
$result = $account->type;
$hitField = $field;
++$count;
}
// we found a match before, and it's different too.
if (0 !== $count && $account->type !== $result) {
Log::warning(sprintf('Recognized "%s" as a "%s"-account (on the "%s"-field) but ALSO as a "%s"-account (previous match was on the "%s"-field)!', $value, $result, $field, $account->type, $hitField));
// the previous result always trumps the current result because the order of accountIdentificationSuffixes
Log::debug(sprintf('System will keep the previous match and assume account with %s "%s" is a "%s" account', $field, $value, $result));
++$count;
}
// we found a match before and it's different. But the data importer has found both "revenue" AND "expense" accounts. What to do?
$set = [$account->type, $result];
if (0 !== $count && $account->type !== $result && in_array('revenue', $set, true) && in_array('expense', $set, true) && $lessThanZero) {
Log::warning(sprintf('Recognized "%s" as a "%s"-account (on the "%s"-field) but ALSO as a "%s"-account (previous match was on the "%s"-field)!', $value, $result, $field, $account->type, $hitField));
Log::debug('Because amount is less than zero, we assume "expense" is the correct type.');
$result = 'expense';
++$count;
}
// we found a match before and it's different. But: previous result was "expense", current result is "revenue"
if (0 !== $count && $account->type !== $result && in_array('revenue', $set, true) && in_array('expense', $set, true) && !$lessThanZero) {
Log::warning(sprintf('Recognized "%s" as a "%s"-account (on the "%s"-field) but ALSO as a "%s"-account (previous match was on the "%s"-field)!', $value, $result, $field, $account->type, $hitField));
Log::debug('Because amount is more than zero, we assume "revenue" is the correct type.');
$result = 'revenue';
++$count;
}
}
}
if (null === $result) {
Log::debug(sprintf('Unable to recognize the account type of "%s" "%s", or skipped because unsure.', $field, $value));
}
return $result;
}
private function getAccountId($direction, $current): string
{
Log::debug('getAccountId');
foreach ($this->accountIdentificationSuffixes as $suffix) {
$field = sprintf('%s_%s', $direction, $suffix);
if (array_key_exists($field, $current)) {
// there is a value...
foreach ($this->allAccounts as $account) {
// so we check all accounts for a match
if ($current[$field] === $account->{$suffix}) {
// we have a match
// only select accounts that are suitable for the type of transaction
if ($current['amount'] > 0) {
// seems a deposit or transfer
if (in_array($account->type, ['asset', 'revenue'], true)) {
return (string) $account->id;
}
}
if ($current['amount'] < 0) {
// seems a withtrawal or transfer
if (in_array($account->type, ['asset', 'expense'], true)) {
return (string) $account->id;
}
}
Log::warning(sprintf('Just mapped account "%s" (%s)', $account->id, $account->type));
return (string) $account->id;
}
}
// Log::warning(sprintf('Unable to map an account for "%s"',$current[$field]));
}
// Log::warning(sprintf('There is no field for "%s" in the transaction',$direction));
}
return '';
}
private function validAccountInfo(string $direction, array $current): bool
{
// search for existing IBAN
// search for existing number
// search for existing name, TODO under which types?
foreach ($this->accountIdentificationSuffixes as $accountIdentificationSuffix) {
$field = sprintf('%s_%s', $direction, $accountIdentificationSuffix);
if (array_key_exists($field, $current)) {
// there is a value...
foreach ($this->allAccounts as $account) {
// so we check all accounts for a match
if ($current[$field] === $account->{$accountIdentificationSuffix}) {
// we have a match
return true;
}
}
}
}
return false;
}
}