Stop asking for a signature the law does not want, and lay out the tab
**The owner is right about the agreement.** Art. 28 wants a CONTRACT, not a ceremony — and a contract is concluded by incorporating the agreement into the terms the customer accepts at checkout, which is exactly how every hoster they have bought from does it. Nothing in the regulation asks for a second, separate click. What it does ask is that the agreement is in writing (electronic form included, Art. 28(9)), that it is the version in force, and that the customer can obtain it. So: the terms now say the agreement is part of the contract and needs no separate signing, and name where it is. The card states that rather than flagging the customer as outstanding — the warning chip is gone. The button stays, reworded to "Zusätzlich bestätigen": a practice or a firm that gets audited often wants an explicit record, and it costs a click. The console's counter says how many confirmed rather than how many are "open", because none of them are. **The contract tab was a staircase** — a short card, a wide one spanning both columns, then a short one again. It is one column of full-width sections now, each with the same shape: a header row carrying the state and its action, then the body. Nothing tracks sideways any more. The package section states its own state in the header instead of a paragraph in the body, the withdrawal sits under it as a row rather than a second card, and the agreement's four files each have a button — reading and keeping, agreement and measures, which the single "Herunterladen" could not express. Also: the generated version no longer carries "Aus dem Repository erzeugt (clupilot:publish-dpa)" in the console's list. That is the command line, not information. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feature/betriebsmodus^2
parent
976637afb2
commit
18193a731d
|
|
@ -59,7 +59,6 @@ class PublishProcessingAgreement extends Command
|
||||||
'version' => $version,
|
'version' => $version,
|
||||||
'agreement_path' => $agreementPath,
|
'agreement_path' => $agreementPath,
|
||||||
'measures_path' => $measuresPath,
|
'measures_path' => $measuresPath,
|
||||||
'note' => 'Aus dem Repository erzeugt (clupilot:publish-dpa).',
|
|
||||||
// Publishing is the point of the command; --draft is for looking at
|
// Publishing is the point of the command; --draft is for looking at
|
||||||
// it first. Every customer who accepted an earlier version is
|
// it first. Every customer who accepted an earlier version is
|
||||||
// outstanding again from this moment.
|
// outstanding again from this moment.
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
return [
|
return [
|
||||||
'title' => 'AV-Vertrag & TOM',
|
'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.',
|
'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_agreement' => 'AV-Vertrag ansehen',
|
||||||
'read_measures' => 'TOM ansehen',
|
'read_measures' => 'TOM ansehen',
|
||||||
'version' => 'Fassung :version',
|
'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' => 'Zusätzlich bestätigen',
|
||||||
'accept_cta' => 'Zur Kenntnis genommen und abgeschlossen',
|
'accepted_on' => 'Fassung :version von Ihnen bestätigt am :when. Bei einer neuen Fassung informieren wir Sie.',
|
||||||
'accepted_on' => 'Fassung :version abgeschlossen am :when. Bei einer neuen Fassung melden wir uns und bitten Sie erneut um Ihre Zustimmung.',
|
'accepted_notice' => 'Bestätigung festgehalten. Den Vertrag finden Sie jederzeit hier.',
|
||||||
'accepted_notice' => 'AV-Vertrag abgeschlossen. Sie finden ihn jederzeit hier.',
|
|
||||||
'download' => 'Herunterladen',
|
'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',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ return [
|
||||||
'current_title' => 'In Kraft',
|
'current_title' => 'In Kraft',
|
||||||
'none_yet' => 'Noch keine Fassung veröffentlicht. Solange nichts in Kraft ist, sehen Ihre Kunden den Punkt nicht.',
|
'none_yet' => 'Noch keine Fassung veröffentlicht. Solange nichts in Kraft ist, sehen Ihre Kunden den Punkt nicht.',
|
||||||
'in_force_since' => 'seit :when',
|
'in_force_since' => 'seit :when',
|
||||||
'accepted_count' => ':accepted von :total Kunden haben abgeschlossen.',
|
'accepted_count' => ':accepted von :total Kunden haben zusätzlich bestätigt.',
|
||||||
'outstanding' => ':count offen',
|
'outstanding' => 'Eine Bestätigung ist nicht erforderlich — der Vertrag gilt mit den AGB.',
|
||||||
'versions_title' => 'Fassungen',
|
'versions_title' => 'Fassungen',
|
||||||
'no_versions' => 'Noch nichts hochgeladen.',
|
'no_versions' => 'Noch nichts hochgeladen.',
|
||||||
'col_version' => 'Fassung',
|
'col_version' => 'Fassung',
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
return [
|
return [
|
||||||
'title' => 'Processing agreement & TOMs',
|
'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.',
|
'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_agreement' => 'Read the agreement',
|
||||||
'read_measures' => 'Read the measures',
|
'read_measures' => 'Read the measures',
|
||||||
'version' => 'Version :version',
|
'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' => 'Confirm additionally',
|
||||||
'accept_cta' => 'Read and concluded',
|
'accepted_on' => 'Version :version confirmed by you on :when. We will tell you when a new version is issued.',
|
||||||
'accepted_on' => 'Version :version concluded on :when. When a new version is issued we will tell you and ask again.',
|
'accepted_notice' => 'Confirmation recorded. The agreement stays available here.',
|
||||||
'accepted_notice' => 'Agreement concluded. You can find it here at any time.',
|
|
||||||
'download' => 'Download',
|
'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',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ return [
|
||||||
'current_title' => 'In force',
|
'current_title' => 'In force',
|
||||||
'none_yet' => 'No version published yet. While nothing is in force, customers do not see the section.',
|
'none_yet' => 'No version published yet. While nothing is in force, customers do not see the section.',
|
||||||
'in_force_since' => 'since :when',
|
'in_force_since' => 'since :when',
|
||||||
'accepted_count' => ':accepted of :total customers have concluded it.',
|
'accepted_count' => ':accepted of :total customers confirmed it additionally.',
|
||||||
'outstanding' => ':count outstanding',
|
'outstanding' => 'A confirmation is not required — the agreement applies with the terms.',
|
||||||
'versions_title' => 'Versions',
|
'versions_title' => 'Versions',
|
||||||
'no_versions' => 'Nothing uploaded yet.',
|
'no_versions' => 'Nothing uploaded yet.',
|
||||||
'col_version' => 'Version',
|
'col_version' => 'Version',
|
||||||
|
|
|
||||||
|
|
@ -203,8 +203,12 @@
|
||||||
<p class="text-md leading-relaxed text-body">
|
<p class="text-md leading-relaxed text-body">
|
||||||
Welche Daten CluPilot zu welchem Zweck verarbeitet, steht in der
|
Welche Daten CluPilot zu welchem Zweck verarbeitet, steht in der
|
||||||
<a href="{{ route('legal.datenschutz') }}" class="font-medium text-accent-text underline-offset-4 hover:underline">Datenschutzerklärung</a>.
|
<a href="{{ route('legal.datenschutz') }}" class="font-medium text-accent-text underline-offset-4 hover:underline">Datenschutzerklärung</a>.
|
||||||
Soweit CluPilot Daten im Auftrag des Kunden verarbeitet, gilt dafür ein Vertrag über die
|
Soweit CluPilot Daten im Auftrag des Kunden verarbeitet, gilt der Vertrag über die
|
||||||
Auftragsverarbeitung nach Art. 28 DSGVO, den der Kunde anfordern kann.
|
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.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -284,30 +284,43 @@
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- ── Vertrag ──────────────────────────────────────────────────────── --}}
|
{{-- ── 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')
|
@if ($tab === 'contract')
|
||||||
<div class="grid gap-5 lg:grid-cols-2 lg:items-start animate-rise [animation-delay:60ms]">
|
<div class="space-y-5 animate-rise [animation-delay:60ms]">
|
||||||
|
|
||||||
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs">
|
{{-- Package, cancellation and — while it is open — the withdrawal. --}}
|
||||||
|
<section class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-6 py-4">
|
||||||
|
<div class="min-w-0">
|
||||||
<h2 class="text-sm font-bold text-ink">{{ __('settings.package_title') }}</h2>
|
<h2 class="text-sm font-bold text-ink">{{ __('settings.package_title') }}</h2>
|
||||||
|
<p class="mt-0.5 text-sm text-muted">
|
||||||
@if ($cancellationScheduled)
|
@if ($cancellationScheduled)
|
||||||
<div class="flex items-start gap-3 rounded border border-warning-border bg-warning-bg p-4">
|
{{ __('settings.cancel_scheduled_title') }}
|
||||||
<x-ui.icon name="alert-triangle" class="size-5 shrink-0 text-warning" />
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold text-ink">{{ __('settings.cancel_scheduled_title') }}</p>
|
|
||||||
<p class="mt-0.5 text-sm text-body">{{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@elseif ($hasActivePackage)
|
@elseif ($hasActivePackage)
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
{{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }}
|
||||||
<p class="text-sm text-muted">{{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }}</p>
|
@else
|
||||||
|
{{ __('settings.no_package') }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@if ($hasActivePackage && ! $cancellationScheduled)
|
||||||
<x-ui.button variant="secondary" size="sm"
|
<x-ui.button variant="secondary" size="sm"
|
||||||
x-on:click="$dispatch('openModal', { component: 'confirm-cancel-package' })">
|
x-on:click="$dispatch('openModal', { component: 'confirm-cancel-package' })">
|
||||||
{{ __('settings.cancel_cta') }}
|
{{ __('settings.cancel_cta') }}
|
||||||
</x-ui.button>
|
</x-ui.button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($cancellationScheduled)
|
||||||
|
<div class="flex items-start gap-3 border-b border-line bg-warning-bg px-6 py-4">
|
||||||
|
<x-ui.icon name="alert-triangle" class="mt-0.5 size-5 shrink-0 text-warning" />
|
||||||
|
<p class="text-sm text-body">{{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
@else
|
|
||||||
<p class="text-sm text-muted">{{ __('settings.no_package') }}</p>
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- The fourteen-day right of withdrawal. Shown only where it
|
{{-- The fourteen-day right of withdrawal. Shown only where it
|
||||||
|
|
@ -322,52 +335,47 @@
|
||||||
unwinds the contract itself, ends the service the same day
|
unwinds the contract itself, ends the service the same day
|
||||||
and sends the money back. --}}
|
and sends the money back. --}}
|
||||||
@if ($withdrawal->applies && $withdrawal->open)
|
@if ($withdrawal->applies && $withdrawal->open)
|
||||||
<div class="border-t border-line pt-4">
|
<div class="flex flex-wrap items-center justify-between gap-3 px-6 py-4">
|
||||||
|
<div class="min-w-0">
|
||||||
<p class="text-sm font-semibold text-ink">{{ __('withdrawal.card_title') }}</p>
|
<p class="text-sm font-semibold text-ink">{{ __('withdrawal.card_title') }}</p>
|
||||||
<p class="mt-0.5 text-sm text-muted">
|
<p class="mt-0.5 max-w-[70ch] text-sm text-muted">
|
||||||
{{ __('withdrawal.card_sub', [
|
{{ __('withdrawal.card_sub', [
|
||||||
'date' => $withdrawal->endsAt->local()->isoFormat('LL'),
|
'date' => $withdrawal->endsAt->local()->isoFormat('LL'),
|
||||||
'days' => $withdrawal->daysLeft(),
|
'days' => $withdrawal->daysLeft(),
|
||||||
]) }}
|
]) }}
|
||||||
</p>
|
</p>
|
||||||
<x-ui.button variant="secondary" size="sm" class="mt-3"
|
</div>
|
||||||
|
<x-ui.button variant="secondary" size="sm"
|
||||||
x-on:click="$dispatch('openModal', { component: 'confirm-withdraw' })">
|
x-on:click="$dispatch('openModal', { component: 'confirm-withdraw' })">
|
||||||
{{ __('withdrawal.cta') }}
|
{{ __('withdrawal.cta') }}
|
||||||
</x-ui.button>
|
</x-ui.button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{{-- ── AV-Vertrag & TOM ──────────────────────────────────────
|
{{-- ── AV-Vertrag & TOM ──────────────────────────────────────────
|
||||||
Art. 28(3) DSGVO wants a contract wherever personal data is
|
Art. 28 DSGVO wants a contract wherever personal data is
|
||||||
processed on somebody else's behalf, which is the whole of what
|
processed on somebody else's behalf. It is concluded WITH the
|
||||||
this product does. "In writing" there includes electronic form,
|
terms at checkout (see the AGB), so this is not a second signing
|
||||||
so the document plus a recorded acceptance is enough — and the
|
ceremony: the document has to be available, current, and
|
||||||
customer needs to be able to READ it without asking us for it,
|
retrievable. An extra confirmation is offered because some
|
||||||
because that is what the website promises.
|
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
|
Nothing renders until an operator has published a version. --}}
|
||||||
card offering an agreement that does not exist would be worse
|
|
||||||
than the silence. --}}
|
|
||||||
@if ($dpa !== null)
|
@if ($dpa !== null)
|
||||||
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs lg:col-span-2">
|
<section class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-6 py-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h2 class="text-sm font-bold text-ink">{{ __('dpa.title') }}</h2>
|
<h2 class="text-sm font-bold text-ink">{{ __('dpa.title') }}</h2>
|
||||||
<p class="mt-0.5 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('dpa.sub') }}</p>
|
<p class="mt-0.5 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('dpa.sub') }}</p>
|
||||||
</div>
|
</div>
|
||||||
@if ($dpaAcceptance)
|
<span class="inline-flex shrink-0 items-center gap-1.5 rounded-pill border border-line bg-surface-2 px-2.5 py-0.5 font-mono text-xs text-body">
|
||||||
<span class="inline-flex shrink-0 items-center gap-1.5 rounded-pill border border-success-border bg-success-bg px-2.5 py-0.5 text-xs font-medium text-success">
|
{{ __('dpa.version', ['version' => $dpa->version]) }}
|
||||||
<x-ui.icon name="check" class="size-3.5" />{{ __('dpa.state_accepted') }}
|
|
||||||
</span>
|
</span>
|
||||||
@else
|
|
||||||
<span class="inline-flex shrink-0 items-center gap-1.5 rounded-pill border border-warning-border bg-warning-bg px-2.5 py-0.5 text-xs font-medium text-warning">
|
|
||||||
<x-ui.icon name="alert-triangle" class="size-3.5" />{{ __('dpa.state_open') }}
|
|
||||||
</span>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2 px-6 py-4">
|
||||||
<x-ui.button :href="route('dpa.file', 'agreement')" variant="secondary" size="sm" target="_blank">
|
<x-ui.button :href="route('dpa.file', 'agreement')" variant="secondary" size="sm" target="_blank">
|
||||||
<x-ui.icon name="file-text" class="size-4" />{{ __('dpa.read_agreement') }}
|
<x-ui.icon name="file-text" class="size-4" />{{ __('dpa.read_agreement') }}
|
||||||
</x-ui.button>
|
</x-ui.button>
|
||||||
|
|
@ -377,38 +385,44 @@
|
||||||
</x-ui.button>
|
</x-ui.button>
|
||||||
@endif
|
@endif
|
||||||
{{-- Keep, not just read: the file carries the version in
|
{{-- Keep, not just read: the file carries the version in
|
||||||
its name, so "which fassung did I agree to" is
|
its name, so which fassung applied is answerable from
|
||||||
answerable from a downloads folder months later. --}}
|
a downloads folder months later. --}}
|
||||||
<x-ui.button :href="route('dpa.file', ['which' => 'agreement', 'download' => 1])" variant="ghost" size="sm" download>
|
<x-ui.button :href="route('dpa.file', ['which' => 'agreement', 'download' => 1])" variant="ghost" size="sm" download>
|
||||||
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download') }}
|
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download_agreement') }}
|
||||||
</x-ui.button>
|
</x-ui.button>
|
||||||
<span class="font-mono text-xs text-muted">{{ __('dpa.version', ['version' => $dpa->version]) }}</span>
|
@if ($dpa->measures_path)
|
||||||
|
<x-ui.button :href="route('dpa.file', ['which' => 'measures', 'download' => 1])" variant="ghost" size="sm" download>
|
||||||
|
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download_measures') }}
|
||||||
|
</x-ui.button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-line bg-surface-2 px-6 py-4">
|
||||||
@if ($dpaAcceptance)
|
@if ($dpaAcceptance)
|
||||||
{{-- What was agreed, when, and by whom — the same three
|
{{-- R19: stored in UTC, read on the wall clock. --}}
|
||||||
facts the register holds. R19: stored in UTC, read on
|
<p class="flex flex-wrap items-center gap-2 text-sm text-body">
|
||||||
the wall clock. --}}
|
<x-ui.icon name="check" class="size-4 text-success" />
|
||||||
<p class="rounded border border-line bg-surface-2 p-4 text-sm leading-relaxed text-body">
|
|
||||||
{{ __('dpa.accepted_on', [
|
{{ __('dpa.accepted_on', [
|
||||||
'version' => $dpa->version,
|
'version' => $dpa->version,
|
||||||
'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'),
|
'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'),
|
||||||
]) }}
|
]) }}
|
||||||
</p>
|
</p>
|
||||||
@else
|
@else
|
||||||
<div class="rounded border border-accent-border bg-accent-subtle p-4">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<p class="text-sm leading-relaxed text-body">{{ __('dpa.accept_hint') }}</p>
|
<p class="max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('dpa.in_force_note') }}</p>
|
||||||
<x-ui.button variant="primary" size="sm" class="mt-3"
|
<x-ui.button variant="secondary" size="sm"
|
||||||
wire:click="acceptProcessingAgreement" wire:loading.attr="disabled" wire:target="acceptProcessingAgreement">
|
wire:click="acceptProcessingAgreement" wire:loading.attr="disabled" wire:target="acceptProcessingAgreement">
|
||||||
{{ __('dpa.accept_cta') }}
|
{{ __('dpa.accept_cta') }}
|
||||||
</x-ui.button>
|
</x-ui.button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs">
|
{{-- Closing the account, and the deadlines that close one by itself. --}}
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<section class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-6 py-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h2 class="text-sm font-bold text-ink">{{ __('settings.close_account_title') }}</h2>
|
<h2 class="text-sm font-bold text-ink">{{ __('settings.close_account_title') }}</h2>
|
||||||
<p class="mt-0.5 text-sm text-muted">{{ __('settings.close_account_sub') }}</p>
|
<p class="mt-0.5 text-sm text-muted">{{ __('settings.close_account_sub') }}</p>
|
||||||
|
|
@ -425,7 +439,7 @@
|
||||||
nobody confirmed. The other rule — a year without a package —
|
nobody confirmed. The other rule — a year without a package —
|
||||||
was nowhere at all. Both come from the commands that enforce
|
was nowhere at all. Both come from the commands that enforce
|
||||||
them, so the page cannot drift from what actually happens. --}}
|
them, so the page cannot drift from what actually happens. --}}
|
||||||
<div class="rounded border border-line bg-surface-2 p-4">
|
<div class="px-6 py-4">
|
||||||
<p class="text-sm font-semibold text-ink">{{ __('settings.lifecycle_title') }}</p>
|
<p class="text-sm font-semibold text-ink">{{ __('settings.lifecycle_title') }}</p>
|
||||||
<ul class="mt-2 space-y-1.5 text-sm leading-relaxed text-muted">
|
<ul class="mt-2 space-y-1.5 text-sm leading-relaxed text-muted">
|
||||||
<li>{{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}</li>
|
<li>{{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}</li>
|
||||||
|
|
@ -437,7 +451,7 @@
|
||||||
{{ __('settings.lifecycle_terms') }}
|
{{ __('settings.lifecycle_terms') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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']);
|
$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'))
|
$page->assertSee(__('dpa.title'))
|
||||||
->assertSee(__('dpa.state_open'))
|
->assertSee(__('dpa.in_force_note'))
|
||||||
->assertSee(__('dpa.accept_cta'))
|
->assertSee(__('dpa.accept_cta'))
|
||||||
->call('acceptProcessingAgreement')
|
->call('acceptProcessingAgreement')
|
||||||
->assertDispatched('notify');
|
->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->accepted_at)->not->toBeNull()
|
||||||
->and($acceptance->ip)->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'])
|
Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract'])
|
||||||
->assertSee(__('dpa.state_accepted'))
|
->assertSee($acceptance->accepted_at->local()->isoFormat('LL, LT'))
|
||||||
->assertDontSee(__('dpa.accept_cta'));
|
->assertDontSee(__('dpa.accept_cta'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -316,3 +319,27 @@ it('serves the console download too, not only the portal one', function () {
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDownload('CluPilot-AV-Vertrag-3.3.pdf');
|
->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');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue