Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ To maintain a clean and professional structure, all deep-dive documentation has
- [**Deployment & Environments**](./docs/DEPLOYMENT_GUIDE.md): How to run the app in Local, Docker, and Cloud.
- [**Tax Engine Standards**](./docs/TAX_ENGINE_STANDARDS.md): Logical breakdown of Indian Tax laws and our Strategy implementation.
- [**Quality Standards**](./docs/QUALITY_STANDARDS.md): Guide to Checkstyle, PMD, and SpotBugs integration.
- [**API Test Plan**](./docs/API_TEST_PLAN.md): Manual verification steps and Postman payloads.
- [**Java 21 Migration Guide**](./docs/JAVA_21_MIGRATION_GUIDE.md): Technical checklist of features migrated (Records, Switch Expressions, etc.).


---

## 🚀 Quick Start
Expand All @@ -17,8 +19,9 @@ To maintain a clean and professional structure, all deep-dive documentation has
cd docker
docker-compose up -d --build
```
- People Service: `http://localhost:8080`
- Tax Engine Service: `http://localhost:8081`
> **Note**: If you face database errors like `database "peopledb" does not exist`, run `docker-compose down -v` to reset the volumes and restart.


### Running Locally (Individual Services)
```bash
Expand Down Expand Up @@ -53,8 +56,9 @@ Current Phase: **Phase 2 - Intelligent Orchestration** (Completed ✅)
- [ ] **Internal Research**: Explore `RestClient` vs `OpenFeign`.

- [ ] **Observability & Ops**
- [ ] Add Spring Boot Actuator to all services.
- [ ] Standardize structured logging and Correlation IDs.
- [x] Add Spring Boot Actuator to all services.
- [x] Standardize structured logging and Correlation IDs.


- [ ] **Infrastructure**
- [x] GitHub Actions CI/CD Pipeline integration.
Expand Down
31 changes: 31 additions & 0 deletions common-lib/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,36 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>9.0</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>




Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.common.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class OpenApiConfig {

@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("People & Tax Ecosystem API")
.version("0.0.1-SNAPSHOT")
.description("API for managing people and calculating tax in the Indian context.")
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.common.exception;

import java.time.LocalDateTime;

public record ErrorResponse(
String message,
String correlationId,
int status,
LocalDateTime timestamp) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.common.exception;

import com.example.common.logging.CorrelationIdFilter;
import org.slf4j.MDC;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.time.LocalDateTime;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ControllerAdvice
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
if (logger.isErrorEnabled()) {
logger.error("Unhandled exception occurred: {}", ex.getMessage(), ex);
}
return buildErrorResponse(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Invalid argument: {}", ex.getMessage());
}
return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<ErrorResponse> handleIllegalStateException(IllegalStateException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Illegal state: {}", ex.getMessage());
}
return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST);
}

private ResponseEntity<ErrorResponse> buildErrorResponse(String message, HttpStatus status) {
String correlationId = MDC.get(CorrelationIdFilter.CORRELATION_ID_LOG_VAR);
ErrorResponse error = new ErrorResponse(
message,
correlationId,
status.value(),
LocalDateTime.now());
return new ResponseEntity<>(error, status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.common.logging;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.UUID;

@Component
public class CorrelationIdFilter implements Filter {

public static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
public static final String CORRELATION_ID_LOG_VAR = "correlationId";

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

String correlationId = httpRequest.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null || correlationId.isEmpty()) {
correlationId = UUID.randomUUID().toString();
}

MDC.put(CORRELATION_ID_LOG_VAR, correlationId);
httpResponse.setHeader(CORRELATION_ID_HEADER, correlationId);

try {
chain.doFilter(request, response);
} finally {
MDC.remove(CORRELATION_ID_LOG_VAR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.common.logging;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

@Component
public class CorrelationIdInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate template) {
String correlationId = MDC.get(CorrelationIdFilter.CORRELATION_ID_LOG_VAR);
if (correlationId != null) {
template.header(CorrelationIdFilter.CORRELATION_ID_HEADER, correlationId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.common.logging;

import com.example.common.exception.GlobalExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Import;

@AutoConfiguration
@ConditionalOnWebApplication
@Import({
CorrelationIdFilter.class,
CorrelationIdInterceptor.class,
GlobalExceptionHandler.class,
com.example.common.config.OpenApiConfig.class
})
public class ObservabilityAutoConfiguration {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.example.common.logging.ObservabilityAutoConfiguration
87 changes: 87 additions & 0 deletions docs/API_TEST_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# API Test Plan & Manual Verification

This document outlines the steps to verify the stability of the People & Tax Ecosystem APIs.

## 🔗 Swagger / OpenAPI Documentation
Once the services are running, you can access the interactive API docs at:
- **People Service**: [http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html)
- **Tax Engine Service**: [http://localhost:8081/swagger-ui/index.html](http://localhost:8081/swagger-ui/index.html)

## 🧪 1. People Management Service
**Base URL**: `http://localhost:8080`

### 1.1 Create Person (Full Time Employee)
**Endpoint**: `POST /people`
**Headers**:
- `Content-Type`: `application/json`

**Body**:
```json
{
"personType": "EMPLOYEE_FULL_TIME",
"id": 101,
"name": "Rahul Dravid",
"email": "rahul.dravid@example.com",
"annualSalary": 1200000
}
```
**Expected Response**: `201 Created`

### 1.2 Create Person (Contractor)
**Endpoint**: `POST /people`
**Body**:
```json
{
"personType": "EMPLOYEE_CONTRACTOR",
"id": 102,
"name": "Hardik Pandya",
"email": "hardik@example.com",
"hourlyRate": 2000,
"hoursWorked": 160
}
```

### 1.3 Get Person by ID
**Endpoint**: `GET /people/101`
**Expected Response**: JSON object of Rahul Dravid.

### 1.4 Get Monthly Income
**Endpoint**: `GET /people/101/income`
**Expected Response**: `100000.00` (12,00,000 / 12)

---

## 💰 2. Tax Engine Service
**Base URL**: `http://localhost:8081`

### 2.1 Calculate Tax (Standalone)
**Endpoint**: `POST /tax/calculate`
**Body**:
```json
{
"person": {
"personType": "EMPLOYEE_FULL_TIME",
"id": 999,
"name": "Richie Rich",
"email": "richie@example.com",
"annualSalary": 1500000
},
"regime": "NEW"
}
```
**Expected Response**: JSON with calculated tax breakdown.

### 2.2 Calculate Tax for Existing Person (Orchestrated)
**Endpoint**: `GET /tax/calculate/101?regime=NEW`
**Description**: Fetches Rahul Dravid (101) from People Service and calculates tax.
**Verification**: Check logs for `X-Correlation-ID` to ensure it matches across both services.

---

## 🛠️ Verification Checklist
- [x] Swagger UI loads for both services.
- [x] `POST /people` creates data successfully.
- [x] `GET /people/{id}` retrieves correct data.
- [x] `POST /tax/calculate` returns valid tax computation.
- [x] `GET /tax/calculate/{id}` works and shows orchestration success.
- [x] Logs show matching `X-Correlation-ID` for the orchestrated call.
Loading
Loading