Skip to content

Commit

Permalink
ofac: setup mapper for Aircraft and Vessels
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdecaf committed Jan 12, 2024
1 parent 31205d5 commit ce83beb
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 41 deletions.
105 changes: 65 additions & 40 deletions pkg/ofac/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,10 @@ func ToEntity(sdn SDN) search.Entity[SDN] {
Gender: search.Gender(strings.ToLower(firstValue(findMatchingRemarks(remarks, "Gender")))),
}
out.Person.BirthDate = withFirstP(findMatchingRemarks(remarks, "DOB"), func(in remark) *time.Time {
// TODO(adam): handle
// DOB 01 Apr 1950
// DOB 01 Feb 1958 to 28 Feb 1958
// DOB 1928
// DOB 1929 to 1930
// DOB Sep 1958
// DOB circa 01 Jan 1961
// DOB circa 1934
// DOB circa 1979-1982

t, _ := time.Parse("02 Jan 2006", in.value)
t, err := parseTime(dobPatterns, in.value)
if t.IsZero() || err != nil {
return nil
}
return &t
})

Expand All @@ -78,50 +71,46 @@ func ToEntity(sdn SDN) search.Entity[SDN] {
case "vessel":
out.Type = search.EntityVessel
out.Vessel = &search.Vessel{
Name: sdn.SDNName,

// IMONumber string `json:"imoNumber"`
// Type VesselType `json:"type"`
// Flag string `json:"flag"` // ISO-3166
Name: sdn.SDNName,
IMONumber: firstValue(findMatchingRemarks(remarks, "IMO")),
Type: withFirstF(findMatchingRemarks(remarks, "Vessel Type"), func(r remark) search.VesselType {
return search.VesselType(r.value) // TODO(adam): OFAC values are not an enum
}),
Flag: firstValue(findMatchingRemarks(remarks, "Flag")), // TODO(adam): ISO-3166
// Built *time.Time `json:"built"`
// Model string `json:"model"`
// Tonnage int `json:"tonnage"` // TODO(adam): remove , and ParseInt
// MMSI string `json:"mmsi"` // Maritime Mobile Service Identity
MMSI: firstValue(findMatchingRemarks(remarks, "MMSI")),
}

// TODO(adam):
// Vessel Registration Identification IMO 9569712;
// MMSI 572469210;
//
// Former Vessel Flag None Identified; alt. Former Vessel Flag Tanzania;

case "aircraft":
out.Type = search.EntityAircraft
out.Aircraft = &search.Aircraft{
Name: sdn.SDNName,

// Type AircraftType `json:"type"`
// Flag string `json:"flag"` // ISO-3166
// Built *time.Time `json:"built"`
Flag: firstValue(findMatchingRemarks(remarks, "Flag")), // TODO(adam): ISO-3166
Built: withFirstP(findMatchingRemarks(remarks, "Manufacture Date"), func(in remark) *time.Time {
t, err := parseTime(dobPatterns, in.value)
if t.IsZero() || err != nil {
return nil
}
return &t
}),
// ICAOCode string `json:"icaoCode"` // ICAO aircraft type designator
// Model string `json:"model"`
// SerialNumber string `json:"serialNumber"`
Model: firstValue(findMatchingRemarks(remarks, "Aircraft Model")),
SerialNumber: withFirstF(findMatchingRemarks(remarks, "Serial Number"), func(r remark) string {
// Trim parens from these remarks
// e.g. "Aircraft Manufacturer's Serial Number (MSN) 1023409321;"
idx := strings.Index(r.value, ")")
if idx > -1 && len(r.value) > idx+1 {
r.value = strings.TrimSpace(r.value[idx+1:])
}
return r.value
}),
}

// TODO(adam):
// Aircraft Construction Number (also called L/N or S/N or F/N) 10907;
//
// Aircraft Manufacture Date 1992;
// Aircraft Manufacture Date 01 Dec 1981;
// Aircraft Manufacture Date Apr 1993;
//
// Aircraft Model IL76-TD;
// Aircraft Model B.747-422
// Aircraft Model Gulfstream 200
//
// Aircraft Operator YAS AIR;
// Aircraft Manufacturer's Serial Number (MSN) 1023409321;
//
// Previous Aircraft Tail Number 2-WGLP
}

Expand Down Expand Up @@ -159,6 +148,42 @@ func makeIdentifiers(remarks []string, needles []string) []search.Identifier {
return out
}

var (
dobPatterns = []string{
"02 Jan 2006", // 01 Apr 1950
"Jan 2006", // Sep 1958
"2006", // 1928
}
)

