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)',
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.
| Type | Means | Adverse? |
|---|---|---|
added | something new — a clause, a recipient, a purpose | no |
removed | something gone | yes |
modified | different content, neither plainly wider nor narrower | no |
clarified | the same rule, said more clearly | no |
extended | broader reach — an optional processor becoming mandatory | yes |
restricted | a right limited, a liability excluded | yes |
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