Enrichment Service

Added producer API
Added producer simulator for tests
Added a mock

Change-Id: Ic627e9e608cd55cb193f72bf1194f5ae1c5a56e2
Issue-ID: NONRTRIC-173
Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
diff --git a/enrichment-coordinator-service/config/application.yaml b/enrichment-coordinator-service/config/application.yaml
index 6279a3e..db7d3af 100644
--- a/enrichment-coordinator-service/config/application.yaml
+++ b/enrichment-coordinator-service/config/application.yaml
@@ -17,7 +17,7 @@
     org.springframework: ERROR
     org.springframework.data: ERROR
     org.springframework.web.reactive.function.client.ExchangeFunctions: ERROR
-    org.oransc.policyagent: INFO
+    org.oransc.enrichment: INFO
   file: /var/log/policy-agent/application.log
 server:
    port : 8433
diff --git a/enrichment-coordinator-service/docs/api.yaml b/enrichment-coordinator-service/docs/api.yaml
index a5c07e7..bb34e2b 100644
--- a/enrichment-coordinator-service/docs/api.yaml
+++ b/enrichment-coordinator-service/docs/api.yaml
@@ -8,6 +8,10 @@
 tags:
   - name: A1-E Enrichment Data Consumer API
     description: Consumer Controller
+  - name: Enrichment Data Producer API
+    description: Producer Controller
+  - name: Producer Simulator
+    description: Producer Simulator Controller
 paths:
   /A1-EI/v1/eitypes:
     get:
@@ -246,9 +250,279 @@
           schema:
             $ref: '#/definitions/error_information'
       deprecated: false
+  /ei-producer/v1/eiproducers:
+    get:
+      tags:
+        - Enrichment Data Producer API
+      summary: Query EI producer identifiers
+      description: DETAILS TBD
+      operationId: getEiProducerIdentifiersUsingGET
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: EI producer identifiers
+          schema:
+            type: array
+            items:
+              type: string
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Not Found
+      deprecated: false
+  '/ei-producer/v1/eiproducers/{eiProducerId}':
+    get:
+      tags:
+        - Enrichment Data Producer API
+      summary: Job definition for an individual EI producer
+      description: Query EI jobs
+      operationId: getEiProducerUsingGET
+      produces:
+        - application/json
+      parameters:
+        - name: eiProducerId
+          in: path
+          description: eiProducerId
+          required: true
+          type: string
+      responses:
+        '200':
+          description: EI Jobs
+          schema:
+            $ref: '#/definitions/producer_ei_type_info'
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Enrichment Information producer is not found
+          schema:
+            $ref: '#/definitions/error_information'
+      deprecated: false
+    put:
+      tags:
+        - Enrichment Data Producer API
+      summary: Definitions for an individual EI producer
+      description: Put EI producer
+      operationId: putEiProducerUsingPUT
+      consumes:
+        - application/json
+      produces:
+        - application/json
+      parameters:
+        - name: eiProducerId
+          in: path
+          description: eiProducerId
+          required: true
+          type: string
+        - in: body
+          name: registrationInfo
+          description: registrationInfo
+          required: true
+          schema:
+            $ref: '#/definitions/producer_registration_info'
+      responses:
+        '200':
+          description: Producer updated
+          schema:
+            type: object
+        '201':
+          description: Producer created
+          schema:
+            type: object
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Not Found
+      deprecated: false
+    delete:
+      tags:
+        - Enrichment Data Producer API
+      summary: Individual EI Producer
+      description: Delete an EI Producer
+      operationId: deleteEiProducerUsingDELETE
+      produces:
+        - application/json
+      parameters:
+        - name: eiProducerId
+          in: path
+          description: eiProducerId
+          required: true
+          type: string
+      responses:
+        '200':
+          description: Not used
+          schema:
+            type: object
+        '204':
+          description: Producer deleted
+          schema:
+            type: object
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Producer is not found
+          schema:
+            $ref: '#/definitions/error_information'
+      deprecated: false
+  '/ei-producer/v1/eiproducers/{eiProducerId}/eijobs':
+    get:
+      tags:
+        - Enrichment Data Producer API
+      summary: Job definition for an individual EI producer
+      description: Query EI producer jobs
+      operationId: getEiProducerJobsUsingGET
+      produces:
+        - application/json
+      parameters:
+        - name: eiProducerId
+          in: path
+          description: eiProducerId
+          required: true
+          type: string
+      responses:
+        '200':
+          description: EI jobs
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/producer_ei_job_request'
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Enrichment Information producer is not found
+          schema:
+            $ref: '#/definitions/error_information'
+      deprecated: false
+  /ei-producer/v1/eitypes:
+    get:
+      tags:
+        - Enrichment Data Producer API
+      summary: Query EI type identifiers
+      description: DETAILS TBD
+      operationId: getEiTypeIdentifiersUsingGET_1
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: EI type identifiers
+          schema:
+            type: array
+            items:
+              type: string
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Not Found
+      deprecated: false
+  '/ei-producer/v1/eitypes/{eiTypeId}':
+    get:
+      tags:
+        - Enrichment Data Producer API
+      summary: Definitions for an individual EI Type
+      description: Query EI type
+      operationId: getEiTypeUsingGET_1
+      produces:
+        - application/json
+      parameters:
+        - name: eiTypeId
+          in: path
+          description: eiTypeId
+          required: true
+          type: string
+      responses:
+        '200':
+          description: EI type
+          schema:
+            $ref: '#/definitions/producer_ei_type_info'
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Enrichment Information type is not found
+          schema:
+            $ref: '#/definitions/error_information'
+      deprecated: false
+  /producer_simulator/job_created:
+    post:
+      tags:
+        - Producer Simulator
+      summary: Callback for job creation
+      operationId: jobCreatedCallbackUsingPOST
+      consumes:
+        - application/json
+      produces:
+        - application/json
+      parameters:
+        - in: body
+          name: request
+          description: request
+          required: true
+          schema:
+            $ref: '#/definitions/producer_ei_job_request'
+      responses:
+        '200':
+          description: OK
+          schema:
+            type: object
+        '201':
+          description: Created
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Not Found
+      deprecated: false
+  /producer_simulator/job_deleted:
+    post:
+      tags:
+        - Producer Simulator
+      summary: Callback for job deletion
+      operationId: jobDeletedCallbackUsingPOST
+      consumes:
+        - application/json
+      produces:
+        - application/json
+      parameters:
+        - in: body
+          name: request
+          description: request
+          required: true
+          schema:
+            $ref: '#/definitions/producer_ei_job_request'
+      responses:
+        '200':
+          description: OK
+          schema:
+            type: object
+        '201':
+          description: Created
+        '401':
+          description: Unauthorized
+        '403':
+          description: Forbidden
+        '404':
+          description: Not Found
+      deprecated: false
 definitions:
   ei_job_info:
     type: object
+    required:
+      - job_data
+      - owner
     properties:
       job_data:
         type: object
@@ -256,13 +530,12 @@
       owner:
         type: string
         description: Identity of the owner of the job
-      result_target:
-        type: string
-        description: the deliver information for the EI. This is typically a URL.
     title: ei_job_info
     description: Information for a Enrichment Information Job
   ei_job_status:
     type: object
+    required:
+      - operational_state
     properties:
       operational_state:
         type: string
@@ -297,4 +570,66 @@
         description: 'The HTTP status code generated by the origin server for this occurrence of the problem. '
     title: error_information
     description: 'Problem as defined in https://tools.ietf.org/html/rfc7807'
