Skip to content
This repository has been archived by the owner on Dec 9, 2021. It is now read-only.

Commit

Permalink
cmd/server: whitelist only certain CustomerStatus transistions
Browse files Browse the repository at this point in the history
Not every CustomerStatus can be converted into another, so we need to block bad transistions.

Issue: moov-io#5
  • Loading branch information
adamdecaf committed Jun 3, 2019
1 parent e06933b commit 59121c9
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 3 deletions.
43 changes: 43 additions & 0 deletions cmd/server/customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,39 @@ type updateCustomerStatusRequest struct {
Status CustomerStatus `json:"status"`
}

// validCustomerStatusTransition determines if a future CustomerStatus is valid for a given
// Customer. There are several rules which apply to a CustomerStatus, such as:
// - Deceased, Rejected statuses can never be changed
// - 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, futureStatus CustomerStatus) 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)
}
switch futureStatus {
case CustomerStatusKYC:
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 len(existing.Addresses) == 0 { // TODO(adam): we should probably check existing.Addresses.exists(_.Validated)
return fmt.Errorf("customer=%s is missing an Address", existing.Id)
}
case CustomerStatusOFAC: // TODO(adam): need to impl lookup
return fmt.Errorf("customers=%s %s to OFAC transition needs to lookup OFAC search results", existing.Id, existing.Status)
case CustomerStatusCIP: // TODO(adam): need to impl lookup
return fmt.Errorf("customers=%s %s to CIP transition needs to lookup encrypted SSN", existing.Id, existing.Status)
}
return nil
}

func updateCustomerStatus(logger log.Logger, repo customerRepository) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w = wrapResponseWriter(logger, w, r)
Expand All @@ -265,6 +298,16 @@ func updateCustomerStatus(logger log.Logger, repo customerRepository) http.Handl
return
}

cust, err := repo.getCustomer(customerId)
if err != nil {
moovhttp.Problem(w, err)
return
}
if err := validCustomerStatusTransition(cust, req.Status); err != nil {
moovhttp.Problem(w, err)
return
}

// Update Customer's status in the database
if err := repo.updateCustomerStatus(customerId, req.Status, req.Comment); err != nil {
moovhttp.Problem(w, err)
Expand Down
53 changes: 50 additions & 3 deletions cmd/server/customers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/moov-io/base"
client "github.com/moov-io/customers/client"
Expand Down Expand Up @@ -309,7 +310,7 @@ func TestCustomers__updateCustomerStatus(t *testing.T) {
},
}

body := strings.NewReader(`{"status": "OFAC", "comment": "test comment"}`)
body := strings.NewReader(`{"status": "ReviewRequired", "comment": "test comment"}`)

w := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "/customers/foo/status", body)
Expand All @@ -322,7 +323,7 @@ func TestCustomers__updateCustomerStatus(t *testing.T) {
w.Flush()

if w.Code != http.StatusOK {
t.Errorf("bogus HTTP status: %d", w.Code)
t.Errorf("bogus HTTP status: %d: %v", w.Code, w.Body.String())
}

var customer client.Customer
Expand All @@ -332,7 +333,7 @@ func TestCustomers__updateCustomerStatus(t *testing.T) {
if customer.Id == "" {
t.Errorf("missing customer JSON: %#v", customer)
}
if repo.updatedStatus != CustomerStatusOFAC {
if repo.updatedStatus != CustomerStatusReviewRequired {
t.Errorf("unexpected status: %s", repo.updatedStatus)
}
}
Expand Down Expand Up @@ -540,3 +541,49 @@ func TestCustomers__validateMetadata(t *testing.T) {
t.Error("expected error")
}
}

func TestCustomers__validCustomerStatusTransition(t *testing.T) {
cust := &client.Customer{
Id: base.ID(),
Status: CustomerStatusNone,
}

if err := validCustomerStatusTransition(cust, CustomerStatusDeceased); err != nil {
t.Errorf("expected no error: %v", err)
}

// block Deceased and Rejected customers
cust.Status = CustomerStatusDeceased
if err := validCustomerStatusTransition(cust, CustomerStatusKYC); err == nil {
t.Error("expected error")
}
cust.Status = CustomerStatusRejected
if err := validCustomerStatusTransition(cust, CustomerStatusKYC); 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, CustomerStatusKYC); err == nil {
t.Error("expected error")
}
cust.BirthDate = time.Now()
if err := validCustomerStatusTransition(cust, CustomerStatusKYC); err == nil {
t.Error("expected error")
}
cust.Addresses = append(cust.Addresses, client.Address{
Type: "primary",
Address1: "123 1st st",
})

// OFAC and CIP transistions are WIP // TODO(adam): impl both transistions
cust.Status = CustomerStatusReviewRequired
if err := validCustomerStatusTransition(cust, CustomerStatusOFAC); err == nil {
t.Error("OFAC transition is WIP")
}
cust.Status = CustomerStatusReviewRequired
if err := validCustomerStatusTransition(cust, CustomerStatusCIP); err == nil {
t.Error("CIP transition is WIP")
}
}

0 comments on commit 59121c9

Please sign in to comment.