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.
 
 
 
 
 

444 lines
18 KiB

<?php
declare(strict_types=1);
// contains plain-text information as they have to be used for the API or wherever. no objects and stuff
namespace App\Services\Camt;
use Genkgo\Camt\DTO\DomainBankTransactionCode;
use Genkgo\Camt\DTO\Account;
use App\Exceptions\ImporterErrorException;
use App\Services\Shared\Configuration\Configuration;
use Genkgo\Camt\Camt053\DTO\Statement;
use Genkgo\Camt\DTO\Address;
use Genkgo\Camt\DTO\BBANAccount;
use Genkgo\Camt\DTO\Creditor;
use Genkgo\Camt\DTO\Debtor;
use Genkgo\Camt\DTO\Entry;
use Genkgo\Camt\DTO\EntryTransactionDetail;
use Genkgo\Camt\DTO\IbanAccount;
use Genkgo\Camt\DTO\Message;
use Genkgo\Camt\DTO\OtherAccount;
use Genkgo\Camt\DTO\ProprietaryAccount;
use Genkgo\Camt\DTO\RelatedParty;
use Genkgo\Camt\DTO\UnstructuredRemittanceInformation;
use Genkgo\Camt\DTO\UPICAccount;
use Illuminate\Support\Facades\Log;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\DecimalMoneyFormatter;
use Money\Money;
class Transaction
{
public const string TIME_FORMAT = 'Y-m-d H:i:s';
public function __construct(
private readonly Message $levelA,
private readonly Statement $levelB,
private readonly Entry $levelC,
private array $levelD
) {
Log::debug('Constructed a CAMT Transaction');
}
public function countSplits(): int
{
return count($this->levelD);
}
public function getCurrencyCode(int $index): string
{
// TODO loop level D for the date that belongs to the index
return (string) $this->levelC->getAmount()->getCurrency()->getCode();
}
public function getAmount(int $index): string
{
// TODO loop level D for the date that belongs to the index
return (string) $this->getDecimalAmount($this->levelC->getAmount());
}
private function getDecimalAmount(?Money $money): string
{
if (!$money instanceof Money) {
return '';
}
$currencies = new ISOCurrencies();
$moneyDecimalFormatter = new DecimalMoneyFormatter($currencies);
return $moneyDecimalFormatter->format($money);
}
public function getDate(int $index): string
{
// TODO loop level D for the date that belongs to the index
return (string) $this->levelC->getValueDate()->format(self::TIME_FORMAT);
}
/**
* @throws ImporterErrorException
*/
public function getFieldByIndex(string $field, int $index): string
{
Log::debug(sprintf('getFieldByIndex("%s", %d)', $field, $index));
switch ($field) {
default:
// temporary debug message:
// echo sprintf('Unknown field "%s" in getFieldByIndex(%d)', $field, $index);
// echo PHP_EOL;
// exit;
// end temporary debug message
throw new ImporterErrorException(sprintf('Unknown field "%s" in getFieldByIndex(%d)', $field, $index));
// LEVEL A
case 'messageId':
// always the same, since its level A.
return (string) $this->levelA->getGroupHeader()->getMessageId();
// LEVEL B
case 'statementId':
// always the same, since its level B.
return (string) $this->levelB->getId();
case 'statementCreationDate':
// always the same, since its level B.
return (string) $this->levelB->getCreatedOn()->format(self::TIME_FORMAT);
case 'CdtDbtInd':
/** @var null|EntryTransactionDetail $set */
$set = $this->levelD[$index];
return (string) $set?->getCreditDebitIndicator();
case 'statementAccountIban':
if (IbanAccount::class === $this->levelB->getAccount()::class) {
return $this->levelB->getAccount()->getIdentification();
}
return '';
case 'statementAccountNumber':
// always the same, since its level B.
$list = [OtherAccount::class, ProprietaryAccount::class, UPICAccount::class, BBANAccount::class];
$class = $this->levelB->getAccount()::class;
if (in_array($class, $list, true)) {
return $this->levelB->getAccount()->getIdentification();
}
// LEVEL C
return '';
case 'entryAccountServicerReference':
// always the same, since its level C.
return (string) $this->levelC->getAccountServicerReference();
case 'entryReference':
// always the same, since its level C.
return (string) $this->levelC->getReference();
case 'entryAdditionalInfo':
// always the same, since its level C.
return (string) $this->levelC->getAdditionalInfo();
case 'entryAmount':
// always the same, since its level C.
return (string) $this->getDecimalAmount($this->levelC->getAmount());
case 'entryAmountCurrency':
// always the same, since its level C.
return (string) $this->levelC->getAmount()->getCurrency()->getCode();
case 'entryValueDate':
// always the same, since its level C.
return (string) $this->levelC->getValueDate()?->format(self::TIME_FORMAT);
case 'entryBookingDate':
// always the same, since its level C.
return (string) $this->levelC->getBookingDate()?->format(self::TIME_FORMAT);
case 'entryBtcDomainCode':
// always the same, since its level C.
if ($this->levelC->getBankTransactionCode()->getDomain() instanceof DomainBankTransactionCode) {
return (string) $this->levelC->getBankTransactionCode()->getDomain()->getCode();
}
return '';
case 'entryBtcFamilyCode':
$return = '';
// always the same, since its level C.
if ($this->levelC->getBankTransactionCode()->getDomain() instanceof DomainBankTransactionCode) {
$return = (string) $this->levelC->getBankTransactionCode()->getDomain()->getFamily()->getCode();
}
return '';
case 'entryBtcSubFamilyCode':
$return = '';
// always the same, since its level C.
if ($this->levelC->getBankTransactionCode()->getDomain() instanceof DomainBankTransactionCode) {
return (string) $this->levelC->getBankTransactionCode()->getDomain()->getFamily()->getSubFamilyCode();
}
return $return;
// LEVEL D
case 'entryDetailAccountServicerReference':
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
return '';
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
return (string) $info?->getReference()?->getAccountServicerReference();
case 'entryDetailRemittanceInformationUnstructuredBlockMessage':
$result = '';
// this is level D, so grab from level C or loop.
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
Log::debug('There is no info for this thing.');
// TODO return nothing?
return $result;
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
if (null !== $info->getRemittanceInformation()) {
$unstructured = $info->getRemittanceInformation()->getUnstructuredBlocks();
/** @var UnstructuredRemittanceInformation $block */
foreach ($unstructured as $block) {
$result .= sprintf('%s ', $block->getMessage());
}
}
return $result;
case 'entryDetailRemittanceInformationStructuredBlockAdditionalRemittanceInformation':
// this is level D, so grab from level C or loop.
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
// TODO return nothing?
return '';
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index]; // TODO, check if always readable or if we need some checks like with "unstructuredBlockMessage"
// like the unstructured block, these could be multiple blocks, so loop:
if (null !== $info->getRemittanceInformation() && count($info->getRemittanceInformation()->getStructuredBlocks()) > 0) {
$return = '';
foreach ($info->getRemittanceInformation()->getStructuredBlocks() as $block) {
$return .= sprintf('%s ', $block->getAdditionalRemittanceInformation());
}
// #8994 add info.
$string = (string) $info->getRemittanceInformation()?->getCreditorReferenceInformation()?->getRef();
if ('' !== $string) {
return sprintf('%s %s', $return, $string);
}
return $return;
}
return '';
break;
case 'entryDetailAmount':
// this is level D, so grab from level C or loop.
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
return ''; // config.-depending fallback handled in mapping
// return $this->getDecimalAmount($this->levelC->getAmount());
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
return $this->getDecimalAmount($info->getAmount());
case 'entryDetailAmountCurrency':
// this is level D, so grab from level C or loop.
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
return ''; // config.-depending fallback handled in mapping
// return (string)$this->levelC->getAmount()->getCurrency()->getCode();
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
return (string) $info->getAmount()?->getCurrency()?->getCode();
case 'entryDetailBtcDomainCode':
// this is level D, so grab from level C or loop.
$return = '';
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
// return (string)$this->levelC->getBankTransactionCode()->getDomain()->getCode();
return $return; // config.-depending fallback handled in mapping
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
if (null !== $info->getBankTransactionCode()->getDomain()) {
return (string) $info->getBankTransactionCode()->getDomain()->getCode();
}
return $return;
case 'entryDetailBtcFamilyCode':
// this is level D, so grab from level C or loop.
$return = '';
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
// return (string)$this->levelC->getBankTransactionCode()->getDomain()->getFamily()->getCode();
return $return; // config.-depending fallback handled in mapping
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
if (null !== $info->getBankTransactionCode()->getDomain()) {
return (string) $info->getBankTransactionCode()->getDomain()->getFamily()->getCode();
}
return $return;
case 'entryDetailBtcSubFamilyCode':
// this is level D, so grab from level C or loop.
$return = '';
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
// return (string)$this->levelC->getBankTransactionCode()->getDomain()->getFamily()->getSubFamilyCode();
return $return; // config.-depending fallback handled in mapping
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
if (null !== $info->getBankTransactionCode()->getDomain()) {
return (string) $info->getBankTransactionCode()->getDomain()->getFamily()->getSubFamilyCode();
}
return $return;
case 'entryDetailOpposingAccountIban':
$result = '';
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
return $result;
}
/** @var null|EntryTransactionDetail $info */
$info = $this->levelD[$index] ?? null;
if (null !== $info) {
$opposingAccount = $this->getOpposingParty($info)?->getAccount();
if (is_object($opposingAccount) && IbanAccount::class === $opposingAccount::class) {
$result = (string) $opposingAccount->getIdentification();
}
}
return $result;
case 'entryDetailOpposingAccountNumber':
$result = '';
$list = [OtherAccount::class, ProprietaryAccount::class, UPICAccount::class, BBANAccount::class];
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
return $result;
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
$opposingAccount = $this->getOpposingParty($info)?->getAccount();
$class = $opposingAccount instanceof Account ? $opposingAccount::class : '';
if (in_array($class, $list, true)) {
return (string) $opposingAccount->getIdentification();
}
return $result;
case 'entryDetailOpposingName':
$result = '';
if (0 === count($this->levelD) || !array_key_exists($index, $this->levelD)) {
return $result;
}
/** @var EntryTransactionDetail $info */
$info = $this->levelD[$index];
$opposingParty = $this->getOpposingParty($info);
if (!$opposingParty instanceof RelatedParty) {
Log::debug('In entryDetailOpposingName, opposing party is NULL, return "".');
}
if ($opposingParty instanceof RelatedParty) {
return $this->getOpposingName($opposingParty);
}
return $result;
}
}
/**
* @return null|Creditor|Debtor
*/
private function getOpposingParty(EntryTransactionDetail $transactionDetail): ?RelatedParty
{
Log::debug('getOpposingParty(), interested in Creditor.');
$relatedParties = $transactionDetail->getRelatedParties();
$targetRelatedPartyObject = Creditor::class;
// get amount from "getAmount":
$amount = $transactionDetail?->getAmount()?->getAmount();
if (null !== $amount) {
Log::debug(sprintf('Amount in getAmount() is "%s"', $amount));
}
if (null === $amount) {
$amount = $transactionDetail->getAmountDetails()?->getAmount();
Log::debug(sprintf('Amount in getAmountDetails() is "%s"', $amount));
}
if (null !== $amount && $amount > 0) { // which part in this array is the interesting one?
Log::debug('getOpposingParty(), interested in Debtor!');
$targetRelatedPartyObject = Debtor::class;
}
foreach ($relatedParties as $relatedParty) {
Log::debug(sprintf('Found related party of type "%s"', $relatedParty->getRelatedPartyType()::class));
if ($relatedParty->getRelatedPartyType()::class === $targetRelatedPartyObject) {
Log::debug('This is the type we are looking for!');
return $relatedParty;
}
}
Log::debug('getOpposingParty(), no opposing party found, return NULL.');
return null;
}
private function getOpposingName(RelatedParty $relatedParty): string
{
$opposingName = '';
// TODO make depend on configuration
if ('' === (string) $relatedParty->getRelatedPartyType()->getName()) {
// there is no "name", so use the address instead
$opposingName = $this->generateAddressLine($relatedParty->getRelatedPartyType()->getAddress());
}
if ('' !== (string) $relatedParty->getRelatedPartyType()->getName()) {
// there is a name
$opposingName = $relatedParty->getRelatedPartyType()->getName();
// but maybe you want also the entire address
// 2025-07-19: method is always uses $useEntireAddress=false, nobody uses this.
// if ($useEntireAddress && $addressLine = $this->generateAddressLine($relatedParty->getRelatedPartyType()->getAddress())) {
// $opposingName .= sprintf(', %s', $addressLine);
// }
}
return $opposingName;
}
private function generateAddressLine(?Address $address = null): string
{
return implode(', ', $address->getAddressLines());
}
}