Class: ActiveSanction::Subject
- Inherits:
-
Object
- Object
- ActiveSanction::Subject
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/subject.rb
Overview
One entry in a book of business: an application's own id, and everything it knows about the person or company behind it.
subject = ActiveSanction::Subject.new(
id: "cust_1",
name: "Bosco Ntaganda",
type: :individual,
date_of_birth: "1973",
country: "CD"
)
subject.id # => "cust_1"
subject.name # => "Bosco Ntaganda"
Why an id is the whole of what this adds
screen answers about a name. Rescreening answers about a customer, and
the two are not the same question: an alert has to name the row in the
host's database that a compliance team is going to open, hold and
eventually dispose of. A book screened as bare names comes back as an
array somebody has to re-join by position, which works exactly until a
book is filtered, streamed in batches, or contains the same name twice --
and two customers called Jane Miller is not a corner case, it is Tuesday.
So the id is required, it is the caller's own, and nothing here interprets it. It travels onto every alert the subject produces (Rescreen::Alert), so a run's output joins back to the host's records without the host having kept the order it sent them in.
It takes what screen takes
Every evidence field a screening call accepts is accepted here, in every
spelling Query accepts it in -- date_of_birth: and dates_of_birth:,
country:, countries: and nationalities:, identifier: and
identifiers:. A subject is a query about a customer, and a caller should
not have to learn a second vocabulary to write one.
The two things it refuses are the search options that a rescreen decides
for itself. sources: is settled by the diff being applied -- a rescreen
runs against one list version pair and nothing else -- and limit: would
cap the alerts a subject can raise, which is not a thing this library is
willing to do: an alert dropped for being eleventh is a sanctions hit
nobody sees. Both are refused rather than ignored, because a search option
that is silently dropped is a caller screening under a rule they think
they set.
threshold: is accepted, and is the one policy knob a subject carries.
Risk-based screening is ordinary -- a correspondent bank at 70, a retail
customer at 85 -- and a book that could not express it would force a host
into one call per tier. Left unset, the subject is screened at whatever
threshold the run names. See Rescreen.
Where the evidence lives
In a Scorer::Subject, built once here, which is the same object a Query holds and hands the scorer. The name is folded once, at construction, and a rescreening run compares that one folded form against every changed record rather than re-folding per comparison -- which is what makes streaming a large book past a small diff cheap.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
-
#evidence ⇒ Scorer::Subject
readonly
The evidence, folded once.
-
#id ⇒ String
readonly
The caller's own id for this subject, carried onto every alert.
-
#threshold ⇒ Float?
readonly
The lowest score worth an alert for this subject, or nil to take the run's.
Class Method Summary collapse
-
.build(value) ⇒ Subject
Whatever a caller had, as a Subject:.
-
.from_h(hash) ⇒ Subject
Rebuilds a subject from #to_h output, accepting string keys so a book read out of a database or a JSON payload needs no translation.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #dates_of_birth ⇒ Array<PartialDate>
-
#form ⇒ Normalizer::Form
The folded name every comparison runs against, folded once at construction.
- #hash ⇒ Integer
- #identifiers ⇒ Array<Identifier>
-
#initialize(id:, **fields) ⇒ void
constructor
idandnameare required; everything else is optional, for the reason Scorer::Subject gives -- most callers have a name and little else, and a field absent on either side is neutral rather than a conflict. - #inspect ⇒ String
-
#name ⇒ String
The name as the caller wrote it, which is what an alert quotes back.
- #nationalities ⇒ Array<String>
-
#query(threshold: nil, sources: nil) ⇒ Query
This subject as the screening call it is, at the threshold given or its own -- which is what a MatchResult records as the question that was asked.
- #to_h ⇒ Hash{Symbol => T.untyped}
- #type ⇒ Symbol?
Constructor Details
#initialize(id:, **fields) ⇒ void
id and name are required; everything else is optional, for the
reason Scorer::Subject gives -- most callers have a name and little
else, and a field absent on either side is neutral rather than a
conflict.
The evidence fields are taken in any spelling Query accepts them in, and
resolved here rather than in a separate builder: unlike a Query, which a
screening call constructs on a caller's behalf, this is the object a
host writes out by hand, so .new is the door everything comes through.
148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/active_sanction/subject.rb', line 148 def initialize(id:, **fields) attributes = normalize(fields) @id = T.let(id!(id), String) @evidence = T.let( Scorer::Subject.new(name: attributes[:name], type: attributes[:type], dates_of_birth: attributes[:dates_of_birth] || [], nationalities: attributes[:nationalities] || [], identifiers: attributes[:identifiers] || []), Scorer::Subject ) @threshold = T.let(threshold!(attributes[:threshold]), T.nilable(Float)) freeze end |
Instance Attribute Details
#evidence ⇒ Scorer::Subject (readonly)
The evidence, folded once. What the scorer compares against a record.
98 99 100 |
# File 'lib/active_sanction/subject.rb', line 98 def evidence @evidence end |
#id ⇒ String (readonly)
The caller's own id for this subject, carried onto every alert.
94 95 96 |
# File 'lib/active_sanction/subject.rb', line 94 def id @id end |
#threshold ⇒ Float? (readonly)
The lowest score worth an alert for this subject, or nil to take the run's. See the class comment.
103 104 105 |
# File 'lib/active_sanction/subject.rb', line 103 def threshold @threshold end |
Class Method Details
.build(value) ⇒ Subject
116 117 118 119 120 121 122 123 124 125 |
# File 'lib/active_sanction/subject.rb', line 116 def build(value) case value when Subject then value when Hash then from_h(value) else raise InvalidArgument, "a book holds ActiveSanction::Subject or Hash entries, got #{value.class}. A rescreen reports " \ "alerts against a caller's own id, so a bare name is not enough to raise one" end end |
.from_h(hash) ⇒ Subject
Rebuilds a subject from #to_h output, accepting string keys so a book read out of a database or a JSON payload needs no translation.
130 131 132 133 134 135 |
# File 'lib/active_sanction/subject.rb', line 130 def from_h(hash) # `new(**hash)` past a required keyword parameter is one of the few # things Sorbet cannot check statically. #initialize validates what # arrives, which is where a bad round-trip is caught. T.unsafe(self).new(**hash.to_h.transform_keys { |key| key.to_s.to_sym }) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
196 197 198 199 200 |
# File 'lib/active_sanction/subject.rb', line 196 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#dates_of_birth ⇒ Array<PartialDate>
170 |
# File 'lib/active_sanction/subject.rb', line 170 def dates_of_birth = evidence.dates_of_birth |
#form ⇒ Normalizer::Form
The folded name every comparison runs against, folded once at construction. What a rescreening run hands the index rather than the string it came from.
182 |
# File 'lib/active_sanction/subject.rb', line 182 def form = evidence.form |
#hash ⇒ Integer
204 |
# File 'lib/active_sanction/subject.rb', line 204 def hash = [self.class, to_h].hash |
#identifiers ⇒ Array<Identifier>
176 |
# File 'lib/active_sanction/subject.rb', line 176 def identifiers = evidence.identifiers |
#inspect ⇒ String
207 |
# File 'lib/active_sanction/subject.rb', line 207 def inspect = "#<#{self.class} #{id} #{name.inspect}>" |
#name ⇒ String
The name as the caller wrote it, which is what an alert quotes back.
164 |
# File 'lib/active_sanction/subject.rb', line 164 def name = evidence.name |
#nationalities ⇒ Array<String>
173 |
# File 'lib/active_sanction/subject.rb', line 173 def nationalities = evidence.nationalities |
#query(threshold: nil, sources: nil) ⇒ Query
This subject as the screening call it is, at the threshold given or its
own -- which is what a MatchResult records as the question that was
asked. sources: is the list the run covers.
188 189 190 |
# File 'lib/active_sanction/subject.rb', line 188 def query(threshold: nil, sources: nil) T.unsafe(Query).new(**evidence.to_h, sources: sources, threshold: threshold || self.threshold) end |
#to_h ⇒ Hash{Symbol => T.untyped}
193 |
# File 'lib/active_sanction/subject.rb', line 193 def to_h = { id: id }.merge(evidence.to_h).merge(threshold: threshold) |
#type ⇒ Symbol?
167 |
# File 'lib/active_sanction/subject.rb', line 167 def type = evidence.type |