SEPA-Import über SepaMandate createfull API
Beschreibung
Folgend mein PHP-Script für die Stapelerzeugung von SEPA-Mandaten über die createfull API zusammen mit exemplarischen Mitgliedsdaten als CSV.
Ich habe es damals für den Import der Daten von GLS-Vereinsmeister in CiviCRM genutzt.
Zu beachten:
- Mitglieder werden hier aus historischen Gründen über die „external ID“ identifiziert
- CiviCRM wird als Plugin in Wordpress gehostet
- Im PHP-Script muss in Zeile 30 der Pfad zur wp-load.php definiert werden
- Ab Zeile 200 sind die Parameter für den Import hardcoded definiert
Exemplarischer Aufruf:
sudo -u MEINBENUTZER /pfad/zur/bin/php sepaMandatImporter.php sepaimport.csv
Bei weiteren Rückfragen gerne melden.
CSV-Datei: sepaimport.csv (Mitgliederdaten)
Mitgliedsnummer;IBAN;Kontoinhaber;Zahlungsart;Jahresbeitrag;Austrittsdatum;BIC;Datum LS-Mandat;frequency_unit;cycle_day
2;;;rechnung;;;;;year;1
3;DE89 3704 0044 0532 0130 00 ;;lastschrift;35;;COBADEFFXXX;06.03.2014;year;1
6;DE89 3704 0044 0532 0130 00 ;Max Mustermann;lastschrift;20;;COBADEFFXXX;06.03.2014;year;1
7;DE89 3704 0044 0532 0130 00 ;;lastschrift;20;;COBADEFFXXX;06.03.2014;year;1
PHP-Datei: sepaMandatImporter.php (Script)
#!/usr/bin/env php
<?php
/**
* import-sepa-mandates.php
*
* CLI script to import SEPA mandates into CiviCRM (WordPress).
*
* Usage:
* php import-sepa-mandates.php /pfad/zu/mandate.csv [--dry-run]
*
* Requirements:
* - PHP CLI with access to your WordPress/Civi installation.
* - Adjust WP_LOAD_PATH to your wp-load.php path.
*
* Notes:
* - This script resolves the contact by external_identifier = Mitgliedsnummer.
* - Only rows with Zahlungsart containing "SEPA" or "Lastschrift" are processed.
* - cycle_day (Abbuchungstag) is set to 1 .
*/
if ($argc < 2) {
echo "Usage: php {$argv[0]} /path/to/mandates.csv [--dry-run]\n";
exit(1);
}
$csvFile = $argv[1];
$dryRun = in_array('--dry-run', $argv, true);
// === CONFIG: Pfad zur wp-load.php ===
define('WP_LOAD_PATH', '/PATH/TO/WORDPRESS/wp-load.php');
// ======================================================
if (!file_exists(WP_LOAD_PATH)) {
fwrite(STDERR, "ERROR: wp-load.php nicht gefunden unter " . WP_LOAD_PATH . "\n");
exit(2);
}
// Bootstrap WordPress (und damit CiviCRM, wenn Plugin aktiv)
require_once WP_LOAD_PATH;
// Try to ensure CiviCRM is initialized
try {
if (!function_exists('civicrm_api3')) {
// attempt to load CiviCRM if it's not already loaded
if (defined('WP_CONTENT_DIR')) {
// nothing more here — in most WP installs requiring wp-load is enough
}
}
// ensure core config available
if (class_exists('CRM_Core_Config')) {
CRM_Core_Config::singleton();
}
} catch (Exception $e) {
fwrite(STDERR, "WARN: CiviCRM Bootstrap Problem: " . $e->getMessage() . "\n");
// continue, will likely fail on api call if not available
}
// Log setup
$logDir = sys_get_temp_dir();
$logFile = $logDir . DIRECTORY_SEPARATOR . 'import-sepa-mandates.log';
$logHandle = fopen($logFile, 'a');
fwrite($logHandle, "[" . date('c') . "] Starting import (dry-run=" . ($dryRun ? 'yes' : 'no') . ")\n");
if (!file_exists($csvFile)) {
fwrite(STDERR, "ERROR: CSV-Datei nicht gefunden: $csvFile\n");
fwrite($logHandle, "ERROR: CSV-Datei nicht gefunden: $csvFile\n");
fclose($logHandle);
exit(3);
}
// Try to autodetect delimiter (comma or semicolon)
$firstLine = trim(file($csvFile)[0]);
$delimiter = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
if (($handle = fopen($csvFile, 'r')) === false) {
fwrite(STDERR, "ERROR: Konnte CSV nicht öffnen: $csvFile\n");
fwrite($logHandle, "ERROR: Konnte CSV nicht öffnen: $csvFile\n");
fclose($logHandle);
exit(4);
}
// read header
$header = fgetcsv($handle, 0, $delimiter);
if ($header === false) {
fwrite(STDERR, "ERROR: CSV hat keine Kopfzeile oder ist leer\n");
fwrite($logHandle, "ERROR: CSV hat keine Kopfzeile oder ist leer\n");
fclose($handle);
fclose($logHandle);
exit(5);
}
// normalize header keys (trim)
foreach ($header as &$h) {
$h = trim($h);
}
unset($h);
// Helper: German date d.m.Y -> Y-m-d
function parseGermanDate($v) {
$v = trim($v);
if ($v === '' || $v === null) return null;
// if already in Y-m-d
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $v)) return $v;
// handle dd.mm.YYYY or d.m.YYYY
$parts = preg_split('/[\.\/\-]/', $v);
if (count($parts) >= 3) {
$d = str_pad($parts[0], 2, '0', STR_PAD_LEFT);
$m = str_pad($parts[1], 2, '0', STR_PAD_LEFT);
$y = $parts[2];
if (strlen($y) === 2) {
// assume 19xx/20xx? We'll assume 19 if > 30 else 20
$y = (intval($y) > 30) ? '19'.$y : '20'.$y;
}
return "$y-$m-$d";
}
return null;
}
// Helper: sanitize IBAN
function sanitizeIban($iban) {
$iban = strtoupper(preg_replace('/[^A-Z0-9]/', '', $iban));
return $iban;
}
// Process rows
$rowNo = 1;
$created = 0;
$skipped = 0;
$errors = 0;
while (($row = fgetcsv($handle, 0, $delimiter)) !== false) {
$rowNo++;
// combine
$data = array_combine($header, $row);
// normalize keys
foreach ($data as $k => $v) {
$data[$k] = trim($v);
}
// Only SEPA/Lastschrift rows
$zahlungsart = mb_strtolower($data['Zahlungsart'] ?? '');
if ($zahlungsart === '') {
fwrite($logHandle, "[".date('c')."] Row $rowNo: Zahlungsart leer — skipped\n");
$skipped++;
continue;
}
if (strpos($zahlungsart, 'sepa') === false && strpos($zahlungsart, 'lastschrift') === false) {
fwrite($logHandle, "[".date('c')."] Row $rowNo: Nicht SEPA ($zahlungsart) — skipped\n");
$skipped++;
continue;
}
$externalId = $data['Mitgliedsnummer'] ?? '';
if ($externalId === '') {
fwrite($logHandle, "[".date('c')."] Row $rowNo: Keine Mitgliedsnummer (external_identifier) — skipped\n");
$skipped++;
continue;
}
// find contact by external_identifier
try {
$contactResp = civicrm_api3('Contact', 'get', [
'external_identifier' => $externalId,
'sequential' => 1,
'return' => ['id']
]);
} catch (Exception $e) {
fwrite($logHandle, "[".date('c')."] Row $rowNo: Fehler Contact.get für external_identifier={$externalId}: " . $e->getMessage() . "\n");
$errors++;
continue;
}
if (empty($contactResp['values']) || empty($contactResp['values'][0]['id'])) {
fwrite($logHandle, "[".date('c')."] Row $rowNo: Kontakt mit external_identifier={$externalId} nicht gefunden — skipped\n");
$skipped++;
continue;
}
$contactId = $contactResp['values'][0]['id'];
// prepare mandate data
$ibanRaw = $data['IBAN'] ?? '';
$iban = sanitizeIban($ibanRaw);
$bic = strtoupper(trim($data['BIC'] ?? ''));
$accountHolder = $data['Kontoinhaber'] ?? '';
// if Kontoinhaber empty, attempt to use Vorname + Nachname
if ($accountHolder === '') {
$accountHolder = trim(($data['Vorname'] ?? '') . ' ' . ($data['Nachname'] ?? ''));
}
$amountRaw = $data['Jahresbeitrag'] ?? '';
// remove commas, dots etc and normalize to float
$amount = null;
if ($amountRaw !== '') {
// replace comma decimal with dot
$amountNormalized = str_replace('.', '', $amountRaw); // remove thousands dot
$amountNormalized = str_replace(',', '.', $amountNormalized);
$amount = floatval($amountNormalized);
}
$type = strtoupper(trim($data['Type'] ?? ($data['type'] ?? 'RCUR')));
if ($type === '') $type = 'RCUR';
$lsDatumRaw = $data['Datum LS-Mandat'] ?? ($data['Datum LS Mandat'] ?? '');
$startDate = parseGermanDate($lsDatumRaw);
if ($startDate === null) {
// Fallback: Eintrittsdatum
$startDate = parseGermanDate($data['Eintrittsdatum'] ?? '');
}
if ($startDate === null) {
fwrite($logHandle, "[".date('c')."] Row $rowNo: Kein gültiges Datum für Mandatsbeginn (Datum LS-Mandat) — skipped\n");
$skipped++;
continue;
}
// frequency unit
$frequencyUnit = strtolower(trim($data['frequency_unit'] ?? 'year'));
if ($frequencyUnit === '') $frequencyUnit = 'year';
// build API params for SepaMandate.createfull
$params = [
'contact_id' => $contactId,
'iban' => $iban,
'bic' => $bic,
'account_holder' => $accountHolder,
'type' => $type,
// signature Date
'date' => $startDate,
// Wann soll der 1. Einzug erfolgen?
'start_date' => parseGermanDate("01.02.2026"),
'creation_date' => parseGermanDate("13.12.2025"),
'frequency_unit' => $frequencyUnit,
// frequency_interval default 1 (yearly)
'frequency_interval' => 1,
'amount' => $amount,
'cycle_day' => 1,
// initial status, FRST used for first collection in some setups
'status' => 'FRST',
// source for traceability
'source' => 'Importiert am DATUM aus ALTESPROGRAMM',
// financial_type
"financial_type_id" => 2, // <---- bitte ggf. anpassen
];
// Remove empty values to avoid API errors for empty strings
foreach ($params as $k => $v) {
if ($v === '' || $v === null) {
unset($params[$k]);
}
}
// Dry run output
if ($dryRun) {
echo "[DRY] Row $rowNo -> contact_id={$contactId}, iban={$iban}, bic={$bic}, amount={$amount}, start_date={$startDate}, type={$type}\n";
fwrite($logHandle, "[".date('c')."] [DRY] Row $rowNo prepared: " . json_encode($params) . "\n");
$created++;
continue;
}
// Call API
try {
$result = civicrm_api3('SepaMandate', 'createfull', $params);
if (!empty($result['is_error'])) {
fwrite($logHandle, "[".date('c')."] Row $rowNo: API returned error: " . json_encode($result) . "\n");
$errors++;
continue;
} else {
$mandateId = $result['id'] ?? ($result['values'][0]['id'] ?? null);
echo "OK: Row $rowNo -> Mandat erstellt (ID: {$mandateId}) für Kontakt {$contactId}\n";
fwrite($logHandle, "[".date('c')."] Row $rowNo -> Mandat erstellt (ID: {$mandateId}) contact_id={$contactId}\n");
$contribution_recur_id = $result['values'][$result['id']]['entity_id'];
echo "contribution_recur_id ist $contribution_recur_id für Kontakt {$contactId}\n";
fwrite($logHandle, "[".date('c')."] Die contribution_recur_id ist $contribution_recur_id \n");
// MitgliedsID für KontaktID finden
$memberships = civicrm_api3('Membership', 'get', [
'contact_id' => $contactId,
'status_id' => ['IN' => ['New', 'Current']],
]);
// Ersten Treffer aller Mitgliedschaften nehmen
if (!empty($memberships['values'])) {
$first_membership = reset($memberships['values']); // reset() gibt das erste Element des Arrays
$membership_id = $first_membership['id'];
echo "Passende MembershipID $membership_id gefunden für Kontakt {$contactId}\n";
// 2. Membership auf diese Contribution verweisen
civicrm_api3('Membership', 'create', [
'id' => $membership_id,
'custom_6' => $contribution_recur_id,
]);
echo "Passende MembershipID $membership_id die wiederkehrende Zahlung $contribution_recur_id zugewiesen \n";
} else {
throw new Exception("Keine Membership für Contact ID $contact_id gefunden.");
}
$created++;
}
} catch (Exception $e) {
fwrite($logHandle, "[".date('c')."] Row $rowNo: Ausnahme bei API-Aufruf: " . $e->getMessage() . "\n");
echo "ERROR: Row $rowNo: " . $e->getMessage() . "\n";
$errors++;
continue;
}
}
fclose($handle);
fwrite($logHandle, "[".date('c')."] Finished: created={$created}, skipped={$skipped}, errors={$errors}\n");
fclose($logHandle);
echo "Done. Log: $logFile\n";
exit(0);
Weitere Informationen
Serverumgebung:
- PHP-Version: 8.4
- CiviCRM-Version: 6.9
- In Wordpress-Umgebung
- Linux Ubuntu-Server (22.04) mit Plesk