diff --git a/app/Console/Commands/PublishProcessingAgreement.php b/app/Console/Commands/PublishProcessingAgreement.php index 1e69d9f..4a97309 100644 --- a/app/Console/Commands/PublishProcessingAgreement.php +++ b/app/Console/Commands/PublishProcessingAgreement.php @@ -59,7 +59,6 @@ class PublishProcessingAgreement extends Command 'version' => $version, 'agreement_path' => $agreementPath, 'measures_path' => $measuresPath, - 'note' => 'Aus dem Repository erzeugt (clupilot:publish-dpa).', // Publishing is the point of the command; --draft is for looking at // it first. Every customer who accepted an earlier version is // outstanding again from this moment. diff --git a/lang/de/dpa.php b/lang/de/dpa.php index 7cd9a97..6f79b85 100644 --- a/lang/de/dpa.php +++ b/lang/de/dpa.php @@ -5,14 +5,14 @@ return [ 'title' => 'AV-Vertrag & TOM', 'sub' => 'Der Vertrag zur Auftragsverarbeitung nach Art. 28 DSGVO samt Dokumentation der technischen und organisatorischen Maßnahmen — für Ihre eigene Dokumentation und Prüfung.', - 'state_accepted' => 'Abgeschlossen', - 'state_open' => 'Offen', 'read_agreement' => 'AV-Vertrag ansehen', 'read_measures' => 'TOM ansehen', 'version' => 'Fassung :version', - 'accept_hint' => 'Bitte lesen Sie den Vertrag und schließen Sie ihn ab. Wir halten dazu Zeitpunkt, Fassung und IP-Adresse fest; eine Unterschrift auf Papier ist nach Art. 28 Abs. 9 DSGVO nicht nötig.', - 'accept_cta' => 'Zur Kenntnis genommen und abgeschlossen', - 'accepted_on' => 'Fassung :version abgeschlossen am :when. Bei einer neuen Fassung melden wir uns und bitten Sie erneut um Ihre Zustimmung.', - 'accepted_notice' => 'AV-Vertrag abgeschlossen. Sie finden ihn jederzeit hier.', + 'accept_cta' => 'Zusätzlich bestätigen', + 'accepted_on' => 'Fassung :version von Ihnen bestätigt am :when. Bei einer neuen Fassung informieren wir Sie.', + 'accepted_notice' => 'Bestätigung festgehalten. Den Vertrag finden Sie jederzeit hier.', 'download' => 'Herunterladen', + 'in_force_note' => 'Dieser Vertrag ist mit Ihrer Bestellung Bestandteil unserer Vereinbarung; eine gesonderte Unterschrift ist nicht nötig. Wenn Sie für Ihre Unterlagen eine ausdrückliche Bestätigung möchten, halten wir Fassung, Zeitpunkt und IP-Adresse fest.', + 'download_agreement' => 'AV-Vertrag laden', + 'download_measures' => 'TOM laden', ]; diff --git a/lang/de/dpa_admin.php b/lang/de/dpa_admin.php index 1711fb0..20b1b2d 100644 --- a/lang/de/dpa_admin.php +++ b/lang/de/dpa_admin.php @@ -17,8 +17,8 @@ return [ 'current_title' => 'In Kraft', 'none_yet' => 'Noch keine Fassung veröffentlicht. Solange nichts in Kraft ist, sehen Ihre Kunden den Punkt nicht.', 'in_force_since' => 'seit :when', - 'accepted_count' => ':accepted von :total Kunden haben abgeschlossen.', - 'outstanding' => ':count offen', + 'accepted_count' => ':accepted von :total Kunden haben zusätzlich bestätigt.', + 'outstanding' => 'Eine Bestätigung ist nicht erforderlich — der Vertrag gilt mit den AGB.', 'versions_title' => 'Fassungen', 'no_versions' => 'Noch nichts hochgeladen.', 'col_version' => 'Fassung', diff --git a/lang/en/dpa.php b/lang/en/dpa.php index 598c85b..4c0b3a5 100644 --- a/lang/en/dpa.php +++ b/lang/en/dpa.php @@ -5,14 +5,14 @@ return [ 'title' => 'Processing agreement & TOMs', 'sub' => 'The data-processing agreement under Art. 28 GDPR together with the documentation of technical and organisational measures — for your own records and audits.', - 'state_accepted' => 'Concluded', - 'state_open' => 'Outstanding', 'read_agreement' => 'Read the agreement', 'read_measures' => 'Read the measures', 'version' => 'Version :version', - 'accept_hint' => 'Please read the agreement and conclude it. We record the moment, the version and the IP address; a signature on paper is not required (Art. 28(9) GDPR).', - 'accept_cta' => 'Read and concluded', - 'accepted_on' => 'Version :version concluded on :when. When a new version is issued we will tell you and ask again.', - 'accepted_notice' => 'Agreement concluded. You can find it here at any time.', + 'accept_cta' => 'Confirm additionally', + 'accepted_on' => 'Version :version confirmed by you on :when. We will tell you when a new version is issued.', + 'accepted_notice' => 'Confirmation recorded. The agreement stays available here.', 'download' => 'Download', + 'in_force_note' => 'This agreement became part of our contract with your order; a separate signature is not required. If you would like an explicit confirmation for your records, we note the version, the moment and the IP address.', + 'download_agreement' => 'Download agreement', + 'download_measures' => 'Download measures', ]; diff --git a/lang/en/dpa_admin.php b/lang/en/dpa_admin.php index 96c1802..e1ee1bb 100644 --- a/lang/en/dpa_admin.php +++ b/lang/en/dpa_admin.php @@ -17,8 +17,8 @@ return [ 'current_title' => 'In force', 'none_yet' => 'No version published yet. While nothing is in force, customers do not see the section.', 'in_force_since' => 'since :when', - 'accepted_count' => ':accepted of :total customers have concluded it.', - 'outstanding' => ':count outstanding', + 'accepted_count' => ':accepted of :total customers confirmed it additionally.', + 'outstanding' => 'A confirmation is not required — the agreement applies with the terms.', 'versions_title' => 'Versions', 'no_versions' => 'Nothing uploaded yet.', 'col_version' => 'Version', diff --git a/resources/views/legal/terms.blade.php b/resources/views/legal/terms.blade.php index 08759f0..0fa6a86 100644 --- a/resources/views/legal/terms.blade.php +++ b/resources/views/legal/terms.blade.php @@ -203,8 +203,12 @@

