Repository: moov-io/customers
Pull request #46 (2019-10-28 17:32:00)

cmd/server: add methods for easier checking of Customer status levels
Paygate (and other apps which use Customers) needs to ensure their status is sufficient to initiate Transfers (meaning OFAC or CIP). This is a helper for doing so. 


Patch:
diff --git a/cmd/server/approval.go b/cmd/server/approval.go
index 5e95da9..77db359 100644
--- a/cmd/server/approval.go
+++ b/cmd/server/approval.go
@@ -5,6 +5,7 @@
 package main
 
 import (
+	"database/sql"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -15,6 +16,7 @@ import (
 
 	"github.com/moov-io/base/admin"
 	moovhttp "github.com/moov-io/base/http"
+	"github.com/moov-io/customers"
 	client "github.com/moov-io/customers/client"
 
 	"github.com/go-kit/kit/log"
@@ -44,8 +46,8 @@ func addApprovalRoutes(logger log.Logger, svc *admin.Server, repo customerReposi
 }
 
 type updateCustomerStatusRequest struct {
-	Comment string         `json:"comment,omitempty"`
-	Status  CustomerStatus `json:"status"`
+	Comment string           `json:"comment,omitempty"`
+	Status  customers.Status `json:"status"`
 }
 
 // validCustomerStatusTransition determines if a future CustomerStatus is valid for a given
@@ -54,29 +56,23 @@ type updateCustomerStatusRequest struct {
 //  - KYC is only valid if the Customer has first, last, address, and date of birth
 //  - OFAC can only be after an OFAC search has been performed (and search info recorded)
 //  - CIP can only be if the SSN has been set
-func validCustomerStatusTransition(existing *client.Customer, ssn *SSN, futureStatus CustomerStatus, repo customerRepository, ofac *ofacSearcher, requestID string) error {
-	eql := func(s string, status CustomerStatus) bool {
-		return strings.EqualFold(s, string(status))
-	}
-	// Check Deceased and Rejected
-	if eql(existing.Status, CustomerStatusDeceased) || eql(existing.Status, CustomerStatusRejected) {
-		return fmt.Errorf("customer status '%s' cannot be changed", existing.Status)
+func validCustomerStatusTransition(existing *client.Customer, ssn *SSN, futureStatus customers.Status, repo customerRepository, ofac *ofacSearcher, requestID string) error {
+	// Reject certain Deceased and Rejected statuses
+	if cs, err := customers.LiftStatus(existing.Status); err != nil || cs == nil || *cs <= customers.Rejected {
+		return fmt.Errorf("customer status '%s' cannot be changed: %v", existing.Status, err)
 	}
 	switch futureStatus {
-	case CustomerStatusKYC:
+	case customers.KYC:
 		if existing.FirstName == "" || existing.LastName == "" {
 			return fmt.Errorf("customer=%s is missing fist/last name", existing.ID)
 		}
 		if existing.BirthDate.IsZero() {
 			return fmt.Errorf("customer=%s is missing date of birth", existing.ID)
 		}
-		if existing.Email == "" {
-			return fmt.Errorf("customer=%s is missing an email address", existing.ID)
-		}
 		if !containsValidPrimaryAddress(existing.Addresses) {
 			return fmt.Errorf("customer=%s is missing a valid primary Address", existing.ID)
 		}
-	case CustomerStatusOFAC:
+	case customers.OFAC:
 		searchResult, err := repo.getLatestCustomerOFACSearch(existing.ID)
 		if err != nil {
 			return fmt.Errorf("validCustomerStatusTransition: error getting OFAC search: %v", err)
@@ -94,7 +90,7 @@ func validCustomerStatusTransition(existing *client.Customer, ssn *SSN, futureSt
 			return fmt.Errorf("validCustomerStatusTransition: customer=%s has positive OFAC match (%.2f) with SDN=%s", existing.ID, searchResult.match, searchResult.entityId)
 		}
 		return nil
-	case CustomerStatusCIP: // TODO(adam): need to impl lookup
+	case customers.CIP: // TODO(adam): need to impl lookup
 		// What can we do to validate an SSN?
 		// https://www.ssa.gov/employer/randomization.html (not much)
 		if ssn == nil || len(ssn.encrypted) == 0 {
@@ -141,7 +137,7 @@ func updateCustomerStatus(logger log.Logger, repo customerRepository, customerSS
 		}
 
 		ssn, err := customerSSNRepo.getCustomerSSN(customerID)
-		if err != nil {
+		if err != nil && err != sql.ErrNoRows {
 			moovhttp.Problem(w, err)
 			return
 		}
diff --git a/cmd/server/customers.go b/cmd/server/customers.go
index 8192f4d..a134497 100644
--- a/cmd/server/customers.go
+++ b/cmd/server/customers.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/moov-io/base"
 	moovhttp "github.com/moov-io/base/http"
+	"github.com/moov-io/customers"
 	client "github.com/moov-io/customers/client"
 
 	"github.com/go-kit/kit/log"
@@ -26,43 +27,6 @@ var (
 	errNoCustomerID = errors.New("no Customer ID found")
 )
 
-type CustomerStatus string
-
-const (
-	CustomerStatusDeceased       = "deceased"
-	CustomerStatusRejected       = "rejected"
-	CustomerStatusNone           = "none"
-	CustomerStatusReviewRequired = "reviewrequired"
-	CustomerStatusKYC            = "kyc"
-	CustomerStatusOFAC           = "ofac"
-	CustomerStatusCIP            = "cip"
-)
-
-func (cs CustomerStatus) validate() error {
-	switch cs {
-	case CustomerStatusDeceased, CustomerStatusRejected:
-		return nil
-	case CustomerStatusReviewRequired, CustomerStatusNone:
-		return nil
-	case CustomerStatusKYC, CustomerStatusOFAC, CustomerStatusCIP:
-		return nil
-	default:
-		return fmt.Errorf("CustomerStatus(%s) is invalid", cs)
-	}
-}
-
-func (cs *CustomerStatus) UnmarshalJSON(b []byte) error {
-	var s string
-	if err := json.Unmarshal(b, &s); err != nil {
-		return err
-	}
-	*cs = CustomerStatus(strings.TrimSpace(strings.ToLower(s)))
-	if err := cs.validate(); err != nil {
-		return err
-	}
-	return nil
-}
-
 func addCustomerRoutes(logger log.Logger, r *mux.Router, repo customerRepository, customerSSNStorage *ssnStorage, ofac *ofacSearcher) {
 	r.Methods("GET").Path("/customers/{customerID}").HandlerFunc(getCustomer(logger, repo))
 	r.Methods("POST").Path("/customers").HandlerFunc(createCustomer(logger, repo, customerSSNStorage, ofac))
@@ -186,7 +150,7 @@ func (req customerRequest) asCustomer(storage *ssnStorage) (*client.Customer, *S
 		Suffix:     req.Suffix,
 		BirthDate:  req.BirthDate,
 		Email:      req.Email,
-		Status:     CustomerStatusNone,
+		Status:     customers.None.String(),
 		Metadata:   req.Metadata,
 	}
 	for i := range req.Phones {
@@ -328,7 +292,7 @@ func addCustomerAddress(logger log.Logger, repo customerRepository) http.Handler
 type customerRepository interface {
 	getCustomer(customerID string) (*client.Customer, error)
 	createCustomer(c *client.Customer) error
-	updateCustomerStatus(customerID string, status CustomerStatus, comment string) error
+	updateCustomerStatus(customerID string, status customers.Status, comment string) error
 
 	getCustomerMetadata(customerID string) (map[string]string, error)
 	replaceCustomerMetadata(customerID string, metadata map[string]string) error
@@ -496,7 +460,7 @@ func (r *sqlCustomerRepository) readAddresses(customerID string) ([]client.Addre
 	return adds, rows.Err()
 }
 
-func (r *sqlCustomerRepository) updateCustomerStatus(customerID string, status CustomerStatus, comment string) error {
+func (r *sqlCustomerRepository) updateCustomerStatus(customerID string, status customers.Status, comment string) error {
 	tx, err := r.db.Begin()
 	if err != nil {
 		return fmt.Errorf("updateCustomerStatus: tx begin: %v", err)
@@ -508,7 +472,7 @@ func (r *sqlCustomerRepository) updateCustomerStatus(customerID string, status C
 	if err != nil {
 		return fmt.Errorf("updateCustomerStatus: update customers prepare: %v", err)
 	}
-	if _, err := stmt.Exec(status, customerID); err != nil {
+	if _, err := stmt.Exec(status.String(), customerID); err != nil {
 		stmt.Close()
 		return fmt.Errorf("updateCustomerStatus: update customers exec: %v", err)
 	}
@@ -521,7 +485,7 @@ func (r *sqlCustomerRepository) updateCustomerStatus(customerID string, status C
 		return fmt.Errorf("updateCustomerStatus: insert status prepare: %v", err)
 	}
 	defer stmt.Close()
-	if _, err := stmt.Exec(customerID, status, comment, time.Now()); err != nil {
+	if _, err := stmt.Exec(customerID, status.String(), comment, time.Now()); err != nil {
 		return fmt.Errorf("updateCustomerStatus: insert status exec: %v", err)
 	}
 	return tx.Commit()
diff --git a/status.go b/status.go
new file mode 100644
index 0000000..3e51ad5
--- /dev/null
+++ b/status.go
@@ -0,0 +1,97 @@
+// Copyright 2019 The Moov Authors
+// Use of this source code is governed by an Apache License
+// license that can be found in the LICENSE file.
+
+package customers
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+)
+
+type Status int
+
+const (
+	Deceased Status = iota
+	Rejected
+	None
+	ReviewRequired
+	KYC
+	OFAC
+	CIP
+)
+
+var (
+	customerStatusStrings = []string{"deceased", "rejected", "none", "reviewrequired", "kyc", "ofac", "cip"}
+)
+
+func (cs Status) validate() error {
+	switch cs {
+	case Deceased, Rejected:
+		return nil
+	case ReviewRequired, None:
+		return nil
+	case KYC, OFAC, CIP:
+		return nil
+	default:
+		return fmt.Errorf("status '%v' is invalid", cs)
+	}
+}
+
+func (cs Status) String() string {
+	if cs < Deceased || cs > CIP {
+		return "unknown"
+	}
+	return customerStatusStrings[int(cs)]
+}
+
+func (cs *Status) UnmarshalJSON(b []byte) error {
+	var s string
+	if err := json.Unmarshal(b, &s); err != nil {
+		return err
+	}
+	cs.fromString(s)
+	if err := cs.validate(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (cs *Status) fromString(s string) {
+	for i := range customerStatusStrings {
+		if strings.EqualFold(s, customerStatusStrings[i]) {
+			*cs = Status(i)
+			return
+		}
+	}
+	*cs = Status(-1)
+}
+
+// LiftStatus will attempt to return an enum value of CustomerStatus after reading
+// the string value.
+func LiftStatus(str string) (*Status, error) {
+	var cs Status
+	cs.fromString(str)
+	if err := cs.validate(); err != nil {
+		return nil, err
+	}
+	return &cs, nil
+}
+
+// ApprovedAt returns true only if the customerStatus is higher than ReviewRequired
+// and is at least the minimum status. It's used to ensure a specific customer is at least
+// KYC, OFAC, or CIP in applications.
+func (cs Status) ApprovedAt(minimum Status) bool {
+	return ApprovedAt(cs, minimum)
+}
+
+// ApprovedAt returns true only if the customerStatus is higher than ReviewRequired
+// and is at least the minimum status. It's used to ensure a specific customer is at least
+// KYC, OFAC, or CIP in applications.
+func ApprovedAt(customerStatus Status, minimum Status) bool {
+	if customerStatus <= ReviewRequired {
+		return false // any status below ReveiewRequired is never approved
+	}
+	return customerStatus >= minimum
+}


Test patch:
diff --git a/cmd/server/approval_test.go b/cmd/server/approval_test.go
index 8decb0c..2f9e060 100644
--- a/cmd/server/approval_test.go
+++ b/cmd/server/approval_test.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/moov-io/base"
 	"github.com/moov-io/base/admin"
+	"github.com/moov-io/customers"
 	client "github.com/moov-io/customers/client"
 	ofac "github.com/moov-io/ofac/client"
 
@@ -27,7 +28,8 @@ import (
 func TestCustomers__updateCustomerStatus(t *testing.T) {
 	repo := &testCustomerRepository{
 		customer: &client.Customer{
-			ID: base.ID(),
+			ID:     base.ID(),
+			Status: "none",
 		},
 	}
 	searcher := createTestOFACSearcher(repo, nil)
@@ -63,7 +65,7 @@ func TestCustomers__updateCustomerStatus(t *testing.T) {
 	if customer.ID == "" {
 		t.Errorf("missing customer JSON: %#v", customer)
 	}
-	if repo.updatedStatus != CustomerStatusReviewRequired {
+	if repo.updatedStatus != customers.ReviewRequired {
 		t.Errorf("unexpected status: %s", repo.updatedStatus)
 	}
 }
@@ -141,35 +143,35 @@ func TestCustomers__containsValidPrimaryAddress(t *testing.T) {
 func TestCustomers__validCustomerStatusTransition(t *testing.T) {
 	cust := &client.Customer{
 		ID:     base.ID(),
-		Status: CustomerStatusNone,
+		Status: customers.None.String(),
 	}
 	repo := &testCustomerRepository{}
 	searcher := createTestOFACSearcher(repo, nil)
 
 	ssn := &SSN{customerID: cust.ID, encrypted: []byte("secret")}
 
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusDeceased, repo, searcher, "requestID"); err != nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.Deceased, repo, searcher, "requestID"); err != nil {
 		t.Errorf("expected no error: %v", err)
 	}
 
 	// block Deceased and Rejected customers
-	cust.Status = CustomerStatusDeceased
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusKYC, repo, searcher, "requestID"); err == nil {
+	cust.Status = customers.Deceased.String()
+	if err := validCustomerStatusTransition(cust, ssn, customers.KYC, repo, searcher, "requestID"); err == nil {
 		t.Error("expected error")
 	}
-	cust.Status = CustomerStatusRejected
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusKYC, repo, searcher, "requestID"); err == nil {
+	cust.Status = customers.Rejected.String()
+	if err := validCustomerStatusTransition(cust, ssn, customers.KYC, repo, searcher, "requestID"); err == nil {
 		t.Error("expected error")
 	}
 
 	// normal KYC approval (rejected due to missing info)
 	cust.FirstName, cust.LastName = "Jane", "Doe"
-	cust.Status = CustomerStatusReviewRequired
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusKYC, repo, searcher, "requestID"); err == nil {
+	cust.Status = customers.ReviewRequired.String()
+	if err := validCustomerStatusTransition(cust, ssn, customers.KYC, repo, searcher, "requestID"); err == nil {
 		t.Error("expected error")
 	}
 	cust.BirthDate = time.Now()
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusKYC, repo, searcher, "requestID"); err == nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.KYC, repo, searcher, "requestID"); err == nil {
 		t.Error("expected error")
 	}
 	cust.Addresses = append(cust.Addresses, client.Address{
@@ -178,15 +180,15 @@ func TestCustomers__validCustomerStatusTransition(t *testing.T) {
 	})
 
 	// CIP transistions are WIP // TODO(adam):
-	cust.Status = CustomerStatusReviewRequired
-	if err := validCustomerStatusTransition(cust, nil, CustomerStatusCIP, repo, searcher, "requestID"); err != nil {
+	cust.Status = customers.ReviewRequired.String()
+	if err := validCustomerStatusTransition(cust, nil, customers.CIP, repo, searcher, "requestID"); err != nil {
 		if !strings.Contains(err.Error(), "is missing SSN") {
 			t.Errorf("CIP: unexpected error: %v", err)
 		}
 	} else {
 		t.Error("CIP transition is WIP")
 	}
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusCIP, repo, searcher, "requestID"); err == nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.CIP, repo, searcher, "requestID"); err == nil {
 		t.Error("CIP transition is WIP")
 	}
 }
@@ -194,7 +196,7 @@ func TestCustomers__validCustomerStatusTransition(t *testing.T) {
 func TestCustomers__validCustomerStatusTransitionError(t *testing.T) {
 	cust := &client.Customer{
 		ID:     base.ID(),
-		Status: CustomerStatusReviewRequired,
+		Status: customers.ReviewRequired.String(),
 	}
 	repo := &testCustomerRepository{}
 	ofacClient := &testOFACClient{}
@@ -203,13 +205,13 @@ func TestCustomers__validCustomerStatusTransitionError(t *testing.T) {
 	ssn := &SSN{customerID: cust.ID, encrypted: []byte("secret")}
 
 	repo.err = errors.New("bad error")
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusOFAC, repo, searcher, ""); err == nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.OFAC, repo, searcher, ""); err == nil {
 		t.Error("expected error, but got none")
 	}
 	repo.err = nil
 
 	ofacClient.err = errors.New("bad error")
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusOFAC, repo, searcher, ""); err == nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.OFAC, repo, searcher, ""); err == nil {
 		t.Error("expected error, but got none")
 	}
 }
@@ -217,7 +219,7 @@ func TestCustomers__validCustomerStatusTransitionError(t *testing.T) {
 func TestCustomers__validCustomerStatusTransitionOFAC(t *testing.T) {
 	cust := &client.Customer{
 		ID:     base.ID(),
-		Status: CustomerStatusReviewRequired,
+		Status: customers.ReviewRequired.String(),
 	}
 	repo := &testCustomerRepository{}
 	searcher := createTestOFACSearcher(repo, nil)
@@ -228,13 +230,13 @@ func TestCustomers__validCustomerStatusTransitionOFAC(t *testing.T) {
 		sdnName: "Jane Doe",
 		match:   0.10,
 	}
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusOFAC, repo, searcher, "requestID"); err != nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.OFAC, repo, searcher, "requestID"); err != nil {
 		t.Errorf("unexpected error in OFAC transition: %v", err)
 	}
 
 	// OFAC transition with positive match
 	repo.ofacSearchResult.match = 0.99
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusOFAC, repo, searcher, "requestID"); err != nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.OFAC, repo, searcher, "requestID"); err != nil {
 		if !strings.Contains(err.Error(), "positive OFAC match") {
 			t.Errorf("unexpected error in OFAC transition: %v", err)
 		}
@@ -247,7 +249,7 @@ func TestCustomers__validCustomerStatusTransitionOFAC(t *testing.T) {
 			EntityID: "12124",
 		}
 	}
-	if err := validCustomerStatusTransition(cust, ssn, CustomerStatusOFAC, repo, searcher, "requestID"); err != nil {
+	if err := validCustomerStatusTransition(cust, ssn, customers.OFAC, repo, searcher, "requestID"); err != nil {
 		t.Errorf("unexpected error: %v", err)
 	}
 	if repo.savedOFACSearchResult.entityId != "12124" {
diff --git a/cmd/server/customers_test.go b/cmd/server/customers_test.go
index 567ff07..81957d5 100644
--- a/cmd/server/customers_test.go
+++ b/cmd/server/customers_test.go
@@ -18,6 +18,7 @@ import (
 	"github.com/moov-io/customers/internal/database"
 
 	"github.com/moov-io/base"
+	"github.com/moov-io/customers"
 	client "github.com/moov-io/customers/client"
 
 	"github.com/go-kit/kit/log"
@@ -30,7 +31,7 @@ type testCustomerRepository struct {
 	ofacSearchResult *ofacSearchResult
 
 	createdCustomer       *client.Customer
-	updatedStatus         CustomerStatus
+	updatedStatus         customers.Status
 	savedOFACSearchResult *ofacSearchResult
 }
 
@@ -46,7 +47,7 @@ func (r *testCustomerRepository) createCustomer(c *client.Customer) error {
 	return r.err
 }
 
-func (r *testCustomerRepository) updateCustomerStatus(customerID string, status CustomerStatus, comment string) error {
+func (r *testCustomerRepository) updateCustomerStatus(customerID string, status customers.Status, comment string) error {
 	r.updatedStatus = status
 	return r.err
 }
@@ -92,34 +93,6 @@ func TestCustomers__getCustomerID(t *testing.T) {
 	}
 }
 
-func TestCustomerStatus__json(t *testing.T) {
-	cs := CustomerStatus("invalid")
-	valid := map[string]CustomerStatus{
-		"deCEAsed":       CustomerStatusDeceased,
-		"Rejected":       CustomerStatusRejected,
-		"ReviewRequired": CustomerStatusReviewRequired,
-		"NONE":           CustomerStatusNone,
-		"KYC":            CustomerStatusKYC,
-		"ofaC":           CustomerStatusOFAC,
-		"cip":            CustomerStatusCIP,
-	}
-	for k, v := range valid {
-		in := []byte(fmt.Sprintf(`"%v"`, k))
-		if err := json.Unmarshal(in, &cs); err != nil {
-			t.Error(err.Error())
-		}
-		if cs != v {
-			t.Errorf("got cs=%#v, v=%#v", cs, v)
-		}
-	}
-
-	// make sure other values fail
-	in := []byte(fmt.Sprintf(`"%v"`, base.ID()))
-	if err := json.Unmarshal(in, &cs); err == nil {
-		t.Error("expected error")
-	}
-}
-
 func TestCustomers__formatCustomerName(t *testing.T) {
 	if out := formatCustomerName(nil); out != "" {
 		t.Errorf("got %q", out)
@@ -393,7 +366,7 @@ func TestCustomerRepository__updateCustomerStatus(t *testing.T) {
 	}
 
 	// update status
-	if err := repo.updateCustomerStatus(cust.ID, CustomerStatusKYC, "test comment"); err != nil {
+	if err := repo.updateCustomerStatus(cust.ID, customers.KYC, "test comment"); err != nil {
 		t.Fatal(err)
 	}
 
@@ -402,7 +375,7 @@ func TestCustomerRepository__updateCustomerStatus(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if customer.Status != CustomerStatusKYC {
+	if customer.Status != customers.KYC.String() {
 		t.Errorf("unexpected status: %s", customer.Status)
 	}
 }
diff --git a/status_test.go b/status_test.go
new file mode 100644
index 0000000..1c12257
--- /dev/null
+++ b/status_test.go
@@ -0,0 +1,89 @@
+// Copyright 2019 The Moov Authors
+// Use of this source code is governed by an Apache License
+// license that can be found in the LICENSE file.
+
+package customers
+
+import (
+	"encoding/json"
+	"fmt"
+	"testing"
+
+	"github.com/moov-io/base"
+)
+
+func TestStatus__json(t *testing.T) {
+	cs := Status(10)
+	valid := map[string]Status{
+		"deCEAsed":       Deceased,
+		"Rejected":       Rejected,
+		"ReviewRequired": ReviewRequired,
+		"NONE":           None,
+		"KYC":            KYC,
+		"ofaC":           OFAC,
+		"cip":            CIP,
+	}
+	for k, v := range valid {
+		in := []byte(fmt.Sprintf(`"%s"`, k))
+		if err := json.Unmarshal(in, &cs); err != nil {
+			t.Error(err.Error())
+		}
+		if cs != v {
+			t.Errorf("got cs=%#v, v=%#v", cs, v)
+		}
+	}
+
+	// make sure other values fail
+	in := []byte(fmt.Sprintf(`"%v"`, base.ID()))
+	if err := json.Unmarshal(in, &cs); err == nil {
+		t.Error("expected error")
+	}
+}
+
+func TestStatus__string(t *testing.T) {
+	if v := OFAC.String(); v != "ofac" {
+		t.Errorf("got %s", v)
+	}
+	if v := Deceased.String(); v != "deceased" {
+		t.Errorf("got %s", v)
+	}
+}
+
+func TestStatus__liftStatus(t *testing.T) {
+	if cs, err := LiftStatus("kyc"); *cs != KYC || err != nil {
+		t.Errorf("got %s error=%v", cs, err)
+	}
+	if cs, err := LiftStatus("none"); *cs != None || err != nil {
+		t.Errorf("got %s error=%v", cs, err)
+	}
+	if cs, err := LiftStatus("cip"); *cs != CIP || err != nil {
+		t.Errorf("got %s error=%v", cs, err)
+	}
+}
+
+func TestStatus__approvedAt(t *testing.T) {
+	// authorized
+	if !ApprovedAt(OFAC, OFAC) {
+		t.Errorf("expected ApprovedAt")
+	}
+	if !ApprovedAt(OFAC, KYC) {
+		t.Errorf("expected ApprovedAt")
+	}
+	if !ApprovedAt(CIP, KYC) {
+		t.Errorf("expected ApprovedAt")
+	}
+
+	// not authorized
+	if ApprovedAt(ReviewRequired, ReviewRequired) {
+		t.Errorf("expected not ApprovedAt")
+	}
+	if ApprovedAt(None, OFAC) {
+		t.Errorf("expected not ApprovedAt")
+	}
+	if ApprovedAt(OFAC, CIP) {
+		t.Errorf("expected not ApprovedAt")
+	}
+	if ApprovedAt(Rejected, OFAC) {
+		t.Errorf("expected not ApprovedAt")
+	}
+}