Class: ActiveSanction::Identifier
- Inherits:
-
Object
- Object
- ActiveSanction::Identifier
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/identifier.rb
Overview
A document number a sanctions list published for an entity: passport, national ID, tax number, company registration. It is the highest-value signal the matcher has -- an exact passport match is near-decisive in the scorer (#32), where a name match never is.
ActiveSanction::Identifier.new(
kind: :passport,
value: "AB-123 456",
country: "Egypt",
issued_on: PartialDate.parse("2004-06-01"),
expires_on: PartialDate.parse("2009-05-31"),
note: "expired"
)
value is the one required field: an identifier with no number is not an
identifier. Everything else is optional, because OFAC's remarks often give
just Passport 123456 (Egypt) and nothing more.
The published string is kept verbatim -- it is what gets shown back to a user justifying a hit -- while comparison runs on #normalized_value, since two governments transcribing one passport rarely agree on its punctuation. Instances are frozen on construction and compare by value.
Constant Summary collapse
- KINDS =
The UN publishes TYPE_OF_DOCUMENT as free text ("Passport", "National Identification Number"), so adapters map onto these rather than passing a source's own vocabulary through. :other is a real answer, not a failure: a document we cannot classify still matches on its number.
T.let(%i[passport national_id tax_id registration_number other].freeze, T::Array[Symbol])
Instance Attribute Summary collapse
- #country ⇒ String? readonly
- #expires_on ⇒ PartialDate? readonly
- #issued_on ⇒ PartialDate? readonly
- #kind ⇒ Symbol readonly
-
#normalized_value ⇒ String
readonly
What comparison actually runs on -- see #== below.
- #note ⇒ String? readonly
-
#value ⇒ String
readonly
The published string, verbatim: it is what gets shown back to whoever has to justify a hit.
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds an identifier from #to_h output.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Equality is the comparison the acceptance criteria asks for:
AB-123 456andab123456are one passport written down twice, and de-duplicating the same document across OFAC and the UN depends on saying so. -
#comparison_key ⇒ Array<(Symbol, String, [String, nil])>
protected
Country is folded rather than dropped: two passports with the same number from different countries are different documents.
- #hash ⇒ Integer
-
#initialize(value:, kind: :other, country: nil, issued_on: nil, expires_on: nil, note: nil) ⇒ void
constructor
kinddefaults to :other because a number we cannot classify is still worth matching on -- OFAC remarks carry plenty of them. - #inspect ⇒ String
- #passport? ⇒ Boolean
- #to_h ⇒ Hash{Symbol => T.untyped}
- #to_s ⇒ String
Constructor Details
#initialize(value:, kind: :other, country: nil, issued_on: nil, expires_on: nil, note: nil) ⇒ void
kind defaults to :other because a number we cannot classify is still
worth matching on -- OFAC remarks carry plenty of them.
Untyped on purpose, and the same choice Entity makes: all six are the publisher's own text arriving as whatever the parser made of it. The two dates additionally accept a PartialDate, a #to_h hash or anything PartialDate.parse reads -- see #date_or_nil.
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/active_sanction/identifier.rb', line 105 def initialize(value:, kind: :other, country: nil, issued_on: nil, expires_on: nil, note: nil) @value = T.let(value!(value), String) @normalized_value = T.let(-@value.downcase.gsub(INSIGNIFICANT, ""), String) @kind = T.let(kind!(kind), Symbol) @country = T.let(string_or_nil(country), T.nilable(String)) @issued_on = T.let(date_or_nil(:issued_on, issued_on), T.nilable(PartialDate)) @expires_on = T.let(date_or_nil(:expires_on, expires_on), T.nilable(PartialDate)) @note = T.let(string_or_nil(note), T.nilable(String)) freeze end |
Instance Attribute Details
#country ⇒ String? (readonly)
64 65 66 |
# File 'lib/active_sanction/identifier.rb', line 64 def country @country end |
#expires_on ⇒ PartialDate? (readonly)
70 71 72 |
# File 'lib/active_sanction/identifier.rb', line 70 def expires_on @expires_on end |
#issued_on ⇒ PartialDate? (readonly)
67 68 69 |
# File 'lib/active_sanction/identifier.rb', line 67 def issued_on @issued_on end |
#kind ⇒ Symbol (readonly)
56 57 58 |
# File 'lib/active_sanction/identifier.rb', line 56 def kind @kind end |
#normalized_value ⇒ String (readonly)
What comparison actually runs on -- see #== below.
77 78 79 |
# File 'lib/active_sanction/identifier.rb', line 77 def normalized_value @normalized_value end |
#note ⇒ String? (readonly)
73 74 75 |
# File 'lib/active_sanction/identifier.rb', line 73 def note @note end |
#value ⇒ String (readonly)
The published string, verbatim: it is what gets shown back to whoever has to justify a hit.
61 62 63 |
# File 'lib/active_sanction/identifier.rb', line 61 def value @value end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds an identifier from #to_h output. Accepts string keys, and dates as the hashes JSON leaves behind, so a record survives a round-trip through storage (#24) without a separate coercion step.
83 84 85 86 87 88 89 90 91 92 |
# File 'lib/active_sanction/identifier.rb', line 83 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Identifier attribute(s): #{unknown.join(", ")}" if unknown.any? # `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(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Equality is the comparison the acceptance criteria asks for: AB-123 456
and ab123456 are one passport written down twice, and de-duplicating
the same document across OFAC and the UN depends on saying so. Dates and
notes stay out of the key -- publishers report them inconsistently, and
letting them split one document into two records would defeat the dedup
this exists for. Entity (#4) still compares its members through #to_h, so
nothing here hides a differing published string from a record diff.
142 143 144 145 146 |
# File 'lib/active_sanction/identifier.rb', line 142 def ==(other) return false unless other.instance_of?(self.class) comparison_key == other.comparison_key end |
#comparison_key ⇒ Array<(Symbol, String, [String, nil])> (protected)
Country is folded rather than dropped: two passports with the same number from different countries are different documents.
164 165 166 |
# File 'lib/active_sanction/identifier.rb', line 164 def comparison_key [kind, normalized_value, country&.downcase] end |
#hash ⇒ Integer
150 151 152 |
# File 'lib/active_sanction/identifier.rb', line 150 def hash [self.class, comparison_key].hash end |
#inspect ⇒ String
155 156 157 |
# File 'lib/active_sanction/identifier.rb', line 155 def inspect "#<#{self.class} #{kind.inspect} #{value.inspect}#{" country=#{country.inspect}" if country}>" end |
#passport? ⇒ Boolean
117 |
# File 'lib/active_sanction/identifier.rb', line 117 def passport? = kind == :passport |
#to_h ⇒ Hash{Symbol => T.untyped}
120 121 122 123 124 125 126 127 128 129 |
# File 'lib/active_sanction/identifier.rb', line 120 def to_h { kind: kind, value: value, country: country, issued_on: issued_on&.to_h, expires_on: expires_on&.to_h, note: note } end |
#to_s ⇒ String
132 |
# File 'lib/active_sanction/identifier.rb', line 132 def to_s = value |