Die Nachricht wurde fest als pain.001.001.03 erzeugt. Banken, die nur das ältere DK-Format pain.001.003.03 führen, weisen die Überweisung damit schon beim Einreichen ab, noch vor der TAN. FinTsUeberweisung liest die unterstützten Formate nach der Anmeldung aus dem Segment HISPAS; SepaXml::schemaWaehlen() bestimmt daraus den Namensraum – bevorzugt ISO, sonst DK. Für die Einzelüberweisung sind beide Fassungen im Aufbau gleich, es genügt der andere Namensraum. Das DK-Schema liegt der Testsuite bei, damit die Nachricht wirklich dagegen geprüft wird. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iURiWYZJQYVGQNXFFGyWu
147 lines
5.4 KiB
PHP
147 lines
5.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Finanzmanager\Services\SepaXml;
|
||
|
||
/**
|
||
* Die SEPA-Nachricht im Format, das die Bank annimmt.
|
||
*
|
||
* Nicht jede Bank führt das ISO-Format pain.001.001.03. Etliche Häuser nehmen
|
||
* ausschließlich das ältere DK-Format pain.001.003.03 an und weisen alles
|
||
* andere schon beim Einreichen ab ("The bank does not support the XML schema
|
||
* …"). Welche Formate eine Bank führt, meldet sie im Segment HISPAS.
|
||
*
|
||
* Das DK-Schema liegt als `tests/schemata/pain.001.003.03.xsd` bei, damit die
|
||
* erzeugte Nachricht hier wirklich geprüft wird und nicht nur auf Textbausteine
|
||
* hin abgeklopft.
|
||
*/
|
||
class SepaSchemaTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 39: SEPA-Schema der Überweisung';
|
||
|
||
private const XSD = __DIR__ . '/schemata/pain.001.003.03.xsd';
|
||
|
||
public function run(): void
|
||
{
|
||
$this->schemaFolgtDenAngabenDerBank();
|
||
$this->unbekannteFormateFuehrenZuNull();
|
||
$this->dkFormatIstSchemakonform();
|
||
$this->dasFormatStehtImNamensraum();
|
||
}
|
||
|
||
private function schemaFolgtDenAngabenDerBank(): void
|
||
{
|
||
$this->assertEquals(
|
||
SepaXml::SCHEMA_PAIN_001_001_03,
|
||
SepaXml::schemaWaehlen([]),
|
||
'Ohne Angabe der Bank bleibt es beim Vorzugsformat'
|
||
);
|
||
|
||
$this->assertEquals(
|
||
SepaXml::SCHEMA_PAIN_001_003_03,
|
||
SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_003_03, SepaXml::SCHEMA_PAIN_001_003_03]),
|
||
'Führt die Bank nur das DK-Format, wird es genommen'
|
||
);
|
||
|
||
$this->assertEquals(
|
||
SepaXml::SCHEMA_PAIN_001_001_03,
|
||
SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_003_03, SepaXml::SCHEMA_PAIN_001_001_03]),
|
||
'Bei mehreren Formaten hat das ISO-Format Vorrang'
|
||
);
|
||
|
||
// Manche Banken hängen dem Namensraum eine Fassung des Deutschen
|
||
// Kreditgewerbes an – der Vergleich muss das aushalten.
|
||
$this->assertEquals(
|
||
SepaXml::SCHEMA_PAIN_001_001_03,
|
||
SepaXml::schemaWaehlen([SepaXml::SCHEMA_PAIN_001_001_03 . '_GBIC_4']),
|
||
'Der Zusatz _GBIC_4 stört den Vergleich nicht'
|
||
);
|
||
}
|
||
|
||
private function unbekannteFormateFuehrenZuNull(): void
|
||
{
|
||
$this->assertNull(
|
||
SepaXml::schemaWaehlen(['urn:iso:std:iso:20022:tech:xsd:pain.001.001.09']),
|
||
'Führt die Bank nur Formate, die hier niemand erzeugt, kommt null zurück'
|
||
);
|
||
}
|
||
|
||
private function dkFormatIstSchemakonform(): void
|
||
{
|
||
foreach ($this->auftraege() as $bezeichnung => $auftrag) {
|
||
$xml = SepaXml::ueberweisung($auftrag, SepaXml::SCHEMA_PAIN_001_003_03);
|
||
|
||
$dokument = new DOMDocument();
|
||
$dokument->loadXML($xml);
|
||
|
||
$vorher = libxml_use_internal_errors(true);
|
||
$gueltig = $dokument->schemaValidate(self::XSD);
|
||
$meldungen = array_map(
|
||
static fn(LibXMLError $fehler): string => trim($fehler->message),
|
||
libxml_get_errors()
|
||
);
|
||
libxml_clear_errors();
|
||
libxml_use_internal_errors($vorher);
|
||
|
||
$this->assert(
|
||
$gueltig,
|
||
'pain.001.003.03 (' . $bezeichnung . ') ist schemakonform' . ($gueltig ? '' : ': ' . implode(' | ', $meldungen))
|
||
);
|
||
}
|
||
}
|
||
|
||
private function dasFormatStehtImNamensraum(): void
|
||
{
|
||
$auftrag = $this->auftraege()['mit BIC'];
|
||
|
||
$iso = SepaXml::ueberweisung($auftrag, SepaXml::SCHEMA_PAIN_001_001_03);
|
||
$dk = SepaXml::ueberweisung($auftrag, SepaXml::SCHEMA_PAIN_001_003_03);
|
||
|
||
$this->assertEquals(
|
||
SepaXml::SCHEMA_PAIN_001_001_03,
|
||
(string)(simplexml_load_string($iso)?->getDocNamespaces()[''] ?? ''),
|
||
'Der Namensraum des ISO-Formats'
|
||
);
|
||
$this->assertEquals(
|
||
SepaXml::SCHEMA_PAIN_001_003_03,
|
||
(string)(simplexml_load_string($dk)?->getDocNamespaces()[''] ?? ''),
|
||
'Der Namensraum des DK-Formats'
|
||
);
|
||
|
||
// Ohne ausdrückliches Format bleibt es beim ISO-Format – der Aufruf aus
|
||
// dem Zahlungsverkehr setzt es sonst still um.
|
||
$this->assert(
|
||
str_contains(SepaXml::ueberweisung($auftrag), 'pain.001.001.03'),
|
||
'Ohne Angabe wird weiterhin pain.001.001.03 erzeugt'
|
||
);
|
||
}
|
||
|
||
/** @return array<string,array<string,mixed>> */
|
||
private function auftraege(): array
|
||
{
|
||
return [
|
||
'mit BIC' => [
|
||
'absender_name' => 'Max Mustermann',
|
||
'absender_iban' => 'DE02 1203 0000 0000 2020 51',
|
||
'absender_bic' => 'BYLADEM1001',
|
||
'empfaenger_name' => 'Stadtwerke Größenwahn',
|
||
'empfaenger_iban' => 'DE02500105170137075030',
|
||
'empfaenger_bic' => 'INGDDEFFXXX',
|
||
'betrag' => 123.45,
|
||
'waehrung' => 'EUR',
|
||
'verwendungszweck' => 'Abschlag Strom & Wasser 09/2026',
|
||
'ausfuehrung_am' => '2026-09-05',
|
||
],
|
||
'ohne BIC' => [
|
||
'absender_name' => 'Max Mustermann',
|
||
'absender_iban' => 'DE02120300000000202051',
|
||
'empfaenger_name' => 'Stadtwerke',
|
||
'empfaenger_iban' => 'DE02500105170137075030',
|
||
'betrag' => 1.0,
|
||
'verwendungszweck' => '',
|
||
'ausfuehrung_am' => null,
|
||
],
|
||
];
|
||
}
|
||
}
|