Repository: moov-io/customers
Pull request #53 (2019-11-06 00:49:30)

cmd/server: endpoint to grab the latest OFAC search
Paygate (and others) often want to inspect when the last OFAC search was performed for a given Customer. 

Issue: https://github.com/moov-io/paygate/issues/326 


Patch:
diff --git a/client/README.md b/client/README.md
index 160dba7..c81d913 100644
--- a/client/README.md
+++ b/client/README.md
@@ -40,6 +40,7 @@ Class | Method | HTTP request | Description
 *CustomersApi* | [**GetCustomerDisclaimers**](docs/CustomersApi.md#getcustomerdisclaimers) | **Get** /customers/{customerID}/disclaimers | Get active disclaimers for the given customer
 *CustomersApi* | [**GetCustomerDocumentContents**](docs/CustomersApi.md#getcustomerdocumentcontents) | **Get** /customers/{customerID}/documents/{documentID} | Retrieve the referenced document
 *CustomersApi* | [**GetCustomerDocuments**](docs/CustomersApi.md#getcustomerdocuments) | **Get** /customers/{customerID}/documents | Get documents for a customer
+*CustomersApi* | [**GetLatestOFACSearch**](docs/CustomersApi.md#getlatestofacsearch) | **Get** /customers/{customerID}/ofac | Get the latest OFAC search for a customer
 *CustomersApi* | [**Ping**](docs/CustomersApi.md#ping) | **Get** /ping | Ping the Customers service to check if running
 *CustomersApi* | [**RefreshOFACSearch**](docs/CustomersApi.md#refreshofacsearch) | **Put** /customers/{customerID}/refresh/ofac | Refresh OFAC search for a given Customer
 *CustomersApi* | [**ReplaceCustomerMetadata**](docs/CustomersApi.md#replacecustomermetadata) | **Put** /customers/{customerID}/metadata | Replace the metadata object for a customer. Metadata is a map of unique keys associated to values to act as foreign key relationships or arbitrary data associated to a Customer.
@@ -58,6 +59,7 @@ Class | Method | HTTP request | Description
  - [Disclaimer](docs/Disclaimer.md)
  - [Document](docs/Document.md)
  - [Error](docs/Error.md)
+ - [OfacSearch](docs/OfacSearch.md)
  - [Phone](docs/Phone.md)
  - [UpdateCustomerStatus](docs/UpdateCustomerStatus.md)
 
diff --git a/client/api/openapi.yaml b/client/api/openapi.yaml
index 3e0a551..32f0282 100644
--- a/client/api/openapi.yaml
+++ b/client/api/openapi.yaml
@@ -549,6 +549,54 @@ paths:
       summary: Retrieve the referenced document
       tags:
       - Customers
+  /customers/{customerID}/ofac:
+    get:
+      operationId: getLatestOFACSearch
+      parameters:
+      - description: Optional Request ID 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: Moov User ID
+        example: e3cdf999
+        explode: false
+        in: header
+        name: X-User-ID
+        required: false
+        schema:
+          type: string
+        style: simple
+      - description: Customer ID
+        explode: false
+        in: path
+        name: customerID
+        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 customer
+        400:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: An error occurred when searching OFAC data
+      summary: Get the latest OFAC search for a customer
+      tags:
+      - Customers
   /customers/{customerID}/refresh/ofac:
     put:
       operationId: refreshOFACSearch
@@ -1050,3 +1098,28 @@ components:
       items:
         $ref: '#/components/schemas/Document'
       type: array
+    OFACSearch:
+      example:
+        sdnType: Individual
+        match: 0.91
+        entityId: "2141"
+        sdnName: John Smith
+      properties:
+        entityId:
+          description: SDN EntityID of the Entity
+          example: "2141"
+          type: string
+        sdnName:
+          description: Name of the SDN entity
+          example: John Smith
+          type: string
+        sdnType:
+          description: SDN entity type
+          example: Individual
+          type: string
+        match:
+          description: Percentage of similarity between the Customer name and this
+            OFAC entity
+          example: 0.91
+          type: number
+      type: object
diff --git a/client/api_customers.go b/client/api_customers.go
index c206f95..1d1955d 100644
--- a/client/api_customers.go
+++ b/client/api_customers.go
@@ -784,6 +784,117 @@ func (a *CustomersApiService) GetCustomerDocuments(ctx _context.Context, custome
 	return localVarReturnValue, localVarHTTPResponse, nil
 }
 
+// GetLatestOFACSearchOpts Optional parameters for the method 'GetLatestOFACSearch'
+type GetLatestOFACSearchOpts struct {
+	XRequestID optional.String
+	XUserID    optional.String
+}
+
+/*
+GetLatestOFACSearch Get the latest OFAC search for a customer
+ * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ * @param customerID Customer ID
+ * @param optional nil or *GetLatestOFACSearchOpts - Optional Parameters:
+ * @param "XRequestID" (optional.String) -  Optional Request ID allows application developer to trace requests through the systems logs
+ * @param "XUserID" (optional.String) -  Moov User ID
+@return OfacSearch
+*/
+func (a *CustomersApiService) GetLatestOFACSearch(ctx _context.Context, customerID string, localVarOptionals *GetLatestOFACSearchOpts) (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}/ofac"
+	localVarPath = strings.Replace(localVarPath, "{"+"customerID"+"}", _neturl.QueryEscape(fmt.Sprintf("%v", customerID)), -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 == 200 {
+			var v OfacSearch
+			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
+		}
+		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
+}
+
 /*
 Ping Ping the Customers service to check if running
  * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
diff --git a/client/docs/CustomersApi.md b/client/docs/CustomersApi.md
index 03f8b31..2a89992 100644
--- a/client/docs/CustomersApi.md
+++ b/client/docs/CustomersApi.md
@@ -11,6 +11,7 @@ Method | HTTP request | Description
 [**GetCustomerDisclaimers**](CustomersApi.md#GetCustomerDisclaimers) | **Get** /customers/{customerID}/disclaimers | Get active disclaimers for the given customer
 [**GetCustomerDocumentContents**](CustomersApi.md#GetCustomerDocumentContents) | **Get** /customers/{customerID}/documents/{documentID} | Retrieve the referenced document
 [**GetCustomerDocuments**](CustomersApi.md#GetCustomerDocuments) | **Get** /customers/{customerID}/documents | Get documents for a customer
+[**GetLatestOFACSearch**](CustomersApi.md#GetLatestOFACSearch) | **Get** /customers/{customerID}/ofac | Get the latest OFAC search for a customer
 [**Ping**](CustomersApi.md#Ping) | **Get** /ping | Ping the Customers service to check if running
 [**RefreshOFACSearch**](CustomersApi.md#RefreshOFACSearch) | **Put** /customers/{customerID}/refresh/ofac | Refresh OFAC search for a given Customer
 [**ReplaceCustomerMetadata**](CustomersApi.md#ReplaceCustomerMetadata) | **Put** /customers/{customerID}/metadata | Replace the metadata object for a customer. Metadata is a map of unique keys associated to values to act as foreign key relationships or arbitrary data associated to a Customer.
@@ -333,6 +334,50 @@ No authorization required
 [[Back to README]](../README.md)
 
 
+## GetLatestOFACSearch
+
+> OfacSearch GetLatestOFACSearch(ctx, customerID, optional)
+
+Get the latest OFAC search for a customer
+
+### Required Parameters
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+**customerID** | **string**| Customer ID | 
+ **optional** | ***GetLatestOFACSearchOpts** | optional parameters | nil if no parameters
+
+### Optional Parameters
+
+Optional parameters are passed through a pointer to a GetLatestOFACSearchOpts struct
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+
+ **xRequestID** | **optional.String**| Optional Request ID allows application developer to trace requests through the systems logs | 
+ **xUserID** | **optional.String**| Moov User ID | 
+
+### 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)
+
+
 ## Ping
 
 > Ping(ctx, )
diff --git a/client/docs/OfacSearch.md b/client/docs/OfacSearch.md
new file mode 100644
index 0000000..a041606
--- /dev/null
+++ b/client/docs/OfacSearch.md
@@ -0,0 +1,14 @@
+# OfacSearch
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**EntityId** | **string** | SDN EntityID of the Entity | [optional] 
+**SdnName** | **string** | Name of the SDN entity | [optional] 
+**SdnType** | **string** | SDN entity type | [optional] 
+**Match** | **float32** | Percentage of similarity between the Customer name and this OFAC entity | [optional] 
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/client/model_ofac_search.go b/client/model_ofac_search.go
new file mode 100644
index 0000000..b5aaed5
--- /dev/null
+++ b/client/model_ofac_search.go
@@ -0,0 +1,22 @@
+/*
+ * Customers API
+ *
+ * Customers ...
+ *
+ * API version: v1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+// OfacSearch struct for OfacSearch
+type OfacSearch struct {
+	// SDN EntityID of the Entity
+	EntityId string `json:"entityId,omitempty"`
+	// Name of the SDN entity
+	SdnName string `json:"sdnName,omitempty"`
+	// SDN entity type
+	SdnType string `json:"sdnType,omitempty"`
+	// Percentage of similarity between the Customer name and this OFAC entity
+	Match float32 `json:"match,omitempty"`
+}
diff --git a/cmd/server/approval.go b/cmd/server/approval.go
index 77db359..e3ecd34 100644
--- a/cmd/server/approval.go
+++ b/cmd/server/approval.go
@@ -86,8 +86,8 @@ func validCustomerStatusTransition(existing *client.Customer, ssn *SSN, futureSt
 				return fmt.Errorf("validCustomerStatusTransition: inner lookup searchResult=%#v: %v", searchResult, err)
 			}
 		}
-		if searchResult.match > ofacMatchThreshold {
-			return fmt.Errorf("validCustomerStatusTransition: customer=%s has positive OFAC match (%.2f) with SDN=%s", existing.ID, searchResult.match, searchResult.entityId)
+		if searchResult.Match > ofacMatchThreshold {
+			return fmt.Errorf("validCustomerStatusTransition: customer=%s has positive OFAC match (%.2f) with SDN=%s", existing.ID, searchResult.Match, searchResult.EntityId)
 		}
 		return nil
 	case customers.CIP: // TODO(adam): need to impl lookup
diff --git a/cmd/server/approval_ofac.go b/cmd/server/approval_ofac.go
index 4545b86..7a21487 100644
--- a/cmd/server/approval_ofac.go
+++ b/cmd/server/approval_ofac.go
@@ -22,10 +22,10 @@ import (
 )
 
 type ofacSearchResult struct {
-	entityId string
-	sdnName  string
-	sdnType  string
-	match    float32
+	EntityId string  `json:"entityId"`
+	SDNName  string  `json:"sdnName"`
+	SDNType  string  `json:"sdnType"`
+	Match    float32 `json:"match"`
 }
 
 type ofacSearcher struct {
@@ -58,17 +58,17 @@ func (s *ofacSearcher) storeCustomerOFACSearch(cust *client.Customer, requestID
 	switch {
 	case nickSDN != nil && nickSDN.Match > sdn.Match:
 		err = s.repo.saveCustomerOFACSearch(cust.ID, ofacSearchResult{
-			entityId: nickSDN.EntityID,
-			sdnName:  nickSDN.SdnName,
-			sdnType:  nickSDN.SdnType,
-			match:    nickSDN.Match,
+			EntityId: nickSDN.EntityID,
+			SDNName:  nickSDN.SdnName,
+			SDNType:  nickSDN.SdnType,
+			Match:    nickSDN.Match,
 		})
 	case sdn != nil:
 		err = s.repo.saveCustomerOFACSearch(cust.ID, ofacSearchResult{
-			entityId: sdn.EntityID,
-			sdnName:  sdn.SdnName,
-			sdnType:  sdn.SdnType,
-			match:    sdn.Match,
+			EntityId: sdn.EntityID,
+			SDNName:  sdn.SdnName,
+			SDNType:  sdn.SdnType,
+			Match:    sdn.Match,
 		})
 	}
 	if err != nil {
@@ -78,9 +78,31 @@ func (s *ofacSearcher) storeCustomerOFACSearch(cust *client.Customer, requestID
 }
 
 func addOFACRoutes(logger log.Logger, r *mux.Router, repo customerRepository, ofac *ofacSearcher) {
+	r.Methods("GET").Path("/customers/{customerID}/ofac").HandlerFunc(getLatestCustomerOFACSearch(logger, repo))
 	r.Methods("PUT").Path("/customers/{customerID}/refresh/ofac").HandlerFunc(refreshOFACSearch(logger, repo, ofac))
 }
 
+func getLatestCustomerOFACSearch(logger log.Logger, repo customerRepository) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		w = wrapResponseWriter(logger, w, r)
+		w.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+		customerID := getCustomerID(w, r)
+		if customerID == "" {
+			return
+		}
+
+		result, err := repo.getLatestCustomerOFACSearch(customerID)
+		if err != nil {
+			moovhttp.Problem(w, err)
+			return
+		}
+
+		w.WriteHeader(http.StatusOK)
+		json.NewEncoder(w).Encode(result)
+	}
+}
+
 func refreshOFACSearch(logger log.Logger, repo customerRepository, ofac *ofacSearcher) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		w = wrapResponseWriter(logger, w, r)
@@ -109,8 +131,8 @@ func refreshOFACSearch(logger log.Logger, repo customerRepository, ofac *ofacSea
 			moovhttp.Problem(w, err)
 			return
 		}
-		if result.match > ofacMatchThreshold {
-			err = fmt.Errorf("customer=%s matched against OFAC entity=%s with a score of %.2f - rejecting customer", cust.ID, result.entityId, result.match)
+		if result.Match > ofacMatchThreshold {
+			err = fmt.Errorf("customer=%s matched against OFAC entity=%s with a score of %.2f - rejecting customer", cust.ID, result.EntityId, result.Match)
 			logger.Log("ofac", err.Error(), "requestID", requestID, "userID", userID)
 
 			cust.Status = customers.Rejected.String()
diff --git a/cmd/server/customers.go b/cmd/server/customers.go
index a134497..be249f4 100644
--- a/cmd/server/customers.go
+++ b/cmd/server/customers.go
@@ -591,7 +591,7 @@ func (r *sqlCustomerRepository) getLatestCustomerOFACSearch(customerID string) (
 
 	row := stmt.QueryRow(customerID)
 	var res ofacSearchResult
-	if err := row.Scan(&res.entityId, &res.sdnName, &res.sdnType, &res.match); err != nil {
+	if err := row.Scan(&res.EntityId, &res.SDNName, &res.SDNType, &res.Match); err != nil {
 		if err == sql.ErrNoRows {
 			return nil, nil // nothing found
 		}
@@ -608,7 +608,7 @@ func (r *sqlCustomerRepository) saveCustomerOFACSearch(customerID string, result
 	}
 	defer stmt.Close()
 
-	if _, err := stmt.Exec(customerID, result.entityId, result.sdnName, result.sdnType, result.match, time.Now()); err != nil {
+	if _, err := stmt.Exec(customerID, result.EntityId, result.SDNName, result.SDNType, result.Match, time.Now()); err != nil {
 		return fmt.Errorf("saveCustomerOFACSearch: exec: %v", err)
 	}
 	return nil
diff --git a/openapi.yaml b/openapi.yaml
index 0424166..349fb4e 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -445,6 +445,45 @@ paths:
               schema:
                 type: string
                 format: binary
+  /customers/{customerID}/ofac:
+    get:
+      tags: [Customers]
+      summary: Get the latest OFAC search for a customer
+      operationId: getLatestOFACSearch
+      parameters:
+        - name: X-Request-ID
+          in: header
+          description: Optional Request ID allows application developer to trace requests through the systems logs
+          example: rs4f9915
+          schema:
+            type: string
+        - name: X-User-ID
+          in: header
+          description: Moov User ID
+          example: e3cdf999
+          schema:
+            type: string
+        - name: customerID
+          in: path
+          description: Customer ID
+          required: true
+          schema:
+            type: string
+            example: e210a9d6
+      responses:
+        '200':
+          description: OFAC search did not block customer
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/OFACSearch'
+        '400':
+          description: An error occurred when searching OFAC data
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
   /customers/{customerID}/refresh/ofac:
     put:
       tags: [Customers]
@@ -818,3 +857,22 @@ components:
       type: array
       items:
         $ref: '#/components/schemas/Document'
+    OFACSearch:
+      type: object
+      properties:
+        entityId:
+          type: string
+          description: SDN EntityID of the Entity
+          example: "2141"
+        sdnName:
+          type: string
+          description: Name of the SDN entity
+          example: John Smith
+        sdnType:
+          type: string
+          description: SDN entity type
+          example: Individual
+        match:
+          type: number
+          example: 0.91
+          description: Percentage of similarity between the Customer name and this OFAC entity


Test patch:
diff --git a/cmd/server/approval_ofac_test.go b/cmd/server/approval_ofac_test.go
index d5fd7de..98ca476 100644
--- a/cmd/server/approval_ofac_test.go
+++ b/cmd/server/approval_ofac_test.go
@@ -60,7 +60,7 @@ func TestOFACSearcher__storeCustomerOFACSearch(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if res.entityId != "1241421" {
+	if res.EntityId != "1241421" {
 		t.Errorf("ofacSearchResult: %#v", res)
 	}
 
@@ -71,6 +71,45 @@ func TestOFACSearcher__storeCustomerOFACSearch(t *testing.T) {
 	}
 }
 
+func TestOFACApproval__getLatest(t *testing.T) {
+	logger := log.NewNopLogger()
+	router := mux.NewRouter()
+
+	customerID := base.ID()
+
+	repo := &testCustomerRepository{
+		customer: &client.Customer{
+			ID: customerID,
+		},
+		savedOFACSearchResult: &ofacSearchResult{
+			EntityId: "142",
+			Match:    1.0,
+		},
+	}
+
+	addOFACRoutes(logger, router, repo, nil)
+
+	w := httptest.NewRecorder()
+	req := httptest.NewRequest("GET", fmt.Sprintf("/customers/%s/ofac", customerID), nil)
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	if w.Code != http.StatusOK {
+		t.Errorf("bogus HTTP status: %d", w.Code)
+	}
+
+	// error case
+	repo.err = errors.New("bad error")
+
+	w = httptest.NewRecorder()
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	if w.Code != http.StatusBadRequest {
+		t.Errorf("bogus HTTP status: %d", w.Code)
+	}
+}
+
 func TestOFACApproval__refresh(t *testing.T) {
 	logger := log.NewNopLogger()
 	router := mux.NewRouter()
@@ -82,8 +121,8 @@ func TestOFACApproval__refresh(t *testing.T) {
 			ID: customerID,
 		},
 		savedOFACSearchResult: &ofacSearchResult{
-			entityId: "142",
-			match:    1.0,
+			EntityId: "142",
+			Match:    1.0,
 		},
 	}
 	testOFACClient := &testOFACClient{}
@@ -103,7 +142,7 @@ func TestOFACApproval__refresh(t *testing.T) {
 		t.Errorf("bogus HTTP status: %d", w.Code)
 	}
 
-	repo.savedOFACSearchResult.match = 0.90 // match isn't high enough to block
+	repo.savedOFACSearchResult.Match = 0.90 // match isn't high enough to block
 
 	w = httptest.NewRecorder()
 	router.ServeHTTP(w, req)
@@ -125,8 +164,8 @@ func TestOFACApproval__refreshErr(t *testing.T) {
 			ID: customerID,
 		},
 		savedOFACSearchResult: &ofacSearchResult{
-			entityId: "142",
-			match:    0.88,
+			EntityId: "142",
+			Match:    0.88,
 		},
 	}
 	testOFACClient := &testOFACClient{err: errors.New("bad error")}
diff --git a/cmd/server/approval_test.go b/cmd/server/approval_test.go
index 2f9e060..86a51bd 100644
--- a/cmd/server/approval_test.go
+++ b/cmd/server/approval_test.go
@@ -227,15 +227,15 @@ func TestCustomers__validCustomerStatusTransitionOFAC(t *testing.T) {
 	ssn := &SSN{customerID: cust.ID, encrypted: []byte("secret")}
 
 	repo.ofacSearchResult = &ofacSearchResult{
-		sdnName: "Jane Doe",
-		match:   0.10,
+		SDNName: "Jane Doe",
+		Match:   0.10,
 	}
 	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
+	repo.ofacSearchResult.Match = 0.99
 	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)
@@ -252,7 +252,7 @@ func TestCustomers__validCustomerStatusTransitionOFAC(t *testing.T) {
 	if err := validCustomerStatusTransition(cust, ssn, customers.OFAC, repo, searcher, "requestID"); err != nil {
 		t.Errorf("unexpected error: %v", err)
 	}
-	if repo.savedOFACSearchResult.entityId != "12124" {
+	if repo.savedOFACSearchResult.EntityId != "12124" {
 		t.Errorf("unexpected saved OFAC result: %#v", repo.savedOFACSearchResult)
 	}
 }
diff --git a/cmd/server/customers_test.go b/cmd/server/customers_test.go
index 81957d5..6b05927 100644
--- a/cmd/server/customers_test.go
+++ b/cmd/server/customers_test.go
@@ -648,26 +648,26 @@ func TestCustomerRepository__OFAC(t *testing.T) {
 	}
 
 	// save a record and read it back
-	if err := repo.saveCustomerOFACSearch(customerID, ofacSearchResult{entityId: "14141"}); err != nil {
+	if err := repo.saveCustomerOFACSearch(customerID, ofacSearchResult{EntityId: "14141"}); err != nil {
 		t.Fatal(err)
 	}
 	res, err = repo.getLatestCustomerOFACSearch(customerID)
 	if err != nil {
 		t.Fatal(err)
 	}
-	if res == nil || res.entityId != "14141" {
+	if res == nil || res.EntityId != "14141" {
 		t.Errorf("ofacSearchResult: %#v", res)
 	}
 
 	// save another and get it back
-	if err := repo.saveCustomerOFACSearch(customerID, ofacSearchResult{entityId: "777121"}); err != nil {
+	if err := repo.saveCustomerOFACSearch(customerID, ofacSearchResult{EntityId: "777121"}); err != nil {
 		t.Fatal(err)
 	}
 	res, err = repo.getLatestCustomerOFACSearch(customerID)
 	if err != nil {
 		t.Fatal(err)
 	}
-	if res == nil || res.entityId != "777121" {
+	if res == nil || res.EntityId != "777121" {
 		t.Errorf("ofacSearchResult: %#v", res)
 	}
 }