130 lines
4.5 KiB
PHP
130 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Admin;
|
|
|
|
use App\Models\Customer;
|
|
use App\Models\DpaVersion;
|
|
use App\Services\Legal\ProcessingAgreement;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Component;
|
|
use Livewire\WithFileUploads;
|
|
|
|
/**
|
|
* The data-processing agreement: upload a version, publish it, see who has it.
|
|
*
|
|
* The TEXT is not this application's and never will be. Art. 28(3) GDPR wants a
|
|
* contract between the controller (the customer) and the processor (us); what
|
|
* belongs in it is a lawyer's answer, and inventing one here would be worse than
|
|
* having none. What this page does is the part software can do properly: keep
|
|
* the document, say which version is in force, hand it to the customer, and hold
|
|
* the evidence that they accepted it.
|
|
*
|
|
* ## Publishing is what asks every customer again
|
|
*
|
|
* Acceptance is per VERSION. Publishing a new one therefore leaves every
|
|
* customer outstanding until they accept it, which is the correct behaviour and
|
|
* an expensive one to trigger by accident — so a draft is uploaded first and
|
|
* published deliberately, in two steps.
|
|
*/
|
|
#[Layout('layouts.admin')]
|
|
class ProcessingAgreements extends Component
|
|
{
|
|
use WithFileUploads;
|
|
|
|
/** What the document itself calls this version — "1.0", "2026-08". */
|
|
public string $version = '';
|
|
|
|
public string $note = '';
|
|
|
|
/** The agreement, and the technical and organisational measures beside it. */
|
|
public $agreement = null;
|
|
|
|
public $measures = null;
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->authorize('dpa.manage');
|
|
}
|
|
|
|
public function upload(): void
|
|
{
|
|
$this->authorize('dpa.manage');
|
|
|
|
$data = $this->validate([
|
|
'version' => 'required|string|max:64|unique:dpa_versions,version',
|
|
'note' => 'nullable|string|max:2000',
|
|
// PDF only, and a real size limit: this is handed to customers as
|
|
// the contract between us, not as an editable file.
|
|
'agreement' => 'required|file|mimes:pdf|max:10240',
|
|
'measures' => 'nullable|file|mimes:pdf|max:10240',
|
|
]);
|
|
|
|
// The private disk. An agreement is not a public asset, and a guessable
|
|
// URL to one would be a list of who our customers are.
|
|
$version = DpaVersion::query()->create([
|
|
'version' => $data['version'],
|
|
'note' => $data['note'] ?: null,
|
|
'agreement_path' => $this->agreement->store('dpa', 'local'),
|
|
'measures_path' => $this->measures?->store('dpa', 'local'),
|
|
'created_by' => Auth::guard('operator')->id(),
|
|
]);
|
|
|
|
$this->reset('version', 'note', 'agreement', 'measures');
|
|
|
|
$this->dispatch('notify', message: __('dpa_admin.uploaded', ['version' => $version->version]));
|
|
}
|
|
|
|
/**
|
|
* Put a version in force.
|
|
*
|
|
* Deliberately separate from the upload: every customer who had accepted the
|
|
* previous one is outstanding again from this moment, and that is not
|
|
* something to do by dropping a file on a form.
|
|
*/
|
|
#[On('dpa-publish-confirmed')]
|
|
public function publish(string $uuid): void
|
|
{
|
|
$this->authorize('dpa.manage');
|
|
|
|
$version = DpaVersion::query()->where('uuid', $uuid)->firstOrFail();
|
|
|
|
if ($version->isPublished()) {
|
|
return;
|
|
}
|
|
|
|
$version->update(['published_at' => now()]);
|
|
|
|
$this->dispatch('notify', message: __('dpa_admin.published', ['version' => $version->version]));
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$this->authorize('dpa.manage');
|
|
|
|
$current = app(ProcessingAgreement::class)->current();
|
|
|
|
// How many customers are outstanding on the version in force. Counted
|
|
// against customers who are actually still customers — a closed record
|
|
// is not somebody we are waiting on.
|
|
$customers = Customer::query()->whereNull('closed_at')->count();
|
|
$accepted = $current === null ? 0 : $current->acceptances()->count();
|
|
|
|
return view('livewire.admin.processing-agreements', [
|
|
'versions' => DpaVersion::query()->withCount('acceptances')->latest('id')->get(),
|
|
'current' => $current,
|
|
'customers' => $customers,
|
|
'accepted' => $accepted,
|
|
'outstanding' => max(0, $customers - $accepted),
|
|
]);
|
|
}
|
|
|
|
/** Whether a stored file is actually there, for the list. */
|
|
public function exists(?string $path): bool
|
|
{
|
|
return $path !== null && Storage::disk('local')->exists($path);
|
|
}
|
|
}
|