Welche Daten CluPilot zu welchem Zweck verarbeitet, steht in der Datenschutzerklärung. - Soweit CluPilot Daten im Auftrag des Kunden verarbeitet, gilt dafür ein Vertrag über die - Auftragsverarbeitung nach Art. 28 DSGVO, den der Kunde anfordern kann. + Soweit CluPilot Daten im Auftrag des Kunden verarbeitet, gilt der Vertrag über die + Auftragsverarbeitung nach Art. 28 DSGVO in seiner jeweils geltenden Fassung. Er wird mit + diesen AGB Bestandteil des Vertrags; einer gesonderten Unterzeichnung bedarf es nicht. + Der Vertrag und die Dokumentation der technischen und organisatorischen Maßnahmen stehen + im Kundenbereich jederzeit zum Ansehen und Herunterladen bereit. Eine neue Fassung wird + dem Kunden mitgeteilt.

diff --git a/resources/views/livewire/settings.blade.php b/resources/views/livewire/settings.blade.php index 48b924c..2663fd1 100644 --- a/resources/views/livewire/settings.blade.php +++ b/resources/views/livewire/settings.blade.php @@ -284,30 +284,43 @@ @endif {{-- ── Vertrag ──────────────────────────────────────────────────────── --}} + {{-- ── Vertrag ────────────────────────────────────────────────────── + One column of full-width sections, not a two-column grid with one card + spanning both: that produced a short box, a wide one, then a short one + again — a staircase with no rhythm to it. Each section here has the same + shape (a header row, then its body), so the eye can run down the page + instead of tracking sideways. --}} @if ($tab === 'contract') -
+
-
-

{{ __('settings.package_title') }}

- - @if ($cancellationScheduled) -
- -
-

{{ __('settings.cancel_scheduled_title') }}

-

{{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }}

-
+ {{-- Package, cancellation and — while it is open — the withdrawal. --}} +
+
+
+

{{ __('settings.package_title') }}

+

+ @if ($cancellationScheduled) + {{ __('settings.cancel_scheduled_title') }} + @elseif ($hasActivePackage) + {{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }} + @else + {{ __('settings.no_package') }} + @endif +

- @elseif ($hasActivePackage) -
-

{{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }}

+ @if ($hasActivePackage && ! $cancellationScheduled) {{ __('settings.cancel_cta') }} + @endif +
+ + @if ($cancellationScheduled) +
+ +

{{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }}

- @else -

{{ __('settings.no_package') }}

@endif {{-- The fourteen-day right of withdrawal. Shown only where it @@ -322,52 +335,47 @@ unwinds the contract itself, ends the service the same day and sends the money back. --}} @if ($withdrawal->applies && $withdrawal->open) -
-

{{ __('withdrawal.card_title') }}

-

- {{ __('withdrawal.card_sub', [ - 'date' => $withdrawal->endsAt->local()->isoFormat('LL'), - 'days' => $withdrawal->daysLeft(), - ]) }} -

- +
+

{{ __('withdrawal.card_title') }}

+

+ {{ __('withdrawal.card_sub', [ + 'date' => $withdrawal->endsAt->local()->isoFormat('LL'), + 'days' => $withdrawal->daysLeft(), + ]) }} +

+
+ {{ __('withdrawal.cta') }}
@endif -
+
- {{-- ── AV-Vertrag & TOM ────────────────────────────────────── - Art. 28(3) DSGVO wants a contract wherever personal data is - processed on somebody else's behalf, which is the whole of what - this product does. "In writing" there includes electronic form, - so the document plus a recorded acceptance is enough — and the - customer needs to be able to READ it without asking us for it, - because that is what the website promises. + {{-- ── AV-Vertrag & TOM ────────────────────────────────────────── + Art. 28 DSGVO wants a contract wherever personal data is + processed on somebody else's behalf. It is concluded WITH the + terms at checkout (see the AGB), so this is not a second signing + ceremony: the document has to be available, current, and + retrievable. An extra confirmation is offered because some + customers — a practice, a firm, anybody who gets audited — want + one on file, and it costs a click. - Nothing renders until an operator has published a version: a - card offering an agreement that does not exist would be worse - than the silence. --}} + Nothing renders until an operator has published a version. --}} @if ($dpa !== null) -
-
+
+

{{ __('dpa.title') }}

-

{{ __('dpa.sub') }}

+

{{ __('dpa.sub') }}

- @if ($dpaAcceptance) - - {{ __('dpa.state_accepted') }} - - @else - - {{ __('dpa.state_open') }} - - @endif + + {{ __('dpa.version', ['version' => $dpa->version]) }} +
-
+
{{ __('dpa.read_agreement') }} @@ -377,38 +385,44 @@ @endif {{-- Keep, not just read: the file carries the version in - its name, so "which fassung did I agree to" is - answerable from a downloads folder months later. --}} + its name, so which fassung applied is answerable from + a downloads folder months later. --}} - {{ __('dpa.download') }} + {{ __('dpa.download_agreement') }} - {{ __('dpa.version', ['version' => $dpa->version]) }} + @if ($dpa->measures_path) + + {{ __('dpa.download_measures') }} + + @endif
- @if ($dpaAcceptance) - {{-- What was agreed, when, and by whom — the same three - facts the register holds. R19: stored in UTC, read on - the wall clock. --}} -

