Repository: moov-io/customers
Pull request #168 (2020-09-18 22:28:33)

configuration: add routes and database persistence 
Fixes: https://github.com/moov-io/customers/issues/167 


Patch:
diff --git a/api/client.yaml b/api/client.yaml
index 3bf0f6a..94f14c3 100644
--- a/api/client.yaml
+++ b/api/client.yaml
@@ -19,6 +19,8 @@ servers:
     description: Local development
 
 tags:
+  - name: Configuration
+    description: Endpoints for retrieving and updating configuration for a namespace.
   - name: Customers
     description: Endpoints for searching, creating, and verifying individuals according to US government criteria for money movement within the country.
 
@@ -32,6 +34,58 @@ paths:
       responses:
         '200':
           description: Service is running properly
+
+  /configuration/customers:
+    get:
+      tags: [Configuration]
+      summary: Get Configuration
+      description: Retrieve current configuration for the provided namespace.
+      operationId: getConfiguration
+      parameters:
+        - name: X-Namespace
+          in: header
+          description: Value used to separate and identify models
+          example: org342
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Configuration was successfully updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/NamespaceConfiguration'
+    put:
+      tags: [Configuration]
+      summary: Update Configuration
+      description: Update the configuration for the provided namespace.
+      operationId: updateConfiguration
+      parameters:
+        - name: X-Namespace
+          in: header
+          description: Value used to separate and identify models
+          example: org342
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/NamespaceConfiguration'
+      responses:
+        '200':
+          description: Configuration was successfully updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/NamespaceConfiguration'
+        '400':
+          description: Configuration was not updated, see error(s)
+          content:
+            application/json:
+              schema:
+                $ref: 'https://raw.githubusercontent.com/moov-io/api/master/openapi-common.yaml#/components/schemas/Error'
   /customers:
     get:
       tags: [Customers]
@@ -1142,6 +1196,19 @@ paths:
 
 components:
   schemas:
+    NamespaceConfiguration:
+      properties:
+        legalEntity:
+          type: string
+          example: 5c20933f
+          description: customerID of legal entity for this Namespace
+        primaryAccount:
+          type: string
+          example: f6eddffd
+          description: accountID of funding/source account for this Namespace
+      required:
+        - legalEntity
+        - primaryAccount
     CreateCustomer:
       type: object
       properties:
