Class: Clickwrap::Retention::Planner
- Inherits:
-
Object
- Object
- Clickwrap::Retention::Planner
- Defined in:
- lib/clickwrap/retention/planner.rb
Overview
Builds a reviewable disposition plan. Deletes nothing.
plan = Clickwrap::Retention::Planner.new(created_by: current_operator).call
plan.summary # => counts by policy, by part, plus held and unresolved
plan.id # => the id `clickwrap:retention:apply PLAN=...` needs
Disposition is always two steps here: plan, review, then apply. This is the first step, and it is deliberately read-only — an operator can run it on a Friday afternoon without consequence, and the numbers it reports are the numbers the applier will re-derive.
Three properties of the report matter more than the totals:
* Held records are counted SEPARATELY rather than silently dropped, so
an operator who expected 4,000 items and sees 12 can tell that a legal
hold is working rather than that the query is broken.
* A host-event rule that resolves to nil is UNRESOLVED, never due. "Five
years, or three years after this contract is liquidated, whichever is
later" has not started its clock until liquidation happens, and a job
that treats "we cannot say yet" as "delete it now" destroys regulated
evidence early. That failure mode is specific, foreseeable, and the
reason this class reports a third category instead of two.
* Nothing here decides a retention period. The host and its counsel
chose the rules; this reads them back and says what they currently
imply.
Defined Under Namespace
Classes: Eligibility, HoldIndex, Item
Constant Summary collapse
- KIND =
"retention"- PARTS =
Parts of the evidence a plan can cover. The first four are the parts of an event, named exactly as the retention DSL names them; the fifth is a persisted presentation, which is not evidence of an act and ages out on its own schedule.
(RetentionClass::PARTS + %i[presentation]).freeze
- ANNEX_PARTS =
%i[ip_address browser_user_agent ip_geolocation].freeze
- EXAMPLE_LIMIT =
How many held and unresolved examples travel in the summary. The counts are always complete; the examples exist so an operator can see what kind of thing is being held without opening a console, and are capped so a plan row stays a plan row rather than a data export.
25
Instance Attribute Summary collapse
-
#actor_reference ⇒ Object
readonly
Returns the value of attribute actor_reference.
-
#at ⇒ Object
readonly
Returns the value of attribute at.
-
#because ⇒ Object
readonly
Returns the value of attribute because.
-
#created_by ⇒ Object
readonly
Returns the value of attribute created_by.
-
#items ⇒ Object
readonly
Returns the value of attribute items.
-
#policy_key ⇒ Object
readonly
Returns the value of attribute policy_key.
Class Method Summary collapse
-
.annex_eligibility(annex, part) ⇒ Object
What one annex field's rule currently says.
-
.core_event_eligibility(event) ⇒ Object
What the core event's rule currently says.
-
.resolve_host_event(name, event) ⇒ Object
A host calculation may legitimately return nil ("the triggering event has not happened"), may be unregistered, and may raise — a retention calculation runs the host's own domain code.
Instance Method Summary collapse
-
#call ⇒ Object
Builds and persists the plan.
- #due_items ⇒ Object
- #held_items ⇒ Object
-
#initialize(at: Clickwrap.now, policy_key: nil, actor_reference: nil, created_by: nil, because: nil) ⇒ Planner
constructor
A new instance of Planner.
- #unresolved_items ⇒ Object
Constructor Details
#initialize(at: Clickwrap.now, policy_key: nil, actor_reference: nil, created_by: nil, because: nil) ⇒ Planner
Returns a new instance of Planner.
186 187 188 189 190 191 192 193 |
# File 'lib/clickwrap/retention/planner.rb', line 186 def initialize(at: Clickwrap.now, policy_key: nil, actor_reference: nil, created_by: nil, because: nil) @at = at @policy_key = policy_key&.to_s @actor_reference = reference_for(actor_reference) @created_by = created_by @because = because @items = [] end |
Instance Attribute Details
#actor_reference ⇒ Object (readonly)
Returns the value of attribute actor_reference.
195 196 197 |
# File 'lib/clickwrap/retention/planner.rb', line 195 def actor_reference @actor_reference end |
#at ⇒ Object (readonly)
Returns the value of attribute at.
195 196 197 |
# File 'lib/clickwrap/retention/planner.rb', line 195 def at @at end |
#because ⇒ Object (readonly)
Returns the value of attribute because.
195 196 197 |
# File 'lib/clickwrap/retention/planner.rb', line 195 def because @because end |
#created_by ⇒ Object (readonly)
Returns the value of attribute created_by.
195 196 197 |
# File 'lib/clickwrap/retention/planner.rb', line 195 def created_by @created_by end |
#items ⇒ Object (readonly)
Returns the value of attribute items.
195 196 197 |
# File 'lib/clickwrap/retention/planner.rb', line 195 def items @items end |
#policy_key ⇒ Object (readonly)
Returns the value of attribute policy_key.
195 196 197 |
# File 'lib/clickwrap/retention/planner.rb', line 195 def policy_key @policy_key end |
Class Method Details
.annex_eligibility(annex, part) ⇒ Object
What one annex field's rule currently says. The row carries its own schedule or its own named rule, recorded when the value was captured, so this reads the row rather than today's policy: the person whose IP address it is was told the period that applied then.
144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/clickwrap/retention/planner.rb', line 144 def annex_eligibility(annex, part) delete_after = annex.public_send(:"#{part}_delete_after") return Eligibility.new(eligible_at: delete_after, rule: "recorded_schedule") if delete_after.present? rule_name = annex.public_send(:"#{part}_retain_until_rule") return resolve_host_event(rule_name, annex.event) if rule_name.present? Eligibility.new( rule: nil, unresolved_reason: "No deletion rule is recorded for #{part} on this row." ) end |
.core_event_eligibility(event) ⇒ Object
What the core event's rule currently says. Shared with the applier so planning and applying can never disagree about what "due" means.
The order matters. A schedule computed at capture (retain_core_event_until)
wins, because that is what the receipt already told the world. Only
when there is none does this fall back to the rule the class names
today. That fallback exists for records written by an older Clickwrap
version; current writers freeze a schedule onto captures, imports,
exemptions, and lifecycle events when each event is written.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/clickwrap/retention/planner.rb', line 116 def core_event_eligibility(event) return Eligibility.new(eligible_at: event.retain_core_event_until, rule: "recorded_schedule") if event.retain_core_event_until.present? return resolve_host_event(event.retention_rule_name, event) if event.retention_rule_name.present? retention_class = Clickwrap.retention_classes[event.retention_class_key.to_s] if retention_class.nil? return Eligibility.new( rule: "retention_class:#{event.retention_class_key}", unresolved_reason: "Retention class #{event.retention_class_key.inspect} is no longer " \ "defined, so nothing can say when this event is due." ) end rule = retention_class.rule_for(:core_event) return Eligibility.new(rule: nil, unresolved_reason: "No core-event rule is defined.") if rule.nil? return resolve_host_event(rule.host_event_name, event) if rule.host_event? Eligibility.new(eligible_at: event.recorded_at_by_server + rule.duration, rule: "duration:#{rule.duration.to_i}s") end |
.resolve_host_event(name, event) ⇒ Object
A host calculation may legitimately return nil ("the triggering event has not happened"), may be unregistered, and may raise — a retention calculation runs the host's own domain code. All three are reported as unresolved with the reason attached, because a disposition run that crashes on one row, or that guesses a date to keep going, is worse than one that says which rows it could not evaluate.
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/clickwrap/retention/planner.rb', line 163 def resolve_host_event(name, event) return Eligibility.new(rule: nil, unresolved_reason: "No rule name recorded.") if name.blank? rule = "host_event:#{name}" resolved = Clickwrap.configuration.resolve_retention_time(name, event) if resolved.nil? return Eligibility.new( rule: rule, unresolved_reason: "The host calculation #{name} has not resolved yet, so the " \ "triggering event has not happened and the clock has not started." ) end Eligibility.new(eligible_at: resolved, rule: rule) rescue StandardError => error Eligibility.new( rule: "host_event:#{name}", unresolved_reason: "The host calculation #{name} could not be evaluated (#{error.class})." ) end |
Instance Method Details
#call ⇒ Object
Builds and persists the plan. Nothing is deleted, nothing is marked, and no state changes anywhere else.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/clickwrap/retention/planner.rb', line 199 def call SchemaRequirements.require!(:retention_ops) @items = [] plan_core_events plan_request_evidence plan_presentations DispositionPlan.create!( kind: KIND, disposition_scope: scope_document, summary: summary_document, item_count: due_items.length, created_by_reference: reference_for(created_by), reason: because ) end |
#due_items ⇒ Object
218 |
# File 'lib/clickwrap/retention/planner.rb', line 218 def due_items = items.select { |item| item.status == :due } |
#held_items ⇒ Object
219 |
# File 'lib/clickwrap/retention/planner.rb', line 219 def held_items = items.select { |item| item.status == :held } |
#unresolved_items ⇒ Object
220 |
# File 'lib/clickwrap/retention/planner.rb', line 220 def unresolved_items = items.select { |item| item.status == :unresolved } |