- {{ __('dpa.accepted_on', [ - 'version' => $dpa->version, - 'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'), - ]) }} -

- @else -
-

{{ __('dpa.accept_hint') }}

- - {{ __('dpa.accept_cta') }} - -
- @endif -
+
+ @if ($dpaAcceptance) + {{-- R19: stored in UTC, read on the wall clock. --}} +

+ + {{ __('dpa.accepted_on', [ + 'version' => $dpa->version, + 'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'), + ]) }} +

+ @else +
+

{{ __('dpa.in_force_note') }}

+ + {{ __('dpa.accept_cta') }} + +
+ @endif +
+
@endif -
-
+ {{-- Closing the account, and the deadlines that close one by itself. --}} +
+

{{ __('settings.close_account_title') }}

{{ __('settings.close_account_sub') }}

@@ -425,7 +439,7 @@ nobody confirmed. The other rule — a year without a package — was nowhere at all. Both come from the commands that enforce them, so the page cannot drift from what actually happens. --}} -
+

{{ __('settings.lifecycle_title') }}

  • {{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}
  • @@ -437,7 +451,7 @@ {{ __('settings.lifecycle_terms') }}
-
+
@endif
diff --git a/tests/Feature/ProcessingAgreementTest.php b/tests/Feature/ProcessingAgreementTest.php index d01c630..f0ddc2e 100644 --- a/tests/Feature/ProcessingAgreementTest.php +++ b/tests/Feature/ProcessingAgreementTest.php @@ -61,8 +61,11 @@ it('offers the document and records the acceptance with its evidence', function $page = Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract']); + // The agreement is concluded WITH the terms at checkout, so the page states + // that rather than nagging — and offers the extra confirmation for whoever + // wants one on file. $page->assertSee(__('dpa.title')) - ->assertSee(__('dpa.state_open')) + ->assertSee(__('dpa.in_force_note')) ->assertSee(__('dpa.accept_cta')) ->call('acceptProcessingAgreement') ->assertDispatched('notify'); @@ -76,9 +79,9 @@ it('offers the document and records the acceptance with its evidence', function ->and($acceptance->accepted_at)->not->toBeNull() ->and($acceptance->ip)->not->toBeNull(); - // And the card now states it rather than asking again. + // And the card states the confirmation rather than asking again. Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract']) - ->assertSee(__('dpa.state_accepted')) + ->assertSee($acceptance->accepted_at->local()->isoFormat('LL, LT')) ->assertDontSee(__('dpa.accept_cta')); }); @@ -316,3 +319,27 @@ it('serves the console download too, not only the portal one', function () { ->assertOk() ->assertDownload('CluPilot-AV-Vertrag-3.3.pdf'); }); + +it('never treats the agreement as something outstanding', function () { + // Art. 28 wants a contract, not a ceremony: it is concluded with the terms + // at checkout (see the AGB), so the page must not imply that a customer who + // has not clicked is missing something. Hosters that ask for a separate + // signature are the exception, not the rule. + publishedDpa(); + [$user] = dpaCustomer(); + + $page = Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract'])->html(); + + expect($page)->toContain(__('dpa.in_force_note')) + // No warning colour, no "open" chip: the state is "available", and it is. + ->and($page)->not->toContain('text-warning">'.__('dpa.title')); +}); + +it('says in the terms that the agreement comes with them', function () { + // Which is what makes the click optional rather than a gap. + $terms = $this->get(route('legal.agb'))->assertOk()->getContent(); + + expect($terms)->toContain('Auftragsverarbeitung') + ->and($terms)->toContain('gesonderten Unterzeichnung') + ->and($terms)->toContain('Kundenbereich'); +});