+  producer_ei_job_request:
+    type: object
+    required:
+      - identity
+    properties:
+      identity:
+        type: string
+        description: Json for the job data
+      job_data:
+        type: object
+        description: Json for the job data
+      type_identity:
+        type: string
+        description: Type idenitity for the job
+    title: producer_ei_job_request
+    description: Information EI job start
+  producer_ei_type_info:
+    type: object
+    properties:
+      job_data_schema:
+        type: object
+        description: Json schema for the job data
+      producer_ids:
+        type: array
+        description: Registered producers
+        items:
+          type: string
+    title: producer_ei_type_info
+    description: Information for an EI type
+  producer_ei_type_registration_info:
+    type: object
+    required:
+      - ei_type_identity
+    properties:
+      ei_type_identity:
+        type: string
+        description: EI type identity
+      job_data_schema:
+        type: object
+        description: Json schema for the job data
+    title: producer_ei_type_registration_info
+    description: Information for an EI type
+  producer_registration_info:
+    type: object
+    required:
+      - job_creation_callback_url
+      - job_deletion_callback_url
+      - supported_ei_types
+    properties:
+      job_creation_callback_url:
+        type: string
+        description: callback for job creation
+      job_deletion_callback_url:
+        type: string
+        description: callback for job deletion
+      supported_ei_types:
+        type: array
+        description: Supported EI types
+        items:
+          $ref: '#/definitions/producer_ei_type_registration_info'
+    title: producer_registration_info
+    description: Information for an EI Producer
 
diff --git a/enrichment-coordinator-service/pom.xml b/enrichment-coordinator-service/pom.xml
index e1884fc..3f156a7 100644
--- a/enrichment-coordinator-service/pom.xml
+++ b/enrichment-coordinator-service/pom.xml
@@ -30,8 +30,8 @@
         <relativePath />
     </parent>
     <groupId>org.o-ran-sc.nonrtric</groupId>
-    <artifactId>policy-agent</artifactId>
-    <version>2.1.0-SNAPSHOT</version>
+    <artifactId>enrichment-coordinator-service</artifactId>
+    <version>0.0.0-SNAPSHOT</version>
     <licenses>
         <license>
             <name>The Apache Software License, Version 2.0</name>
@@ -54,8 +54,8 @@
         <json.version>20190722</json.version>
         <commons-net.version>3.6</commons-net.version>
         <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
-        <formatter-maven-plugin.version>2.8.1</formatter-maven-plugin.version>
-        <spotless-maven-plugin.version>1.18.0</spotless-maven-plugin.version>
+        <formatter-maven-plugin.version>2.12.2</formatter-maven-plugin.version>
+        <spotless-maven-plugin.version>1.24.3</spotless-maven-plugin.version>
         <docker-maven-plugin>0.30.0</docker-maven-plugin>
         <version.dmaap>1.1.11</version.dmaap>
         <javax.ws.rs-api.version>2.1.1</javax.ws.rs-api.version>
@@ -293,7 +293,7 @@
                 <inherited>false</inherited>
                 <executions>
                     <execution>
-                        <id>generate-policy-agent-image</id>
+                        <id>generate-enrichment-coordinator-service-image</id>
                         <phase>package</phase>
                         <goals>
                             <goal>build</goal>
@@ -302,7 +302,7 @@
                             <pullRegistry>${env.CONTAINER_PULL_REGISTRY}</pullRegistry>
                             <images>
                                 <image>
-                                    <name>o-ran-sc/nonrtric-policy-agent:${project.version}</name>
+                                    <name>o-ran-sc/nonrtric-enrichment-coordinator-service:${project.version}</name>
                                     <build>
                                         <cleanup>try</cleanup>
                                         <contextDir>${basedir}</contextDir>
@@ -319,7 +319,7 @@
                         </configuration>
                     </execution>
                     <execution>
-                        <id>push-policy-agent-image</id>
+                        <id>push-enrichment-coordinator-service-image</id>
                         <goals>
                             <goal>build</goal>
                             <goal>push</goal>
@@ -329,7 +329,7 @@
                             <pushRegistry>${env.CONTAINER_PUSH_REGISTRY}</pushRegistry>
                             <images>
                                 <image>
-                                    <name>o-ran-sc/nonrtric-policy-agent:${project.version}</name>
+                                    <name>o-ran-sc/nonrtric-enrichment-coordinator-service:${project.version}</name>
                                     <build>
                                         <contextDir>${basedir}</contextDir>
                                         <dockerFile>Dockerfile</dockerFile>
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java
index 7b868f5..9e2142e 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java
@@ -23,8 +23,10 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.catalina.connector.Connector;
+import org.oransc.enrichment.clients.ProducerCallbacks;
 import org.oransc.enrichment.configuration.ApplicationConfig;
 import org.oransc.enrichment.repository.EiJobs;
+import org.oransc.enrichment.repository.EiProducers;
 import org.oransc.enrichment.repository.EiTypes;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
@@ -65,10 +67,20 @@
     }
 
     @Bean
+    public EiProducers eiProducers() {
+        return new EiProducers();
+    }
+
+    @Bean
     public ApplicationConfig getApplicationConfig() {
         return this.applicationConfig;
     }
 
