diff --git a/app/imports.php b/app/imports.php index d3a4a67..c7bef11 100644 --- a/app/imports.php +++ b/app/imports.php @@ -39,14 +39,16 @@ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array FROM participants WHERE tenant_id = ? AND (LOWER(paypal_name) = LOWER(?) OR LOWER(display_name) = LOWER(?)) - LIMIT 1' + ORDER BY id + LIMIT 2' ); $stmt->execute([$tenantId, $name, $name]); - $row = $stmt->fetch(); + $rows = $stmt->fetchAll(); - if ($row === false) { + if (count($rows) !== 1) { return null; } + $row = $rows[0]; return [ 'participant_id' => (int)$row['id'], diff --git a/einzahlung.php b/einzahlung.php index 2c9c951..6c0fffd 100644 --- a/einzahlung.php +++ b/einzahlung.php @@ -1,5 +1,7 @@ $rohBetrag, 'bemerkung' => $bemerkung]; + $name = $namen[$participantId] ?? ('Teilnehmer ' . $participantId); if ($rohBetrag === '') { // Leere Zeile: nur meckern, wenn trotzdem eine Bemerkung dransteht @@ -102,16 +115,23 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) { continue; } - $anzahlBetrag = floatval(str_replace(',', '.', $rohBetrag)); - if ($anzahlBetrag == 0.0) { + $betragCents = saas_parse_money_cents($rohBetrag); + if ($betragCents === null) { + $validierungsFehler[] = sprintf( + '%s: Der Betrag "%s" ist kein gültiger Geldbetrag.', + $name, + $rohBetrag + ); continue; } - $name = $namen[$participantId] ?? ('Teilnehmer ' . $participantId); + if ($betragCents === 0) { + continue; + } // Abzuege muessen begruendet werden, sonst ist spaeter nicht mehr // nachvollziehbar, warum jemandem Geld abgezogen wurde. - if ($anzahlBetrag < 0 && $bemerkung === '') { + if ($betragCents < 0 && $bemerkung === '') { $validierungsFehler[] = sprintf( '%s: Bei einem Abzug (negativer Betrag) ist eine Bemerkung Pflicht.', $name @@ -120,18 +140,18 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) { } // Groessenordnungs-Tippfehler abfangen (500 statt 5,00). - if (abs($anzahlBetrag) > EINZAHLUNG_MAX_BETRAG) { + if (abs($betragCents) > EINZAHLUNG_MAX_BETRAG_CENTS) { $validierungsFehler[] = sprintf( '%s: %s € übersteigt die Plausibilitätsgrenze von %s €. Bitte prüfen oder in mehreren Schritten eintragen.', $name, - number_format($anzahlBetrag, 2, ',', '.'), + saas_format_money_cents($betragCents), number_format(EINZAHLUNG_MAX_BETRAG, 2, ',', '.') ); continue; } $zuBuchen[$participantId] = [ - 'betrag' => $anzahlBetrag, + 'betrag_cents' => $betragCents, 'bemerkung' => $bemerkung !== '' ? $bemerkung : null, ]; } @@ -147,7 +167,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) { $pdo, $tenantId, $participantId, - (int)round($zeile['betrag'] * 100), + (int)$zeile['betrag_cents'], 'manual_bulk', $saasUser['user_id'] ?? null, $zeile['bemerkung'] @@ -157,6 +177,9 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) { $pdo->commit(); $eingaben = []; // Erfolgreich gebucht: Formular wieder leeren. + $_SESSION['flash_einzahlung'] = ['type' => 'success', 'count' => $eingetragen]; + header('Location: einzahlung.php'); + exit; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); @@ -188,10 +211,11 @@ $mitarbeiter = einzahlung_fetch_participants($pdo, $tenantId, $action);

Die Einzahlungen konnten nicht gespeichert werden.

- -

Einträge erfolgreich hinzugefügt.

+ +

+

In der Bemerkung könnt ihr festhalten, worum es bei einer Buchung ging. Ein negativer Betrag bucht einen Abzug (z. B. Auszahlung bei Austritt oder eine Erstattung) und braucht deshalb immer eine Bemerkung.

diff --git a/hinweise.php b/hinweise.php index b18b58f..1651aec 100644 --- a/hinweise.php +++ b/hinweise.php @@ -1,5 +1,7 @@ Kaffeeliste - Hinweise"; + $meldung = null; + $fehler = null; + + if (isset($_SESSION['flash_hinweise']) && is_array($_SESSION['flash_hinweise'])) { + $flash = $_SESSION['flash_hinweise']; + unset($_SESSION['flash_hinweise']); + $meldung = isset($flash['meldung']) ? (string)$flash['meldung'] : null; + $fehler = isset($flash['fehler']) ? (string)$flash['fehler'] : null; + } // Hinweis speichern oder als geloescht markieren if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -44,26 +54,50 @@ if($hasAccess){ $id = (int)($_POST['id'] ?? 0); if ($id > 0 && notices_soft_delete($pdo, $tenantId, $id)) { app_audit_log($pdo, $tenantId, $actorUserId, 'notice.deleted', 'notice', $id); + $meldung = 'Hinweis wurde gelöscht.'; + } else { + $fehler = 'Der Hinweis konnte nicht gelöscht werden.'; } } else { - $nachricht = $_POST['nachricht'] ?? ''; - $gueltig_bis = $_POST['gueltig_bis'] ?? ''; // z.B. "2025-09-03T14:00" + $nachricht = trim((string)($_POST['nachricht'] ?? '')); + $gueltig_bis = trim((string)($_POST['gueltig_bis'] ?? '')); // z.B. "2025-09-03T14:00" $dt = DateTime::createFromFormat('Y-m-d\TH:i', $gueltig_bis); + $dtErrors = DateTime::getLastErrors(); - if ($dt) { + if ($nachricht === '') { + $fehler = 'Bitte eine Nachricht eintragen.'; + } elseif ($dt === false || ($dtErrors !== false && ((int)$dtErrors['warning_count'] > 0 || (int)$dtErrors['error_count'] > 0))) { + $fehler = 'Bitte ein gültiges Datum mit Uhrzeit angeben.'; + } else { $gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00" notices_create($pdo, $tenantId, (string)$nachricht, $gueltig_bis_sql, $actorUserId); app_audit_log($pdo, $tenantId, $actorUserId, 'notice.created', 'notice', null, ['valid_until' => $gueltig_bis_sql]); + $meldung = 'Hinweis wurde gespeichert.'; } } + + $_SESSION['flash_hinweise'] = [ + 'meldung' => $meldung, + 'fehler' => $fehler, + ]; + header('Location: hinweise.php'); + exit; } $hinweise = notices_fetch_all($pdo, $tenantId); ?> +