diff --git a/cmd/server/main.go b/cmd/server/main.go
index d322013..dce4a0e 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -29,6 +29,7 @@ import (
 	"github.com/moov-io/customers/cmd/server/paygate"
 	"github.com/moov-io/customers/internal/database"
 	"github.com/moov-io/customers/internal/util"
+	"github.com/moov-io/customers/pkg/configuration"
 	"github.com/moov-io/customers/pkg/secrets"
 
 	"github.com/go-kit/kit/log"
@@ -174,6 +175,10 @@ func main() {
 	addDocumentRoutes(logger, router, documentRepo, getBucket(bucketName, cloudProvider, signer))
 	addOFACRoutes(logger, router, customerRepo, ofac)
 
+	// Add Configuration routes
+	configRepo := configuration.NewRepository(db)
+	configuration.RegisterRoutes(logger, router, configRepo)
+
 	// Optionally serve /files/ as our fileblob routes
 	// Note: FILEBLOB_BASE_URL needs to match something that's routed to /files/...
 	if cloudProvider == "file" {
diff --git a/internal/database/mysql.go b/internal/database/mysql.go
index 2d48b99..dd32c52 100644
--- a/internal/database/mysql.go
+++ b/internal/database/mysql.go
@@ -124,6 +124,10 @@ var (
 			"add_namespace__to__customers",
 			"alter table customers add column namespace varchar(40) not null;",
 		),
+		execsql(
+			"create_namespace_configuration",
+			`create table namespace_configuration(namespace varchar(40) primary key not null, legal_entity varchar(40) not null, primary_account varchar(40) not null);`,
+		),
 	)
 )
 
diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go
index 552ab94..cbf04f6 100644
--- a/internal/database/sqlite.go
+++ b/internal/database/sqlite.go
@@ -142,6 +142,10 @@ PRAGMA foreign_keys=on;`,
 			"add_namespace__to__customers",
 			"alter table customers add column namespace varchar(40) not null default 'unknown';",
 		),
+		execsql(
+			"create_namespace_configuration",
+			`create table namespace_configuration(namespace varchar(40) primary key not null, legal_entity varchar(40) not null, primary_account varchar(40) not null);`,
+		),
 	)
 )
 
diff --git a/pkg/client/README.md b/pkg/client/README.md
index b4aa317..f144426 100644
--- a/pkg/client/README.md
+++ b/pkg/client/README.md
@@ -38,6 +38,8 @@ All URIs are relative to *http://localhost:8087*
 
 Class | Method | HTTP request | Description
 ------------ | ------------- | ------------- | -------------
+*ConfigurationApi* | [**GetConfiguration**](docs/ConfigurationApi.md#getconfiguration) | **Get** /configuration/customers | Get Configuration
+*ConfigurationApi* | [**UpdateConfiguration**](docs/ConfigurationApi.md#updateconfiguration) | **Put** /configuration/customers | Update Configuration
 *CustomersApi* | [**AcceptDisclaimer**](docs/CustomersApi.md#acceptdisclaimer) | **Post** /customers/{customerID}/disclaimers/{disclaimerID} | Accept customer disclaimer
 *CustomersApi* | [**AddCustomerAddress**](docs/CustomersApi.md#addcustomeraddress) | **Post** /customers/{customerID}/address | Add customer address
 *CustomersApi* | [**CompleteAccountValidation**](docs/CustomersApi.md#completeaccountvalidation) | **Put** /customers/{customerID}/accounts/{accountID}/validations | Complete Account Validation
@@ -88,6 +90,7 @@ Class | Method | HTTP request | Description
  - [InitAccountValidationResponse](docs/InitAccountValidationResponse.md)
  - [InstitutionAddress](docs/InstitutionAddress.md)
  - [InstitutionDetails](docs/InstitutionDetails.md)
+ - [NamespaceConfiguration](docs/NamespaceConfiguration.md)
  - [OfacSearch](docs/OfacSearch.md)
  - [Phone](docs/Phone.md)
  - [TransitAccountNumber](docs/TransitAccountNumber.md)
diff --git a/pkg/client/api/openapi.yaml b/pkg/client/api/openapi.yaml
index 01979ed..5848e32 100644
--- a/pkg/client/api/openapi.yaml
+++ b/pkg/client/api/openapi.yaml
@@ -17,6 +17,8 @@ servers:
 - description: Local development
   url: http://localhost:8087
 tags:
+- description: Endpoints for retrieving and updating configuration for a namespace.
+  name: Configuration
 - description: Endpoints for searching, creating, and verifying individuals according
     to US government criteria for money movement within the country.
   name: Customers
@@ -31,6 +33,65 @@ paths:
       summary: Ping Customers
       tags:
       - Customers
+  /configuration/customers:
+    get:
+      description: Retrieve current configuration for the provided namespace.
+      operationId: getConfiguration
+      parameters:
+      - description: Value used to separate and identify models
+        example: org342
+        explode: false
+        in: header
+        name: X-Namespace
+        required: false
+        schema:
+          type: string
+        style: simple
+      responses:
+        "200":
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/NamespaceConfiguration'
+          description: Configuration was successfully updated
+      summary: Get Configuration
+      tags:
+      - Configuration
+    put:
+      description: Update the configuration for the provided namespace.
+      operationId: updateConfiguration
+      parameters:
+      - description: Value used to separate and identify models
+        example: org342
+        explode: false
+        in: header
+        name: X-Namespace
+        required: false
+        schema:
+          type: string
+        style: simple
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/NamespaceConfiguration'
+        required: true
+      responses:
+        "200":
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/NamespaceConfiguration'
+          description: Configuration was successfully updated
+        "400":
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: Configuration was not updated, see error(s)
+      summary: Update Configuration
+      tags:
+      - Configuration
   /customers:
     get:
       description: Search for customers using different filter parameters
@@ -1397,6 +1458,22 @@ paths:
       - Customers
 components:
   schemas:
+    NamespaceConfiguration:
+      example:
+        primaryAccount: f6eddffd
+        legalEntity: 5c20933f
+      properties:
+        legalEntity:
+          description: customerID of legal entity for this Namespace
+          example: 5c20933f
+          type: string
+        primaryAccount:
+          description: accountID of funding/source account for this Namespace
+          example: f6eddffd
+          type: string
+      required:
+      - legalEntity
+      - primaryAccount
     CreateCustomer:
       example:
         firstName: Robert
diff --git a/pkg/client/api_configuration.go b/pkg/client/api_configuration.go
new file mode 100644
index 0000000..4096766
--- /dev/null
+++ b/pkg/client/api_configuration.go
@@ -0,0 +1,208 @@
+/*
+ * Customers API
+ *
+ * Customers focuses on solving authentic identification of humans who are legally able to hold and transfer currency within the US. Primarily this project solves [Know Your Customer](https://en.wikipedia.org/wiki/Know_your_customer) (KYC), [Customer Identification Program](https://en.wikipedia.org/wiki/Customer_Identification_Program) (CIP), [Office of Foreign Asset Control](https://www.treasury.gov/about/organizational-structure/offices/Pages/Office-of-Foreign-Assets-Control.aspx) (OFAC) checks and verification workflows to comply with US federal law and ensure authentic transfers. Customers has an objective to be a service for detailed due diligence on individuals and companies for Financial Institutions and services in a modernized and extensible way.  Customer phone numbers and addresses are stored and partially used in KYC/OFAC validation. Arbitrary key/value pairs can be stored for a Customer. Documents and Disclaimers, and their acknowledgement are also stored under a Customer as they're accepted. Bank Accounts, which can be validated with micro-deposits currently, are stored under each Customer.  ![](https://raw.githubusercontent.com/adamdecaf/customers/create-accounts/docs/images/customer.png)
+ *
+ * API version: v1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package client
+
+import (
+	_context "context"
+	"github.com/antihax/optional"
+	_ioutil "io/ioutil"
+	_nethttp "net/http"
+	_neturl "net/url"
+)
+
+// Linger please
+var (
+	_ _context.Context
+)
+
+// ConfigurationApiService ConfigurationApi service
+type ConfigurationApiService service
+
+// GetConfigurationOpts Optional parameters for the method 'GetConfiguration'
+type GetConfigurationOpts struct {
+	XNamespace optional.String
+}
+
+/*
+GetConfiguration Get Configuration
+Retrieve current configuration for the provided namespace.
+ * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ * @param optional nil or *GetConfigurationOpts - Optional Parameters:
+ * @param "XNamespace" (optional.String) -  Value used to separate and identify models
+@return NamespaceConfiguration
+*/
+func (a *ConfigurationApiService) GetConfiguration(ctx _context.Context, localVarOptionals *GetConfigurationOpts) (NamespaceConfiguration, *_nethttp.Response, error) {
+	var (
+		localVarHTTPMethod   = _nethttp.MethodGet
+		localVarPostBody     interface{}
+		localVarFormFileName string
+		localVarFileName     string
+		localVarFileBytes    []byte
+		localVarReturnValue  NamespaceConfiguration
+	)
+
+	// create path and map variables
+	localVarPath := a.client.cfg.BasePath + "/configuration/customers"
+	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.XNamespace.IsSet() {
+		localVarHeaderParams["X-Namespace"] = parameterToString(localVarOptionals.XNamespace.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,
+		}
+		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
+}
+
+// UpdateConfigurationOpts Optional parameters for the method 'UpdateConfiguration'
+type UpdateConfigurationOpts struct {
+	XNamespace optional.String
+}
+
+/*
+UpdateConfiguration Update Configuration
+Update the configuration for the provided namespace.
+ * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ * @param namespaceConfiguration
+ * @param optional nil or *UpdateConfigurationOpts - Optional Parameters:
+ * @param "XNamespace" (optional.String) -  Value used to separate and identify models
+@return NamespaceConfiguration
+*/
+func (a *ConfigurationApiService) UpdateConfiguration(ctx _context.Context, namespaceConfiguration NamespaceConfiguration, localVarOptionals *UpdateConfigurationOpts) (NamespaceConfiguration, *_nethttp.Response, error) {
+	var (
+		localVarHTTPMethod   = _nethttp.MethodPut
+		localVarPostBody     interface{}
+		localVarFormFileName string
+		localVarFileName     string
+		localVarFileBytes    []byte
+		localVarReturnValue  NamespaceConfiguration
+	)
+
+	// create path and map variables
+	localVarPath := a.client.cfg.BasePath + "/configuration/customers"
+	localVarHeaderParams := make(map[string]string)
+	localVarQueryParams := _neturl.Values{}
+	localVarFormParams := _neturl.Values{}
+
+	// to determine the Content-Type header
+	localVarHTTPContentTypes := []string{"application/json"}
+
+	// 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.XNamespace.IsSet() {
+		localVarHeaderParams["X-Namespace"] = parameterToString(localVarOptionals.XNamespace.Value(), "")
+	}
+	// body params
+	localVarPostBody = &namespaceConfiguration
+	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
+}
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 341469b..b1c380c 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -48,6 +48,8 @@ type APIClient struct {
 
 	// API Services
 
+	ConfigurationApi *ConfigurationApiService
+
 	CustomersApi *CustomersApiService
 }
 
@@ -67,6 +69,7 @@ func NewAPIClient(cfg *Configuration) *APIClient {
 	c.common.client = c
 
 	// API Services
+	c.ConfigurationApi = (*ConfigurationApiService)(&c.common)
 	c.CustomersApi = (*CustomersApiService)(&c.common)
 
 	return c
diff --git a/pkg/client/docs/ConfigurationApi.md b/pkg/client/docs/ConfigurationApi.md
new file mode 100644
index 0000000..c415ec6
--- /dev/null
+++ b/pkg/client/docs/ConfigurationApi.md
@@ -0,0 +1,98 @@
+# \ConfigurationApi
+
+All URIs are relative to *http://localhost:8087*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**GetConfiguration**](ConfigurationApi.md#GetConfiguration) | **Get** /configuration/customers | Get Configuration
+[**UpdateConfiguration**](ConfigurationApi.md#UpdateConfiguration) | **Put** /configuration/customers | Update Configuration
+
+
+
+## GetConfiguration
+
+> NamespaceConfiguration GetConfiguration(ctx, optional)
+
+Get Configuration
+
+Retrieve current configuration for the provided namespace.
+
+### Required Parameters
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+ **optional** | ***GetConfigurationOpts** | optional parameters | nil if no parameters
+
+### Optional Parameters
+
+Optional parameters are passed through a pointer to a GetConfigurationOpts struct
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **xNamespace** | **optional.String**| Value used to separate and identify models | 
+
+### Return type
+
+[**NamespaceConfiguration**](NamespaceConfiguration.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)
+
+
+## UpdateConfiguration
+
+> NamespaceConfiguration UpdateConfiguration(ctx, namespaceConfiguration, optional)
+
+Update Configuration
+
+Update the configuration for the provided namespace.
+
+### Required Parameters
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+**namespaceConfiguration** | [**NamespaceConfiguration**](NamespaceConfiguration.md)|  | 
+ **optional** | ***UpdateConfigurationOpts** | optional parameters | nil if no parameters
+
+### Optional Parameters
+
+Optional parameters are passed through a pointer to a UpdateConfigurationOpts struct
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+
+ **xNamespace** | **optional.String**| Value used to separate and identify models | 
+
+### Return type
+
+[**NamespaceConfiguration**](NamespaceConfiguration.md)
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+- **Content-Type**: application/json
+- **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)
+
diff --git a/pkg/client/docs/NamespaceConfiguration.md b/pkg/client/docs/NamespaceConfiguration.md
new file mode 100644
index 0000000..d2ce6aa
--- /dev/null
+++ b/pkg/client/docs/NamespaceConfiguration.md
@@ -0,0 +1,12 @@
+# NamespaceConfiguration
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**LegalEntity** | **string** | customerID of legal entity for this Namespace | 
+**PrimaryAccount** | **string** | accountID of funding/source account for this Namespace | 
+
+[[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/pkg/client/model_namespace_configuration.go b/pkg/client/model_namespace_configuration.go
new file mode 100644
index 0000000..a380a65
--- /dev/null
+++ b/pkg/client/model_namespace_configuration.go
@@ -0,0 +1,18 @@
+/*
+ * Customers API
+ *
+ * Customers focuses on solving authentic identification of humans who are legally able to hold and transfer currency within the US. Primarily this project solves [Know Your Customer](https://en.wikipedia.org/wiki/Know_your_customer) (KYC), [Customer Identification Program](https://en.wikipedia.org/wiki/Customer_Identification_Program) (CIP), [Office of Foreign Asset Control](https://www.treasury.gov/about/organizational-structure/offices/Pages/Office-of-Foreign-Assets-Control.aspx) (OFAC) checks and verification workflows to comply with US federal law and ensure authentic transfers. Customers has an objective to be a service for detailed due diligence on individuals and companies for Financial Institutions and services in a modernized and extensible way.  Customer phone numbers and addresses are stored and partially used in KYC/OFAC validation. Arbitrary key/value pairs can be stored for a Customer. Documents and Disclaimers, and their acknowledgement are also stored under a Customer as they're accepted. Bank Accounts, which can be validated with micro-deposits currently, are stored under each Customer.  ![](https://raw.githubusercontent.com/adamdecaf/customers/create-accounts/docs/images/customer.png)
+ *
+ * API version: v1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package client
+
+// NamespaceConfiguration struct for NamespaceConfiguration
+type NamespaceConfiguration struct {
+	// customerID of legal entity for this Namespace
+	LegalEntity string `json:"legalEntity"`
+	// accountID of funding/source account for this Namespace
+	PrimaryAccount string `json:"primaryAccount"`
+}
diff --git a/pkg/configuration/mock_repository.go b/pkg/configuration/mock_repository.go
new file mode 100644
index 0000000..e48f31e
--- /dev/null
+++ b/pkg/configuration/mock_repository.go
@@ -0,0 +1,30 @@
+// 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 configuration
+
+import (
+	"github.com/moov-io/customers/pkg/client"
+)
+
+type mockRepository struct {
+	cfg *client.NamespaceConfiguration
+	err error
+}
+
+func (r *mockRepository) Get(namespace string) (*client.NamespaceConfiguration, error) {
+	if r.err != nil {
+		var cfg client.NamespaceConfiguration
+		return &cfg, r.err
+	}
+	return r.cfg, nil
+}
+
+func (r *mockRepository) Update(namespace string, cfg *client.NamespaceConfiguration) (*client.NamespaceConfiguration, error) {
+	if r.err != nil {
+		var cfg client.NamespaceConfiguration
+		return &cfg, r.err
+	}
+	return r.cfg, nil
+}
diff --git a/pkg/configuration/repository.go b/pkg/configuration/repository.go
new file mode 100644
index 0000000..3c9d495
--- /dev/null
+++ b/pkg/configuration/repository.go
@@ -0,0 +1,83 @@
+// 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 configuration
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+
+	"github.com/moov-io/customers/pkg/client"
+)
+
+type Repository interface {
+	Get(namespace string) (*client.NamespaceConfiguration, error)
+	Update(namespace string, cfg *client.NamespaceConfiguration) (*client.NamespaceConfiguration, error)
+}
+
+func NewRepository(db *sql.DB) Repository {
+	return &sqlRepo{db: db}
+}
+
+type sqlRepo struct {
+	db *sql.DB
+}
+
+func (r *sqlRepo) Get(namespace string) (*client.NamespaceConfiguration, error) {
+	query := `select legal_entity, primary_account from namespace_configuration
+where namespace = ? limit 1;`
+	stmt, err := r.db.Prepare(query)
+	if err != nil {
+		return nil, fmt.Errorf("namespace get: %v", err)
+	}
+	defer stmt.Close()
+
+	var cfg client.NamespaceConfiguration
+	if err := stmt.QueryRow(namespace).Scan(&cfg.LegalEntity, &cfg.PrimaryAccount); err != nil {
+		if err == sql.ErrNoRows {
+			return &cfg, nil // nothing found, return an empty config
+		}
+		return nil, fmt.Errorf("namespace scan: %v", err)
+	}
+	return &cfg, nil
+}
+
+func (r *sqlRepo) Update(namespace string, cfg *client.NamespaceConfiguration) (*client.NamespaceConfiguration, error) {
+	if err := r.verifyCustomerInfo(namespace, cfg); err != nil {
+		return nil, errors.New("namespace: customerID or accountID does not belong")
+	}
+
+	query := `replace into namespace_configuration (namespace, legal_entity, primary_account) values (?, ?, ?);`
+	stmt, err := r.db.Prepare(query)
+	if err != nil {
+		return nil, fmt.Errorf("namespace update: %v", err)
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(namespace, cfg.LegalEntity, cfg.PrimaryAccount)
+	return cfg, err
+}
+
+func (r *sqlRepo) verifyCustomerInfo(namespace string, cfg *client.NamespaceConfiguration) error {
+	query := `select c.namespace from customers as c
+inner join accounts as a
+on c.customer_id = a.customer_id
+where c.namespace = ? and (c.customer_id = ? and c.deleted_at is null) and (a.account_id = ? and a.deleted_at is null)
+limit 1;`
+	stmt, err := r.db.Prepare(query)
+	if err != nil {
+		return err
+	}
+	defer stmt.Close()
+
+	var ns string
+	if err := stmt.QueryRow(namespace, cfg.LegalEntity, cfg.PrimaryAccount).Scan(&ns); err != nil {
+		return err
+	}
+	if ns != namespace {
+		return errors.New("namespace mis-match")
+	}
+	return nil
+}
diff --git a/pkg/configuration/router.go b/pkg/configuration/router.go
new file mode 100644
index 0000000..3eab402
--- /dev/null
+++ b/pkg/configuration/router.go
@@ -0,0 +1,64 @@
+// 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 configuration
+
+import (
+	"encoding/json"
+	"net/http"
+
+	moovhttp "github.com/moov-io/base/http"
+	"github.com/moov-io/customers/cmd/server/route"
+	"github.com/moov-io/customers/pkg/client"
+
+	"github.com/go-kit/kit/log"
+	"github.com/gorilla/mux"
+)
+
+func RegisterRoutes(logger log.Logger, r *mux.Router, repo Repository) {
+	r.Methods("GET").Path("/configuration/customers").HandlerFunc(getNamespaceConfig(logger, repo))
+	r.Methods("PUT").Path("/configuration/customers").HandlerFunc(updateNamespaceConfig(logger, repo))
+}
+
+func getNamespaceConfig(logger log.Logger, repo Repository) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		w = route.Responder(logger, w, r)
+
+		namespace := route.GetNamespace(w, r)
+		if namespace == "" {
+			return
+		}
+		cfg, err := repo.Get(namespace)
+		if err != nil {
+			moovhttp.Problem(w, err)
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+		json.NewEncoder(w).Encode(cfg)
+	}
+}
+
+func updateNamespaceConfig(logger log.Logger, repo Repository) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		w = route.Responder(logger, w, r)
+
+		namespace := route.GetNamespace(w, r)
+		if namespace == "" {
+			return
+		}
+
+		var body client.NamespaceConfiguration
+		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+			moovhttp.Problem(w, err)
+			return
+		}
+		cfg, err := repo.Update(namespace, &body)
+		if err != nil {
+			moovhttp.Problem(w, err)
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+		json.NewEncoder(w).Encode(cfg)
+	}
+}


Test patch:
diff --git a/pkg/configuration/repository_test.go b/pkg/configuration/repository_test.go
new file mode 100644
index 0000000..b43c0aa
--- /dev/null
+++ b/pkg/configuration/repository_test.go
@@ -0,0 +1,87 @@
+// 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 configuration
+
+import (
+	"database/sql"
+	"testing"
+
+	"github.com/moov-io/base"
+	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/customers/pkg/client"
+)
+
+func TestRepository(t *testing.T) {
+	t.Parallel()
+
+	check := func(t *testing.T, repo *sqlRepo) {
+		namespace := base.ID()
+
+		cfg, err := repo.Get(namespace)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if cfg.LegalEntity != "" || cfg.PrimaryAccount != "" {
+			t.Errorf("unexpected legal entity: %q", cfg.LegalEntity)
+			t.Errorf("unexpected primary account: %q", cfg.PrimaryAccount)
+		}
+
+		customerID, accountID := base.ID(), base.ID()
+		writeCustomerAndAccount(t, repo.db, namespace, customerID, accountID)
+
+		// write config
+		cfg = &client.NamespaceConfiguration{
+			LegalEntity:    customerID,
+			PrimaryAccount: accountID,
+		}
+		if _, err := repo.Update(namespace, cfg); err != nil {
+			t.Fatal(err)
+		}
+
+		// verify
+		cfg, err = repo.Get(namespace)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if cfg.LegalEntity == "" || cfg.PrimaryAccount == "" {
+			t.Errorf("expected legal entity: %q", cfg.LegalEntity)
+			t.Errorf("expected primary account: %q", cfg.PrimaryAccount)
+		}
+	}
+
+	check(t, sqliteRepo(t))
+}
+
+func sqliteRepo(t *testing.T) *sqlRepo {
+	db := database.CreateTestSqliteDB(t)
+	t.Cleanup(func() {
+		db.Close()
+	})
+	return &sqlRepo{db: db.DB}
+}
+
+func writeCustomerAndAccount(t *testing.T, db *sql.DB, namespace string, customerID, accountID string) {
+	// TODO(adam): replace after customers/acconts Repository are moved to ./pkg/
+	query := `insert into customers (customer_id, namespace, first_name, last_name) values (?, ?, ?, ?);`
+	stmt, err := db.Prepare(query)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer stmt.Close()
+	if _, err := stmt.Exec(customerID, namespace, "jane", "doe"); err != nil {
+		t.Fatal(err)
+	}
+
+	// insert account
+	query = `insert into accounts (account_id, customer_id, masked_account_number) values (?, ?, ?);`
+	stmt, err = db.Prepare(query)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer stmt.Close()
+	if _, err := stmt.Exec(accountID, customerID, "XXX456"); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/pkg/configuration/router_test.go b/pkg/configuration/router_test.go
new file mode 100644
index 0000000..56d7e22
--- /dev/null
+++ b/pkg/configuration/router_test.go
@@ -0,0 +1,173 @@
+// 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 configuration
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/moov-io/base"
+	"github.com/moov-io/customers/pkg/client"
+
+	"github.com/go-kit/kit/log"
+	"github.com/gorilla/mux"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRouterGet(t *testing.T) {
+	repo := &mockRepository{
+		cfg: &client.NamespaceConfiguration{
+			LegalEntity:    base.ID(),
+			PrimaryAccount: base.ID(),
+		},
+	}
+
+	req := httptest.NewRequest("GET", "/configuration/customers", nil)
+	req.Header.Set("X-Namespace", "moov")
+	w := httptest.NewRecorder()
+
+	router := mux.NewRouter()
+	RegisterRoutes(log.NewNopLogger(), router, repo)
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	require.Equal(t, w.Code, http.StatusOK)
+
+	var response client.NamespaceConfiguration
+	if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
+		t.Fatal(err)
+	}
+	if response.LegalEntity == "" {
+		t.Errorf("LegalEntity=%q", response.LegalEntity)
+	}
+	if response.PrimaryAccount == "" {
+		t.Errorf("PrimaryAccount=%q", response.PrimaryAccount)
+	}
+}
+
+func TestRouterGetErr(t *testing.T) {
+	repo := &mockRepository{
+		err: errors.New("bad error"),
+	}
+
+	req := httptest.NewRequest("GET", "/configuration/customers", nil)
+	req.Header.Set("X-Namespace", "moov")
+	w := httptest.NewRecorder()
+
+	router := mux.NewRouter()
+	RegisterRoutes(log.NewNopLogger(), router, repo)
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	require.Equal(t, w.Code, http.StatusBadRequest)
+}
+
+func TestRouterGetMissing(t *testing.T) {
+	repo := &mockRepository{
+		cfg: &client.NamespaceConfiguration{
+			LegalEntity:    base.ID(),
+			PrimaryAccount: base.ID(),
+		},
+	}
+
+	req := httptest.NewRequest("GET", "/configuration/customers", nil)
+	w := httptest.NewRecorder()
+
+	router := mux.NewRouter()
+	RegisterRoutes(log.NewNopLogger(), router, repo)
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	require.Equal(t, w.Code, http.StatusBadRequest)
+}
+
+func TestRouterUpdate(t *testing.T) {
+	repo := &mockRepository{
+		cfg: &client.NamespaceConfiguration{
+			LegalEntity:    base.ID(),
+			PrimaryAccount: base.ID(),
+		},
+	}
+
+	var body bytes.Buffer
+	json.NewEncoder(&body).Encode(&client.NamespaceConfiguration{
+		LegalEntity:    base.ID(),
+		PrimaryAccount: base.ID(),
+	})
+
+	req := httptest.NewRequest("PUT", "/configuration/customers", &body)
+	req.Header.Set("X-Namespace", "moov")
+	w := httptest.NewRecorder()
+
+	router := mux.NewRouter()
+	RegisterRoutes(log.NewNopLogger(), router, repo)
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	require.Equal(t, w.Code, http.StatusOK)
+
+	var response client.NamespaceConfiguration
+	if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
+		t.Fatal(err)
+	}
+	if response.LegalEntity == "" {
+		t.Errorf("LegalEntity=%q", response.LegalEntity)
+	}
+	if response.PrimaryAccount == "" {
+		t.Errorf("PrimaryAccount=%q", response.PrimaryAccount)
+	}
+}
+
+func TestRouterUpdateErr(t *testing.T) {
+	repo := &mockRepository{
+		err: errors.New("bad error"),
+	}
+
+	var body bytes.Buffer
+	json.NewEncoder(&body).Encode(&client.NamespaceConfiguration{
+		LegalEntity:    base.ID(),
+		PrimaryAccount: base.ID(),
+	})
+
+	req := httptest.NewRequest("PUT", "/configuration/customers", &body)
+	req.Header.Set("X-Namespace", "moov")
+	w := httptest.NewRecorder()
+
+	router := mux.NewRouter()
+	RegisterRoutes(log.NewNopLogger(), router, repo)
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	require.Equal(t, w.Code, http.StatusBadRequest)
+}
+
+func TestRouterUpdateMissing(t *testing.T) {
+	repo := &mockRepository{
+		cfg: &client.NamespaceConfiguration{
+			LegalEntity:    base.ID(),
+			PrimaryAccount: base.ID(),
+		},
+	}
+
+	var body bytes.Buffer
+	json.NewEncoder(&body).Encode(&client.NamespaceConfiguration{
+		LegalEntity:    base.ID(),
+		PrimaryAccount: base.ID(),
+	})
+
+	req := httptest.NewRequest("PUT", "/configuration/customers", &body)
+	w := httptest.NewRecorder()
+
+	router := mux.NewRouter()
+	RegisterRoutes(log.NewNopLogger(), router, repo)
+	router.ServeHTTP(w, req)
+	w.Flush()
+
+	require.Equal(t, w.Code, http.StatusBadRequest)
+}