Repository: moov-io/customers
Pull request #152 (2020-09-14 18:36:46)

account ofac search
This PR supersedes [https://github.com/moov-io/customers/pull/147](https://github.com/moov-io/customers/pull/147). There were many conflicts in the original and I thought it'd be easier to create a new PR.

codecov-commenter: # [Codecov](https://codecov.io/gh/moov-io/customers/pull/152?src=pr&el=h1) Report
> Merging [#152](https://codecov.io/gh/moov-io/customers/pull/152?src=pr&el=desc) into [master](https://codecov.io/gh/moov-io/customers/commit/873779e44d2b2b8cd6e379dda7959dbcb1f45689?el=desc) will **decrease** coverage by `0.36%`.
> The diff coverage is `52.77%`.


```diff
@@            Coverage Diff             @@
##           master     #152      +/-   ##
==========================================
- Coverage   64.82%   64.45%   -0.37%     
==========================================
  Files          42       43       +1     
  Lines        2402     2470      +68     
==========================================
+ Hits         1557     1592      +35     
- Misses        592      613      +21     
- Partials      253      265      +12     
```


Patch:
diff --git a/README.md b/README.md
index 34c3f22..97dc7c7 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ Following parameters should be set via environment to configure the account vali
 
 * `PLAID_CLIENT_ID`: Client ID
 * `PLAID_SECRET`: API secret (depends on the environent)
-* `PLAID_ENVIRONMENT`: Plaid environment (e.g., sandbox, development or production | default `sandbox`)
+* `PLAID_ENVIRONMENT`: Plaid environment (e.g., sandbox, development or production)
 * `PLAID_CLIENT_NAME`: The app name that should be displayed in Link
 
 [Here](https://plaid.com/docs/#api-keys-and-access) you can find more information on how to get them.
diff --git a/api/client.yaml b/api/client.yaml
index fe11c13..e4b262d 100644
--- a/api/client.yaml
+++ b/api/client.yaml
@@ -527,6 +527,52 @@ paths:
             application/json:
               schema:
                 $ref: 'https://raw.githubusercontent.com/moov-io/api/master/openapi-common.yaml#/components/schemas/Error'
+  /customers/{customerID}/accounts/{accountID}/ofac:
+    get:
+      tags: [ Customers ]
+      summary: Get latest OFAC search
+      description: Get the latest OFAC search for an account
+      operationId: getLatestAccountOFACSearch
+      parameters:
+        - name: X-Request-ID
+          in: header
+          description: Optional requestID allows application developer to trace requests through the systems logs
+          example: rs4f9915
+          schema:
+            type: string
+        - name: X-User-ID
+          in: header
+          description: Unique userID set by an auth proxy or client to identify and isolate objects.
+          example: e3cdf999
+          schema:
+            type: string
+        - name: customerID
+          in: path
+          description: customerID of the Customer
+          required: true
+          schema:
+            type: string
+            example: e210a9d6
+        - name: accountID
+          in: path
+          description: accountID of the Account to get latest OFAC search
+          required: true
+          schema:
+            type: string
+            example: e210a9d6
+      responses:
+        '200':
+          description: OFAC search did not block account
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/OFACSearch'
+        '400':
+          description: An error occurred when refreshing OFAC data, see error(s)
+          content:
+            application/json:
+              schema:
+                $ref: 'https://raw.githubusercontent.com/moov-io/api/master/openapi-common.yaml#/components/schemas/Error'
   /customers/{customerID}/accounts/{accountID}/validations:
     post:
       tags: [Customers]
diff --git a/cmd/server/accounts/accounts_ofac.go b/cmd/server/accounts/accounts_ofac.go
new file mode 100644
index 0000000..0afdd41
--- /dev/null
+++ b/cmd/server/accounts/accounts_ofac.go
@@ -0,0 +1,53 @@
+package accounts
+
+import (
+	"context"
+	"fmt"
+	"github.com/moov-io/customers/pkg/client"
+	watchman "github.com/moov-io/watchman/client"
+	"github.com/pkg/errors"
+	"time"
+)
+
+type AccountOfacSearcher struct {
+	Repo           Repository
+	WatchmanClient WatchmanClient
+}
+
+type WatchmanClient interface {
+	Ping() error
+
+	Search(ctx context.Context, name string, requestID string) (*watchman.OfacSdn, error)
+}
+
+// StoreAccountOFACSearch performs OFAC searches against the Account's HolderName and nickname if populated.
+// The search result is stored in s.Repo for use later (in approvals)
+func (s *AccountOfacSearcher) StoreAccountOFACSearch(account *client.Account, requestID string) error {
+	ctx, cancelFn := context.WithTimeout(context.TODO(), 10*time.Second)
+	defer cancelFn()
+
+	if account == nil {
+		return errors.New("nil account")
+	}
+
+	if account.HolderName == "" {
+		return errors.New("no account HolderName to perform check with")
+	}
+
+	sdn, err := s.WatchmanClient.Search(ctx, account.HolderName, requestID)
+	if err != nil {
+		return fmt.Errorf("AccountOfacSearcher.StoreAccountOFACSearch: name search for account=%s: %v", account.AccountID, err)
+	}
+	err = s.Repo.saveAccountOFACSearch(account.AccountID, &client.OfacSearch{
+		EntityID:  sdn.EntityID,
+		SdnName:   sdn.SdnName,
+		SdnType:   sdn.SdnType,
+		Match:     sdn.Match,
+		CreatedAt: time.Now(),
+	})
+	if err != nil {
+		return fmt.Errorf("AccountOfacSearcher.StoreAccountOFACSearch: saveAccountOFACSearch account=%s: %v", account.AccountID, err)
+	}
+
+	return nil
+}
diff --git a/cmd/server/accounts/mock_repository.go b/cmd/server/accounts/mock_repository.go
index aebdab5..bf7550e 100644
--- a/cmd/server/accounts/mock_repository.go
+++ b/cmd/server/accounts/mock_repository.go
@@ -56,3 +56,11 @@ func (r *mockRepository) getEncryptedAccountNumber(customerID, accountID string)
 	}
 	return r.AccountNumber, nil
 }
+
+func (r *mockRepository) getLatestAccountOFACSearch(accountID string) (*client.OfacSearch, error) {
+	panic("implement me")
+}
+
+func (r *mockRepository) saveAccountOFACSearch(id string, result *client.OfacSearch) error {
+	return nil
+}
diff --git a/cmd/server/accounts/repository.go b/cmd/server/accounts/repository.go
index 46278d3..21e3220 100644
--- a/cmd/server/accounts/repository.go
+++ b/cmd/server/accounts/repository.go
@@ -27,6 +27,9 @@ type Repository interface {
 	updateAccountStatus(accountID string, status admin.AccountStatus) error
 
 	getEncryptedAccountNumber(customerID, accountID string) (string, error)
+
+	getLatestAccountOFACSearch(accountID string) (*client.OfacSearch, error)
+	saveAccountOFACSearch(id string, result *client.OfacSearch) error
 }
 
 func NewRepo(logger log.Logger, db *sql.DB) *sqlAccountRepository {
@@ -89,6 +92,7 @@ func (r *sqlAccountRepository) getCustomerAccounts(customerID string) ([]*client
 func (r *sqlAccountRepository) createCustomerAccount(customerID, userID string, req *createAccountRequest) (*client.Account, error) {
 	account := &client.Account{
 		AccountID:           base.ID(),
+		HolderName:          req.HolderName,
 		MaskedAccountNumber: req.maskedAccountNumber,
 		RoutingNumber:       req.RoutingNumber,
 		Status:              client.NONE,
@@ -160,3 +164,40 @@ func (r *sqlAccountRepository) getEncryptedAccountNumber(customerID, accountID s
 	}
 	return encrypted, nil
 }
+
+func (r *sqlAccountRepository) getLatestAccountOFACSearch(accountID string) (*client.OfacSearch, error) {
+	query := `select entity_id, sdn_name, sdn_type, percentage_match, created_at from account_ofac_searches where account_id = ? order by created_at desc limit 1;`
+	stmt, err := r.db.Prepare(query)
+	if err != nil {
+		return nil, fmt.Errorf("getLatestAccountOFACSearch: prepare: %v", err)
+	}
+	defer stmt.Close()
+
+	row := stmt.QueryRow(accountID)
+	var res client.OfacSearch
+	if err := row.Scan(&res.EntityID, &res.SdnType, &res.SdnType, &res.Match, &res.CreatedAt); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, nil // nothing found
+		}
+		return nil, fmt.Errorf("getLatestAccountOFACSearch: scan: %v", err)
+	}
+	return &res, nil
+}
+
+func (r *sqlAccountRepository) saveAccountOFACSearch(accountID string, result *client.OfacSearch) error {
+	query := `insert into account_ofac_searches (account_id, entity_id, sdn_name, sdn_type, percentage_match, created_at) values (?, ?, ?, ?, ?, ?);`
+	stmt, err := r.db.Prepare(query)
+	if err != nil {
+		return fmt.Errorf("saveAccountOFACSearch: prepare: %v", err)
+	}
+	defer stmt.Close()
+
+	if result.CreatedAt.IsZero() {
+		result.CreatedAt = time.Now()
+	}
+
+	if _, err := stmt.Exec(accountID, result.EntityID, result.SdnName, result.SdnType, result.Match, result.CreatedAt); err != nil {
+		return fmt.Errorf("saveAccountOFACSearch: exec: %v", err)
+	}
+	return nil
+}
diff --git a/cmd/server/accounts/router.go b/cmd/server/accounts/router.go
index 8e99e31..6b5bc6f 100644
--- a/cmd/server/accounts/router.go
+++ b/cmd/server/accounts/router.go
@@ -25,12 +25,14 @@ import (
 	"github.com/gorilla/mux"
 )
 
-func RegisterRoutes(logger log.Logger, r *mux.Router, accounts Repository, validations validator.Repository, fedClient fed.Client, keeper, transitKeeper *secrets.StringKeeper, validationStrategies map[validator.StrategyKey]validator.Strategy) {
+func RegisterRoutes(logger log.Logger, r *mux.Router, accounts Repository, validations validator.Repository, fedClient fed.Client, keeper, transitKeeper *secrets.StringKeeper, validationStrategies map[validator.StrategyKey]validator.Strategy, ofac *AccountOfacSearcher) {
 	r.Methods("GET").Path("/customers/{customerID}/accounts").HandlerFunc(getCustomerAccounts(logger, accounts, fedClient))
-	r.Methods("POST").Path("/customers/{customerID}/accounts").HandlerFunc(createCustomerAccount(logger, accounts, fedClient, keeper))
+	r.Methods("POST").Path("/customers/{customerID}/accounts").HandlerFunc(createCustomerAccount(logger, accounts, fedClient, keeper, ofac))
 	r.Methods("POST").Path("/customers/{customerID}/accounts/{accountID}/decrypt").HandlerFunc(decryptAccountNumber(logger, accounts, keeper, transitKeeper))
 	r.Methods("DELETE").Path("/customers/{customerID}/accounts/{accountID}").HandlerFunc(removeCustomerAccount(logger, accounts))
 
+	r.Methods("GET").Path("/customers/{customerID}/accounts/{accountID}/ofac").HandlerFunc(getAccountOfacSearch(logger, accounts))
+
 	r.Methods("POST").Path("/customers/{customerID}/accounts/{accountID}/validations").HandlerFunc(initAccountValidation(logger, accounts, validations, validationStrategies))
 	r.Methods("GET").Path("/customers/{customerID}/accounts/{accountID}/validations/{validationID}").HandlerFunc(getAccountValidation(logger, accounts, validations))
 	r.Methods("PUT").Path("/customers/{customerID}/accounts/{accountID}/validations/{validationID}").HandlerFunc(completeAccountValidation(logger, accounts, validations, keeper, validationStrategies))
@@ -111,10 +113,12 @@ func (req *createAccountRequest) disfigure(keeper *secrets.StringKeeper) error {
 	return nil
 }
 
-func createCustomerAccount(logger log.Logger, repo Repository, fedClient fed.Client, keeper *secrets.StringKeeper) http.HandlerFunc {
+func createCustomerAccount(logger log.Logger, repo Repository, fedClient fed.Client, keeper *secrets.StringKeeper, ofac *AccountOfacSearcher) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		w = route.Responder(logger, w, r)
 
+		requestID := moovhttp.GetRequestID(r)
+
 		var request createAccountRequest
 		if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
 			moovhttp.Problem(w, err)
@@ -145,6 +149,11 @@ func createCustomerAccount(logger log.Logger, repo Repository, fedClient fed.Cli
 			return
 		}
 
+		// Perform an OFAC search with the Customer information
+		if err := ofac.StoreAccountOFACSearch(account, requestID); err != nil {
+			logger.Log("accounts", fmt.Sprintf("error with OFAC search for account=%s: %v", account.AccountID, err), "requestID", requestID)
+		}
+
 		w.Header().Set("Content-Type", "application/json; charset=utf-8")
 		w.WriteHeader(http.StatusOK)
 		json.NewEncoder(w).Encode(account)
@@ -210,3 +219,24 @@ func removeCustomerAccount(logger log.Logger, repo Repository) http.HandlerFunc
 		w.WriteHeader(http.StatusOK)
 	}
 }
+
+func getAccountOfacSearch(logger log.Logger, repo Repository) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		w = route.Responder(logger, w, r)
+		w.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+		accountID := getAccountID(w, r)
+		if accountID == "" {
+			return
+		}
+
+		result, err := repo.getLatestAccountOFACSearch(accountID)
+		if err != nil {
+			moovhttp.Problem(w, err)
+			return
+		}
+
+		w.WriteHeader(http.StatusOK)
+		json.NewEncoder(w).Encode(result)
+	}
+}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 61fc953..c1043b5 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -161,11 +161,15 @@ func main() {
 		panic(err)
 	}
 
+	accountOfacSeacher := accounts.AccountOfacSearcher{
+		Repo: accountsRepo, WatchmanClient: watchmanClient,
+	}
+
 	// Setup business HTTP routes
 	router := mux.NewRouter()
 	moovhttp.AddCORSHandler(router)
 	addPingRoute(router)
-	accounts.RegisterRoutes(logger, router, accountsRepo, validationsRepo, fedClient, stringKeeper, transitStringKeeper, validationStrategies)
+	accounts.RegisterRoutes(logger, router, accountsRepo, validationsRepo, fedClient, stringKeeper, transitStringKeeper, validationStrategies, &accountOfacSeacher)
 	addCustomerRoutes(logger, router, customerRepo, customerSSNStorage, ofac)
 	addDisclaimerRoutes(logger, router, disclaimerRepo)
 	addDocumentRoutes(logger, router, documentRepo, getBucket(bucketName, cloudProvider, signer))
@@ -265,7 +269,7 @@ func setupValidationStrategies(logger log.Logger, adminServer *admin.Server) (ma
 		options := plaid.StrategyOptions{
 			ClientID:    os.Getenv("PLAID_CLIENT_ID"),
 			Secret:      os.Getenv("PLAID_SECRET"),
-			Environment: util.Or(os.Getenv("PLAID_ENVIRONMENT"), "sandbox"),
+			Environment: os.Getenv("PLAID_ENVIRONMENT"),
 			ClientName:  os.Getenv("PLAID_CLIENT_NAME"),
 		}
 
diff --git a/go.mod b/go.mod
index 1cbb547..b00ba33 100644
--- a/go.mod
+++ b/go.mod
@@ -32,6 +32,7 @@ require (
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/ory/dockertest/v3 v3.6.0
 	github.com/pierrec/lz4 v2.5.2+incompatible // indirect
+	github.com/pkg/errors v0.9.1
 	github.com/plaid/plaid-go v0.0.0-20200805000941-b566963d2892
 	github.com/prometheus/client_golang v1.7.1
 	github.com/prometheus/common v0.13.0 // indirect
diff --git a/internal/database/mysql.go b/internal/database/mysql.go
index ac34899..0161aa5 100644
--- a/internal/database/mysql.go
+++ b/internal/database/mysql.go
@@ -116,6 +116,10 @@ var (
 				INDEX idx_validations_validation_account_ids (validation_id, account_id)
 			);`,
 		),
+		execsql(
+			"create_account_ofac_searches",
+			`create table if not exists account_ofac_searches(account_ofac_search_id varchar(40) primary key, account_id varchar(40), entity_id varchar(40), sdn_name varchar(40), sdn_type integer, percentage_match double precision (5,2), created_at datetime);`,
+		),
 	)
 )
 
diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go
index 7d9183d..88ffc09 100644
--- a/internal/database/sqlite.go
+++ b/internal/database/sqlite.go
@@ -134,6 +134,10 @@ PRAGMA foreign_keys=on;`,
 				validations (validation_id, account_id);
 			`,
 		),
+		execsql(
+			"create_account_ofac_searches",
+			`create table if not exists account_ofac_searches(account_ofac_search_id varchar(40) primary key, account_id varchar(40), entity_id varchar(40), sdn_name varchar(40), sdn_type integer, percentage_match double precision (5,2), created_at datetime);`,
+		),
 	)
 )
 
diff --git a/pkg/client/README.md b/pkg/client/README.md
index 6315b8e..707f205 100644
--- a/pkg/client/README.md
+++ b/pkg/client/README.md
@@ -51,6 +51,7 @@ Class | Method | HTTP request | Description
 *CustomersApi* | [**GetCustomerDisclaimers**](docs/CustomersApi.md#getcustomerdisclaimers) | **Get** /customers/{customerID}/disclaimers | Get customer disclaimers
 *CustomersApi* | [**GetCustomerDocumentContents**](docs/CustomersApi.md#getcustomerdocumentcontents) | **Get** /customers/{customerID}/documents/{documentID} | Get customer document
 *CustomersApi* | [**GetCustomerDocuments**](docs/CustomersApi.md#getcustomerdocuments) | **Get** /customers/{customerID}/documents | Get customer documents
+*CustomersApi* | [**GetLatestAccountOFACSearch**](docs/CustomersApi.md#getlatestaccountofacsearch) | **Get** /customers/{customerID}/accounts/{accountID}/ofac | Get latest OFAC search
 *CustomersApi* | [**GetLatestOFACSearch**](docs/CustomersApi.md#getlatestofacsearch) | **Get** /customers/{customerID}/ofac | Get latest OFAC search
 *CustomersApi* | [**InitAccountValidation**](docs/CustomersApi.md#initaccountvalidation) | **Post** /customers/{customerID}/accounts/{accountID}/validations | Initiate Account Validation
 *CustomersApi* | [**Ping**](docs/CustomersApi.md#ping) | **Get** /ping | Ping Customers
diff --git a/pkg/client/api/openapi.yaml b/pkg/client/api/openapi.yaml
index 09f4539..431fad2 100644
--- a/pkg/client/api/openapi.yaml
+++ b/pkg/client/api/openapi.yaml
@@ -663,6 +663,65 @@ paths:
       summary: Get Account Validation
       tags:
       - Customers
+  /customers/{customerID}/accounts/{accountID}/ofac:
+    get:
+      description: Get the latest OFAC search for an account
+      operationId: getLatestAccountOFACSearch
+      parameters:
+      - description: Optional requestID allows application developer to trace requests
+          through the systems logs
+        example: rs4f9915
+        explode: false
+        in: header
+        name: X-Request-ID
+        required: false
+        schema:
+          type: string
+        style: simple
+      - description: Unique userID set by an auth proxy or client to identify and
+          isolate objects.
+        example: e3cdf999
+        explode: false
+        in: header
+        name: X-User-ID
+        required: false
+        schema:
+          type: string
+        style: simple
+      - description: customerID of the Customer
+        explode: false
+        in: path
+        name: customerID
+        required: true
+        schema:
+          example: e210a9d6
+          type: string
+        style: simple
+      - description: accountID of the Account to get latest OFAC search
+        explode: false
+        in: path
+        name: accountID
+        required: true
+        schema:
+          example: e210a9d6
+          type: string
+        style: simple
+      responses:
+        "200":
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/OFACSearch'
+          description: OFAC search did not block account
+        "400":
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: An error occurred when refreshing OFAC data, see error(s)
+      summary: Get latest OFAC search
+      tags:
+      - Customers
   /customers/{customerID}/accounts/{accountID}/validations:
     post:
       description: |
diff --git a/pkg/client/api_customers.go b/pkg/client/api_customers.go
index 6bfb7c1..3e899b7 100644
--- a/pkg/client/api_customers.go
+++ b/pkg/client/api_customers.go
@@ -1370,6 +1370,111 @@ func (a *CustomersApiService) GetCustomerDocuments(ctx _context.Context, custome
 	return localVarReturnValue, localVarHTTPResponse, nil
 }
 
+// GetLatestAccountOFACSearchOpts Optional parameters for the method 'GetLatestAccountOFACSearch'
+type GetLatestAccountOFACSearchOpts struct {
+	XRequestID optional.String
+	XUserID    optional.String
+}
+
+/*
+GetLatestAccountOFACSearch Get latest OFAC search
+Get the latest OFAC search for an account
+ * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ * @param customerID customerID of the Customer
+ * @param accountID accountID of the Account to get latest OFAC search
+ * @param optional nil or *GetLatestAccountOFACSearchOpts - Optional Parameters:
+ * @param "XRequestID" (optional.String) -  Optional requestID allows application developer to trace requests through the systems logs
+ * @param "XUserID" (optional.String) -  Unique userID set by an auth proxy or client to identify and isolate objects.
+@return OfacSearch
+*/
+func (a *CustomersApiService) GetLatestAccountOFACSearch(ctx _context.Context, customerID string, accountID string, localVarOptionals *GetLatestAccountOFACSearchOpts) (OfacSearch, *_nethttp.Response, error) {
+	var (
+		localVarHTTPMethod   = _nethttp.MethodGet
+		localVarPostBody     interface{}
+		localVarFormFileName string
+		localVarFileName     string
+		localVarFileBytes    []byte
+		localVarReturnValue  OfacSearch
+	)
+
+	// create path and map variables
+	localVarPath := a.client.cfg.BasePath + "/customers/{customerID}/accounts/{accountID}/ofac"
+	localVarPath = strings.Replace(localVarPath, "{"+"customerID"+"}", _neturl.QueryEscape(parameterToString(customerID, "")), -1)
+
+	localVarPath = strings.Replace(localVarPath, "{"+"accountID"+"}", _neturl.QueryEscape(parameterToString(accountID, "")), -1)
+
+	localVarHeaderParams := make(map[string]string)
+	localVarQueryParams := _neturl.Values{}
+	localVarFormParams := _neturl.Values{}
+
+	// to determine the Content-Type header
+	localVarHTTPContentTypes := []string{}
+
+	// set Content-Type header
+	localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+	if localVarHTTPContentType != "" {
+		localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+	}
+
+	// to determine the Accept header
+	localVarHTTPHeaderAccepts := []string{"application/json"}
+
+	// set Accept header
+	localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+	if localVarHTTPHeaderAccept != "" {
+		localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+	}
+	if localVarOptionals != nil && localVarOptionals.XRequestID.IsSet() {
+		localVarHeaderParams["X-Request-ID"] = parameterToString(localVarOptionals.XRequestID.Value(), "")
+	}
+	if localVarOptionals != nil && localVarOptionals.XUserID.IsSet() {
+		localVarHeaderParams["X-User-ID"] = parameterToString(localVarOptionals.XUserID.Value(), "")
+	}
+	r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
+	if err != nil {
+		return localVarReturnValue, nil, err
+	}
+
+	localVarHTTPResponse, err := a.client.callAPI(r)
+	if err != nil || localVarHTTPResponse == nil {
+		return localVarReturnValue, localVarHTTPResponse, err
+	}
+
+	localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body)
+	localVarHTTPResponse.Body.Close()
+	if err != nil {
+		return localVarReturnValue, localVarHTTPResponse, err
+	}
+
+	if localVarHTTPResponse.StatusCode >= 300 {
+		newErr := GenericOpenAPIError{
+			body:  localVarBody,
+			error: localVarHTTPResponse.Status,
+		}
+		if localVarHTTPResponse.StatusCode == 400 {
+			var v Error
+			err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+			if err != nil {
+				newErr.error = err.Error()
+				return localVarReturnValue, localVarHTTPResponse, newErr
+			}
+			newErr.model = v
+		}
+		return localVarReturnValue, localVarHTTPResponse, newErr
+	}
+
+	err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+	if err != nil {
+		newErr := GenericOpenAPIError{
+			body:  localVarBody,
+			error: err.Error(),
+		}
+		return localVarReturnValue, localVarHTTPResponse, newErr
+	}
+
+	return localVarReturnValue, localVarHTTPResponse, nil
+}
+
 // GetLatestOFACSearchOpts Optional parameters for the method 'GetLatestOFACSearch'
 type GetLatestOFACSearchOpts struct {
 	XRequestID optional.String
diff --git a/pkg/client/docs/CustomersApi.md b/pkg/client/docs/CustomersApi.md
index e45f820..5026134 100644
--- a/pkg/client/docs/CustomersApi.md
+++ b/pkg/client/docs/CustomersApi.md
@@ -17,6 +17,7 @@ Method | HTTP request | Description
 [**GetCustomerDisclaimers**](CustomersApi.md#GetCustomerDisclaimers) | **Get** /customers/{customerID}/disclaimers | Get customer disclaimers
 [**GetCustomerDocumentContents**](CustomersApi.md#GetCustomerDocumentContents) | **Get** /customers/{customerID}/documents/{documentID} | Get customer document
 [**GetCustomerDocuments**](CustomersApi.md#GetCustomerDocuments) | **Get** /customers/{customerID}/documents | Get customer documents
+[**GetLatestAccountOFACSearch**](CustomersApi.md#GetLatestAccountOFACSearch) | **Get** /customers/{customerID}/accounts/{accountID}/ofac | Get latest OFAC search
 [**GetLatestOFACSearch**](CustomersApi.md#GetLatestOFACSearch) | **Get** /customers/{customerID}/ofac | Get latest OFAC search
 [**InitAccountValidation**](CustomersApi.md#InitAccountValidation) | **Post** /customers/{customerID}/accounts/{accountID}/validations | Initiate Account Validation
 [**Ping**](CustomersApi.md#Ping) | **Get** /ping | Ping Customers
@@ -644,6 +645,54 @@ No authorization required
 [[Back to README]](../README.md)
 
 
+## GetLatestAccountOFACSearch
+
+> OfacSearch GetLatestAccountOFACSearch(ctx, customerID, accountID, optional)
+
+Get latest OFAC search
+
+Get the latest OFAC search for an account
+
+### Required Parameters
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+**customerID** | **string**| customerID of the Customer | 
+**accountID** | **string**| accountID of the Account to get latest OFAC search | 
+ **optional** | ***GetLatestAccountOFACSearchOpts** | optional parameters | nil if no parameters
+
+### Optional Parameters
+
+Optional parameters are passed through a pointer to a GetLatestAccountOFACSearchOpts struct
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+
+
+ **xRequestID** | **optional.String**| Optional requestID allows application developer to trace requests through the systems logs | 
+ **xUserID** | **optional.String**| Unique userID set by an auth proxy or client to identify and isolate objects. | 
+
+### Return type
+
+[**OfacSearch**](OFACSearch.md)
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+- **Content-Type**: Not defined
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
+
 ## GetLatestOFACSearch
 
 > OfacSearch GetLatestOFACSearch(ctx, customerID, optional)


Test patch:
diff --git a/cmd/server/accounts/accounts_ofac_test.go b/cmd/server/accounts/accounts_ofac_test.go
new file mode 100644
index 0000000..397b779
--- /dev/null
+++ b/cmd/server/accounts/accounts_ofac_test.go
@@ -0,0 +1,48 @@
+// Copyright 2020 The Moov Authors
+// Use of this source code is governed by an Apache License
+// license that can be found in the LICENSE file.
+
+package accounts
+
+import (
+	"github.com/moov-io/customers/pkg/client"
+	"testing"
+)
+
+func TestStoreAccountOFACSearchSuccess(t *testing.T) {
+	mockRepository := mockRepository{}
+	accountOfacSearcher := createTestOFACSearcher(&mockRepository, nil)
+	requestID := "someRequestId"
+	account := &client.Account{
+		AccountID:  "123",
+		HolderName: "John Doe",
+	}
+	err := accountOfacSearcher.StoreAccountOFACSearch(account, requestID)
+	if err != nil {
+		t.Errorf("got err: %v", err)
+	}
+}
+
+func TestStoreAccountOFACSearchErrorNilAccountHolderName(t *testing.T) {
+	mockRepository := mockRepository{}
+	accountOfacSearcher := createTestOFACSearcher(&mockRepository, nil)
+	requestID := "someRequestId"
+	account := &client.Account{
+		AccountID:  "123",
+		HolderName: "",
+	}
+	err := accountOfacSearcher.StoreAccountOFACSearch(account, requestID)
+	if err == nil {
+		t.Errorf("got resp: %v", err)
+	}
+}
+
+func TestStoreAccountOFACSearchErrorNilAccount(t *testing.T) {
+	mockRepository := mockRepository{}
+	accountOfacSearcher := createTestOFACSearcher(&mockRepository, nil)
+	requestID := "someRequestId"
+	err := accountOfacSearcher.StoreAccountOFACSearch(nil, requestID)
+	if err == nil {
+		t.Errorf("got resp: %v", err)
+	}
+}
diff --git a/cmd/server/accounts/router_test.go b/cmd/server/accounts/router_test.go
index 1a9428a..5eb25f0 100644
--- a/cmd/server/accounts/router_test.go
+++ b/cmd/server/accounts/router_test.go
@@ -6,8 +6,10 @@ package accounts
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
+	watchman "github.com/moov-io/watchman/client"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -25,6 +27,40 @@ import (
 	"github.com/gorilla/mux"
 )
 
+type testWatchmanClient struct {
+	// error to be returned instead of field from above
+	err error
+}
+
+func (c *testWatchmanClient) Ping() error {
+	return c.err
+}
+
+func (c *testWatchmanClient) Search(_ context.Context, name string, _ string) (*watchman.OfacSdn, error) {
+	if c.err != nil {
+		return nil, c.err
+	}
+	return &watchman.OfacSdn{
+		EntityID: "123",
+		SdnName:  "dummy_sdn",
+		SdnType:  "dummy_sdn_type",
+		Programs: nil,
+		Title:    "dummy_title",
+		Remarks:  "dummy_remarks",
+		Match:    10.2,
+	}, nil
+}
+
+func createTestOFACSearcher(repo Repository, watchmanClient WatchmanClient) *AccountOfacSearcher {
+	if repo == nil {
+		repo = &testAccountRepository{}
+	}
+	if watchmanClient == nil {
+		watchmanClient = &testWatchmanClient{}
+	}
+	return &AccountOfacSearcher{Repo: repo, WatchmanClient: watchmanClient}
+}
+
 func TestAccountRoutes(t *testing.T) {
 	customerID := base.ID()
 
@@ -61,6 +97,30 @@ func TestAccountRoutes(t *testing.T) {
 	}
 }
 
+func TestCreateAccountAndCheckAccountOfacSearch(t *testing.T) {
+	customerID := base.ID()
+
+	handler := setupRouter(t)
+
+	// first read, expect no accounts
+	accounts := httpReadAccounts(t, handler, customerID)
+	if len(accounts) != 0 {
+		t.Errorf("got accounts: %v", accounts)
+	}
+
+	// create an account
+	account := httpCreateAccount(t, handler, customerID)
+	if account.MaskedAccountNumber != "*8749" {
+		t.Errorf("masked account number: %q", account.MaskedAccountNumber)
+	}
+
+	// Check for ofac search after creating
+	accountOfacSearch := httpReadAccountOfacSearch(t, handler, customerID, account.AccountID)
+	if accountOfacSearch == nil {
+		t.Errorf("got account ofac search: %v", accountOfacSearch)
+	}
+}
+
 func TestAccountCreationRequest(t *testing.T) {
 	req := &createAccountRequest{}
 	if err := req.validate(); err == nil {
@@ -121,7 +181,7 @@ func setupRouter(t *testing.T) *mux.Router {
 		{Strategy: "test", Vendor: "moov"}: testvalidator.NewStrategy(),
 	}
 
-	RegisterRoutes(log.NewNopLogger(), handler, accounts, validations, testFedClient, keeper, keeper, validationStrategies)
+	RegisterRoutes(log.NewNopLogger(), handler, accounts, validations, testFedClient, keeper, keeper, validationStrategies, createTestOFACSearcher(accounts, nil))
 
 	return handler
 }
@@ -269,3 +329,20 @@ func httpDecryptAccountNumber(t *testing.T, handler *mux.Router, customerID, acc
 	//   {"accountNumber":"XueflKMjfidC2Ifommst9iSK+xF/sn2x+pK/K"}
 	require.Contains(t, w.Body.String(), `"accountNumber":`)
 }
+
+func httpReadAccountOfacSearch(t *testing.T, handler *mux.Router, customerID, accountID string) *client.OfacSearch {
+	w := httptest.NewRecorder()
+	req := httptest.NewRequest("GET", fmt.Sprintf("/customers/%s/accounts/%s/ofac", customerID, accountID), nil)
+	handler.ServeHTTP(w, req)
+	w.Flush()
+
+	if w.Code != http.StatusOK {
+		t.Errorf("bogus HTTP status %d: %v", w.Code, w.Body.String())
+	}
+
+	var ofacSearch client.OfacSearch
+	if err := json.NewDecoder(w.Body).Decode(&ofacSearch); err != nil {
+		t.Fatal(err)
+	}
+	return &ofacSearch
+}