diff --git a/README.md b/README.md index c36fb16..6a1e0f9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. diff --git a/common-lib/pom.xml b/common-lib/pom.xml index 2fd5df9..0add339 100644 --- a/common-lib/pom.xml +++ b/common-lib/pom.xml @@ -27,5 +27,36 @@ lombok true + + org.springframework.boot + spring-boot-starter-web + provided + + + net.logstash.logback + logstash-logback-encoder + 9.0 + + + + org.slf4j + slf4j-api + + + org.springframework.cloud + spring-cloud-starter-openfeign + 4.1.0 + provided + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 3.0.1 + provided + + + + + diff --git a/common-lib/src/main/java/com/example/common/config/OpenApiConfig.java b/common-lib/src/main/java/com/example/common/config/OpenApiConfig.java new file mode 100644 index 0000000..60fa3fc --- /dev/null +++ b/common-lib/src/main/java/com/example/common/config/OpenApiConfig.java @@ -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"))); + } +} diff --git a/common-lib/src/main/java/com/example/common/exception/ErrorResponse.java b/common-lib/src/main/java/com/example/common/exception/ErrorResponse.java new file mode 100644 index 0000000..1525004 --- /dev/null +++ b/common-lib/src/main/java/com/example/common/exception/ErrorResponse.java @@ -0,0 +1,10 @@ +package com.example.common.exception; + +import java.time.LocalDateTime; + +public record ErrorResponse( + String message, + String correlationId, + int status, + LocalDateTime timestamp) { +} diff --git a/common-lib/src/main/java/com/example/common/exception/GlobalExceptionHandler.java b/common-lib/src/main/java/com/example/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..9d64a75 --- /dev/null +++ b/common-lib/src/main/java/com/example/common/exception/GlobalExceptionHandler.java @@ -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 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 handleIllegalArgumentException(IllegalArgumentException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Invalid argument: {}", ex.getMessage()); + } + return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalStateException(IllegalStateException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Illegal state: {}", ex.getMessage()); + } + return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + private ResponseEntity 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); + } +} diff --git a/common-lib/src/main/java/com/example/common/logging/CorrelationIdFilter.java b/common-lib/src/main/java/com/example/common/logging/CorrelationIdFilter.java new file mode 100644 index 0000000..ae30ec6 --- /dev/null +++ b/common-lib/src/main/java/com/example/common/logging/CorrelationIdFilter.java @@ -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); + } + } +} diff --git a/common-lib/src/main/java/com/example/common/logging/CorrelationIdInterceptor.java b/common-lib/src/main/java/com/example/common/logging/CorrelationIdInterceptor.java new file mode 100644 index 0000000..ae9c1ae --- /dev/null +++ b/common-lib/src/main/java/com/example/common/logging/CorrelationIdInterceptor.java @@ -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); + } + } +} diff --git a/common-lib/src/main/java/com/example/common/logging/ObservabilityAutoConfiguration.java b/common-lib/src/main/java/com/example/common/logging/ObservabilityAutoConfiguration.java new file mode 100644 index 0000000..d115405 --- /dev/null +++ b/common-lib/src/main/java/com/example/common/logging/ObservabilityAutoConfiguration.java @@ -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 { +} diff --git a/common-lib/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/common-lib/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..ca6d378 --- /dev/null +++ b/common-lib/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.example.common.logging.ObservabilityAutoConfiguration diff --git a/docs/API_TEST_PLAN.md b/docs/API_TEST_PLAN.md new file mode 100644 index 0000000..83d8316 --- /dev/null +++ b/docs/API_TEST_PLAN.md @@ -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. diff --git a/docs/ARCHITECTURAL_BLUEPRINT.md b/docs/ARCHITECTURAL_BLUEPRINT.md index d77ba56..0150763 100644 --- a/docs/ARCHITECTURAL_BLUEPRINT.md +++ b/docs/ARCHITECTURAL_BLUEPRINT.md @@ -66,6 +66,78 @@ graph TD 4. **Computation**: The Tax Engine applies Java 21 `switch` expressions to determine the tax liability based on the latest Indian Budget slabs. 5. **Output**: Returns a detailed breakdown of Tax, Cess, Surcharge, and Savings Tips. +### 3.1 Interaction Flow (Orchestration) +The following sequence explains how a tax calculation request is enriched with live person data. + +```mermaid +sequenceDiagram + participant User + participant TaxApp as Tax Engine Service + participant PeopleApp as People Service + participant DB as PostgreSQL + + User->>TaxApp: GET /tax/calculate/{personId} (Regime: NEW) + activate TaxApp + + Note right of TaxApp: Generated Correlation ID + + TaxApp->>PeopleApp: GET /people/{id} + activate PeopleApp + PeopleApp->>DB: Fetch Person Entity + DB-->>PeopleApp: Person Data + PeopleApp-->>TaxApp: Return Person (JSON Record) + deactivate PeopleApp + + Note right of TaxApp: Orchestrator selects Strategy + TaxApp->>TaxApp: Apply Deductions (Pattern Matching) + TaxApp->>TaxApp: Calculate Tax & Cess + + TaxApp-->>User: Return TaxResult (Breakdown) + deactivate TaxApp +``` + +### 3.2 Domain Model (Sealed Hierarchy) +The core `Person` model uses Java 21 advanced features to ensure type safety and exhaustive pattern matching. + +```mermaid +classDiagram + class Person { + <> + +Long id + +String name + +String email + +BigDecimal income() + } + + class FullTimeEmployee { + <> + +BigDecimal annualSalary + } + + class Contractor { + <> + +BigDecimal hourlyRate + +int hoursWorked + } + + class SelfEmployed { + <> + +BigDecimal annualTurnover + +String profession + } + + class BusinessOwner { + <> + +BigDecimal annualBusinessTurnover + +String businessType + } + + Person <|-- FullTimeEmployee + Person <|-- Contractor + Person <|-- SelfEmployed + Person <|-- BusinessOwner +``` + --- ## 4. Java 21 Showcase Features diff --git a/DOCKER_SETUP_SUMMARY.md b/docs/DOCKER_SETUP_SUMMARY.md similarity index 100% rename from DOCKER_SETUP_SUMMARY.md rename to docs/DOCKER_SETUP_SUMMARY.md diff --git a/people-management-service/pom.xml b/people-management-service/pom.xml index f583ea9..3dd2d5a 100644 --- a/people-management-service/pom.xml +++ b/people-management-service/pom.xml @@ -37,11 +37,23 @@ spring-boot-starter-actuator - + com.h2database h2 runtime + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 3.0.1 + + + org.postgresql postgresql diff --git a/people-management-service/src/main/java/com/example/javamigrationlab/exception/GlobalExceptionHandler.java b/people-management-service/src/main/java/com/example/javamigrationlab/exception/GlobalExceptionHandler.java deleted file mode 100644 index fdd581a..0000000 --- a/people-management-service/src/main/java/com/example/javamigrationlab/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.javamigrationlab.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleRuntimeException(RuntimeException ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); - } -} diff --git a/people-management-service/src/main/java/com/example/javamigrationlab/modern/service/PersonService.java b/people-management-service/src/main/java/com/example/javamigrationlab/modern/service/PersonService.java index aa31531..0aa8573 100644 --- a/people-management-service/src/main/java/com/example/javamigrationlab/modern/service/PersonService.java +++ b/people-management-service/src/main/java/com/example/javamigrationlab/modern/service/PersonService.java @@ -3,6 +3,7 @@ import com.example.common.domain.*; import com.example.javamigrationlab.entity.PersonEntity; import com.example.javamigrationlab.repository.PersonRepository; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -10,6 +11,7 @@ import java.util.List; import java.util.stream.Collectors; +@Slf4j @Service public class PersonService { @@ -26,6 +28,7 @@ public Person createPerson(Person person) { } public Person getPerson(Long id) { + log.info("Fetching person with ID: {}", id); return personRepository.findById(id) .map(this::mapToDomain) .orElseThrow(() -> new RuntimeException("Person not found with id: " + id)); diff --git a/people-management-service/src/main/resources/application-docker.properties b/people-management-service/src/main/resources/application-docker.properties index f12785f..c435e82 100644 --- a/people-management-service/src/main/resources/application-docker.properties +++ b/people-management-service/src/main/resources/application-docker.properties @@ -1,34 +1,7 @@ -# Docker profile configuration -# This profile is activated when running in Docker containers - -# PostgreSQL Database Configuration spring.datasource.url=jdbc:postgresql://postgres:5432/peopledb spring.datasource.username=postgres spring.datasource.password=postgres spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect - -# JPA/Hibernate spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=false -spring.jpa.properties.hibernate.format_sql=true - -# Virtual Threads -spring.threads.virtual.enabled=true - -# Hikari Connection Pool -spring.datasource.hikari.maximum-pool-size=50 -spring.datasource.hikari.minimum-idle=10 -spring.datasource.hikari.connection-timeout=20000 -spring.datasource.hikari.idle-timeout=300000 -spring.datasource.hikari.max-lifetime=1200000 - -# Actuator endpoints -management.endpoints.web.exposure.include=health,info,metrics -management.endpoint.health.show-details=always -management.health.defaults.enabled=true - -# Logging -logging.level.root=INFO -logging.level.com.example.javamigrationlab=DEBUG -logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n +spring.sql.init.mode=always diff --git a/people-management-service/src/main/resources/logback-spring.xml b/people-management-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..4b3d0a4 --- /dev/null +++ b/people-management-service/src/main/resources/logback-spring.xml @@ -0,0 +1,32 @@ + + + + + + + + + UTC + + + + { + "timestamp": "%d{yyyy-MM-dd HH:mm:ss.SSS}", + "level": "%level", + "service": "people-management-service", + "correlationId": "%X{correlationId:-undefined}", + "thread": "%thread", + "logger": "%logger{36}", + "message": "%message", + "exception": "%ex" + } + + + + + + + + + + diff --git a/tax-engine-service/pom.xml b/tax-engine-service/pom.xml index 2fddb56..d97d587 100644 --- a/tax-engine-service/pom.xml +++ b/tax-engine-service/pom.xml @@ -40,6 +40,17 @@ spotbugs-annotations provided + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 3.0.1 + + + + org.projectlombok + lombok + true + diff --git a/tax-engine-service/src/main/java/com/example/tax/service/TaxCalculationService.java b/tax-engine-service/src/main/java/com/example/tax/service/TaxCalculationService.java index 6aca147..4775487 100644 --- a/tax-engine-service/src/main/java/com/example/tax/service/TaxCalculationService.java +++ b/tax-engine-service/src/main/java/com/example/tax/service/TaxCalculationService.java @@ -5,6 +5,7 @@ import com.example.tax.constants.TaxConstants; import com.example.tax.strategy.TaxRegimeStrategy; import com.example.tax.strategy.TaxStrategyFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; @@ -13,6 +14,7 @@ * Smart Tax Calculation Orchestrator. * USES Strategy Pattern and Java 21 Record Patterns for maximum flexibility. */ +@Slf4j @Service public class TaxCalculationService { @@ -23,6 +25,9 @@ public TaxCalculationService(TaxStrategyFactory strategyFactory) { } public TaxResult calculateTax(Person person, TaxRegime regime) { + if (log.isInfoEnabled()) { + log.info("Calculating tax for person ID: {}", person.id()); + } TaxRegimeStrategy strategy = strategyFactory.getStrategy(regime); BigDecimal grossIncome = person.income(); diff --git a/tax-engine-service/src/main/resources/application-docker.properties b/tax-engine-service/src/main/resources/application-docker.properties index 6f38044..8cd1ce1 100644 --- a/tax-engine-service/src/main/resources/application-docker.properties +++ b/tax-engine-service/src/main/resources/application-docker.properties @@ -5,3 +5,8 @@ spring.threads.virtual.enabled=true # Service endpoints in Docker (use container name of People service) app.services.people-service.url=http://people-service:8080 + +# Restricted Actuator for Docker +management.endpoints.web.exposure.include=health,info +management.endpoint.health.show-details=when-authorized +management.info.env.enabled=false diff --git a/tax-engine-service/src/main/resources/application-test.properties b/tax-engine-service/src/main/resources/application-test.properties new file mode 100644 index 0000000..fbfce32 --- /dev/null +++ b/tax-engine-service/src/main/resources/application-test.properties @@ -0,0 +1,4 @@ +# Test profile for Tax Engine +management.endpoints.web.exposure.include=health,info +management.endpoint.health.show-details=always +management.info.env.enabled=true diff --git a/tax-engine-service/src/main/resources/application.properties b/tax-engine-service/src/main/resources/application.properties index bf0f584..2c5e130 100644 --- a/tax-engine-service/src/main/resources/application.properties +++ b/tax-engine-service/src/main/resources/application.properties @@ -4,3 +4,6 @@ spring.threads.virtual.enabled=true # Service endpoints app.services.people-service.url=http://localhost:8080 + + + diff --git a/tax-engine-service/src/main/resources/logback-spring.xml b/tax-engine-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..6d65640 --- /dev/null +++ b/tax-engine-service/src/main/resources/logback-spring.xml @@ -0,0 +1,32 @@ + + + + + + + + + UTC + + + + { + "timestamp": "%d{yyyy-MM-dd HH:mm:ss.SSS}", + "level": "%level", + "service": "tax-engine-service", + "correlationId": "%X{correlationId:-undefined}", + "thread": "%thread", + "logger": "%logger{36}", + "message": "%message", + "exception": "%ex" + } + + + + + + + + + +