Skip to content
This repository has been archived by the owner on Aug 26, 2022. It is now read-only.

Commit

Permalink
Merge pull request #46 from adamdecaf/status-approvedat
Browse files Browse the repository at this point in the history
cmd/server: add methods for easier checking of Customer status levels
  • Loading branch information
adamdecaf authored Oct 28, 2019
2 parents efadd92 + 045cce0 commit c87fa03
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 111 deletions.
28 changes: 12 additions & 16 deletions cmd/server/approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package main

import (
"database/sql"
"encoding/json"
"errors"
"fmt"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
44 changes: 23 additions & 21 deletions cmd/server/approval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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{
Expand All @@ -178,23 +180,23 @@ 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")
}
}

func TestCustomers__validCustomerStatusTransitionError(t *testing.T) {
cust := &client.Customer{
ID: base.ID(),
Status: CustomerStatusReviewRequired,
Status: customers.ReviewRequired.String(),
}
repo := &testCustomerRepository{}
ofacClient := &testOFACClient{}
Expand All @@ -203,21 +205,21 @@ 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")
}
}

func TestCustomers__validCustomerStatusTransitionOFAC(t *testing.T) {
cust := &client.Customer{
ID: base.ID(),
Status: CustomerStatusReviewRequired,
Status: customers.ReviewRequired.String(),
}
repo := &testCustomerRepository{}
searcher := createTestOFACSearcher(repo, nil)
Expand All @@ -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)
}
Expand All @@ -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" {
Expand Down
48 changes: 6 additions & 42 deletions cmd/server/customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand All @@ -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()
Expand Down
Loading

0 comments on commit c87fa03

Please sign in to comment.