+    @Bean
+    public ProducerCallbacks getProducerCallbacks() {
+        return new ProducerCallbacks();
+    }
+
     private static Connector getHttpConnector(int httpPort) {
         Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
         connector.setScheme("http");
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java
index 07dbefd..ee80183 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java
@@ -23,7 +23,10 @@
 import com.fasterxml.classmate.TypeResolver;
 import com.google.common.base.Predicates;
 
+import org.oransc.enrichment.clients.ProducerJobInfo;
 import org.oransc.enrichment.controllers.consumer.ConsumerEiJobInfo;
+import org.oransc.enrichment.controllers.producer.ProducerEiTypeInfo;
+import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -65,6 +68,9 @@
         return new Docket(DocumentationType.SWAGGER_2) //
             .apiInfo(apiInfo()) //
             .additionalModels(resolver.resolve(ConsumerEiJobInfo.class)) //
+            .additionalModels(resolver.resolve(ProducerRegistrationInfo.class)) //
+            .additionalModels(resolver.resolve(ProducerEiTypeInfo.class)) //
+            .additionalModels(resolver.resolve(ProducerJobInfo.class)) //
             .select() //
             .apis(RequestHandlerSelectors.any()) //
             .paths(PathSelectors.any()) //
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java
new file mode 100644
index 0000000..a77b772
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java
@@ -0,0 +1,94 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.clients;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.lang.invoke.MethodHandles;
+
+import org.oransc.enrichment.configuration.ApplicationConfig;
+import org.oransc.enrichment.configuration.ImmutableWebClientConfig;
+import org.oransc.enrichment.configuration.WebClientConfig;
+import org.oransc.enrichment.repository.EiJob;
+import org.oransc.enrichment.repository.EiProducer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Callbacks to the EiProducer
+ */
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
+public class ProducerCallbacks {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private static Gson gson = new GsonBuilder() //
+        .serializeNulls() //
+        .create(); //
+
+    @Autowired
+    ApplicationConfig applicationConfig;
+
+    public void notifyProducersJobCreated(EiJob eiJob) {
+        for (EiProducer producer : eiJob.type().getProducers()) {
+            notifyProducerJobStarted(producer, eiJob);
+        }
+    }
+
+    public void notifyProducersJobDeleted(EiJob eiJob) {
+        AsyncRestClient restClient = restClient(false);
+        ProducerJobInfo request = new ProducerJobInfo(eiJob.jobData(), eiJob, eiJob.type());
+        String body = gson.toJson(request);
+        for (EiProducer producer : eiJob.type().getProducers()) {
+            restClient.post(producer.jobDeletionCallbackUrl(), body) //
+                .subscribe(notUsed -> logger.debug("Job subscription started OK {}", producer.id()), //
+                    throwable -> logger.warn("Job subscription failed {}", producer.id(), throwable.toString()), null);
+        }
+    }
+
+    public void notifyProducerJobStarted(EiProducer producer, EiJob eiJob) {
+        AsyncRestClient restClient = restClient(false);
+        ProducerJobInfo request = new ProducerJobInfo(eiJob.jobData(), eiJob, eiJob.type());
+        String body = gson.toJson(request);
+
+        restClient.post(producer.jobCreationCallbackUrl(), body) //
+            .subscribe(notUsed -> logger.debug("Job subscription started OK {}", producer.id()), //
+                throwable -> logger.warn("Job subscription failed {}", producer.id(), throwable.toString()), null);
+
+    }
+
+    private AsyncRestClient restClient(boolean useTrustValidation) {
+        WebClientConfig config = this.applicationConfig.getWebClientConfig();
+        config = ImmutableWebClientConfig.builder() //
+            .keyStoreType(config.keyStoreType()) //
+            .keyStorePassword(config.keyStorePassword()) //
+            .keyStore(config.keyStore()) //
+            .keyPassword(config.keyPassword()) //
+            .isTrustStoreUsed(useTrustValidation) //
+            .trustStore(config.trustStore()) //
+            .trustStorePassword(config.trustStorePassword()) //
+            .build();
+
+        return new AsyncRestClient("", config);
+    }
+
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerJobInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerJobInfo.java
new file mode 100644
index 0000000..60762b9
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerJobInfo.java
@@ -0,0 +1,65 @@
+/*-
+ * ========================LICENSE_START=================================
+ * ONAP : ccsdk oran
+ * ======================================================================
+ * Copyright (C) 2020 Nordix Foundation. All rights reserved.
+ * ======================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.clients;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import org.immutables.gson.Gson;
+import org.oransc.enrichment.repository.EiJob;
+import org.oransc.enrichment.repository.EiType;
+
+@Gson.TypeAdapters
+@ApiModel(value = "producer_ei_job_request", description = "Information EI job start")
+public class ProducerJobInfo {
+
+    @ApiModelProperty(value = "Json for the job data", required = true)
+    @SerializedName("identity")
+    @JsonProperty("identity")
+    public String id;
+
+    @ApiModelProperty(value = "Type idenitity for the job")
+    @SerializedName("type_identity")
+    @JsonProperty("type_identity")
+    public String typeId;
+
+    @ApiModelProperty(value = "Json for the job data")
+    @SerializedName("job_data")
+    @JsonProperty("job_data")
+    public Object jobData;
+
+    public ProducerJobInfo(Object jobData, String id, String typeId) {
+        this.id = id;
+        this.jobData = jobData;
+        this.typeId = typeId;
+    }
+
+    public ProducerJobInfo(Object jobData, EiJob job, EiType type) {
+        this(jobData, job.id(), type.getId());
+    }
+
+    public ProducerJobInfo() {
+    }
+
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java
index 1df2df7..edaa3bb 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java
@@ -83,24 +83,19 @@
         this.message = message;
     }
 
-    public static Mono<ResponseEntity<Object>> createMono(String text, HttpStatus code) {
-        return Mono.just(create(text, code));
-    }
-
     public static Mono<ResponseEntity<Object>> createMono(Exception e, HttpStatus code) {
-        return createMono(e.toString(), code);
+        return Mono.just(create(e, code));
     }
 
-    public static ResponseEntity<Object> create(String text, HttpStatus code) {
-        ErrorInfo p = new ErrorInfo(text, code.value());
+    public static ResponseEntity<Object> create(Exception e, HttpStatus code) {
+        if (e instanceof RuntimeException) {
+            code = HttpStatus.INTERNAL_SERVER_ERROR;
+        }
+        ErrorInfo p = new ErrorInfo(e.toString(), code.value());
         String json = gson.toJson(p);
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON);
         return new ResponseEntity<>(json, headers, code);
     }
 
-    public static ResponseEntity<Object> create(Exception e, HttpStatus code) {
-        return create(e.toString(), code);
-    }
-
 }
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java
index d38d6dc..dcddef3 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java
@@ -22,7 +22,7 @@
 
 public class ConsumerConsts {
 
-    public static final String A1E_API_ROOT = "/A1-EI/v1";
+    public static final String API_ROOT = "/A1-EI/v1";
     public static final String CONSUMER_API_NAME = "A1-E Enrichment Data Consumer API";
     public static final String OWNER_PARAM = "owner";
     public static final String OWNER_PARAM_DESCRIPTION = "identifies the owner of the job";
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java
index 7dfaeca..c43a495 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java
@@ -22,6 +22,7 @@
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
 
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -29,15 +30,20 @@
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 
+import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.oransc.enrichment.clients.ProducerCallbacks;
+import org.oransc.enrichment.configuration.ApplicationConfig;
 import org.oransc.enrichment.controllers.ErrorResponse;
 import org.oransc.enrichment.repository.EiJob;
 import org.oransc.enrichment.repository.EiJobs;
 import org.oransc.enrichment.repository.EiType;
 import org.oransc.enrichment.repository.EiTypes;
 import org.oransc.enrichment.repository.ImmutableEiJob;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
@@ -49,21 +55,30 @@
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
 
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
 @RestController("ConsumerController")
 @Api(tags = {ConsumerConsts.CONSUMER_API_NAME})
 public class ConsumerController {
 
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Autowired
+    ApplicationConfig applicationConfig;
+
     @Autowired
     private EiJobs eiJobs;
 
     @Autowired
     private EiTypes eiTypes;
 
+    @Autowired
+    ProducerCallbacks producerCallbacks;
+
     private static Gson gson = new GsonBuilder() //
         .serializeNulls() //
         .create(); //
 
-    @GetMapping(path = ConsumerConsts.A1E_API_ROOT + "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(path = ConsumerConsts.API_ROOT + "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Query EI type identifiers", notes = "DETAILS TBD")
     @ApiResponses(
         value = { //
@@ -77,13 +92,13 @@
     ) {
         List<String> result = new ArrayList<>();
         for (EiType eiType : this.eiTypes.getAllEiTypes()) {
-            result.add(eiType.id());
+            result.add(eiType.getId());
         }
 
         return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
     }
 
-    @GetMapping(path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}", produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Definitions for an individual EI Type", notes = "Query EI type")
     @ApiResponses(
         value = { //
@@ -104,7 +119,7 @@
     }
 
     @GetMapping(
-        path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs",
+        path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs",
         produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Query EI job identifiers", notes = "Returns the EI Job identifiers for an EI Type")
     @ApiResponses(
@@ -138,7 +153,7 @@
     }
 
     @GetMapping(
-        path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}",
+        path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}",
         produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Individual EI Job", notes = "")
     @ApiResponses(
@@ -161,7 +176,7 @@
     }
 
     @GetMapping(
-        path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}/status",
+        path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}/status",
         produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "EI Job status", notes = "")
     @ApiResponses(
@@ -184,11 +199,12 @@
     }
 
     private ConsumerEiJobStatus toEiJobStatus(EiJob job) {
+        // TODO
         return new ConsumerEiJobStatus(ConsumerEiJobStatus.OperationalState.ENABLED);
     }
 
     @DeleteMapping(
-        path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}",
+        path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}",
         produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Individual EI Job", notes = "Delete EI job")
     @ApiResponses(
@@ -203,7 +219,9 @@
         @PathVariable("eiTypeId") String eiTypeId, //
         @PathVariable("eiJobId") String eiJobId) {
         try {
-            this.eiJobs.remove(eiJobId);
+            EiJob job = this.eiJobs.getJob(eiJobId);
+            this.eiJobs.remove(job);
+            this.producerCallbacks.notifyProducersJobDeleted(job);
             return new ResponseEntity<>(HttpStatus.NO_CONTENT);
         } catch (Exception e) {
             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
@@ -211,7 +229,7 @@
     }
 
     @PutMapping(
-        path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}", //
+        path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}", //
         produces = MediaType.APPLICATION_JSON_VALUE, //
         consumes = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Individual EI Job", notes = "Create or update an EI Job")
@@ -228,28 +246,38 @@
         @PathVariable("eiJobId") String eiJobId, //
         @RequestBody ConsumerEiJobInfo eiJobInfo) {
         try {
-            this.eiTypes.getType(eiTypeId); // Just to check that the type exists
+            EiType eiType = this.eiTypes.getType(eiTypeId);
+            validateJobData(eiType.getJobDataSchema(), eiJobInfo.jobData);
             final boolean newJob = this.eiJobs.get(eiJobId) == null;
-            this.eiJobs.put(toEiJob(eiJobInfo, eiJobId, eiTypeId));
+            EiJob eiJob = toEiJob(eiJobInfo, eiJobId, eiType);
+            this.eiJobs.put(eiJob);
+            this.producerCallbacks.notifyProducersJobCreated(eiJob);
             return new ResponseEntity<>(newJob ? HttpStatus.CREATED : HttpStatus.OK);
         } catch (Exception e) {
             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
         }
     }
 
+    private void validateJobData(Object schemaObj, Object json) {
+        if (schemaObj instanceof JsonObject) {
+            JsonObject schema = (JsonObject) schemaObj;
+            logger.debug("schema {} json {}", schema, json);
+        }
+    }
+
     // Status TBD
 
-    private EiJob toEiJob(ConsumerEiJobInfo info, String id, String typeId) {
+    private EiJob toEiJob(ConsumerEiJobInfo info, String id, EiType type) {
         return ImmutableEiJob.builder() //
             .id(id) //
-            .typeId(typeId) //
+            .type(type) //
             .owner(info.owner) //
             .jobData(info.jobData) //
             .build();
     }
 
     private ConsumerEiTypeInfo toEiTypeInfo(EiType t) {
-        return new ConsumerEiTypeInfo(t.jobDataSchema());
+        return new ConsumerEiTypeInfo(t.getJobDataSchema());
     }
 
     private ConsumerEiJobInfo toEiJobInfo(EiJob s) {
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java
index 08d3cae..515c616 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java
@@ -32,14 +32,14 @@
 @ApiModel(value = "ei_job_info", description = "Information for a Enrichment Information Job")
 public class ConsumerEiJobInfo {
 
-    @ApiModelProperty(value = "Identity of the owner of the job")
+    @ApiModelProperty(value = "Identity of the owner of the job", required = true)
     @SerializedName("owner")
-    @JsonProperty("owner")
+    @JsonProperty(value = "owner", required = true)
     public String owner;
 
-    @ApiModelProperty(value = "EI Type specific job data")
+    @ApiModelProperty(value = "EI Type specific job data", required = true)
     @SerializedName("job_data")
-    @JsonProperty("job_data")
+    @JsonProperty(value = "job_data", required = true)
     public Object jobData;
 
     public ConsumerEiJobInfo() {
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java
index dbdd1a3..282f44d 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java
@@ -42,9 +42,9 @@
         + "ENABLED: TBD\n" //
         + "DISABLED: TBD.";
 
-    @ApiModelProperty(value = OPERATIONAL_STATE_DESCRIPTION, name = "operational_state")
+    @ApiModelProperty(value = OPERATIONAL_STATE_DESCRIPTION, name = "operational_state", required = true)
     @SerializedName("operational_state")
-    @JsonProperty("operational_state")
+    @JsonProperty(value = "operational_state", required = true)
     public final OperationalState state;
 
     public ConsumerEiJobStatus(OperationalState state) {
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerConsts.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerConsts.java
new file mode 100644
index 0000000..9e56197
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerConsts.java
@@ -0,0 +1,31 @@
+/*-
+ * ========================LICENSE_START=================================
+ * ONAP : ccsdk oran
+ * ======================================================================
+ * Copyright (C) 2020 Nordix Foundation. All rights reserved.
+ * ======================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controllers.producer;
+
+public class ProducerConsts {
+
+    public static final String API_ROOT = "/ei-producer/v1";
+    public static final String PRODUCER_API_NAME = "Enrichment Data Producer API";
+
+    private ProducerConsts() {
+    }
+
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java
new file mode 100644
index 0000000..b88e047
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java
@@ -0,0 +1,306 @@
+/*-
+ * ========================LICENSE_START=================================
+ * ONAP : ccsdk oran
+ * ======================================================================
+ * Copyright (C) 2019-2020 Nordix Foundation. All rights reserved.
+ * ======================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controllers.producer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.oransc.enrichment.clients.ProducerCallbacks;
+import org.oransc.enrichment.clients.ProducerJobInfo;
+import org.oransc.enrichment.controllers.ErrorResponse;
+import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo.ProducerEiTypeRegistrationInfo;
+import org.oransc.enrichment.repository.EiJob;
+import org.oransc.enrichment.repository.EiJobs;
+import org.oransc.enrichment.repository.EiProducer;
+import org.oransc.enrichment.repository.EiProducers;
+import org.oransc.enrichment.repository.EiType;
+import org.oransc.enrichment.repository.EiTypes;
+import org.oransc.enrichment.repository.ImmutableEiProducer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
+@RestController("ProducerController")
+@Api(tags = {ProducerConsts.PRODUCER_API_NAME})
+public class ProducerController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    private static Gson gson = new GsonBuilder() //
+        .serializeNulls() //
+        .create(); //
+
+    @Autowired
+    private EiJobs eiJobs;
+
+    @Autowired
+    private EiTypes eiTypes;
+
+    @Autowired
+    private EiProducers eiProducers;
+
+    @Autowired
+    ProducerCallbacks producerCallbacks;
+
+    @GetMapping(path = ProducerConsts.API_ROOT + "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Query EI type identifiers", notes = "DETAILS TBD")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                code = 200,
+                message = "EI type identifiers",
+                response = String.class,
+                responseContainer = "List"), //
+        })
+    public ResponseEntity<Object> getEiTypeIdentifiers( //
+    ) {
+        List<String> result = new ArrayList<>();
+        for (EiType eiType : this.eiTypes.getAllEiTypes()) {
+            result.add(eiType.getId());
+        }
+
+        return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+    }
+
+    @GetMapping(path = ProducerConsts.API_ROOT + "/eitypes/{eiTypeId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Definitions for an individual EI Type", notes = "Query EI type")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 200, message = "EI type", response = ProducerEiTypeInfo.class), //
+            @ApiResponse(
+                code = 404,
+                message = "Enrichment Information type is not found",
+                response = ErrorResponse.ErrorInfo.class)})
+    public ResponseEntity<Object> getEiType( //
+        @PathVariable("eiTypeId") String eiTypeId) {
+        try {
+            EiType t = this.eiTypes.getType(eiTypeId);
+            ProducerEiTypeInfo info = toEiTypeInfo(t);
+            return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = ProducerConsts.API_ROOT + "/eiproducers", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Query EI producer identifiers", notes = "DETAILS TBD")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                code = 200,
+                message = "EI producer identifiers",
+                response = String.class,
+                responseContainer = "List"), //
+        })
+    public ResponseEntity<Object> getEiProducerIdentifiers( //
+    ) {
+        List<String> result = new ArrayList<>();
+        for (EiProducer eiProducer : this.eiProducers.getAllProducers()) {
+            result.add(eiProducer.id());
+        }
+
+        return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+    }
+
+    @GetMapping(
+        path = ProducerConsts.API_ROOT + "/eiproducers/{eiProducerId}",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Definition for an individual EI producer", notes = "Query EI jobs")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 200, message = "EI Jobs", response = ProducerEiTypeInfo.class), //
+            @ApiResponse(
+                code = 404,
+                message = "Enrichment Information producer is not found",
+                response = ErrorResponse.ErrorInfo.class)})
+    public ResponseEntity<Object> getEiProducer( //
+        @PathVariable("eiProducerId") String eiProducerId) {
+        try {
+            EiProducer p = this.eiProducers.getProducer(eiProducerId);
+            ProducerRegistrationInfo info = toEiProducerRegistrationInfo(p);
+            return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(
+        path = ProducerConsts.API_ROOT + "/eiproducers/{eiProducerId}/eijobs",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Job definitions for an individual EI producer", notes = "Query EI producer jobs")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 200, message = "EI jobs", response = ProducerJobInfo.class, responseContainer = "List"), //
+            @ApiResponse(
+                code = 404,
+                message = "Enrichment Information producer is not found",
+                response = ErrorResponse.ErrorInfo.class)})
+    public ResponseEntity<Object> getEiProducerJobs( //
+        @PathVariable("eiProducerId") String eiProducerId) {
+        try {
+            EiProducer producer = this.eiProducers.getProducer(eiProducerId);
+            Collection<ProducerJobInfo> producerJobs = new ArrayList<>();
+            for (EiType type : producer.eiTypes()) {
+                for (EiJob eiJob : this.eiJobs.getJobsForType(type)) {
+                    ProducerJobInfo request = new ProducerJobInfo(eiJob.jobData(), eiJob, eiJob.type());
+                    producerJobs.add(request);
+                }
+            }
+
+            return new ResponseEntity<>(gson.toJson(producerJobs), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PutMapping(
+        path = ProducerConsts.API_ROOT + "/eiproducers/{eiProducerId}",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Individual EI producer", notes = "Put EI producer")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 201, message = "Producer created", response = void.class), //
+            @ApiResponse(code = 200, message = "Producer updated", response = void.class)}//
+    )
+    public ResponseEntity<Object> putEiProducer( //
+        @PathVariable("eiProducerId") String eiProducerId, //
+        @RequestBody ProducerRegistrationInfo registrationInfo) {
+        try {
+            final EiProducer previousDefinition = this.eiProducers.get(eiProducerId);
+            if (previousDefinition != null) {
+                deregisterProducer(previousDefinition, false);
+            }
+
+            registerProducer(eiProducerId, registrationInfo);
+
+            return new ResponseEntity<>(previousDefinition == null ? HttpStatus.CREATED : HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @DeleteMapping(
+        path = ProducerConsts.API_ROOT + "/eiproducers/{eiProducerId}",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Individual EI Producer", notes = "Delete an EI Producer")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 200, message = "Not used", response = void.class),
+            @ApiResponse(code = 204, message = "Producer deleted", response = void.class),
+            @ApiResponse(code = 404, message = "Producer is not found", response = ErrorResponse.ErrorInfo.class)})
+    public ResponseEntity<Object> deleteEiProducer(@PathVariable("eiProducerId") String eiProducerId) {
+        try {
+            final EiProducer producer = this.eiProducers.getProducer(eiProducerId);
+            deregisterProducer(producer, true);
+            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    private EiType registerType(ProducerEiTypeRegistrationInfo typeInfo) {
+        EiType type = this.eiTypes.get(typeInfo.eiTypeId);
+        if (type == null) {
+            type = new EiType(typeInfo.eiTypeId, typeInfo.jobDataSchema);
+            this.eiTypes.put(type);
+        }
+        return type;
+
+    }
+
+    EiProducer createProducer(Collection<EiType> types, String producerId, ProducerRegistrationInfo registrationInfo) {
+        return ImmutableEiProducer.builder() //
+            .id(producerId) //
+            .eiTypes(types) //
+            .jobCreationCallbackUrl(registrationInfo.jobCreationCallbackUrl) //
+            .jobDeletionCallbackUrl(registrationInfo.jobDeletionCallbackUrl) //
+            .build();
+    }
+
+    private void registerProducer(String producerId, ProducerRegistrationInfo registrationInfo) {
+        ArrayList<EiType> types = new ArrayList<>();
+        for (ProducerEiTypeRegistrationInfo typeInfo : registrationInfo.types) {
+            types.add(registerType(typeInfo));
+        }
+        EiProducer producer = createProducer(types, producerId, registrationInfo);
+        this.eiProducers.put(producer);
+
+        for (EiType type : types) {
+            for (EiJob job : this.eiJobs.getJobsForType(type)) {
+                this.producerCallbacks.notifyProducerJobStarted(producer, job);
+            }
+            type.addProducer(producer);
+        }
+    }
+
+    private void deregisterProducer(EiProducer producer, boolean deleteJobs) {
+        this.eiProducers.remove(producer);
+        for (EiType type : producer.eiTypes()) {
+            boolean removed = type.removeProducer(producer) != null;
+            if (!removed) {
+                this.logger.error("Bug, no producer found");
+            }
+            if (type.getProducerIds().isEmpty() && deleteJobs) {
+                this.eiTypes.remove(type);
+                for (EiJob job : this.eiJobs.getJobsForType(type.getId())) {
+                    this.eiJobs.remove(job);
+                    this.logger.warn("Deleted job {} because no producers left", job.id());
+                }
+            }
+        }
+    }
+
+    ProducerRegistrationInfo toEiProducerRegistrationInfo(EiProducer p) {
+        Collection<ProducerEiTypeRegistrationInfo> types = new ArrayList<>();
+        for (EiType type : p.eiTypes()) {
+            types.add(toEiTypeRegistrationInfo(type));
+        }
+        return new ProducerRegistrationInfo(types, p.jobCreationCallbackUrl(), p.jobDeletionCallbackUrl());
+    }
+
+    private ProducerEiTypeRegistrationInfo toEiTypeRegistrationInfo(EiType type) {
+        return new ProducerEiTypeRegistrationInfo(type.getJobDataSchema(), type.getId());
+    }
+
+    private ProducerEiTypeInfo toEiTypeInfo(EiType t) {
+        return new ProducerEiTypeInfo(t.getJobDataSchema(), t.getProducerIds());
+    }
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerEiTypeInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerEiTypeInfo.java
new file mode 100644
index 0000000..1c9167b
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerEiTypeInfo.java
@@ -0,0 +1,55 @@
+/*-
+ * ========================LICENSE_START=================================
+ * ONAP : ccsdk oran
+ * ======================================================================
+ * Copyright (C) 2020 Nordix Foundation. All rights reserved.
+ * ======================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controllers.producer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Collection;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@ApiModel(value = "producer_ei_type_info", description = "Information for an EI type")
+public class ProducerEiTypeInfo {
+
+    @ApiModelProperty(value = "Json schema for the job data")
+    @SerializedName("job_data_schema")
+    @JsonProperty("job_data_schema")
+    public Object jobDataSchema;
+
+    @ApiModelProperty(value = "Registered producers")
+    @SerializedName("producer_ids")
+    @JsonProperty(value = "producer_ids", required = true)
+    public Collection<String> producerIds;
+
+    public ProducerEiTypeInfo(Object jobDataSchema, Collection<String> producers) {
+        this.jobDataSchema = jobDataSchema;
+        this.producerIds = producers;
+    }
+
+    public ProducerEiTypeInfo() {
+    }
+
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java
new file mode 100644
index 0000000..c48c716
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java
@@ -0,0 +1,85 @@
+/*-
+ * ========================LICENSE_START=================================
+ * ONAP : ccsdk oran
+ * ======================================================================
+ * Copyright (C) 2020 Nordix Foundation. All rights reserved.
+ * ======================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controllers.producer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Collection;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@ApiModel(value = "producer_registration_info", description = "Information for an EI Producer")
+public class ProducerRegistrationInfo {
+
+    @Gson.TypeAdapters
+    @ApiModel(value = "producer_ei_type_registration_info", description = "Information for an EI type")
+    public static class ProducerEiTypeRegistrationInfo {
+
+        @ApiModelProperty(value = "EI type identity", required = true)
+        @SerializedName("ei_type_identity")
+        @JsonProperty(value = "ei_type_identity", required = true)
+        public String eiTypeId;
+
+        @ApiModelProperty(value = "Json schema for the job data")
+        @SerializedName("job_data_schema")
+        @JsonProperty("job_data_schema")
+        public Object jobDataSchema;
+
+        public ProducerEiTypeRegistrationInfo(Object jobDataSchema, String eiTypeId) {
+            this.jobDataSchema = jobDataSchema;
+            this.eiTypeId = eiTypeId;
+        }
+
+        public ProducerEiTypeRegistrationInfo() {
+        }
+    }
+
+    @ApiModelProperty(value = "Supported EI types", required = true)
+    @SerializedName("supported_ei_types")
+    @JsonProperty(value = "supported_ei_types", required = true)
+    public Collection<ProducerEiTypeRegistrationInfo> types;
+
+    @ApiModelProperty(value = "callback for job creation", required = true)
+    @SerializedName("job_creation_callback_url")
+    @JsonProperty(value = "job_creation_callback_url", required = true)
+    public String jobCreationCallbackUrl;
+
+    @ApiModelProperty(value = "callback for job deletion", required = true)
+    @SerializedName("job_deletion_callback_url")
+    @JsonProperty(value = "job_deletion_callback_url", required = true)
+    public String jobDeletionCallbackUrl;
+
+    public ProducerRegistrationInfo(Collection<ProducerEiTypeRegistrationInfo> types, String jobCreationCallbackUrl,
+        String jobDeletionCallbackUrl) {
+        this.types = types;
+        this.jobCreationCallbackUrl = jobCreationCallbackUrl;
+        this.jobDeletionCallbackUrl = jobDeletionCallbackUrl;
+    }
+
+    public ProducerRegistrationInfo() {
+    }
+
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java
index 79f62f8..7df4a50 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java
@@ -32,7 +32,7 @@
 
     String id();
 
-    String typeId();
+    EiType type();
 
     String owner();
 
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java
index bb2e40f..9326195 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java
@@ -21,7 +21,6 @@
 package org.oransc.enrichment.repository;
 
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Vector;
@@ -33,11 +32,12 @@
  */
 public class EiJobs {
     private Map<String, EiJob> allEiJobs = new HashMap<>();
-    private Map<String, Map<String, EiJob>> jobsByType = new HashMap<>();
+
+    private MultiMap<EiJob> jobsByType = new MultiMap<>();
 
     public synchronized void put(EiJob job) {
         allEiJobs.put(job.id(), job);
-        multiMapPut(this.jobsByType, job.typeId(), job);
+        jobsByType.put(job.type().getId(), job.id(), job);
     }
 
     public synchronized Collection<EiJob> getJobs() {
@@ -53,7 +53,11 @@
     }
 
     public synchronized Collection<EiJob> getJobsForType(String typeId) {
-        return multiMapGet(this.jobsByType, typeId);
+        return jobsByType.get(typeId);
+    }
+
+    public synchronized Collection<EiJob> getJobsForType(EiType type) {
+        return jobsByType.get(type.getId());
     }
 
     public synchronized EiJob get(String id) {
@@ -70,7 +74,7 @@
 
     public synchronized void remove(EiJob job) {
         this.allEiJobs.remove(job.id());
-        multiMapRemove(this.jobsByType, job.typeId(), job);
+        jobsByType.remove(job.type().getId(), job.id());
     }
 
     public synchronized int size() {
@@ -79,28 +83,7 @@
 
     public synchronized void clear() {
         this.allEiJobs.clear();
-    }
-
-    private void multiMapPut(Map<String, Map<String, EiJob>> multiMap, String key, EiJob value) {
-        multiMap.computeIfAbsent(key, k -> new HashMap<>()).put(value.id(), value);
-    }
-
-    private void multiMapRemove(Map<String, Map<String, EiJob>> multiMap, String key, EiJob value) {
-        Map<String, EiJob> map = multiMap.get(key);
-        if (map != null) {
-            map.remove(value.id());
-            if (map.isEmpty()) {
-                multiMap.remove(key);
-            }
-        }
-    }
-
-    private Collection<EiJob> multiMapGet(Map<String, Map<String, EiJob>> multiMap, String key) {
-        Map<String, EiJob> map = multiMap.get(key);
-        if (map == null) {
-            return Collections.emptyList();
-        }
-        return new Vector<>(map.values());
+        this.jobsByType.clear();
     }
 
 }
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java
new file mode 100644
index 0000000..30e62de
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java
@@ -0,0 +1,39 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.repository;
+
+import java.util.Collection;
+
+import org.immutables.gson.Gson;
+import org.immutables.value.Value;
+
+@Value.Immutable
+@Gson.TypeAdapters
+public interface EiProducer {
+    public String id();
+
+    public Collection<EiType> eiTypes();
+
+    public String jobCreationCallbackUrl();
+
+    public String jobDeletionCallbackUrl();
+
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java
new file mode 100644
index 0000000..483850d
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java
@@ -0,0 +1,72 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.repository;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+import org.oransc.enrichment.exceptions.ServiceException;
+
+/**
+ * Dynamic representation of all Rics in the system.
+ */
+public class EiProducers {
+    private Map<String, EiProducer> allEiProducers = new HashMap<>();
+
+    public synchronized void put(EiProducer producer) {
+        allEiProducers.put(producer.id(), producer);
+
+    }
+
+    public synchronized Collection<EiProducer> getAllProducers() {
+        return new Vector<>(allEiProducers.values());
+    }
+
+    public synchronized EiProducer getProducer(String id) throws ServiceException {
+        EiProducer p = allEiProducers.get(id);
+        if (p == null) {
+            throw new ServiceException("Could not find EI producer: " + id);
+        }
+        return p;
+    }
+
+    public synchronized EiProducer get(String id) {
+        return allEiProducers.get(id);
+    }
+
+    public synchronized void remove(String id) {
+        this.allEiProducers.remove(id);
+    }
+
+    public synchronized void remove(EiProducer producer) {
+        this.allEiProducers.remove(producer.id());
+    }
+
+    public synchronized int size() {
+        return allEiProducers.size();
+    }
+
+    public synchronized void clear() {
+        this.allEiProducers.clear();
+    }
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java
index 997484d..803fcba 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java
@@ -20,13 +20,40 @@
 
 package org.oransc.enrichment.repository;
 
-import org.immutables.gson.Gson;
-import org.immutables.value.Value;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
-@Value.Immutable
-@Gson.TypeAdapters
-public interface EiType {
-    public String id();
+import lombok.Getter;
 
-    public Object jobDataSchema();
+public class EiType {
+    @Getter
+    private final String id;
+
+    @Getter
+    private final Object jobDataSchema;
+
+    private final Map<String, EiProducer> producers = new HashMap<>();
+
+    public EiType(String id, Object jobDataSchema) {
+        this.id = id;
+        this.jobDataSchema = jobDataSchema;
+    }
+
+    public synchronized Collection<EiProducer> getProducers() {
+        return Collections.unmodifiableCollection(producers.values());
+    }
+
+    public synchronized Collection<String> getProducerIds() {
+        return Collections.unmodifiableCollection(producers.keySet());
+    }
+
+    public synchronized void addProducer(EiProducer producer) {
+        this.producers.put(producer.id(), producer);
+    }
+
+    public synchronized EiProducer removeProducer(EiProducer producer) {
+        return this.producers.remove(producer.id());
+    }
 }
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java
index 7668ff1..5454e8a 100644
--- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java
@@ -34,7 +34,7 @@
     Map<String, EiType> allEiTypes = new HashMap<>();
 
     public synchronized void put(EiType type) {
-        allEiTypes.put(type.id(), type);
+        allEiTypes.put(type.getId(), type);
     }
 
     public synchronized Collection<EiType> getAllEiTypes() {
@@ -44,7 +44,7 @@
     public synchronized EiType getType(String id) throws ServiceException {
         EiType type = allEiTypes.get(id);
         if (type == null) {
-            throw new ServiceException("Could not find EI Job: " + id);
+            throw new ServiceException("Could not find EI type: " + id);
         }
         return type;
     }
@@ -57,6 +57,10 @@
         allEiTypes.remove(id);
     }
 
+    public synchronized void remove(EiType type) {
+        this.remove(type.getId());
+    }
+
     public synchronized int size() {
         return allEiTypes.size();
     }
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/MultiMap.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/MultiMap.java
new file mode 100644
index 0000000..e64ea22
--- /dev/null
+++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/MultiMap.java
@@ -0,0 +1,63 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.repository;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * Dynamic representation of all Rics in the system.
+ */
+
+public class MultiMap<T> {
+
+    private final Map<String, Map<String, T>> map = new HashMap<>();
+
+    public void put(String key, String id, T value) {
+        this.map.computeIfAbsent(key, k -> new HashMap<>()).put(id, value);
+    }
+
+    public void remove(String key, String id) {
+        Map<String, T> innerMap = this.map.get(key);
+        if (innerMap != null) {
+            innerMap.remove(id);
+            if (innerMap.isEmpty()) {
+                this.map.remove(key);
+            }
+        }
+    }
+
+    public Collection<T> get(String key) {
+        Map<String, T> innerMap = this.map.get(key);
+        if (innerMap == null) {
+            return Collections.emptyList();
+        }
+        return new Vector<>(innerMap.values());
+    }
+
+    public void clear() {
+        this.map.clear();
+    }
+
+}
diff --git a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java
index d871427..cf34076 100644
--- a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java
+++ b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java
@@ -21,6 +21,7 @@
 package org.oransc.enrichment;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.gson.Gson;
@@ -29,29 +30,44 @@
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.oransc.enrichment.clients.AsyncRestClient;
+import org.oransc.enrichment.clients.ProducerJobInfo;
 import org.oransc.enrichment.configuration.ApplicationConfig;
 import org.oransc.enrichment.configuration.ImmutableWebClientConfig;
 import org.oransc.enrichment.configuration.WebClientConfig;
+import org.oransc.enrichment.controller.ProducerSimulatorController;
 import org.oransc.enrichment.controllers.consumer.ConsumerConsts;
 import org.oransc.enrichment.controllers.consumer.ConsumerEiJobInfo;
+import org.oransc.enrichment.controllers.producer.ProducerConsts;
+import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo;
+import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo.ProducerEiTypeRegistrationInfo;
 import org.oransc.enrichment.repository.EiJob;
 import org.oransc.enrichment.repository.EiJobs;
+import org.oransc.enrichment.repository.EiProducer;
+import org.oransc.enrichment.repository.EiProducers;
 import org.oransc.enrichment.repository.EiType;
 import org.oransc.enrichment.repository.EiTypes;
 import org.oransc.enrichment.repository.ImmutableEiJob;
-import org.oransc.enrichment.repository.ImmutableEiType;
+import org.oransc.enrichment.repository.ImmutableEiProducer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
 import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
 import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
 import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
@@ -81,8 +97,14 @@
     EiTypes eiTypes;
 
     @Autowired
+    EiProducers eiProducers;
+
+    @Autowired
     ApplicationConfig applicationConfig;
 
+    @Autowired
+    ProducerSimulatorController producerSimulator;
+
     private static Gson gson = new GsonBuilder() //
         .serializeNulls() //
         .create(); //
@@ -92,7 +114,10 @@
      */
     @TestConfiguration
     static class TestBeanFactory {
-
+        @Bean
+        public ServletWebServerFactory servletContainer() {
+            return new TomcatServletWebServerFactory();
+        }
     }
 
     @LocalServerPort
@@ -102,12 +127,19 @@
     void reset() {
         this.eiJobs.clear();
         this.eiTypes.clear();
+        this.eiProducers.clear();
+        this.producerSimulator.getTestResults().reset();
+    }
+
+    @AfterEach
+    void check() {
+        assertThat(this.producerSimulator.getTestResults().errorFound).isFalse();
     }
 
     @Test
     void getEiTypes() throws Exception {
         addEiType("test");
-        String url = "/eitypes";
+        String url = ConsumerConsts.API_ROOT + "/eitypes";
         String rsp = restClient().get(url).block();
         assertThat(rsp).isEqualTo("[\"test\"]");
     }
@@ -115,35 +147,35 @@
     @Test
     void getEiType() throws Exception {
         addEiType("test");
-        String url = "/eitypes/test";
+        String url = ConsumerConsts.API_ROOT + "/eitypes/test";
         String rsp = restClient().get(url).block();
         assertThat(rsp).contains("job_data_schema");
     }
 
     @Test
     void getEiTypeNotFound() throws Exception {
-        String url = "/eitypes/junk";
-        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find EI Job: junk");
+        String url = ConsumerConsts.API_ROOT + "/eitypes/junk";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find EI type: junk");
     }
 
     @Test
     void getEiJobsIds() throws Exception {
         addEiJob("typeId", "jobId");
-        String url = "/eitypes/typeId/eijobs";
+        String url = ConsumerConsts.API_ROOT + "/eitypes/typeId/eijobs";
         String rsp = restClient().get(url).block();
         assertThat(rsp).isEqualTo("[\"jobId\"]");
     }
 
     @Test
     void getEiJobTypeNotFound() throws Exception {
-        String url = "/eitypes/junk/eijobs";
-        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find EI Job: junk");
+        String url = ConsumerConsts.API_ROOT + "/eitypes/junk/eijobs";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find EI type: junk");
     }
 
     @Test
     void getEiJob() throws Exception {
         addEiJob("typeId", "jobId");
-        String url = "/eitypes/typeId/eijobs/jobId";
+        String url = ConsumerConsts.API_ROOT + "/eitypes/typeId/eijobs/jobId";
         String rsp = restClient().get(url).block();
         assertThat(rsp).contains("job_data");
     }
@@ -151,7 +183,7 @@
     @Test
     void getEiJobStatus() throws Exception {
         addEiJob("typeId", "jobId");
-        String url = "/eitypes/typeId/eijobs/jobId/status";
+        String url = ConsumerConsts.API_ROOT + "/eitypes/typeId/eijobs/jobId/status";
         String rsp = restClient().get(url).block();
         assertThat(rsp).contains("ENABLED");
     }
@@ -162,38 +194,131 @@
     void deleteEiJob() throws Exception {
         addEiJob("typeId", "jobId");
         assertThat(this.eiJobs.size()).isEqualTo(1);
-        String url = "/eitypes/typeId/eijobs/jobId";
+        String url = ConsumerConsts.API_ROOT + "/eitypes/typeId/eijobs/jobId";
         restClient().delete(url).block();
         assertThat(this.eiJobs.size()).isEqualTo(0);
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStopped.size()).isEqualTo(1));
+        assertThat(simulatorResults.jobsStopped.get(0).id).isEqualTo("jobId");
     }
 
     @Test
     void putEiJob() throws Exception {
         addEiType("typeId");
 
-        String url = "/eitypes/typeId/eijobs/jobId";
+        String url = ConsumerConsts.API_ROOT + "/eitypes/typeId/eijobs/jobId";
         String body = gson.toJson(eiJobInfo());
         ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
         assertThat(this.eiJobs.size()).isEqualTo(1);
         assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
 
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted.size()).isEqualTo(1));
+        ProducerJobInfo request = simulatorResults.jobsStarted.get(0);
+        assertThat(request.id).isEqualTo("jobId");
+
         resp = restClient().putForEntity(url, body).block();
         assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
         EiJob job = this.eiJobs.getJob("jobId");
         assertThat(job.owner()).isEqualTo("owner");
     }
 
-    ConsumerEiJobInfo eiJobInfo() {
-        return new ConsumerEiJobInfo(jsonObject(), "owner");
+    @Test
+    void getEiProducerTypes() throws Exception {
+        this.addEiJob("typeId", "jobId");
+        String url = ProducerConsts.API_ROOT + "/eitypes";
+
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
     }
 
-    // @Test
-    @SuppressWarnings("squid:S2699")
-    void runMock() throws Exception {
-        logger.info("Keeping server alive! " + this.port);
-        synchronized (this) {
-            this.wait();
-        }
+    @Test
+    void putEiProducer() throws Exception {
+        String url = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId";
+        String body = gson.toJson(producerEiRegistratioInfo());
+
+        ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+
+        assertThat(this.eiTypes.size()).isEqualTo(1);
+        EiType type = this.eiTypes.getType("typeId");
+        assertThat(type.getProducerIds().contains("eiProducerId")).isTrue();
+        assertThat(this.eiProducers.size()).isEqualTo(1);
+        assertThat(this.eiProducers.get("eiProducerId").eiTypes().iterator().next().getId().equals("typeId")).isTrue();
+
+        resp = restClient().putForEntity(url, body).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+    }
+
+    @Test
+    void putEiProducerExistingJob() throws Exception {
+        this.addEiJob("typeId", "jobId");
+        String url = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId";
+        String body = gson.toJson(producerEiRegistratioInfo());
+
+        ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted.size()).isEqualTo(1));
+        ProducerJobInfo request = simulatorResults.jobsStarted.get(0);
+        assertThat(request.id).isEqualTo("jobId");
+    }
+
+    @Test
+    void getEiJobsForProducer() {
+        this.addEiJob("typeId", "jobId1");
+        this.addEiJob("typeId", "jobId2");
+
+        // PUT a consumer
+        String url = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId";
+        String body = gson.toJson(producerEiRegistratioInfo());
+        restClient().putForEntity(url, body).block();
+
+        url = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId/eijobs";
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+        ProducerJobInfo[] parsedResp = gson.fromJson(resp.getBody(), ProducerJobInfo[].class);
+        assertThat(parsedResp[0].typeId).isEqualTo("typeId");
+        assertThat(parsedResp[1].typeId).isEqualTo("typeId");
+    }
+
+    @Test
+    void deleteEiProducer() throws Exception {
+        String url = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId";
+        String url2 = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId2";
+        String body = gson.toJson(producerEiRegistratioInfo());
+        restClient().putForEntity(url, body).block();
+        restClient().putForEntity(url2, body).block();
+        assertThat(this.eiProducers.size()).isEqualTo(2);
+        EiType type = this.eiTypes.getType("typeId");
+        assertThat(type.getProducerIds().contains("eiProducerId")).isTrue();
+        assertThat(type.getProducerIds().contains("eiProducerId2")).isTrue();
+
+        restClient().deleteForEntity(url).block();
+        assertThat(this.eiProducers.size()).isEqualTo(1);
+        assertThat(this.eiTypes.getType("typeId").getProducerIds().contains("eiProducerId")).isFalse();
+
+        restClient().deleteForEntity(url2).block();
+        assertThat(this.eiProducers.size()).isEqualTo(0);
+        assertThat(this.eiTypes.size()).isEqualTo(0);
+    }
+
+    ProducerEiTypeRegistrationInfo producerEiTypeRegistrationInfo() {
+        return new ProducerEiTypeRegistrationInfo(jsonObject(), "typeId");
+    }
+
+    ProducerRegistrationInfo producerEiRegistratioInfo() {
+        Collection<ProducerEiTypeRegistrationInfo> types = new ArrayList<>();
+        types.add(producerEiTypeRegistrationInfo());
+        return new ProducerRegistrationInfo(types, baseUrl() + ProducerSimulatorController.JOB_CREATED_URL,
+            baseUrl() + ProducerSimulatorController.JOB_DELETED_URL);
+    }
+
+    ConsumerEiJobInfo eiJobInfo() {
+        return new ConsumerEiJobInfo(jsonObject(), "owner");
     }
 
     JsonObject jsonObject() {
@@ -204,10 +329,10 @@
     }
 
     private EiJob addEiJob(String typeId, String jobId) {
-        addEiType(typeId);
+        EiType type = addEiType(typeId);
         EiJob job = ImmutableEiJob.builder() //
             .id(jobId) //
-            .typeId(typeId) //
+            .type(type) //
             .owner("owner") //
             .jobData(jsonObject()) //
             .build();
@@ -216,16 +341,21 @@
     }
 
     private EiType addEiType(String typeId) {
-        EiType t = ImmutableEiType.builder() //
-            .id(typeId) //
-            .jobDataSchema(jsonObject()) //
+        EiType eiType = new EiType(typeId, jsonObject());
+        this.eiTypes.put(eiType); //
+        EiProducer producer = ImmutableEiProducer.builder() //
+            .id("producerId") //
+            .eiTypes(Arrays.asList(eiType)) //
+            .jobCreationCallbackUrl(baseUrl() + ProducerSimulatorController.JOB_CREATED_URL) //
+            .jobDeletionCallbackUrl(baseUrl() + ProducerSimulatorController.JOB_DELETED_URL) //
             .build();
-        this.eiTypes.put(t);
-        return t;
+        this.eiProducers.put(producer);
+        eiType.addProducer(producer);
+        return eiType;
     }
 
     private String baseUrl() {
-        return "https://localhost:" + this.port + ConsumerConsts.A1E_API_ROOT;
+        return "https://localhost:" + this.port;
     }
 
     private AsyncRestClient restClient(boolean useTrustValidation) {
diff --git a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/MockEnrichmentService.java b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/MockEnrichmentService.java
new file mode 100644
index 0000000..eaaf543
--- /dev/null
+++ b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/MockEnrichmentService.java
@@ -0,0 +1,62 @@
+/*-
+ * ========================LICENSE_START=================================
+ * ONAP : ccsdk oran
+ * ======================================================================
+ * Copyright (C) 2019-2020 Nordix Foundation. All rights reserved.
+ * ======================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
+@TestPropertySource(
+    properties = { //
+        "server.ssl.key-store=./config/keystore.jks", //
+        "app.webclient.trust-store=./config/truststore.jks"})
+class MockEnrichmentService {
+    private static final Logger logger = LoggerFactory.getLogger(ApplicationTest.class);
+
+    /**
+     * Overrides the BeanFactory.
+     */
+    @TestConfiguration
+    static class TestBeanFactory {
+
+    }
+
+    @LocalServerPort
+    private int port;
+
+    @Test
+    @SuppressWarnings("squid:S2699")
+    void runMock() throws Exception {
+        logger.warn("**************** Keeping server alive! " + this.port);
+        synchronized (this) {
+            this.wait();
+        }
+    }
+}
diff --git a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java
new file mode 100644
index 0000000..307d409
--- /dev/null
+++ b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java
@@ -0,0 +1,112 @@
+/*-
+ * ========================LICENSE_START=================================
+ * ONAP : ccsdk oran
+ * ======================================================================
+ * Copyright (C) 2019-2020 Nordix Foundation. All rights reserved.
+ * ======================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import lombok.Getter;
+
+import org.oransc.enrichment.clients.ProducerJobInfo;
+import org.oransc.enrichment.controllers.ErrorResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController("ProducerSimulatorController")
+@Api(tags = {"Producer Simulator"})
+public class ProducerSimulatorController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    public static final String JOB_CREATED_URL = "/producer_simulator/job_created";
+    public static final String JOB_DELETED_URL = "/producer_simulator/job_deleted";
+
+    public static class TestResults {
+
+        public List<ProducerJobInfo> jobsStarted = Collections.synchronizedList(new ArrayList<ProducerJobInfo>());
+        public List<ProducerJobInfo> jobsStopped = Collections.synchronizedList(new ArrayList<ProducerJobInfo>());
+        public boolean errorFound = false;
+
+        public TestResults() {
+        }
+
+        public void reset() {
+            jobsStarted.clear();
+            jobsStopped.clear();
+            this.errorFound = false;
+        }
+    }
+
+    @Getter
+    private TestResults testResults = new TestResults();
+
+    @PostMapping(path = JOB_CREATED_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Callback for job creation", notes = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 200, message = "OK", response = void.class)}//
+    )
+    public ResponseEntity<Object> jobCreatedCallback( //
+        @RequestBody ProducerJobInfo request) {
+        try {
+            this.testResults.jobsStarted.add(request);
+            logger.info("Job started callback {}", request.id);
+            if (request.id == null) {
+                throw new NullPointerException("Illegal argument");
+            }
+            return new ResponseEntity<>(HttpStatus.OK);
+        } catch (Exception e) {
+            this.testResults.errorFound = true;
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PostMapping(path = JOB_DELETED_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Callback for job deletion", notes = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 200, message = "OK", response = void.class)}//
+    )
+    public ResponseEntity<Object> jobDeletedCallback( //
+        @RequestBody ProducerJobInfo request) {
+        try {
+            logger.info("Job deleted callback {}", request.id);
+            this.testResults.jobsStopped.add(request);
+            return new ResponseEntity<>(HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+}