Finanzmanager/tests/39_SepaSchemaTest.php
fds2610-bot 3444f8f6ec Fix #54: Überweisung im SEPA-Format der Bank
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
2026-09-01 18:22:04 +02:00

147 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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,
],
];
}
}