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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81d911005203680cbf4a2c85a8da0c4bec60c04c9094e3e35af19c8e4fb595d7
4
- data.tar.gz: 67a34497cc8a10c5f2803745e28b45949eb0ef550aa951b57f08f588c1c809cb
3
+ metadata.gz: 921978d679f2c21c42fda41e4caa14e844e5fbfbb57a10c78738811441814547
4
+ data.tar.gz: 342464faa1422920f63ca881f7d64728d35e6b88c7e595d351a06c181a7692cd
5
5
  SHA512:
6
- metadata.gz: 6f78483f20c53fdb31f4c0537c96016d216efb6b362ea35bdb8090752c6585a4ad76f19885cd81f051f55955d355bb58510edb0031266eef37bc32e89c45ed84
7
- data.tar.gz: 6c64d27234a8d20e0bdece9b695ec67e3272da6df194abbe4f894490831f9659361e0891a3a70a38a2ed375e3caf773bacd13586fbc358381e53df39213ada35
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
- The newest inserted row for an agreement key is current immediately. There is
89
- no draft, publish, activate, or scheduling lifecycle. A correction or legal
90
- update is a new immutable row with a new version label.
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
- 4. Deploy. The new row becomes current and subjects missing it are prompted.
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
- def self.current_for(agreement_key)
21
- where(agreement_key: agreement_key).order(created_at: :desc, id: :desc).first
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agreements
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.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