Skip to main content

Saying what changed

A notice that says "we have updated our terms" tells a reader nothing they could act on. Every change notice worth the name leads with what changed, and often as a list rather than a sentence: processors added, one removed, an optional one becoming required.

That description is authored per document and per locale, frozen onto the version when you publish, and rendered into whichever notice the version's mode produces.

Describe the change

use Pushery\LegalConsent\Facades\ChangeItems;

ChangeItems::for('terms', 'de')
->headline('Wir haben zwei Auftragsverarbeiter aufgenommen und § 7 angepasst.')
->impact('Deine Kontodaten werden künftig auch in Irland verarbeitet.')
->added(
subject: 'Cloudflare, Inc.',
partyName: 'Cloudflare, Inc.',
partyLocation: 'Irland (EU)',
partyContact: '[email protected]',
purpose: 'Auslieferung statischer Inhalte und DDoS-Schutz',
)
->extended('Analytics', 'Bisher optional, künftig fester Bestandteil.')
->restricted('§ 7 Haftung', 'Haftung für einfache Fahrlässigkeit ausgeschlossen.')
->save();

Call it from a release script, a migration, or your own admin screen — it is a plain API, not a console wizard, because a change description is legal copy that should be reviewed before it exists.

Two fields, not one. § 327r Abs. 2 Satz 2 Nr. 1 BGB wants the characteristics of the change; WP260 rev.01 Rz. 31 separately wants its likely impact on the reader. A package that offered one field would quietly satisfy half of that.

The six entry types

They are not a taxonomy of edits. They are the distinctions that decide what else the notice owes.

TypeMeansAdverse?
addedsomething new — a clause, a recipient, a purposeno
removedsomething goneyes
modifieddifferent content, neither plainly wider nor narrowerno
clarifiedthe same rule, said more clearlyno
extendedbroader reach — an optional processor becoming mandatoryyes
restricteda right limited, a liability excludedyes

extended is the one a naive list misses. Something optional becoming mandatory reads as a bonus and is a narrowing of the reader's choice — the sub-processors that "will become required" in a typical vendor notice.

An adverse entry makes the free-termination line mandatory, whether or not you set offers_termination: § 675g Abs. 2 Satz 3 BGB ties that notice to the change being disadvantageous, not to a flag you remembered to set. A notice missing it is refused certification and its legal_notices.mandatory_content_ok is false.

Third parties

added, removed and extended accept the facets EDPB Opinion 22/2024 Rz. 22 expects when a new party starts handling personal data: who they are, where the data goes, whom to ask, and what they do with it. They are named by legal function rather than as "sub-processor", so the same four columns describe a new payment service provider or a joint controller.

They are reported when incomplete, never enforced. You may be describing a clause rather than a processor, and the package refusing your notice over that would be deciding what your change was about.

Publishing freezes it

legal-consent:publish and the atomic release both freeze the draft onto the version, inside the same transaction. It is a transition of the same row, not a copy — so the description on file and the description a subject read cannot drift apart. The frozen row also stores the version's content_hash, binding the description to exactly the text it describes.

Frozen means frozen. A database trigger on PostgreSQL and MySQL refuses UPDATE and DELETE on a published row and on every one of its items; a model hook covers SQLite. A correction is described in the next version, exactly like the legal text itself.

A publish rolls back cleanly: your draft is still there.

Where it lands in the notice

As lines on the mail, before the call to action — a reader decides whether to click on the strength of the delta, not after it. That placement is also what puts the delta into the durable-medium proof: legal_notices.notice_body is built from the mail's own lines and hashed, so a description rendered by swapping the mail view would reach the subject and be absent from the record of what they were shown.

Requiring it

Off by default. A version with no description renders exactly the notice it rendered before.

// config/legal-consent.php
'change_items' => [
'required' => true,
],

With it on, a change that owes a notice cannot be released until every locale has a description with both fields — § 327r Abs. 1 Nr. 3 BGB, Art. 12(1) GDPR and DSA Art. 14(1) all require the reader's own language, and a release is one change across every locale. The pre-flight also reports a description written against a legal text that has since moved.

What it cannot see is a German field containing English text. That gap is real and the honest answer is editorial process, not a predicate.

Inspecting

php artisan legal-consent:changes terms de # print the pending description
php artisan legal-consent:changes terms de --clear # discard it