Schalter fuer den Hinweis auf die Kaffeeliste

Der Hinweis unten rechts laesst sich ab einem bezahlten Tarif in den
Mandant-Einstellungen abschalten (neue Spalte tenant_settings.
watermark_enabled, Standard an). Im kostenlosen Tarif bleibt er fest
eingeschaltet: das Kaestchen ist angehakt und disabled, daneben steht der
Grund. Zusaetzlich erzwingt saas_update_tenant_settings() den Wert dort noch
einmal, damit ein nachgebauter POST ihn nicht abschalten kann.

Der Titel des Wasserzeichens spricht nicht mehr vom kostenlosen Tarif -
zahlende Mandanten koennen es jetzt freiwillig zeigen.

Der Smoke-Test prueft die Einstellungsseite jetzt auch angemeldet. Sein
Muster fuer PHP-Meldungen verlangt dafuer einen Doppelpunkt, sonst haette
das Feld "negative_warning" als Warnung gezaehlt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 00:27:44 +02:00
co-authored by Claude Opus 5
parent 1d53b66e3f
commit b244451c42
8 changed files with 153 additions and 16 deletions
+18 -4
View File
@@ -44,7 +44,10 @@ function plan_watermark_plan_code(PDO $pdo, int $tenantId): string
}
/**
* Zeigt dieser Mandant das Wasserzeichen? Nur der kostenlose Tarif tut das.
* Zeigt dieser Mandant das Wasserzeichen? Im kostenlosen Tarif immer - es ist
* der Gegenwert fuer die kostenlose Nutzung und deshalb nicht abschaltbar.
* Alle anderen entscheiden ueber die Mandant-Einstellung
* `watermark_enabled` (Standard: an).
*/
function plan_watermark_visible(PDO $pdo, int $tenantId): bool
{
@@ -52,7 +55,18 @@ function plan_watermark_visible(PDO $pdo, int $tenantId): bool
return false;
}
return plan_watermark_plan_code($pdo, $tenantId) === 'free';
$planCode = plan_watermark_plan_code($pdo, $tenantId);
if ($planCode === 'free') {
return true;
}
if ($planCode === 'unknown') {
return false;
}
require_once __DIR__ . '/features.php';
return app_tenant_switch_enabled($pdo, $tenantId, 'watermark_enabled');
}
/**
@@ -84,10 +98,10 @@ function plan_watermark_html(): string
. '<span class="plan-watermark-text">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>';
if ($url === '') {
return '<div class="plan-watermark" title="Kostenloser Tarif der Kaffeeliste">' . $inner . '</div>';
return '<div class="plan-watermark" title="Kaffeeliste die digitale Kaffeekasse">' . $inner . '</div>';
}
return '<a class="plan-watermark" href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '"'
. ' target="_blank" rel="noopener noreferrer"'
. ' title="Diese Kaffeeliste läuft im kostenlosen Tarif">' . $inner . '</a>';
. ' title="Kaffeeliste die digitale Kaffeekasse">' . $inner . '</a>';
}
+18 -4
View File
@@ -288,7 +288,8 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array
ts.bank_account_holder,
ts.bank_iban,
ts.brand_logo,
ts.brand_color
ts.brand_color,
ts.watermark_enabled
FROM tenants t
LEFT JOIN tenant_settings ts ON ts.tenant_id = t.id
WHERE t.id = ?
@@ -336,6 +337,7 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array
'bank_iban' => (string)$settings['bank_iban'],
'brand_logo' => (string)$settings['brand_logo'],
'brand_color' => (string)$settings['brand_color'],
'watermark_enabled' => (int)$settings['watermark_enabled'],
];
}
@@ -425,6 +427,16 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr
$pdfShowEmptyRows = !empty($input['pdf_show_empty_rows']) ? 1 : 0;
$paymentReminderEnabled = !empty($input['payment_reminder_enabled']) ? 1 : 0;
// Der Hinweis auf die Kaffeeliste laesst sich abschalten - ausser im
// kostenlosen Tarif. Die Pruefung steht hier und nicht nur im Formular,
// damit auch ein nachgebauter POST das Kaestchen nicht leeren kann. Vor
// der Transaktion, weil billing_fetch_or_init() eine fehlende Zeile
// anlegt.
$watermarkEnabled = !empty($input['watermark_enabled']) ? 1 : 0;
if (billing_fetch_or_init($pdo, $tenantId)['plan_code'] === 'free') {
$watermarkEnabled = 1;
}
try {
$pdo->beginTransaction();
@@ -443,8 +455,8 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr
pdf_watermark_text, pdf_footer_text,
payment_reminder_enabled, payment_reminder_interval_days,
cash_enabled, cash_contact, bank_transfer_enabled,
bank_account_holder, bank_iban, brand_color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
bank_account_holder, bank_iban, brand_color, watermark_enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
mark_price_cents = VALUES(mark_price_cents),
self_entry_enabled = VALUES(self_entry_enabled),
@@ -464,7 +476,8 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr
bank_transfer_enabled = VALUES(bank_transfer_enabled),
bank_account_holder = VALUES(bank_account_holder),
bank_iban = VALUES(bank_iban),
brand_color = VALUES(brand_color)'
brand_color = VALUES(brand_color),
watermark_enabled = VALUES(watermark_enabled)'
);
$stmt->execute([
$tenantId,
@@ -487,6 +500,7 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr
$bankAccountHolder,
$bankIban,
$brandColor,
$watermarkEnabled,
]);
$pdo->commit();
+14
View File
@@ -215,3 +215,17 @@ a.plan-watermark {
display: none;
}
}
/* ---------------------------------------------------------------------------
* Festgestellte Kaestchen (z. B. der Hinweis auf die Kaffeeliste im
* kostenlosen Tarif)
*
* Das Theme zeichnet das Kaestchen ueber das folgende <label>; ein disabled
* gesetztes Feld saehe sonst genauso aus wie ein bedienbares. Der gedimmte
* Zustand macht sichtbar, dass hier nichts zu klicken ist - die Begruendung
* steht als Hinweis daneben.
* ------------------------------------------------------------------------ */
input[type="checkbox"]:disabled + label {
cursor: not-allowed;
opacity: 0.6;
}
@@ -0,0 +1,9 @@
-- Schalter fuer den Hinweis auf die Kaffeeliste (Logo unten rechts).
--
-- Standard ist "an": die Marke ist Teil der Oberflaeche, wer sie nicht
-- moechte, blendet sie aus. Im kostenlosen Tarif bleibt sie unabhaengig von
-- diesem Wert eingeblendet - erzwungen in saas_update_tenant_settings() und
-- app/plan-watermark.php, damit auch ein manipulierter Formular-POST sie
-- dort nicht abschalten kann.
ALTER TABLE tenant_settings
ADD COLUMN watermark_enabled TINYINT(1) NOT NULL DEFAULT 1 AFTER brand_color;
+18 -7
View File
@@ -328,27 +328,38 @@ mandant-einstellungen.php (vollständige Tarif-Vergleichstabelle mit Bestell-But
gegen einen echten Test-Mandanten durchspielen (Upgrade, Downgrade
zwischen zwei bezahlten Stufen, Downgrade auf `free`).
## Wasserzeichen im kostenlosen Tarif (umgesetzt)
## Hinweis auf die Kaffeeliste (Wasserzeichen, umgesetzt)
Stand: 2026-08-21. Mandanten im Tarif `free` (bis 10 Teilnehmer) zeigen
unten rechts eine kleine Marke mit Logo und Adresse der Kaffeeliste.
Zahlende Mandanten sehen die Oberfläche ohne fremdes Logo.
Stand: 2026-08-21. Unten rechts steht eine kleine Marke mit Logo und
Adresse der Kaffeeliste. Im Tarif `free` (bis 10 Teilnehmer) ist sie fest
eingeschaltet; ab einem bezahlten Tarif entscheidet der Mandant selbst
(Mandant-Einstellungen, Standard: an).
Umgesetzte Dateien:
```text
app/plan-watermark.php (Sichtbarkeit + Markup)
app/bootstrap.php (app_marketing_url())
app/saas-auth.php (Speichern, Zwang im kostenlosen Tarif)
mandant-einstellungen.php (Schalter + Hinweis)
database/migrations/0027_watermark_setting.sql (Spalte watermark_enabled)
footer.php (Einbindung vor den Skripten)
assets/css/app.css (.plan-watermark)
scripts/http-smoke.php (Dashboard-Check)
scripts/http-smoke.php, scripts/check-m3-settings-flow.php (Regressionstests)
```
- **Nur der Tarif entscheidet.** `plan_watermark_visible()` blendet die
Marke ein, wenn `tenant_billing.plan_code` `free` ist fehlt der
- **Kostenlos heißt fest eingeschaltet.** `plan_watermark_visible()` zeigt
die Marke immer, wenn `tenant_billing.plan_code` `free` ist fehlt der
Datensatz, gilt wie beim Anlegen `free`. Lässt sich der Tarif nicht
lesen, bleibt die Oberfläche sauber: ein Wasserzeichen bei einem
zahlenden Kunden wäre der schlimmere Fehler.
- **Ab einem bezahlten Tarif entscheidet der Mandant.** Die Einstellung
`tenant_settings.watermark_enabled` (Standard 1) steuert die Anzeige;
der Schalter steht in den Mandant-Einstellungen. Im kostenlosen Tarif
wird das Kästchen angehakt und `disabled` dargestellt, mit einem Hinweis
daneben. Verlassen kann man sich darauf nicht `saas_update_tenant_settings()`
erzwingt den Wert im Tarif `free` noch einmal, damit auch ein
nachgebauter POST ihn nicht abschalten kann.
- **Keine Schreibzugriffe im Seitenaufbau.** Bewusst nicht
`billing_fetch_or_init()`, das eine fehlende Zeile anlegen würde; die
Prüfung ist eine reine Leseabfrage und wird pro Request zwischengespeichert.
+24
View File
@@ -90,6 +90,7 @@ if ($canManageSettings) {
'payment_reminder_interval_days' => $_POST['payment_reminder_interval_days'] ?? 7,
'brand_color' => $_POST['brand_color'] ?? '',
'brand_logo' => (string)(saas_fetch_tenant_settings($pdo, (int)$user['tenant_id'])['brand_logo'] ?? ''),
'watermark_enabled' => !empty($_POST['watermark_enabled']) ? 1 : 0,
];
}
} else {
@@ -256,6 +257,29 @@ include 'nav.php';
<?php endif; ?>
</div>
<?php endif; ?>
<?php
// Der Hinweis auf die Kaffeeliste (Logo unten rechts) ist im kostenlosen
// Tarif Teil des Angebots und deshalb dort festgestellt. Das Kaestchen bleibt
// sichtbar, damit erkennbar ist, was passiert - nur eben unveraenderlich.
$watermarkErzwungen = (string)($billing['plan_code'] ?? 'free') === 'free';
$watermarkAn = $watermarkErzwungen || (int)($settings['watermark_enabled'] ?? 1) === 1;
?>
<div class="col-12">
<h3>Hinweis auf die Kaffeeliste</h3>
<p>Unten rechts steht eine kleine Marke mit dem Logo der Kaffeeliste und
einem Link auf unsere Seite.</p>
</div>
<div class="col-12">
<input type="checkbox" id="watermark_enabled" name="watermark_enabled" value="1"
<?php echo $watermarkAn ? 'checked' : ''; ?>
<?php echo $watermarkErzwungen ? 'disabled' : ''; ?>>
<label for="watermark_enabled">Logo der Kaffeeliste unten rechts anzeigen</label>
<?php if ($watermarkErzwungen): ?>
<div class="hint-box"><p>Im kostenlosen Tarif bleibt dieser Hinweis eingeblendet
er ist der Gegenwert für die kostenlose Nutzung. Ab dem Tarif „Basic" könnt ihr
ihn hier ausschalten.</p></div>
<?php endif; ?>
</div>
<div class="col-6 col-12-small">
<input type="checkbox" id="payment_reminder_enabled" name="payment_reminder_enabled" value="1" <?php echo (int)$settings['payment_reminder_enabled'] === 1 ? 'checked' : ''; ?>>
<label for="payment_reminder_enabled">Automatische Zahlungserinnerung ab Warnschwelle</label>
+34
View File
@@ -106,7 +106,41 @@ if ($registration['ok']) {
settings_check_assert('self entry is enabled', $updated['self_entry_enabled'] === 1, $failures, $passes);
settings_check_assert('pdf row height is updated', (int)$updated['pdf_row_height_px'] === 18, $failures, $passes);
settings_check_assert('reminder interval is updated', (int)$updated['payment_reminder_interval_days'] === 14, $failures, $passes);
// Das Formular oben schickt kein watermark_enabled - im kostenlosen
// Tarif bleibt der Hinweis auf die Kaffeeliste trotzdem an.
settings_check_assert('watermark stays on in the free plan', (int)$updated['watermark_enabled'] === 1, $failures, $passes);
}
// Ab einem bezahlten Tarif entscheidet der Mandant selbst.
$paidInput = [
'tenant_name' => 'M3 Settings Flow Updated',
'timezone' => 'Europe/Berlin',
'locale' => 'de-DE',
'currency_code' => 'EUR',
'mark_price' => '0,35',
'sheet_window_days' => '90',
'negative_warning' => '12,50',
'pdf_split_mode' => 'alphabetical',
'pdf_row_height_px' => '18',
'payment_reminder_interval_days' => '14',
];
billing_update($pdo, $tenantId, ['plan_code' => 'basic']);
$paidOff = saas_update_tenant_settings($pdo, $tenantId, $paidInput);
settings_check_assert(
'watermark can be switched off on a paid plan',
$paidOff['ok'] === true && (int)$paidOff['settings']['watermark_enabled'] === 0,
$failures,
$passes
);
$paidOn = saas_update_tenant_settings($pdo, $tenantId, $paidInput + ['watermark_enabled' => '1']);
settings_check_assert(
'watermark can be switched back on',
$paidOn['ok'] === true && (int)$paidOn['settings']['watermark_enabled'] === 1,
$failures,
$passes
);
}
settings_check_cleanup($pdo, $slug, $emailNorm);
+18 -1
View File
@@ -5,7 +5,11 @@ declare(strict_types=1);
require __DIR__ . '/dev-db.php';
$baseUrl = rtrim((string)(getenv('SMOKE_BASE_URL') ?: 'http://127.0.0.1:8080'), '/');
$issuePattern = '~(?:Deprecated|Warning|Fatal error|Parse error|Notice|Uncaught (?:Error|Exception))~i';
// PHP-Meldungen enden immer auf einen Doppelpunkt ("Warning: ...", in HTML
// auch "<b>Warning</b>: ..."). Der verlangte Doppelpunkt haelt harmlose
// Treffer heraus, die dieselben Woerter enthalten - z. B. das Feld
// "negative_warning" in den Mandant-Einstellungen.
$issuePattern = '~(?:Deprecated|Warning|Fatal error|Parse error|Notice|Uncaught (?:Error|Exception))(?:</b>)?\s*:~i';
// Der Smoke-Test legt sich seinen eigenen Mandanten mit bekannten Zahlen an und
// meldet sich per HTTP an, statt sich auf vorhandene Daten oder eine
@@ -171,6 +175,19 @@ $checks = [
'expect_redirect' => 'login.php',
'contains' => [],
],
[
// Der Smoke-Mandant ist im kostenlosen Tarif: der Schalter fuer den
// Hinweis auf die Kaffeeliste muss erscheinen, aber festgestellt sein.
'label' => 'Mandant-Einstellungen',
'path' => 'mandant-einstellungen.php',
'auth' => true,
'contains' => [
'Hinweis auf die Kaffeeliste',
'Logo der Kaffeeliste unten rechts anzeigen',
'Im kostenlosen Tarif bleibt dieser Hinweis eingeblendet',
'disabled',
],
],
[
'label' => 'Namensanpassung',
'path' => 'namenanpassen.php',