This guide aims at enabling the readers to setup a development environment on their system, develop their very own micro-service and integrate/communicate with services running remotely on the sandbox environment.
Prerequisites:
Prior Knowledge of Java/J2EE.
Prior Knowledge of Spring Boot.
Prior Knowledge of REST APIs and related concepts like path parameters, headers, JSON etc.
Prior knowledge of Git
PostgreSQL
Kafka
Following services should be up and running( or else should be pointed to sandbox environment):
user
MDMS
Persister
Location
Localization
Id-Gen
Billing-service
URL-shortener
If you are starting off with a fresh linux/windows machine, we will setup a few things before we can start developing a service -
i) Install Git - Git is software for tracking changes in any set of files, usually used for coordinating work among programmers collaboratively developing source code during software development. Git can be downloaded from the following link -
ii) Install JDK8 -
iii) Install IDE - For creating SpringBoot/Java applications we recommend using IntelliJ IDE. IntelliJ can be downloaded from the following links -
iv) Install Kafka (version 3.2.0 which is the latest version) - Kafka is the messaging queue that DIGIT services use to communicate with each other asynchronously. To install kafka, follow the following links -
v) Install Postman - Postman is the tool we use to hit and test the APIs exposed by various services that we have. To install postman, follow the following links -
vi) Install Kubectl - Kubectl is the tool that we use to interact with services deployed on our sandbox environment.
https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/
https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
vii) Install aws-iam-authenticator - https://docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html
viii) Post installation of kubectl, you need to add configuration to allow access to resources present in our sandbox environment. Steps for creation of this config file are a part of the set up guides mentioned above. Once config file is created, add the following content to it -
apiVersion: v1 clusters: - cluster: certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJd01EVXhNekV6TlRReE5sb1hEVE13TURVeE1URXpOVFF4Tmxvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTkJyClN6aHJjdDNORE1VZVF5TENTYWhwbEgyajJ1bkdYSWk1QThJZjF6OTgwNEZpSjZ6OS9qUHVpY3FjaTB1VURJQnUKS3hjdVFJRkozMG1MRWg3RGNiQlh2dDRnUlppZWtlZzVZNGxDT2NlTWZFZkFHY01KdDE1RVVCUFVzdlYyclRMcQp6a0ovRzVRUUFXMmhwREJLaFBoblZJTktYN1YzOU9tMUtuTklTbllPWERsZ1g3dW9Wa3I1OFhzREFHWEVsdC9uClpyc3laM2pkMWplWS8rMXlQQzlxbkorT0QwZlRQVGdCV1hMQlFwMHZKdHVzNE1JV2JLdkhlcUZ5eWtGd2V5MmoKSzk5eU1Yb0oraUpCaFJvWGllU3ZrNnFYdG44S2l4bVJtOXZPQk1hcWpuNkwwTjc3UWNCNjVRaHNKb0tWKzBiMQp5VVpJTHVTWWVTY0Yra3h6TzFVQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFNdnF3THl6d2RUL05OWlkvanNzb0lmQmIyNDgKZ3oxSHRuSXJ4UGhaY3RrYjBSMExxeTYzRFZBMFNSN0MrWk90aTNNd3BHMkFSVHVzdG1vYm9HV3poUXlXRk16awpVMVNIZSt6S3poeGcweUpjUjliZnlxM1ZtQVVCZlQyTVV5cVl2OVg0aWxpbmV0SURQaFBuWnlPMERQTHJITGoyCkcxZy8vWmZYbmFCT2k3dlZLSXFXUUR6RlltWGkwME9vOEVoalVyMU5sQ3FISnF1dUo3TlRWQWk1cXA0Qm1xWU8KUTBrbTVxTVVHbG9ZdkNmN1lHQWREWTVnWGg4dzFVMVdaNWNub0Q4WWc3aEtlSjRMRzRram1adlNucGZrS3VxNApiVDdUSjEwUEZlWFJkek8xa2FkQ3VMQSttUlg3OEd5WEw0UTZnOFdPUlhOVDYzdXN3MnlpMXVVN1lMTT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= server: https://3201E325058272AA0990C04346DA6E82.yl4.ap-south-1.eks.amazonaws.com name: eks_egov-dev contexts: - context: cluster: eks_egov-dev namespace: egov user: eks_egov-dev name: dev current-context: dev kind: Config preferences: {} users: - name: eks_egov-dev user: exec: apiVersion: client.authentication.k8s.io/v1alpha1 args: - token - -i - egov-dev command: aws-iam-authenticator env: - name: AWS_ACCESS_KEY value: AKIA42DEGMQ2KKCGGNXA - name: AWS_SECRET_ACCESS_KEY value: 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 - name: AWS_REGION value: ap-south-1
*** NOTE - In case you run into an error stating “error: You must be logged in to the server (Unauthorized)”, try to add sudo before the command. For example, “sudo kubectl get pods”. That should resolve the error.
Swagger Documentation & Generating Project from it:
The first step to develop any micro-service starts at preparing a Swagger documentation which details out all the APIs that the service is going to expose which the clients can consume.
Swagger is a utility which allows you to describe the structure of your APIs so that machines can read them. Readers can go through the following link to understand what swagger is in depth - What is Swagger?
Now comes the big question, why swagger?
There are a couple of reasons as to why we emphasize the creation of swagger contracts at the start of creating any micro-services.
Allows us to generate REST API documentation and interact with them ( Using Swagger UI ). Interaction with the APIs helps clients to understand so as to how the APIs respond to various parameters.
Swagger codegen tool and SwaggerHub can be used to generate server stubs and client SDKs using these swagger contracts (fancy way of saying it implements all the humdrum code that is required to get the API skeleton ready, right from the controller layer to the models). This in turn allows the developers to focus on the business logic rather than worrying about creating model classes and controller code.
The following tutorial can be used for the creation of swagger contracts - OpenAPI 3.0 Tutorial| Swagger Tutorial For Beginners | Design REST API Using Swagger Editor
For generating projects from swagger contracts, we use our customized swagger codegen jar.
We have to download the jar from the following link - CODEGEN JAR LINK
Following is the generic command to create API skeleton for any swagger contract:
java -jar codegen-1.0-SNAPSHOT-jar-with-dependencies.jar -l -t -u {CONTRACT_PATH } -a ARTIFACT_ID -b BASE_FOLDER
For this guide, the following should be the sequence to generate API skeleton using codegen jar:
Go to the folder where you have downloaded the codegen jar.
Execute the following command:
java -jar codegen-1.0-SNAPSHOT-jar-with-dependencies.jar -l -t -u https://raw.githubusercontent.com/egovernments/DIGIT-OSS/DIGIT-DEVELOPER-TUTORIAL/municipal-services/docs/birth-registration.yaml -a birth-registration -b digit
3. Update the spring-boot-starter-parent to 2.2.6-RELEASE (After updating spring boot version do maven update)
4. Put a slash in front of server.contextPath
and add this property to application.properties file which helps requests handlers to serve requests -
server.contextPath=/birth-registration server.servlet.context-path=/birth-registration
Add these external dependencies to pom.xml:
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.2.jre7</version> </dependency>
Setting up database connection and adding variables in application.properties
All dependent service host urls and API endpoints should be added in application.properties. Along with it whatever properties can be overwritten during deployment should be part of this file(eg: DB url and passwords,Kafka server properties, control knobs for functionalities etc.). To remove boilerplate code for referring variables from application.properties, we create a configuration file and autowire this configuration file wherever we need to refer to these variables. Following properties should be added for configuring database and kafka server( Use the default values, in case you want to tune kafka server that can be overwritten during deployment).
Once all the external dependencies have been added to pom.xml and these maven changes have been reloaded, the following properties should be added to application.properties file to configure database and kafka for development -
#DATABASE CONFIGURATION spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/postgres spring.datasource.username=postgres spring.datasource.password=postgres
Kafka Configuration properties -
# KAFKA SERVER CONFIGURATIONS kafka.config.bootstrap_server_config=localhost:9092 spring.kafka.consumer.value-deserializer=org.egov.tracer.kafka.deserializer.HashMapDeserializer spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer spring.kafka.consumer.group-id={PLACEHOLDER_PUT_KAFKA_CONSUMER_NAME} spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer spring.kafka.listener.missing-topics-fatal=false spring.kafka.consumer.properties.spring.json.use.type.headers=false # KAFKA CONSUMER CONFIGURATIONS kafka.consumer.config.auto_commit=true kafka.consumer.config.auto_commit_interval=100 kafka.consumer.config.session_timeout=15000 kafka.consumer.config.auto_offset_reset=earliest # KAFKA PRODUCER CONFIGURATIONS kafka.producer.config.retries_config=0 kafka.producer.config.batch_size_config=16384 kafka.producer.config.linger_ms_config=1 kafka.producer.config.buffer_memory_config=33554432
To add custom properties in application.properties file and then referencing them in your application -
Add SQL scripts to create DB using flyway:
Once the database has been configured, for creating tables in postgres DB we will use flyway. The following properties should be configured in application.properties file to enable flyway migration:
spring.flyway.url=jdbc:postgresql://localhost:5432/postgres spring.flyway.user=postgres spring.flyway.password=postgres spring.flyway.table=public spring.flyway.baseline-on-migrate=true spring.flyway.outOfOrder=true spring.flyway.locations=classpath:/db/migration/main spring.flyway.enabled=true
For adding the flyway scripts the following folder structure should be maintained:
Now, the migration files should be added in the main folder. Specific nomenclature should be followed while naming the file. The file name should be in the following format:
V[YEAR][MONTH][DAY][HR][MIN][SEC]__modulecode_ …_ddl.sql
Example: V20180920110535__tl_tradelicense_ddl.sql
We can reuse the flyway docker image and script already created in other services. Links for these files are attached below,copy paste this two files in db folder (this is required only while building the service on jenkins and deploying it to the DIGIT cluster and can be skipped for local development):
Script to run flyway migration
For this sample service, we will be using the following psql script to create the required tables -
CREATE TABLE eg_bt_registration( id character varying(64), tenantId character varying(64), babyFirstName character varying(64), applicationNumber character varying(64), babyLastName character varying(64), motherName character varying(64), fatherName character varying(64), doctorAttendingBirth character varying(64), hospitalName character varying(64), placeOfBirth character varying(64), dateOfBirth bigint, createdTime bigint, lastModifiedTime bigint, CONSTRAINT uk_eg_tl_TradeLicense UNIQUE (id) ); CREATE TABLE eg_bt_address( id character varying(64), tenantId character varying(64), doorNo character varying(64), latitude FLOAT, longitude FLOAT, buildingName character varying(64), addressId character varying(64), addressNumber character varying(64), type character varying(64), addressLine1 character varying(256), addressLine2 character varying(256), landmark character varying(64), street character varying(64), city character varying(64), locality character varying(64), pincode character varying(64), detail character varying(64), registrationId character varying(64), createdBy character varying(64), lastModifiedBy character varying(64), createdTime bigint, lastModifiedTime bigint, CONSTRAINT uk_eg_tl_address PRIMARY KEY (id), CONSTRAINT fk_eg_tl_address FOREIGN KEY (registrationId) REFERENCES eg_bt_registration (id) ON UPDATE CASCADE ON DELETE CASCADE );
Project Structure:
We maintain the following project service for all microservices -
Project structure can also be looked at this link - Git Link
Adding MDMS data:
MDMS data is the master data used by the application. This data is stored as JSON on git in the format given below. For any service delivery typical master data will be the allowed values for certain fields and the taxheads (in case if payment is present in the flow).
MDMS json files are in the following format -
{ “tenantid” - City code to which the master data belongs to “moduleName” - Module name to which the master data belongs to “{MASTER_NAME_PLACEHOLDER}” - Master data which references the data to be retrieved }
Once data is added to mdms repository, mdms service has to be restarted which will then load the newly added/updated mdms configs. A sample mdms config file can be viewed here - Sample MDMS data file
Adding Workflow configuration:
Workflow configuration should be created based on the business requirements. The configuration can be inserted using the /businessservice/_create API. To create the workflow configuration refer the following documentation: Configuring Workflows For New Product/Entity
Workflow configuration create request for the sample birth-registration service that we are creating in this guide:
{ "RequestInfo": { "apiId": "Rainmaker", "action": "", "did": 1, "key": "", "msgId": "20170310130900|en_IN", "requesterId": "", "ts": 1513579888683, "ver": ".01", "authToken": "{{devAuth}}" }, "BusinessServices": [ { "tenantId": "pb", "businessService": "BTR", "business": "birth-services", "businessServiceSla": 432000000, "states": [ { "sla": null, "state": null, "applicationStatus": null, "docUploadRequired": true, "isStartState": true, "isTerminateState": false, "isStateUpdatable": true, "actions": [ { "action": "APPLY", "nextState": "APPLIED", "roles": [ "CITIZEN", "EMPLOYEE" ] } ] }, { "sla": null, "state": "APPLIED", "applicationStatus": "APPLIED", "docUploadRequired": false, "isStartState": false, "isTerminateState": true, "isStateUpdatable": false, "actions": [ { "action": "APPROVE", "nextState": "APPROVED", "roles": [ "EMPLOYEE" ] }, { "action": "REJECT", "nextState": "REJECTED", "roles": [ "EMPLOYEE" ] } ] }, { "sla": null, "state": "APPROVED", "applicationStatus": "APPROVED", "docUploadRequired": false, "isStartState": false, "isTerminateState": false, "isStateUpdatable": false, "actions": [ { "action": "PAY", "nextState": "REGISTRATIONCOMPLETED", "roles": [ "SYSTEM_PAYMENT", "CITIZEN", "EMPLOYEE" ] } ] }, { "sla": null, "state": "REJECTED", "applicationStatus": "REJECTED", "docUploadRequired": false, "isStartState": false, "isTerminateState": true, "isStateUpdatable": false, "actions": null }, { "sla": null, "state": "REGISTRATIONCOMPLETED", "applicationStatus": "REGISTRATIONCOMPLETED", "docUploadRequired": false, "isStartState": false, "isTerminateState": true, "isStateUpdatable": false, "actions": null } ] } ] }
Import core service models:
Models/POJOs of the dependent service can be imported from digit-core-models library (work on creating library is ongoing). These models will be used in integration with the dependent services.
<dependency> <groupId>org.egov.services</groupId> <artifactId>tracer</artifactId> <version>2.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.egov.services</groupId> <artifactId>services-common</artifactId> <version>1.0.1-SNAPSHOT</version> </dependency>
<repositories> <repository> <id>repo.egovernments.org</id> <name>eGov ERP Releases Repository</name> <url>https://nexus-repo.egovernments.org/nexus/content/repositories/releases/</url> </repository> <repository> <id>repo.egovernments.org.snapshots</id> <name>eGov ERP Releases Repository</name> <url>https://nexus-repo.egovernments.org/nexus/content/repositories/snapshots/</url> </repository> <repository> <id>repo.egovernments.org.public</id> <name>eGov Public Repository Group</name> <url>https://nexus-repo.egovernments.org/nexus/content/groups/public/</url> </repository> </repositories>
These are pre-written libraries which contain tracer support, common models like MDMS, Auth and Auth and capability to raise custom exceptions.
Once these core models are imported, it is safe to delete the RequestInfo, ResponseInfo classes from the models folder and use the ones present under common contract which we just imported.
Before starting development, create/update the following classes -
a) Under models
folder, create RequestInfoWrapper
POJO -
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class RequestInfoWrapper { @JsonProperty("RequestInfo") private RequestInfo requestInfo; }
Update the Applicant POJO to have the following content -
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class Applicant { @JsonProperty("id") private String id = null; @JsonProperty("babyFirstName") private String babyFirstName = null; @JsonProperty("babyLastName") private String babyLastName = null; @JsonProperty("password") private String password = null; @JsonProperty("salutation") private String salutation = null; @JsonProperty("gender") private String gender = null; @JsonProperty("mobileNumber") private String mobileNumber = null; @JsonProperty("emailId") private String emailId = null; @JsonProperty("altContactNumber") private String altContactNumber = null; @JsonProperty("fatherName") private String fatherName = null; @JsonProperty("motherName") private String motherName = null; @JsonProperty("doctorAttendingBirth") private String doctorAttendingBirth = null; @JsonProperty("permanentAddress") private String permanentAddress = null; @JsonProperty("permanentCity") private String permanentCity = null; @JsonProperty("permanentPincode") private String permanentPincode = null; @JsonProperty("correspondenceCity") private String correspondenceCity = null; @JsonProperty("correspondencePincode") private String correspondencePincode = null; @JsonProperty("correspondenceAddress") private String correspondenceAddress = null; @JsonProperty("hospitalName") private String hospitalName = null; @JsonProperty("placeOfBirth") private String placeOfBirth = null; @JsonProperty("active") private Boolean active = null; @JsonProperty("locale") private String locale = null; @JsonProperty("type") private String type = null; @JsonProperty("signature") private String signature = null; @JsonProperty("accountLocked") private Boolean accountLocked = null; @JsonProperty("roles") @Valid private List<Role> roles = null; @JsonProperty("fatherOrHusbandName") private String fatherOrHusbandName = null; @JsonProperty("bloodGroup") private String bloodGroup = null; @JsonProperty("identificationMark") private String identificationMark = null; @JsonProperty("photo") private String photo = null; @JsonProperty("createdBy") private Long createdBy = null; @JsonProperty("createdDate") private LocalDate createdDate = null; @JsonProperty("lastModifiedBy") private Long lastModifiedBy = null; @JsonProperty("lastModifiedDate") private LocalDate lastModifiedDate = null; @JsonProperty("otpReference") private String otpReference = null; @JsonProperty("tenantId") private String tenantId = null; public Applicant addRolesItem(Role rolesItem) { if (this.roles == null) { this.roles = new ArrayList<>(); } this.roles.add(rolesItem); return this; } }
b) Under config
folder, create BTRConfiguration
and MainConfiguration
classes -
@Component @Data @Import({TracerConfiguration.class}) @NoArgsConstructor @AllArgsConstructor public class BTRConfiguration { @Value("${app.timezone}") private String timeZone; @PostConstruct public void initialize() { TimeZone.setDefault(TimeZone.getTimeZone(timeZone)); } @Bean @Autowired public MappingJackson2HttpMessageConverter jacksonConverter(ObjectMapper objectMapper) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); return converter; } // User Config @Value("${egov.user.host}") private String userHost; @Value("${egov.user.context.path}") private String userContextPath; @Value("${egov.user.create.path}") private String userCreateEndpoint; @Value("${egov.user.search.path}") private String userSearchEndpoint; @Value("${egov.user.update.path}") private String userUpdateEndpoint; //Idgen Config @Value("${egov.idgen.host}") private String idGenHost; @Value("${egov.idgen.path}") private String idGenPath; //Workflow Config @Value("${egov.workflow.host}") private String wfHost; @Value("${egov.workflow.transition.path}") private String wfTransitionPath; @Value("${egov.workflow.businessservice.search.path}") private String wfBusinessServiceSearchPath; @Value("${egov.workflow.processinstance.search.path}") private String wfProcessInstanceSearchPath; @Value("${is.workflow.enabled}") private Boolean isWorkflowEnabled; // BTR Variables @Value("${btr.kafka.create.topic}") private String createTopic; @Value("${btr.kafka.update.topic}") private String updateTopic; @Value("${btr.default.offset}") private Integer defaultOffset; @Value("${btr.default.limit}") private Integer defaultLimit; @Value("${btr.search.max.limit}") private Integer maxLimit; //MDMS @Value("${egov.mdms.host}") private String mdmsHost; @Value("${egov.mdms.search.endpoint}") private String mdmsEndPoint; //HRMS @Value("${egov.hrms.host}") private String hrmsHost; @Value("${egov.hrms.search.endpoint}") private String hrmsEndPoint; @Value("${egov.url.shortner.host}") private String urlShortnerHost; @Value("${egov.url.shortner.endpoint}") private String urlShortnerEndpoint; @Value("${egov.sms.notification.topic}") private String smsNotificationTopic; }
@Import({TracerConfiguration.class}) public class MainConfiguration { @Value("${app.timezone}") private String timeZone; @PostConstruct public void initialize() { TimeZone.setDefault(TimeZone.getTimeZone(timeZone)); } @Bean public ObjectMapper objectMapper(){ return new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).setTimeZone(TimeZone.getTimeZone(timeZone)); } @Bean @Autowired public MappingJackson2HttpMessageConverter jacksonConverter(ObjectMapper objectMapper) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); return converter; } }
Controller Layer:
Controller Layer contains the REST API endpoints which the service wants to expose. The code flow will start from this class. Controller class should be marked with @RestController
annotation.
Adding @RestController
is a convenient way of combining @Controller
and @Response body
annotations which eliminates the need to annotate each of the request handler methods of the Controller class with @ResponseBody
annotation.
Also @RequestMapping("/v1")
annotation should be added on top of the controller class. It will contain the version of the API(This will become part of the API endpoint url)
Each of the handler methods should be annotated with @RequestMapping
to expose them as REST API endpoints. Example - @RequestMapping(value="/document/_create", method = RequestMethod.POST)
Any request handler in the controller layer is going to have the following sequence of execution -
Making a call to the method in the Service layer and getting the response back from it.
Building responseInfo.
Building final response to be returned to the client.
For this guide, our controller class will contain the following content -
@Controller @RequestMapping("/birth-services") public class V1ApiController{ private final ObjectMapper objectMapper; private final HttpServletRequest request; private BirthRegistrationService birthRegistrationService; @Autowired private ResponseInfoFactory responseInfoFactory; @Autowired public V1ApiController(ObjectMapper objectMapper, HttpServletRequest request, BirthRegistrationService birthRegistrationService) { this.objectMapper = objectMapper; this.request = request; this.birthRegistrationService = birthRegistrationService; } @RequestMapping(value="/v1/registration/_create", method = RequestMethod.POST) public ResponseEntity<BirthRegistrationResponse> v1RegistrationCreatePost(@ApiParam(value = "Details for the new Birth Registration Application(s) + RequestInfo meta data." ,required=true ) @Valid @RequestBody BirthRegistrationRequest birthRegistrationRequest) { List<BirthRegistrationApplication> applications = birthRegistrationService.registerBtRequest(birthRegistrationRequest); ResponseInfo responseInfo = responseInfoFactory.createResponseInfoFromRequestInfo(birthRegistrationRequest.getRequestInfo(), true); BirthRegistrationResponse response = BirthRegistrationResponse.builder().birthRegistrationApplications(applications).responseInfo(responseInfo).build(); return new ResponseEntity<>(response, HttpStatus.OK); } @RequestMapping(value="/v1/registration/_search", method = RequestMethod.POST) public ResponseEntity<BirthRegistrationResponse> v1RegistrationSearchPost(@RequestBody RequestInfoWrapper requestInfoWrapper, @Valid @ModelAttribute BirthApplicationSearchCriteria birthApplicationSearchCriteria) { List<BirthRegistrationApplication> applications = birthRegistrationService.searchBtApplications(requestInfoWrapper.getRequestInfo(), birthApplicationSearchCriteria); ResponseInfo responseInfo = responseInfoFactory.createResponseInfoFromRequestInfo(requestInfoWrapper.getRequestInfo(), true); BirthRegistrationResponse response = BirthRegistrationResponse.builder().birthRegistrationApplications(applications).responseInfo(responseInfo).build(); return new ResponseEntity<>(response,HttpStatus.OK); } @RequestMapping(value="/v1/registration/_update", method = RequestMethod.POST) public ResponseEntity<BirthRegistrationResponse> v1RegistrationUpdatePost(@ApiParam(value = "Details for the new (s) + RequestInfo meta data." ,required=true ) @Valid @RequestBody BirthRegistrationRequest birthRegistrationRequest) { BirthRegistrationApplication application = birthRegistrationService.updateBtApplication(birthRegistrationRequest); ResponseInfo responseInfo = responseInfoFactory.createResponseInfoFromRequestInfo(birthRegistrationRequest.getRequestInfo(), true); BirthRegistrationResponse response = BirthRegistrationResponse.builder().birthRegistrationApplications(Collections.singletonList(application)).responseInfo(responseInfo).build(); return new ResponseEntity<>(response, HttpStatus.OK); } }
*** NOTE: At this point, your IDE must be showing a lot of errors but do not worry we will add all dependent layers as we progress through this guide and the errors will go away.
Since the codegen jar creates the search API with search parameters annotated with @RequestParam
rather than taking request parameters as a POJO. For this, we will create a POJO by the name of BirthApplicationSearchCriteria
under models folder. Put the following content in the POJO -
@Data @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder @ToString public class BirthApplicationSearchCriteria { @JsonProperty("tenantId") private String tenantId; @JsonProperty("status") private String status; @JsonProperty("ids") private List<Long> ids; @JsonProperty("applicationNumber") private String applicationNumber; }
Also, create a utils folder under digit. Add a new java class under utils folder by the name of ResponseInfoFactory
. Put the following content in this newly created class -
@Component public class ResponseInfoFactory { public ResponseInfo createResponseInfoFromRequestInfo(final RequestInfo requestInfo, final Boolean success) { final String apiId = requestInfo != null ? requestInfo.getApiId() : ""; final String ver = requestInfo != null ? requestInfo.getVer() : ""; Long ts = null; if(requestInfo!=null) ts = requestInfo.getTs(); final String resMsgId = "uief87324"; final String msgId = requestInfo != null ? requestInfo.getMsgId() : ""; final String responseStatus = success ? "successful" : "failed"; return ResponseInfo.builder().apiId(apiId).ver(ver).ts(ts).resMsgId(resMsgId).msgId(msgId).resMsgId(resMsgId) .status(responseStatus).build(); } }
Service Layer:
Request handlers in the Controller layer call upon the methods defined in the Service layer to perform business logic on the RequestData and prepare the Response to be returned back to the client.
For this guide, to create the service layer -
Create a new folder within digit folder called service.
Create a new class in this folder by the name
BirthRegistrationService
.Annotate the class with
@Service
annotation.Add the following content to the class -
@Service public class BirthRegistrationService { @Autowired private BirthApplicationValidator validator; @Autowired private BirthApplicationEnrichment enrichmentUtil; @Autowired private UserService userService; @Autowired private WorkflowService workflowService; @Autowired private BirthRegistrationRepository birthRegistrationRepository; @Autowired private Producer producer; public List<BirthRegistrationApplication> registerBtRequest(BirthRegistrationRequest birthRegistrationRequest) { // Validate applications validator.validateBirthApplication(birthRegistrationRequest); // Enrich applications enrichmentUtil.enrichBirthApplication(birthRegistrationRequest); // Enrich/Upsert user in upon birth registration userService.callUserService(birthRegistrationRequest); // Initiate workflow for the new application workflowService.updateWorkflowStatus(birthRegistrationRequest); // Push the application to the topic for persister to listen and persist producer.push("save-bt-application", birthRegistrationRequest); // Return the response back to user return birthRegistrationRequest.getBirthRegistrationApplications(); } public List<BirthRegistrationApplication> searchBtApplications(RequestInfo requestInfo, BirthApplicationSearchCriteria birthApplicationSearchCriteria) { // Fetch applications from database according to the given search criteria List<BirthRegistrationApplication> applications = BirthRegistrationRepository.getApplications(birthApplicationSearchCriteria); // If no applications are found matching the given criteria, return an empty list if(CollectionUtils.isEmpty(applications)) return new ArrayList<>(); // Otherwise return the found applications return applications; } public BirthRegistrationApplication updateBtApplication(BirthRegistrationRequest birthRegistrationRequest) { // Validate whether the application that is being requested for update indeed exists BirthRegistrationApplication existingApplication = validator.validateApplicationExistence(birthRegistrationRequest.getBirthRegistrationApplications().get(0)); existingApplication.setWorkflow(birthRegistrationRequest.getBirthRegistrationApplications().get(0).getWorkflow()); birthRegistrationRequest.setBirthRegistrationApplications(Collections.singletonList(existingApplication)); // Enrich application upon update enrichmentUtil.enrichBirthApplicationUponUpdate(birthRegistrationRequest); workflowService.updateWorkflowStatus(birthRegistrationRequest); // Just like create request, update request will be handled asynchronously by the persister producer.push("update-bt-application", birthRegistrationRequest.getBirthRegistrationApplications().get(0)); return birthRegistrationRequest.getBirthRegistrationApplications().get(0); } }
*** NOTE: At this point, your IDE must be showing a lot of errors but do not worry we will add all dependent layers as we progress through this guide and the errors will go away.
The service layer typically contains the following layers -
Validation Layer - All business validation logic should be added in this class. For example, verifying the values against the master data, ensuring non duplication of data etc.
In this guide, for creating the validation layer, the following steps should be followed -
a. Create a folder under digit by the name of validators. This is being done so that we keep the validation logic separate so that the code is easy to navigate through and readable.
b. Create a class by the name of BirthApplicationValidator
c. Annotate the class with @Component
annotation and put the following content in the class -
@Component public class BirthApplicationValidator { @Autowired private BirthRegistrationRepository repository; public void validateBirthApplication(BirthRegistrationRequest birthRegistrationRequest) { birthRegistrationRequest.getBirthRegistrationApplications().forEach(application -> { if(ObjectUtils.isEmpty(application.getTenantId())) throw new CustomException("EG_VT_APP_ERR", "tenantId is mandatory for creating birth registration applications"); }); } public BirthRegistrationApplication validateApplicationExistence(BirthRegistrationApplication birthRegistrationApplication) { return repository.getApplications(BirthApplicationSearchCriteria.builder().applicationNumber(birthRegistrationApplication.getApplicationNumber()).build()).get(0); } }
*** NOTE: For the sake of simplicity the above mentioned validations have been implemented. Required validations will vary on case by case basis.
2. Enrichment Layer - This layer will enrich the request. System generated values like id, auditDetails etc. will be generated and added to the request.
In this guide, for creating the enrichment layer, the following steps should be followed -
a. Create a folder under digit by the name of enrichment. Again, this is being done so that enrichment code is separate from business logic so that the codebase is easy to navigate through and readable.
b. Create a class by the name of BirthApplicationEnrichment
c. Annotate the class with @Component
and add the following methods to the class -
@Component public class BirthApplicationEnrichment { @Autowired private IdgenUtil idgenUtil; public void enrichBirthApplication(BirthRegistrationRequest birthRegistrationRequest) { List<String> birthRegistrationIdList = idgenUtil.getIdList(birthRegistrationRequest.getRequestInfo(), birthRegistrationRequest.getBirthRegistrationApplications().get(0).getTenantId(), "btr.registrationid", "", birthRegistrationRequest.getBirthRegistrationApplications().size()); Integer index = 0; for(BirthRegistrationApplication application : birthRegistrationRequest.getBirthRegistrationApplications()){ // Enrich audit details AuditDetails auditDetails = AuditDetails.builder().createdBy(birthRegistrationRequest.getRequestInfo().getUserInfo().getUuid()).createdTime(System.currentTimeMillis()).lastModifiedBy(birthRegistrationRequest.getRequestInfo().getUserInfo().getUuid()).lastModifiedTime(System.currentTimeMillis()).build(); application.setAuditDetails(auditDetails); // Enrich UUID application.setId(UUID.randomUUID().toString()); // Enrich registration Id application.getAddress().setRegistrationId(application.getId()); // Enrich address UUID application.getAddress().setId(UUID.randomUUID().toString()); //Enrich application number from IDgen application.setApplicationNumber(birthRegistrationIdList.get(index++)); } } public void enrichBirthApplicationUponUpdate(BirthRegistrationRequest birthRegistrationRequest) { // Enrich lastModifiedTime and lastModifiedBy in case of update birthRegistrationRequest.getBirthRegistrationApplications().get(0).getAuditDetails().setLastModifiedTime(System.currentTimeMillis()); birthRegistrationRequest.getBirthRegistrationApplications().get(0).getAuditDetails().setLastModifiedBy(birthRegistrationRequest.getRequestInfo().getUserInfo().getUuid()); } }
*** NOTE: For the sake of simplicity the above mentioned enrichment methods have been implemented. Required enrichment will vary on case by case basis.
3. Integration - A separate class should be created for integrating with each dependent microservice. Only one method from that class should be called from the main service class for integration.
In this guide, we will be showcasing how we can integrate our microservices with other microservices like MDMS, IdGen, User and Workflow.
For interacting with other microservices, we can create and implement the following ServiceRequestRepository
class under repository folder -
@Repository @Slf4j public class ServiceRequestRepository { private ObjectMapper mapper; private RestTemplate restTemplate; @Autowired public ServiceRequestRepository(ObjectMapper mapper, RestTemplate restTemplate) { this.mapper = mapper; this.restTemplate = restTemplate; } public Object fetchResult(StringBuilder uri, Object request) { mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); Object response = null; try { response = restTemplate.postForObject(uri.toString(), request, Map.class); }catch(HttpClientErrorException e) { log.error("External Service threw an Exception: ",e); throw new ServiceCallException(e.getResponseBodyAsString()); }catch(Exception e) { log.error("Exception while fetching from searcher: ",e); } return response; } }
a. Integration with MDMS - Integration with MDMS requires the following steps to be followed:
i) Add a new MDMS file in MDMS repo. For this guide, a sample MDMS file has already been added which can be found here - egov-mdms-data/BtrCharges.json at DEV · egovernments/egov-mdms-data
ii) Restart MDMS service after adding the new file.
iii) Once restarted, hit the curl mentioned below to verify that the new file has been properly added -
curl --location --request POST 'https://dev.digit.org/egov-mdms-service/v1/_search' \ --header 'Content-Type: application/json' \ --data-raw '{ "RequestInfo": { "apiId": "asset-services", "ver": null, "ts": null, "action": null, "did": null, "key": null, "msgId": "search with from and to values", "authToken": "{{devAuth}}" }, "MdmsCriteria": { "tenantId": "pb", "moduleDetails": [ { "moduleName": "BTR", "masterDetails": [ { "name": "RegistrationCharges" } ] } ] } }'
iv) Once verified, we can call mdms service from within our application and fetch the required master data. For this, create a java class by the name of MdmsUtil
under utils folder. Annotate this class with @Component
and put the following content in the class -
@Autowired private RestTemplate restTemplate; @Value("${egov.mdms.host}") private String mdmsHost; @Value("${egov.mdms.search.endpoint}") private String mdmsUrl; public Integer fetchRegistrationChargesFromMdms(RequestInfo requestInfo, String tenantId) { StringBuilder uri = new StringBuilder(); uri.append(mdmsHost).append(mdmsUrl); MdmsCriteriaReq mdmsCriteriaReq = getMdmsRequestForCategoryList(requestInfo, tenantId); Object response = new HashMap<>(); Integer rate = 0; try { response = restTemplate.postForObject(uri.toString(), mdmsCriteriaReq, Map.class); rate = JsonPath.read(response, "$.MdmsRes.BTR.RegistrationCharges.[0].amount"); }catch(Exception e) { log.error("Exception occurred while fetching category lists from mdms: ",e); } return rate; } private MdmsCriteriaReq getMdmsRequestForCategoryList(RequestInfo requestInfo, String tenantId) { MasterDetail masterDetail = new MasterDetail(); masterDetail.setName("RegistrationCharges"); List<MasterDetail> masterDetailList = new ArrayList<>(); masterDetailList.add(masterDetail); ModuleDetail moduleDetail = new ModuleDetail(); moduleDetail.setMasterDetails(masterDetailList); moduleDetail.setModuleName("BTR"); List<ModuleDetail> moduleDetailList = new ArrayList<>(); moduleDetailList.add(moduleDetail); MdmsCriteria mdmsCriteria = new MdmsCriteria(); mdmsCriteria.setTenantId(tenantId.split("\\.")[0]); mdmsCriteria.setModuleDetails(moduleDetailList); MdmsCriteriaReq mdmsCriteriaReq = new MdmsCriteriaReq(); mdmsCriteriaReq.setMdmsCriteria(mdmsCriteria); mdmsCriteriaReq.setRequestInfo(requestInfo); return mdmsCriteriaReq; }
v) Add the following properties in application.properties file -
#mdms urls egov.mdms.host=https://dev.digit.org egov.mdms.search.endpoint=/egov-mdms-service/v1/_search
b. Integration with IdGen - Integration with Idgen requires the following steps -
i) Add the Id Format that needs to be generated in this file - Id Format Mdms File
ii) For this tutorial, the following Id format has been added as part of this PR - Tutorial Id Format
iii) Now, restart IDGen service and Mdms service and port-forward IDGen service to port 8285 using -
kubectl port-forward <IDGEN_SERVICE_POD_NAME> 8285:8080
iv) Hit the following curl to verify that the format is added properly -
curl --location --request POST 'http://localhost:8285/egov-idgen/id/_generate' \ --header 'Content-Type: application/json' \ --data-raw '{ "RequestInfo": { "apiId": "string", "ver": "string", "ts": null, "action": "string", "did": "string", "key": "string", "msgId": "string", "authToken": "6456b2cf-49ca-47c7-b7b6-c179f19614c7", "correlationId": "e721639b-c095-40b3-86e2-acecb2cb6efb", "userInfo": { "id": 23299, "uuid": "e721639b-c095-40b3-86e2-acecb2cb6efb", "userName": "9337682030", "name": "Abhilash Seth", "type": "EMPLOYEE", "mobileNumber": "9337682030", "emailId": "abhilash.seth@gmail.com", "roles": [ { "id": 281, "name": "Employee" } ] } }, "idRequests": [ { "tenantId": "pb.amritsar", "idName": "btr.registrationid" } ] }'
v) Once verified, we can call idgen service from within our application and generate registrationId. For this, create a java class by the name of IdgenUtil
under utils folder. Annotate this class with @Component
and put the following content in the class -
@Component public class IdgenUtil { @Value("${egov.idgen.host}") private String idGenHost; @Value("${egov.idgen.path}") private String idGenPath; @Autowired private ObjectMapper mapper; @Autowired private ServiceRequestRepository restRepo; public List<String> getIdList(RequestInfo requestInfo, String tenantId, String idName, String idformat, Integer count) { List<IdRequest> reqList = new ArrayList<>(); for (int i = 0; i < count; i++) { reqList.add(IdRequest.builder().idName(idName).format(idformat).tenantId(tenantId).build()); } IdGenerationRequest request = IdGenerationRequest.builder().idRequests(reqList).requestInfo(requestInfo).build(); StringBuilder uri = new StringBuilder(idGenHost).append(idGenPath); IdGenerationResponse response = mapper.convertValue(restRepo.fetchResult(uri, request), IdGenerationResponse.class); List<IdResponse> idResponses = response.getIdResponses(); if (CollectionUtils.isEmpty(idResponses)) throw new CustomException("IDGEN ERROR", "No ids returned from idgen Service"); return idResponses.stream().map(IdResponse::getId).collect(Collectors.toList()); } }
Add the following model POJOs under models folder -
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class IdGenerationRequest { @JsonProperty("RequestInfo") private RequestInfo requestInfo; private List<IdRequest> idRequests; }
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class IdRequest { @JsonProperty("idName") @NotNull private String idName; @NotNull @JsonProperty("tenantId") private String tenantId; @JsonProperty("format") private String format; }
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class IdResponse { private String id; }
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class IdGenerationResponse { private ResponseInfo responseInfo; private List<IdResponse> idResponses; }
vi) Add the following properties in application.properties file -
#Idgen Config egov.idgen.host=http://localhost:8285/ egov.idgen.path=egov-idgen/id/_generate
c. Integration with User service - Integration with user service requires the following steps -
i) Create a new class under utils by the name of UserUtil
and update the User
POJO to have the following content -
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class User { @JsonProperty("id") private Long id = null; @JsonProperty("uuid") private String uuid = null; @JsonProperty("userName") private String userName = null; @JsonProperty("password") private String password = null; @JsonProperty("salutation") private String salutation = null; @JsonProperty("gender") private String gender = null; @JsonProperty("mobileNumber") private String mobileNumber = null; @JsonProperty("emailId") private String emailId = null; @JsonProperty("altContactNumber") private String altContactNumber = null; @JsonProperty("babyFirstName") private String babyFirstName = null; @JsonProperty("babyLastName") private String babyLastName = null; @JsonProperty("doctorAttendingBirth") private String doctorAttendingBirth = null; @JsonProperty("permanentAddress") private String permanentAddress = null; @JsonProperty("permanentCity") private String permanentCity = null; @JsonProperty("permanentPincode") private String permanentPincode = null; @JsonProperty("correspondenceCity") private String correspondenceCity = null; @JsonProperty("correspondencePincode") private String correspondencePincode = null; @JsonProperty("correspondenceAddress") private String correspondenceAddress = null; @JsonProperty("hospitalName") private String hospitalName = null; @JsonProperty("placeOfBirth") private String placeOfBirth = null; @JsonProperty("active") private Boolean active = null; @JsonProperty("locale") private String locale = null; @JsonProperty("type") private String type = null; @JsonProperty("signature") private String signature = null; @JsonProperty("accountLocked") private Boolean accountLocked = null; @JsonProperty("roles") @Valid private List<Role> roles = null; @JsonProperty("fatherOrHusbandName") private String fatherOrHusbandName = null; @JsonProperty("bloodGroup") private String bloodGroup = null; @JsonProperty("identificationMark") private String identificationMark = null; @JsonProperty("photo") private String photo = null; @JsonProperty("createdBy") private Long createdBy = null; @JsonProperty("lastModifiedBy") private Long lastModifiedBy = null; @JsonProperty("otpReference") private String otpReference = null; @JsonProperty("tenantId") private String tenantId = null; public User addRolesItem(Role rolesItem) { if (this.roles == null) { this.roles = new ArrayList<>(); } this.roles.add(rolesItem); return this; } }
ii) Annotate the created UserUtil
class with @Component
and add the following code in the created class -
@Component public class UserUtil { private ObjectMapper mapper; private ServiceRequestRepository serviceRequestRepository; private BTRConfiguration config; @Autowired public UserUtil(ObjectMapper mapper, ServiceRequestRepository serviceRequestRepository, BTRConfiguration config) { this.mapper = mapper; this.serviceRequestRepository = serviceRequestRepository; this.config = config; } /** * Returns UserDetailResponse by calling user service with given uri and object * @param userRequest Request object for user service * @param uri The address of the endpoint * @return Response from user service as parsed as userDetailResponse */ public UserDetailResponse userCall(Object userRequest, StringBuilder uri) { String dobFormat = null; if(uri.toString().contains(config.getUserSearchEndpoint()) || uri.toString().contains(config.getUserUpdateEndpoint())) dobFormat="yyyy-MM-dd"; else if(uri.toString().contains(config.getUserCreateEndpoint())) dobFormat = "dd/MM/yyyy"; try{ LinkedHashMap responseMap = (LinkedHashMap)serviceRequestRepository.fetchResult(uri, userRequest); parseResponse(responseMap,dobFormat); UserDetailResponse userDetailResponse = mapper.convertValue(responseMap,UserDetailResponse.class); return userDetailResponse; } catch(IllegalArgumentException e) { throw new CustomException("IllegalArgumentException","ObjectMapper not able to convertValue in userCall"); } } /** * Parses date formats to long for all users in responseMap * @param responeMap LinkedHashMap got from user api response */ public void parseResponse(LinkedHashMap responeMap, String dobFormat){ List<LinkedHashMap> users = (List<LinkedHashMap>)responeMap.get("user"); String format1 = "dd-MM-yyyy HH:mm:ss"; if(users!=null){ users.forEach( map -> { map.put("createdDate",dateTolong((String)map.get("createdDate"),format1)); if((String)map.get("lastModifiedDate")!=null) map.put("lastModifiedDate",dateTolong((String)map.get("lastModifiedDate"),format1)); if((String)map.get("dob")!=null) map.put("dob",dateTolong((String)map.get("dob"),dobFormat)); if((String)map.get("pwdExpiryDate")!=null) map.put("pwdExpiryDate",dateTolong((String)map.get("pwdExpiryDate"),format1)); } ); } } /** * Converts date to long * @param date date to be parsed * @param format Format of the date * @return Long value of date */ private Long dateTolong(String date,String format){ SimpleDateFormat f = new SimpleDateFormat(format); Date d = null; try { d = f.parse(date); } catch (ParseException e) { throw new CustomException("INVALID_DATE_FORMAT","Failed to parse date format in user"); } return d.getTime(); } /** * enriches the userInfo with statelevel tenantId and other fields * @param mobileNumber * @param tenantId * @param userInfo */ public void addUserDefaultFields(String mobileNumber,String tenantId, User userInfo){ Role role = getCitizenRole(tenantId); userInfo.setRoles(Collections.singletonList(role)); userInfo.setType("CITIZEN"); userInfo.setUserName(mobileNumber); userInfo.setTenantId(getStateLevelTenant(tenantId)); userInfo.setActive(true); } /** * Returns role object for citizen * @param tenantId * @return */ private Role getCitizenRole(String tenantId){ Role role = new Role(); role.setCode("CITIZEN"); role.setName("Citizen"); role.setTenantId(getStateLevelTenant(tenantId)); return role; } public String getStateLevelTenant(String tenantId){ return tenantId.split("\\.")[0]; } }
iii) Create the following POJOs -
@AllArgsConstructor @Getter @NoArgsConstructor public class CreateUserRequest { @JsonProperty("requestInfo") private RequestInfo requestInfo; @JsonProperty("user") private User user; }
@Getter @Setter public class UserSearchRequest { @JsonProperty("RequestInfo") private RequestInfo requestInfo; @JsonProperty("uuid") private List<String> uuid; @JsonProperty("id") private List<String> id; @JsonProperty("userName") private String userName; @JsonProperty("name") private String name; @JsonProperty("babyFirstName") private String babyFirstName; @JsonProperty("babyLastName") private String babyLastName; @JsonProperty("doctorAttendingBirth") private String doctorAttendingBirth; @JsonProperty("mobileNumber") private String mobileNumber; @JsonProperty("emailId") private String emailId; @JsonProperty("fuzzyLogic") private boolean fuzzyLogic; @JsonProperty("hospitalName") private String hospitalName; @JsonProperty("placeOfBirth") private String placeOfBirth; @JsonProperty("active") @Setter private Boolean active; @JsonProperty("tenantId") private String tenantId; @JsonProperty("pageSize") private int pageSize; @JsonProperty("pageNumber") private int pageNumber = 0; @JsonProperty("sort") private List<String> sort = Collections.singletonList("name"); @JsonProperty("userType") private String userType; @JsonProperty("roleCodes") private List<String> roleCodes; }
@AllArgsConstructor @NoArgsConstructor @Getter public class UserDetailResponse { @JsonProperty("responseInfo") ResponseInfo responseInfo; @JsonProperty("user") List<User> user; }
v) Create a class by the name of UserService
under service
folder and add the following content to it -
@Service public class UserService { private UserUtil userUtils; private BTRConfiguration config; @Autowired public UserService(UserUtil userUtils, BTRConfiguration config) { this.userUtils = userUtils; this.config = config; } /** * Calls user service to enrich user from search or upsert user * @param request */ public void callUserService(BirthRegistrationRequest request){ request.getBirthRegistrationApplications().forEach(application -> { if(!StringUtils.isEmpty(application.getApplicant().getId())) enrichUser(application, request.getRequestInfo()); else upsertUser(application, request.getRequestInfo()); }); } private void upsertUser(BirthRegistrationApplication application, RequestInfo requestInfo){ Applicant applicant = application.getApplicant(); User user = User.builder().userName(applicant.getUserName()) .password(applicant.getPassword()) .salutation(applicant.getSalutation()) .name(applicant.getName()) .gender(applicant.getGender()) .mobileNumber(applicant.getMobileNumber()) .emailId(applicant.getEmailId()) .altContactNumber(applicant.getAltContactNumber()) .babyFirstName(applicant.babyFirstName()) .babyLastName(applicant.babyLastName()) .doctorAttendingBirth(applicant.doctorAttendingBirth()) .hospitalName(applicant.hospitalName()) .placeOfBirth(applicant.placeOfBirth()) .permanentAddress(applicant.getPermanentAddress()) .permanentCity(applicant.getPermanentCity()) .permanentPincode(applicant.getPermanentPincode()) .correspondenceCity(applicant.getCorrespondenceCity()) .correspondencePincode(applicant.getCorrespondencePincode()) .correspondenceAddress(applicant.getCorrespondenceAddress()) .active(applicant.getActive()) .locale(applicant.getLocale()) .signature(applicant.getSignature()) .accountLocked(applicant.getAccountLocked()) .fatherOrHusbandName(applicant.getFatherOrHusbandName()) .bloodGroup(applicant.getBloodGroup()) .identificationMark(applicant.getIdentificationMark()) .photo(applicant.getPhoto()) .otpReference(applicant.getOtpReference()) .tenantId(applicant.getTenantId()) .type(applicant.getType()) .roles(applicant.getRoles()) .tenantId(applicant.getTenantId()) .aadhaarNumber(applicant.getAadhaarNumber()) .build(); String tenantId = applicant.getTenantId(); User userServiceResponse = null; // Search on mobile number as user name UserDetailResponse userDetailResponse = searchUser(userUtils.getStateLevelTenant(tenantId),null, user.getMobileNumber()); if (!userDetailResponse.getUser().isEmpty()) { User userFromSearch = userDetailResponse.getUser().get(0); if(!user.getName().equalsIgnoreCase(userFromSearch.getName())){ userServiceResponse = updateUser(requestInfo,user,userFromSearch); } else userServiceResponse = userDetailResponse.getUser().get(0); } else { userServiceResponse = createUser(requestInfo,tenantId,user); } // Enrich the accountId applicant.setId(userServiceResponse.getUuid()); } private void enrichUser(BirthRegistrationApplication application, RequestInfo requestInfo){ String accountId = application.getApplicant().getId(); String tenantId = application.getApplicant().getTenantId(); UserDetailResponse userDetailResponse = searchUser(userUtils.getStateLevelTenant(tenantId),accountId,null); if(userDetailResponse.getUser().isEmpty()) throw new CustomException("INVALID_ACCOUNTID","No user exist for the given accountId"); else application.getApplicant().setId(userDetailResponse.getUser().get(0).getUuid()); } /** * Creates the user from the given userInfo by calling user service * @param requestInfo * @param tenantId * @param userInfo * @return */ private User createUser(RequestInfo requestInfo,String tenantId, User userInfo) { userUtils.addUserDefaultFields(userInfo.getMobileNumber(),tenantId, userInfo); StringBuilder uri = new StringBuilder(config.getUserHost()) .append(config.getUserContextPath()) .append(config.getUserCreateEndpoint()); UserDetailResponse userDetailResponse = userUtils.userCall(new CreateUserRequest(requestInfo, userInfo), uri); return userDetailResponse.getUser().get(0); } /** * Updates the given user by calling user service * @param requestInfo * @param user * @param userFromSearch * @return */ private User updateUser(RequestInfo requestInfo,User user,User userFromSearch) { userFromSearch.setName(user.getName()); userFromSearch.setActive(true); StringBuilder uri = new StringBuilder(config.getUserHost()) .append(config.getUserContextPath()) .append(config.getUserUpdateEndpoint()); UserDetailResponse userDetailResponse = userUtils.userCall(new CreateUserRequest(requestInfo, userFromSearch), uri); return userDetailResponse.getUser().get(0); } /** * calls the user search API based on the given accountId and userName * @param stateLevelTenant * @param accountId * @param userName * @return */ private UserDetailResponse searchUser(String stateLevelTenant, String accountId, String userName){ UserSearchRequest userSearchRequest =new UserSearchRequest(); userSearchRequest.setActive(true); userSearchRequest.setUserType("CITIZEN"); userSearchRequest.setTenantId(stateLevelTenant); if(StringUtils.isEmpty(accountId) && StringUtils.isEmpty(userName)) return null; if(!StringUtils.isEmpty(accountId)) userSearchRequest.setUuid(Collections.singletonList(accountId)); if(!StringUtils.isEmpty(userName)) userSearchRequest.setUserName(userName); StringBuilder uri = new StringBuilder(config.getUserHost()).append(config.getUserSearchEndpoint()); return userUtils.userCall(userSearchRequest,uri); } /** * calls the user search API based on the given list of user uuids * @param uuids * @return */ private Map<String,User> searchBulkUser(List<String> uuids){ UserSearchRequest userSearchRequest =new UserSearchRequest(); userSearchRequest.setActive(true); userSearchRequest.setUserType("CITIZEN"); if(!CollectionUtils.isEmpty(uuids)) userSearchRequest.setUuid(uuids); StringBuilder uri = new StringBuilder(config.getUserHost()).append(config.getUserSearchEndpoint()); UserDetailResponse userDetailResponse = userUtils.userCall(userSearchRequest,uri); List<User> users = userDetailResponse.getUser(); if(CollectionUtils.isEmpty(users)) throw new CustomException("USER_NOT_FOUND","No user found for the uuids"); Map<String,User> idToUserMap = users.stream().collect(Collectors.toMap(User::getUuid, Function.identity())); return idToUserMap; } }
vi) Add the following properties in application.properties file -
#User config egov.user.host=http://localhost:8284/ egov.user.context.path=/user/users egov.user.create.path=/_createnovalidate egov.user.search.path=/user/_search egov.user.update.path=/_updatenovalidate
d) Integration with URL Shortener - Integration with URL shortener requires the following steps -
i) Create a new class by the name of UrlShortnerUtil
ii) Annotate this class with @Component
and add the following code -
@Autowired private RestTemplate restTemplate; @Autowired private BTRConfiguration config; public String getShortenedUrl(String url){ HashMap<String,String> body = new HashMap<>(); body.put("url",url); StringBuilder builder = new StringBuilder(config.getUrlShortnerHost()); builder.append(config.getUrlShortnerEndpoint()); String res = restTemplate.postForObject(builder.toString(), body, String.class); if(StringUtils.isEmpty(res)){ log.error("URL_SHORTENING_ERROR", "Unable to shorten url: " + url); ; return url; } else return res; }
iii) Add the following properties in application.properties file -
#url shortner egov.url.shortner.host=https://dev.digit.org egov.url.shortner.endpoint=/egov-url-shortening/shortener
e) Integration with workflow - Integration with workflow service requires the following steps -
i) Add a workflow object to BirthRegistrationApplication
POJO -
@Valid @JsonProperty("workflow") private Workflow workflow = null;
Create ProcessInstance
, State
, Action
, ProcessInstanceRequest
, ProcessInstanceResponse
, BusinessService
, BusinessServiceResponse
POJOs -
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder @EqualsAndHashCode(of = { "id" }) @ToString public class ProcessInstance { @Size(max = 64) @JsonProperty("id") private String id; @NotNull @Size(max = 128) @JsonProperty("tenantId") private String tenantId; @NotNull @Size(max = 128) @JsonProperty("businessService") private String businessService; @NotNull @Size(max = 128) @JsonProperty("businessId") private String businessId; @NotNull @Size(max = 128) @JsonProperty("action") private String action; @NotNull @Size(max = 64) @JsonProperty("moduleName") private String moduleName; @JsonProperty("state") private State state; @JsonProperty("comment") private String comment; @JsonProperty("documents") @Valid private List<Document> documents; @JsonProperty("assignes") private List<User> assignes; public ProcessInstance addDocumentsItem(Document documentsItem) { if (this.documents == null) { this.documents = new ArrayList<>(); } if (!this.documents.contains(documentsItem)) this.documents.add(documentsItem); return this; } }
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder @ToString @EqualsAndHashCode(of = {"tenantId","businessServiceId","state"}) public class State { @Size(max=256) @JsonProperty("uuid") private String uuid; @Size(max=256) @JsonProperty("tenantId") private String tenantId; @Size(max=256) @JsonProperty("businessServiceId") private String businessServiceId; @JsonProperty("sla") private Long sla; @Size(max=256) @JsonProperty("state") private String state; @Size(max=256) @JsonProperty("applicationStatus") private String applicationStatus; @JsonProperty("docUploadRequired") private Boolean docUploadRequired; @JsonProperty("isStartState") private Boolean isStartState; @JsonProperty("isTerminateState") private Boolean isTerminateState; @JsonProperty("isStateUpdatable") private Boolean isStateUpdatable; @JsonProperty("actions") @Valid private List<Action> actions; private AuditDetails auditDetails; public State addActionsItem(Action actionsItem) { if (this.actions == null) { this.actions = new ArrayList<>(); } this.actions.add(actionsItem); return this; } }
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder @ToString @EqualsAndHashCode(of = {"tenantId","currentState","action"}) public class Action { @Size(max=256) @JsonProperty("uuid") private String uuid; @Size(max=256) @JsonProperty("tenantId") private String tenantId; @Size(max=256) @JsonProperty("currentState") private String currentState; @Size(max=256) @JsonProperty("action") private String action; @Size(max=256) @JsonProperty("nextState") private String nextState; @Size(max=1024) @JsonProperty("roles") @Valid private List<String> roles; private AuditDetails auditDetails; public Action addRolesItem(String rolesItem) { if (this.roles == null) { this.roles = new ArrayList<>(); } this.roles.add(rolesItem); return this; } }
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder @ToString public class ProcessInstanceRequest { @JsonProperty("RequestInfo") private RequestInfo requestInfo; @JsonProperty("ProcessInstances") @Valid @NotNull private List<ProcessInstance> processInstances; public ProcessInstanceRequest addProcessInstanceItem(ProcessInstance processInstanceItem) { if (this.processInstances == null) { this.processInstances = new ArrayList<>(); } this.processInstances.add(processInstanceItem); return this; } }
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class ProcessInstanceResponse { @JsonProperty("ResponseInfo") private ResponseInfo responseInfo; @JsonProperty("ProcessInstances") @Valid private List<ProcessInstance> processInstances; public ProcessInstanceResponse addProceInstanceItem(ProcessInstance proceInstanceItem) { if (this.processInstances == null) { this.processInstances = new ArrayList<>(); } this.processInstances.add(proceInstanceItem); return this; } }
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder @ToString @EqualsAndHashCode(of = {"tenantId","businessService"}) @JsonInclude(JsonInclude.Include.NON_NULL) public class BusinessService { @Size(max=256) @JsonProperty("tenantId") private String tenantId = null; @Size(max=256) @JsonProperty("uuid") private String uuid = null; @Size(max=256) @JsonProperty("businessService") private String businessService = null; @Size(max=256) @JsonProperty("business") private String business = null; @Size(max=1024) @JsonProperty("getUri") private String getUri = null; @Size(max=1024) @JsonProperty("postUri") private String postUri = null; @JsonProperty("businessServiceSla") private Long businessServiceSla = null; @NotNull @Valid @JsonProperty("states") private List<State> states = null; @JsonProperty("auditDetails") private AuditDetails auditDetails = null; public BusinessService addStatesItem(State statesItem) { if (this.states == null) { this.states = new ArrayList<>(); } this.states.add(statesItem); return this; } /** * Returns the currentState with the given uuid if not present returns null * @param uuid the uuid of the currentState to be returned * @return */ public State getStateFromUuid(String uuid) { State state = null; if(this.states!=null){ for(State s : this.states){ if(s.getUuid().equalsIgnoreCase(uuid)){ state = s; break; } } } return state; } }
@Data @NoArgsConstructor @AllArgsConstructor @Builder @ToString public class BusinessServiceResponse { @JsonProperty("ResponseInfo") private ResponseInfo responseInfo; @JsonProperty("BusinessServices") @Valid @NotNull private List<BusinessService> businessServices; public BusinessServiceResponse addBusinessServiceItem(BusinessService businessServiceItem) { if (this.businessServices == null) { this.businessServices = new ArrayList<>(); } this.businessServices.add(businessServiceItem); return this; } }
ii) Next, we have to create a class to transition the workflow object across its states. For this, create a class by the name of WorkflowService
and annotate it with @Service
annotation. Add the following content to this class -
@Component public class WorkflowService { @Autowired private ObjectMapper mapper; @Autowired private ServiceRequestRepository repository; @Autowired private BTRConfiguration config; public void updateWorkflowStatus(BirthRegistrationRequest birthRegistrationRequest) { birthRegistrationRequest.getBirthRegistrationApplications().forEach(application -> { ProcessInstance processInstance = getProcessInstanceForBTR(application, birthRegistrationRequest.getRequestInfo()); ProcessInstanceRequest workflowRequest = new ProcessInstanceRequest(birthRegistrationRequest.getRequestInfo(), Collections.singletonList(processInstance)); callWorkFlow(workflowRequest); }); } public State callWorkFlow(ProcessInstanceRequest workflowReq) { ProcessInstanceResponse response = null; StringBuilder url = new StringBuilder(config.getWfHost().concat(config.getWfTransitionPath())); Object optional = repository.fetchResult(url, workflowReq); response = mapper.convertValue(optional, ProcessInstanceResponse.class); return response.getProcessInstances().get(0).getState(); } private ProcessInstance getProcessInstanceForBTR(BirthRegistrationApplication application, RequestInfo requestInfo) { Workflow workflow = application.getWorkflow(); ProcessInstance processInstance = new ProcessInstance(); processInstance.setBusinessId(application.getApplicationNumber()); processInstance.setAction(workflow.getAction()); processInstance.setModuleName("birth-services"); processInstance.setTenantId(application.getTenantId()); processInstance.setBusinessService("BTR"); processInstance.setDocuments(workflow.getDocuments()); processInstance.setComment(workflow.getComments()); if(!CollectionUtils.isEmpty(workflow.getAssignes())){ List<User> users = new ArrayList<>(); workflow.getAssignes().forEach(uuid -> { digit.web.models.User user = new digit.web.models.User(); user.setUuid(uuid); users.add(user); }); processInstance.setAssignes(users); } return processInstance; } private BusinessService getBusinessService(BirthRegistrationApplication application, RequestInfo requestInfo) { String tenantId = application.getTenantId(); StringBuilder url = getSearchURLWithParams(tenantId, "BTR"); RequestInfoWrapper requestInfoWrapper = RequestInfoWrapper.builder().requestInfo(requestInfo).build(); Object result = repository.fetchResult(url, requestInfoWrapper); BusinessServiceResponse response = null; try { response = mapper.convertValue(result, BusinessServiceResponse.class); } catch (IllegalArgumentException e) { throw new CustomException("PARSING ERROR", "Failed to parse response of workflow business service search"); } if (CollectionUtils.isEmpty(response.getBusinessServices())) throw new CustomException("BUSINESSSERVICE_NOT_FOUND", "The businessService " + "BTR" + " is not found"); return response.getBusinessServices().get(0); } private StringBuilder getSearchURLWithParams(String tenantId, String businessService) { StringBuilder url = new StringBuilder(config.getWfHost()); url.append(config.getWfBusinessServiceSearchPath()); url.append("?tenantId="); url.append(tenantId); url.append("&businessServices="); url.append(businessService); return url; } public ProcessInstanceRequest getProcessInstanceForBirthRegistrationPayment(BirthRegistrationRequest updateRequest) { BirthRegistrationApplication application = updateRequest.getBirthRegistrationApplications().get(0); ProcessInstance process = ProcessInstance.builder() .businessService("BTR") .businessId(application.getApplicationNumber()) .comment("Payment for birth registration processed") .moduleName("birth-services") .tenantId(application.getTenantId()) .action("PAY") .build(); return ProcessInstanceRequest.builder() .requestInfo(updateRequest.getRequestInfo()) .processInstances(Arrays.asList(process)) .build(); } }
iii) Add the following properties to application.properties file -
#Workflow config is.workflow.enabled=true egov.workflow.host=http://localhost:8282 egov.workflow.transition.path=/egov-workflow-v2/egov-wf/process/_transition egov.workflow.businessservice.search.path=/egov-workflow-v2/egov-wf/businessservice/_search egov.workflow.processinstance.search.path=/egov-workflow-v2/egov-wf/process/_search
Calculation - The calculation class will contain the calculation logic for given service delivery. Based on the application submitted the calculator class will calculate the tax/charges and call billing service to generate demand.
For our guide, we are going to create a sample calculation class with some dummy logic. For this, we are going to perform the following steps -
i) Create a class under service folder by the name of CalculationService
ii) Now, annotate this class with @Service
annotation and add the following logic within it -
@Service public class CalculationService { public Double calculateLaminationCharges(BirthRegistrationApplication application){ // Add calculation logic according to business requirement return 10.0; } }
Repository Layer:
Methods in the service layer, upon performing all the business logic, call methods in the Repository layer to persist or lookup data i.e. it interacts with the configured data store. For executing the queries, JdbcTemplate class is used. JdbcTemplate takes care of creation and release of resources such as creating and closing the connection etc. All database operations namely insert, update, search and delete can be performed on the database using methods of JdbcTemplate class.
On DIGIT however, we handle create and update operations asynchronously. Our persister service listens on the topic to which service applications are pushed for insertion and updation. Persister then takes care of executing insert and update operations on the database without hogging our application’s threads.
That leaves us with execution of search queries on the database to return applications as per the search parameters provided by the user.
For this guide, these are the steps that we will be taking to implement repository layer -
i) Create querybuilder and rowmapper folders within repository folder.
ii) Create a class by the name of BirthApplicationQueryBuilder
in querybuilder folder and annotate it with @Component
annotation. Put the following content in BirthApplicationQueryBuilder
class -
@Component public class BirthApplicationQueryBuilder { private static final String BASE_BTR_QUERY = " SELECT btr.id as bid, btr.tenantid as btenantid, btr.hospitalName as bhospitalName, btr.applicationnumber as bapplicationnumber, btr.applicantid as bapplicantid, btr.dateOfBirth as bdateOfBirth, btr.babyFirstName as bbabyFirstName, btr.babyLastName as bbabyLastName, btr.motherName as bmotherName, btr.fatherName as bfatherName, btr.doctorAttendingBirth as bdoctorAttendingBirth, btr.createdtime as bcreatedtime, btr.lastmodifiedtime as blastmodifiedtime, "; private static final String ADDRESS_SELECT_QUERY = " add.id as aid, add.tenantid as atenantid, add.doorno as adoorno, add.latitude as alatitude, add.longitude as alongitude, add.buildingname as abuildingname, add.addressid as aaddressid, add.addressnumber as aaddressnumber, add.type as atype, add.addressline1 as aaddressline1, add.addressline2 as aaddressline2, add.landmark as alandmark, add.street as astreet, add.city as acity, add.locality as alocality, add.pincode as apincode, add.detail as adetail, add.registrationid as aregistrationid "; private static final String FROM_TABLES = " FROM eg_bt_registration btr LEFT JOIN eg_bt_address add ON btr.id = add.registrationid "; private final String ORDERBY_CREATEDTIME = " ORDER BY btr.createdtime DESC "; public String getBirthApplicationSearchQuery(BirthApplicationSearchCriteria criteria, List<Object> preparedStmtList){ StringBuilder query = new StringBuilder(BASE_BTR_QUERY); query.append(ADDRESS_SELECT_QUERY); query.append(FROM_TABLES); if(!ObjectUtils.isEmpty(criteria.getTenantId())){ addClauseIfRequired(query, preparedStmtList); query.append(" btr.tenantid = ? "); preparedStmtList.add(criteria.getTenantId()); } if(!ObjectUtils.isEmpty(criteria.getStatus())){ addClauseIfRequired(query, preparedStmtList); query.append(" btr.status = ? "); preparedStmtList.add(criteria.getStatus()); } if(!CollectionUtils.isEmpty(criteria.getIds())){ addClauseIfRequired(query, preparedStmtList); query.append(" btr.id IN ( ").append(createQuery(criteria.getIds())).append(" ) "); addToPreparedStatement(preparedStmtList, criteria.getIds()); } if(!ObjectUtils.isEmpty(criteria.getApplicationNumber())){ addClauseIfRequired(query, preparedStmtList); query.append(" btr.applicationnumber = ? "); preparedStmtList.add(criteria.getApplicationNumber()); } // order birth registration applications based on their createdtime in latest first manner query.append(ORDERBY_CREATEDTIME); return query.toString(); } private void addClauseIfRequired(StringBuilder query, List<Object> preparedStmtList){ if(preparedStmtList.isEmpty()){ query.append(" WHERE "); }else{ query.append(" AND "); } } private String createQuery(List<String> ids) { StringBuilder builder = new StringBuilder(); int length = ids.size(); for (int i = 0; i < length; i++) { builder.append(" ?"); if (i != length - 1) builder.append(","); } return builder.toString(); } private void addToPreparedStatement(List<Object> preparedStmtList, List<String> ids) { ids.forEach(id -> { preparedStmtList.add(id); }); } }
iii) Next, create a class by the name of BirthApplicationRowMapper
under rowmapper folder and annotate it with @Component
. Add the following content in the class -
@Component public class BirthApplicationRowMapper implements ResultSetExtractor<List<BirthRegistrationApplication>> { public List<BirthRegistrationApplication> extractData(ResultSet rs) throws SQLException, DataAccessException { Map<String,BirthRegistrationApplication> birthRegistrationApplicationMap = new LinkedHashMap<>(); while (rs.next()){ String uuid = rs.getString("bapplicationnumber"); BirthRegistrationApplication birthRegistrationApplication = birthRegistrationApplicationMap.get(uuid); if(birthRegistrationApplication == null) { Long lastModifiedTime = rs.getLong("blastModifiedTime"); if (rs.wasNull()) { lastModifiedTime = null; } Applicant applicant = Applicant.builder().id(rs.getString("bapplicantid")).build(); AuditDetails auditdetails = AuditDetails.builder() .createdBy(rs.getString("bcreatedBy")) .createdTime(rs.getLong("bcreatedTime")) .lastModifiedBy(rs.getString("blastModifiedBy")) .lastModifiedTime(lastModifiedTime) .build(); birthRegistrationApplication = BirthRegistrationApplication.builder() .applicationNumber(rs.getString("bapplicationnumber")) .tenantId(rs.getString("btenantid")) .id(rs.getString("bid")) .assemblyConstituency(rs.getString("bassemblyconstituency")) .dateSinceResidence(rs.getInt("bdatesinceresidence")) .applicant(applicant) .auditDetails(auditdetails) .build(); } addChildrenToProperty(rs, birthRegistrationApplication); birthRegistrationApplicationMap.put(uuid, birthRegistrationApplication); } return new ArrayList<>(birthRegistrationApplicationMap.values()); } private void addChildrenToProperty(ResultSet rs, BirthRegistrationApplication birthRegistrationApplication) throws SQLException { addAddressToApplication(rs, birthRegistrationApplication); } private void addAddressToApplication(ResultSet rs, BirthRegistrationApplication birthRegistrationApplication) throws SQLException { Address address = Address.builder() .id(rs.getString("aid")) .tenantId(rs.getString("atenantid")) .doorNo(rs.getString("adoorno")) .latitude(rs.getDouble("alatitude")) .longitude(rs.getDouble("alongitude")) .buildingName(rs.getString("abuildingname")) .addressId(rs.getString("aaddressid")) .addressNumber(rs.getString("aaddressnumber")) .type(rs.getString("atype")) .addressLine1(rs.getString("aaddressline1")) .addressLine2(rs.getString("aaddressline2")) .landmark(rs.getString("alandmark")) .street(rs.getString("astreet")) .city(rs.getString("acity")) .pincode(rs.getString("apincode")) .detail("adetail") .registrationId("aregistrationid") .build(); birthRegistrationApplication.setAddress(address); } }
iv) Finally, create a class by the name of BirthRegistrationRepository
under repository
folder and annotate it with @Repository
annotation. Put the following content into the class -
@Slf4j @Repository public class BirthRegistrationRepository { @Autowired private BirthApplicationQueryBuilder queryBuilder; @Autowired private JdbcTemplate jdbcTemplate; @Autowired private BirthApplicationRowMapper rowMapper; public List<BirthRegistrationApplication> getApplications(BirthApplicationSearchCriteria searchCriteria){ List<Object> preparedStmtList = new ArrayList<>(); String query = queryBuilder.getBirthApplicationSearchQuery(searchCriteria, preparedStmtList); log.info("Final query: " + query); return jdbcTemplate.query(query, preparedStmtList.toArray(), rowMapper); } }
Producer:
Producer classes help in pushing data from the application to kafka topics. For this, we have a custom implementation of KafkaTemplate class in our tracer library called CustomKafkaTemplate. This implementation of producer class does not change across services of DIGIT. Producer implementation can be viewed here - Producer Implementation
Now, for adding producer support in this guide, the following steps need to be followed -
i) Update tracer version in pom.xml to 2.0.0-SNAPSHOT
ii) Create a producer
folder and add a new class to it by the name of Producer
. Add the following code the this class -
@Service @Slf4j public class Producer { @Autowired private CustomKafkaTemplate<String, Object> kafkaTemplate; public void push(String topic, Object value) { kafkaTemplate.send(topic, value); } }
Consumers:
Customized SMS creation: Once an application is created/updated the data is pushed on kafka topic. We trigger notification by consuming data from this topic. Whenever any message is consumed the service will call the localisation service to fetch the SMS template. It will then replace the placeholders in the SMS template with the values in the message it consumed(For example: It will replace the {NAME} placeholder with owner name from the data consumed). Once the SMS text is ready, the service will push this data(Create the SMSRequest object which is part of common modules and push the object) on notification topic. (SMS service consumes data from notification topic and triggers SMS).
For our guide, we will be implementing a notification consumer in the following section.
Notification:
Once an application is created/requested or progresses further in the workflow, notifications can be triggered as each of these events are pushed onto kafka topics which can be listened on and a sms/email/in-app notification can be sent to the concerned user(s).
For our guide, we will be implementing a notification consumer which will listen onto the topic on which birth registration applications are created, create a customized message and send it to the notification service(sms/email) to be sent to the concerned users.
@Component @Slf4j public class NotificationConsumer { @Autowired private ObjectMapper mapper; @Autowired private NotificationService notificationService; @KafkaListener(topics = {"${egov.bt.registration.create.topic}"}) public void listen(final HashMap<String, Object> record, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) { try { BirthRegistrationRequest request = mapper.convertValue(record, BirthRegistrationRequest.class); //log.info(request.toString()); notificationService.prepareEventAndSend(request); } catch (final Exception e) { log.error("Error while listening to value: " + record + " on topic: " + topic + ": ", e); } } }
Create a POJO by the name of SMSRequest under models folder and add the following content into it -
@Getter @AllArgsConstructor @NoArgsConstructor @Builder @ToString public class SMSRequest { private String mobileNumber; private String message; }
Next, to handle preparation of customized message and pushing the notification we will create a class by the name of NotificationService
under service
folder. Annotate it with @Service
annotation and add the following content to it -
@Slf4j @Service public class NotificationService { @Autowired private Producer producer; @Autowired private BTRConfiguration config; @Autowired private RestTemplate restTemplate; private static final String smsTemplate = "Dear {NAME}, your birth registration application has been successfully created on the system with application number - {APPNUMBER}."; public void prepareEventAndSend(BirthRegistrationRequest request){ List<SMSRequest> smsRequestList = new ArrayList<>(); request.getBirthRegistrationApplications().forEach(application -> { SMSRequest smsRequest = SMSRequest.builder().mobileNumber(application.getApplicant().getMobileNumber()).message(getCustomMessage(smsTemplate, application)).build(); smsRequestList.add(smsRequest); }); for (SMSRequest smsRequest : smsRequestList) { producer.push(config.getSmsNotificationTopic(), smsRequest); log.info("Messages: " + smsRequest.getMessage()); } } private String getCustomMessage(String template, BirthRegistrationApplication application) { template = template.replace("{APPNUMBER}", application.getApplicationNumber()); template = template.replace("{NAME}", application.getApplicant().getName()); return template; } }
Persister:
Persister service listens onto the topics on which the services push their records and based on the persister configuration provided it persists data into our relational datastore i.e. PostgreSQL.
Payment Backupdate:
Once payment is done the application status has to be updated. Since we have a microservice architecture the two services can communicate with each other either through API calls or using message queues(kafka in our case). To avoid any service specific code in collection service we use the second approach to notify the service of payment for its application. Whenever a payment is done the collection service will publish the payment details on a kafka topic. Any microservice which wants to get notified when payments are done can subscribe to this topic. Once the service consumes the payment message it will check if the payment is done for its service by checking the businessService code. If it is done for the given service it will update the application status to PAID or will trigger workflow action PAY depending on the use case.
For our guide, we will follow the following steps to create payment backupdate consumer -
i) Create a consumer class by the name of PaymentBackUpdateConsumer
. Annotate it with @Component
annotation and add the following content to it -
@Component public class PaymentBackUpdateConsumer { @Autowired private PaymentUpdateService paymentUpdateService; @KafkaListener(topics = {"${kafka.topics.receipt.create}"}) public void listenPayments(final HashMap<String, Object> record, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) { paymentUpdateService.process(record); } }
ii) Next, under service
folder create a new class by the name of PaymentUpdateService
and annotate it with @Service
. Put the following content in this class -
@Slf4j @Service public class PaymentUpdateService { @Autowired private WorkflowService workflowService; @Autowired private ObjectMapper mapper; @Autowired private BirthRegistrationRepository repository; public void process(HashMap<String, Object> record) { try { PaymentRequest paymentRequest = mapper.convertValue(record, PaymentRequest.class); RequestInfo requestInfo = paymentRequest.getRequestInfo(); List<PaymentDetail> paymentDetails = paymentRequest.getPayment().getPaymentDetails(); String tenantId = paymentRequest.getPayment().getTenantId(); for (PaymentDetail paymentDetail : paymentDetails) { updateWorkflowForBirthRegistrationPayment(requestInfo, tenantId, paymentDetail); } } catch (Exception e) { log.error("KAFKA_PROCESS_ERROR:", e); } } private void updateWorkflowForBirthRegistrationPayment(RequestInfo requestInfo, String tenantId, PaymentDetail paymentDetail) { Bill bill = paymentDetail.getBill(); BirthApplicationSearchCriteria criteria = BirthApplicationSearchCriteria.builder() .applicationNumber(bill.getConsumerCode()) .tenantId(tenantId) .build(); List<BirthRegistrationApplication> birthRegistrationApplicationList = repository.getApplications(criteria); if (CollectionUtils.isEmpty(birthRegistrationApplicationList)) throw new CustomException("INVALID RECEIPT", "No applications found for the consumerCode " + criteria.getApplicationNumber()); Role role = Role.builder().code("SYSTEM_PAYMENT").tenantId(tenantId).build(); requestInfo.getUserInfo().getRoles().add(role); birthRegistrationApplicationList.forEach( application -> { BirthRegistrationRequest updateRequest = BirthRegistrationRequest.builder().requestInfo(requestInfo) .birthRegistrationApplications(Collections.singletonList(application)).build(); ProcessInstanceRequest wfRequest = workflowService.getProcessInstanceForBirthRegistrationPayment(updateRequest); State state = workflowService.callWorkFlow(wfRequest); }); } }
iii) Create the following POJOs under models folder -
@Data @NoArgsConstructor @AllArgsConstructor public class PaymentRequest { @NotNull @Valid @JsonProperty("RequestInfo") private RequestInfo requestInfo; @NotNull @Valid @JsonProperty("Payment") private Payment payment; }
@Data @NoArgsConstructor @AllArgsConstructor @Builder @EqualsAndHashCode public class Payment { @Size(max = 64) @JsonProperty("id") private String id; @NotNull @Size(max = 64) @JsonProperty("tenantId") private String tenantId; @JsonProperty("totalDue") private BigDecimal totalDue; @NotNull @JsonProperty("totalAmountPaid") private BigDecimal totalAmountPaid; @Size(max = 128) @JsonProperty("transactionNumber") private String transactionNumber; @JsonProperty("transactionDate") private Long transactionDate; @NotNull @JsonProperty("paymentMode") private String paymentMode; @JsonProperty("instrumentDate") private Long instrumentDate; @Size(max = 128) @JsonProperty("instrumentNumber") private String instrumentNumber; @JsonProperty("instrumentStatus") private String instrumentStatus; @Size(max = 64) @JsonProperty("ifscCode") private String ifscCode; @JsonProperty("auditDetails") private AuditDetails auditDetails; @JsonProperty("additionalDetails") private JsonNode additionalDetails; @JsonProperty("paymentDetails") @Valid private List<PaymentDetail> paymentDetails; @Size(max = 128) @NotNull @JsonProperty("paidBy") private String paidBy; @Size(max = 64) @NotNull @JsonProperty("mobileNumber") private String mobileNumber; @Size(max = 128) @JsonProperty("payerName") private String payerName; @Size(max = 1024) @JsonProperty("payerAddress") private String payerAddress; @Size(max = 64) @JsonProperty("payerEmail") private String payerEmail; @Size(max = 64) @JsonProperty("payerId") private String payerId; @JsonProperty("paymentStatus") private String paymentStatus; public Payment addpaymentDetailsItem(PaymentDetail paymentDetail) { if (this.paymentDetails == null) { this.paymentDetails = new ArrayList<>(); } this.paymentDetails.add(paymentDetail); return this; } }
@Data @NoArgsConstructor @AllArgsConstructor @Builder @EqualsAndHashCode public class PaymentDetail { @Size(max = 64) @JsonProperty("id") private String id; @Size(max = 64) @JsonProperty("tenantId") private String tenantId; @JsonProperty("totalDue") private BigDecimal totalDue; @NotNull @JsonProperty("totalAmountPaid") private BigDecimal totalAmountPaid; @Size(max = 64) @JsonProperty("receiptNumber") private String receiptNumber; @Size(max = 64) @JsonProperty("manualReceiptNumber") private String manualReceiptNumber; @JsonProperty("manualReceiptDate") private Long manualReceiptDate; @JsonProperty("receiptDate") private Long receiptDate; @JsonProperty("receiptType") private String receiptType; @JsonProperty("businessService") private String businessService; @NotNull @Size(max = 64) @JsonProperty("billId") private String billId; @JsonProperty("bill") private Bill bill; @JsonProperty("additionalDetails") private JsonNode additionalDetails; @JsonProperty("auditDetails") private AuditDetails auditDetails; }
@Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class Bill { @JsonProperty("id") private String id; @JsonProperty("mobileNumber") private String mobileNumber; @JsonProperty("paidBy") private String paidBy; @JsonProperty("payerName") private String payerName; @JsonProperty("payerAddress") private String payerAddress; @JsonProperty("payerEmail") private String payerEmail; @JsonProperty("payerId") private String payerId; @JsonProperty("status") private StatusEnum status; @JsonProperty("reasonForCancellation") private String reasonForCancellation; @JsonProperty("isCancelled") private Boolean isCancelled; @JsonProperty("additionalDetails") private JsonNode additionalDetails; @JsonProperty("billDetails") @Valid private List<BillDetail> billDetails; @JsonProperty("tenantId") private String tenantId; @JsonProperty("auditDetails") private AuditDetails auditDetails; @JsonProperty("collectionModesNotAllowed") private List<String> collectionModesNotAllowed; @JsonProperty("partPaymentAllowed") private Boolean partPaymentAllowed; @JsonProperty("isAdvanceAllowed") private Boolean isAdvanceAllowed; @JsonProperty("minimumAmountToBePaid") private BigDecimal minimumAmountToBePaid; @JsonProperty("businessService") private String businessService; @JsonProperty("totalAmount") private BigDecimal totalAmount; @JsonProperty("consumerCode") private String consumerCode; @JsonProperty("billNumber") private String billNumber; @JsonProperty("billDate") private Long billDate; @JsonProperty("amountPaid") private BigDecimal amountPaid; public enum StatusEnum { ACTIVE("ACTIVE"), CANCELLED("CANCELLED"), PAID("PAID"), EXPIRED("EXPIRED"); private String value; StatusEnum(String value) { this.value = value; } @Override @JsonValue public String toString() { return String.valueOf(value); } public static boolean contains(String test) { for (StatusEnum val : StatusEnum.values()) { if (val.name().equalsIgnoreCase(test)) { return true; } } return false; } @JsonCreator public static StatusEnum fromValue(String text) { for (StatusEnum b : StatusEnum.values()) { if (String.valueOf(b.value).equals(text)) { return b; } } return null; } } public Boolean addBillDetail(BillDetail billDetail) { if (CollectionUtils.isEmpty(billDetails)) { billDetails = new ArrayList<>(); return billDetails.add(billDetail); } else { if (!billDetails.contains(billDetail)) return billDetails.add(billDetail); else return false; } } }
@Setter @Getter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(of = { "id" }) public class BillDetail { @JsonProperty("id") private String id; @JsonProperty("tenantId") private String tenantId; @JsonProperty("demandId") private String demandId; @JsonProperty("billId") private String billId; @JsonProperty("amount") @NotNull private BigDecimal amount; @JsonProperty("amountPaid") private BigDecimal amountPaid; @NotNull @JsonProperty("fromPeriod") private Long fromPeriod; @NotNull @JsonProperty("toPeriod") private Long toPeriod; @JsonProperty("additionalDetails") private JsonNode additionalDetails; @JsonProperty("channel") private String channel; @JsonProperty("voucherHeader") private String voucherHeader; @JsonProperty("boundary") private String boundary; @JsonProperty("manualReceiptNumber") private String manualReceiptNumber; @JsonProperty("manualReceiptDate") private Long manualReceiptDate; @JsonProperty("billAccountDetails") private List<BillAccountDetail> billAccountDetails; @NotNull @JsonProperty("collectionType") private String collectionType; @JsonProperty("auditDetails") private AuditDetails auditDetails; private String billDescription; @NotNull @JsonProperty("expiryDate") private Long expiryDate; public Boolean addBillAccountDetail(BillAccountDetail billAccountDetail) { if (CollectionUtils.isEmpty(billAccountDetails)) { billAccountDetails = new ArrayList<>(); return billAccountDetails.add(billAccountDetail); } else { if (!billAccountDetails.contains(billAccountDetail)) return billAccountDetails.add(billAccountDetail); else return false; } } }
@Setter @Getter @ToString @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class BillAccountDetail { @Size(max = 64) @JsonProperty("id") private String id; @Size(max = 64) @JsonProperty("tenantId") private String tenantId; @Size(max = 64) @JsonProperty("billDetailId") private String billDetailId; @Size(max = 64) @JsonProperty("demandDetailId") private String demandDetailId; @JsonProperty("order") private Integer order; @JsonProperty("amount") private BigDecimal amount; @JsonProperty("adjustedAmount") private BigDecimal adjustedAmount; @JsonProperty("isActualDemand") private Boolean isActualDemand; @Size(max = 64) @JsonProperty("taxHeadCode") private String taxHeadCode; @JsonProperty("additionalDetails") private JsonNode additionalDetails; @JsonProperty("auditDetails") private AuditDetails auditDetails; }
Persister configurations:
The persister configuration is written in a YAML format. The INSERT and UPDATE queries for each table is added in prepared Statement format, followed by the jsonPaths of values which has to be inserted/updated.
For example, for a table named studentinfo with id, name, age, marks fields, the following configuration will get the persister ready to insert data into studentinfo table -
serviceMaps: serviceName: student-management-service mappings: - version: 1.0 description: Persists student details in studentinfo table fromTopic: save-student-info isTransaction: true queryMaps: - query: INSERT INTO studentinfo( id, name, age, marks) VALUES (?, ?, ?, ?); basePath: Students.* jsonMaps: - jsonPath: $.Students.*.id - jsonPath: $.Students.*.name - jsonPath: $.Students.*.age - jsonPath: $.Students.*.marks
For our guide, for adding persister config, the following steps need to be followed -
i) Clone configs repo locally.
git clone -o upstream https://github.com/egovernments/configs
ii) Create a file by the name of digit-developer-guide.yml
under persister configs folder.
iii) Add the following content into it -
serviceMaps: serviceName: btr-services mappings: - version: 1.0 description: Persists birth details in tables fromTopic: save-bt-application isTransaction: true queryMaps: - query: INSERT INTO eg_bt_registration(id,tenantid,babyFirstName,applicationnumber,babyLastName,motherName,fatherName,doctorAttendingBirth,hospitalName,placeOfBirth,dateOfBirth,createdtime, lastmodifiedtime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); basePath: BirthRegistrationApplications.* jsonMaps: - jsonPath: $.BirthRegistrationApplications.*.id - jsonPath: $.BirthRegistrationApplications.*.tenantId - jsonPath: $.BirthRegistrationApplications.*.babyFirstName - jsonPath: $.BirthRegistrationApplications.*.applicationNumber - jsonPath: $.BirthRegistrationApplications.*.babyLastName - jsonPath: $.BirthRegistrationApplications.*.motherName - jsonPath: $.BirthRegistrationApplications.*.fatherName - jsonPath: $.BirthRegistrationApplications.*.doctorAttendingBirth - jsonPath: $.BirthRegistrationApplications.*.hospitalName - jsonPath: $.BirthRegistrationApplications.*.placeOfBirth - jsonPath: $.BirthRegistrationApplications.*.dateOfBirth - jsonPath: $.BirthRegistrationApplications.*.auditDetails.createdTime - jsonPath: $.BirthRegistrationApplications.*.auditDetails.lastModifiedTime - query: INSERT INTO eg_bt_address(id, tenantid, doorno, latitude, longitude, buildingname, addressid, addressnumber, type, addressline1, addressline2, landmark, street, city, locality, pincode, detail, registrationid, createdby, lastmodifiedby, createdtime, lastmodifiedtime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); basePath: BirthRegistrationApplications.* jsonMaps: - jsonPath: $.BirthRegistrationApplications.*.address.id - jsonPath: $.BirthRegistrationApplications.*.address.tenantId - jsonPath: $.BirthRegistrationApplications.*.address.doorNo - jsonPath: $.BirthRegistrationApplications.*.address.latitude - jsonPath: $.BirthRegistrationApplications.*.address.longitude - jsonPath: $.BirthRegistrationApplications.*.address.buildingName - jsonPath: $.BirthRegistrationApplications.*.address.addressId - jsonPath: $.BirthRegistrationApplications.*.address.addressNumber - jsonPath: $.BirthRegistrationApplications.*.address.type - jsonPath: $.BirthRegistrationApplications.*.address.addressLine1 - jsonPath: $.BirthRegistrationApplications.*.address.addressLine2 - jsonPath: $.BirthRegistrationApplications.*.address.landmark - jsonPath: $.BirthRegistrationApplications.*.address.street - jsonPath: $.BirthRegistrationApplications.*.address.city - jsonPath: $.BirthRegistrationApplications.*.address.locality.name - jsonPath: $.BirthRegistrationApplications.*.address.pincode - jsonPath: $.BirthRegistrationApplications.*.address.detail - jsonPath: $.BirthRegistrationApplications.*.address.registrationId - jsonPath: $.BirthRegistrationApplications.*.address.createdBy - jsonPath: $.BirthRegistrationApplications.*.address.lastModifiedBy - jsonPath: $.BirthRegistrationApplications.*.address.createdTime - jsonPath: $.BirthRegistrationApplications.*.address.lastModifiedTime - version: 1.0 description: Update birth registration applications in table fromTopic: update-bt-application isTransaction: true queryMaps: - query: UPDATE eg_bt_registration SET tenantid = ?,babyFirstName = ?, dateOfBirth = ? WHERE id=?; basePath: BirthRegistrationApplications.* jsonMaps: - jsonPath: $.BirthRegistrationApplications.*.tenantId - jsonPath: $.BirthRegistrationApplications.*.babyFirstName - jsonPath: $.BirthRegistrationApplications.*.dateOfBirth - jsonPath: $.BirthRegistrationApplications.*.id
Indexer Configuration:
Indexer is designed to perform all the indexing tasks of the digit platform. The service reads records posted on specific kafka topics and picks the corresponding index configuration from the yaml file provided by the respective module configuration. Configurations are yaml based. Detailed guide to create indexer configs are mentioned in the following document - Indexer Configuration Guide .
For our guide, we will create a new file under egov-indexer in configs repo by the name of digit-developer-guide.yml
and put the following content into it -
ServiceMaps: serviceName: Birth Registration Service version: 1.0.0 mappings: - topic: save-bt-application configKey: INDEX indexes: - name: btindex-v1 type: general id: $.id isBulk: true timeStampField: $.auditDetails.createdTime jsonPath: $.BirthRegistrationApplications customJsonMapping: indexMapping: {"Data":{"birthapplication":{},"history":{}}} fieldMapping: - inJsonPath: $ outJsonPath: $.Data.birthapplication externalUriMapping: - path: http://localhost:8282/egov-workflow-v2/egov-wf/process/_search queryParam: businessIds=$.applicationNumber,history=true,tenantId=$.tenantId apiRequest: {"RequestInfo":{"apiId":"org.egov.pt","ver":"1.0","ts":1502890899493,"action":"asd","did":"4354648646","key":"xyz","msgId":"654654","requesterId":"61","authToken":"d9994555-7656-4a67-ab3a-a952a0d4dfc8","userInfo":{"id":1,"uuid":"1fec8102-0e02-4d0a-b283-cd80d5dab067","type":"EMPLOYEE","tenantId":"pb.amritsar","roles":[{"name":"Employee","code":"EMPLOYEE","tenantId":"pb.amritsar"}]}}} uriResponseMapping: - inJsonPath: $.ProcessInstances outJsonPath: $.Data.history - topic: update-bt-application configKey: INDEX indexes: - name: btindex-v1 type: general id: $.id isBulk: true timeStampField: $.auditDetails.createdTime jsonPath: $.BirthRegistrationApplications customJsonMapping: indexMapping: {"Data":{"tradelicense":{},"history":{}}} fieldMapping: - inJsonPath: $ outJsonPath: $.Data.tradelicense externalUriMapping: - path: http://localhost:8282/egov-workflow-v2/egov-wf/process/_search queryParam: businessIds=$.applicationNumber,history=true,tenantId=$.tenantId apiRequest: {"RequestInfo":{"apiId":"org.egov.pt","ver":"1.0","ts":1502890899493,"action":"asd","did":"4354648646","key":"xyz","msgId":"654654","requesterId":"61","authToken":"d9994555-7656-4a67-ab3a-a952a0d4dfc8","userInfo":{"id":1,"uuid":"1fec8102-0e02-4d0a-b283-cd80d5dab067","type":"EMPLOYEE","tenantId":"pb.amritsar","roles":[{"name":"Employee","code":"EMPLOYEE","tenantId":"pb.amritsar"}]}}} uriResponseMapping: - inJsonPath: $.ProcessInstances outJsonPath: $.Data.history
Certificate Generation:
The final step in this process is the creation of configs to create a birth registration PDF for the citizens to download. For this, we will make use of DIGIT’s PDF service which uses PDFMake and Mustache libraries to generate PDF. A detailed documentation on PDF service and generating PDFs using PDF service can be found here - PDF Generation Service.
For our guide, we will follow the following steps to set up PDF service locally and generate PDF for our birth registration service -
i) Clone DIGIT Services repo.
> git clone -o upstream https://github.com/egovernments/DIGIT-Dev
ii) Clone Configs repo.
> git clone -o upstream https://github.com/egovernments/configs
iii) Now, go into the DIGIT-Dev repo and open up a terminal. Checkout DIGIT_DEVELOPER_GUIDE branch.
> cd DIGIT-Dev
> git checkout DIGIT_DEVELOPER_GUIDE
iv) Now, go inside configs folder and under pdf-service
data config and format config folders, create file by the name of digit-developer-guide.json
> cd configs/pdf-service/data-config
> touch digit-developer-guide.json
Add the following content in this newly created data config file -
{ "key": "btcertificate", "DataConfigs": { "serviceName": "rainmaker-common", "version": "1.0.0", "baseKeyPath": "$.BirthRegistrationApplications.*", "entityIdPath":"$.id", "isCommonTableBorderRequired": true, "mappings": [ { "mappings": [ { "direct": [ { "variable": "logoImage", "url":"https://raw.githubusercontent.com/egovernments/egov-web-app/master/web/rainmaker/dev-packages/egov-ui-kit-dev/src/assets/images/pblogo.png", "type":"image" }, { "variable": "applicantName", "value": { "path": "$.applicant.name" } }, { "variable": "applicationNo", "value": { "path": "$.applicationNumber" } }, { "variable": "address", "value": { "path": "$.address.city" } }, { "variable": "birthIdIssueDate", "value": { "path": "$.auditDetails.createdTime" }, "type": "date" }, { "variable": "signedCertificateData", "value": { "path": "$.signedCertificate" } }, { "variable": "to", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_RECEIPT_TO" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "municipal_corportaion", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_MUNICIPAL_CORPORATION" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "corporation_contact", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_LICENSE_CORPORATION_CONTACT" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "corporation_website", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_LICENSE_CORPORATION_WEBSITE" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "corporation_email", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_LICENSE_CORPORATION_EMAIL" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "application_no", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_APPLICATION_NO" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "reciept_no", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_RECIEPT_NO" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "financial_year", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_FINANCIAL_YEAR" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "trade_name", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_TRADE_NAME" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "trade_owner_name", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_TRADE_OWNER_NAME" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "trade_owner_contact", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_TRADE_OWNER_CONTACT" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "trade_address", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_TRADE_ADDRESS" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "trade_type", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_TRADE_TYPE" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "accessories_label", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_ACCESSORIES_LABEL" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "trade_license_fee", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_TRADE_LICENSE_FEE" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "license_issue_date", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_LICENSE_ISSUE_DATE" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "license_validity", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_LICENSE_VALIDITY" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "approved_by", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_APPROVED_BY" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } }, { "variable": "commissioner", "value": { "path": "PDF_STATIC_LABEL_CONSOLIDATED_TLCERTIFICATE_COMMISSIONER" }, "type": "label", "localisation":{ "required":true, "prefix": null, "module":"rainmaker-common" } } ] }, { "externalAPI": [ { "path": "http://localhost:8082/egov-mdms-service/v1/_get", "queryParam": "moduleName=tenant&masterName=tenants&tenantId=pb&filter=%5B?(@.code=='{$.tenantId}')%5D", "apiRequest": null, "responseMapping":[ { "variable":"ulb-address", "value":"$.MdmsRes.tenant.tenants[0].address" }, { "variable":"corporationContact", "value":"$.MdmsRes.tenant.tenants[0].contactNumber" }, { "variable":"corporationWebsite", "value":"$.MdmsRes.tenant.tenants[0].domainUrl" }, { "variable":"corporationEmail", "value":"$.MdmsRes.tenant.tenants[0].emailId" } ] }, { "path": "http://localhost:8288/filestore/v1/files/url", "queryParam": "tenantId=pb,fileStoreIds=$.tradeLicenseDetail.applicationDocuments[?(@.documentType== 'OWNERPHOTO')].fileStoreId", "apiRequest": null, "requesttype": "GET", "responseMapping":[ { "variable":"userpic", "value":"$.fileStoreIds[0].url", "type": "image" } ] }, { "path": "http://localhost:8282/egov-workflow-v2/egov-wf/process/_search", "queryParam": "businessIds=$.applicationNumber,history=true,tenantId=$.tenantId", "apiRequest": null, "responseMapping":[ { "variable":"approvedBy", "value":"$.ProcessInstances[?(@.action == 'APPROVE')].assigner.name" } ] } ] }, { "qrcodeConfig": [ { "variable": "qrCode", "value": "{{signedCertificateData}}" } ] } ] } ] } }
v) Now, under format-config folder, again create a file by the name of digit-developer-guide.json
and put the following content into it -
{ "key": "vtcertificate", "config": { "defaultStyle": { "font": "Cambay" }, "content": [ { "image": "{{qrCode}}", "absolutePosition" : { "x" : 480, "y" : 5 }, "width": 100, "height": 100 }, { "style":"noc-head", "table":{ "widths":[ "*" ], "body":[ [ { "image": "{{userpic}}", "width": 70, "height": 82, "alignment": "center", "margin": [ 0, 10, 0, 10 ] } ], [ { "stack": [ { "text":"{{municipal_corportaion}}", "style":"receipt-logo-header" }, { "text":"{{ulb-address}}", "style":"receipt-logo-sub-header" }, { "style": "noc-head", "table": { "widths": [ "*", "*" ], "body": [ [ { "text": "{{corporation_contact}} : {{corporationContact}} ", "style": "receipt-sub-address-sub-header" } ] ] }, "layout": "noBorders" }, { "style": "noc-head", "table": { "widths": [ "*", "*" ], "body": [ [ { "text": "{{corporation_website}} : {{corporationWebsite}}", "style": "receipt-sub-website-sub-header" } ] ] }, "layout": "noBorders" }, { "style": "noc-head", "table": { "widths": [ "*", "*" ], "body": [ [ { "text": "{{corporation_email}} : {{corporationEmail}}", "style": "receipt-sub-email-sub-header" } ] ] }, "layout": "noBorders" } ], "alignment":"left", "margin":[ 0, 10, 0, 0 ] } ], [ { "stack": [ { "text":"Birth ID Certificate", "style":"receipt-sub-logo-header" }, { "style":"noc-head", "table":{ "widths":[ "35%", "65%" ], "body":[ [ { "text":"Applicant Name", "style":"receipt-sub-logo-sub-header" }, { "text":"{{applicantName}}", "style":"receipt-sub-logo-sub-header" } ] ] }, "layout":"noBorders" }, { "style":"noc-head", "table":{ "widths":[ "35%", "65%" ], "body":[ [ { "text":"Application Number", "style":"receipt-sub-logo-sub-header" }, { "text":"{{applicationNo}}", "style":"receipt-sub-logo-sub-header" } ] ] }, "layout":"noBorders" }, { "style":"noc-head", "table":{ "widths":[ "35%", "65%" ], "body":[ [ { "text":"Applicant Address", "style":"receipt-sub-logo-sub-header" }, { "text":"{{address}}", "style":"receipt-sub-logo-sub-header" } ] ] }, "layout":"noBorders" }, { "style":"noc-head", "table":{ "widths":[ "35%", "65%" ], "body":[ [ { "text":"Birth ID issue date", "style":"receipt-sub-logo-sub-header" }, { "text":"{{birthIdIssueDate}}", "style":"receipt-sub-logo-sub-header" } ] ] }, "layout":"noBorders" } ], "alignment":"left", "margin":[ 0, 10, 0, 0 ] } ] ] }, "layout":"noBorders" }, { "style":"receipt-approver", "columns": [ { "text":[ { "text":"{{approved_by}} ", "bold": true }, { "text":" {{approvedBy}}", "bold": false } ], "alignment":"left" }, { "text":[ { "text":"{{commissioner}}", "bold": true } ], "alignment":"right" } ] } ], "styles": { "noc-head": { "margin": [ -30, -35, 0, -2 ] }, "receipt-approver": { "color": "#000000", "fontSize": 14, "letterSpacing": 0.6, "alignment": "center", "margin": [ -10, 50, 0, 1 ] }, "receipt-logo-header": { "color": "#000000", "fontSize": 20, "letterSpacing": 0.74, "alignment": "center", "margin": [ 0, 0, 0, 5 ] }, "receipt-sub-logo-header": { "color": "#000000", "fontSize": 18, "letterSpacing": 0.74, "alignment": "center", "margin": [ 0, 5, 0, 5 ] }, "receipt-logo-sub-header": { "color": "#484848", "fontSize": 14, "letterSpacing": 0.6, "alignment": "center", "margin": [ 0, 5, 0, 0 ] }, "receipt-sub-logo-sub-header": { "color": "#484848", "fontSize": 14, "letterSpacing": 0.6, "alignment": "left", "margin": [ 50, 40, 0, 0 ] }, "receipt-sub-address-sub-header": { "color": "#484848", "fontSize": 14, "letterSpacing": 0.1, "alignment": "right", "margin": [ 50, 30, -90, 0 ] }, "receipt-sub-website-sub-header": { "color": "#484848", "fontSize": 14, "letterSpacing": 0.1, "alignment": "right", "margin": [ 50, 30, -120, 0 ] }, "receipt-sub-email-sub-header": { "color": "#484848", "fontSize": 14, "letterSpacing": 0.1, "alignment": "right", "margin": [ 50, 30, -110, 0 ] } } } }
vi) Now, open PDF service (under core-services repository of DIGIT-Dev) on your IDE. Open Environment.js
file and change the following properties to point to the local config files that have been created. For example, in my local setup I have pointed these to the local files that I created -
DATA_CONFIG_URLS: "file:///eGov/configs/pdf-service/data-config/digit-developer-guide.json", FORMAT_CONFIG_URLS: "file:///eGov/configs/pdf-service/format-config/digit-developer-guide.json"
vii) Now, make sure that kafka and workflow services are running locally and port-forward the following services -
egov-user to port 8284
egov-localization to port 8286
egov-filestore to 8288
egov-mdms to 8082
viii) PDF service is now ready to be started up. Execute the following commands to start it up -
> npm install
> npm run dev
ix) Once PDF service is up hit the following CURL to look at the created PDF -
To be added
Congratulations for making it through this guide. Once all the dependent services are configured/integrated, it is time to test our completed application!
To run and test our sample application, the following steps need to be followed -
Run kafka, workflow, pdf service locally and the code of DIGIT_DEVELOPER_GUIDE branch (for consistency).
Port-forward the following services -
a. egov-user to port 8284
b. egov-localization to port 8286
c. egov-filestore to 8288
d. egov-mdms to 8082
Run
birth-registration-service
that we just created.Import the postman collection of the API’s that this sample service exposes from here - Voter Registration Postman Collection
Hit the _create API request to create a birth registration application.
Hit the _search API request to search for the created birth registration applications.
Hit _update API request to update your application or transition it further in the workflow by changing the actions.
Add Comment