agreements 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +109 -6
- data/app/models/agreements/version.rb +23 -2
- data/lib/agreements/test_helper.rb +20 -0
- data/lib/agreements/version.rb +1 -1
- data/lib/agreements.rb +4 -0
- data/lib/generators/agreements/install/templates/create_agreements_tables.rb.tt +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 921978d679f2c21c42fda41e4caa14e844e5fbfbb57a10c78738811441814547
|
|
4
|
+
data.tar.gz: 342464faa1422920f63ca881f7d64728d35e6b88c7e595d351a06c181a7692cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1cf67625ad815b0e3df2033a609cd510cf9c4ca97888b518c842546bb174b3c22abfd7bfc2670d0756337c3e24465b2e70b4a52887dbbf7e0a1a9c7c78a939b
|
|
7
|
+
data.tar.gz: 60cd61c042671c9bcb86f1e2f3c37ceeba9bfa2600aee3082766fe8b2124a38a7c81bc3b7ae5328f65f6265d6ac8ac6ef2f77416cd3d28547fd1c70bb47607a3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-11-12
|
|
4
|
+
|
|
5
|
+
- Add an optional `effective_at` to versions, so a version can be inserted when
|
|
6
|
+
notice is given and become current on the date it takes effect. Requires a
|
|
7
|
+
new nullable column; see "Upgrading from 0.3" in the README.
|
|
8
|
+
- Add `Agreements.upcoming_version` to show the next scheduled version.
|
|
9
|
+
- Add `Agreements::TestHelper#accept_agreement` for host test suites.
|
|
10
|
+
- Explain in the README what acceptance evidence does and does not prove.
|
|
11
|
+
|
|
3
12
|
## 0.3.0 - 2026-11-12
|
|
4
13
|
|
|
5
14
|
- Answer pending non-HTML requests with `403 Forbidden` instead of letting them
|
data/README.md
CHANGED
|
@@ -29,6 +29,17 @@ view, initializer, JavaScript, CSS, or admin dashboard to integrate.
|
|
|
29
29
|
|
|
30
30
|
Ruby >= 3.2 · Rails >= 7.1 and < 9 · PostgreSQL and SQLite
|
|
31
31
|
|
|
32
|
+
### Upgrading from 0.3
|
|
33
|
+
|
|
34
|
+
Version 0.4 adds a nullable `effective_at` column to `agreements_versions`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bin/rails generate migration AddEffectiveAtToAgreementsVersions effective_at:datetime
|
|
38
|
+
bin/rails db:migrate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Existing rows keep `effective_at` empty and stay in effect from their creation.
|
|
42
|
+
|
|
32
43
|
### Upgrading from 0.2
|
|
33
44
|
|
|
34
45
|
Version 0.3 has no schema change. `require_agreement` now answers a pending
|
|
@@ -85,9 +96,44 @@ changes later. The final check stops a deploy if a version label already exists
|
|
|
85
96
|
with different content: a deployed version must never change. Use the same
|
|
86
97
|
create-then-check loop in your seeds.
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
no draft, publish
|
|
90
|
-
|
|
99
|
+
A version without `effective_at` is current as soon as it is inserted. There is
|
|
100
|
+
no draft, publish or activate step. A correction or legal update is a new
|
|
101
|
+
immutable row with a new version label.
|
|
102
|
+
|
|
103
|
+
### Give notice before a version takes effect
|
|
104
|
+
|
|
105
|
+
Terms often promise notice before a change applies, for example "at least 14
|
|
106
|
+
days before the changes take effect". Insert the version when you send the
|
|
107
|
+
notice, with the date it takes effect:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
VERSION = {
|
|
111
|
+
agreement_key: "user_terms",
|
|
112
|
+
version: "2026-12-01",
|
|
113
|
+
effective_at: Time.utc(2026, 12, 1),
|
|
114
|
+
# acceptance_statement, documents ...
|
|
115
|
+
}.freeze
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Until that time the previous version stays current, nobody is asked to accept,
|
|
119
|
+
and `accept!` refuses the new version. From that time the new version is
|
|
120
|
+
current and every subject who has not accepted it is pending. Show the notice
|
|
121
|
+
in your app with:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
Agreements.upcoming_version("user_terms") # => the next scheduled version, or nil
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The current version is the one that took effect most recently: its
|
|
128
|
+
`effective_at`, or its creation time when `effective_at` is empty. When two
|
|
129
|
+
versions take effect at the same time, the one inserted later wins.
|
|
130
|
+
|
|
131
|
+
Subjects cannot accept a scheduled version early. Someone who accepts the
|
|
132
|
+
current version during the notice period is asked again on the effective date.
|
|
133
|
+
|
|
134
|
+
A scheduled version cannot have acceptances before it takes effect. To cancel
|
|
135
|
+
it, delete it in a data migration with a migration-local model before its
|
|
136
|
+
effective date. After that date it is evidence like any other version.
|
|
91
137
|
|
|
92
138
|
Document references accept `title`, an HTTPS `url`, an optional 64-character
|
|
93
139
|
`sha256`, and an optional `key`. Use `key` (lowercase letters, digits and
|
|
@@ -246,6 +292,29 @@ end
|
|
|
246
292
|
When no version exists for a key, nothing is pending and nothing is blocked.
|
|
247
293
|
Insert the first version before you rely on enforcement.
|
|
248
294
|
|
|
295
|
+
## Test your application
|
|
296
|
+
|
|
297
|
+
Once enforcement is on, most of your tests need subjects that have already
|
|
298
|
+
accepted. Include the test helper:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# test/test_helper.rb
|
|
302
|
+
require "agreements/test_helper"
|
|
303
|
+
|
|
304
|
+
class ActiveSupport::TestCase
|
|
305
|
+
include Agreements::TestHelper
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
accept_agreement("user_terms", subject: user)
|
|
311
|
+
accept_agreement("organization_dpa", subject: organization, actor: owner, authority: "organization_owner")
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`accept_agreement` accepts the current version with its canonical statement and
|
|
315
|
+
the `en` locale. It raises when no current version exists, so insert versions
|
|
316
|
+
in your fixtures or setup first.
|
|
317
|
+
|
|
249
318
|
## Evidence model
|
|
250
319
|
|
|
251
320
|
`agreements_versions` contains:
|
|
@@ -254,6 +323,7 @@ Insert the first version before you rely on enforcement.
|
|
|
254
323
|
- human version label;
|
|
255
324
|
- exact acceptance statement;
|
|
256
325
|
- external document references and optional SHA-256 digests;
|
|
326
|
+
- optional effective date;
|
|
257
327
|
- timestamps.
|
|
258
328
|
|
|
259
329
|
`agreements_acceptances` contains:
|
|
@@ -283,12 +353,43 @@ controls.
|
|
|
283
353
|
|
|
284
354
|
1. Publish immutable/versioned legal-document URLs and any verified digests.
|
|
285
355
|
2. Add the finished bundle to the host's seed registry.
|
|
286
|
-
3. Add a host data migration that inserts the same immutable version
|
|
287
|
-
|
|
356
|
+
3. Add a host data migration that inserts the same immutable version, with
|
|
357
|
+
`effective_at` if your terms promise notice.
|
|
358
|
+
4. Deploy. The new row becomes current at once, or on its effective date, and
|
|
359
|
+
subjects missing it are prompted.
|
|
288
360
|
|
|
289
361
|
Production deployments normally run migrations, not seeds. Changing a seed
|
|
290
362
|
registry alone does not deploy a new version to an existing installation.
|
|
291
363
|
|
|
364
|
+
## What the evidence proves
|
|
365
|
+
|
|
366
|
+
The gem records which version and which sentence a subject accepted, who did
|
|
367
|
+
it, under what authority, and when. That is the part your application can
|
|
368
|
+
prove later. Some questions it cannot answer for you:
|
|
369
|
+
|
|
370
|
+
- **The whole page counts.** Courts look at the entire screen, not only the
|
|
371
|
+
checkbox. In *Berman v. Freedom Financial Network* (9th Cir. 2022) the terms
|
|
372
|
+
did not bind the user: the notice was small grey text, the links did not
|
|
373
|
+
look like links, and the button said "Continue" without saying that it meant
|
|
374
|
+
acceptance. Your layout, contrast and button text decide this. The gem does
|
|
375
|
+
not see them.
|
|
376
|
+
- **It is not a qualified electronic signature.** Under eIDAS (Regulation (EU)
|
|
377
|
+
No 910/2014, Article 25), an electronic signature cannot be denied legal
|
|
378
|
+
effect only because it is electronic or not qualified. A checkbox is still
|
|
379
|
+
not a qualified electronic signature, and this gem does not create one.
|
|
380
|
+
- **Acknowledging a privacy notice is not consent.** Under the GDPR you inform
|
|
381
|
+
people (Article 13). Consent (Articles 6 and 7) is a separate, specific and
|
|
382
|
+
withdrawable choice. Keep the two apart in your copy and your records.
|
|
383
|
+
- **The text of your terms still matters.** In the EU, an unfair term in a
|
|
384
|
+
consumer contract does not bind the consumer (Directive 93/13/EEC), however
|
|
385
|
+
clearly it was accepted.
|
|
386
|
+
- **Identity and authority are yours to check.** The gem stores the actor and
|
|
387
|
+
the authority you pass. It does not verify who the person is or whether they
|
|
388
|
+
may bind an organization.
|
|
389
|
+
|
|
390
|
+
Your counsel decides what your agreements need. The gem keeps the record of
|
|
391
|
+
what was accepted.
|
|
392
|
+
|
|
292
393
|
## Not this gem
|
|
293
394
|
|
|
294
395
|
- Legal-document hosting, editing, rendering, uploads, or a CMS.
|
|
@@ -302,7 +403,9 @@ registry alone does not deploy a new version to an existing installation.
|
|
|
302
403
|
layouts, routes, or copy.
|
|
303
404
|
|
|
304
405
|
Those are real product categories. They are not prerequisites for proving an
|
|
305
|
-
ordinary Terms or organization-DPA acceptance.
|
|
406
|
+
ordinary Terms or organization-DPA acceptance. If you need consent withdrawal,
|
|
407
|
+
signed receipts, request evidence or a form builder, look at
|
|
408
|
+
[clickwrap](https://github.com/rameerez/clickwrap).
|
|
306
409
|
|
|
307
410
|
## Later, only after a concrete trigger
|
|
308
411
|
|
|
@@ -17,8 +17,29 @@ module Agreements
|
|
|
17
17
|
validates :version, :acceptance_statement, presence: true
|
|
18
18
|
validate :documents_are_external_references
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
# Filtering by time in Ruby keeps the SQL identical, so the query cache
|
|
21
|
+
# serves repeated checks within a request. Each key has only a few versions.
|
|
22
|
+
def self.current_for(agreement_key, now: Time.current)
|
|
23
|
+
versions = where(agreement_key: agreement_key).to_a
|
|
24
|
+
versions.select { |version| version.in_effect?(now) }.max_by(&:precedence)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.upcoming_for(agreement_key, now: Time.current)
|
|
28
|
+
scheduled = where(agreement_key: agreement_key).to_a.reject { |version| version.in_effect?(now) }
|
|
29
|
+
next_from = scheduled.map(&:effective_from).min
|
|
30
|
+
scheduled.select { |version| version.effective_from == next_from }.max_by(&:precedence)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def precedence
|
|
34
|
+
[effective_from, created_at, id]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def effective_from
|
|
38
|
+
effective_at || created_at
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def in_effect?(now = Time.current)
|
|
42
|
+
effective_from <= now
|
|
22
43
|
end
|
|
23
44
|
|
|
24
45
|
def accepted_by?(subject)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agreements
|
|
4
|
+
module TestHelper
|
|
5
|
+
def accept_agreement(agreement_key, subject:, actor: subject, authority: "self", locale: "en")
|
|
6
|
+
version = Agreements.current_version(agreement_key)
|
|
7
|
+
raise ArgumentError, "no current #{agreement_key} version; insert one in your test setup" unless version
|
|
8
|
+
|
|
9
|
+
Agreements.accept!(
|
|
10
|
+
agreement_key,
|
|
11
|
+
version_id: version.id,
|
|
12
|
+
subject: subject,
|
|
13
|
+
actor: actor,
|
|
14
|
+
authority: authority,
|
|
15
|
+
acceptance_statement: version.acceptance_statement,
|
|
16
|
+
locale: locale
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/agreements/version.rb
CHANGED
data/lib/agreements.rb
CHANGED
|
@@ -17,6 +17,10 @@ module Agreements
|
|
|
17
17
|
Version.current_for(agreement_key)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def upcoming_version(agreement_key)
|
|
21
|
+
Version.upcoming_for(agreement_key)
|
|
22
|
+
end
|
|
23
|
+
|
|
20
24
|
def pending_version(agreement_key, subject:)
|
|
21
25
|
current_version(agreement_key).then do |version|
|
|
22
26
|
version unless version.nil? || version.accepted_by?(subject)
|
|
@@ -7,6 +7,7 @@ class CreateAgreementsTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
7
7
|
t.string :version, null: false
|
|
8
8
|
t.text :acceptance_statement, null: false
|
|
9
9
|
t.json :documents, null: false, default: []
|
|
10
|
+
t.datetime :effective_at
|
|
10
11
|
t.timestamps
|
|
11
12
|
|
|
12
13
|
t.index %i[agreement_key version], unique: true
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agreements
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- lib/agreements/errors.rb
|
|
109
109
|
- lib/agreements/identity.rb
|
|
110
110
|
- lib/agreements/recorder.rb
|
|
111
|
+
- lib/agreements/test_helper.rb
|
|
111
112
|
- lib/agreements/version.rb
|
|
112
113
|
- lib/generators/agreements/install/install_generator.rb
|
|
113
114
|
- lib/generators/agreements/install/templates/create_agreements_tables.rb.tt
|