func parseTime(acceptedLayouts []string, value string) (time.Time, error) {
// We don't currently support ranges for birth dates, so take the first date provided
// Examples include:
// 01 Feb 1958 to 28 Feb 1958
// circa 1934
// circa 1979-1982
value = strings.TrimSpace(strings.ReplaceAll(value, "circa", ""))

parts := strings.Split(value, "to")
if len(parts) > 1 {
value = parts[0]
} else {
parts = strings.Split(value, "-")
if len(parts) > 1 {
value = parts[0]
}
}
value = strings.TrimSpace(value)

for i := range acceptedLayouts {
tt, err := time.Parse(acceptedLayouts[i], value)
if !tt.IsZero() && err == nil {
return tt, nil
}
}
return time.Time{}, nil
}

// TODO(adam):
// Drop "alt. "

Expand Down
92 changes: 91 additions & 1 deletion pkg/ofac/mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
)

func TestMapper(t *testing.T) {
func TestMapper__Person(t *testing.T) {
res, err := Read(filepath.Join("..", "..", "test", "testdata", "sdn.csv"))
require.NoError(t, err)

Expand Down Expand Up @@ -41,3 +41,93 @@ func TestMapper(t *testing.T) {

require.Equal(t, "15102", e.SourceData.EntityID)
}

func TestMapper__Vessel(t *testing.T) {
res, err := Read(filepath.Join("..", "..", "test", "testdata", "sdn.csv"))
require.NoError(t, err)

var sdn *SDN
for i := range res.SDNs {
if res.SDNs[i].EntityID == "15036" {
sdn = res.SDNs[i]
}
}
require.NotNil(t, sdn)

e := ToEntity(*sdn)
require.Equal(t, "ARTAVIL", e.Name)
require.Equal(t, search.EntityVessel, e.Type)
require.Equal(t, search.SourceUSOFAC, e.Source)

require.Nil(t, e.Person)
require.Nil(t, e.Business)
require.Nil(t, e.Organization)
require.Nil(t, e.Aircraft)
require.NotNil(t, e.Vessel)

require.Equal(t, "ARTAVIL", e.Vessel.Name)
require.Equal(t, "Malta", e.Vessel.Flag)
require.Equal(t, "9187629", e.Vessel.IMONumber)
require.Equal(t, "572469210", e.Vessel.MMSI)

require.Equal(t, "15036", e.SourceData.EntityID)
}

func TestMapper__Aircraft(t *testing.T) {
res, err := Read(filepath.Join("..", "..", "test", "testdata", "sdn.csv"))
require.NoError(t, err)

var sdn *SDN
for i := range res.SDNs {
if res.SDNs[i].EntityID == "18158" {
sdn = res.SDNs[i]
}
}
require.NotNil(t, sdn)

e := ToEntity(*sdn)
require.Equal(t, "MSN 550", e.Name)
require.Equal(t, search.EntityAircraft, e.Type)
require.Equal(t, search.SourceUSOFAC, e.Source)

require.Nil(t, e.Person)
require.Nil(t, e.Business)
require.Nil(t, e.Organization)
require.NotNil(t, e.Aircraft)
require.Nil(t, e.Vessel)

require.Equal(t, "MSN 550", e.Aircraft.Name)
require.Equal(t, "1995-01-01", e.Aircraft.Built.Format(time.DateOnly))
require.Equal(t, "Airbus A321-131", e.Aircraft.Model)
require.Equal(t, "550", e.Aircraft.SerialNumber)

require.Equal(t, "18158", e.SourceData.EntityID)
}

func TestParseTime(t *testing.T) {
t.Run("DOB", func(t *testing.T) {
tt, _ := parseTime(dobPatterns, "01 Apr 1950")
require.Equal(t, "1950-04-01", tt.Format(time.DateOnly))

tt, _ = parseTime(dobPatterns, "01 Feb 1958 to 28 Feb 1958")
require.Equal(t, "1958-02-01", tt.Format(time.DateOnly))

tt, _ = parseTime(dobPatterns, "1928")
require.Equal(t, "1928-01-01", tt.Format(time.DateOnly))

tt, _ = parseTime(dobPatterns, "1928 to 1930")
require.Equal(t, "1928-01-01", tt.Format(time.DateOnly))

tt, _ = parseTime(dobPatterns, "Sep 1958")
require.Equal(t, "1958-09-01", tt.Format(time.DateOnly))

tt, _ = parseTime(dobPatterns, "circa 01 Jan 1961")
require.Equal(t, "1961-01-01", tt.Format(time.DateOnly))

tt, _ = parseTime(dobPatterns, "circa 1934")
require.Equal(t, "1934-01-01", tt.Format(time.DateOnly))

tt, _ = parseTime(dobPatterns, "circa 1979-1982")
require.Equal(t, "1979-01-01", tt.Format(time.DateOnly))
})
}

0 comments on commit ce83beb

Please sign in to comment.