PayPal-Name aus der manuellen Zuordnung lernen
Die automatische Zuordnung verlangt exakte Uebereinstimmung des Zahlernamens mit Anzeigename oder PayPal-Name des Mitglieds. Heisst jemand bei PayPal anders, landet jede Zahlung in der Warteschlange - auch die zehnte, obwohl der Fall laengst einmal von Hand geklaert wurde. Nach einer manuellen Zuordnung wird der Zahlername deshalb beim Mitglied hinterlegt. Zurueckhaltend: ein gepflegter PayPal-Name wird nie ueberschrieben, ein Name gleich dem Anzeigenamen bringt nichts, und passt der Name auch auf ein anderes Mitglied, wird nichts gelernt - das wuerde die Zuordnung mehrdeutig machen und damit blockieren. Was gelernt wurde, steht in der Erfolgsmeldung und im Audit-Log; eine stillschweigende Aenderung am Mitglied waere sonst schwer nachvollziehbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+59
-1
@@ -334,6 +334,52 @@ function paypal_fetch_unmatched(PDO $pdo, int $tenantId): array
|
|||||||
*
|
*
|
||||||
* @return array{ok:bool, error?:string}
|
* @return array{ok:bool, error?:string}
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Soll der Zahlername als PayPal-Name beim Mitglied hinterlegt werden?
|
||||||
|
* Liefert den zu speichernden Namen oder null, wenn nichts zu lernen ist.
|
||||||
|
*
|
||||||
|
* Zurueckhaltend absichtlich: ein bereits gepflegter PayPal-Name wird nie
|
||||||
|
* ueberschrieben, ein Name gleich dem Anzeigenamen bringt nichts (der wird
|
||||||
|
* ohnehin geprueft), und wenn der Name im Mandanten nicht eindeutig waere,
|
||||||
|
* wuerde das Merken die automatische Zuordnung sogar blockieren - dann
|
||||||
|
* lieber weiter von Hand zuordnen.
|
||||||
|
*/
|
||||||
|
function paypal_remember_payer_name(PDO $pdo, int $tenantId, int $participantId, string $payerName): ?string
|
||||||
|
{
|
||||||
|
$payerName = trim($payerName);
|
||||||
|
if ($payerName === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT display_name, paypal_name FROM participants WHERE id = ? AND tenant_id = ?');
|
||||||
|
$stmt->execute([$participantId, $tenantId]);
|
||||||
|
$participant = $stmt->fetch();
|
||||||
|
if ($participant === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (trim((string) ($participant['paypal_name'] ?? '')) !== '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (strcasecmp(trim((string) $participant['display_name']), $payerName) === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wuerde der Name auf ein weiteres Mitglied passen, bliebe die Zuordnung
|
||||||
|
// mehrdeutig (imports_find_participant verlangt genau einen Treffer).
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT COUNT(*) FROM participants
|
||||||
|
WHERE tenant_id = ?
|
||||||
|
AND id <> ?
|
||||||
|
AND (LOWER(paypal_name) = LOWER(?) OR LOWER(display_name) = LOWER(?))'
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId, $participantId, $payerName, $payerName]);
|
||||||
|
if ((int) $stmt->fetchColumn() > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payerName;
|
||||||
|
}
|
||||||
|
|
||||||
function paypal_assign_payment(PDO $pdo, int $tenantId, int $paymentId, int $participantId, ?int $actorUserId): array
|
function paypal_assign_payment(PDO $pdo, int $tenantId, int $paymentId, int $participantId, ?int $actorUserId): array
|
||||||
{
|
{
|
||||||
$stmt = $pdo->prepare("SELECT id, net_cents, status, payer_name, note, paid_at FROM paypal_payments WHERE id = ? AND tenant_id = ?");
|
$stmt = $pdo->prepare("SELECT id, net_cents, status, payer_name, note, paid_at FROM paypal_payments WHERE id = ? AND tenant_id = ?");
|
||||||
@@ -349,11 +395,22 @@ function paypal_assign_payment(PDO $pdo, int $tenantId, int $paymentId, int $par
|
|||||||
return ['ok' => false, 'error' => 'Das gewählte Mitglied wurde nicht gefunden.'];
|
return ['ok' => false, 'error' => 'Das gewählte Mitglied wurde nicht gefunden.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aus der Handarbeit lernen: heisst jemand bei PayPal anders als in der
|
||||||
|
// Kaffeeliste, greift die automatische Zuordnung nie - denn sie verlangt
|
||||||
|
// exakte Uebereinstimmung. Nach der ersten manuellen Zuordnung wird der
|
||||||
|
// Zahlername deshalb beim Mitglied hinterlegt, damit die naechste Zahlung
|
||||||
|
// von allein ankommt.
|
||||||
|
$gemerkterName = paypal_remember_payer_name($pdo, $tenantId, $participantId, (string) $payment['payer_name']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
$ledgerId = paypal_book_payment($pdo, $tenantId, $participant, (int) $payment['net_cents'], $actorUserId, paypal_booking_note($payment));
|
$ledgerId = paypal_book_payment($pdo, $tenantId, $participant, (int) $payment['net_cents'], $actorUserId, paypal_booking_note($payment));
|
||||||
$pdo->prepare("UPDATE paypal_payments SET participant_id = ?, ledger_entry_id = ?, status = 'booked' WHERE id = ?")
|
$pdo->prepare("UPDATE paypal_payments SET participant_id = ?, ledger_entry_id = ?, status = 'booked' WHERE id = ?")
|
||||||
->execute([$participantId, $ledgerId, $paymentId]);
|
->execute([$participantId, $ledgerId, $paymentId]);
|
||||||
|
if ($gemerkterName !== null) {
|
||||||
|
$pdo->prepare('UPDATE participants SET paypal_name = ? WHERE id = ? AND tenant_id = ?')
|
||||||
|
->execute([$gemerkterName, $participantId, $tenantId]);
|
||||||
|
}
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
if ($pdo->inTransaction()) {
|
if ($pdo->inTransaction()) {
|
||||||
@@ -366,9 +423,10 @@ function paypal_assign_payment(PDO $pdo, int $tenantId, int $paymentId, int $par
|
|||||||
app_audit_log($pdo, $tenantId, $actorUserId, 'paypal_import.manual_assigned', 'paypal_payment', $paymentId, [
|
app_audit_log($pdo, $tenantId, $actorUserId, 'paypal_import.manual_assigned', 'paypal_payment', $paymentId, [
|
||||||
'participant_id' => $participantId,
|
'participant_id' => $participantId,
|
||||||
'net_cents' => (int) $payment['net_cents'],
|
'net_cents' => (int) $payment['net_cents'],
|
||||||
|
'learned_paypal_name' => $gemerkterName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return ['ok' => true];
|
return ['ok' => true, 'learned_paypal_name' => $gemerkterName];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -204,3 +204,21 @@ automatisch gutgeschrieben.
|
|||||||
- Die Zuordnungsseite bleibt für offene Zahlungen erreichbar (siehe
|
- Die Zuordnungsseite bleibt für offene Zahlungen erreichbar (siehe
|
||||||
`paypal_inbox_accessible()`), der Mandant sieht die geparkte Zahlung
|
`paypal_inbox_accessible()`), der Mandant sieht die geparkte Zahlung
|
||||||
also und kann sie zuordnen oder abhaken.
|
also und kann sie zuordnen oder abhaken.
|
||||||
|
|
||||||
|
### PayPal-Name lernen
|
||||||
|
|
||||||
|
Die automatische Zuordnung verlangt **exakte** Übereinstimmung des
|
||||||
|
Zahlernamens mit `display_name` oder `paypal_name` des Mitglieds
|
||||||
|
(`imports_find_participant()`, genau ein Treffer). Heißt jemand bei PayPal
|
||||||
|
anders — Zweitname, Firmenkonto, anderer Nachname —, landet jede Zahlung in
|
||||||
|
der Warteschlange, auch die zehnte.
|
||||||
|
|
||||||
|
`paypal_assign_payment()` hinterlegt den Zahlernamen deshalb beim Mitglied,
|
||||||
|
sobald eine Zahlung von Hand zugeordnet wurde; die nächste Zahlung desselben
|
||||||
|
Zahlers wird automatisch gebucht. Bewusst zurückhaltend
|
||||||
|
(`paypal_remember_payer_name()`): ein bereits gepflegter PayPal-Name wird nie
|
||||||
|
überschrieben, ein Name gleich dem Anzeigenamen bringt nichts, und passt der
|
||||||
|
Name auch auf ein anderes Mitglied, wird nichts gelernt — sonst wäre die
|
||||||
|
Zuordnung mehrdeutig und würde gar nicht mehr greifen. Was gelernt wurde,
|
||||||
|
steht in der Erfolgsmeldung und im Audit-Log
|
||||||
|
(`paypal_import.manual_assigned`, Feld `learned_paypal_name`).
|
||||||
|
|||||||
@@ -108,8 +108,16 @@ if ($hasAccess && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$flash = ['type' => 'error', 'text' => 'Bitte ein Mitglied auswählen.'];
|
$flash = ['type' => 'error', 'text' => 'Bitte ein Mitglied auswählen.'];
|
||||||
} else {
|
} else {
|
||||||
$result = paypal_assign_payment($pdo, $tenantId, $paymentId, $participantId, $actorUserId);
|
$result = paypal_assign_payment($pdo, $tenantId, $paymentId, $participantId, $actorUserId);
|
||||||
|
$erfolgstext = 'Zahlung wurde zugeordnet und als Einzahlung gebucht.';
|
||||||
|
// Wurde dabei der PayPal-Name gelernt, gehoert das in die Meldung -
|
||||||
|
// sonst wirkt die stillschweigende Aenderung am Mitglied wie Magie.
|
||||||
|
if (($result['learned_paypal_name'] ?? null) !== null) {
|
||||||
|
$erfolgstext .= ' Der PayPal-Name „' . $result['learned_paypal_name']
|
||||||
|
. '" ist jetzt bei diesem Mitglied hinterlegt – künftige Zahlungen'
|
||||||
|
. ' werden automatisch zugeordnet.';
|
||||||
|
}
|
||||||
$flash = $result['ok']
|
$flash = $result['ok']
|
||||||
? ['type' => 'success', 'text' => 'Zahlung wurde zugeordnet und als Einzahlung gebucht.']
|
? ['type' => 'success', 'text' => $erfolgstext]
|
||||||
: ['type' => 'error', 'text' => $result['error'] ?? 'Die Zuordnung ist fehlgeschlagen.'];
|
: ['type' => 'error', 'text' => $result['error'] ?? 'Die Zuordnung ist fehlgeschlagen.'];
|
||||||
}
|
}
|
||||||
} elseif ($aktion === 'ignorieren') {
|
} elseif ($aktion === 'ignorieren') {
|
||||||
|
|||||||
@@ -129,6 +129,35 @@ try {
|
|||||||
paypal_flow_assert('unbekannter Zahler landet in der Warteschlange', ($ergebnisB['status'] ?? '') === 'unmatched', $failures, $passes);
|
paypal_flow_assert('unbekannter Zahler landet in der Warteschlange', ($ergebnisB['status'] ?? '') === 'unmatched', $failures, $passes);
|
||||||
paypal_flow_assert('Warteschlange enthaelt die Zahlung', paypal_count_unmatched($pdo, $tenantA) === 1, $failures, $passes);
|
paypal_flow_assert('Warteschlange enthaelt die Zahlung', paypal_count_unmatched($pdo, $tenantA) === 1, $failures, $passes);
|
||||||
|
|
||||||
|
// --- Der PayPal-Name wird aus der manuellen Zuordnung gelernt: beim
|
||||||
|
// naechsten Mal soll dieselbe Person automatisch getroffen werden.
|
||||||
|
$abweichend = paypal_flow_participant($pdo, $tenantA, "Bernd Beispiel {$namensSuffix}", $suffix . 'x');
|
||||||
|
$codeE = 'TXNLERN' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
|
$zahlername = "Bernd B. Beispiel-Handel {$namensSuffix}";
|
||||||
|
$mailE = paypal_flow_mail($zahlername, '4,00', $codeE, 'erste Zahlung');
|
||||||
|
$ergebnisE = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailE);
|
||||||
|
paypal_flow_assert('abweichender Zahlername landet zunaechst in der Warteschlange', ($ergebnisE['status'] ?? '') === 'unmatched', $failures, $passes);
|
||||||
|
|
||||||
|
$zuordnung = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisE['payment_id'], $abweichend, null);
|
||||||
|
paypal_flow_assert('manuelle Zuordnung bucht die Zahlung', ($zuordnung['ok'] ?? false) === true, $failures, $passes);
|
||||||
|
paypal_flow_assert('manuelle Zuordnung merkt sich den PayPal-Namen', ($zuordnung['learned_paypal_name'] ?? null) === $zahlername, $failures, $passes);
|
||||||
|
|
||||||
|
$codeF = 'TXNAUTO2' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
|
$mailF = paypal_flow_mail($zahlername, '4,00', $codeF, 'zweite Zahlung');
|
||||||
|
$ergebnisF = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailF);
|
||||||
|
paypal_flow_assert('zweite Zahlung desselben Zahlers wird automatisch gebucht', ($ergebnisF['status'] ?? '') === 'booked', $failures, $passes);
|
||||||
|
paypal_flow_assert('beide Zahlungen sind gutgeschrieben', paypal_flow_balance($pdo, $abweichend) === 800, $failures, $passes);
|
||||||
|
|
||||||
|
// Ein bereits gepflegter PayPal-Name wird nicht ueberschrieben.
|
||||||
|
$codeG = 'TXNKEEP' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
|
$mailG = paypal_flow_mail('Ganz Anderer Name', '2,00', $codeG, 'fremder Name');
|
||||||
|
$ergebnisG = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailG);
|
||||||
|
$zuordnungG = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisG['payment_id'], $abweichend, null);
|
||||||
|
paypal_flow_assert('gepflegter PayPal-Name bleibt unveraendert', ($zuordnungG['learned_paypal_name'] ?? null) === null, $failures, $passes);
|
||||||
|
$gespeichert = $pdo->prepare('SELECT paypal_name FROM participants WHERE id = ?');
|
||||||
|
$gespeichert->execute([$abweichend]);
|
||||||
|
paypal_flow_assert('gespeichert ist weiterhin der erste Zahlername', (string) $gespeichert->fetchColumn() === $zahlername, $failures, $passes);
|
||||||
|
|
||||||
// --- Fall 3: Mandant hat PayPal abgeschaltet -> parken, nicht buchen
|
// --- Fall 3: Mandant hat PayPal abgeschaltet -> parken, nicht buchen
|
||||||
$tenantB = paypal_flow_tenant($pdo, $suffix, 'b', $tenantIds);
|
$tenantB = paypal_flow_tenant($pdo, $suffix, 'b', $tenantIds);
|
||||||
$participantB = paypal_flow_participant($pdo, $tenantB, "Max Mustermann {$namensSuffix}", $suffix);
|
$participantB = paypal_flow_participant($pdo, $tenantB, "Max Mustermann {$namensSuffix}", $suffix);
|
||||||
|
|||||||
Reference in New Issue
Block a user