Class: Clickwrap::DispositionPlan

Inherits:
ApplicationRecord show all
Defined in:
lib/clickwrap/models/disposition_plan.rb

Overview

A reviewable plan to delete something.

Deletion here is always two steps: plan, then apply. The plan is immutable, scoped, and expiring, and it is rechecked at apply time. A newly placed legal hold, a changed policy, a changed eligibility, or a stale plan stops the run rather than deleting a broader set than the person who reviewed it agreed to.

The plan itself never decides whether an erasure request overrides a retention duty, a legal claim, or a hold. It shows what would happen.

Constant Summary collapse

KINDS =
%w[retention actor_privacy].freeze
STATES =
%w[open applying applied applied_with_errors superseded].freeze
DEFAULT_LIFETIME =
24.hours

Instance Method Summary collapse

Instance Method Details

#applied?Boolean

Returns:

  • (Boolean)


37
# File 'lib/clickwrap/models/disposition_plan.rb', line 37

def applied? = state == "applied"

#claim_for_application!(by_reference:, recover_if_stale_after: nil, because_recovery_is_needed: nil) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/clickwrap/models/disposition_plan.rb', line 60

def claim_for_application!(by_reference:, recover_if_stale_after: nil,
                           because_recovery_is_needed: nil)
  if by_reference.to_s.strip.empty?
    raise DispositionPlanInvalid,
          "Applying a disposition plan needs the stable reference of the operator doing it."
  end

  with_lock do
    if state == "applying"
      reclaim_stale_application!(
        by_reference: by_reference,
        stale_after: recover_if_stale_after,
        because: because_recovery_is_needed
      )
    else
      ensure_usable!
      start_application_attempt!(by_reference)
    end
  end
  self
end

#digest_verified?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'lib/clickwrap/models/disposition_plan.rb', line 112

def digest_verified?
  Digest.secure_compare?(plan_digest.to_s, compute_plan_digest)
end

#ensure_usable!(at = Clickwrap.now) ⇒ Object

Raises with the specific reason this plan can no longer be applied, so an operator sees "the plan expired" or "this was already applied" rather than a generic refusal.



46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/clickwrap/models/disposition_plan.rb', line 46

def ensure_usable!(at = Clickwrap.now)
  raise DispositionPlanInvalid, "Disposition plan #{id} failed its immutable plan digest." unless digest_verified?
  return true if usable?(at)

  raise DispositionPlanInvalid,
        case state
        when "applied", "applied_with_errors" then "Disposition plan #{id} was already applied at #{applied_at}."
        when "applying" then "Disposition plan #{id} is already being applied."
        when "superseded" then "Disposition plan #{id} was superseded by a newer plan."
        else "Disposition plan #{id} expired at #{expires_at}. Run the plan again and " \
             "review the current set before applying it."
        end
end

#expired?(at = Clickwrap.now) ⇒ Boolean

Returns:

  • (Boolean)


36
# File 'lib/clickwrap/models/disposition_plan.rb', line 36

def expired?(at = Clickwrap.now) = expires_at <= at

#finish_application!(outcome_summary:, had_errors: false) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/clickwrap/models/disposition_plan.rb', line 82

def finish_application!(outcome_summary:, had_errors: false)
  with_lock do
    unless state == "applying"
      raise DispositionPlanInvalid, "Disposition plan #{id} is not currently being applied."
    end

    update_columns(
      state: had_errors ? "applied_with_errors" : "applied",
      applied_at: Clickwrap.now,
      application_outcome: outcome_summary
    )
  end
  self
end

#supersede!(because:, by:) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/clickwrap/models/disposition_plan.rb', line 97

def supersede!(because:, by:)
  raise DispositionPlanInvalid, "Superseding a plan needs a `because:`." if because.to_s.strip.empty?

  with_lock do
    ensure_usable!
    update_columns(
      state: "superseded",
      superseded_at: Clickwrap.now,
      superseded_by_reference: Reference.actor(by),
      superseded_reason: because
    )
  end
  self
end

#to_sObject



116
# File 'lib/clickwrap/models/disposition_plan.rb', line 116

def to_s = "#{kind} disposition plan #{id} (#{item_count} items)"

#usable?(at = Clickwrap.now) ⇒ Boolean

Returns:

  • (Boolean)


39
40
41
# File 'lib/clickwrap/models/disposition_plan.rb', line 39

def usable?(at = Clickwrap.now)
  state == "open" && !expired?(at)
end