Class: ActiveSanction::Rescreen::Alert
- Inherits:
-
Object
- Object
- ActiveSanction::Rescreen::Alert
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/rescreen/alert.rb
Overview
One subject, one record, and what moved between two list versions.
alert.subject_id # => "cust_1"
alert.change # => :newly_listed
alert.result # => MatchResult, scored against the record as it is now
alert.previous_result # => nil -- there was no such record before
alert.score # => 94.1
alert.previous_score # => nil
puts alert
# => cust_1 newly listed un_consolidated:6908021 BOSCO TAGANDA 94.1
The two sides, and why either may be missing
An alert is a change, so it is two screening results rather than one:
previous_result is what this subject scored against the record as the
old list had it, and result is what it scores against the record as
the new list has it. A newly listed record has no previous side. A
delisted one has no current side. Everything else has both, and the pair
is what lets an alert say a subject moved from 71 to 94 rather than
merely that it now matches -- which is the difference between an analyst
reading a record and an analyst reading a change to one.
Both are full MatchResults, each stamped with the checksum of the list
version it was scored against, so an alert is defensible the same way a
screening decision is: the explanation is on it, and it adds up.
#evidence is the side the alert was raised on, for a caller that wants
the record and does not care which list version described it.
The side that did not clear is scored again without a cutoff, so its result may sit below the threshold its own query names -- which is exactly what a subject moving into or out of range looks like, and is the whole reason the score is carried rather than only the fact of a match.
What change says, and what it does not
It is about this subject's match, not about the record's paperwork:
:newly_listed-- the subject did not reach the threshold against this record before and does now. Usually because the record is new; also because an existing record gained the alias, the identifier or the date of birth that brought the subject over the line, which is the same event for a compliance team and is why it is not filed separately.:delisted-- it did reach the threshold before and does not now. Usually because the record was withdrawn; also because an amendment moved it out of range. This is the half of a rescreen that a run against new records only would miss, and it is the half that lets a customer back through the door.:details_changed-- it matched before, it matches now, and the record moved underneath it. The score may be identical: a program added or an address corrected changes what a hit means without changing what it scores, and deciding that such a change is too small to report would be deciding which sanctions hits a host is willing to miss.
Which of those two routes into :newly_listed and :delisted a given
alert took is not guesswork -- #fields is empty when the record itself
arrived or left, and names the fields that moved when it was amended.
Both snapshot ids, on every alert
A MatchResult cites the one list version it was scored against, and an
alert is about two. So it carries both: previous_snapshot_id and
snapshot_id are the checksums the diff was computed over, which is
what makes an alert reproducible under audit -- keep the pair and the
whole run can be derived again, from lists that can be identified rather
than from a copy of an answer nobody can check.
Instances are frozen on construction and compare by value.
Constant Summary collapse
- CHANGES =
What happened to this subject's match. See the class comment.
T.let(%i[newly_listed delisted details_changed].freeze, T::Array[Symbol])
Instance Attribute Summary collapse
- #change ⇒ Symbol readonly
-
#fields ⇒ Array<Symbol>
readonly
The fields of the record that moved, in Entity's member order, or an empty array when the record was added or withdrawn whole.
-
#previous_result ⇒ MatchResult?
readonly
Scored against the record as the old list had it, or nil when the old list did not have it.
-
#previous_snapshot_id ⇒ String
readonly
The checksum of the list version it was compared with.
-
#result ⇒ MatchResult?
readonly
Scored against the record as the new list has it, or nil when the new list does not have it.
-
#snapshot_id ⇒ String
readonly
The checksum of the list version this run screened against.
-
#subject ⇒ Subject
readonly
The book entry this alert is about, as the caller supplied it.
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds an alert from #to_h output, accepting string keys so one survives the round-trip through JSON and back out of whatever a host stored it in.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #delisted? ⇒ Boolean
- #details_changed? ⇒ Boolean
-
#entity ⇒ Entity
The record this alert is about, as the surviving side has it.
- #entity_id ⇒ String
-
#evidence ⇒ MatchResult
The side the alert was raised on: the current one, or the previous one for a delisting, which is the version that actually matched.
- #hash ⇒ Integer
- #initialize(subject:, change:, snapshot_id:, previous_snapshot_id:, result: nil, previous_result: nil, fields: []) ⇒ void constructor
- #inspect ⇒ String
-
#matched_name ⇒ Name
The spelling that produced the score, as its publisher wrote it.
- #newly_listed? ⇒ Boolean
-
#previous_score ⇒ Float?
What it scored against the record before, or nil if the record was not on the previous list.
-
#score ⇒ Float?
What the subject scores against the record now, or nil if the record is no longer on the list.
-
#screened_at ⇒ Time
When the run that produced this alert happened.
- #source ⇒ Symbol
-
#subject_id ⇒ String
The caller's own id for the subject, which is what an alert is joined back to a book of business by.
-
#threshold ⇒ Float
The threshold this subject was screened at, which is half of what makes the change mean anything: the same pair of scores is a new listing at 75 and nothing at all at 95.
-
#to_h ⇒ Hash{Symbol => T.untyped}
The documented shape.
-
#to_s ⇒ String
One line, for the summary a human reads:.
Constructor Details
#initialize(subject:, change:, snapshot_id:, previous_snapshot_id:, result: nil, previous_result: nil, fields: []) ⇒ void
151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/active_sanction/rescreen/alert.rb', line 151 def initialize(subject:, change:, snapshot_id:, previous_snapshot_id:, result: nil, previous_result: nil, fields: []) @subject = T.let(instance!(:subject, Subject, subject), Subject) @change = T.let(change!(change), Symbol) @result = T.let(result!(:result, result), T.nilable(MatchResult)) @previous_result = T.let(result!(:previous_result, previous_result), T.nilable(MatchResult)) sides! @fields = T.let(Array(fields).map(&:to_sym).freeze, T::Array[Symbol]) @snapshot_id = T.let(string!(:snapshot_id, snapshot_id), String) @previous_snapshot_id = T.let(string!(:previous_snapshot_id, previous_snapshot_id), String) freeze end |
Instance Attribute Details
#change ⇒ Symbol (readonly)
100 101 102 |
# File 'lib/active_sanction/rescreen/alert.rb', line 100 def change @change end |
#fields ⇒ Array<Symbol> (readonly)
The fields of the record that moved, in Entity's member order, or an empty array when the record was added or withdrawn whole. See Diff::Change.
106 107 108 |
# File 'lib/active_sanction/rescreen/alert.rb', line 106 def fields @fields end |
#previous_result ⇒ MatchResult? (readonly)
Scored against the record as the old list had it, or nil when the old list did not have it.
116 117 118 |
# File 'lib/active_sanction/rescreen/alert.rb', line 116 def previous_result @previous_result end |
#previous_snapshot_id ⇒ String (readonly)
The checksum of the list version it was compared with.
124 125 126 |
# File 'lib/active_sanction/rescreen/alert.rb', line 124 def previous_snapshot_id @previous_snapshot_id end |
#result ⇒ MatchResult? (readonly)
Scored against the record as the new list has it, or nil when the new list does not have it.
111 112 113 |
# File 'lib/active_sanction/rescreen/alert.rb', line 111 def result @result end |
#snapshot_id ⇒ String (readonly)
The checksum of the list version this run screened against.
120 121 122 |
# File 'lib/active_sanction/rescreen/alert.rb', line 120 def snapshot_id @snapshot_id end |
#subject ⇒ Subject (readonly)
The book entry this alert is about, as the caller supplied it.
97 98 99 |
# File 'lib/active_sanction/rescreen/alert.rb', line 97 def subject @subject end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds an alert from #to_h output, accepting string keys so one survives the round-trip through JSON and back out of whatever a host stored it in.
130 131 132 133 134 135 136 137 138 139 |
# File 'lib/active_sanction/rescreen/alert.rb', line 130 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Alert attribute(s): #{unknown.join(", ")}" if unknown.any? T.unsafe(self).new(**attributes, subject: build(Subject, attributes[:subject]), result: build(MatchResult, attributes[:result]), previous_result: build(MatchResult, attributes[:previous_result])) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
250 251 252 253 254 |
# File 'lib/active_sanction/rescreen/alert.rb', line 250 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#delisted? ⇒ Boolean
219 |
# File 'lib/active_sanction/rescreen/alert.rb', line 219 def delisted? = change == :delisted |
#details_changed? ⇒ Boolean
222 |
# File 'lib/active_sanction/rescreen/alert.rb', line 222 def details_changed? = change == :details_changed |
#entity ⇒ Entity
The record this alert is about, as the surviving side has it.
181 |
# File 'lib/active_sanction/rescreen/alert.rb', line 181 def entity = evidence.entity |
#entity_id ⇒ String
184 |
# File 'lib/active_sanction/rescreen/alert.rb', line 184 def entity_id = entity.id |
#evidence ⇒ MatchResult
The side the alert was raised on: the current one, or the previous one
for a delisting, which is the version that actually matched. It is
what #entity and #matched_name read, so an alert describes the
record the way it looked when it crossed the threshold.
Never nil: an alert with neither side is not a change, and is refused at construction.
177 |
# File 'lib/active_sanction/rescreen/alert.rb', line 177 def evidence = T.must(delisted? ? previous_result || result : result || previous_result) |
#hash ⇒ Integer
258 |
# File 'lib/active_sanction/rescreen/alert.rb', line 258 def hash = [self.class, to_h].hash |
#inspect ⇒ String
261 |
# File 'lib/active_sanction/rescreen/alert.rb', line 261 def inspect = "#<#{self.class} #{self}>" |
#matched_name ⇒ Name
The spelling that produced the score, as its publisher wrote it. See MatchResult#matched_name.
189 |
# File 'lib/active_sanction/rescreen/alert.rb', line 189 def matched_name = evidence.matched_name |
#newly_listed? ⇒ Boolean
216 |
# File 'lib/active_sanction/rescreen/alert.rb', line 216 def newly_listed? = change == :newly_listed |
#previous_score ⇒ Float?
What it scored against the record before, or nil if the record was not on the previous list.
202 |
# File 'lib/active_sanction/rescreen/alert.rb', line 202 def previous_score = previous_result&.score |
#score ⇒ Float?
What the subject scores against the record now, or nil if the record is no longer on the list.
197 |
# File 'lib/active_sanction/rescreen/alert.rb', line 197 def score = result&.score |
#screened_at ⇒ Time
When the run that produced this alert happened. One instant for a whole run -- see Rescreen.
213 |
# File 'lib/active_sanction/rescreen/alert.rb', line 213 def screened_at = evidence.screened_at |
#source ⇒ Symbol
192 |
# File 'lib/active_sanction/rescreen/alert.rb', line 192 def source = entity.source |
#subject_id ⇒ String
The caller's own id for the subject, which is what an alert is joined back to a book of business by.
167 |
# File 'lib/active_sanction/rescreen/alert.rb', line 167 def subject_id = subject.id |
#threshold ⇒ Float
The threshold this subject was screened at, which is half of what makes the change mean anything: the same pair of scores is a new listing at 75 and nothing at all at 95.
208 |
# File 'lib/active_sanction/rescreen/alert.rb', line 208 def threshold = evidence.threshold |
#to_h ⇒ Hash{Symbol => T.untyped}
The documented shape. Every value is a String, a Float, an Integer, an
Array or a Hash of the same, so JSON.generate(alert.to_h) needs
nothing from this library and Alert.from_h(JSON.parse(json)) rebuilds
exactly this object.
229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/active_sanction/rescreen/alert.rb', line 229 def to_h { subject: subject.to_h, change: change, fields: fields, result: result&.to_h, previous_result: previous_result&.to_h, snapshot_id: snapshot_id, previous_snapshot_id: previous_snapshot_id } end |
#to_s ⇒ String
One line, for the summary a human reads:
cust_1 newly listed un_consolidated:6908021 BOSCO TAGANDA 94.1 (was 71.0)
245 246 247 |
# File 'lib/active_sanction/rescreen/alert.rb', line 245 def to_s "#{subject_id} #{change.to_s.tr("_", " ")} #{entity_id} #{evidence.matched_name.value} #{movement}" end |