Kaffeeliste - Hinweise

+ + +

+ + +

+

Neuen Hinweis hinzufügen

-
+ "> @@ -74,20 +108,22 @@ if($hasAccess){

Alle Hinweise

- -
-
- Gültig bis: format('d.m.Y H:i')); ?>
-
" onsubmit="return confirm('Diesen Hinweis wirklich löschen?')"> - - - - -
-
- - - + +

Aktuell sind keine Hinweise angelegt.

+ + +
+
+ Gültig bis: format('d.m.Y H:i')); ?>
+
" onsubmit="return confirm('Diesen Hinweis wirklich löschen?')"> + + + + +
+
+ + $meldung, + 'fehler' => $fehler, + 'einladungslink' => $einladungslink, + ]; + header('Location: mitarbeiterverwalten.php'); + exit; + } } @@ -227,15 +246,6 @@ if($hasAccess){ } ?> - - - - - - Kaffeeliste - Mitglieder verwalten - - -

Mitglieder verwalten

@@ -415,10 +425,6 @@ if($hasAccess){ - - - - $cookies - * @return array{status:int, body:string} + * @return array{status:int, body:string, location:?string} */ function bc_request(string $url, array &$cookies, string $method = 'GET', ?string $postBody = null): array { @@ -43,6 +48,7 @@ function bc_request(string $url, array &$cookies, string $method = 'GET', ?strin $body = @file_get_contents($url, false, $context); $responseHeaders = $http_response_header ?? []; $status = 0; + $location = null; foreach ($responseHeaders as $header) { if (preg_match('~^HTTP/\S+\s+(\d{3})~', $header, $m) === 1) { @@ -51,9 +57,28 @@ function bc_request(string $url, array &$cookies, string $method = 'GET', ?strin if (preg_match('/^Set-Cookie:\s*([^=;]+)=([^;]+)/i', $header, $m) === 1) { $cookies[$m[1]] = $m[2]; } + if (preg_match('/^Location:\s*(.+)$/i', $header, $m) === 1) { + $location = trim($m[1]); + } } - return ['status' => $status, 'body' => (string)$body]; + return ['status' => $status, 'body' => (string)$body, 'location' => $location]; +} + +function bc_follow_redirect(array $response, array &$cookies, string $baseUrl): array +{ + if ($response['status'] < 300 || $response['status'] >= 400 || $response['location'] === null) { + return $response; + } + + $location = (string)$response['location']; + if (preg_match('~^https?://~i', $location) === 1) { + $url = $location; + } else { + $url = $baseUrl . '/' . ltrim($location, '/'); + } + + return bc_request($url, $cookies); } function bc_csrf(string $body): string @@ -121,38 +146,38 @@ bc_request("{$baseUrl}/login.php", $cookies, 'POST', http_build_query([ // 9 aktive Teilnehmer vorhanden (Cap 10) -> ein aktives Anlegen muss klappen (10/10). $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -$body10 = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +$body10 = bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Zehntes Mitglied', 'email' => "billingcap-tenth-{$suffix}@test.local", 'paypalname' => '', 'aktiv' => '1', -])); +])), $cookies, $baseUrl); bc_assert('10. aktives Mitglied bei Cap 10 wird angelegt', str_contains($body10['body'], 'wurde angelegt'), $failures, $passes); // Jetzt bei 10/10 -> ein weiteres aktives Anlegen muss abgelehnt werden. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -$body11 = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +$body11 = bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Elftes Mitglied', 'email' => "billingcap-eleventh-{$suffix}@test.local", 'paypalname' => '', 'aktiv' => '1', -])); +])), $cookies, $baseUrl); bc_assert('11. aktives Mitglied bei Cap 10 wird abgelehnt', str_contains($body11['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes); bc_assert('11. Mitglied wurde nicht in der DB angelegt', (int)$pdo->query("SELECT COUNT(*) FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-eleventh-{$suffix}@test.local'")->fetchColumn() === 0, $failures, $passes); // Inaktives Anlegen bei vollem Cap muss weiterhin moeglich sein. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -$bodyInactive = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +$bodyInactive = bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Inaktives Mitglied', 'email' => "billingcap-inactive-{$suffix}@test.local", 'paypalname' => '', -])); +])), $cookies, $baseUrl); bc_assert('Inaktives Mitglied bei vollem Cap wird angelegt', str_contains($bodyInactive['body'], 'wurde angelegt'), $failures, $passes); $inactiveId = (int)$pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-inactive-{$suffix}@test.local'")->fetchColumn(); @@ -160,45 +185,45 @@ bc_assert('Neu angelegtes Mitglied ist tatsaechlich inaktiv', (int)$pdo->query(" // Aktivieren des inaktiven Mitglieds bei vollem Cap muss abgelehnt werden. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -$bodyActivateBlocked = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +$bodyActivateBlocked = bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'aktivieren', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$inactiveId, -])); +])), $cookies, $baseUrl); bc_assert('Aktivieren bei vollem Cap wird abgelehnt', str_contains($bodyActivateBlocked['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes); bc_assert('Mitglied ist nach abgelehntem Aktivieren weiterhin inaktiv', (int)$pdo->query("SELECT active FROM participants WHERE id = {$inactiveId}")->fetchColumn() === 0, $failures, $passes); // Ein aktives Mitglied deaktivieren (Platz schaffen), dann klappt Aktivieren. $freeUpId = $extraParticipantIds[0]; $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'deaktivieren', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$freeUpId, -])); +])), $cookies, $baseUrl); $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -$bodyActivateOk = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +$bodyActivateOk = bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'aktivieren', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$inactiveId, -])); +])), $cookies, $baseUrl); bc_assert('Aktivieren nach Platzschaffen (Deaktivierung) klappt', str_contains($bodyActivateOk['body'], 'wurde aktiviert'), $failures, $passes); // bearbeitenspeichern: ein weiteres inaktives Mitglied anlegen, dann per // Bearbeiten-Formular aktivieren versuchen, wieder bei vollem Cap (10/10). $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Editier-Testmitglied', 'email' => "billingcap-edit-{$suffix}@test.local", 'paypalname' => '', -])); +])), $cookies, $baseUrl); $editId = (int)$pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-edit-{$suffix}@test.local'")->fetchColumn(); $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -$bodyEditBlocked = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +$bodyEditBlocked = bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'bearbeitenspeichern', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$editId, @@ -206,19 +231,19 @@ $bodyEditBlocked = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, ' 'email' => "billingcap-edit-{$suffix}@test.local", 'paypalname' => '', 'aktiv' => '1', -])); +])), $cookies, $baseUrl); bc_assert('bearbeitenspeichern-Reaktivierung bei vollem Cap wird abgelehnt', str_contains($bodyEditBlocked['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes); // Editieren ohne Statuswechsel (bleibt inaktiv) muss weiterhin klappen. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); -$bodyEditOk = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ +$bodyEditOk = bc_follow_redirect(bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'bearbeitenspeichern', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$editId, 'name' => 'Editier-Testmitglied Neu', 'email' => "billingcap-edit-{$suffix}@test.local", 'paypalname' => '', -])); +])), $cookies, $baseUrl); bc_assert('bearbeitenspeichern ohne Aktivierung bei vollem Cap klappt', str_contains($bodyEditOk['body'], 'wurde gespeichert'), $failures, $passes); // Aufraeumen diff --git a/scripts/check-konto-und-mandantenwechsel.php b/scripts/check-konto-und-mandantenwechsel.php index bd0a1c6..dbeafbb 100644 --- a/scripts/check-konto-und-mandantenwechsel.php +++ b/scripts/check-konto-und-mandantenwechsel.php @@ -2,6 +2,11 @@ declare(strict_types=1); +if (PHP_SAPI !== 'cli') { + http_response_code(403); + exit("Dieses Skript ist nur fuer die Kommandozeile gedacht.\n"); +} + /** * Prueft die vier Kontofunktionen, die vor dem Go-Live ergaenzt wurden: * @@ -78,6 +83,22 @@ function konto_request(string $url, array &$cookies, string $method = 'GET', ?st return ['status' => $status, 'body' => (string)$body, 'location' => $location]; } +function konto_follow_redirect(array $response, array &$cookies, string $baseUrl): array +{ + if ($response['status'] < 300 || $response['status'] >= 400 || $response['location'] === null) { + return $response; + } + + $location = (string)$response['location']; + if (preg_match('~^https?://~i', $location) === 1) { + $url = $location; + } else { + $url = $baseUrl . '/' . ltrim($location, '/'); + } + + return konto_request($url, $cookies); +} + /** @param array $cookies */ function konto_csrf(string $url, array &$cookies): string { @@ -160,12 +181,12 @@ try { // 3. Verifikation wird fuer Einladungen erzwungen // --------------------------------------------------------------- $csrf = konto_csrf("{$baseUrl}/mitarbeiterverwalten.php", $cookies); - $einladung = konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ + $einladung = konto_follow_redirect(konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'aktion' => 'zugang_gewaehren', 'mitgliedID' => (string)$participantId, 'rolle' => 'member', - ])); + ])), $cookies, $baseUrl); pruefe( 'Einladung ohne bestaetigte Adresse wird abgelehnt', str_contains($einladung['body'], 'bestätigt sein') @@ -179,12 +200,12 @@ try { $pdo->prepare('UPDATE users SET email_verified_at = NOW() WHERE id = ?')->execute([$userId]); $csrf = konto_csrf("{$baseUrl}/mitarbeiterverwalten.php", $cookies); - $einladung = konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ + $einladung = konto_follow_redirect(konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'aktion' => 'zugang_gewaehren', 'mitgliedID' => (string)$participantId, 'rolle' => 'member', - ])); + ])), $cookies, $baseUrl); pruefe( 'Einladung mit bestaetigter Adresse funktioniert', str_contains($einladung['body'], 'Zugang wurde gewährt') diff --git a/scripts/check-payment-matching.php b/scripts/check-payment-matching.php new file mode 100644 index 0000000..393944e --- /dev/null +++ b/scripts/check-payment-matching.php @@ -0,0 +1,161 @@ +prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)'); + $stmt->execute(["payment-match-{$key}-{$suffix}", "Payment Match {$key}", 'active']); + + return (int)$pdo->lastInsertId(); +} + +function payment_matching_insert_participant(PDO $pdo, int $tenantId, string $displayName, string $email, ?string $paypalName): int +{ + $stmt = $pdo->prepare( + 'INSERT INTO participants (tenant_id, display_name, email, email_norm, paypal_name, active) VALUES (?, ?, ?, ?, ?, 1)' + ); + $stmt->execute([$tenantId, $displayName, $email, strtolower($email), $paypalName]); + + return (int)$pdo->lastInsertId(); +} + +function payment_matching_cleanup(PDO $pdo, array $tenantIds): void +{ + foreach ($tenantIds as $tenantId) { + $pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tenantId]); + $pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]); + } +} + +$pdo = dev_pdo(); +$suffix = bin2hex(random_bytes(4)); +$failures = []; +$passes = 0; +$tenantIds = []; + +try { + $tenantId = payment_matching_insert_tenant($pdo, $suffix, 'a'); + $otherTenantId = payment_matching_insert_tenant($pdo, $suffix, 'b'); + $tenantIds = [$tenantId, $otherTenantId]; + + $uniqueId = payment_matching_insert_participant( + $pdo, + $tenantId, + "Eindeutig {$suffix}", + "unique-{$suffix}@test.local", + "PayPal Unique {$suffix}" + ); + payment_matching_insert_participant( + $pdo, + $tenantId, + "Doppelt {$suffix}", + "duplicate-one-{$suffix}@test.local", + null + ); + payment_matching_insert_participant( + $pdo, + $tenantId, + "Doppelt {$suffix}", + "duplicate-two-{$suffix}@test.local", + null + ); + payment_matching_insert_participant( + $pdo, + $tenantId, + "Paypal A {$suffix}", + "paypal-a-{$suffix}@test.local", + "PayPal Doppelt {$suffix}" + ); + payment_matching_insert_participant( + $pdo, + $tenantId, + "Paypal B {$suffix}", + "paypal-b-{$suffix}@test.local", + "PayPal Doppelt {$suffix}" + ); + $otherTenantUniqueId = payment_matching_insert_participant( + $pdo, + $otherTenantId, + "Doppelt {$suffix}", + "other-{$suffix}@test.local", + null + ); + + $uniqueByName = imports_find_participant($pdo, $tenantId, "Eindeutig {$suffix}"); + payment_matching_assert( + 'eindeutiger Anzeigename wird gefunden', + $uniqueByName !== null && $uniqueByName['participant_id'] === $uniqueId, + $failures, + $passes + ); + + $uniqueByPaypalName = imports_find_participant($pdo, $tenantId, "paypal unique {$suffix}"); + payment_matching_assert( + 'eindeutiger PayPal-Name wird case-insensitive gefunden', + $uniqueByPaypalName !== null && $uniqueByPaypalName['participant_id'] === $uniqueId, + $failures, + $passes + ); + + payment_matching_assert( + 'doppelter Anzeigename bleibt unmatched', + imports_find_participant($pdo, $tenantId, "Doppelt {$suffix}") === null, + $failures, + $passes + ); + + payment_matching_assert( + 'doppelter PayPal-Name bleibt unmatched', + imports_find_participant($pdo, $tenantId, "PayPal Doppelt {$suffix}") === null, + $failures, + $passes + ); + + $otherTenantMatch = imports_find_participant($pdo, $otherTenantId, "Doppelt {$suffix}"); + payment_matching_assert( + 'gleicher Name in anderem Mandanten bleibt eindeutig', + $otherTenantMatch !== null && $otherTenantMatch['participant_id'] === $otherTenantUniqueId, + $failures, + $passes + ); +} catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + fwrite(STDERR, "Payment matching check failed to run: {$e->getMessage()}\n"); + payment_matching_cleanup($pdo, $tenantIds); + exit(1); +} + +payment_matching_cleanup($pdo, $tenantIds); + +if ($failures !== []) { + echo "\nPayment matching check failed with " . count($failures) . " failure(s):\n"; + foreach ($failures as $failure) { + echo "- {$failure}\n"; + } + exit(1); +} + +echo "\nPayment matching check passed with {$passes} assertions.\n"; diff --git a/scripts/send-payment-reminders.php b/scripts/send-payment-reminders.php index 9380e6e..496dbf8 100644 --- a/scripts/send-payment-reminders.php +++ b/scripts/send-payment-reminders.php @@ -47,6 +47,8 @@ foreach ($tenants as $tenantIdRaw) { $schwelleCents = (int)$settings['negative_warning_cents']; // <= 0 $intervallTage = max(1, (int)$settings['payment_reminder_interval_days']); $jetzt = new DateTimeImmutable('now'); + $tenantGesendet = 0; + $tenantUebersprungen = 0; $teilnehmer = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]); $letzteErinnerungen = saas_fetch_last_payment_reminders($pdo, $tenantId); @@ -68,11 +70,13 @@ foreach ($tenants as $tenantIdRaw) { try { $letzte = new DateTimeImmutable($letzteErinnerungen[$participantId]); if ($letzte->add(new DateInterval("P{$intervallTage}D")) > $jetzt) { + $tenantUebersprungen++; $gesamtUebersprungen++; continue; } } catch (Throwable $e) { // Unparsebares Datum: sicherheitshalber ueberspringen. + $tenantUebersprungen++; $gesamtUebersprungen++; continue; } @@ -84,6 +88,7 @@ foreach ($tenants as $tenantIdRaw) { if ($dryRun) { echo "[dry-run] Mandant {$tenantId}: {$person['display_name']} <{$email}> offen " . saas_format_money_cents($debtCents) . " EUR\n"; + $tenantGesendet++; $gesamtGesendet++; continue; } @@ -94,12 +99,16 @@ foreach ($tenants as $tenantIdRaw) { $sendResult['ok'] ? 'sent' : 'failed', $sendResult['error'] ?? null, null ); if ($sendResult['ok']) { + $tenantGesendet++; $gesamtGesendet++; } } if (!$dryRun) { - app_audit_log($pdo, $tenantId, null, 'payment_reminder.batch', 'tenant', $tenantId, ['gesendet' => $gesamtGesendet]); + app_audit_log($pdo, $tenantId, null, 'payment_reminder.batch', 'tenant', $tenantId, [ + 'gesendet' => $tenantGesendet, + 'uebersprungen' => $tenantUebersprungen, + ]); } } diff --git a/stricheintragen.php b/stricheintragen.php index bea4064..7d72131 100644 --- a/stricheintragen.php +++ b/stricheintragen.php @@ -1,5 +1,7 @@ STRICHE_MAX_PREIS_CENTS) { + $validierungsFehler[] = 'Die Kosten pro Strich überschreiten die Plausibilitätsgrenze von 100,00 €. Bitte prüfen.'; + } // Teilnehmer des Mandanten - dient der Zugehoerigkeitspruefung, damit ueber // das Formular keine fremden IDs bebucht werden koennen. $eigeneTeilnehmer = []; - $stmt = $pdo->prepare('SELECT id FROM participants WHERE tenant_id = ?'); + $stmt = $pdo->prepare('SELECT id, display_name FROM participants WHERE tenant_id = ?'); $stmt->execute([$tenantId]); - foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $id) { - $eigeneTeilnehmer[(int)$id] = true; + foreach ($stmt->fetchAll() as $row) { + $eigeneTeilnehmer[(int)$row['id']] = (string)$row['display_name']; } - try { - $pdo->beginTransaction(); + $zuBuchen = []; + foreach ($_POST["anzahlStriche"] ?? [] as $participantId => $anzahlStriche) { + $participantId = (int)$participantId; + if ($participantId <= 0 || !array_key_exists($participantId, $eigeneTeilnehmer)) { + continue; + } - foreach ($_POST["anzahlStriche"] ?? [] as $participantId => $anzahlStriche) { - $participantId = (int)$participantId; - $anzahlStriche = (int)$anzahlStriche; - if ($participantId <= 0 || $anzahlStriche === 0 || !isset($eigeneTeilnehmer[$participantId])) { - continue; + $rohStriche = trim((string)$anzahlStriche); + $eingaben[$participantId] = $rohStriche; + if ($rohStriche === '' || $rohStriche === '0') { + continue; + } + + $name = $eigeneTeilnehmer[$participantId]; + if (!preg_match('/^[0-9]+$/', $rohStriche)) { + $validierungsFehler[] = sprintf('%s: Bitte eine ganze positive Strich-Anzahl eintragen.', $name); + continue; + } + + $anzahlStriche = (int)$rohStriche; + if ($anzahlStriche > STRICHE_MAX_PRO_PERSON) { + $validierungsFehler[] = sprintf( + '%s: %d Striche überschreiten die Plausibilitätsgrenze von %d pro Buchung.', + $name, + $anzahlStriche, + STRICHE_MAX_PRO_PERSON + ); + continue; + } + + $zuBuchen[$participantId] = $anzahlStriche; + } + + if ($validierungsFehler === [] && $zuBuchen === []) { + $validierungsFehler[] = 'Bitte mindestens eine Strich-Anzahl eintragen.'; + } + + if ($validierungsFehler === []) { + try { + $pdo->beginTransaction(); + + foreach ($zuBuchen as $participantId => $anzahlStriche) { + ledger_record_consumption($pdo, $tenantId, $participantId, $anzahlStriche, (int)$unitPriceCents, 'manual_bulk'); + $eingetragen++; } - ledger_record_consumption($pdo, $tenantId, $participantId, $anzahlStriche, $unitPriceCents, 'manual_bulk'); - $eingetragen++; + $pdo->commit(); + $_SESSION['flash_stricheintragen'] = ['type' => 'success', 'count' => $eingetragen]; + header('Location: stricheintragen.php?action=' . urlencode($action)); + exit; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + $fehlgeschlagen = true; } - - $pdo->commit(); - } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - $fehlgeschlagen = true; } - - $action = 'alle'; -} else { - $action = (string)($_GET['action'] ?? 'alle'); } $mitarbeiter = stricheintragen_fetch_participants($pdo, $tenantId, $action); $settings = saas_fetch_tenant_settings($pdo, $tenantId); -$kostenproStrichAnzeige = number_format(($settings['mark_price_cents'] ?? 20) / 100, 2, '.', ''); +$kostenproStrichAnzeige = $kostenproStrichEingabe !== null + ? $kostenproStrichEingabe + : number_format(($settings['mark_price_cents'] ?? 20) / 100, 2, '.', ''); ?>

Anzahl der Striche für alle Mitarbeiter

- + +
+
    + +
  • + +
+
+

Die Einträge konnten nicht gespeichert werden.

- -

Einträge erfolgreich hinzugefügt.

+ +

+