diff --git a/property-tax/enumeration-backend/.dockerignore b/property-tax/enumeration-backend/.dockerignore new file mode 100644 index 0000000..d2d69d3 --- /dev/null +++ b/property-tax/enumeration-backend/.dockerignore @@ -0,0 +1,7 @@ +*.json +*.md +/migrations +.env +*.yaml +*.yml +.gitignore \ No newline at end of file diff --git a/property-tax/enumeration-backend/.gitignore b/property-tax/enumeration-backend/.gitignore new file mode 100644 index 0000000..92adfa0 --- /dev/null +++ b/property-tax/enumeration-backend/.gitignore @@ -0,0 +1,57 @@ +# Environment files +.env +.env.local +.env.development +.env.test +.env.production + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Go workspace file +go.work + +# Dependency directories +vendor/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Log files +*.log + +# Temporary files +*.tmp +*.temp + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Build directories +build/ +dist/ \ No newline at end of file diff --git a/property-tax/enumeration-backend/Dockerfile b/property-tax/enumeration-backend/Dockerfile new file mode 100644 index 0000000..d823334 --- /dev/null +++ b/property-tax/enumeration-backend/Dockerfile @@ -0,0 +1,36 @@ +# Build stage +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +# Copy go.mod and go.sum files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy the source code +COPY . . + +# Build the application (main.go is in cmd/server/) +RUN CGO_ENABLED=0 GOOS=linux go build -o property-enumeration-service ./cmd/server + +# Final stage +FROM alpine:3.19 + +WORKDIR /app + +# Install ca-certificates for HTTPS requests +RUN apk --no-cache add ca-certificates + +# Copy the binary from the builder stage +COPY --from=builder /app/property-enumeration-service . + +# Set permissions +RUN chmod +x /app/property-enumeration-service + +# Expose port 8080 (default property service port) +EXPOSE 8080 + +# Set the entry point +ENTRYPOINT ["./property-enumeration-service"] \ No newline at end of file diff --git a/property-tax/enumeration-backend/README.md b/property-tax/enumeration-backend/README.md new file mode 100644 index 0000000..3854022 --- /dev/null +++ b/property-tax/enumeration-backend/README.md @@ -0,0 +1,515 @@ +# Property Tax Enumeration Service + +A robust Go microservice for managing property tax enumeration, including applications, properties, owners, GIS data, amenities, documents, and workflow integration. Part of the DIGIT3 platform. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Architecture](#architecture) +- [Project Structure](#project-structure) +- [API Overview](#api-overview) +- [Authentication & Authorization](#authentication--authorization) +- [Getting Started](#getting-started) +- [Configuration](#configuration) +- [Development Guidelines](#development-guidelines) +- [API Documentation](#api-documentation) + +--- + +## Overview + +The Property Tax Enumeration Service manages the end-to-end lifecycle of property tax applications, property and owner records, GIS and amenity data, and document management. It is designed for seamless integration with workflow, ID generation, and file storage microservices, providing a robust backend for the Property Tax Management System. + +The service is extensible and supports integration with additional modules such as notification, audit logging, and master data management (MDMS). It is built to handle high transaction volumes and supports multi-tenancy, role-based access control, and flexible business rules. The architecture enables rapid onboarding of new property types, owner categories, and workflow states, making it suitable for evolving municipal and state-level property tax requirements. + +--- + + +## Features + +### Business Features +- End-to-end property application lifecycle management (initiate, assign, verify, approve, reject, resubmit) +- Multi-owner property support with share validation and joint ownership +- Comprehensive user management with roles: CITIZEN, AGENT, SERVICE_MANAGER, COMMISSIONER +- GIS and coordinates management for spatial property data +- Amenity and construction details tracking for each property +- Document upload, verification, and audit logging for compliance +- Workflow integration for application status transitions and approvals +- Flexible business rules for property types, owner categories, and workflow states +- Multi-tenancy and support for evolving municipal/state requirements + +### Technical Features +- RESTful APIs with OpenAPI 3.0 specification +- JWT-based authentication and role-based authorization +- MDMS integration for dynamic role and permission management +- PostgreSQL database with GORM ORM +- Gin framework for high-performance HTTP routing +- Docker support for containerized deployment +- Structured and configurable logging +- Health check endpoints for monitoring +- Pagination, filtering, and search for all major endpoints + +--- + + +## Architecture & Design + +### High-Level Architecture +``` +┌─────────────────────────────────────────┐ +│ API Gateway │ ← Handles Auth/AuthZ +├─────────────────────────────────────────┤ +│ HTTP Handlers │ ← Request Processing +├─────────────────────────────────────────┤ +│ Service Layer │ ← Business Logic +├─────────────────────────────────────────┤ +│ Repository Layer │ ← Data Access +├─────────────────────────────────────────┤ +│ Database (PostgreSQL) │ ← Data Persistence +└─────────────────────────────────────────┘ +``` + +### Design Patterns +- **Layered Architecture**: Used throughout the codebase—handlers (`internal/handlers/`), services (`internal/services/`), repositories (`internal/repositories/`), and models (`internal/models/`). Each layer has a clear responsibility, improving maintainability and testability. +- **RESTful API Design**: All endpoints (see `internal/routes/routes.go`) use standard HTTP methods and resource-oriented URLs, returning consistent JSON responses. Pagination, filtering, and role-based access are supported via query parameters. +- **Repository Pattern**: Database access is abstracted behind interfaces in `internal/repositories/interfaces.go`. Each entity has a dedicated repository (e.g., `application_repository.go`), making DB logic modular and testable. +- **Dependency Injection**: Dependencies are injected at runtime in `cmd/server/main.go`, allowing handlers and services to receive repositories and other services as parameters. This enables easy mocking and unit testing. +- **Standardized API Response**: All API responses use a consistent structure from `pkg/response/response.go` (`success`, `message`, `data`, `errors`), ensuring predictable client handling and error tracking. +- **External Service Integration**: Clients for workflow, IDGen, MDMS, and notification are implemented in `internal/clients/`, encapsulating external API logic and supporting retries and error handling. +- **Validation Pattern**: Input validation is centralized in `internal/validators/` using go-playground/validator, ensuring consistent validation rules and clear error messages across all endpoints. +- **Soft Delete Pattern**: Most models (see `internal/models/models.go`) include a `deleted_at` timestamp, enabling soft deletion for audit and recovery without physical data removal. + +### Key Components +- **Handlers**: Parse HTTP requests, validate input, invoke business logic, and format API responses. (`internal/handlers/`) +- **Services**: Contain business logic, orchestrate workflows, and coordinate between handlers and repositories. (`internal/services/`) +- **Repositories**: Abstract all database access and persistence logic behind interfaces for easy mocking and DB swaps. (`internal/repositories/`) +- **Models**: Define data structures for both API and database, including GORM models and DTOs. (`internal/models/`) +- **Config**: Centralized configuration management, including environment variable parsing and structured config loading. (`internal/config/`) +- **Routes**: API route definitions, grouping, and middleware setup. (`internal/routes/`) + +--- + +## Project Structure + +``` +enumeration/ +├── cmd/ +│ └── server/ +│ └── main.go +├── internal/ +│ ├── clients/ +│ │ ├── mdms.go +│ │ ├── notification.go +│ │ └── workflow.go +│ ├── config/ +│ │ └── config.go +│ ├── constants/ +│ │ ├── application.go +│ │ ├── errors.go +│ │ └── validation.go +│ ├── database/ +│ │ └── database.go +│ ├── dto/ +│ │ └── dto.go +│ ├── handlers/ +│ │ ├── additional_property_details_handler.go +│ │ ├── amenity_handler.go +│ │ ├── application_handler.go +│ │ ├── application_log_handler.go +│ │ ├── assessment_details_handler.go +│ │ ├── construction_details_handler.go +│ │ ├── coordinates_handler.go +│ │ ├── document_handler.go +│ │ ├── floor_details_handler.go +│ │ ├── gis_handler.go +│ │ ├── igrs_handler.go +│ │ ├── property_address_handler.go +│ │ ├── property_handler.go +│ │ └── property_owner_handler.go +│ ├── middleware/ +│ │ ├── auth_middleware.go +│ │ └── cors_logMiddleware.go +│ ├── models/ +│ │ └── models.go +│ ├── repositories/ +│ │ ├── additional_property_details_repository.go +│ │ ├── amenity_repo.go +│ │ ├── application_log_repository.go +│ │ ├── application_repository.go +│ │ ├── assessment_details_repository.go +│ │ ├── construction_details_repository.go +│ │ ├── coordinates_repo.go +│ │ ├── document_repository.go +│ │ ├── floor_details_repository.go +│ │ ├── gis_repository.go +│ │ ├── igrs_repository.go +│ │ ├── interfaces.go +│ │ ├── property_address_repository.go +│ │ ├── property_owner_repository.go +│ │ └── property_repository.go +│ ├── routes/ +│ │ └── routes.go +│ ├── security/ +│ │ └── security.go +│ ├── services/ +│ │ ├── additional_property_details_service.go +│ │ ├── amenity_service.go +│ │ ├── application_log_service.go +│ │ ├── application_service.go +│ │ ├── assessment_details_service.go +│ │ ├── construction_details_service.go +│ │ ├── coordinates_service.go +│ │ ├── document_service.go +│ │ ├── floor_details_service.go +│ │ ├── gis_service.go +│ │ ├── igrs_service.go +│ │ ├── interfaces.go +│ │ ├── property_address_service.go +│ │ ├── property_owner_service.go +│ │ └── property_service.go +│ └── validators/ +│ └── coordinates_validator.go +├── migrations/ +│ ├── initial_schema.sql +│ └── insert_table.sql +├── pkg/ +│ ├── logger/ +│ │ └── logger.go +│ ├── response/ +│ │ └── response.go +│ └── utils/ +│ └── custom_date.go +├── Dockerfile +├── go.mod +├── go.sum +├── property-tax-apiv2.yml +├── README.md +├── README_UPDATED.md +├── service-documentation 2.md +└── .env +``` + +--- + + + + +## API Overview + +The Property Tax Enumeration Service exposes a comprehensive set of RESTful APIs for managing property tax applications, properties, owners, documents, GIS data, amenities, and related modules. Below is a categorized list of all major endpoints: + +### Additional Property Details +- `GET /v1/additional-property-details` — List additional property details +- `POST /v1/additional-property-details` — Create additional property details +- `GET /v1/additional-property-details/field/{fieldName}` — Get details by field name +- `GET /v1/additional-property-details/property/{propertyId}` — Get details by property ID +- `GET /v1/additional-property-details/{id}` — Get details by ID +- `PUT /v1/additional-property-details/{id}` — Update additional property details +- `DELETE /v1/additional-property-details/{id}` — Delete additional property details + +### Amenities +- `GET /v1/amenities` — List amenities +- `POST /v1/amenities` — Create amenity +- `GET /v1/amenities/property/{propertyId}` — Get amenities by property ID +- `GET /v1/amenities/{id}` — Get amenity by ID +- `PUT /v1/amenities/{id}` — Update amenity +- `DELETE /v1/amenities/{id}` — Delete amenity + +### Application Logs +- `GET /v1/application-logs` — List application logs +- `POST /v1/application-logs` — Create application log +- `GET /v1/application-logs/{id}` — Get application log by ID +- `PUT /v1/application-logs/{id}` — Update application log +- `DELETE /v1/application-logs/{id}` — Delete application log +- `GET /v1/applications/{applicationId}/logs` — List logs by application ID + +### Applications +- `GET /v1/applications` — List applications +- `POST /v1/applications` — Create application +- `GET /v1/applications/by-number/{applicationNo}` — Get application by application number +- `GET /v1/applications/search` — Search applications +- `GET /v1/applications/{id}` — Get application by ID +- `PUT /v1/applications/{id}` — Update application +- `DELETE /v1/applications/{id}` — Delete application +- `POST /v1/applications/{id}/action` — Perform action on application +- `PATCH /v1/applications/{id}/status` — Update application status + +### Assessment Details +- `GET /v1/assessment-details` — List assessment details +- `POST /v1/assessment-details` — Create assessment details +- `GET /v1/assessment-details/property/{propertyId}` — Get assessment details by property ID +- `GET /v1/assessment-details/{id}` — Get assessment details by ID +- `PUT /v1/assessment-details/{id}` — Update assessment details +- `DELETE /v1/assessment-details/{id}` — Delete assessment details + +### Construction Details +- `GET /v1/construction-details` — List construction details +- `POST /v1/construction-details` — Create construction details +- `GET /v1/construction-details/property/{propertyId}` — Get construction details by property ID +- `GET /v1/construction-details/{id}` — Get construction details by ID +- `PUT /v1/construction-details/{id}` — Update construction details +- `DELETE /v1/construction-details/{id}` — Delete construction details + +### Coordinates +- `GET /v1/coordinates` — List coordinates +- `POST /v1/coordinates` — Create coordinates +- `POST /v1/coordinates/batch` — Create coordinates batch +- `PUT /v1/coordinates/gis/{gisDataId}` — Replace coordinates by GIS Data ID +- `GET /v1/coordinates/{id}` — Get coordinates by ID +- `PUT /v1/coordinates/{id}` — Update coordinates +- `DELETE /v1/coordinates/{id}` — Delete coordinates + +### Documents +- `GET /v1/documents` — List documents +- `POST /v1/documents` — Create document +- `POST /v1/documents/batch` — Create multiple documents +- `GET /v1/documents/property/{propertyId}` — Get documents by property ID +- `GET /v1/documents/{id}` — Get document by ID +- `PUT /v1/documents/{id}` — Update document +- `DELETE /v1/documents/{id}` — Delete document + +### Floor Details +- `GET /v1/floor-details` — List floor details +- `POST /v1/floor-details` — Create floor details +- `GET /v1/floor-details/{id}` — Get floor details by ID +- `PUT /v1/floor-details/{id}` — Update floor details +- `DELETE /v1/floor-details/{id}` — Delete floor details + +### GIS Data +- `GET /v1/gis-data` — List GIS data +- `POST /v1/gis-data` — Create GIS data +- `GET /v1/gis-data/property/{propertyId}` — Get GIS data by property ID +- `GET /v1/gis-data/{id}` — Get GIS data by ID +- `PUT /v1/gis-data/{id}` — Update GIS data +- `DELETE /v1/gis-data/{id}` — Delete GIS data + +### IGRS +- `GET /v1/igrs` — List IGRS +- `POST /v1/igrs` — Create IGRS +- `GET /v1/igrs/{id}` — Get IGRS by ID +- `PUT /v1/igrs/{id}` — Update IGRS +- `DELETE /v1/igrs/{id}` — Delete IGRS + +### Properties +- `GET /v1/properties` — List all properties +- `POST /v1/properties` — Create a new property +- `GET /v1/properties/propertyNo/{propertyNo}` — Get property by property number +- `GET /v1/properties/search` — Search properties +- `PUT /v1/properties/{id}` — Update property +- `DELETE /v1/properties/{id}` — Delete property + +### Property Addresses +- `GET /v1/property-addresses` — List all property addresses +- `POST /v1/property-addresses` — Create property address +- `GET /v1/property-addresses/property/{propertyId}` — Get property address by property ID +- `GET /v1/property-addresses/search` — Search property addresses +- `GET /v1/property-addresses/{id}` — Get property address by ID +- `PUT /v1/property-addresses/{id}` — Update property address +- `DELETE /v1/property-addresses/{id}` — Delete property address + +### Property Owners +- `POST /v1/property-owners` — Create property owner +- `POST /v1/property-owners/batch` — Create property owners batch +- `GET /v1/property-owners/property/{propertyId}` — Get property owners by property ID +- `PUT /v1/property-owners/{id}` — Update property owner +- `DELETE /v1/property-owners/{id}` — Delete property owner + +--- + + + + +## Authentication & Authorization + +All endpoints (except health checks) require authentication and authorization to ensure data security and compliance. + +### JWT Authentication +- Every API request must include a valid JWT token in the `Authorization` header. +- JWT claims must include user ID, roles, and tenant information. +- Required headers: + - `Authorization: Bearer ` + - `X-Tenant-ID: ` + - `X-User-ID: ` + - `X-User-Role: ` +- JWTs are signed using the secret defined in environment variables. +- Expired or invalid tokens return a `401 Unauthorized` response. + +#### Example JWT Claims +```json +{ + "sub": "user-uuid", + "role": "CITIZEN", + "tenant": "tenant-id", + "exp": 1700000000, + "iat": 1699990000 +} +``` + +### Role-Based Access Control (RBAC) +- Endpoint access is governed by user roles, validated on every request: + - **CITIZEN**: Can create and view own applications and properties. + - **AGENT**: Can view assigned applications and perform field verification. + - **SERVICE_MANAGER**: Can manage all applications within their jurisdiction. + - **COMMISSIONER**: Can approve or reject applications. +- Role checks are enforced in middleware and handlers. Unauthorized access returns `403 Forbidden`. + +### MDMS-Driven Dynamic Permissions +- Role permissions and access rules are dynamically loaded from the Master Data Management Service (MDMS). +- Supports endpoint-level and method-level access control, allowing real-time permission updates without redeployment. +- Permission checks are performed on every request to ensure compliance with the latest business rules. + +--- + +--- + +## Getting Started + +Follow these steps to set up and run the Property Tax Enumeration Service for local development or in a containerized environment. + +### Prerequisites +- **Go** 1.24 or higher +- **PostgreSQL** 12 or higher +- **Docker** (optional, for containerized setup) + +### Local Development Setup + +1. **Clone the repository** + ```bash + git clone + cd Property-tax/enumeration + ``` + +2. **Install Go dependencies** + ```bash + go mod download + ``` + +3. **Configure environment variables** + - Copy the example environment file and update values as needed: + ```bash + cp .env.example .env + # Edit .env with your database, JWT, and service URLs + ``` + +4. **Set up the PostgreSQL database** + - Ensure PostgreSQL is running and accessible as per your `.env` configuration. + - Create the database and user if not already present. + +5. **Run database migrations** + - Apply schema and seed data: + ```bash + go run migrations/*.go + # Or use a migration tool if provided + ``` + +6. **Start the service** + ```bash + go run cmd/server/main.go + ``` + The service will be available at [http://localhost:8080](http://localhost:8080) + +### Dockerized Setup + +1. **Build the Docker image** + ```bash + docker build -t property-enumeration-service . + ``` + +2. **Run the service using Docker Compose** + - Ensure your `docker-compose.yml` is configured (create if needed). + ```bash + docker-compose up -d + ``` + This will start the service and any dependencies (e.g., PostgreSQL) as defined in the compose file. + +3. **Access the service** + - The API will be available at [http://localhost:8080](http://localhost:8080) (or as configured). + +--- + +## Configuration + +This section describes how to configure the Property Tax Enumeration Service for different environments. + +### Environment Variables +Set the following environment variables in your `.env` file or deployment environment: + +#### Database Configuration +```bash +DB_HOST=localhost # Database host +DB_PORT=5432 # Database port +DB_USER=postgres # Database user +DB_PASSWORD=your_password # Database password +DB_NAME=property # Database name +DB_SCHEMA=DIGIT3 # Database schema (default: DIGIT3) +``` + +#### Server Configuration +```bash +PORT=8080 # Port for the HTTP server +``` + +#### External Service Endpoints +```bash +WORKFLOW_URL=http://localhost:8081 # Workflow service URL +AUDIT_URL=http://localhost:8082/audit/logs # Audit logging service URL +IDGEN_URL=http://localhost:8083/idgen/v1/generate # ID generation service URL +MDMS_URL=http://localhost:8084 # Master Data Management Service URL +``` + +#### JWT Configuration +```bash +JWT_SECRET=your_jwt_secret # Secret for signing JWT tokens +JWT_ISSUER=property-tax-service # JWT issuer +JWT_AUDIENCE=property-tax-users # JWT audience +``` + +--- + +### Database Schema +The service uses PostgreSQL with the `DIGIT3` schema. The main tables are: + +- **applications**: Stores property tax application data and workflow status +- **properties**: Master table for property details +- **property_owners**: Owner and joint ownership information +- **property_addresses**: Address and location details +- **assessment_details**: Tax assessment records for properties +- **documents**: Uploaded documents and metadata +- **application_logs**: Audit trail and activity logs + +Refer to the `migrations/` directory for schema definitions and initial data. + +--- + + +## Development Guidelines + +- Follow clean, layered architecture: keep handlers, services, repositories, and models separated for maintainability and testability +- Use dependency injection for all services and repositories to enable easy unit testing +- Always use context for database and external service operations +- Implement robust error handling and return standardized API responses +- Use parameterized queries and GORM best practices to prevent SQL injection +- Validate all input data using centralized validators in `internal/validators/` +- Implement proper transaction management for multi-step operations +- Use consistent response formats as defined in [`pkg/response/response.go`](pkg/response/response.go) +- Support pagination, filtering, and searching on all list endpoints +- Provide clear and actionable error messages +- Enforce role-based access control and dynamic permissions via MDMS +- Implement rate limiting and CORS policies as needed +- Use HTTPS in production and follow JWT security best practices + +--- + +## API Documentation + +The complete API documentation is available in OpenAPI 3.0 format: +- File: `property-tax-apiv2.yml` +- Import into Swagger UI or Postman for interactive documentation +- Includes request/response schemas, authentication details, and examples + +### Live Swagger UI +- [Deployed Swagger UI](http://10.232.161.103:30117/swagger/index.html#) — View and interact with the live API documentation + diff --git a/property-tax/enumeration-backend/cmd/server/main.go b/property-tax/enumeration-backend/cmd/server/main.go new file mode 100644 index 0000000..b661463 --- /dev/null +++ b/property-tax/enumeration-backend/cmd/server/main.go @@ -0,0 +1,116 @@ +// Entry point for the Property Tax Enumeration Service +package main + +import ( + workflow "enumeration/internal/clients" + "enumeration/internal/config" + "enumeration/internal/database" + "enumeration/internal/handlers" + "enumeration/internal/middleware" + "enumeration/internal/repositories" + "enumeration/internal/routes" + "enumeration/internal/services" + "enumeration/pkg/logger" + + // Standard and third-party packages + "fmt" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" +) + +// init loads environment variables and initializes middleware +func init() { + err := godotenv.Load() + if err != nil { + logger.Error("\nError loading .env file using default environment variables\n") + } + middleware.Init() +} + +// main sets up dependencies and starts the HTTP server +func main() { + cfg := config.GetConfig() // Load configuration + + logger.Info("Starting Property Tax Enumeration Service...") + + // Initialize workflow client for external workflow integration + workflowClient := workflow.NewClient(cfg.WorkflowURL) + + // Connect to the database + db := database.GetDB() + + // Initialize repositories for data access + coordinatesRepo := repositories.NewCoordinatesRepository(db) + floorDetailsRepo := repositories.NewFloorDetailsRepository(db) + applicationRepo := repositories.NewApplicationRepository(db) + applicationLogRepo:=repositories.NewApplicationLogRepository(db) + propertyOwnerRepo := repositories.NewPropertyOwnerRepository(db) + constructionDetailsRepo := repositories.NewConstructionDetailsRepository(db) + additionalPropertyDetailsRepo := repositories.NewAdditionalPropertyDetailsRepository(db) + assessmentDetailsRepo := repositories.NewAssessmentDetailsRepository(db) + propertyRepo := repositories.NewPropertyRepository(db) + propertyAddressRepo := repositories.NewPropertyAddressRepository(db) + gisRepo := repositories.NewGISRepository(db) + amenityRepo := repositories.NewAmenityRepository(db) + documentRepo := repositories.NewDocumentRepository(db) + igrsRepo := repositories.NewIGRSRepository(db) + + // Initialize services for business logic + coordinatesService := services.NewCoordinatesService(coordinatesRepo) + floorDetailsService := services.NewFloorDetailsService(floorDetailsRepo) + applicationLogService := services.NewApplicationLogService(applicationLogRepo) + applicationService := services.NewApplicationService(applicationRepo, propertyOwnerRepo, workflowClient, cfg,applicationLogService) + + + propertyOwnerService := services.NewPropertyOwnerService(propertyOwnerRepo) + constructionDetailsService := services.NewConstructionDetailsService(constructionDetailsRepo) + additionalPropertyDetailsService := services.NewAdditionalPropertyDetailsService(additionalPropertyDetailsRepo) + assessmentDetailsService := services.NewAssessmentDetailsService(assessmentDetailsRepo) + propertyService := services.NewPropertyService(propertyRepo) + propertyAddressService := services.NewPropertyAddressService(propertyAddressRepo) + gisService := services.NewGISService(gisRepo) + amenityService := services.NewAmenityService(amenityRepo) + documentService := services.NewDocumentService(documentRepo) + igrsService := services.NewIGRSService(igrsRepo) + + // Initialize handlers + coordinatesHandler := handlers.NewCoordinatesHandler(coordinatesService) + floorDetailsHandler := handlers.NewFloorDetailsHandler(floorDetailsService) + applicationHandler := handlers.NewApplicationHandler(applicationService) + applicationLogHandler:= handlers.NewApplicationLogHandler(applicationLogService) + propertyOwnerHandler := handlers.NewPropertyOwnerHandler(propertyOwnerService) + constructionDetailsHandler := handlers.NewConstructionDetailsHandler(constructionDetailsService) + additionalPropertyDetailsHandler := handlers.NewAdditionalPropertyDetailsHandler(additionalPropertyDetailsService) + assessmentDetailsHandler := handlers.NewAssessmentDetailsHandler(assessmentDetailsService) + propertyHandler := handlers.NewPropertyHandler(propertyService) + propertyAddressHandler := handlers.NewPropertyAddressHandler(propertyAddressService) + gisHandler := handlers.NewGISHandler(gisService) + amenityHandler := handlers.NewAmenityHandler(amenityService) + documentHandler := handlers.NewDocumentHandler(documentService) + igrsHandler := handlers.NewIGRSHandler(igrsService) + + // Setup Gin router for HTTP requests + router := gin.Default() + + // Add CORS and logging middleware + router.Use(middleware.CorsMiddleware(), middleware.LoggerMiddleware()) + + // Health check endpoint + router.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "UP", + "service": "property-tax-enumeration", + }) + }) + + // Setup routes + routes.SetupRoutes(router, coordinatesHandler, floorDetailsHandler, constructionDetailsHandler, additionalPropertyDetailsHandler, assessmentDetailsHandler, propertyHandler, propertyAddressHandler, gisHandler, applicationHandler, propertyOwnerHandler, amenityHandler, documentHandler, igrsHandler,applicationLogHandler) + + // Start the HTTP server + address := fmt.Sprintf(":%s", cfg.Port) + logger.Info("Server starting on", address) + if err := router.Run(address); err != nil { + logger.Fatal("Failed to start server:", err) + } +} diff --git a/property-tax/enumeration-backend/go.mod b/property-tax/enumeration-backend/go.mod new file mode 100644 index 0000000..4c343c7 --- /dev/null +++ b/property-tax/enumeration-backend/go.mod @@ -0,0 +1,63 @@ +module enumeration + +go 1.24.4 + +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.11.1 + gorm.io/datatypes v1.2.7 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.56.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gorm.io/driver/mysql v1.5.6 // indirect +) diff --git a/property-tax/enumeration-backend/go.sum b/property-tax/enumeration-backend/go.sum new file mode 100644 index 0000000..22251cd --- /dev/null +++ b/property-tax/enumeration-backend/go.sum @@ -0,0 +1,143 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= +github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/property-tax/enumeration-backend/internal/clients/mdms.go b/property-tax/enumeration-backend/internal/clients/mdms.go new file mode 100644 index 0000000..943f1ab --- /dev/null +++ b/property-tax/enumeration-backend/internal/clients/mdms.go @@ -0,0 +1,63 @@ +package workflow + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +// MDMSResponse represents the structure of the response from the MDMS service. +type MDMSResponse struct { + MDMS []struct { + Data struct { + NoOfDays int `json:"noOfDays"` // Number of days field in the MDMS data + } `json:"data"` + } `json:"mdms"` +} + +// FetchNoOfDays fetches the number of days from the MDMS service for a given tenant. +// It sends a GET request to the MDMS endpoint, parses the response, and extracts the noOfDays field. +func FetchNoOfDays(url string, tenantID string) (int, error) { + // Build the MDMS endpoint URL + url = fmt.Sprintf("%s/mdms-v2/v2?schemaCode=PropertyTax.Enumeration.Duedate", url) + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return 0, fmt.Errorf("failed to create HTTP request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("X-Tenant-ID", tenantID) + httpReq.Header.Set("X-Client-Id", "test-client") + + // Send the HTTP request + client := &http.Client{} + resp, err := client.Do(httpReq) + if err != nil { + return 0, fmt.Errorf("failed to fetch MDMS data: %w", err) + } + defer resp.Body.Close() + + // Check for non-200 status codes + if resp.StatusCode != http.StatusOK { + return 0, fmt.Errorf("MDMS service returned status: %s", resp.Status) + } + + // Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, fmt.Errorf("failed to read MDMS response body: %w", err) + } + + // Parse the response JSON into MDMSResponse struct + var mdmsResp MDMSResponse + if err := json.Unmarshal(body, &mdmsResp); err != nil { + return 0, fmt.Errorf("failed to unmarshal MDMS response: %w", err) + } + + // Extract the noOfDays field from the response + if len(mdmsResp.MDMS) > 0 { + return mdmsResp.MDMS[0].Data.NoOfDays, nil + } + + return 0, fmt.Errorf("no data found in MDMS response") +} diff --git a/property-tax/enumeration-backend/internal/clients/notification.go b/property-tax/enumeration-backend/internal/clients/notification.go new file mode 100644 index 0000000..1fa7fd0 --- /dev/null +++ b/property-tax/enumeration-backend/internal/clients/notification.go @@ -0,0 +1,48 @@ +package workflow + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +// NotificationRequest represents the payload for sending a notification. +type NotificationRequest struct { + Title string `json:"title"` // Notification title + Body string `json:"body"` // Notification body/message + Data map[string]interface{} `json:"data"` // Additional data to send with the notification + Tokens []string `json:"tokens"` // Device tokens to which the notification will be sent +} + +// SendNotification sends a notification to the specified URL using the provided NotificationRequest payload. +// It marshals the request to JSON, sends a POST request, and checks for a successful response. +func SendNotification(url string, req NotificationRequest) error { + // Marshal the notification request to JSON + payload, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal notification request: %w", err) + } + + // Create the HTTP POST request + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + // Send the HTTP request + client := &http.Client{} + resp, err := client.Do(httpReq) + if err != nil { + return fmt.Errorf("failed to send notification: %w", err) + } + defer resp.Body.Close() + + // Check for non-200 status codes + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("notification service returned status: %s", resp.Status) + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/clients/user.go b/property-tax/enumeration-backend/internal/clients/user.go new file mode 100644 index 0000000..e997fc2 --- /dev/null +++ b/property-tax/enumeration-backend/internal/clients/user.go @@ -0,0 +1,53 @@ +package workflow + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +// UserResponse represents the structure of the response from the user service API. +type UserResponse struct { + Success bool `json:"success"` // Indicates if the API call was successful + Message string `json:"message"` // Message from the API (error or success info) + Data struct { + ID string `json:"id"` // User ID + Username string `json:"username"` // Username of the user + // Other fields omitted for brevity + } `json:"data"` +} + +// GetUsername fetches the username for a given userID from the user service. +// It sends a GET request to the user service API, decodes the response, and returns the username. +func GetUsername(userID string) (string, error) { + // Build the user service API URL + url := fmt.Sprintf("http://10.232.161.103:30105/api/v1/users/%s", userID) + + // Create an HTTP client with a timeout + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // Check for non-200 status codes + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + // Decode the JSON response into UserResponse struct + var userResp UserResponse + if err := json.NewDecoder(resp.Body).Decode(&userResp); err != nil { + return "", err + } + + // Check if the API call was successful + if !userResp.Success { + return "", fmt.Errorf("API error: %s", userResp.Message) + } + + // Return the username from the response + return userResp.Data.Username, nil +} diff --git a/property-tax/enumeration-backend/internal/clients/workflow.go b/property-tax/enumeration-backend/internal/clients/workflow.go new file mode 100644 index 0000000..d4a1bac --- /dev/null +++ b/property-tax/enumeration-backend/internal/clients/workflow.go @@ -0,0 +1,167 @@ +package workflow + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// Client is a workflow service client for making HTTP requests to the workflow API. +type Client struct { + baseURL string // Base URL of the workflow service + httpClient *http.Client // HTTP client used for requests +} + +// Process represents a workflow process definition. +type Process struct { + ID string `json:"id"` // Unique process ID + Code string `json:"code"` // Process code + Name string `json:"name"` // Process name +} + +// ProcessResponse represents the response from a workflow transition or process action. +type ProcessResponse struct { + ID string `json:"id"` // Unique response ID + ProcessID string `json:"processId"` // Associated process ID + EntityID string `json:"entityId"` // Entity ID involved in the process + Action string `json:"action"` // Action performed + Status string `json:"status"` // Status of the process + Comment string `json:"comment"` // Comment for the action + Documents []string `json:"documents"` // Related documents + Assigner string `json:"assigner"` // User who assigned the task + Assignees []string `json:"assignees"` // Users assigned to the task + CurrentState string `json:"currentState"` // Current state of the process + StateSla int64 `json:"stateSla"` // State SLA in milliseconds + ProcessSla int64 `json:"processSla"` // Process SLA in milliseconds + Attributes map[string][]string `json:"attributes"` // Additional attributes + AuditDetails AuditDetail `json:"auditDetails"` // Audit details +} + +// AuditDetail contains audit information for workflow entities. +type AuditDetail struct { + CreatedBy string `json:"createdBy,omitempty" db:"created_by" gorm:"column:created_by"` // User who created the entity + CreatedTime int64 `json:"createdTime,omitempty" db:"created_at" gorm:"column:created_at"` // Creation timestamp + ModifiedBy string `json:"modifiedBy,omitempty" db:"modified_by" gorm:"column:modified_by"` // User who last modified the entity + ModifiedTime int64 `json:"modifiedTime,omitempty" db:"modified_at" gorm:"column:modified_at"` // Last modification timestamp +} + +// StateResponse represents a workflow state. +type StateResponse struct { + ID string `json:"id"` // State ID + Code string `json:"code"` // State code + Name string `json:"name"` // State name + Description string `json:"description"` // State description +} + +// ActionResponse represents a workflow action and its next state. +type ActionResponse struct { + ID string `json:"id"` // Action ID + Name string `json:"name"` // Action name + NextState string `json:"nextState"` // Next state after action +} + +// TransitionRequest represents a request to transition a workflow process. +type TransitionRequest struct { + ProcessID string `json:"processId"` // ID of the process to transition + EntityID string `json:"entityId"` // ID of the entity involved + Action string `json:"action"` // Action to perform + Attributes map[string]interface{} `json:"attributes"` // Additional attributes for the transition +} + +// NewClient creates a new workflow Client with the given base URL and a default HTTP client. +func NewClient(baseURL string) *Client { + return &Client{ + baseURL: baseURL, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// GetProcess fetches a workflow process definition by process code for a given tenant. +// Returns the process if found, or an error if not found or on failure. +func (c *Client) GetProcess(ctx context.Context, tenantID, processCode string) (*Process, error) { + url := fmt.Sprintf("%s/workflow/v1/process?code=%s", c.baseURL, processCode) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("X-Tenant-ID", tenantID) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("workflow service returned status %d", resp.StatusCode) + } + + var processes []Process + if err := json.NewDecoder(resp.Body).Decode(&processes); err != nil { + return nil, err + } + + for _, p := range processes { + if p.Code == processCode { + return &p, nil + } + } + + return nil, fmt.Errorf("process with code %s not found", processCode) +} + +// CreateTransition sends a transition request to the workflow service to perform an action on a process. +// It builds the payload, sends a POST request, and returns the resulting process response. +func (c *Client) CreateTransition(ctx context.Context, tenantID string, req *TransitionRequest) (*ProcessResponse, error) { + url := fmt.Sprintf("%s/workflow/v1/transition", c.baseURL) + + // Add comment field as per workflow service requirement + transitionPayload := map[string]interface{}{ + "processId": req.ProcessID, + "entityId": req.EntityID, + "action": req.Action, + "comment": fmt.Sprintf("Executing %s action", req.Action), + "attributes": req.Attributes, + } + + jsonData, err := json.Marshal(transitionPayload) + if err != nil { + return nil, err + } + + // Debug: print the request payload + fmt.Printf("DEBUG: Transition request: %s\n", string(jsonData)) + + httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("X-Tenant-ID", tenantID) + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + fmt.Printf("DEBUG: Workflow service returned status %d for URL: %s\n", resp.StatusCode, url) + return nil, fmt.Errorf("workflow service returned status %d", resp.StatusCode) + } + + var result ProcessResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/property-tax/enumeration-backend/internal/config/config.go b/property-tax/enumeration-backend/internal/config/config.go new file mode 100644 index 0000000..f6341b0 --- /dev/null +++ b/property-tax/enumeration-backend/internal/config/config.go @@ -0,0 +1,69 @@ +// Package config handles application configuration loading +package config + +import ( + "os" + "sync" +) + +// Config holds all environment-based configuration values for the application. +type Config struct { + DBHost string // Database host + DBPort string // Database port + DBUser string // Database username + DBPassword string // Database password + DBName string // Database name + DBSchema string // Database schema + Port string // Service port for the application + WorkflowURL string // Workflow service URL + AuditURL string // Audit service URL + IDGenURL string // ID generation service URL + MDMSURL string // MDMS service URL +} + +// configInstance holds the singleton Config instance. +// configOnce ensures the config is loaded only once (thread-safe singleton pattern). +var ( + configInstance *Config // Singleton instance + configOnce sync.Once // Ensures config is loaded once +) + +// Load reads environment variables and returns a Config struct populated with values. +// If an environment variable is not set, a default value is used. +func Load() *Config { + return &Config{ + DBHost: getEnv("DB_HOST", "localhost"), + DBPort: getEnv("DB_PORT", "5432"), + DBUser: getEnv("DB_USER", "postgres"), + DBPassword: getEnv("DB_PASSWORD", ""), + DBName: getEnv("DB_NAME", "property"), + DBSchema: getEnv("DB_SCHEMA", "DIGIT3"), + Port: getEnv("PORT", "8080"), + WorkflowURL: getEnv("WORKFLOW_URL", "http://localhost:8081"), + AuditURL: getEnv("AUDIT_URL", "http://localhost:8082/audit/logs"), + IDGenURL: getEnv("IDGEN_URL", "http://localhost:8083/idgen/v1/generate"), + MDMSURL: getEnv("MDMS_URL", "http://localhost:8084"), + } +} + +// GetConfig returns the singleton Config instance. +// Initializes the config on first call using sync.Once for thread safety. +func GetConfig() *Config { + configOnce.Do(func() { + configInstance = Load() + }) + return configInstance +} + +// getEnv returns the value of an environment variable or a default if not set. +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// RoleConfig holds role-based permissions loaded from a YAML file. +type RoleConfig struct { + RolePermissions map[string][]string `yaml:"rolePermissions"` // Map of role to permissions +} diff --git a/property-tax/enumeration-backend/internal/constants/application.go b/property-tax/enumeration-backend/internal/constants/application.go new file mode 100644 index 0000000..86568c2 --- /dev/null +++ b/property-tax/enumeration-backend/internal/constants/application.go @@ -0,0 +1,69 @@ +// Package constants holds application-wide constant values +package constants + +// Tenant and Workflow Constants +const ( + // Default tenant ID for the application + DefaultTenantID = "pb.amritsar" + + // Workflow process type for property enumeration + PropertyEnumerationWorkflow = "PROP_ENUM" + + // Application number formatting + ApplicationNumberPrefix = "APPL" + ApplicationNumberDateFormat = "20060102" // Date format for application number + ApplicationNumberSequenceFormat = "%04d" // Sequence format for application number +) + +// User role constants +const ( + RoleCitizen = "CITIZEN" + RoleAgent = "AGENT" + RoleServiceManager = "SERVICE_MANAGER" + RoleCommissioner = "COMMISSIONER" +) + +// Application status constants +const ( + StatusInitiated = "INITIATED" + StatusVerified = "VERIFIED" + StatusApproved = "APPROVED" + StatusAuditVerified = "AUDIT_VERIFIED" + StatusAssigned = "ASSIGNED" + StatusRejected = "REJECTED" +) + +// Application priority constants +const ( + PriorityHigh = "HIGH" + PriorityMedium = "MEDIUM" + PriorityLow = "LOW" + PriorityUrgent = "URGENT" +) + +// Validation-related constants +const ( + MaxApplicationsPerCitizen = 10 // Max applications allowed per citizen + MaxFileUploadSize = 10 * 1024 * 1024 // 10MB file upload limit + ApplicationNumberLength = 20 // Length of application number +) + +// Default values for pagination and application +const ( + DefaultPage = 0 + DefaultSize = 20 + MaxSize = 100 + + // Add jurisdiction constants + DefaultJurisdiction = "pb.amritsar" + + // Header names + HeaderTenantID = "X-Tenant-ID" + HeaderUserID = "X-User-ID" + HeaderUserRole = "X-User-Role" + HeaderStatus = "X-Status" + HeaderTotalCount = "X-Total-Count" + HeaderCurrentPage = "X-Current-Page" + HeaderPerPage = "X-Per-Page" + HeaderTotalPages = "X-Total-Pages" +) diff --git a/property-tax/enumeration-backend/internal/constants/errors.go b/property-tax/enumeration-backend/internal/constants/errors.go new file mode 100644 index 0000000..a552a6d --- /dev/null +++ b/property-tax/enumeration-backend/internal/constants/errors.go @@ -0,0 +1,45 @@ +// Package constants holds error message constants for the service +package constants + +// Error message constants for various failure scenarios +const ( + // Application Errors + ErrApplicationNotFound = "application not found" + ErrApplicationAlreadyExists = "application already exists" + ErrApplicationInvalidStatus = "invalid application status" + ErrApplicationInvalidPriority = "invalid application priority" + ErrApplicationCreationFailed = "failed to create application" + ErrApplicationUpdateFailed = "failed to update application" + ErrApplicationDeletionFailed = "failed to delete application" + + // Workflow Errors + ErrWorkflowProcessNotFound = "workflow process not found - please ensure workflow is pre-created" + ErrWorkflowInstanceCreationFailed = "failed to create workflow instance" + + // Validation Errors + ErrInvalidApplicationID = "invalid application ID format" + ErrInvalidPropertyID = "invalid property ID" + ErrInvalidTenantID = "invalid tenant ID" + ErrInvalidCitizenID = "invalid citizen ID" + ErrMissingRequiredFields = "missing required fields" + ErrExceededMaxApplications = "exceeded maximum applications per citizen" + + // Property Errors + ErrPropertyNotFound = "property not found" + ErrPropertyValidationFailed = "property validation failed" + + // Coordinates Errors + ErrCoordinatesNotFound = "coordinates not found" + ErrCoordinatesValidationFailed = "coordinates validation failed" + ErrInvalidLatitude = "invalid latitude value" + ErrInvalidLongitude = "invalid longitude value" + ErrInvalidGISDataID = "invalid GIS data ID" + ErrCoordinatesOutOfBounds = "coordinates out of geographic bounds" + ErrBatchValidationFailed = "batch validation failed" + + // General Errors + ErrInternalServer = "internal server error" + ErrUnauthorized = "unauthorized access" + ErrForbidden = "forbidden access" + ErrBadRequest = "bad request" +) diff --git a/property-tax/enumeration-backend/internal/constants/validation.go b/property-tax/enumeration-backend/internal/constants/validation.go new file mode 100644 index 0000000..c5b818f --- /dev/null +++ b/property-tax/enumeration-backend/internal/constants/validation.go @@ -0,0 +1,43 @@ +// Package constants holds validation rule constants for the service +package constants + +import "time" + +// Validation rule constants for input and business logic +const ( + // String Length Limits + MinNameLength = 2 + MaxNameLength = 100 + MinDescriptionLength = 10 + MaxDescriptionLength = 500 + MaxComplexNameLength = 200 + + // Time Limits + MinDueDateDays = 1 + MaxDueDateDays = 365 + ApplicationTimeout = 30 * time.Second + DatabaseQueryTimeout = 15 * time.Second + + // Business Rules + MaxPropertiesPerApplication = 1 + MinPropertyOwners = 1 + MaxPropertyOwners = 10 + + // Coordinate Validation Ranges + MinLatitude = -90.0 + MaxLatitude = 90.0 + MinLongitude = -180.0 + MaxLongitude = 180.0 + + // Geographic ranges for India (approximate bounds) + IndiaMinLatitude = 8.0 + IndiaMaxLatitude = 37.0 + IndiaMinLongitude = 68.0 + IndiaMaxLongitude = 97.5 + + // Geographic ranges for Karnataka, India (approximate bounds) + KarnatakaMinLatitude = 11.5 + KarnatakaMaxLatitude = 18.5 + KarnatakaMinLongitude = 74.0 + KarnatakaMaxLongitude = 78.5 +) diff --git a/property-tax/enumeration-backend/internal/database/database.go b/property-tax/enumeration-backend/internal/database/database.go new file mode 100644 index 0000000..fff18ce --- /dev/null +++ b/property-tax/enumeration-backend/internal/database/database.go @@ -0,0 +1,48 @@ +// Package database handles database connection and access +package database + +import ( + "enumeration/internal/config" + "enumeration/pkg/logger" + "fmt" + "sync" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +// dbInstance holds the singleton GORM DB instance for the application. +// dbOnce ensures the database is initialized only once (thread-safe singleton pattern). +var ( + dbInstance *gorm.DB // Singleton DB instance + dbOnce sync.Once // Ensures DB is initialized once +) + +// Connect creates a new database connection using the provided config. +// It sets up the GORM ORM with a custom schema naming strategy and returns the DB instance. +func Connect(cfg *config.Config) *gorm.DB { + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable client_encoding=UTF8 search_path=%s", + cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort, cfg.DBSchema) + + // Initialize GORM with schema naming strategy + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + TablePrefix: "DIGIT3.", // Prefix for all table names + }, + }) + if err != nil { + logger.Fatal("Failed to connect to database:", err) + } + logger.Info("Database connected and migrated successfully") + return db +} + +// GetDB returns the singleton database instance. +// Initializes the DB connection on first call using sync.Once for thread safety. +func GetDB() *gorm.DB { + dbOnce.Do(func() { + dbInstance = Connect(config.GetConfig()) + }) + return dbInstance +} diff --git a/property-tax/enumeration-backend/internal/dto/dto.go b/property-tax/enumeration-backend/internal/dto/dto.go new file mode 100644 index 0000000..e1bb528 --- /dev/null +++ b/property-tax/enumeration-backend/internal/dto/dto.go @@ -0,0 +1,194 @@ +// Package dto defines data transfer objects for API requests and responses +package dto + +import ( + "time" + + "github.com/google/uuid" +) + +// ============================================================================ +// APPLICATION DTOs +// ============================================================================ + +// CreateApplicationRequest is used to create a new application +type CreateApplicationRequest struct { + PropertyID uuid.UUID `json:"propertyId" binding:"required"` + Priority string `json:"priority"` + DueDate time.Time `json:"dueDate"` + AppliedBy string `json:"appliedBy" binding:"required"` + AssesseeID uuid.UUID `json:"assesseeId" binding:"required"` + IsDraft bool `json:"isDraft"` + ImportantNote string `json:"importantNote" binding:"max=500,omitempty"` + // PropertyOwners []PropertyOwnerRequest `json:"propertyOwners" binding:"required,min=1,dive"` +} + +// PropertyOwnerRequest holds property owner details in an application request +type PropertyOwnerRequest struct { + AdhaarNo uint64 `json:"adhaarNo" binding:"required"` + Name string `json:"name" binding:"required"` + ContactNo string `json:"contactNo" binding:"required"` + Email string `json:"email" binding:"omitempty,email"` + Gender string `json:"gender" binding:"required,oneof=MALE FEMALE OTHER"` + Guardian string `json:"guardian"` + GuardianType string `json:"guardianType" binding:"omitempty,oneof=FATHER MOTHER SPOUSE GUARDIAN OTHER"` + RelationshipToProperty string `json:"relationshipToProperty" binding:"omitempty,oneof=OWNER CO_OWNER JOINT_OWNER LEGAL_HEIR POWER_OF_ATTORNEY TENANT FAMILY_MEMBER OTHER"` + OwnershipShare float64 `json:"ownershipShare" binding:"required,min=0.01,max=100"` + IsPrimaryOwner bool `json:"isPrimaryOwner"` +} + +// UpdateApplicationRequest is used to update an existing application +type UpdateApplicationRequest struct { + Priority string `json:"priority" binding:"omitempty,oneof=LOW MEDIUM HIGH"` + DueDate *time.Time `json:"dueDate"` + AssignedAgent *uuid.UUID `json:"assignedAgent"` + AppliedBy string `json:"appliedBy"` + IsDraft *bool `json:"isDraft"` + PropertyOwners []PropertyOwnerRequest `json:"propertyOwners" binding:"omitempty,dive"` + ImportantNote *string `json:"importantNote" binding:"omitempty,max=500"` +} + +// UpdateStatusRequest is used to update the status of an application +type UpdateStatusRequest struct { + Status string `json:"status" binding:"required"` + Comments *string `json:"comments"` +} + +// ActionRequest is used for agent assignment and action requests +type ActionRequest struct { + Action string `json:"action"` + AgentID uuid.UUID `json:"agentId"` + Comments string `json:"comments"` + Verified bool `json:"verified"` + Approved bool `json:"approved"` +} + +// ApplicationResponse represents the application response +type ApplicationResponse struct { + ID uuid.UUID `json:"id"` + ApplicationNo string `json:"applicationNo"` + PropertyID uuid.UUID `json:"propertyId"` + Priority string `json:"priority"` + DueDate time.Time `json:"dueDate"` + AssignedAgent *uuid.UUID `json:"assignedAgent,omitempty"` + Status string `json:"status"` + WorkflowInstanceID string `json:"workflowInstanceId,omitempty"` + AppliedBy string `json:"appliedBy"` + AssesseeID *uuid.UUID `json:"assesseeId,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ImportantNote string `json:"importantNote"` +} + +// ApplicationSearchCriteria holds filters for searching applications +type ApplicationSearchCriteria struct { + Status string `form:"status"` + Priority string `form:"priority"` + PropertyID string `form:"propertyId"` + AssignedAgent string `form:"assignedAgent"` + AppliedBy string `form:"appliedBy"` + ApplicationNo string `form:"applicationNo"` + CreatedDateFrom *time.Time `form:"createdDateFrom"` + CreatedDateTo *time.Time `form:"createdDateTo"` + DueDateFrom *time.Time `form:"dueDateFrom"` + DueDateTo *time.Time `form:"dueDateTo"` + IsDraft *bool `form:"isDraft"` + ZoneNo string `form:"zoneNo"` + WardNo []string `form:"wardNo"` + AssesseeID string `form:"assesseeId"` + SortBy string `form:"sortBy"` + SortField string `form:"sortField"` // created_at or due_date + IsCountOnly bool `form:"isCountOnly"` + PropertyNo string `form:"propertyNo"` +} + +// ============================================================================ +// PROPERTY OWNER DTOs +// ============================================================================ + +// CreatePropertyOwnerRequest is used to add a new property owner +type CreatePropertyOwnerRequest struct { + PropertyID uuid.UUID `json:"propertyId" binding:"required"` + AdhaarNo uint64 `json:"adhaarNo" binding:"required"` + Name string `json:"name" binding:"required"` + ContactNo string `json:"contactNo" binding:"required"` + Email string `json:"email" binding:"omitempty,email"` + Gender string `json:"gender" binding:"required,oneof=MALE FEMALE OTHER"` + Guardian string `json:"guardian"` + GuardianType string `json:"guardianType" binding:"omitempty,oneof=FATHER MOTHER SPOUSE OTHER"` + RelationshipToProperty string `json:"relationshipToProperty" binding:"omitempty,oneof=OWNER CO_OWNER JOINT_OWNER LEGAL_HEIR POWER_OF_ATTORNEY OTHER"` + OwnershipShare float64 `json:"ownershipShare" binding:"required,min=0.01,max=100"` + IsPrimaryOwner bool `json:"isPrimaryOwner"` +} + +// PropertyOwnerResponse is returned as the response for property owner APIs +type PropertyOwnerResponse struct { + ID uuid.UUID `json:"id"` + ApplicationID string `json:"applicationId"` + PropertyID uuid.UUID `json:"propertyId"` + AdhaarNo uint64 `json:"adhaarNo"` + Name string `json:"name"` + ContactNo string `json:"contactNo"` + Email string `json:"email,omitempty"` + Gender string `json:"gender"` + Guardian string `json:"guardian,omitempty"` + GuardianType string `json:"guardianType,omitempty"` + RelationshipToProperty string `json:"relationshipToProperty,omitempty"` + OwnershipShare float64 `json:"ownershipShare"` + IsPrimaryOwner bool `json:"isPrimaryOwner"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// UpdatePropertyOwnerRequest is used to update property owner details +type UpdatePropertyOwnerRequest struct { + Name string `json:"name"` + ContactNo string `json:"contactNo"` + Email string `json:"email" binding:"omitempty,email"` + Gender string `json:"gender" binding:"omitempty,oneof=MALE FEMALE OTHER"` + Guardian string `json:"guardian"` + GuardianType string `json:"guardianType" binding:"omitempty,oneof=FATHER MOTHER SPOUSE GUARDIAN OTHER"` + RelationshipToProperty string `json:"relationshipToProperty" binding:"omitempty,oneof=OWNER CO_OWNER JOINT_OWNER LEGAL_HEIR POWER_OF_ATTORNEY TENANT FAMILY_MEMBER OTHER"` + OwnershipShare float64 `json:"ownershipShare" binding:"omitempty,min=0.01,max=100"` + IsPrimaryOwner bool `json:"isPrimaryOwner"` +} + +//IGRS DTOs + +type CreateIGRSRequest struct { + PropertyID uuid.UUID `json:"propertyId" binding:"required"` + Habitation string `json:"habitation" binding:"required"` + IGRSWard string `json:"igrsWard" binding:"omitempty"` + IGRSLocality string `json:"igrsLocality" binding:"omitempty"` + IGRSBlock string `json:"igrsBlock" binding:"omitempty"` + DoorNoFrom string `json:"doorNoFrom" binding:"omitempty"` + DoorNoTo string `json:"doorNoTo" binding:"omitempty"` + IGRSClassification string `json:"igrsClassification" binding:"omitempty"` + BuiltUpAreaPct *float64 `json:"builtUpAreaPct" binding:"omitempty,gte=0,lte=100"` + FrontSetback *float64 `json:"frontSetback" binding:"omitempty,gte=0"` + RearSetback *float64 `json:"rearSetback" binding:"omitempty,gte=0"` + SideSetback *float64 `json:"sideSetback" binding:"omitempty,gte=0"` + TotalPlinthArea *float64 `json:"totalPlinthArea" binding:"omitempty,gte=0"` +} + +type UpdateIGRSRequest = CreateIGRSRequest + +// CreateApplicationLogRequest represents request for creating application log +type CreateApplicationLogRequest struct { + Action string `json:"action"` + PerformedBy string `json:"performedBy" binding:"required"` + Comments string `json:"comments"` + Metadata map[string]interface{} `json:"metadata"` + FileStoreID *uuid.UUID `json:"fileStoreId"` + Actor string `json:"actor"` + ApplicationID uuid.UUID `json:"applicationId" binding:"required"` +} + +// UpdateApplicationLogRequest represents request for updating application log +type UpdateApplicationLogRequest struct { + Action string `json:"action,omitempty" binding:"omitempty,oneof=CREATE UPDATE FILE_UPLOAD STATUS_CHANGE"` + PerformedBy string `json:"performedBy,omitempty"` + Comments string `json:"comments,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + FileStoreID *uuid.UUID `json:"fileStoreId,omitempty"` +} diff --git a/property-tax/enumeration-backend/internal/handlers/additional_property_details_handler.go b/property-tax/enumeration-backend/internal/handlers/additional_property_details_handler.go new file mode 100644 index 0000000..038eb8f --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/additional_property_details_handler.go @@ -0,0 +1,222 @@ +// Package handlers contains HTTP handlers for API endpoints +package handlers + +import ( + "encoding/json" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/datatypes" +) + +type AdditionalPropertyDetailsHandler struct { + service services.AdditionalPropertyDetailsService // Business logic service +} + +// NewAdditionalPropertyDetailsHandler creates a new handler instance +func NewAdditionalPropertyDetailsHandler(service services.AdditionalPropertyDetailsService) *AdditionalPropertyDetailsHandler { + return &AdditionalPropertyDetailsHandler{ + service: service, + } +} + +// CreateAdditionalPropertyDetailsRequest is the request body for creating additional property details +type CreateAdditionalPropertyDetailsRequest struct { + FieldName string `json:"fieldName" binding:"required"` + FieldValue interface{} `json:"fieldValue" binding:"required"` + PropertyID uuid.UUID `json:"propertyId" binding:"required"` +} + +// UpdateAdditionalPropertyDetailsRequest is the request body for updating additional property details +type UpdateAdditionalPropertyDetailsRequest struct { + FieldName string `json:"fieldName" binding:"required"` + FieldValue interface{} `json:"fieldValue" binding:"required"` + PropertyID uuid.UUID `json:"propertyId" binding:"required"` +} + +// CreateAdditionalPropertyDetails handles POST to create a new additional property details record +func (h *AdditionalPropertyDetailsHandler) CreateAdditionalPropertyDetails(c *gin.Context) { + var req CreateAdditionalPropertyDetailsRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + // Convert interface{} to JSON bytes + fieldValueBytes, err := json.Marshal(req.FieldValue) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid field value format", err.Error())) + return + } + + details := models.AdditionalPropertyDetails{ + FieldName: req.FieldName, + FieldValue: datatypes.JSON(fieldValueBytes), + PropertyID: req.PropertyID, + } + + if err := h.service.CreateAdditionalPropertyDetails(c.Request.Context(), &details); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to create additional property details", err.Error())) + return + } + + c.JSON(http.StatusCreated, response.SuccessResponseBody("Additional property details created successfully", details)) +} + +// GetAdditionalPropertyDetailsByID handles GET by ID for additional property details +func (h *AdditionalPropertyDetailsHandler) GetAdditionalPropertyDetailsByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid additional property details ID", err.Error())) + return + } + + details, err := h.service.GetAdditionalPropertyDetailsByID(c.Request.Context(), id) + if err != nil { + if err.Error() == "additional property details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Additional property details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get additional property details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Additional property details retrieved successfully", details)) +} + +// UpdateAdditionalPropertyDetails handles PUT to update existing additional property details +func (h *AdditionalPropertyDetailsHandler) UpdateAdditionalPropertyDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid additional property details ID", err.Error())) + return + } + + var req UpdateAdditionalPropertyDetailsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + // Convert interface{} to JSON bytes + fieldValueBytes, err := json.Marshal(req.FieldValue) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid field value format", err.Error())) + return + } + + details := models.AdditionalPropertyDetails{ + ID: id, + FieldName: req.FieldName, + FieldValue: datatypes.JSON(fieldValueBytes), + PropertyID: req.PropertyID, + } + + if err := h.service.UpdateAdditionalPropertyDetails(c.Request.Context(), &details); err != nil { + if err.Error() == "additional property details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Additional property details not found", err.Error())) + return + } + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to update additional property details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Additional property details updated successfully", details)) +} + +// DeleteAdditionalPropertyDetails handles DELETE by ID for additional property details +func (h *AdditionalPropertyDetailsHandler) DeleteAdditionalPropertyDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid additional property details ID", err.Error())) + return + } + + if err := h.service.DeleteAdditionalPropertyDetails(c.Request.Context(), id); err != nil { + if err.Error() == "additional property details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Additional property details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to delete additional property details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Additional property details deleted successfully", nil)) +} + +// GetAllAdditionalPropertyDetails handles GET for all additional property details with pagination and filtering +func (h *AdditionalPropertyDetailsHandler) GetAllAdditionalPropertyDetails(c *gin.Context) { + page, _ := strconv.Atoi (c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + var propertyID *uuid.UUID + if propertyIDStr := c.Query("propertyId"); propertyIDStr != "" { + if id, err := uuid.Parse(propertyIDStr); err == nil { + propertyID = &id + } + } + + var fieldName *string + if fieldNameStr := c.Query("fieldName"); fieldNameStr != "" { + fieldName = &fieldNameStr + } + + details, total, err := h.service.GetAllAdditionalPropertyDetails(c.Request.Context(), page, size, propertyID, fieldName) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get additional property details", err.Error())) + return + } + + // Set pagination headers + c.Header(constants.HeaderTotalCount, strconv.FormatInt(total, 10)) + c.Header(constants.HeaderCurrentPage, strconv.Itoa(page)) + c.Header(constants.HeaderPerPage, strconv.Itoa(size)) + c.Header(constants.HeaderTotalPages, strconv.Itoa((int((total + int64(size) - 1) / int64(size))))) + + c.JSON(http.StatusOK, details) +} + +// GetAdditionalPropertyDetailsByPropertyID handles GET by property ID for additional property details +func (h *AdditionalPropertyDetailsHandler) GetAdditionalPropertyDetailsByPropertyID(c *gin.Context) { + propertyIDStr := c.Param("propertyId") + propertyID, err := uuid.Parse(propertyIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property ID", err.Error())) + return + } + + details, err := h.service.GetAdditionalPropertyDetailsByPropertyID(c.Request.Context(), propertyID) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get additional property details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Additional property details retrieved successfully", details)) +} + +// GetAdditionalPropertyDetailsByFieldName handles GET by field name for additional property details +func (h *AdditionalPropertyDetailsHandler) GetAdditionalPropertyDetailsByFieldName(c *gin.Context) { + fieldName := c.Param("fieldName") + if fieldName == "" { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Field name is required", "")) + return + } + + details, err := h.service.GetAdditionalPropertyDetailsByFieldName(c.Request.Context(), fieldName) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get additional property details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Additional property details retrieved successfully", details)) +} diff --git a/property-tax/enumeration-backend/internal/handlers/amenity_handler.go b/property-tax/enumeration-backend/internal/handlers/amenity_handler.go new file mode 100644 index 0000000..ccfdfb9 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/amenity_handler.go @@ -0,0 +1,202 @@ +package handlers + +import ( + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + "strconv" + "time" + "github.com/lib/pq" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// AmenityRequest represents the request body for creating/updating amenities +type AmenityRequest struct { + PropertyID string `json:"property_id" binding:"required,uuid"` + Type pq.StringArray `gorm:"type:text[]" json:"type" binding:"required,dive"` + Description string `json:"description"` + ExpiryDate *time.Time `json:"expiry_date"` +} + +type AmenityHandler struct { + service services.AmenityService +} + +func NewAmenityHandler(service services.AmenityService) *AmenityHandler { + return &AmenityHandler{service} +} + +func (h *AmenityHandler) GetAll(c *gin.Context) { + // Parse query parameters; service will enforce defaults/bounds + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + amenityType := c.Query("type") + propertyID := c.Query("propertyId") + + var ( + amenities []models.Amenities + total int64 + err error + ) + + // If no filters provided, call plain paginated GetAll; otherwise call filtered paginated method + if amenityType == "" && propertyID == "" { + amenities, total, err = h.service.GetAll(c.Request.Context(), page, size) + } else { + amenities, total, err = h.service.GetAllWithFilters(c.Request.Context(), page, size, amenityType, propertyID) + } + if err != nil { + response.InternalServerError(c, "Failed to retrieve amenities: "+err.Error()) + return + } + + // Calculate pagination meta (use the requested 'size' to compute pages; service has already clamped size) + totalPages := int((total + int64(size) - 1) / int64(size)) + + // Return paginated response + response.Paginated(c, amenities, response.PaginationMeta{ + Page: page, + PageSize: size, + TotalItems: total, + TotalPages: totalPages, + }) +} + +func (h *AmenityHandler) GetByID(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + response.BadRequest(c, "Invalid ID format") + return + } + + amenity, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + response.NotFound(c, "Amenity not found") + return + } + + response.Success(c, "Amenity retrieved successfully", amenity) +} + +func (h *AmenityHandler) Create(c *gin.Context) { + var req AmenityRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request body: "+err.Error()) + return + } + + // Parse property ID + propertyUUID, err := uuid.Parse(req.PropertyID) + if err != nil { + response.BadRequest(c, "Invalid property ID format") + return + } + + // Create amenity model + amenity := &models.Amenities{ + PropertyID: propertyUUID, + Type: req.Type, + Description: req.Description, + ExpiryDate: req.ExpiryDate, + } + + if err := h.service.Create(c.Request.Context(), amenity); err != nil { + response.InternalServerError(c, "Failed to create amenity: "+err.Error()) + return + } + + response.Created(c, map[string]interface{}{ + "message": "Amenity created successfully", + "data": amenity, + }) +} + +func (h *AmenityHandler) Update(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + response.BadRequest(c, "Invalid ID format") + return + } + + var req AmenityRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request body: "+err.Error()) + return + } + + // Parse property ID + propertyUUID, err := uuid.Parse(req.PropertyID) + if err != nil { + response.BadRequest(c, "Invalid property ID format") + return + } + + // Create amenity model for update + amenity := &models.Amenities{ + PropertyID: propertyUUID, + Type: req.Type, + Description: req.Description, + ExpiryDate: req.ExpiryDate, + } + + if err := h.service.Update(c.Request.Context(), id, amenity); err != nil { + response.InternalServerError(c, "Failed to update amenity: "+err.Error()) + return + } + + // Get updated amenity to return + updatedAmenity, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + response.InternalServerError(c, "Failed to retrieve updated amenity") + return + } + + response.Success(c, "Amenity updated successfully", updatedAmenity) +} + +func (h *AmenityHandler) Delete(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + response.BadRequest(c, "Invalid ID format") + return + } + + // Check if amenity exists + _, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + response.NotFound(c, "Amenity not found") + return + } + + if err := h.service.Delete(c.Request.Context(), id); err != nil { + response.InternalServerError(c, "Failed to delete amenity: "+err.Error()) + return + } + + response.Success(c, "Amenity deleted successfully", nil) +} + +func (h *AmenityHandler) GetByPropertyID(c *gin.Context) { + propertyId := c.Param("propertyId") + + // Validate UUID format + if _, err := uuid.Parse(propertyId); err != nil { + response.BadRequest(c, "Invalid property ID format") + return + } + + amenities, err := h.service.GetByPropertyID(c.Request.Context(), propertyId) + if err != nil { + response.InternalServerError(c, "Failed to retrieve amenities by property ID: "+err.Error()) + return + } + + response.Success(c, "Amenities retrieved successfully", amenities) +} \ No newline at end of file diff --git a/property-tax/enumeration-backend/internal/handlers/application_handler.go b/property-tax/enumeration-backend/internal/handlers/application_handler.go new file mode 100644 index 0000000..42aeed4 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/application_handler.go @@ -0,0 +1,425 @@ +// Package handlers contains HTTP handlers for API endpoints +package handlers + +import ( + "enumeration/internal/constants" + "enumeration/internal/dto" + "enumeration/internal/services" + + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ApplicationHandler handles HTTP requests for applications +type ApplicationHandler struct { + service services.ApplicationService +} + +// NewApplicationHandler creates a new ApplicationHandler instance +func NewApplicationHandler(service services.ApplicationService) *ApplicationHandler { + return &ApplicationHandler{service: service} +} + +// Create handles POST /applications to create a new application +func (h *ApplicationHandler) Create(c *gin.Context) { + var req dto.CreateApplicationRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + citizenID := c.GetHeader(constants.HeaderUserID) + tenantID := c.GetHeader(constants.HeaderTenantID) + application, err := h.service.Create(c.Request.Context(), tenantID, citizenID, &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": constants.ErrApplicationCreationFailed, + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "success": true, + "message": "Application created successfully", + "data": application, + }) +} + +// GetByID handles GET /applications/:id to fetch an application by ID +func (h *ApplicationHandler) GetByID(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid application ID", + "errors": []string{err.Error()}, + }) + return + } + + application, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": constants.ErrApplicationNotFound, + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application retrieved successfully", + "data": application, + }) +} + +// GetByApplicationNo handles GET /applications/by-number/:applicationNo to fetch by application number +func (h *ApplicationHandler) GetByApplicationNo(c *gin.Context) { + applicationNo := c.Param("applicationNo") + if applicationNo == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Application number is required", + "errors": []string{}, + }) + return + } + + application, err := h.service.GetByApplicationNo(c.Request.Context(), applicationNo) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": constants.ErrApplicationNotFound, + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application retrieved successfully", + "data": application, + }) +} + +// Update handles PUT /applications/:id to update an application +func (h *ApplicationHandler) Update(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid application ID", + "errors": []string{err.Error()}, + }) + return + } + + var req dto.UpdateApplicationRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + + application, err := h.service.Update(c.Request.Context(), id, &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": constants.ErrApplicationUpdateFailed, + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application updated successfully", + "data": application, + }) +} + +// UpdateStatus handles PATCH /applications/:id/status to update application status +func (h *ApplicationHandler) UpdateStatus(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid application ID", + "errors": []string{err.Error()}, + }) + return + } + + var req dto.UpdateStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + + if err := h.service.UpdateStatus(c.Request.Context(), id, &req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to update status", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Status updated successfully", + }) +} + +// Action handles PATCH /applications/:id/:action for assign, verify, audit-verify, approve +func (h *ApplicationHandler) Action(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid application ID", + "errors": []string{err.Error()}, + }) + return + } + tenantID := c.GetHeader(constants.HeaderTenantID) + userId := c.GetHeader(constants.HeaderUserID) + var req dto.ActionRequest + reqInterface, exists := c.Get("actionRequest") + if !exists { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{}, + }) + return + } + req = reqInterface.(dto.ActionRequest) + fmt.Printf("req: %v\n", req) + + switch req.Action { + case "assign": + if err := h.service.AssignAgent(c.Request.Context(), id, &req, tenantID); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to assign agent", + "errors": []string{err.Error()}, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Agent assigned successfully", + }) + + case "verify": + err := h.service.VerifyApplicationByAgent(c.Request.Context(), tenantID, userId, id.String(), &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to verify application", + "errors": []string{err.Error()}, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application verified successfully by agent", + }) + + case "audit-verify": + err := h.service.VerifyApplication(c.Request.Context(), tenantID, userId, id.String(), &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to audit verify application", + "errors": []string{err.Error()}, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application audit verified successfully by service manager", + }) + + case "approve": + err := h.service.ApproveApplication(c.Request.Context(), tenantID, userId, id.String(), &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to approve the application", + "errors": []string{err.Error()}, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application approved successfully by commissioner", + }) + + case "re-assign": + // service manager reassigns an agent + if err := h.service.AssignAgent(c.Request.Context(), id, &req, tenantID); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to reassign agent", + "errors": []string{err.Error()}, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Agent reassigned successfully", + }) + } +} + +// Delete handles DELETE /applications/:id to remove an application +func (h *ApplicationHandler) Delete(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid application ID", + "errors": []string{err.Error()}, + }) + return + } + + if err := h.service.Delete(c.Request.Context(), id); err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": constants.ErrApplicationDeletionFailed, + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application deleted successfully", + }) +} + +// List handles GET /applications to list all applications with pagination +func (h *ApplicationHandler) List(c *gin.Context) { + + userID := c.GetHeader(constants.HeaderUserID) + role := c.GetHeader(constants.HeaderUserRole) + verify := c.GetHeader(constants.HeaderStatus) + tenantID := c.GetHeader(constants.HeaderTenantID) + + fmt.Printf("\"in handler\": %v\n", "in handler") + // Parse pagination parameters + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + applications, total, err := h.service.List(c.Request.Context(), userID, tenantID, role, verify, page, size) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to retrieve applications", + "errors": []string{err.Error()}, + }) + return + } + + // Calculate pagination + totalPages := int(total) / size + if int(total)%size != 0 { + totalPages++ + } + + // Set pagination headers + c.Header(constants.HeaderTotalCount, strconv.FormatInt(total, 10)) + c.Header(constants.HeaderCurrentPage, strconv.Itoa(page)) + c.Header(constants.HeaderPerPage, strconv.Itoa(size)) + c.Header(constants.HeaderTotalPages, strconv.Itoa(totalPages)) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Applications retrieved successfully", + "data": applications, + "pagination": gin.H{ + "page": page, + "size": size, + "totalItems": total, + "totalPages": totalPages, + }, + }) +} + +// Search handles GET /applications/search to search applications with filters +func (h *ApplicationHandler) Search(c *gin.Context) { + var criteria dto.ApplicationSearchCriteria + if err := c.ShouldBindQuery(&criteria); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid search criteria", + "errors": []string{err.Error()}, + }) + return + } + + // Parse pagination parameters + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + applications, total, err := h.service.Search(c.Request.Context(), &criteria, page, size) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to search applications", + "errors": []string{err.Error()}, + }) + return + } + + // Calculate pagination + totalPages := int(total) / size + if int(total)%size != 0 { + totalPages++ + } + + // Set pagination headers + c.Header(constants.HeaderTotalCount, strconv.FormatInt(total, 10)) + c.Header(constants.HeaderCurrentPage, strconv.Itoa(page)) + c.Header(constants.HeaderPerPage, strconv.Itoa(size)) + c.Header(constants.HeaderTotalPages, strconv.Itoa(totalPages)) + if criteria.IsCountOnly { + c.JSON(http.StatusOK, gin.H{ + "totalItems": total, + + }, + ) + }else{ + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Applications retrieved successfully", + "data": applications, + "pagination": gin.H{ + "page": page, + "size": size, + "totalItems": total, + "totalPages": totalPages, + }, + }) +} +} diff --git a/property-tax/enumeration-backend/internal/handlers/application_log_handler.go b/property-tax/enumeration-backend/internal/handlers/application_log_handler.go new file mode 100644 index 0000000..f93f721 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/application_log_handler.go @@ -0,0 +1,187 @@ +package handlers + +import ( + "enumeration/internal/dto" + "enumeration/internal/services" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type ApplicationLogHandler struct { + service services.ApplicationLogService +} + +func NewApplicationLogHandler(service services.ApplicationLogService) *ApplicationLogHandler { + return &ApplicationLogHandler{ + service: service, + } +} + +// GetApplicationLogs retrieves all application logs with pagination and filtering +func (h *ApplicationLogHandler) GetApplicationLogs(ctx *gin.Context) { + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(ctx.DefaultQuery("size", "20")) + applicationID := ctx.Query("applicationId") + action := ctx.Query("action") + + logs, total, err := h.service.List(ctx, applicationID, action, page, size) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.Header("X-Total-Count", strconv.FormatInt(total, 10)) + ctx.Header("X-Current-Page", strconv.Itoa(page)) + ctx.Header("X-Per-Page", strconv.Itoa(size)) + + ctx.JSON(http.StatusOK, logs) +} + +// CreateApplicationLog creates a new application log entry +func (h *ApplicationLogHandler) CreateApplicationLog(ctx *gin.Context) { + var req dto.CreateApplicationLogRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + log, err := h.service.Create(ctx, &req) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to create application log", + "error": err.Error(), + }) + return + } + + ctx.JSON(http.StatusCreated, gin.H{ + "success": true, + "message": "Application log created successfully", + "data": log, + }) +} + +// GetApplicationLogByID retrieves a specific application log by ID +func (h *ApplicationLogHandler) GetApplicationLogByID(ctx *gin.Context) { + id, err := uuid.Parse(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid log ID"}) + return + } + + log, err := h.service.GetByID(ctx, id) + if err != nil { + if err.Error() == "record not found" { + ctx.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "Application log not found", + }) + return + } + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "success": true, + "data": log, + }) +} + +// UpdateApplicationLog updates an existing application log +func (h *ApplicationLogHandler) UpdateApplicationLog(ctx *gin.Context) { + id, err := uuid.Parse(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid log ID"}) + return + } + + var req dto.UpdateApplicationLogRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + log, err := h.service.Update(ctx, id, &req) + if err != nil { + if err.Error() == "record not found" { + ctx.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "Application log not found", + }) + return + } + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to update application log", + "error": err.Error(), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application log updated successfully", + "data": log, + }) +} + +// DeleteApplicationLog deletes an application log by ID +func (h *ApplicationLogHandler) DeleteApplicationLog(ctx *gin.Context) { + id, err := uuid.Parse(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid log ID"}) + return + } + + err = h.service.Delete(ctx, id) + if err != nil { + if err.Error() == "record not found" { + ctx.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "Application log not found", + }) + return + } + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to delete application log", + "error": err.Error(), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Application log deleted successfully", + }) +} + +// GetApplicationLogsByApplicationID retrieves all logs for a specific application +func (h *ApplicationLogHandler) GetApplicationLogsByApplicationID(ctx *gin.Context) { + applicationID, err := uuid.Parse(ctx.Param("applicationId")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid application ID"}) + return + } + + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(ctx.DefaultQuery("size", "20")) + action := ctx.Query("action") + + logs, total, err := h.service.GetByApplicationID(ctx, applicationID, action, page, size) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.Header("X-Total-Count", strconv.FormatInt(total, 10)) + ctx.Header("X-Current-Page", strconv.Itoa(page)) + ctx.Header("X-Per-Page", strconv.Itoa(size)) + + ctx.JSON(http.StatusOK, logs) +} diff --git a/property-tax/enumeration-backend/internal/handlers/assessment_details_handler.go b/property-tax/enumeration-backend/internal/handlers/assessment_details_handler.go new file mode 100644 index 0000000..4f87979 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/assessment_details_handler.go @@ -0,0 +1,167 @@ +// Package handlers contains HTTP handler implementations for the property tax enumeration system. +package handlers + +import ( + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" +) + +// AssessmentDetailsHandler handles HTTP requests for assessment details resources. +type AssessmentDetailsHandler struct { + assessmentDetailsService services.AssessmentDetailsService // Service layer for assessment details operations +} + +// NewAssessmentDetailsHandler creates a new AssessmentDetailsHandler with the provided service. +func NewAssessmentDetailsHandler(assessmentDetailsService services.AssessmentDetailsService) *AssessmentDetailsHandler { + return &AssessmentDetailsHandler{ + assessmentDetailsService: assessmentDetailsService, + } +} + +// CreateAssessmentDetails handles POST requests to create a new assessment details record. +// Validates the request body and delegates creation to the service layer. +func (h *AssessmentDetailsHandler) CreateAssessmentDetails(c *gin.Context) { + var assessmentDetails models.AssessmentDetails + + if err := c.ShouldBindJSON(&assessmentDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + if err := h.assessmentDetailsService.CreateAssessmentDetails(c.Request.Context(), &assessmentDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to create assessment details", err.Error())) + return + } + + c.JSON(http.StatusCreated, response.SuccessResponseBody("Assessment details created successfully", assessmentDetails)) +} + +// GetAssessmentDetailsByID handles GET requests to retrieve assessment details by their ID. +func (h *AssessmentDetailsHandler) GetAssessmentDetailsByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid assessment details ID", err.Error())) + return + } + + assessmentDetails, err := h.assessmentDetailsService.GetAssessmentDetailsByID(c.Request.Context(), id) + if err != nil { + if err.Error() == "assessment details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Assessment details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get assessment details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Assessment details retrieved successfully", assessmentDetails)) +} + +// UpdateAssessmentDetails handles PUT requests to update existing assessment details by their ID. +// Validates the request body and delegates update to the service layer. +func (h *AssessmentDetailsHandler) UpdateAssessmentDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid assessment details ID", err.Error())) + return + } + + var assessmentDetails models.AssessmentDetails + if err := c.ShouldBindJSON(&assessmentDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + assessmentDetails.ID = id + + if err := h.assessmentDetailsService.UpdateAssessmentDetails(c.Request.Context(), &assessmentDetails); err != nil { + if err.Error() == "assessment details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Assessment details not found", err.Error())) + return + } + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to update assessment details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Assessment details updated successfully", assessmentDetails)) +} + +// DeleteAssessmentDetails handles DELETE requests to remove assessment details by their ID. +func (h *AssessmentDetailsHandler) DeleteAssessmentDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid assessment details ID", err.Error())) + return + } + + if err := h.assessmentDetailsService.DeleteAssessmentDetails(c.Request.Context(), id); err != nil { + if err.Error() == "assessment details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Assessment details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to delete assessment details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Assessment details deleted successfully", nil)) +} + +// GetAllAssessmentDetails handles GET requests to retrieve all assessment details with pagination. +// Optionally filters by property ID and sets pagination headers. +func (h *AssessmentDetailsHandler) GetAllAssessmentDetails(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + var propertyID *uuid.UUID + if propertyIDStr := c.Query("propertyId"); propertyIDStr != "" { + if id, err := uuid.Parse(propertyIDStr); err == nil { + propertyID = &id + } + } + + assessmentDetails, total, err := h.assessmentDetailsService.GetAllAssessmentDetails(c.Request.Context(), page, size, propertyID) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get assessment details", err.Error())) + return + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + + c.JSON(http.StatusOK, assessmentDetails) +} + +// GetAssessmentDetailsByPropertyID handles GET requests to retrieve assessment details by property ID. +func (h *AssessmentDetailsHandler) GetAssessmentDetailsByPropertyID(c *gin.Context) { + propertyIDStr := c.Param("propertyId") + propertyID, err := uuid.Parse(propertyIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property ID", err.Error())) + return + } + + assessmentDetails, err := h.assessmentDetailsService.GetAssessmentDetailsByPropertyID(c.Request.Context(), propertyID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Assessment details not found for this property", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get assessment details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Assessment details retrieved successfully", assessmentDetails)) +} diff --git a/property-tax/enumeration-backend/internal/handlers/construction_details_handler.go b/property-tax/enumeration-backend/internal/handlers/construction_details_handler.go new file mode 100644 index 0000000..29f50ba --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/construction_details_handler.go @@ -0,0 +1,161 @@ +// Package handlers contains HTTP handler implementations for the property tax enumeration system. +package handlers + +import ( + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ConstructionDetailsHandler handles HTTP requests for construction details resources. +type ConstructionDetailsHandler struct { + constructionDetailsService services.ConstructionDetailsService // Service layer for construction details operations +} + +// NewConstructionDetailsHandler creates a new ConstructionDetailsHandler with the provided service. +func NewConstructionDetailsHandler(constructionDetailsService services.ConstructionDetailsService) *ConstructionDetailsHandler { + return &ConstructionDetailsHandler{ + constructionDetailsService: constructionDetailsService, + } +} + +// CreateConstructionDetails handles POST requests to create a new construction details record. +// Validates the request body and delegates creation to the service layer. +func (h *ConstructionDetailsHandler) CreateConstructionDetails(c *gin.Context) { + var constructionDetails models.ConstructionDetails + + if err := c.ShouldBindJSON(&constructionDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + if err := h.constructionDetailsService.CreateConstructionDetails(c.Request.Context(), &constructionDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to create construction details", err.Error())) + return + } + + c.JSON(http.StatusCreated, response.SuccessResponseBody("Construction details created successfully", constructionDetails)) +} + +// GetConstructionDetailsByID handles GET requests to retrieve construction details by their ID. +func (h *ConstructionDetailsHandler) GetConstructionDetailsByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid construction details ID", err.Error())) + return + } + + constructionDetails, err := h.constructionDetailsService.GetConstructionDetailsByID(c.Request.Context(), id) + if err != nil { + if err.Error() == "construction details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Construction details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get construction details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Construction details retrieved successfully", constructionDetails)) +} + +// UpdateConstructionDetails handles PUT requests to update existing construction details by their ID. +// Validates the request body and delegates update to the service layer. +func (h *ConstructionDetailsHandler) UpdateConstructionDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid construction details ID", err.Error())) + return + } + + var constructionDetails models.ConstructionDetails + if err := c.ShouldBindJSON(&constructionDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + constructionDetails.ID = id + + if err := h.constructionDetailsService.UpdateConstructionDetails(c.Request.Context(), &constructionDetails); err != nil { + if err.Error() == "construction details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Construction details not found", err.Error())) + return + } + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to update construction details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Construction details updated successfully", constructionDetails)) +} + +// DeleteConstructionDetails handles DELETE requests to remove construction details by their ID. +func (h *ConstructionDetailsHandler) DeleteConstructionDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid construction details ID", err.Error())) + return + } + + if err := h.constructionDetailsService.DeleteConstructionDetails(c.Request.Context(), id); err != nil { + if err.Error() == "construction details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Construction details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to delete construction details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Construction details deleted successfully", nil)) +} + +// GetAllConstructionDetails handles GET requests to retrieve all construction details with pagination. +// Optionally filters by property ID and sets pagination headers. +func (h *ConstructionDetailsHandler) GetAllConstructionDetails(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + var propertyID *uuid.UUID + if propertyIDStr := c.Query("propertyId"); propertyIDStr != "" { + if id, err := uuid.Parse(propertyIDStr); err == nil { + propertyID = &id + } + } + + constructionDetails, total, err := h.constructionDetailsService.GetAllConstructionDetails(c.Request.Context(), page, size, propertyID) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get construction details", err.Error())) + return + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + + c.JSON(http.StatusOK, constructionDetails) +} + +// GetConstructionDetailsByPropertyID handles GET requests to retrieve construction details by property ID. +func (h *ConstructionDetailsHandler) GetConstructionDetailsByPropertyID(c *gin.Context) { + propertyIDStr := c.Param("propertyId") + propertyID, err := uuid.Parse(propertyIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property ID", err.Error())) + return + } + + constructionDetails, err := h.constructionDetailsService.GetConstructionDetailsByPropertyID(c.Request.Context(), propertyID) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get construction details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Construction details retrieved successfully", constructionDetails)) +} diff --git a/property-tax/enumeration-backend/internal/handlers/coordinates_handler.go b/property-tax/enumeration-backend/internal/handlers/coordinates_handler.go new file mode 100644 index 0000000..ccc117a --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/coordinates_handler.go @@ -0,0 +1,297 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "enumeration/internal/models" + "enumeration/internal/services" + "io" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// CoordinatesHandler handles HTTP requests for coordinates resources. +type CoordinatesHandler struct { + service services.CoordinatesService // Service layer for coordinates operations +} + +// NewCoordinatesHandler creates a new CoordinatesHandler with the provided service. +func NewCoordinatesHandler(service services.CoordinatesService) *CoordinatesHandler { + return &CoordinatesHandler{service: service} +} + +// GetAll handles GET /coordinates and returns a paginated list of coordinates. +// Optionally filters by GIS Data ID and sets pagination headers. +func (h *CoordinatesHandler) GetAll(c *gin.Context) { + // Parse pagination parameters + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + // Parse optional filter + var gisDataID *uuid.UUID + if gisDataIDStr := c.Query("gisDataId"); gisDataIDStr != "" { + id, err := uuid.Parse(gisDataIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid gisDataId format", + "errors": []string{err.Error()}, + }) + return + } + gisDataID = &id + } + + coordinates, total, err := h.service.GetAll(c.Request.Context(), page, size, gisDataID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to retrieve coordinates", + "errors": []string{err.Error()}, + }) + return + } + + // Calculate pagination + totalPages := int(total) / size + if int(total)%size != 0 { + totalPages++ + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + c.Header("X-Total-Pages", strconv.Itoa(totalPages)) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Coordinates retrieved successfully", + "data": coordinates, + "pagination": gin.H{ + "page": page, + "size": size, + "totalItems": total, + "totalPages": totalPages, + }, + }) +} + +// Create handles POST /coordinates and creates a new coordinates record. +// Validates the request body and delegates creation to the service layer. +func (h *CoordinatesHandler) Create(c *gin.Context) { + var coordinates models.Coordinates + if err := c.ShouldBindJSON(&coordinates); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + + if err := h.service.Create(c.Request.Context(), &coordinates); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to create coordinates", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "success": true, + "message": "Coordinates created successfully", + "data": coordinates, + }) +} + +// GetByID handles GET /coordinates/:id and retrieves a coordinates record by its ID. +func (h *CoordinatesHandler) GetByID(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid coordinates ID", + "errors": []string{err.Error()}, + }) + return + } + + coordinates, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "Coordinates not found", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Coordinates retrieved successfully", + "data": coordinates, + }) +} + +// Update handles PUT /coordinates/:id and updates an existing coordinates record by its ID. +// Validates the request body and delegates update to the service layer. +func (h *CoordinatesHandler) Update(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid coordinates ID", + "errors": []string{err.Error()}, + }) + return + } + + var coordinates models.Coordinates + if err := c.ShouldBindJSON(&coordinates); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + + coordinates.ID = id + if err := h.service.Update(c.Request.Context(), &coordinates); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Failed to update coordinates", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Coordinates updated successfully", + "data": coordinates, + }) +} + +// Delete handles DELETE /coordinates/:id and removes a coordinates record by its ID. +func (h *CoordinatesHandler) Delete(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid coordinates ID", + "errors": []string{err.Error()}, + }) + return + } + + if err := h.service.Delete(c.Request.Context(), id); err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "Failed to delete coordinates", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Coordinates deleted successfully", + }) +} + +// CreateBatch handles POST /coordinates/batch and accepts either a single object or an array. +// Tries to unmarshal the request body as a slice first, then as a single object if that fails. +func (h *CoordinatesHandler) CreateBatch(c *gin.Context) { + raw, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "invalid request body", "error": err.Error()}) + return + } + + // Try unmarshalling into slice first + var coordsSlice []*models.Coordinates + if err := json.Unmarshal(raw, &coordsSlice); err == nil { + // slice succeeded + if len(coordsSlice) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"message": "no coordinates provided"}) + return + } + if err := h.service.CreateBatch(c.Request.Context(), coordsSlice); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "failed to create coordinates", "error": err.Error()}) + return + } + c.JSON(http.StatusCreated, gin.H{"message": "coordinates created successfully", "data": coordsSlice}) + return + } + + // If not a slice, try single object + var single models.Coordinates + if err := json.Unmarshal(raw, &single); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "invalid request payload", "error": err.Error()}) + return + } + if err := h.service.Create(c.Request.Context(), &single); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "failed to create coordinates", "error": err.Error()}) + return + } + c.JSON(http.StatusCreated, gin.H{"message": "coordinate created successfully", "data": single}) +} + +// ReplaceByGISDataID handles PUT /coordinates/gis/:gisDataId and replaces all coordinates for a given GIS Data ID. +// Accepts either an array or a single object in the request body. An empty body deletes all coordinates for the GIS Data ID. +func (h *CoordinatesHandler) ReplaceByGISDataID(c *gin.Context) { + gisIDStr := c.Param("gisDataId") + gisID, err := uuid.Parse(gisIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid gisDataId", + "errors": []string{err.Error()}, + }) + return + } + + // Read raw body to support either array or single object + raw, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid request body", "errors": []string{err.Error()}}) + return + } + trimmed := bytes.TrimLeft(raw, " \t\r\n") + var coordsSlice []*models.Coordinates + + if len(trimmed) == 0 { + // Empty body interpreted as delete all coordinates for this GISData + coordsSlice = []*models.Coordinates{} + } else if trimmed[0] == '[' { + if err := json.Unmarshal(raw, &coordsSlice); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid request body", "errors": []string{err.Error()}}) + return + } + } else { + var single models.Coordinates + if err := json.Unmarshal(raw, &single); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid request body", "errors": []string{err.Error()}}) + return + } + coordsSlice = append(coordsSlice, &single) + } + + // call service (service sets GISDataID on each coord and validates) + if err := h.service.ReplaceByGISDataID(c.Request.Context(), gisID, coordsSlice); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Failed to replace coordinates", "errors": []string{err.Error()}}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Coordinates replaced successfully", + "data": coordsSlice, + }) +} diff --git a/property-tax/enumeration-backend/internal/handlers/document_handler.go b/property-tax/enumeration-backend/internal/handlers/document_handler.go new file mode 100644 index 0000000..da23f97 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/document_handler.go @@ -0,0 +1,218 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + "errors" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// DocumentHandler handles HTTP requests for document resources. +type DocumentHandler struct { + service services.DocumentService // Service layer for document operations +} + +// NewDocumentHandler creates a new DocumentHandler with the provided service. +func NewDocumentHandler(service services.DocumentService) *DocumentHandler { + return &DocumentHandler{service: service} +} + +// Create handles POST requests to create a single document record. +// Validates the request body and delegates creation to the service layer. +func (h *DocumentHandler) Create(c *gin.Context) { + var doc models.Document + if err := c.ShouldBindJSON(&doc); err != nil { + response.BadRequest(c, "Invalid request body: "+err.Error()) + return + } + + if err := h.service.CreateDocument(c.Request.Context(), &doc); err != nil { + // Map validation errors to 400, others to 500 + if errors.Is(err, services.ErrValidation) { + response.BadRequest(c, "Invalid document: "+err.Error()) + return + } + response.InternalServerError(c, "Failed to create document: "+err.Error()) + return + } + + response.Created(c, response.SuccessResponseBody("Document created successfully", doc)) +} + +// CreateBatch handles POST requests to create multiple documents (batch). +// Accepts either a single Document object or an array of Document objects in the request body. +func (h *DocumentHandler) CreateBatch(c *gin.Context) { + // Read raw body so we can unmarshal conditionally + raw, err := c.GetRawData() + if err != nil { + response.BadRequest(c, "Invalid request body: "+err.Error()) + return + } + + // Trim leading whitespace to inspect first non-space char + trimmed := bytes.TrimLeft(raw, " \t\r\n") + var docs []*models.Document + + if len(trimmed) == 0 { + response.BadRequest(c, "Empty request body") + return + } + + // If JSON array -> unmarshal into slice + if trimmed[0] == '[' { + if err := json.Unmarshal(raw, &docs); err != nil { + response.BadRequest(c, "Invalid request body: "+err.Error()) + return + } + } else { // JSON object -> unmarshal single doc and wrap into slice + var doc models.Document + if err := json.Unmarshal(raw, &doc); err != nil { + response.BadRequest(c, "Invalid request body: "+err.Error()) + return + } + docs = append(docs, &doc) + } + + if err := h.service.CreateDocuments(c.Request.Context(), docs); err != nil { + if errors.Is(err, services.ErrValidation) { + response.BadRequest(c, "Invalid documents: "+err.Error()) + return + } + response.InternalServerError(c, "Failed to create documents: "+err.Error()) + return + } + + response.Created(c, response.SuccessResponseBody("Documents created successfully", docs)) +} + +// GetByID handles GET requests to retrieve a document by its ID. +func (h *DocumentHandler) GetByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + response.BadRequest(c, "Invalid document ID") + return + } + + doc, err := h.service.GetDocumentByID(c.Request.Context(), id) + if err != nil { + // validation error -> 400, otherwise map not found to 404 if service/repo returns that + if errors.Is(err, services.ErrValidation) { + response.BadRequest(c, "Invalid document ID: "+err.Error()) + return + } + response.NotFound(c, "Document not found") + return + } + + response.Success(c, "Document retrieved successfully", doc) +} + +// GetByPropertyID handles GET requests to retrieve all documents for a given property ID. +func (h *DocumentHandler) GetByPropertyID(c *gin.Context) { + propertyIDStr := c.Param("propertyId") + propertyID, err := uuid.Parse(propertyIDStr) + if err != nil { + response.BadRequest(c, "Invalid property ID") + return + } + + docs, err := h.service.GetDocumentsByPropertyID(c.Request.Context(), propertyID) + if err != nil { + if errors.Is(err, services.ErrValidation) { + response.BadRequest(c, "Invalid property ID: "+err.Error()) + return + } + response.InternalServerError(c, "Failed to retrieve documents: "+err.Error()) + return + } + + response.Success(c, "Documents retrieved successfully", docs) +} + +// GetAll handles GET requests to retrieve all documents with pagination. +// Optionally filters by property ID and sets pagination headers. +func (h *DocumentHandler) GetAll(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + var propertyID *uuid.UUID + if propertyIDStr := c.Query("propertyId"); propertyIDStr != "" { + if id, err := uuid.Parse(propertyIDStr); err == nil { + propertyID = &id + } else { + response.BadRequest(c, "Invalid propertyId query parameter") + return + } + } + + docs, total, err := h.service.GetAllDocuments(c.Request.Context(), page, size, propertyID) + if err != nil { + response.InternalServerError(c, "Failed to retrieve documents: "+err.Error()) + return + } + + response.Paginated(c, docs, response.PaginationMeta{ + Page: page, + PageSize: size, + TotalItems: total, + TotalPages: int((total + int64(size) - 1) / int64(size)), + }) +} + +// Delete handles DELETE requests to remove a document by its ID. +func (h *DocumentHandler) Delete(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + response.BadRequest(c, "Invalid document ID") + return + } + + if err := h.service.DeleteDocument(c.Request.Context(), id); err != nil { + if errors.Is(err, services.ErrValidation) { + response.BadRequest(c, "Invalid document ID: "+err.Error()) + return + } + response.InternalServerError(c, "Failed to delete document: "+err.Error()) + return + } + + response.Success(c, "Document deleted successfully", nil) +} + +// Update handles PUT requests to update an existing document by its ID. +// Validates the request body and delegates update to the service layer. +func (h *DocumentHandler) Update(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + response.BadRequest(c, "Invalid document ID") + return + } + + var doc models.Document + if err := c.ShouldBindJSON(&doc); err != nil { + response.BadRequest(c, "Invalid request body: "+err.Error()) + return + } + + doc.ID = id + + if err := h.service.UpdateDocument(c.Request.Context(), &doc); err != nil { + if errors.Is(err, services.ErrValidation) { + response.BadRequest(c, "Invalid document: "+err.Error()) + return + } + response.InternalServerError(c, "Failed to update document: "+err.Error()) + return + } + + response.Success(c, "Document updated successfully", doc) +} diff --git a/property-tax/enumeration-backend/internal/handlers/floor_details_handler.go b/property-tax/enumeration-backend/internal/handlers/floor_details_handler.go new file mode 100644 index 0000000..3c46fc7 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/floor_details_handler.go @@ -0,0 +1,142 @@ +package handlers + +import ( + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// FloorDetailsHandler handles HTTP requests for floor details resources. +type FloorDetailsHandler struct { + floorDetailsService services.FloorDetailsService // Service layer for floor details operations +} + +// NewFloorDetailsHandler creates a new FloorDetailsHandler with the provided service. +func NewFloorDetailsHandler(floorDetailsService services.FloorDetailsService) *FloorDetailsHandler { + return &FloorDetailsHandler{ + floorDetailsService: floorDetailsService, + } +} + +// CreateFloorDetails handles POST requests to create a new floor details record. +// Validates the request body and delegates creation to the service layer. +func (h *FloorDetailsHandler) CreateFloorDetails(c *gin.Context) { + var floorDetails models.FloorDetails + + if err := c.ShouldBindJSON(&floorDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + if err := h.floorDetailsService.CreateFloorDetails(c.Request.Context(), &floorDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to create floor details", err.Error())) + return + } + + c.JSON(http.StatusCreated, response.SuccessResponseBody("Floor details created successfully", floorDetails)) +} + +// GetFloorDetailsByID handles GET requests to retrieve floor details by their ID. +func (h *FloorDetailsHandler) GetFloorDetailsByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid floor details ID", err.Error())) + return + } + + floorDetails, err := h.floorDetailsService.GetFloorDetailsByID(c.Request.Context(), id) + if err != nil { + if err.Error() == "floor details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Floor details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get floor details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Floor details retrieved successfully", floorDetails)) +} + +// UpdateFloorDetails handles PUT requests to update existing floor details by their ID. +// Validates the request body and delegates update to the service layer. +func (h *FloorDetailsHandler) UpdateFloorDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid floor details ID", err.Error())) + return + } + + var floorDetails models.FloorDetails + if err := c.ShouldBindJSON(&floorDetails); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + floorDetails.ID = id + + if err := h.floorDetailsService.UpdateFloorDetails(c.Request.Context(), &floorDetails); err != nil { + if err.Error() == "floor details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Floor details not found", err.Error())) + return + } + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to update floor details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Floor details updated successfully", floorDetails)) +} + +// DeleteFloorDetails handles DELETE requests to remove floor details by their ID. +func (h *FloorDetailsHandler) DeleteFloorDetails(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid floor details ID", err.Error())) + return + } + + if err := h.floorDetailsService.DeleteFloorDetails(c.Request.Context(), id); err != nil { + if err.Error() == "floor details not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Floor details not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to delete floor details", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Floor details deleted successfully", nil)) +} + +// GetAllFloorDetails handles GET requests to retrieve all floor details with pagination. +// Optionally filters by construction details ID and sets pagination headers. +func (h *FloorDetailsHandler) GetAllFloorDetails(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + var constructionDetailsID *uuid.UUID + if constructionDetailsIDStr := c.Query("constructionDetailsId"); constructionDetailsIDStr != "" { + if id, err := uuid.Parse(constructionDetailsIDStr); err == nil { + constructionDetailsID = &id + } + } + + floorDetails, total, err := h.floorDetailsService.GetAllFloorDetails(c.Request.Context(), page, size, constructionDetailsID) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get floor details", err.Error())) + return + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + + c.JSON(http.StatusOK, floorDetails) +} diff --git a/property-tax/enumeration-backend/internal/handlers/gis_handler.go b/property-tax/enumeration-backend/internal/handlers/gis_handler.go new file mode 100644 index 0000000..2c5e37d --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/gis_handler.go @@ -0,0 +1,237 @@ +package handlers + +import ( + "enumeration/internal/models" + "enumeration/internal/services" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// GISHandler handles HTTP requests for GIS data resources. +type GISHandler struct { + service services.GISService // Service layer for GIS data operations +} + +// NewGISHandler creates a new GISHandler with the provided service. +func NewGISHandler(service services.GISService) *GISHandler { + return &GISHandler{service: service} +} + +// GetAll handles GET /gis-data and returns a paginated list of all GIS data. +// Sets pagination headers in the response. +func (h *GISHandler) GetAll(c *gin.Context) { + // Parse pagination parameters + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + gisDataList, total, err := h.service.GetAllGISData(c.Request.Context(), page, size) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to retrieve GIS data", + "errors": []string{err.Error()}, + }) + return + } + + // Calculate pagination + totalPages := int(total) / size + if int(total)%size != 0 { + totalPages++ + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + c.Header("X-Total-Pages", strconv.Itoa(totalPages)) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "GIS data retrieved successfully", + "data": gisDataList, + "pagination": gin.H{ + "page": page, + "size": size, + "totalItems": total, + "totalPages": totalPages, + }, + }) +} + +// Create handles POST /gis-data and creates a new GIS data record. +// Validates the request body and delegates creation to the service layer. +func (h *GISHandler) Create(c *gin.Context) { + var gisData models.GISData + if err := c.ShouldBindJSON(&gisData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + + if err := h.service.CreateGISData(c.Request.Context(), &gisData); err != nil { + // Check if it's a validation error or conflict + statusCode := http.StatusBadRequest + if err.Error() == "GIS data already exists for this property" { + statusCode = http.StatusConflict + } + + c.JSON(statusCode, gin.H{ + "success": false, + "message": "Failed to create GIS data", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "success": true, + "message": "GIS data created successfully", + "data": gisData, + }) +} + +// GetByID handles GET /gis-data/{id} and retrieves a GIS data record by its ID. +func (h *GISHandler) GetByID(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid GIS data ID", + "errors": []string{err.Error()}, + }) + return + } + + gisData, err := h.service.GetGISDataByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "GIS data not found", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "GIS data retrieved successfully", + "data": gisData, + }) +} + +// Update handles PUT /gis-data/{id} and updates an existing GIS data record by its ID. +// Validates the request body and delegates update to the service layer. +func (h *GISHandler) Update(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid GIS data ID", + "errors": []string{err.Error()}, + }) + return + } + + var gisData models.GISData + if err := c.ShouldBindJSON(&gisData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + + // Set the ID from the URL parameter + gisData.ID = id + + if err := h.service.UpdateGISData(c.Request.Context(), &gisData); err != nil { + // Check if it's a not found error + statusCode := http.StatusBadRequest + if err.Error() == "GIS data not found" { + statusCode = http.StatusNotFound + } + + c.JSON(statusCode, gin.H{ + "success": false, + "message": "Failed to update GIS data", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "GIS data updated successfully", + "data": gisData, + }) +} + +// Delete handles DELETE /gis-data/{id} and removes a GIS data record by its ID. +func (h *GISHandler) Delete(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid GIS data ID", + "errors": []string{err.Error()}, + }) + return + } + + if err := h.service.DeleteGISData(c.Request.Context(), id); err != nil { + // Check if it's a not found error + statusCode := http.StatusInternalServerError + if err.Error() == "GIS data not found" { + statusCode = http.StatusNotFound + } + + c.JSON(statusCode, gin.H{ + "success": false, + "message": "Failed to delete GIS data", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "GIS data deleted successfully", + }) +} + +// GetByPropertyID handles GET /gis-data/property/{propertyId} and retrieves GIS data by property ID. +func (h *GISHandler) GetByPropertyID(c *gin.Context) { + propertyID, err := uuid.Parse(c.Param("propertyId")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid property ID", + "errors": []string{err.Error()}, + }) + return + } + + gisData, err := h.service.GetGISDataByPropertyID(c.Request.Context(), propertyID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "GIS data not found for property", + "errors": []string{err.Error()}, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "GIS data retrieved successfully", + "data": gisData, + }) +} diff --git a/property-tax/enumeration-backend/internal/handlers/igrs_handler.go b/property-tax/enumeration-backend/internal/handlers/igrs_handler.go new file mode 100644 index 0000000..804498d --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/igrs_handler.go @@ -0,0 +1,109 @@ +// Package handlers contains HTTP handler implementations for the property tax enumeration system. +package handlers + +import ( + "enumeration/internal/dto" + "enumeration/internal/services" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// IGRSHandler handles HTTP requests for IGRS (Integrated Grievance Redressal System) resources. +type IGRSHandler struct { + service services.IGRSService // Service layer for IGRS operations +} + +// NewIGRSHandler creates a new IGRSHandler with the provided service. +func NewIGRSHandler(s services.IGRSService) *IGRSHandler { + return &IGRSHandler{service: s} +} + +// Create handles POST requests to create a new IGRS record. +// Validates the request body and delegates creation to the service layer. +func (h *IGRSHandler) Create(c *gin.Context) { + var req dto.CreateIGRSRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid request body", "errors": []string{err.Error()}}) + return + } + out, err := h.service.Create(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to create", "errors": []string{err.Error()}}) + return + } + c.JSON(http.StatusCreated, gin.H{"success": true, "message": "IGRS created", "data": out}) +} + +// GetByID handles GET requests to retrieve an IGRS record by its ID. +func (h *IGRSHandler) GetByID(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid id", "errors": []string{err.Error()}}) + return + } + out, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "Not found", "errors": []string{err.Error()}}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) +} + +// Update handles PUT requests to update an existing IGRS record by its ID. +// Validates the request body and delegates update to the service layer. +func (h *IGRSHandler) Update(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid id", "errors": []string{err.Error()}}) + return + } + var req dto.UpdateIGRSRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid request body", "errors": []string{err.Error()}}) + return + } + out, err := h.service.Update(c.Request.Context(), id, &req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to update", "errors": []string{err.Error()}}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "Updated", "data": out}) +} + +// Delete handles DELETE requests to remove an IGRS record by its ID. +func (h *IGRSHandler) Delete(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid id", "errors": []string{err.Error()}}) + return + } + if err := h.service.Delete(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to delete", "errors": []string{err.Error()}}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "Deleted"}) +} + +// List handles GET requests to retrieve a paginated list of IGRS records. +// Sets pagination headers and returns the result set. +func (h *IGRSHandler) List(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + items, total, err := h.service.List(c.Request.Context(), page, size) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to list", "errors": []string{err.Error()}}) + return + } + totalPages := int(total) / size + if int(total)%size != 0 { + totalPages++ + } + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + c.Header("X-Total-Pages", strconv.Itoa(totalPages)) + c.JSON(http.StatusOK, gin.H{"success": true, "data": items}) +} diff --git a/property-tax/enumeration-backend/internal/handlers/property_address_handler.go b/property-tax/enumeration-backend/internal/handlers/property_address_handler.go new file mode 100644 index 0000000..9704a67 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/property_address_handler.go @@ -0,0 +1,235 @@ +package handlers + +import ( + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// PropertyAddressHandler handles HTTP requests for property address resources. +type PropertyAddressHandler struct { + propertyAddressService services.PropertyAddressService // Service layer for property address operations +} + +// NewPropertyAddressHandler creates a new PropertyAddressHandler with the provided service. +func NewPropertyAddressHandler(propertyAddressService services.PropertyAddressService) *PropertyAddressHandler { + return &PropertyAddressHandler{ + propertyAddressService: propertyAddressService, + } +} + +// CreatePropertyAddress handles POST requests to create a new property address record. +// Validates the request body and delegates creation to the service layer. +func (h *PropertyAddressHandler) CreatePropertyAddress(c *gin.Context) { + var address models.PropertyAddress + + if err := c.ShouldBindJSON(&address); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + if err := h.propertyAddressService.CreatePropertyAddress(c.Request.Context(), &address); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to create property address", err.Error())) + return + } + + c.JSON(http.StatusCreated, response.SuccessResponseBody("Property address created successfully", address)) +} + +// GetPropertyAddressByID handles GET requests to retrieve a property address by its ID. +func (h *PropertyAddressHandler) GetPropertyAddressByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property address ID", err.Error())) + return + } + + address, err := h.propertyAddressService.GetPropertyAddressByID(c.Request.Context(), id) + if err != nil { + if err.Error() == "property address not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Property address not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get property address", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property address retrieved successfully", address)) +} + +// UpdatePropertyAddress handles PUT requests to update an existing property address by its ID. +// Validates the request body and delegates update to the service layer. +func (h *PropertyAddressHandler) UpdatePropertyAddress(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property address ID", err.Error())) + return + } + + var address models.PropertyAddress + if err := c.ShouldBindJSON(&address); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + address.ID = id + + if err := h.propertyAddressService.UpdatePropertyAddress(c.Request.Context(), &address); err != nil { + if err.Error() == "property address not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Property address not found", err.Error())) + return + } + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to update property address", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property address updated successfully", address)) +} + +// DeletePropertyAddress handles DELETE requests to remove a property address by its ID. +func (h *PropertyAddressHandler) DeletePropertyAddress(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property address ID", err.Error())) + return + } + + if err := h.propertyAddressService.DeletePropertyAddress(c.Request.Context(), id); err != nil { + if err.Error() == "property address not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Property address not found", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to delete property address", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property address deleted successfully", nil)) +} + +// GetAllPropertyAddresses handles GET requests to retrieve all property addresses with pagination and optional filtering by property ID. +// Sets pagination headers in the response. +func (h *PropertyAddressHandler) GetAllPropertyAddresses(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + var propertyID *uuid.UUID + if propertyIDStr := c.Query("propertyId"); propertyIDStr != "" { + if id, err := uuid.Parse(propertyIDStr); err == nil { + propertyID = &id + } + } + + addresses, total, err := h.propertyAddressService.GetAllPropertyAddresses(c.Request.Context(), page, size, propertyID) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get property addresses", err.Error())) + return + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + + c.JSON(http.StatusOK, addresses) +} + +// GetPropertyAddressByPropertyID handles GET requests to retrieve a property address by property ID. +func (h *PropertyAddressHandler) GetPropertyAddressByPropertyID(c *gin.Context) { + propertyIDStr := c.Param("propertyId") + propertyID, err := uuid.Parse(propertyIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property ID", err.Error())) + return + } + + address, err := h.propertyAddressService.GetPropertyAddressByPropertyID(c.Request.Context(), propertyID) + if err != nil { + if err.Error() == "property address not found" { + c.JSON(http.StatusNotFound, response.ErrorResponseBody("Property address not found for this property", err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get property address", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property address retrieved successfully", address)) +} + +// SearchPropertyAddresses handles GET requests to search property addresses with advanced filters and pagination. +// Supports filtering by property ID, locality, zone, ward, block, street, election ward, secretariat ward, and pin code. +// Sets pagination headers in the response. +func (h *PropertyAddressHandler) SearchPropertyAddresses(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + sortBy := c.DefaultQuery("sortBy", "createdAt") + sortOrder := c.DefaultQuery("sortOrder", "desc") + + params := services.SearchPropertyAddressParams{ + Page: page, + Size: size, + SortBy: sortBy, + SortOrder: sortOrder, + } + + // Optional filters + if propertyID := c.Query("propertyId"); propertyID != "" { + if id, err := uuid.Parse(propertyID); err == nil { + params.PropertyID = &id + } + } + + if locality := c.Query("locality"); locality != "" { + params.Locality = &locality + } + + if zoneNo := c.Query("zoneNo"); zoneNo != "" { + params.ZoneNo = &zoneNo + } + + if wardNo := c.Query("wardNo"); wardNo != "" { + params.WardNo = &wardNo + } + + if blockNo := c.Query("blockNo"); blockNo != "" { + params.BlockNo = &blockNo + } + + if street := c.Query("street"); street != "" { + params.Street = &street + } + + if electionWard := c.Query("electionWard"); electionWard != "" { + params.ElectionWard = &electionWard + } + + if secretariatWard := c.Query("secretariatWard"); secretariatWard != "" { + params.SecretariatWard = &secretariatWard + } + + if pinCodeStr := c.Query("pinCode"); pinCodeStr != "" { + if pinCode, err := strconv.ParseUint(pinCodeStr, 10, 64); err == nil { + params.PinCode = &pinCode + } + } + + addresses, total, err := h.propertyAddressService.SearchPropertyAddresses(c.Request.Context(), params) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to search property addresses", err.Error())) + return + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + + c.JSON(http.StatusOK, addresses) +} diff --git a/property-tax/enumeration-backend/internal/handlers/property_handler.go b/property-tax/enumeration-backend/internal/handlers/property_handler.go new file mode 100644 index 0000000..a68c428 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/property_handler.go @@ -0,0 +1,225 @@ +package handlers + +import ( + "net/http" + "strconv" + + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/services" + "enumeration/pkg/response" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// PropertyHandler handles HTTP requests for property resources. +type PropertyHandler struct { + propertyService services.PropertyService // Service layer for property operations +} + +// NewPropertyHandler creates a new PropertyHandler with the provided service. +func NewPropertyHandler(propertyService services.PropertyService) *PropertyHandler { + return &PropertyHandler{ + propertyService: propertyService, + } +} + +// CreateProperty handles POST requests to create a new property record. +// Validates the request body and delegates creation to the service layer. +func (h *PropertyHandler) CreateProperty(c *gin.Context) { + var property models.Property + + if err := c.ShouldBindJSON(&property); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + if err := h.propertyService.CreateProperty(c.Request.Context(), &property); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Failed to create property", err.Error())) + return + } + + c.JSON(http.StatusCreated, response.SuccessResponseBody("Property created successfully", property)) +} + +// GetPropertyByID handles GET requests to retrieve a property by its ID. +func (h *PropertyHandler) GetPropertyByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property ID", err.Error())) + return + } + + property, err := h.propertyService.GetPropertyByID(c.Request.Context(), id) + if err != nil { + if err.Error() == constants.ErrPropertyNotFound { + c.JSON(http.StatusNotFound, response.ErrorResponseBody(constants.ErrPropertyNotFound, err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get property", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property retrieved successfully", property)) +} + +// UpdateProperty handles PUT requests to update an existing property by its ID. +// Validates the request body and delegates update to the service layer. +func (h *PropertyHandler) UpdateProperty(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property ID", err.Error())) + return + } + + var property models.Property + if err := c.ShouldBindJSON(&property); err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid request body", err.Error())) + return + } + + property.ID = id + + if err := h.propertyService.UpdateProperty(c.Request.Context(), &property); err != nil { + + c.JSON(http.StatusNotFound, response.ErrorResponseBody(err.Error())) + return + + } + updatedProperty, err := h.propertyService.GetPropertyByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to retrieve updated property", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property updated successfully", updatedProperty)) +} + +// DeleteProperty handles DELETE requests to remove a property by its ID. +func (h *PropertyHandler) DeleteProperty(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Invalid property ID", err.Error())) + return + } + + if err := h.propertyService.DeleteProperty(c.Request.Context(), id); err != nil { + if err.Error() == constants.ErrPropertyNotFound { + c.JSON(http.StatusNotFound, response.ErrorResponseBody(constants.ErrPropertyNotFound, err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to delete property", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property deleted successfully", nil)) +} + +// GetAllProperties handles GET requests to retrieve all properties with pagination and optional filtering by property type. +// Sets pagination headers in the response. +func (h *PropertyHandler) GetAllProperties(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + + var propertyType *string + if propertyTypeStr := c.Query("propertyType"); propertyTypeStr != "" { + propertyType = &propertyTypeStr + } + + properties, total, err := h.propertyService.GetAllProperties(c.Request.Context(), page, size, propertyType) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get properties", err.Error())) + return + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + + c.JSON(http.StatusOK, properties) +} + +// GetPropertyByPropertyNo handles GET requests to retrieve a property by its property number. +func (h *PropertyHandler) GetPropertyByPropertyNo(c *gin.Context) { + propertyNo := c.Param("propertyNo") + if propertyNo == "" { + c.JSON(http.StatusBadRequest, response.ErrorResponseBody("Property number is required", "")) + return + } + + property, err := h.propertyService.GetPropertyByPropertyNo(c.Request.Context(), propertyNo) + if err != nil { + if err.Error() == constants.ErrPropertyNotFound { + c.JSON(http.StatusNotFound, response.ErrorResponseBody(constants.ErrPropertyNotFound, err.Error())) + return + } + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to get property", err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessResponseBody("Property retrieved successfully", property)) +} + +// SearchProperties handles GET requests to search properties with advanced filters and pagination. +// Supports filtering by property type, ownership type, complex name, locality, ward, zone, and street. +// Sets pagination headers in the response. +func (h *PropertyHandler) SearchProperties(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + sortBy := c.DefaultQuery("sortBy", "createdAt") + sortOrder := c.DefaultQuery("sortOrder", "desc") + + params := services.SearchPropertyParams{ + Page: page, + Size: size, + SortBy: sortBy, + SortOrder: sortOrder, + } + + // Optional filters + if propertyType := c.Query("propertyType"); propertyType != "" { + params.PropertyType = &propertyType + } + + if ownershipType := c.Query("ownershipType"); ownershipType != "" { + params.OwnershipType = &ownershipType + } + + if complexName := c.Query("complexName"); complexName != "" { + params.ComplexName = &complexName + } + + if locality := c.Query("locality"); locality != "" { + params.Locality = &locality + } + + if wardNo := c.Query("wardNo"); wardNo != "" { + params.WardNo = &wardNo + } + + if zoneNo := c.Query("zoneNo"); zoneNo != "" { + params.ZoneNo = &zoneNo + } + + if street := c.Query("street"); street != "" { + params.Street = &street + } + + properties, total, err := h.propertyService.SearchProperties(c.Request.Context(), params) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponseBody("Failed to search properties", err.Error())) + return + } + + // Set pagination headers + c.Header("X-Total-Count", strconv.FormatInt(total, 10)) + c.Header("X-Current-Page", strconv.Itoa(page)) + c.Header("X-Per-Page", strconv.Itoa(size)) + + c.JSON(http.StatusOK, properties) +} diff --git a/property-tax/enumeration-backend/internal/handlers/property_owner_handler.go b/property-tax/enumeration-backend/internal/handlers/property_owner_handler.go new file mode 100644 index 0000000..73a7908 --- /dev/null +++ b/property-tax/enumeration-backend/internal/handlers/property_owner_handler.go @@ -0,0 +1,176 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "enumeration/internal/constants" + "enumeration/internal/dto" + "enumeration/internal/services" + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// PropertyOwnerHandler handles HTTP requests for property owner resources. +type PropertyOwnerHandler struct { + service services.PropertyOwnerService // Service layer for property owner operations +} + +// NewPropertyOwnerHandler creates a new PropertyOwnerHandler with the provided service. +func NewPropertyOwnerHandler(service services.PropertyOwnerService) *PropertyOwnerHandler { + return &PropertyOwnerHandler{service: service} +} + +// Create handles POST /property-owners (single). +// Validates the request body and delegates creation to the service layer. +func (h *PropertyOwnerHandler) Create(c *gin.Context) { + var req dto.CreatePropertyOwnerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid request body", "errors": []string{err.Error()}}) + return + } + + owner, err := h.service.Create(c.Request.Context(), &req) + if err != nil { + if errors.Is(err, services.ErrValidation) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid property owner", "errors": []string{err.Error()}}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to create property owner", "errors": []string{err.Error()}}) + return + } + + c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Property owner created successfully", "data": owner}) +} + +// CreateBatch handles POST /property-owners/batch and accepts either a single object or an array. +// Supports both single and batch creation of property owners by detecting the request body type. +func (h *PropertyOwnerHandler) CreateBatch(c *gin.Context) { + raw, err := c.GetRawData() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid request body", "errors": []string{err.Error()}}) + return + } + trimmed := bytes.TrimLeft(raw, " \t\r\n") + if len(trimmed) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Empty request body"}) + return + } + + var reqPtrs []*dto.CreatePropertyOwnerRequest + if trimmed[0] == '[' { + // Handle array of property owners + var arr []dto.CreatePropertyOwnerRequest + if err := json.Unmarshal(raw, &arr); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid request body", "errors": []string{err.Error()}}) + return + } + reqPtrs = make([]*dto.CreatePropertyOwnerRequest, 0, len(arr)) + for i := range arr { + reqPtrs = append(reqPtrs, &arr[i]) + } + } else { + // Handle single property owner + var single dto.CreatePropertyOwnerRequest + if err := json.Unmarshal(raw, &single); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid request body", "errors": []string{err.Error()}}) + return + } + reqPtrs = []*dto.CreatePropertyOwnerRequest{&single} + } + + owners, err := h.service.CreatePropertyOwners(c.Request.Context(), reqPtrs) + if err != nil { + if errors.Is(err, services.ErrValidation) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid property owners payload", "errors": []string{err.Error()}}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to create property owners", "errors": []string{err.Error()}}) + return + } + + c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Property owners created successfully", "data": owners}) +} + +// GetByPropertyID handles GET /property-owners/property/:propertyId?page=0&size=20 +// Retrieves property owners for a given property ID with pagination support. +// Sets pagination headers in the response. +func (h *PropertyOwnerHandler) GetByPropertyID(c *gin.Context) { + propertyID, err := uuid.Parse(c.Param("propertyId")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid property ID", "errors": []string{err.Error()}}) + return + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + if size <= 0 { + size = 20 + } + owners, total, err := h.service.GetByPropertyID(c.Request.Context(), propertyID, page, size) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to retrieve property owners", "errors": []string{err.Error()}}) + return + } + + // Calculate total pages for pagination + totalPages := 0 + if total > 0 { + totalPages = int((total + int64(size) - 1) / int64(size)) + } + + c.Header(constants.HeaderTotalCount, strconv.FormatInt(total, 10)) + c.Header(constants.HeaderCurrentPage, strconv.Itoa(page)) + c.Header(constants.HeaderPerPage, strconv.Itoa(size)) + c.Header(constants.HeaderTotalPages, strconv.Itoa(totalPages)) + + c.JSON(http.StatusOK, gin.H{"success": true, "message": "Property owners retrieved successfully", "data": owners}) +} + +// Update handles PUT /property-owners/:id +// Updates an existing property owner by ID. Validates the request body and delegates update to the service layer. +func (h *PropertyOwnerHandler) Update(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid property owner ID", "errors": []string{err.Error()}}) + return + } + + var req dto.UpdatePropertyOwnerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid request body", "errors": []string{err.Error()}}) + return + } + + owner, err := h.service.Update(c.Request.Context(), id, &req) + if err != nil { + if errors.Is(err, services.ErrValidation) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid update payload", "errors": []string{err.Error()}}) + return + } + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Failed to update property owner", "errors": []string{err.Error()}}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true, "message": "Property owner updated successfully", "data": owner}) +} + +// Delete handles DELETE /property-owners/:id +// Deletes a property owner by ID. Delegates deletion to the service layer. +func (h *PropertyOwnerHandler) Delete(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid property owner ID", "errors": []string{err.Error()}}) + return + } + + if err := h.service.Delete(c.Request.Context(), id); err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "Failed to delete property owner", "errors": []string{err.Error()}}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true, "message": "Property owner deleted successfully"}) +} diff --git a/property-tax/enumeration-backend/internal/middleware/auth_middleware.go b/property-tax/enumeration-backend/internal/middleware/auth_middleware.go new file mode 100644 index 0000000..c2e79ce --- /dev/null +++ b/property-tax/enumeration-backend/internal/middleware/auth_middleware.go @@ -0,0 +1,343 @@ +// Package middleware provides authentication and authorization middleware for the API +package middleware + +import ( + "context" + "encoding/json" + "enumeration/internal/config" + "enumeration/internal/constants" + "enumeration/internal/dto" + "enumeration/internal/security" + "enumeration/pkg/logger" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +// MDMSResponse represents the response structure from MDMS API +type MDMSResponse struct { + MDMS []MDMSData `json:"mdms"` +} + +// MDMSData holds individual MDMS API role data +type MDMSData struct { + ID string `json:"id"` + TenantID string `json:"tenantId"` + SchemaCode string `json:"schemaCode"` + UniqueIdentifier string `json:"uniqueIdentifier"` + Data APIRoleData `json:"data"` + IsActive bool `json:"isActive"` +} + +// APIRoleData holds allowed roles for an API endpoint +type APIRoleData struct { + Method string `json:"method"` + Endpoint string `json:"endpoint"` + AllowedRoles []string `json:"allowedRoles"` +} + +// RoleActionData holds actions allowed for a role +type RoleActionData struct { + Role string `json:"role"` + Actions []string `json:"actions"` +} + +// rolePermissions holds role-action permissions loaded from MDMS +var rolePermissions map[string][]string + +// mdmsRolePermissions holds endpoint-role permissions loaded from MDMS +var mdmsRolePermissions map[string][]string + +// Init loads role and endpoint permissions from MDMS at startup +func Init() { + // Load role-action permissions from MDMS + roleActionPermissions, err := loadRoleActionsFromMDMS() + if err != nil { + logger.Error("\nFailed to load role-action permissions from MDMS:\n", err) + } else { + rolePermissions = roleActionPermissions + logger.Info("\nSuccessfully loaded role-action permissions from MDMS :\t", rolePermissions, "\n") + } + + // Load MDMS API roles once at startup + mdmsRoles, err := loadAllMDMSRoles() + if err != nil { + logger.Error("\nFailed to load MDMS API roles:\n", err) + // Initialize empty map if MDMS fails + mdmsRolePermissions = make(map[string][]string) + } else { + mdmsRolePermissions = mdmsRoles + logger.Info("\nSuccessfully loaded MDMS API roles :\t", mdmsRolePermissions, "\n") + } +} + +// RoleMiddleware checks if the user has the necessary role to perform the action specified in the request. +func ActionMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Extract Keycloak claims from context (set by AuthMiddleware) + claimsVal, exists := c.Get("claims") + if !exists { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "MissingClaims"}) + return + } + claims, ok := claimsVal.(*security.KeycloakClaims) + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "InvalidClaimsType"}) + return + } + + // Extract roles from Keycloak claims (RealmAccess["roles"]) + var roles []string + if claims.RealmAccess != nil { + if r, ok := claims.RealmAccess["roles"]; ok { + roles = r + } + } + if len(roles) == 0 { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "No roles found in token"}) + return + } + + var req dto.ActionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "Invalid request body", + "errors": []string{err.Error()}, + }) + return + } + c.Set("actionRequest", req) + action := req.Action + // Check if any role allows the action + for _, role := range roles { + allowedActions, exists := rolePermissions[role] + if !exists { + continue + } + for _, a := range allowedActions { + if a == action { + c.Next() + return + } + } + } + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Access denied for this action"}) + } +} + +// AuthMiddleware validates JWT tokens and stores user claims in the Gin context +// Performs signature, issuer, and audience checks +func AuthMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + base := os.Getenv("KEYCLOAK_BASE_URL") + realm := os.Getenv("KEYCLOAK_REALM") + clientID := os.Getenv("KEYCLOAK_CLIENT_ID") + expectedIssuer := strings.TrimRight(base, "/") + "/realms/" + realm + authHeader := ctx.GetHeader("Authorization") + if authHeader == "" || !strings.HasPrefix(strings.ToLower(authHeader), "bearer ") { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "MissingBearerToken"}) + return + } + tokenStr := strings.TrimSpace(authHeader[len("Bearer "):]) + token, err := jwt.ParseWithClaims(tokenStr, &security.KeycloakClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method %v", token.Header["alg"]) + } + kid, _ := token.Header["kid"].(string) + return security.GetPublicKey(ctx.Request.Context(), kid) + }) + if err != nil || !token.Valid { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "InvalidToken", "details": err.Error()}) + return + } + claims, _ := token.Claims.(*security.KeycloakClaims) + // Issuer validation + if base != "" && realm != "" && claims.Issuer != expectedIssuer { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "BadIssuer", "expected": expectedIssuer, "actual": claims.Issuer}) + return + } + // Audience / authorized party validation: + // Keycloak may put client id in aud OR azp depending on flow. + strictAud := strings.ToLower(os.Getenv("KEYCLOAK_STRICT_AUD")) == "true" + if clientID != "" { + audOK := false + // Audience may be a slice of strings in RegisteredClaims.Audience + for _, a := range claims.Audience { + if a == clientID { + audOK = true + break + } + } + if claims.AuthorizedParty == clientID { + audOK = true + } + if !audOK && strictAud { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "BadAudience", "expected": clientID, "aud": claims.Audience, "azp": claims.AuthorizedParty}) + return + } + } + var roles []string + if claims.RealmAccess != nil { + if r, ok := claims.RealmAccess["roles"]; ok { + roles = r + } + } + logger.Info("Authenticated user:", claims.PreferredUsername, " with roles: ", claims.RealmAccess["roles"]) + ctx.Set("claims", claims) + ctx.Set("user", claims.PreferredUsername) + ctx.Set("roles", roles) + if len(roles) > 0 { + ctx.Set("role", roles[0]) + } + reqCtx := ctx.Request.Context() + reqCtx = context.WithValue(reqCtx, "user", claims.PreferredUsername) + reqCtx = context.WithValue(reqCtx, "roles", roles) + selectedRole := "" + if len(roles) > 0 { + for _, r := range roles { + if r == strings.ToUpper(r) { + selectedRole = r + break + } + } + reqCtx = context.WithValue(reqCtx, "role", selectedRole) + } + ctx.Request = ctx.Request.WithContext(reqCtx) + + ctx.Next() + } +} + +// MDMSRoleMiddleware checks roles based on MDMS API configuration +func MDMSRoleMiddleware(method, endpoint string) gin.HandlerFunc { + return func(c *gin.Context) { + // Get allowed roles from pre-loaded MDMS cache + allowedRoles := getAllowedRolesFromCache(method, endpoint) + + // If no roles found in MDMS cache, deny access + if len(allowedRoles) == 0 { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "error": "No roles configured for this endpoint", + "endpoint": endpoint, + "method": method, + }) + return + } + + // Use security.RequireRoles middleware with the cached roles + roleMiddleware := security.RequireRoles(allowedRoles...) + roleMiddleware(c) + } +} + +// getAllowedRolesFromCache fetches allowed roles from the pre-loaded cache +func getAllowedRolesFromCache(method, endpoint string) []string { + uniqueIdentifier := fmt.Sprintf("%s.%s", endpoint, method) + if roles, exists := mdmsRolePermissions[uniqueIdentifier]; exists { + return roles + } + return []string{} +} + +// loadAllMDMSRoles loads all MDMS roles once at startup +func loadAllMDMSRoles() (map[string][]string, error) { + mdmsURL := config.GetConfig().MDMSURL + url := fmt.Sprintf("%s/mdms-v2/v2?schemaCode=API_ROLES", mdmsURL) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + // Add required headers + req.Header.Set("X-Tenant-ID", "pg") + req.Header.Set("X-Client-Id", "test-client") + + // Make the HTTP request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Read and parse response + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var mdmsResponse MDMSResponse + if err := json.Unmarshal(body, &mdmsResponse); err != nil { + return nil, err + } + + // Build cache map + roleCache := make(map[string][]string) + for _, mdmsData := range mdmsResponse.MDMS { + if mdmsData.IsActive { + roleCache[mdmsData.UniqueIdentifier] = mdmsData.Data.AllowedRoles + } + } + + return roleCache, nil +} + +// loadRoleActionsFromMDMS loads role-action permissions from MDMS API_ROLE_ACTION schema +func loadRoleActionsFromMDMS() (map[string][]string, error) { + mdmsURL := config.GetConfig().MDMSURL + url := fmt.Sprintf("%s/mdms-v2/v2?schemaCode=API_ROLE_ACTION", mdmsURL) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + // Add required headers + req.Header.Set("X-Tenant-ID", constants.DefaultJurisdiction) + req.Header.Set("X-Client-Id", "test-client") + + // Make the HTTP request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Read and parse response + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Parse as role-action data structure + var roleActionResponse struct { + MDMS []struct { + ID string `json:"id"` + TenantID string `json:"tenantId"` + SchemaCode string `json:"schemaCode"` + UniqueIdentifier string `json:"uniqueIdentifier"` + Data RoleActionData `json:"data"` + IsActive bool `json:"isActive"` + } `json:"mdms"` + } + + if err := json.Unmarshal(body, &roleActionResponse); err != nil { + return nil, err + } + + // Build role permissions map + rolePermissions := make(map[string][]string) + for _, mdmsData := range roleActionResponse.MDMS { + if mdmsData.IsActive { + rolePermissions[mdmsData.Data.Role] = mdmsData.Data.Actions + } + } + + return rolePermissions, nil +} diff --git a/property-tax/enumeration-backend/internal/middleware/cors_logMiddleware.go b/property-tax/enumeration-backend/internal/middleware/cors_logMiddleware.go new file mode 100644 index 0000000..0fc10c3 --- /dev/null +++ b/property-tax/enumeration-backend/internal/middleware/cors_logMiddleware.go @@ -0,0 +1,49 @@ +package middleware + +import ( + "bytes" + "enumeration/pkg/logger" + "io" + + "github.com/gin-gonic/gin" +) + +// CorsMiddleware sets the necessary headers to handle CORS (Cross-Origin Resource Sharing) requests. +// It allows any origin, credentials, a set of headers, and methods, and handles OPTIONS requests by returning 204. +func CorsMiddleware() gin.HandlerFunc { + return (func(c *gin.Context) { + // Allow all origins + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + // Allow credentials such as cookies, authorization headers, or TLS client certificates + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With,X-User-Role,X-User-ID,X-Status,X-Tenant-ID") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") + + // Handle preflight OPTIONS request by aborting with status code 204 (No Content) + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + }) +} + +// LoggerMiddleware logs the raw request body before passing control to the next handler. +// It restores the request body for subsequent handlers, as reading the body consumes it. +func LoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Read the raw request body into bodyBytes + bodyBytes, err := io.ReadAll(c.Request.Body) + if err != nil { + // Log an error if reading fails + logger.Error("Failed to read request body:", err) + } else { + // Log the request body contents + logger.Info("Request Body:", string(bodyBytes)) + // Restore the request body for the next handlers, as reading it consumes the body + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + // Continue with the next handler in the chain + c.Next() + } +} diff --git a/property-tax/enumeration-backend/internal/models/models.go b/property-tax/enumeration-backend/internal/models/models.go new file mode 100644 index 0000000..896edff --- /dev/null +++ b/property-tax/enumeration-backend/internal/models/models.go @@ -0,0 +1,257 @@ +package models + +import ( + "enumeration/pkg/utils" + "time" + + "github.com/google/uuid" + "github.com/lib/pq" + "gorm.io/datatypes" +) + +// Application represents a property tax application. +type Application struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + ApplicationNo string `gorm:"uniqueIndex;size:50"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"` + Priority string `gorm:"size:20;not null"` + TenantID string `gorm:"size:100;not null;index"` + DueDate time.Time `gorm:"not null"` + AssignedAgent *uuid.UUID `gorm:"type:uuid;index"` + Status string `gorm:"size:50;index"` + WorkflowInstanceID string `gorm:"size:100;index"` + AppliedBy string `gorm:"size:200;not null"` + AssesseeID string `gorm:"type:uuid;index"` // Foreign key to User (external service) + Property Property `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:RESTRICT"` + ApplicationLogs []ApplicationLog `gorm:"foreignKey:ApplicationID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + IsDraft bool `gorm:"default:true;not null;index"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + ImportantNote string `gorm:"size:500" json:"importantNote,omitempty"` +} + +func (Application) TableName() string { + return "DIGIT3.applications" +} + +// ApplicationLog represents an action performed on an application. +type ApplicationLog struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + Action string `gorm:"type:text"` + PerformedBy string `gorm:"size:200;not null"` + PerformedDate time.Time `gorm:"not null;index"` + Comments string `gorm:"type:text"` + Actor string `gorm:"size:100"` + Metadata string `gorm:"type:json"` + FileStoreID *uuid.UUID `gorm:"type:uuid;index"` + ApplicationID uuid.UUID `gorm:"type:uuid;not null;index"` + CreatedAt time.Time `gorm:"autoCreateTime"` +} + +// Property represents a property entity. +type Property struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + PropertyNo string `gorm:"unique;size:50;not null;index"` + OwnershipType string `gorm:"size:50"` + PropertyType string `gorm:"size:50"` + ComplexName string `gorm:"size:200"` + Address *PropertyAddress `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + AssessmentDetails *AssessmentDetails `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Amenities *Amenities `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + ConstructionDetails *ConstructionDetails `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + AdditionalDetails *AdditionalPropertyDetails `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + GISData *GISData `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + Documents []Document `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + IGRS *IGRS `gorm:"foreignKey:PropertyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + TypeOfLand string `gorm:"size:50;" json:"typeOfLand,omitempty"` + NoOfFloors string `gorm:"size:10" json:"noOfFloors,omitempty"` + NoOfBasements string `gorm:"size:10" json:"noOfBasements,omitempty"` + NoOfBuildings string `gorm:"size:10" json:"noOfBuildings,omitempty"` + BuildingNumber string `gorm:"size:50" json:"buildingNumber,omitempty"` +} + +func (Property) TableName() string { + return "DIGIT3.properties" +} + +type PropertyOwner struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;index"` + AdhaarNo uint64 `gorm:"not null;index"` + Name string `gorm:"size:200;not null"` + ContactNo string `gorm:"size:15;not null"` + Email string `gorm:"size:100"` + Gender string `gorm:"size:10;not null"` + Guardian string `gorm:"size:200"` + GuardianType string `gorm:"size:10"` + RelationshipToProperty string `gorm:"size:20"` + OwnershipShare float64 `gorm:"type:decimal(5,2);default:0"` + IsPrimaryOwner bool `gorm:"default:false;index"` + CreatedAt time.Time `gorm:"not null;autoCreateTime"` + UpdatedAt time.Time `gorm:"not null;autoUpdateTime"` +} + +// TableName overrides the table name to use DIGIT3 schema +func (PropertyOwner) TableName() string { + return "DIGIT3.property_owner" +} + +// PropertyAddress represents the address of a property. +type PropertyAddress struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + Locality string `gorm:"size:200;not null"` + ZoneNo string `gorm:"size:50;not null"` + WardNo string `gorm:"size:50;not null"` + BlockNo string `gorm:"size:50;not null"` + Street string `gorm:"size:200"` + ElectionWard string `gorm:"size:50;not null"` + SecretariatWard string `gorm:"size:50"` + PinCode uint64 `gorm:"index"` + DifferentCorrespondenceAddress bool `gorm:"default:false;not null"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + CorrespondenceAddress1 string `gorm:"column:correspondence_address_1;size:500"` + CorrespondenceAddress2 string `gorm:"column:correspondence_address_2;size:500"` + CorrespondenceAddress3 string `gorm:"column:correspondence_address_3;size:500"` +} + +func (PropertyAddress) TableName() string { + return "DIGIT3.property_addresses" +} + +// AssessmentDetails represents assessment details for a property. +type AssessmentDetails struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + ReasonOfCreation string `gorm:"size:200"` + OccupancyCertificateNumber string `gorm:"size:100"` + OccupancyCertificateDate *utils.Date `gorm:"type:date"` + ExtentOfSite string `gorm:"size:100;not null"` + IsLandUnderneathBuilding string `gorm:"size:100;not null"` + IsUnspecifiedShare bool `gorm:"default:false;not null"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// Amenities represents an amenity for a property. +type Amenities struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + Type pq.StringArray `gorm:"type:text[]" json:"type" binding:"required,dive"` + Description string `gorm:"type:text"` + ExpiryDate *time.Time + PropertyID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// ConstructionDetails represents construction details for a property. +type ConstructionDetails struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + FloorType string `gorm:"size:100"` + WallType string `gorm:"size:100"` + RoofType string `gorm:"size:100"` + WoodType string `gorm:"size:100"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"` + FloorDetails []FloorDetails `gorm:"foreignKey:ConstructionDetailsID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// FloorDetails represents details for a specific floor. +type FloorDetails struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + FloorNo int `gorm:"not null"` + Classification string `gorm:"size:100"` + NatureOfUsage string `gorm:"size:100"` + FirmName string `gorm:"size:200"` + OccupancyType string `gorm:"size:100"` + OccupancyName string `gorm:"size:200"` + ConstructionDate *utils.Date `gorm:"type:date" json:"constructionDate"` + EffectiveFromDate *utils.Date `gorm:"type:date" json:"effectiveFromDate"` + UnstructuredLand string `gorm:"size:100"` + LengthFt float64 `gorm:"type:decimal(10,2)"` + BreadthFt float64 `gorm:"type:decimal(10,2)"` + PlinthAreaSqFt float64 `gorm:"type:decimal(10,2)"` + BuildingPermissionNo string `gorm:"size:100"` + FloorDetailsEntered bool `gorm:"default:false;not null"` + ConstructionDetailsID uuid.UUID `gorm:"type:uuid;not null;index"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// AdditionalPropertyDetails represents extra metadata for a property. +type AdditionalPropertyDetails struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + FieldName string `gorm:"size:100;not null"` + FieldValue datatypes.JSON `gorm:"type:json" json:"fieldValue"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;index"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// GISData represents GIS information for a property. +type GISData struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + Source string `gorm:"type:enum('GPS','MANUAL_ENTRY','IMPORT');not null"` + Type string `gorm:"type:enum('POINT','LINE','POLYGON');not null"` + EntityType string `gorm:"size:100"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"` + Latitude float64 `gorm:"type:decimal(10,8);not null"` + Longitude float64 `gorm:"type:decimal(11,8);not null"` + Coordinates []Coordinates `gorm:"foreignKey:GISDataID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// Coordinates represents a latitude/longitude pair. +type Coordinates struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + Latitude float64 `gorm:"type:decimal(10,8);not null"` + Longitude float64 `gorm:"type:decimal(11,8);not null"` + GISDataID uuid.UUID `gorm:"type:uuid;not null;index"` + CreatedAt time.Time `gorm:"autoCreateTime"` +} + +type Document struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"` + PropertyID uuid.UUID `gorm:"type:uuid;not null;index"` + DocumentType string `gorm:"size:100;not null"` + DocumentName string `gorm:"size:255;not null"` + FileStoreID string `gorm:"column:file_store_id"` + UploadDate time.Time `gorm:"type:timestamptz;default:now()"` + Action string `gorm:"size:100;not null;default:PENDING" json:"action"` + UploadedBy string `gorm:"size:200" json:"uploadedBy,omitempty"` + Size string `gorm:"not null" json:"size,omitempty"` +} + +// ensure GORM uses DIGIT3 schema explicitly (optional; your NamingStrategy may already prefix tables) +func (Document) TableName() string { + return "DIGIT3.document" +} + +type IGRS struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()" json:"id"` + Habitation string `gorm:"size:200;not null" json:"habitation"` + IGRSWard string `gorm:"size:100" json:"igrsWard,omitempty"` + IGRSLocality string `gorm:"size:100" json:"igrsLocality,omitempty"` + IGRSBlock string `gorm:"size:100" json:"igrsBlock,omitempty"` + DoorNoFrom string `gorm:"size:50" json:"doorNoFrom,omitempty"` + DoorNoTo string `gorm:"size:50" json:"doorNoTo,omitempty"` + IGRSClassification string `gorm:"size:100" json:"igrsClassification,omitempty"` + BuiltUpAreaPct *float64 `gorm:"type:decimal(7,2)" json:"builtUpAreaPct,omitempty"` + FrontSetback *float64 `gorm:"type:decimal(8,2)" json:"frontSetback,omitempty"` + RearSetback *float64 `gorm:"type:decimal(8,2)" json:"rearSetback,omitempty"` + SideSetback *float64 `gorm:"type:decimal(8,2)" json:"sideSetback,omitempty"` + TotalPlinthArea *float64 `gorm:"type:decimal(10,2)" json:"totalPlinthArea,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` + PropertyID uuid.UUID `gorm:"type:uuid;index;unique"` +} + +func (IGRS) TableName() string { + return "DIGIT3.igrs" + +} diff --git a/property-tax/enumeration-backend/internal/repositories/additional_property_details_repository.go b/property-tax/enumeration-backend/internal/repositories/additional_property_details_repository.go new file mode 100644 index 0000000..ca69f65 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/additional_property_details_repository.go @@ -0,0 +1,113 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "errors" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// additionalPropertyDetailsRepository implements the AdditionalPropertyDetailsRepository interface using GORM. +var _ AdditionalPropertyDetailsRepository = (*additionalPropertyDetailsRepository)(nil) + +type additionalPropertyDetailsRepository struct { + db *gorm.DB // GORM database connection +} + +// NewAdditionalPropertyDetailsRepository creates a new repository for additional property details. +func NewAdditionalPropertyDetailsRepository(db *gorm.DB) AdditionalPropertyDetailsRepository { + return &additionalPropertyDetailsRepository{db: db} +} + +// Create inserts a new AdditionalPropertyDetails record into the database. +func (r *additionalPropertyDetailsRepository) Create(ctx context.Context, details *models.AdditionalPropertyDetails) error { + if err := r.db.Create(details).Error; err != nil { + return fmt.Errorf("failed to create additional property details: %w", err) + } + return nil +} + +// GetByID retrieves an AdditionalPropertyDetails record by its ID. +func (r *additionalPropertyDetailsRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.AdditionalPropertyDetails, error) { + var details models.AdditionalPropertyDetails + err := r.db.Where("id = ?", id).First(&details).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("additional property details with id %s not found", id) + } + return nil, fmt.Errorf("failed to get additional property details by id %s: %w", id, err) + } + return &details, nil +} + +// Update modifies an existing AdditionalPropertyDetails record in the database. +func (r *additionalPropertyDetailsRepository) Update(ctx context.Context, details *models.AdditionalPropertyDetails) error { + if err := r.db.Save(details).Error; err != nil { + return fmt.Errorf("failed to update additional property details with id %s: %w", details.ID, err) + } + return nil +} + +// Delete removes an AdditionalPropertyDetails record by its ID. +// Returns an error if the record does not exist or deletion fails. +func (r *additionalPropertyDetailsRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.AdditionalPropertyDetails{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete additional property details with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("additional property details with id %s not found for deletion", id) + } + return nil +} + +// GetAll retrieves all AdditionalPropertyDetails records with optional filtering and pagination. +// Supports filtering by property ID and field name, and returns the total count. +func (r *additionalPropertyDetailsRepository) GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID, fieldName *string) ([]*models.AdditionalPropertyDetails, int64, error) { + var details []*models.AdditionalPropertyDetails + var total int64 + + query := r.db.Model(&models.AdditionalPropertyDetails{}) + + if propertyID != nil { + query = query.Where("property_id = ?", *propertyID) + } + + if fieldName != nil && *fieldName != "" { + query = query.Where("field_name ILIKE ?", "%"+*fieldName+"%") + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count additional property details: %w", err) + } + + // Apply pagination + offset := page * size + if err := query.Offset(offset).Limit(size).Find(&details).Error; err != nil { + return nil, 0, fmt.Errorf("failed to fetch additional property details with pagination: %w", err) + } + + return details, total, nil +} + +// GetByPropertyID retrieves all AdditionalPropertyDetails records for a given property ID. +func (r *additionalPropertyDetailsRepository) GetByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.AdditionalPropertyDetails, error) { + var details []*models.AdditionalPropertyDetails + if err := r.db.Where("property_id = ?", propertyID).Find(&details).Error; err != nil { + return nil, fmt.Errorf("failed to get additional property details by property id %s: %w", propertyID, err) + } + return details, nil +} + +// GetByFieldName retrieves all AdditionalPropertyDetails records for a given field name. +func (r *additionalPropertyDetailsRepository) GetByFieldName(ctx context.Context, fieldName string) ([]*models.AdditionalPropertyDetails, error) { + var details []*models.AdditionalPropertyDetails + if err := r.db.Where("field_name = ?", fieldName).Find(&details).Error; err != nil { + return nil, fmt.Errorf("failed to get additional property details by field name %s: %w", fieldName, err) + } + return details, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/amenity_repo.go b/property-tax/enumeration-backend/internal/repositories/amenity_repo.go new file mode 100644 index 0000000..6d2d267 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/amenity_repo.go @@ -0,0 +1,124 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "errors" + "fmt" + "log" + + "gorm.io/gorm" +) + +// amenityRepository implements the AmenityRepository interface using GORM. +var _ AmenityRepository = (*amenityRepository)(nil) + +type amenityRepository struct { + db *gorm.DB // GORM database connection +} + +// NewAmenityRepository creates a new repository for amenities. +func NewAmenityRepository(db *gorm.DB) AmenityRepository { + return &amenityRepository{db} +} + +// GetAll retrieves all amenities from the database. +func (r *amenityRepository) GetAll(ctx context.Context) ([]models.Amenities, error) { + var amenities []models.Amenities + if err := r.db.Find(&amenities).Error; err != nil { + return nil, fmt.Errorf("failed to get all amenities: %w", err) + } + return amenities, nil +} + +// GetAllWithFilters retrieves amenities with optional filtering by type and property ID, and supports pagination. +// Returns the filtered amenities and the total count. +func (r *amenityRepository) GetAllWithFilters(ctx context.Context, page, size int, amenityType, propertyID string) ([]models.Amenities, int64, error) { + var amenities []models.Amenities + var total int64 + + query := r.db.Model(&models.Amenities{}) + + // Apply filters + if amenityType != "" { + query = query.Where("type = ?", amenityType) + } + if propertyID != "" { + query = query.Where("property_id = ?", propertyID) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count amenities: %w", err) + } + + // Apply pagination + offset := page * size + if err := query.Limit(size).Offset(offset).Find(&amenities).Error; err != nil { + return nil, 0, fmt.Errorf("failed to fetch amenities with pagination: %w", err) + } + + return amenities, total, nil +} + +// GetByID retrieves an amenity by its ID. +func (r *amenityRepository) GetByID(ctx context.Context, id string) (*models.Amenities, error) { + var amenity models.Amenities + err := r.db.First(&amenity, "id = ?", id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("amenity with id %s not found", id) + } + return nil, fmt.Errorf("failed to get amenity by id %s: %w", id, err) + } + return &amenity, nil +} + +// Create inserts a new amenity record into the database. +func (r *amenityRepository) Create(ctx context.Context, amenity *models.Amenities) error { + if err := r.db.Create(amenity).Error; err != nil { + return fmt.Errorf("failed to create amenity: %w", err) + } + return nil +} + +// Update modifies an existing amenity record by its ID. +// Returns an error if the record does not exist or update fails. +func (r *amenityRepository) Update(ctx context.Context, id string, amenity *models.Amenities) error { + result := r.db.Model(&models.Amenities{}).Where("id = ?", id).Updates(amenity) + if result.Error != nil { + return fmt.Errorf("failed to update amenity with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("amenity with id %s not found for update", id) + } + return nil +} + +// Delete removes an amenity record by its ID. +// Returns an error if the record does not exist or deletion fails. +func (r *amenityRepository) Delete(ctx context.Context, id string) error { + result := r.db.Delete(&models.Amenities{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete amenity with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("amenity with id %s not found for deletion", id) + } + return nil +} + +// GetByPropertyID retrieves an amenity by the associated property ID. +// Returns an error if not found or on failure. +func (r *amenityRepository) GetByPropertyID(ctx context.Context, propertyID string) (*models.Amenities, error) { + var amenity models.Amenities // Changed from pointer to value + err := r.db.WithContext(ctx).Where("property_id = ?", propertyID).First(&amenity).Error // Use First() instead of Find() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("amenity with property ID %s not found", propertyID) + } + log.Println("Error fetching amenity by property ID:", err) + return nil, fmt.Errorf("failed to get amenity by property ID %s: %w", propertyID, err) + } + return &amenity, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/application_log_repository.go b/property-tax/enumeration-backend/internal/repositories/application_log_repository.go new file mode 100644 index 0000000..8974e3d --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/application_log_repository.go @@ -0,0 +1,101 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + + "github.com/google/uuid" + "gorm.io/gorm" +) + + +var _ ApplicationLogRepository = (*applicationLogRepository)(nil) +type applicationLogRepository struct { + db *gorm.DB +} + +func NewApplicationLogRepository(db *gorm.DB) ApplicationLogRepository { + return &applicationLogRepository{ + db: db, + } +} + +func (r *applicationLogRepository) Create(ctx context.Context, log *models.ApplicationLog) error { + return r.db.WithContext(ctx).Create(log).Error +} + +func (r *applicationLogRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.ApplicationLog, error) { + var log models.ApplicationLog + err := r.db.WithContext(ctx).Where("id = ?", id).First(&log).Error + if err != nil { + return nil, err + } + return &log, nil +} + +func (r *applicationLogRepository) Update(ctx context.Context, log *models.ApplicationLog) error { + return r.db.WithContext(ctx).Save(log).Error +} + +func (r *applicationLogRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&models.ApplicationLog{}, id).Error +} + +func (r *applicationLogRepository) List(ctx context.Context, applicationID, action string, page, size int) ([]models.ApplicationLog, int64, error) { + var logs []models.ApplicationLog + var total int64 + + query := r.db.WithContext(ctx).Model(&models.ApplicationLog{}) + + // Apply filters + if applicationID != "" { + if appID, err := uuid.Parse(applicationID); err == nil { + query = query.Where("application_id = ?", appID) + } + } + if action != "" { + query = query.Where("action = ?", action) + } + + // Get total count + err := query.Count(&total).Error + if err != nil { + return nil, 0, err + } + + // Apply pagination and get results + offset := page * size + err = query.Offset(offset).Limit(size).Order("performed_date DESC").Find(&logs).Error + if err != nil { + return nil, 0, err + } + + return logs, total, nil +} + +func (r *applicationLogRepository) GetByApplicationID(ctx context.Context, applicationID uuid.UUID, action string, page, size int) ([]models.ApplicationLog, int64, error) { + var logs []models.ApplicationLog + var total int64 + + query := r.db.WithContext(ctx).Model(&models.ApplicationLog{}).Where("application_id = ?", applicationID) + + // Apply action filter if provided + if action != "" { + query = query.Where("action = ?", action) + } + + // Get total count + err := query.Count(&total).Error + if err != nil { + return nil, 0, err + } + + // Apply pagination and get results + offset := page * size + err = query.Offset(offset).Limit(size).Order("performed_date DESC").Find(&logs).Error + if err != nil { + return nil, 0, err + } + + return logs, total, nil +} \ No newline at end of file diff --git a/property-tax/enumeration-backend/internal/repositories/application_repository.go b/property-tax/enumeration-backend/internal/repositories/application_repository.go new file mode 100644 index 0000000..4ee9efb --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/application_repository.go @@ -0,0 +1,512 @@ +package repositories + +import ( + "context" + "enumeration/internal/dto" + "enumeration/internal/models" + "errors" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +var _ ApplicationRepository = (*applicationRepository)(nil) + +// applicationRepository handles database operations for applications +type applicationRepository struct { + db *gorm.DB +} + +// NewApplicationRepository creates a new application repository +func NewApplicationRepository(db *gorm.DB) *applicationRepository { + return &applicationRepository{db: db} +} + +// Create creates a new application record +func (r *applicationRepository) Create(ctx context.Context, application *models.Application) error { + if err := r.db.Create(application).Error; err != nil { + return fmt.Errorf("failed to create application: %w", err) + } + return nil +} + +// GetByID retrieves application by ID +func (r *applicationRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Application, error) { + var application models.Application + err := r.db.Preload("Property"). + Preload("Property.Address"). + Preload("Property.AssessmentDetails"). + Preload("Property.Amenities"). + Preload("Property.ConstructionDetails"). + Preload("Property.ConstructionDetails.FloorDetails"). + Preload("Property.AdditionalDetails"). + Preload("Property.GISData"). + Preload("Property.GISData.Coordinates"). + Preload("Property.IGRS"). + Preload("ApplicationLogs"). + Preload("Property.Documents"). + First(&application, "id = ?", id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("application with id %s not found", id) + } + return nil, fmt.Errorf("failed to get application by id %s: %w", id, err) + } + return &application, nil +} + +// GetByAssignedAgent retrieves applications by assigned agent ID with pagination +func (r *applicationRepository) GetByAssignedAgent(ctx context.Context, agentID string, status string, page, size int) ([]models.Application, int64, error) { + var applications []models.Application + var total int64 + + // Build query with filters + query := r.db.WithContext(ctx).Model(&models.Application{}). + Preload("Property"). + Preload("Property.Address"). + Preload("Property.AssessmentDetails"). + Preload("Property.Amenities"). + Preload("Property.ConstructionDetails"). + Preload("Property.ConstructionDetails.FloorDetails"). + Preload("Property.AdditionalDetails"). + Preload("Property.GISData"). + Preload("Property.GISData.Coordinates"). + Preload("ApplicationLogs"). + Preload("Property.Documents") + + // Add assigned agent filter + query = query.Where("assigned_agent = ?", agentID) + + query = query.Where("status = ?", status) + query = query.Where("is_draft = ?", false) + + // Get total count + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count applications for agent %s: %w", agentID, err) + } + + // Apply pagination and ordering + offset := page * size + if err := query.Offset(offset).Limit(size).Order("created_at DESC").Find(&applications).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fmt.Errorf("no applications found for agent %s with status %s", agentID, status) + } + return nil, 0, fmt.Errorf("failed to get applications for agent %s: %w", agentID, err) + } + return applications, total, nil +} + +// GetByTenantIDAndStatus retrieves applications by tenant ID and status with pagination +func (r *applicationRepository) GetByTenantIDAndStatus(ctx context.Context, tenantID string, status string, page, size int) ([]models.Application, int64, error) { + var applications []models.Application + var total int64 + + // Build query with filters + query := r.db.WithContext(ctx).Model(&models.Application{}). + Preload("Property"). + Preload("Property.Address"). + Preload("Property.AssessmentDetails"). + Preload("Property.Amenities"). + Preload("Property.ConstructionDetails"). + Preload("Property.ConstructionDetails.FloorDetails"). + Preload("Property.AdditionalDetails"). + Preload("Property.GISData"). + Preload("Property.GISData.Coordinates"). + Preload("Property.IGRS"). + Preload("ApplicationLogs"). + Preload("Property.Documents") + + // Add tenant ID filter + if tenantID != "" { + query = query.Where("tenant_id = ?", tenantID) + } + + // Add status filter + if status != "" { + query = query.Where("status = ?", status) + } + query = query.Where("is_draft = ?", false) + // Get total count + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count applications for tenant %s: %w", tenantID, err) + } + + // Apply pagination and ordering + offset := page * size + if err := query.Offset(offset).Limit(size).Order("created_at DESC").Find(&applications).Error; err != nil { + return nil, 0, fmt.Errorf("failed to get applications for tenant %s: %w", tenantID, err) + } + return applications, total, nil +} + +// GetByApplicationNo retrieves application by application number +func (r *applicationRepository) GetByApplicationNo(ctx context.Context, applicationNo string) (*models.Application, error) { + var application models.Application + res := r.db.First(&application, "application_no = ?", applicationNo) + if res.Error != nil { + return nil, fmt.Errorf("failed to get application by application number %s: %w", applicationNo, res.Error) + } + return &application, nil +} + +// Update updates an existing application record +func (r *applicationRepository) Update(ctx context.Context, application *models.Application) error { + result := r.db.Save(application) + if result.Error != nil { + return fmt.Errorf("failed to update application with id %s: %w", application.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("application with id %s not found for update", application.ID) + } + return nil +} + +// Delete deletes application by ID +func (r *applicationRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.Application{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete application with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("application with id %s not found for deletion", id) + } + return nil +} + +// List retrieves all applications with pagination +func (r *applicationRepository) List(ctx context.Context, page, size int) ([]models.Application, int64, error) { + var applications []models.Application + var total int64 + + query := r.db.WithContext(ctx).Model(&models.Application{}). + Preload("Property"). + Preload("Property.Address"). + Preload("Property.AssessmentDetails"). + Preload("Property.Amenities"). + Preload("Property.ConstructionDetails"). + Preload("Property.ConstructionDetails.FloorDetails"). + Preload("Property.AdditionalDetails"). + Preload("Property.GISData"). + Preload("Property.GISData.Coordinates"). + Preload("Property.IGRS"). + Preload("Property.Documents") + query = query.Where("is_draft = ?", false) + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count applications: %w", err) + } + + // Apply pagination + offset := page * size + if err := query.Offset(offset).Limit(size).Order("created_at DESC").Find(&applications).Error; err != nil { + return nil, 0, fmt.Errorf("failed to get applications: %w", err) + } + + return applications, total, nil +} + +// Search retrieves applications based on search criteria +// func (r *applicationRepository) Search(ctx context.Context, criteria *dto.ApplicationSearchCriteria, page, size int) ([]*models.Application, int64, error) { +// var applications []*models.Application +// var total int64 + +// query := r.db.WithContext(ctx).Model(&models.Application{}).Preload("Property"). +// Preload("Property.Address"). +// Preload("Property.AssessmentDetails"). +// Preload("Property.Amenities"). +// Preload("Property.ConstructionDetails"). +// Preload("Property.ConstructionDetails.FloorDetails"). +// Preload("Property.AdditionalDetails"). +// Preload("Property.GISData"). +// Preload("Property.GISData.Coordinates"). +// Preload("Property.IGRS"). +// Preload("Property.Documents") +// // Apply filters +// if criteria.Status != "" { +// query = query.Where("status = ?", criteria.Status) +// } +// if criteria.Priority != "" { +// query = query.Where("priority = ?", criteria.Priority) +// } +// if criteria.PropertyID != "" { +// query = query.Where("property_id = ?", criteria.PropertyID) +// } +// if criteria.AssignedAgent != "" { +// query = query.Where("assigned_agent = ?", criteria.AssignedAgent) +// } +// if criteria.AppliedBy != "" { +// query = query.Where("applied_by = ?", criteria.AppliedBy) +// } +// if criteria.ApplicationNo != "" { +// query = query.Where("application_no = ?", criteria.ApplicationNo) +// } +// if criteria.CreatedDateFrom != nil { +// query = query.Where("created_at >= ?", *criteria.CreatedDateFrom) +// } +// if criteria.CreatedDateTo != nil { +// query = query.Where("created_at <= ?", *criteria.CreatedDateTo) +// } +// if criteria.DueDateFrom != nil { +// query = query.Where("due_date >= ?", *criteria.DueDateFrom) +// } +// if criteria.DueDateTo != nil { +// query = query.Where("due_date <= ?", *criteria.DueDateTo) +// } +// if criteria.AssesseeID != "" { +// query = query.Where("assessee_id = ?", criteria.AssesseeID) +// } +// if criteria.ZoneNo != "" && criteria.WardNo != "" { +// query = query.Joins(`JOIN "DIGIT3"."properties" ON "DIGIT3"."applications"."property_id" = "DIGIT3"."properties"."id"`). +// Joins(`JOIN "DIGIT3"."property_addresses" ON "DIGIT3"."properties"."id" = "DIGIT3"."property_addresses"."property_id"`). +// Where(`"DIGIT3"."property_addresses"."zone_no" = ? AND "DIGIT3"."property_addresses"."ward_no" = ?`, criteria.ZoneNo, criteria.WardNo) +// } else if criteria.ZoneNo != "" { +// query = query.Joins(`JOIN "DIGIT3"."properties" ON "DIGIT3"."applications"."property_id" = "DIGIT3"."properties"."id"`). +// Joins(`JOIN "DIGIT3"."property_addresses" ON "DIGIT3"."properties"."id" = "DIGIT3"."property_addresses"."property_id"`). +// Where(`"DIGIT3"."property_addresses"."zone_no" = ?`, criteria.ZoneNo) +// } else if criteria.WardNo != "" { +// query = query.Joins(`JOIN "DIGIT3"."properties" ON "DIGIT3"."applications"."property_id" = "DIGIT3"."properties"."id"`). +// Joins(`JOIN "DIGIT3"."property_addresses" ON "DIGIT3"."properties"."id" = "DIGIT3"."property_addresses"."property_id"`). +// Where(`"DIGIT3"."property_addresses"."ward_no" = ?`, criteria.WardNo) +// } +// if criteria.IsDraft != nil { +// query = query.Where("is_draft = ?", *criteria.IsDraft) +// } else { +// query = query.Where("is_draft = ?", false) +// } +// // Count total records +// if err := query.Count(&total).Error; err != nil { +// return nil, 0, fmt.Errorf("failed to count applications: %w", err) +// } + +// // Apply pagination and sorting +// offset := page * size + +// // Determine sort field (default to created_at) +// sortField := "created_at" +// if criteria.SortField != "" { +// sortField = criteria.SortField +// } + +// // Determine sort order (default to DESC) +// sortOrder := "DESC" +// if criteria.SortBy == "ASC" { +// sortOrder = "ASC" +// } + +// // Build order clause +// orderClause := fmt.Sprintf("%s %s", sortField, sortOrder) + +// if err := query.Offset(offset).Limit(size).Order(orderClause).Find(&applications).Error; err != nil { +// return nil, 0, fmt.Errorf("failed to get applications: %w", err) +// } +// return applications, total, nil +// } + +// Search retrieves applications based on search criteria +func (r *applicationRepository) Search(ctx context.Context, criteria *dto.ApplicationSearchCriteria, page, size int) ([]*models.Application, int64, error) { + var applications []*models.Application + var total int64 + + query := r.db.WithContext(ctx).Model(&models.Application{}).Preload("Property"). + Preload("Property.Address"). + Preload("Property.AssessmentDetails"). + Preload("Property.Amenities"). + Preload("Property.ConstructionDetails"). + Preload("Property.ConstructionDetails.FloorDetails"). + Preload("Property.AdditionalDetails"). + Preload("Property.GISData"). + Preload("Property.GISData.Coordinates"). + Preload("Property.IGRS"). + Preload("Property.Documents") + + // Apply filters + if criteria.Status != "" { + query = query.Where("status = ?", criteria.Status) + } + if criteria.Priority != "" { + query = query.Where("priority = ?", criteria.Priority) + } + if criteria.PropertyID != "" { + query = query.Where("property_id = ?", criteria.PropertyID) + } + if criteria.AssignedAgent != "" { + query = query.Where("assigned_agent = ?", criteria.AssignedAgent) + } + if criteria.AppliedBy != "" { + query = query.Where("applied_by = ?", criteria.AppliedBy) + } + if criteria.ApplicationNo != "" { + query = query.Where("application_no = ?", criteria.ApplicationNo) + } + if criteria.CreatedDateFrom != nil { + query = query.Where("created_at >= ?", *criteria.CreatedDateFrom) + } + if criteria.CreatedDateTo != nil { + query = query.Where("created_at <= ?", *criteria.CreatedDateTo) + } + if criteria.DueDateFrom != nil { + query = query.Where("due_date >= ?", *criteria.DueDateFrom) + } + if criteria.DueDateTo != nil { + query = query.Where("due_date <= ?", *criteria.DueDateTo) + } + if criteria.AssesseeID != "" { + query = query.Where("assessee_id = ?", criteria.AssesseeID) + } + + // Updated logic for PropertyNo, ZoneNo and WardNo + needsJoin := false + + if criteria.PropertyNo != "" { + needsJoin = true + fmt.Println("PropertyNo filter detected, will add joins") + } + if criteria.ZoneNo != "" { + needsJoin = true + } + if len(criteria.WardNo) > 0 { + needsJoin = true + } + + // Add joins only if needed + if needsJoin { + query = query.Joins(`JOIN "DIGIT3"."properties" ON "DIGIT3"."applications"."property_id" = "DIGIT3"."properties"."id"`) + fmt.Println("Added properties join") + + // Only join address table if zone or ward filters are present + if criteria.ZoneNo != "" || len(criteria.WardNo) > 0 { + query = query.Joins(`JOIN "DIGIT3"."property_addresses" ON "DIGIT3"."properties"."id" = "DIGIT3"."property_addresses"."property_id"`) + } + } + + // Apply property number filter + if criteria.PropertyNo != "" { + query = query.Where(`"DIGIT3"."properties"."property_no" = ?`, criteria.PropertyNo) + fmt.Printf("Added PropertyNo filter: %s\n", criteria.PropertyNo) + } + + // Apply zone filter + if criteria.ZoneNo != "" { + query = query.Where(`"DIGIT3"."property_addresses"."zone_no" = ?`, criteria.ZoneNo) + } + + // Apply ward filter - handle array of ward numbers + if len(criteria.WardNo) > 0 { + // Filter out empty strings from the slice + var validWards []string + for _, ward := range criteria.WardNo { + if ward != "" { + validWards = append(validWards, ward) + } + } + + if len(validWards) > 0 { + query = query.Where(`"DIGIT3"."property_addresses"."ward_no" IN ?`, validWards) + } + } + + if criteria.IsDraft != nil { + query = query.Where("is_draft = ?", *criteria.IsDraft) + } else { + query = query.Where("is_draft = ?", false) + } + + // Debug: Print the SQL query + sqlQuery := query.ToSQL(func(tx *gorm.DB) *gorm.DB { + return tx.Count(&total) + }) + fmt.Printf("Count SQL: %s\n", sqlQuery) + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count applications: %w", err) + } + + fmt.Printf("Total records found: %d\n", total) + + // Apply pagination and sorting + offset := page * size + + // Determine sort field (default to created_at) + sortField := "created_at" + if criteria.SortField != "" { + sortField = criteria.SortField + } + + // Determine sort order (default to DESC) + sortOrder := "DESC" + if criteria.SortBy == "ASC" { + sortOrder = "ASC" + } + + // Build order clause - ensure proper table qualification for joins + var orderClause string + if needsJoin && (sortField == "created_at" || sortField == "due_date") { + // Qualify the sort field with table name when joins are present + orderClause = fmt.Sprintf(`"DIGIT3"."applications"."%s" %s`, sortField, sortOrder) + } else { + orderClause = fmt.Sprintf("%s %s", sortField, sortOrder) + } + + if err := query.Offset(offset).Limit(size).Order(orderClause).Find(&applications).Error; err != nil { + return nil, 0, fmt.Errorf("failed to get applications: %w", err) + } + return applications, total, nil +} + +// ExistsByApplicationNo checks if application exists by application number +func (r *applicationRepository) ExistsByApplicationNo(ctx context.Context, applicationNo string) (bool, error) { + var count int64 + err := r.db.Model(&models.Application{}).Where("application_no = ?", applicationNo).Count(&count).Error + if err != nil { + return false, fmt.Errorf("failed to check existence of application number %s: %w", applicationNo, err) + } + return count > 0, nil +} + +func (r *applicationRepository) GetApplicationsByIDs(ctx context.Context, ids []string, state string, page, size int) ([]models.Application, int64, error) { + if len(ids) == 0 { + return []models.Application{}, 0, nil + } + var apps []models.Application + // Build query with filters + query := r.db.WithContext(ctx).Preload("Property"). + Preload("Property.Address"). + Preload("Property.AssessmentDetails"). + Preload("Property.Amenities"). + Preload("Property.ConstructionDetails"). + Preload("Property.ConstructionDetails.FloorDetails"). + Preload("Property.AdditionalDetails"). + Preload("Property.Documents"). + Preload("Property.GISData"). + Preload("Property.GISData.Coordinates"). + Preload("Property.GISData").Where("id IN ?", ids) + + query = query.Where("status = ?", state) + + // Get total count + var totalCount int64 + if err := query.Model(&models.Application{}).Count(&totalCount).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count applications: %w", err) + } + // Apply pagination + offset := page * size + if err := query.Offset(offset).Limit(size).Order("created_at DESC").Find(&apps).Error; err != nil { + return nil, 0, fmt.Errorf("failed to get applications: %w", err) + } + + return apps, totalCount, nil +} + +func (r *applicationRepository) CheckUserExists(ctx context.Context, userId string) error { + var count int64 + err := r.db.Table("DIGIT3.users").Where("keycloak_user_id = ?", userId).Count(&count).Error + if err != nil { + return fmt.Errorf("failed to check user existence: %w", err) + } + if count == 0 { + return fmt.Errorf("user with ID %s does not exist", userId) + } + return nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/assessment_details_repository.go b/property-tax/enumeration-backend/internal/repositories/assessment_details_repository.go new file mode 100644 index 0000000..9e89eb1 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/assessment_details_repository.go @@ -0,0 +1,113 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// assessmentDetailsRepository implements the AssessmentDetailsRepository interface using GORM. +var _ AssessmentDetailsRepository = (*assessmentDetailsRepository)(nil) + +type assessmentDetailsRepository struct { + db *gorm.DB // GORM database connection +} + +// NewAssessmentDetailsRepository creates a new repository for assessment details. +func NewAssessmentDetailsRepository(db *gorm.DB) AssessmentDetailsRepository { + return &assessmentDetailsRepository{db: db} +} + +// Create inserts a new AssessmentDetails record into the database. +func (r *assessmentDetailsRepository) Create(ctx context.Context, assessmentDetails *models.AssessmentDetails) error { + res := r.db.Create(assessmentDetails) + if res.Error != nil { + return fmt.Errorf("failed to create assessment details: %w", res.Error) + } + return nil +} + +// GetByID retrieves an AssessmentDetails record by its ID. +func (r *assessmentDetailsRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.AssessmentDetails, error) { + var assessmentDetails models.AssessmentDetails + err := r.db.Where("id = ?", id).First(&assessmentDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("assessment details with id %s not found", id) + } + return nil, fmt.Errorf("failed to get assessment details by id %s: %w", id, err) + } + return &assessmentDetails, nil +} + +// Update modifies an existing AssessmentDetails record in the database. +// Returns an error if the record does not exist or update fails. +func (r *assessmentDetailsRepository) Update(ctx context.Context, assessmentDetails *models.AssessmentDetails) error { + result := r.db.Save(assessmentDetails) + if result.Error != nil { + return fmt.Errorf("failed to update assessment details with id %s: %w", assessmentDetails.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("assessment details with id %s not found for update", assessmentDetails.ID) + } + return nil +} + +// Delete removes an AssessmentDetails record by its ID. +// Returns an error if the record does not exist or deletion fails. +func (r *assessmentDetailsRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.AssessmentDetails{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete assessment details with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("assessment details with id %s not found for deletion", id) + } + return nil +} + +// GetAll retrieves all AssessmentDetails records with optional filtering by property ID and supports pagination. +// Returns the filtered assessment details and the total count. +func (r *assessmentDetailsRepository) GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.AssessmentDetails, int64, error) { + var assessmentDetails []*models.AssessmentDetails + var total int64 + + query := r.db.Model(&models.AssessmentDetails{}) + + if propertyID != nil { + query = query.Where("property_id = ?", *propertyID) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count assessment details: %w", err) + } + + // Apply pagination + offset := page * size + err := query.Offset(offset).Limit(size).Find(&assessmentDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no assessment details found") + } + return nil, 0, fmt.Errorf("failed to get assessment details: %w", err) + } + + return assessmentDetails, total, nil +} + +// GetByPropertyID retrieves an AssessmentDetails record by the associated property ID. +func (r *assessmentDetailsRepository) GetByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.AssessmentDetails, error) { + var assessmentDetails models.AssessmentDetails + err := r.db.Where("property_id = ?", propertyID).First(&assessmentDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("assessment details with property id %s not found", propertyID) + } + return nil, fmt.Errorf("failed to get assessment details by property id %s: %w", propertyID, err) + } + return &assessmentDetails, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/construction_details_repository.go b/property-tax/enumeration-backend/internal/repositories/construction_details_repository.go new file mode 100644 index 0000000..c4f20c0 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/construction_details_repository.go @@ -0,0 +1,112 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// constructionDetailsRepository implements the ConstructionDetailsRepository interface using GORM. +var _ ConstructionDetailsRepository = (*constructionDetailsRepository)(nil) + +type constructionDetailsRepository struct { + db *gorm.DB // GORM database connection +} + +// NewConstructionDetailsRepository creates a new repository for construction details. +func NewConstructionDetailsRepository(db *gorm.DB) ConstructionDetailsRepository { + return &constructionDetailsRepository{db: db} +} + +// Create inserts a new ConstructionDetails record into the database. +func (r *constructionDetailsRepository) Create(ctx context.Context, constructionDetails *models.ConstructionDetails) error { + if err := r.db.Create(constructionDetails).Error; err != nil { + return fmt.Errorf("failed to create construction details: %w", err) + } + return nil +} + +// GetByID retrieves a ConstructionDetails record by its ID, including related FloorDetails. +func (r *constructionDetailsRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.ConstructionDetails, error) { + var constructionDetails models.ConstructionDetails + err := r.db.Preload("FloorDetails").Where("id = ?", id).First(&constructionDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("construction details with id %s not found", id) + } + return nil, fmt.Errorf("failed to get construction details by id %s: %w", id, err) + } + return &constructionDetails, nil +} + +// Update modifies an existing ConstructionDetails record in the database. +// Returns an error if the record does not exist or update fails. +func (r *constructionDetailsRepository) Update(ctx context.Context, constructionDetails *models.ConstructionDetails) error { + result := r.db.Save(constructionDetails) + if result.Error != nil { + return fmt.Errorf("failed to update construction details with id %s: %w", constructionDetails.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("construction details with id %s not found for update", constructionDetails.ID) + } + return nil +} + +// Delete removes a ConstructionDetails record by its ID. +// Returns an error if the record does not exist or deletion fails. +func (r *constructionDetailsRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.ConstructionDetails{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete construction details with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("construction details with id %s not found for deletion", id) + } + return nil +} + +// GetAll retrieves all ConstructionDetails records with optional filtering by property ID and supports pagination. +// Preloads related FloorDetails and returns the filtered construction details and the total count. +func (r *constructionDetailsRepository) GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.ConstructionDetails, int64, error) { + var constructionDetails []*models.ConstructionDetails + var total int64 + + query := r.db.Model(&models.ConstructionDetails{}).Preload("FloorDetails") + + if propertyID != nil { + query = query.Where("property_id = ?", *propertyID) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count construction details: %w", err) + } + + // Apply pagination + offset := page * size + err := query.Offset(offset).Limit(size).Find(&constructionDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no construction details found") + } + return nil, 0, fmt.Errorf("failed to get construction details: %w", err) + } + + return constructionDetails, total, nil +} + +// GetByPropertyID retrieves all ConstructionDetails records for a given property ID, including related FloorDetails. +func (r *constructionDetailsRepository) GetByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.ConstructionDetails, error) { + var constructionDetails []*models.ConstructionDetails + err := r.db.Preload("FloorDetails").Where("property_id = ?", propertyID).Find(&constructionDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("no construction details found for property id %s", propertyID) + } + return nil, fmt.Errorf("failed to get construction details by property id %s: %w", propertyID, err) + } + return constructionDetails, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/coordinates_repo.go b/property-tax/enumeration-backend/internal/repositories/coordinates_repo.go new file mode 100644 index 0000000..c6180e9 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/coordinates_repo.go @@ -0,0 +1,155 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "errors" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// coordinatesRepository implements the CoordinatesRepository interface using GORM. +var _ CoordinatesRepository = (*coordinatesRepository)(nil) + +// coordinatesRepository handles database operations for coordinates. +type coordinatesRepository struct { + db *gorm.DB // GORM database connection +} + +// NewCoordinatesRepository creates a new repository for coordinates. +func NewCoordinatesRepository(db *gorm.DB) CoordinatesRepository { + return &coordinatesRepository{db: db} +} + +// Create inserts a new Coordinates record into the database. +func (r *coordinatesRepository) Create(ctx context.Context, coordinates *models.Coordinates) error { + if err := r.db.Create(coordinates).Error; err != nil { + return fmt.Errorf("failed to create coordinates: %w", err) + } + return nil +} + +// GetByID retrieves a Coordinates record by its ID. +func (r *coordinatesRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Coordinates, error) { + var coordinates models.Coordinates + err := r.db.First(&coordinates, "id = ?", id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("coordinates with id %s not found", id) + } + return nil, fmt.Errorf("failed to get coordinates by id %s: %w", id, err) + } + return &coordinates, nil +} + +// Update modifies an existing Coordinates record in the database. +func (r *coordinatesRepository) Update(ctx context.Context, coordinates *models.Coordinates) error { + if err := r.db.Save(coordinates).Error; err != nil { + return fmt.Errorf("failed to update coordinates with id %s: %w", coordinates.ID, err) + } + return nil +} + +// Delete removes a Coordinates record by its ID. +// Returns an error if the record does not exist or deletion fails. +func (r *coordinatesRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.Coordinates{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete coordinates with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("coordinates with id %s not found for deletion", id) + } + return nil +} + +// FindAll retrieves all Coordinates records with optional filtering by GIS Data ID and supports pagination. +// Returns the filtered coordinates and the total count. +func (r *coordinatesRepository) FindAll(ctx context.Context, page, size int, gisDataID *uuid.UUID) ([]models.Coordinates, int64, error) { + var coordinates []models.Coordinates + var total int64 + + query := r.db.Model(&models.Coordinates{}) + + // Filter by GIS Data ID if provided + if gisDataID != nil { + query = query.Where("gis_data_id = ?", *gisDataID) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count coordinates: %w", err) + } + + // Apply pagination + offset := page * size + if err := query.Offset(offset).Limit(size).Find(&coordinates).Error; err != nil { + return nil, 0, fmt.Errorf("failed to fetch coordinates with pagination: %w", err) + } + + return coordinates, total, nil +} + +// FindByGISDataID retrieves all Coordinates records for a specific GIS data record. +func (r *coordinatesRepository) FindByGISDataID(ctx context.Context, gisDataID uuid.UUID) ([]models.Coordinates, error) { + var coordinates []models.Coordinates + if err := r.db.Where("gis_data_id = ?", gisDataID).Find(&coordinates).Error; err != nil { + return nil, fmt.Errorf("failed to get coordinates by gis data id %s: %w", gisDataID, err) + } + return coordinates, nil +} + +// DeleteByGISDataID deletes all Coordinates records for a specific GIS data record. +func (r *coordinatesRepository) DeleteByGISDataID(ctx context.Context, gisDataID uuid.UUID) error { + if err := r.db.Where("gis_data_id = ?", gisDataID).Delete(&models.Coordinates{}).Error; err != nil { + return fmt.Errorf("failed to delete coordinates by gis data id %s: %w", gisDataID, err) + } + return nil +} + +// CreateBatch inserts multiple Coordinates records in a single transaction for efficiency and rollback on failure. +func (r *coordinatesRepository) CreateBatch(ctx context.Context, coords []*models.Coordinates) error { + // Use the DB context, transaction and CreateInBatches for efficiency and rollback on failure + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.CreateInBatches(coords, 100).Error; err != nil { + return fmt.Errorf("failed to create coordinates in batch: %w", err) + } + return nil + }) +} + +// ReplaceByGISDataID replaces all Coordinates records for a specific GIS data record with a new batch. +// Deletes existing records and inserts the new batch in a single transaction. +func (r *coordinatesRepository) ReplaceByGISDataID(ctx context.Context, gisDataID uuid.UUID, coords []*models.Coordinates) error { + // Transaction: delete existing -> create new batch + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // Delete existing coordinates for the GIS data + if err := tx.Where("gis_data_id = ?", gisDataID).Delete(&models.Coordinates{}).Error; err != nil { + return fmt.Errorf("failed to delete existing coordinates for gis data id %s: %w", gisDataID, err) + } + + // Ensure each coordinate has the correct GISDataID and an ID + for _, c := range coords { + if c == nil { + continue + } + c.GISDataID = gisDataID + if c.ID == uuid.Nil { + c.ID = uuid.New() + } + } + + // If nothing to insert, that's fine (we've deleted existing) + if len(coords) == 0 { + return nil + } + + // Insert new coordinates in batches + if err := tx.CreateInBatches(coords, 100).Error; err != nil { + return fmt.Errorf("failed to create coordinates in batch: %w", err) + } + return nil + }) +} diff --git a/property-tax/enumeration-backend/internal/repositories/document_repository.go b/property-tax/enumeration-backend/internal/repositories/document_repository.go new file mode 100644 index 0000000..53cd20a --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/document_repository.go @@ -0,0 +1,122 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// documentRepository implements the DocumentRepository interface using GORM. +var _ DocumentRepository = (*documentRepository)(nil) + +type documentRepository struct { + db *gorm.DB // GORM database connection +} + +// NewDocumentRepository creates a new repository for documents. +func NewDocumentRepository(db *gorm.DB) DocumentRepository { + return &documentRepository{db: db} +} + +// Create inserts a new Document record into the database. +func (r *documentRepository) Create(ctx context.Context, doc *models.Document) error { + if err := r.db.Create(doc).Error; err != nil { + return fmt.Errorf("failed to create document: %w", err) + } + return nil +} + +// CreateBatch inserts multiple Document records in a single transaction for efficiency and rollback on failure. +func (r *documentRepository) CreateBatch(ctx context.Context, docs []*models.Document) error { + return r.db.Transaction(func(tx *gorm.DB) error { + for _, d := range docs { + if err := tx.Create(d).Error; err != nil { + return fmt.Errorf("failed to create document: %w", err) + } + } + return nil + }) +} + +// GetByID retrieves a Document record by its ID. +func (r *documentRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Document, error) { + var doc models.Document + err := r.db.Where("id = ?", id).First(&doc).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("document with id %s not found", id) + } + return nil, fmt.Errorf("failed to fetch document by id %s: %w", id, err) + } + return &doc, nil +} + +// GetByPropertyID retrieves all Document records for a given property ID. +func (r *documentRepository) GetByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.Document, error) { + var docs []*models.Document + err := r.db.Where("property_id = ?", propertyID).Find(&docs).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("no documents found for property id %s", propertyID) + } + return nil, fmt.Errorf("failed to get documents by property id %s: %w", propertyID, err) + } + return docs, nil +} + +// GetAll retrieves all Document records with optional filtering by property ID and supports pagination. +// Returns the filtered documents and the total count. +func (r *documentRepository) GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.Document, int64, error) { + var docs []*models.Document + var total int64 + + query := r.db.Model(&models.Document{}) + + if propertyID != nil { + query = query.Where("property_id = ?", *propertyID) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count documents: %w", err) + } + + offset := page * size + err := query.Offset(offset).Limit(size).Find(&docs).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no documents found") + } + return nil, 0, fmt.Errorf("failed to get documents: %w", err) + } + + return docs, total, nil +} + +// Delete removes a Document record by its ID. +// Returns an error if the record does not exist or deletion fails. +func (r *documentRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.Document{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete document with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("document with id %s not found for deletion", id) + } + return nil +} + +// Update modifies an existing Document record by its ID. +// Returns an error if the record does not exist or update fails. +func (r *documentRepository) Update(ctx context.Context, doc *models.Document) error { + result := r.db.Model(&models.Document{}).Where("id = ?", doc.ID).Updates(doc) + if result.Error != nil { + return fmt.Errorf("failed to update document with id %s: %w", doc.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("document with id %s not found for update", doc.ID) + } + return nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/floor_details_repository.go b/property-tax/enumeration-backend/internal/repositories/floor_details_repository.go new file mode 100644 index 0000000..5068336 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/floor_details_repository.go @@ -0,0 +1,111 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// floorDetailsRepository implements the FloorDetailsRepository interface using GORM. +var _ FloorDetailsRepository = (*floorDetailsRepository)(nil) + +type floorDetailsRepository struct { + db *gorm.DB // GORM database connection +} + +// NewFloorDetailsRepository creates a new repository for floor details. +func NewFloorDetailsRepository(db *gorm.DB) FloorDetailsRepository { + return &floorDetailsRepository{db: db} +} + +// GetAll retrieves all FloorDetails records with optional filtering by construction details ID and supports pagination. +// Returns the filtered floor details and the total count. +func (r *floorDetailsRepository) GetAll(ctx context.Context, page, size int, constructionDetailsID *uuid.UUID) ([]*models.FloorDetails, int64, error) { + var floorDetails []*models.FloorDetails + var total int64 + + query := r.db.Model(&models.FloorDetails{}) + + if constructionDetailsID != nil { + query = query.Where("construction_details_id = ?", *constructionDetailsID) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count floor details: %w", err) + } + + // Apply pagination + offset := page * size + if err := query.Offset(offset).Limit(size).Find(&floorDetails).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no floor details found") + } + return nil, 0, fmt.Errorf("failed to get floor details: %w", err) + } + + return floorDetails, total, nil +} + +// Create inserts a new FloorDetails record into the database. +func (r *floorDetailsRepository) Create(ctx context.Context, floorDetails *models.FloorDetails) error { + if err := r.db.Create(floorDetails).Error; err != nil { + return fmt.Errorf("failed to create floor details: %w", err) + } + return nil +} + +// GetByID retrieves a FloorDetails record by its ID. +func (r *floorDetailsRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.FloorDetails, error) { + var floorDetails models.FloorDetails + err := r.db.Where("id = ?", id).First(&floorDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("floor details with id %s not found", id) + } + return nil, fmt.Errorf("failed to get floor details by id %s: %w", id, err) + } + return &floorDetails, nil +} + +// Update modifies an existing FloorDetails record in the database. +// Returns an error if the record does not exist or update fails. +func (r *floorDetailsRepository) Update(ctx context.Context, floorDetails *models.FloorDetails) error { + result := r.db.Save(floorDetails) + if result.Error != nil { + return fmt.Errorf("failed to update floor details with id %s: %w", floorDetails.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("floor details with id %s not found for update", floorDetails.ID) + } + return nil +} + +// Delete removes a FloorDetails record by its ID. +// Returns an error if the record does not exist or deletion fails. +func (r *floorDetailsRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.FloorDetails{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete floor details with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("floor details with id %s not found for deletion", id) + } + return nil +} + +// GetByConstructionDetailsID retrieves all FloorDetails records for a given construction details ID. +func (r *floorDetailsRepository) GetByConstructionDetailsID(ctx context.Context, constructionDetailsID uuid.UUID) ([]*models.FloorDetails, error) { + var floorDetails []*models.FloorDetails + err := r.db.Where("construction_details_id = ?", constructionDetailsID).Find(&floorDetails).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("no floor details found for construction details id %s", constructionDetailsID) + } + return nil, fmt.Errorf("failed to get floor details by construction details id %s: %w", constructionDetailsID, err) + } + return floorDetails, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/gis_repository.go b/property-tax/enumeration-backend/internal/repositories/gis_repository.go new file mode 100644 index 0000000..7ddf1ce --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/gis_repository.go @@ -0,0 +1,157 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// gisRepository implements the GISRepository interface using GORM. +var _ GISRepository = (*gisRepository)(nil) + +type gisRepository struct { + db *gorm.DB // GORM database connection +} + +// NewGISRepository creates a new repository for GIS data. +func NewGISRepository(db *gorm.DB) GISRepository { + return &gisRepository{db: db} +} + +// Create inserts a new GISData record into the database after validating required fields. +// Returns an error if validation fails or GIS data already exists for the property. +func (r *gisRepository) Create(ctx context.Context, gisData *models.GISData) error { + // Validate required fields + if gisData.PropertyID == uuid.Nil { + return fmt.Errorf("property ID is required") + } + if gisData.Source == "" { + return fmt.Errorf("source is required") + } + if gisData.Type == "" { + return fmt.Errorf("type is required") + } + + // Check if GIS data already exists for this property + var existingGISData models.GISData + if err := r.db.Where("property_id = ?", gisData.PropertyID).First(&existingGISData).Error; err == nil { + return fmt.Errorf("GIS data already exists for property ID %s", gisData.PropertyID) + } + + // Create the GIS data + if err := r.db.Create(gisData).Error; err != nil { + return fmt.Errorf("failed to create GIS data: %w", err) + } + + return nil +} + +// GetByID retrieves a GISData record by its ID, including related Coordinates. +func (r *gisRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.GISData, error) { + var gisData models.GISData + if err := r.db.Preload("Coordinates").First(&gisData, "id = ?", id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("GIS data not found with ID %s", id) + } + return nil, fmt.Errorf("failed to get GIS data: %w", err) + } + return &gisData, nil +} + +// GetByPropertyID retrieves a GISData record by property ID, including related Coordinates. +func (r *gisRepository) GetByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.GISData, error) { + var gisData models.GISData + if err := r.db.Preload("Coordinates").First(&gisData, "property_id = ?", propertyID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("GIS data not found for property ID %s", propertyID) + } + return nil, fmt.Errorf("failed to get GIS data: %w", err) + } + return &gisData, nil +} + +// Update modifies an existing GISData record in the database. +// Checks for property ID conflicts and reloads the updated data with related Coordinates. +func (r *gisRepository) Update(ctx context.Context, gisData *models.GISData) error { + if gisData.ID == uuid.Nil { + return fmt.Errorf("GIS data ID is required for update") + } + + // Check if the record exists + var existingGISData models.GISData + if err := r.db.First(&existingGISData, "id = ?", gisData.ID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("GIS data not found with ID %s", gisData.ID) + } + return fmt.Errorf("failed to find GIS data: %w", err) + } + + // If property ID is being changed, check if another GIS data exists for the new property + if gisData.PropertyID != existingGISData.PropertyID { + var conflictingGISData models.GISData + if err := r.db.Where("property_id = ? AND id != ?", gisData.PropertyID, gisData.ID).First(&conflictingGISData).Error; err == nil { + return fmt.Errorf("GIS data already exists for property ID %s", gisData.PropertyID) + } + } + + // Update the record + if err := r.db.Model(&existingGISData).Updates(gisData).Error; err != nil { + return fmt.Errorf("failed to update GIS data: %w", err) + } + + // Reload the updated data + if err := r.db.Preload("Coordinates").First(gisData, "id = ?", gisData.ID).Error; err != nil { + return fmt.Errorf("failed to reload updated GIS data: %w", err) + } + + return nil +} + +// Delete removes a GISData record by its ID. +// Returns an error if the record does not exist or deletion fails. +// Related coordinates will be deleted automatically due to CASCADE. +func (r *gisRepository) Delete(ctx context.Context, id uuid.UUID) error { + // Check if the record exists + var gisData models.GISData + if err := r.db.First(&gisData, "id = ?", id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("GIS data not found with ID %s", id) + } + return fmt.Errorf("failed to find GIS data: %w", err) + } + + // Delete the record (coordinates will be deleted automatically due to CASCADE) + if err := r.db.Delete(&gisData).Error; err != nil { + return fmt.Errorf("failed to delete GIS data: %w", err) + } + + return nil +} + +// GetAll retrieves all GISData records with pagination, including related Coordinates. +// Returns the paginated GIS data and the total count. +func (r *gisRepository) GetAll(ctx context.Context, page, size int) ([]*models.GISData, int64, error) { + var gisDataList []*models.GISData + var total int64 + + // Count total records + if err := r.db.Model(&models.GISData{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count GIS data: %w", err) + } + + // Calculate offset + offset := page * size + + // Retrieve paginated records + if err := r.db.Preload("Coordinates"). + Offset(offset). + Limit(size). + Find(&gisDataList).Error; err != nil { + return nil, 0, fmt.Errorf("failed to get GIS data: %w", err) + } + + return gisDataList, total, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/igrs_repository.go b/property-tax/enumeration-backend/internal/repositories/igrs_repository.go new file mode 100644 index 0000000..b6d3e19 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/igrs_repository.go @@ -0,0 +1,84 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// igrsRepository implements the IGRSRepository interface using GORM for database operations. +var _ IGRSRepository = (*igrsRepository)(nil) + +type igrsRepository struct { + db *gorm.DB // GORM database connection +} + +// NewIGRSRepository creates a new repository for IGRS data. +func NewIGRSRepository(db *gorm.DB) IGRSRepository { + return &igrsRepository{db: db} +} + +// Create inserts a new IGRS record into the database. +// Returns an error if the operation fails. +func (r *igrsRepository) Create(ctx context.Context, igrs *models.IGRS) error { + if err := r.db.WithContext(ctx).Create(igrs).Error; err != nil { + return fmt.Errorf("failed to create igrs: %w", err) + } + return nil +} + +// GetByPropertyID retrieves an IGRS record by property ID. +// Returns the IGRS record or an error if not found. +func (r *igrsRepository) GetByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.IGRS, error) { + var m models.IGRS + if err := r.db.WithContext(ctx).First(&m, "property_id = ?", propertyID).Error; err != nil { + return nil, err + } + return &m, nil +} + +// GetByID retrieves an IGRS record by its ID. +// Returns the IGRS record or an error if not found. +func (r *igrsRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.IGRS, error) { + var m models.IGRS + if err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error; err != nil { + return nil, err + } + return &m, nil +} + +// Update modifies an existing IGRS record in the database. +// Returns an error if the update fails. +func (r *igrsRepository) Update(ctx context.Context, igrs *models.IGRS) error { + if err := r.db.WithContext(ctx).Save(igrs).Error; err != nil { + return fmt.Errorf("failed to update igrs: %w", err) + } + return nil +} + +// Delete removes an IGRS record by its ID. +// Returns an error if the deletion fails. +func (r *igrsRepository) Delete(ctx context.Context, id uuid.UUID) error { + if err := r.db.WithContext(ctx).Delete(&models.IGRS{}, "id = ?", id).Error; err != nil { + return fmt.Errorf("failed to delete igrs: %w", err) + } + return nil +} + +// FindAll retrieves all IGRS records with pagination. +// Returns the list of IGRS records, total count, or an error. +func (r *igrsRepository) FindAll(ctx context.Context, page, size int) ([]models.IGRS, int64, error) { + var list []models.IGRS + var total int64 + if err := r.db.WithContext(ctx).Model(&models.IGRS{}).Count(&total).Error; err != nil { + return nil, 0, err + } + offset := page * size + if err := r.db.WithContext(ctx).Limit(size).Offset(offset).Find(&list).Error; err != nil { + return nil, 0, err + } + return list, total, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/interfaces.go b/property-tax/enumeration-backend/internal/repositories/interfaces.go new file mode 100644 index 0000000..7afd043 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/interfaces.go @@ -0,0 +1,197 @@ +package repositories + +import ( + "context" + "enumeration/internal/dto" + "enumeration/internal/models" + + "github.com/google/uuid" +) + +// ApplicationLogRepository handles CRUD for ApplicationLog entities +type ApplicationLogRepository interface { + Create(ctx context.Context, log *models.ApplicationLog) error + GetByID(ctx context.Context, id uuid.UUID) (*models.ApplicationLog, error) + Update(ctx context.Context, log *models.ApplicationLog) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, applicationID, action string, page, size int) ([]models.ApplicationLog, int64, error) + GetByApplicationID(ctx context.Context, applicationID uuid.UUID, action string, page, size int) ([]models.ApplicationLog, int64, error) +} + +// FloorDetailsRepository handles CRUD for FloorDetails entities +type FloorDetailsRepository interface { + Create(ctx context.Context, floorDetails *models.FloorDetails) error + GetByID(ctx context.Context, id uuid.UUID) (*models.FloorDetails, error) + Update(ctx context.Context, floorDetails *models.FloorDetails) error + Delete(ctx context.Context, id uuid.UUID) error + GetAll(ctx context.Context, page, size int, constructionDetailsID *uuid.UUID) ([]*models.FloorDetails, int64, error) + GetByConstructionDetailsID(ctx context.Context, constructionDetailsID uuid.UUID) ([]*models.FloorDetails, error) +} + +// CoordinatesRepository handles CRUD and batch operations for Coordinates entities +type CoordinatesRepository interface { + Create(ctx context.Context, coordinates *models.Coordinates) error + GetByID(ctx context.Context, id uuid.UUID) (*models.Coordinates, error) + Update(ctx context.Context, coordinates *models.Coordinates) error + Delete(ctx context.Context, id uuid.UUID) error + FindAll(ctx context.Context, page, size int, gisDataID *uuid.UUID) ([]models.Coordinates, int64, error) + FindByGISDataID(ctx context.Context, gisDataID uuid.UUID) ([]models.Coordinates, error) + DeleteByGISDataID(ctx context.Context, gisDataID uuid.UUID) error + CreateBatch(ctx context.Context, coords []*models.Coordinates) error + ReplaceByGISDataID(ctx context.Context, gisDataID uuid.UUID, coords []*models.Coordinates) error +} + +// ApplicationRepository handles CRUD and search for Application entities +type ApplicationRepository interface { + Create(ctx context.Context, application *models.Application) error + GetByID(ctx context.Context, id uuid.UUID) (*models.Application, error) + GetByApplicationNo(ctx context.Context, applicationNo string) (*models.Application, error) + Update(ctx context.Context, application *models.Application) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, page, size int) ([]models.Application, int64, error) + Search(ctx context.Context, criteria *dto.ApplicationSearchCriteria, page, size int) ([]*models.Application, int64, error) + ExistsByApplicationNo(ctx context.Context, applicationNo string) (bool, error) + GetApplicationsByIDs(ctx context.Context, ids []string, state string, page, size int) ([]models.Application, int64, error) + GetByTenantIDAndStatus(ctx context.Context, tenantID string, status string, page, size int) ([]models.Application, int64, error) + GetByAssignedAgent(ctx context.Context, agentID string, status string, page, size int) ([]models.Application, int64, error) + CheckUserExists(ctx context.Context, userId string) error +} + +// PropertyOwnerRepository handles CRUD and batch operations for PropertyOwner entities +type PropertyOwnerRepository interface { + Create(ctx context.Context, owner *models.PropertyOwner) error + CreateBatch(ctx context.Context, owners []*models.PropertyOwner) error + GetAll(ctx context.Context) ([]*models.PropertyOwner, error) + GetByID(ctx context.Context, id uuid.UUID) (*models.PropertyOwner, error) + GetByPropertyID(ctx context.Context, propertyID uuid.UUID, page, size int) ([]*models.PropertyOwner, int64, error) + Update(ctx context.Context, owner *models.PropertyOwner) error + Delete(ctx context.Context, id uuid.UUID) error +} + +// ConstructionDetailsRepository handles CRUD for ConstructionDetails entities +type ConstructionDetailsRepository interface { + Create(ctx context.Context, constructionDetails *models.ConstructionDetails) error + GetByID(ctx context.Context, id uuid.UUID) (*models.ConstructionDetails, error) + Update(ctx context.Context, constructionDetails *models.ConstructionDetails) error + Delete(ctx context.Context, id uuid.UUID) error + GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.ConstructionDetails, int64, error) + GetByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.ConstructionDetails, error) +} + +// AdditionalPropertyDetailsRepository handles CRUD and filtering for AdditionalPropertyDetails entities +type AdditionalPropertyDetailsRepository interface { + Create(ctx context.Context, details *models.AdditionalPropertyDetails) error + GetByID(ctx context.Context, id uuid.UUID) (*models.AdditionalPropertyDetails, error) + Update(ctx context.Context, details *models.AdditionalPropertyDetails) error + Delete(ctx context.Context, id uuid.UUID) error + GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID, fieldName *string) ([]*models.AdditionalPropertyDetails, int64, error) + GetByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.AdditionalPropertyDetails, error) + GetByFieldName(ctx context.Context, fieldName string) ([]*models.AdditionalPropertyDetails, error) +} + +// AssessmentDetailsRepository handles CRUD for AssessmentDetails entities +type AssessmentDetailsRepository interface { + Create(ctx context.Context, assessmentDetails *models.AssessmentDetails) error + GetByID(ctx context.Context, id uuid.UUID) (*models.AssessmentDetails, error) + Update(ctx context.Context, assessmentDetails *models.AssessmentDetails) error + Delete(ctx context.Context, id uuid.UUID) error + GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.AssessmentDetails, int64, error) + GetByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.AssessmentDetails, error) +} + +// PropertyRepository handles CRUD and search for Property entities +type PropertyRepository interface { + Create(ctx context.Context, property *models.Property) error + GetByID(ctx context.Context, id uuid.UUID) (*models.Property, error) + Update(ctx context.Context, property *models.Property) error + Delete(ctx context.Context, id uuid.UUID) error + GetAll(ctx context.Context, page, size int, propertyType *string) ([]*models.Property, int64, error) + GetByPropertyNo(ctx context.Context, propertyNo string) (*models.Property, error) + Search(ctx context.Context, params SearchPropertyParams) ([]*models.Property, int64, error) +} + +// SearchPropertyParams holds filters and options for property search +type SearchPropertyParams struct { + Page int + Size int + PropertyType *string + OwnershipType *string + ComplexName *string + Locality *string + WardNo *string + ZoneNo *string + Street *string + SortBy string + // SearchPropertyParams holds parameters for searching properties with various filters and sorting options. + SortOrder string +} + +// PropertyAddressRepository handles CRUD and search for PropertyAddress entities +type PropertyAddressRepository interface { + Create(address *models.PropertyAddress) error + GetByID(id uuid.UUID) (*models.PropertyAddress, error) + Update(address *models.PropertyAddress) error + Delete(id uuid.UUID) error + GetAll(page, size int, propertyID *uuid.UUID) ([]*models.PropertyAddress, int64, error) + GetByPropertyID(propertyID uuid.UUID) (*models.PropertyAddress, error) + Search(params SearchPropertyAddressParams) ([]*models.PropertyAddress, int64, error) +} + +// SearchPropertyAddressParams holds filters and options for property address search +type SearchPropertyAddressParams struct { + Page int + Size int + PropertyID *uuid.UUID + Locality *string + ZoneNo *string + WardNo *string + BlockNo *string + Street *string + ElectionWard *string + SecretariatWard *string + PinCode *uint64 + SortBy string + SortOrder string +} + +// GISRepository handles CRUD and pagination for GISData entities +type GISRepository interface { + Create(ctx context.Context, gisData *models.GISData) error + GetByID(ctx context.Context, id uuid.UUID) (*models.GISData, error) + GetByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.GISData, error) + Update(ctx context.Context, gisData *models.GISData) error + Delete(ctx context.Context, id uuid.UUID) error + GetAll(ctx context.Context, page, size int) ([]*models.GISData, int64, error) +} + +// AmenityRepository handles CRUD and filtering for Amenities entities +type AmenityRepository interface { + GetAll(ctx context.Context) ([]models.Amenities, error) + GetAllWithFilters(ctx context.Context, page, size int, amenityType, propertyID string) ([]models.Amenities, int64, error) + GetByID(ctx context.Context, id string) (*models.Amenities, error) + Create(ctx context.Context, amenity *models.Amenities) error + Update(ctx context.Context, id string, amenity *models.Amenities) error + Delete(ctx context.Context, id string) error + GetByPropertyID(ctx context.Context, propertyID string) (*models.Amenities, error) +} + +// DocumentRepository handles CRUD and batch operations for Document entities +type DocumentRepository interface { + Create(ctx context.Context, doc *models.Document) error + CreateBatch(ctx context.Context, docs []*models.Document) error + GetByID(ctx context.Context, id uuid.UUID) (*models.Document, error) + GetByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.Document, error) + GetAll(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.Document, int64, error) + Delete(ctx context.Context, id uuid.UUID) error + Update(ctx context.Context, doc *models.Document) error +} + +// IGRSRepository handles CRUD and pagination for IGRS entities +type IGRSRepository interface { + Create(ctx context.Context, igrs *models.IGRS) error + GetByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.IGRS, error) + GetByID(ctx context.Context, id uuid.UUID) (*models.IGRS, error) + Update(ctx context.Context, igrs *models.IGRS) error + Delete(ctx context.Context, id uuid.UUID) error + FindAll(ctx context.Context, page, size int) ([]models.IGRS, int64, error) +} diff --git a/property-tax/enumeration-backend/internal/repositories/property_address_repository.go b/property-tax/enumeration-backend/internal/repositories/property_address_repository.go new file mode 100644 index 0000000..b4a079f --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/property_address_repository.go @@ -0,0 +1,225 @@ +package repositories + +import ( + "enumeration/internal/models" + "fmt" + "strings" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Ensure propertyAddressRepository implements PropertyAddressRepository interface at compile time. +var _ PropertyAddressRepository = (*propertyAddressRepository)(nil) + +// propertyAddressRepository provides implementation for PropertyAddressRepository using GORM for database operations. +type propertyAddressRepository struct { + db *gorm.DB +} + +// NewPropertyAddressRepository creates a new instance of propertyAddressRepository. +// db: GORM database connection. +// Returns: PropertyAddressRepository implementation. +func NewPropertyAddressRepository(db *gorm.DB) PropertyAddressRepository { + return &propertyAddressRepository{db: db} +} + +// Create inserts a new PropertyAddress record into the database. +// address: pointer to PropertyAddress model to be created. +// Returns: error if creation fails. +func (r *propertyAddressRepository) Create(address *models.PropertyAddress) error { + if err := r.db.Create(address).Error; err != nil { + return fmt.Errorf("failed to create property address: %w", err) + } + return nil +} + +// GetByID retrieves a PropertyAddress by its unique ID. +// id: UUID of the property address. +// Returns: pointer to PropertyAddress and error if not found or on failure. +func (r *propertyAddressRepository) GetByID(id uuid.UUID) (*models.PropertyAddress, error) { + var address models.PropertyAddress + err := r.db.Where("id = ?", id).First(&address).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("property address with id %s not found", id) + } + return nil, fmt.Errorf("failed to get property address by id %s: %w", id, err) + } + return &address, nil +} + +// Update modifies an existing PropertyAddress record in the database. +// address: pointer to PropertyAddress model with updated data. +// Returns: error if update fails or record not found. +func (r *propertyAddressRepository) Update(address *models.PropertyAddress) error { + result := r.db.Save(address) + if result.Error != nil { + return fmt.Errorf("failed to update property address with id %s: %w", address.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("property address with id %s not found for update", address.ID) + } + return nil +} + +// Delete removes a PropertyAddress record by its unique ID. +// id: UUID of the property address to delete. +// Returns: error if deletion fails or record not found. +func (r *propertyAddressRepository) Delete(id uuid.UUID) error { + result := r.db.Delete(&models.PropertyAddress{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete property address with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("property address with id %s not found for deletion", id) + } + return nil +} + +// GetAll retrieves all PropertyAddress records, optionally filtered by propertyID, with pagination. +// page: page number (zero-based), size: number of records per page, propertyID: optional filter. +// Returns: slice of PropertyAddress pointers, total count, and error if any. +func (r *propertyAddressRepository) GetAll(page, size int, propertyID *uuid.UUID) ([]*models.PropertyAddress, int64, error) { + var addresses []*models.PropertyAddress + var total int64 + + query := r.db.Model(&models.PropertyAddress{}) + + if propertyID != nil { + query = query.Where("property_id = ?", *propertyID) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count property addresses: %w", err) + } + + // Apply pagination + offset := page * size + if err := query.Offset(offset).Limit(size).Find(&addresses).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no property addresses found") + } + return nil, 0, fmt.Errorf("failed to get property addresses: %w", err) + } + + return addresses, total, nil +} + +// GetByPropertyID retrieves a PropertyAddress by its associated property ID. +// propertyID: UUID of the property. +// Returns: pointer to PropertyAddress and error if not found or on failure. +func (r *propertyAddressRepository) GetByPropertyID(propertyID uuid.UUID) (*models.PropertyAddress, error) { + var address models.PropertyAddress + err := r.db.Where("property_id = ?", propertyID).First(&address).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("property address with property id %s not found", propertyID) + } + return nil, fmt.Errorf("failed to get property address by property id %s: %w", propertyID, err) + } + return &address, nil +} + +// Search finds PropertyAddress records matching the given search parameters, with filtering, sorting, and pagination. +// params: SearchPropertyAddressParams struct with filter and pagination options. +// Returns: slice of PropertyAddress pointers, total count, and error if any. +func (r *propertyAddressRepository) Search(params SearchPropertyAddressParams) ([]*models.PropertyAddress, int64, error) { + var addresses []*models.PropertyAddress + var total int64 + + query := r.db.Model(&models.PropertyAddress{}) + + // Apply filters + var conditions []string + var args []interface{} + + if params.PropertyID != nil { + conditions = append(conditions, "property_id = ?") + args = append(args, *params.PropertyID) + } + + if params.Locality != nil && *params.Locality != "" { + conditions = append(conditions, "locality ILIKE ?") + args = append(args, "%"+*params.Locality+"%") + } + + if params.ZoneNo != nil && *params.ZoneNo != "" { + conditions = append(conditions, "zone_no = ?") + args = append(args, *params.ZoneNo) + } + + if params.WardNo != nil && *params.WardNo != "" { + conditions = append(conditions, "ward_no = ?") + args = append(args, *params.WardNo) + } + + if params.BlockNo != nil && *params.BlockNo != "" { + conditions = append(conditions, "block_no = ?") + args = append(args, *params.BlockNo) + } + + if params.Street != nil && *params.Street != "" { + conditions = append(conditions, "street ILIKE ?") + args = append(args, "%"+*params.Street+"%") + } + + if params.ElectionWard != nil && *params.ElectionWard != "" { + conditions = append(conditions, "election_ward = ?") + args = append(args, *params.ElectionWard) + } + + if params.SecretariatWard != nil && *params.SecretariatWard != "" { + conditions = append(conditions, "secretariat_ward = ?") + args = append(args, *params.SecretariatWard) + } + + if params.PinCode != nil { + conditions = append(conditions, "pin_code = ?") + args = append(args, *params.PinCode) + } + + if len(conditions) > 0 { + query = query.Where(strings.Join(conditions, " AND "), args...) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count property addresses: %w", err) + } + + // Apply sorting + orderBy := "created_at DESC" + if params.SortBy != "" { + direction := "ASC" + if strings.ToUpper(params.SortOrder) == "DESC" { + direction = "DESC" + } + + switch params.SortBy { + case "locality": + orderBy = fmt.Sprintf("locality %s", direction) + case "zoneNo": + orderBy = fmt.Sprintf("zone_no %s", direction) + case "wardNo": + orderBy = fmt.Sprintf("ward_no %s", direction) + case "createdAt": + orderBy = fmt.Sprintf("created_at %s", direction) + case "updatedAt": + orderBy = fmt.Sprintf("updated_at %s", direction) + default: + orderBy = "created_at DESC" + } + } + + // Apply pagination and ordering + offset := params.Page * params.Size + if err := query.Order(orderBy).Offset(offset).Limit(params.Size).Find(&addresses).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no property addresses found for search params") + } + return nil, 0, fmt.Errorf("failed to get property addresses: %w", err) + } + return addresses, total, nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/property_owner_repository.go b/property-tax/enumeration-backend/internal/repositories/property_owner_repository.go new file mode 100644 index 0000000..84c6c49 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/property_owner_repository.go @@ -0,0 +1,155 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Ensure propertyOwnerRepository implements PropertyOwnerRepository interface at compile time. +var _ PropertyOwnerRepository = (*propertyOwnerRepository)(nil) + +// propertyOwnerRepository provides implementation for PropertyOwnerRepository using GORM for database operations. +type propertyOwnerRepository struct { + db *gorm.DB +} + +// NewPropertyOwnerRepository creates a new instance of propertyOwnerRepository. +// db: GORM database connection. +// Returns: PropertyOwnerRepository implementation. +func NewPropertyOwnerRepository(db *gorm.DB) PropertyOwnerRepository { + return &propertyOwnerRepository{db: db} +} + +// CreateBatch inserts multiple PropertyOwner records in batches using a transaction for efficiency. +// ctx: context for the operation. +// owners: slice of PropertyOwner pointers to be created. +// Returns: error if batch creation fails. +func (r *propertyOwnerRepository) CreateBatch(ctx context.Context, owners []*models.PropertyOwner) error { + // Use a transaction and CreateInBatches for efficiency + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if len(owners) == 0 { + return nil + } + // Adjust batch size if needed + if err := tx.CreateInBatches(owners, 100).Error; err != nil { + return fmt.Errorf("failed to create property owners batch: %w", err) + } + return nil + }) +} + +// Create inserts a new PropertyOwner record into the database. +// ctx: context for the operation. +// owner: pointer to PropertyOwner model to be created. +// Returns: error if creation fails. +func (r *propertyOwnerRepository) Create(ctx context.Context, owner *models.PropertyOwner) error { + + if err := r.db.WithContext(ctx).Create(owner).Error; err != nil { + return fmt.Errorf("failed to create property owner: %w", err) + } + return nil +} + +// GetAll retrieves all PropertyOwner records from the database. +// ctx: context for the operation. +// Returns: slice of PropertyOwner pointers and error if any. +func (r *propertyOwnerRepository) GetAll(ctx context.Context) ([]*models.PropertyOwner, error) { + var owners []*models.PropertyOwner + err := r.db.WithContext(ctx).Find(&owners).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("no property owners found") + } + return nil, fmt.Errorf("failed to get property owners: %w", err) + } + return owners, nil +} + +// GetByID retrieves a PropertyOwner by its unique ID. +// ctx: context for the operation. +// id: UUID of the property owner. +// Returns: pointer to PropertyOwner and error if not found or on failure. +func (r *propertyOwnerRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.PropertyOwner, error) { + var owner models.PropertyOwner + err := r.db.WithContext(ctx).First(&owner, "id = ?", id).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("property owner with id %s not found", id) + } + return nil, fmt.Errorf("failed to get property owner by id %s: %w", id, err) + } + return &owner, nil +} + +// GetByPropertyID retrieves property owners for a property, with manual pagination handled in the repository. +// ctx: context for the operation. +// propertyID: UUID of the property. +// page: page number (zero-based). +// size: number of records per page. +// Returns: slice of PropertyOwner pointers, total count, and error if any. +func (r *propertyOwnerRepository) GetByPropertyID(ctx context.Context, propertyID uuid.UUID, page, size int) ([]*models.PropertyOwner, int64, error) { + // clamp pagination + if page < 0 { + page = 0 + } + if size <= 0 || size > 100 { + size = 20 + } + + var total int64 + query := r.db.WithContext(ctx).Model(&models.PropertyOwner{}).Where("property_id = ?", propertyID) + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count property owners for property id %s: %w", propertyID, err) + } + + // if no rows, return empty slice + total 0 + if total == 0 { + return []*models.PropertyOwner{}, 0, nil + } + + var owners []*models.PropertyOwner + offset := page * size + if err := query.Order("created_at asc").Limit(size).Offset(offset).Find(&owners).Error; err != nil { + // treat no rows as empty result + if err == gorm.ErrRecordNotFound { + return []*models.PropertyOwner{}, total, nil + } + return nil, 0, fmt.Errorf("failed to get property owners by property id %s: %w", propertyID, err) + } + + return owners, total, nil +} + +// Update modifies an existing PropertyOwner record in the database. +// ctx: context for the operation. +// owner: pointer to PropertyOwner model with updated data. +// Returns: error if update fails or record not found. +func (r *propertyOwnerRepository) Update(ctx context.Context, owner *models.PropertyOwner) error { + result := r.db.WithContext(ctx).Save(owner) + if result.Error != nil { + return fmt.Errorf("failed to update property owner with id %s: %w", owner.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("property owner with id %s not found for update", owner.ID) + } + return nil +} + +// Delete removes a PropertyOwner record by its unique ID. +// ctx: context for the operation. +// id: UUID of the property owner to delete. +// Returns: error if deletion fails or record not found. +func (r *propertyOwnerRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.WithContext(ctx).Delete(&models.PropertyOwner{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete property owner with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("property owner with id %s not found for deletion", id) + } + return nil +} diff --git a/property-tax/enumeration-backend/internal/repositories/property_repository.go b/property-tax/enumeration-backend/internal/repositories/property_repository.go new file mode 100644 index 0000000..9c30711 --- /dev/null +++ b/property-tax/enumeration-backend/internal/repositories/property_repository.go @@ -0,0 +1,275 @@ +package repositories + +import ( + "context" + "enumeration/internal/models" + "fmt" + "strings" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Ensure propertyRepository implements PropertyRepository interface at compile time. +var _ PropertyRepository = (*propertyRepository)(nil) + +// propertyRepository provides implementation for PropertyRepository using GORM for database operations. +type propertyRepository struct { + db *gorm.DB +} + +// NewPropertyRepository creates a new instance of propertyRepository. +// db: GORM database connection. +// Returns: PropertyRepository implementation. +func NewPropertyRepository(db *gorm.DB) PropertyRepository { + return &propertyRepository{db: db} +} + +// Create inserts a new Property record and its address (if provided) into the database using a transaction. +// ctx: context for the operation. +// property: pointer to Property model to be created. +// Returns: error if creation fails. +func (r *propertyRepository) Create(ctx context.Context, property *models.Property) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // Create property first + if err := tx.Create(property).Error; err != nil { + return fmt.Errorf("failed to create property: %w", err) + } + + // If address is provided, ensure PropertyID is set and create it + if property.Address != nil { + property.Address.PropertyID = property.ID + if err := tx.Create(property.Address).Error; err != nil { + return fmt.Errorf("failed to create property address: %w", err) + } + } + + return nil + }) +} + +// GetByID retrieves a Property by its unique ID, preloading related entities. +// ctx: context for the operation. +// id: UUID of the property. +// Returns: pointer to Property and error if not found or on failure. +func (r *propertyRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Property, error) { + var property models.Property + err := r.db. + Preload("Address"). + Preload("AssessmentDetails"). + Preload("Amenities"). + Preload("ConstructionDetails"). + Preload("ConstructionDetails.FloorDetails"). + Preload("AdditionalDetails"). + Preload("GISData"). + Preload("GISData.Coordinates"). + Preload("IGRS"). + Preload("Documents"). + Where("id = ?", id).First(&property).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("property with id %s not found", id) + } + return nil, fmt.Errorf("failed to get property by id %s: %w", id, err) + } + return &property, nil +} + +// Update modifies an existing Property record in the database. +// ctx: context for the operation. +// property: pointer to Property model with updated data. +// Returns: error if update fails or record not found. +func (r *propertyRepository) Update(ctx context.Context, property *models.Property) error { + result := r.db.Model(property).Updates(property) + if result.Error != nil { + return fmt.Errorf("failed to update property with id %s: %w", property.ID, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("property with id %s not found for update", property.ID) + } + return nil +} + +// Delete removes a Property record by its unique ID. +// ctx: context for the operation. +// id: UUID of the property to delete. +// Returns: error if deletion fails or record not found. +func (r *propertyRepository) Delete(ctx context.Context, id uuid.UUID) error { + result := r.db.Delete(&models.Property{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete property with id %s: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("property with id %s not found for deletion", id) + } + return nil +} + +// GetAll retrieves all Property records, optionally filtered by propertyType, with pagination and preloaded relations. +// ctx: context for the operation. +// page: page number (zero-based), size: number of records per page, propertyType: optional filter. +// Returns: slice of Property pointers, total count, and error if any. +func (r *propertyRepository) GetAll(ctx context.Context, page, size int, propertyType *string) ([]*models.Property, int64, error) { + var properties []*models.Property + var total int64 + + query := r.db.Model(&models.Property{}). + Preload("Address"). + Preload("AssessmentDetails"). + Preload("Amenities"). + Preload("ConstructionDetails"). + Preload("ConstructionDetails.FloorDetails"). + Preload("AdditionalDetails"). + Preload("GISData"). + Preload("GISData.Coordinates"). + Preload("IGRS"). + Preload("Documents") + + if propertyType != nil && *propertyType != "" { + query = query.Where("property_type = ?", *propertyType) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count properties: %w", err) + } + + // Apply pagination + offset := page * size + if err := query.Offset(offset).Limit(size).Find(&properties).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no properties found") + } + return nil, 0, fmt.Errorf("failed to get properties: %w", err) + } + + return properties, total, nil +} + +// GetByPropertyNo retrieves a Property by its property number, preloading related entities. +// ctx: context for the operation. +// propertyNo: property number string. +// Returns: pointer to Property and error if not found or on failure. +func (r *propertyRepository) GetByPropertyNo(ctx context.Context, propertyNo string) (*models.Property, error) { + var property models.Property + err := r.db. + Preload("Address"). + Preload("AssessmentDetails"). + Preload("Amenities"). + Preload("ConstructionDetails"). + Preload("ConstructionDetails.FloorDetails"). + Preload("AdditionalDetails"). + Preload("GISData"). + Preload("GISData.Coordinates"). + Preload("Documents"). + Preload("IGRS"). + Where("property_no = ?", propertyNo).First(&property).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("property with property_no %s not found", propertyNo) + } + return nil, fmt.Errorf("failed to get property by property_no %s: %w", propertyNo, err) + } + return &property, nil +} + +// Search finds Property records matching the given search parameters, with filtering, sorting, pagination, and preloaded relations. +// ctx: context for the operation. +// params: SearchPropertyParams struct with filter and pagination options. +// Returns: slice of Property pointers, total count, and error if any. +func (r *propertyRepository) Search(ctx context.Context, params SearchPropertyParams) ([]*models.Property, int64, error) { + var properties []*models.Property + var total int64 + + // Use the exact quoted table names as they appear in PostgreSQL + propertiesTable := `"DIGIT3"."properties"` + addressesTable := `"DIGIT3"."property_addresses"` + + query := r.db.Model(&models.Property{}). + Preload("Address"). + Preload("AssessmentDetails"). + Preload("ConstructionDetails"). + Preload("AdditionalDetails"). + Joins(fmt.Sprintf("LEFT JOIN %s pa ON %s.id = pa.property_id", addressesTable, propertiesTable)) + + // Apply filters + var conditions []string + var args []interface{} + + if params.PropertyType != nil && *params.PropertyType != "" { + conditions = append(conditions, fmt.Sprintf("%s.property_type = ?", propertiesTable)) + args = append(args, *params.PropertyType) + } + + if params.OwnershipType != nil && *params.OwnershipType != "" { + conditions = append(conditions, fmt.Sprintf("%s.ownership_type = ?", propertiesTable)) + args = append(args, *params.OwnershipType) + } + + if params.ComplexName != nil && *params.ComplexName != "" { + conditions = append(conditions, fmt.Sprintf("%s.complex_name ILIKE ?", propertiesTable)) + args = append(args, "%"+*params.ComplexName+"%") + } + + if params.Locality != nil && *params.Locality != "" { + conditions = append(conditions, "pa.locality ILIKE ?") + args = append(args, "%"+*params.Locality+"%") + } + + if params.WardNo != nil && *params.WardNo != "" { + conditions = append(conditions, "pa.ward_no = ?") + args = append(args, *params.WardNo) + } + + if params.ZoneNo != nil && *params.ZoneNo != "" { + conditions = append(conditions, "pa.zone_no = ?") + args = append(args, *params.ZoneNo) + } + + if params.Street != nil && *params.Street != "" { + conditions = append(conditions, "pa.street ILIKE ?") + args = append(args, "%"+*params.Street+"%") + } + + if len(conditions) > 0 { + query = query.Where(strings.Join(conditions, " AND "), args...) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count properties: %w", err) + } + + // Apply sorting with proper table names + orderBy := fmt.Sprintf("%s.created_at DESC", propertiesTable) + if params.SortBy != "" { + direction := "ASC" + if strings.ToUpper(params.SortOrder) == "DESC" { + direction = "DESC" + } + + switch params.SortBy { + case "createdAt": + orderBy = fmt.Sprintf("%s.created_at %s", propertiesTable, direction) + case "updatedAt": + orderBy = fmt.Sprintf("%s.updated_at %s", propertiesTable, direction) + case "propertyNo": + orderBy = fmt.Sprintf("%s.property_no %s", propertiesTable, direction) + case "propertyType": + orderBy = fmt.Sprintf("%s.property_type %s", propertiesTable, direction) + default: + orderBy = fmt.Sprintf("%s.created_at DESC", propertiesTable) + } + } + + // Apply pagination and ordering + offset := params.Page * params.Size + if err := query.Order(orderBy).Offset(offset).Limit(params.Size).Find(&properties).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 0, fmt.Errorf("no properties found for search params") + } + return nil, 0, fmt.Errorf("failed to search properties: %w", err) + } + + return properties, total, nil +} diff --git a/property-tax/enumeration-backend/internal/routes/routes.go b/property-tax/enumeration-backend/internal/routes/routes.go new file mode 100644 index 0000000..5452fbb --- /dev/null +++ b/property-tax/enumeration-backend/internal/routes/routes.go @@ -0,0 +1,218 @@ +// Package routes defines all API route registrations for the service +package routes + +import ( + "enumeration/internal/handlers" + "enumeration/internal/middleware" + + "github.com/gin-gonic/gin" +) + +// SetupRoutes sets up all application routes +func SetupRoutes(router *gin.Engine, coordinatesHandler *handlers.CoordinatesHandler, floorDetailsHandler *handlers.FloorDetailsHandler, constructionDetailsHandler *handlers.ConstructionDetailsHandler, additionalPropertyDetailsHandler *handlers.AdditionalPropertyDetailsHandler, assessmentDetailsHandler *handlers.AssessmentDetailsHandler, propertyHandler *handlers.PropertyHandler, propertyAddressHandler *handlers.PropertyAddressHandler, gisHandler *handlers.GISHandler, applicationHandler *handlers.ApplicationHandler, propertyOwnerHandler *handlers.PropertyOwnerHandler, amenityHandler *handlers.AmenityHandler, documentHandler *handlers.DocumentHandler, igrsHandler *handlers.IGRSHandler, applicationLogHandler *handlers.ApplicationLogHandler) { + v1 := router.Group("/v1", middleware.AuthMiddleware()) + { + SetupCoordinatesRoutes(v1, coordinatesHandler) + SetupFloorDetailsRoutes(v1, floorDetailsHandler) + SetupApplicationRoutes(v1, applicationHandler) + SetupPropertyOwnerRoutes(v1, propertyOwnerHandler) + SetupConstructionDetailsRoutes(v1, constructionDetailsHandler) + SetupAdditionalPropertyDetailsRoutes(v1, additionalPropertyDetailsHandler) + SetupAssessmentDetailsRoutes(v1, assessmentDetailsHandler) + SetupPropertyRoutes(v1, propertyHandler) + SetupPropertyAddressRoutes(v1, propertyAddressHandler) + SetupGISRoutes(v1, gisHandler) + SetupAmenitiesRoutes(v1, amenityHandler) + SetupDocumentsRoutes(v1, documentHandler) + SetupIGRSRoutes(v1, igrsHandler) + SetupApplicationLogRoutes(v1, applicationLogHandler) + } +} + +// RegisterApplicationLogRoutes registers all application log routes +func SetupApplicationLogRoutes(router *gin.RouterGroup, handler *handlers.ApplicationLogHandler) { + v1 := router.Group("/application-logs") + { + // Application log endpoints + v1.GET("", handler.GetApplicationLogs) + v1.POST("", handler.CreateApplicationLog) + v1.GET("/:id", handler.GetApplicationLogByID) + v1.PUT("/:id", handler.UpdateApplicationLog) + v1.DELETE("/:id", handler.DeleteApplicationLog) + + // Get logs by application ID + v1.GET("/logs/:id", handler.GetApplicationLogsByApplicationID) + } +} + +// SetupApplicationRoutes registers routes for application endpoints +func SetupApplicationRoutes(router *gin.RouterGroup, applicationHandler *handlers.ApplicationHandler) { + applications := router.Group("/applications", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + applications.GET("/:id", middleware.MDMSRoleMiddleware("GET", "/v1/applications/:id"), applicationHandler.GetByID) + applications.GET("", middleware.MDMSRoleMiddleware("GET", "/v1/applications"), applicationHandler.List) + applications.GET("/search", middleware.MDMSRoleMiddleware("GET", "/v1/applications/search"), applicationHandler.Search) + applications.GET("/", middleware.MDMSRoleMiddleware("GET", "/v1/applications/:applicationNo"), applicationHandler.GetByApplicationNo) + + applications.POST("", middleware.MDMSRoleMiddleware("POST", "/v1/applications"), applicationHandler.Create) + + applications.PUT("/:id", middleware.MDMSRoleMiddleware("PUT", "/v1/applications/:id"), applicationHandler.Update) + applications.PATCH("/:id", middleware.ActionMiddleware(), applicationHandler.Action) + + applications.DELETE("/:id", middleware.MDMSRoleMiddleware("DELETE", "/v1/applications/:id"), applicationHandler.Delete) + } +} + +// SetupPropertyOwnerRoutes registers routes for property owner endpoints +func SetupPropertyOwnerRoutes(router *gin.RouterGroup, propertyOwnerHandler *handlers.PropertyOwnerHandler) { + propertyOwners := router.Group("/property-owners", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + propertyOwners.POST("", propertyOwnerHandler.Create) // POST /property-owners - Create property owner + propertyOwners.POST("/batch", propertyOwnerHandler.CreateBatch) // POST /property-owners/batch - Create owners in batch + propertyOwners.GET("/property/:propertyId", propertyOwnerHandler.GetByPropertyID) // GET /property-owners/property/{propertyId} + propertyOwners.PUT("/:id", propertyOwnerHandler.Update) // PUT /property-owners/{id} + propertyOwners.DELETE("/:id", propertyOwnerHandler.Delete) // DELETE /property-owners/{id} + } +} + +// SetupConstructionDetailsRoutes registers routes for construction details endpoints +func SetupConstructionDetailsRoutes(router *gin.RouterGroup, constructionDetailsHandler *handlers.ConstructionDetailsHandler) { + constructionDetails := router.Group("/construction-details", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + constructionDetails.POST("", constructionDetailsHandler.CreateConstructionDetails) + constructionDetails.GET("", constructionDetailsHandler.GetAllConstructionDetails) + constructionDetails.GET("/:id", constructionDetailsHandler.GetConstructionDetailsByID) + constructionDetails.PUT("/:id", constructionDetailsHandler.UpdateConstructionDetails) + constructionDetails.DELETE("/:id", constructionDetailsHandler.DeleteConstructionDetails) + constructionDetails.GET("/property/:propertyId", constructionDetailsHandler.GetConstructionDetailsByPropertyID) + } +} + +func SetupFloorDetailsRoutes(router *gin.RouterGroup, floorDetailsHandler *handlers.FloorDetailsHandler) { + floorDetails := router.Group("/floor-details", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + floorDetails.POST("", floorDetailsHandler.CreateFloorDetails) + floorDetails.GET("", floorDetailsHandler.GetAllFloorDetails) + floorDetails.GET("/:id", floorDetailsHandler.GetFloorDetailsByID) + floorDetails.PUT("/:id", floorDetailsHandler.UpdateFloorDetails) + floorDetails.DELETE("/:id", floorDetailsHandler.DeleteFloorDetails) + } +} + +func SetupAdditionalPropertyDetailsRoutes(router *gin.RouterGroup, handler *handlers.AdditionalPropertyDetailsHandler) { + additionalDetails := router.Group("/additional-property-details", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + additionalDetails.POST("", handler.CreateAdditionalPropertyDetails) + additionalDetails.GET("", handler.GetAllAdditionalPropertyDetails) + additionalDetails.GET("/:id", handler.GetAdditionalPropertyDetailsByID) + additionalDetails.PUT("/:id", handler.UpdateAdditionalPropertyDetails) + additionalDetails.DELETE("/:id", handler.DeleteAdditionalPropertyDetails) + additionalDetails.GET("/property/:propertyId", handler.GetAdditionalPropertyDetailsByPropertyID) + additionalDetails.GET("/field/:fieldName", handler.GetAdditionalPropertyDetailsByFieldName) + } +} + +// SetupAssessmentDetailsRoutes registers routes for assessment details endpoints +func SetupAssessmentDetailsRoutes(router *gin.RouterGroup, assessmentDetailsHandler *handlers.AssessmentDetailsHandler) { + assessmentDetails := router.Group("/assessment-details", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + assessmentDetails.POST("", assessmentDetailsHandler.CreateAssessmentDetails) + assessmentDetails.GET("", assessmentDetailsHandler.GetAllAssessmentDetails) + assessmentDetails.GET("/:id", assessmentDetailsHandler.GetAssessmentDetailsByID) + assessmentDetails.PUT("/:id", assessmentDetailsHandler.UpdateAssessmentDetails) + assessmentDetails.DELETE("/:id", assessmentDetailsHandler.DeleteAssessmentDetails) + assessmentDetails.GET("/property/:propertyId", assessmentDetailsHandler.GetAssessmentDetailsByPropertyID) + } +} + +// SetupPropertyRoutes registers routes for property endpoints +func SetupPropertyRoutes(router *gin.RouterGroup, propertyHandler *handlers.PropertyHandler) { + properties := router.Group("/properties", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + properties.POST("", propertyHandler.CreateProperty) + properties.GET("", propertyHandler.GetAllProperties) + properties.GET("/search", propertyHandler.SearchProperties) + properties.GET("/:id", propertyHandler.GetPropertyByID) + properties.PUT("/:id", propertyHandler.UpdateProperty) + properties.DELETE("/:id", propertyHandler.DeleteProperty) + properties.GET("/property-no/:propertyNo", propertyHandler.GetPropertyByPropertyNo) + } +} + +// SetupPropertyAddressRoutes registers routes for property address endpoints +func SetupPropertyAddressRoutes(router *gin.RouterGroup, propertyAddressHandler *handlers.PropertyAddressHandler) { + propertyAddresses := router.Group("/property-addresses", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + propertyAddresses.POST("", propertyAddressHandler.CreatePropertyAddress) + propertyAddresses.GET("", propertyAddressHandler.GetAllPropertyAddresses) + propertyAddresses.GET("/search", propertyAddressHandler.SearchPropertyAddresses) + propertyAddresses.GET("/:id", propertyAddressHandler.GetPropertyAddressByID) + propertyAddresses.PUT("/:id", propertyAddressHandler.UpdatePropertyAddress) + propertyAddresses.DELETE("/:id", propertyAddressHandler.DeletePropertyAddress) + propertyAddresses.GET("/property/:propertyId", propertyAddressHandler.GetPropertyAddressByPropertyID) + } +} + +// SetupGISRoutes registers routes for GIS data endpoints +func SetupGISRoutes(router *gin.RouterGroup, gisHandler *handlers.GISHandler) { + gisData := router.Group("/gis-data", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + gisData.GET("", gisHandler.GetAll) // GET /gis-data - Get all GIS data + gisData.POST("", gisHandler.Create) // POST /gis-data - Create GIS data + gisData.GET("/:id", gisHandler.GetByID) // GET /gis-data/{id} - Get GIS data by ID + gisData.PUT("/:id", gisHandler.Update) // PUT /gis-data/{id} - Update GIS data + gisData.DELETE("/:id", gisHandler.Delete) // DELETE /gis-data/{id} - Delete GIS data + gisData.GET("/property/:propertyId", gisHandler.GetByPropertyID) // GET /gis-data/property/{propertyId} - Get GIS data by property ID + } +} + +// SetupCoordinatesRoutes sets up the routes for coordinates endpoints +func SetupCoordinatesRoutes(router *gin.RouterGroup, coordinatesHandler *handlers.CoordinatesHandler) { + coordinates := router.Group("/coordinates", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + coordinates.GET("", coordinatesHandler.GetAll) // GET /coordinates - Get all coordinates + coordinates.POST("", coordinatesHandler.Create) // POST /coordinates - Create coordinates + coordinates.GET("/:id", coordinatesHandler.GetByID) // GET /coordinates/{id} - Get coordinates by ID + coordinates.PUT("/:id", coordinatesHandler.Update) // PUT /coordinates/{id} - Update coordinates + coordinates.DELETE("/:id", coordinatesHandler.Delete) // DELETE /coordinates/{id} - Delete coordinates + coordinates.POST("/batch", coordinatesHandler.CreateBatch) + coordinates.PUT("/gis/:gisDataId", coordinatesHandler.ReplaceByGISDataID) + } +} + +func SetupAmenitiesRoutes(router *gin.RouterGroup, amenityHandler *handlers.AmenityHandler) { + amenities := router.Group("/amenities", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + amenities.GET("", amenityHandler.GetAll) + amenities.POST("", amenityHandler.Create) + amenities.GET("/:id", amenityHandler.GetByID) + amenities.GET("/property/:propertyId", amenityHandler.GetByPropertyID) + amenities.PUT("/:id", amenityHandler.Update) + amenities.DELETE("/:id", amenityHandler.Delete) + } +} + +// SetupDocumentsRoutes registers routes for document endpoints +func SetupDocumentsRoutes(router *gin.RouterGroup, handler *handlers.DocumentHandler) { + docs := router.Group("/documents", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + docs.GET("", handler.GetAll) // GET /v1/documents + docs.POST("", handler.CreateBatch) // POST /v1/documents (accepts []Document) + docs.GET("/:id", handler.GetByID) // GET /v1/documents/{id} + docs.GET("/property/:propertyId", handler.GetByPropertyID) // GET /v1/documents/property/{propertyId} + docs.DELETE("/:id", handler.Delete) + docs.PUT("/:id", handler.Update) // PUT /v1/documents/{id} + } +} + +func SetupIGRSRoutes(router *gin.RouterGroup, igrsHandler *handlers.IGRSHandler) { + igrs := router.Group("/igrs", middleware.MDMSRoleMiddleware("POST", "/v1/applications")) + { + igrs.GET("", igrsHandler.List) + igrs.POST("", igrsHandler.Create) + + igrs.GET("/:id", igrsHandler.GetByID) + igrs.PUT("/:id", igrsHandler.Update) + igrs.DELETE("/:id", igrsHandler.Delete) + } +} diff --git a/property-tax/enumeration-backend/internal/security/security.go b/property-tax/enumeration-backend/internal/security/security.go new file mode 100644 index 0000000..74478cf --- /dev/null +++ b/property-tax/enumeration-backend/internal/security/security.go @@ -0,0 +1,225 @@ +// Package security provides JWT and role-based security utilities for the API +package security + +import ( + "context" + "crypto/rsa" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +// keycloakCerts holds the JWKS keys fetched from Keycloak for JWT validation +type keycloakCerts struct { + Keys []struct { + KeyID string `json:"kid"` + KeyType string `json:"kty"` + Algorithm string `json:"alg"` + PublicKeyUse string `json:"use"` + Modulus string `json:"n"` + Exponent string `json:"e"` + CertificateChain []string `json:"x5c"` + } `json:"keys"` +} + +var ( + jwksCache keycloakCerts // Cached JWKS keys + jwksFetchedAt time.Time // Last fetch time + jwksMutex sync.RWMutex // Mutex for thread safety + jwksTTL = 10 * time.Minute // Cache TTL + ErrUnauthorized = errors.New("unauthorized") +) + +// fetchJWKS retrieves the JSON Web Key Set (JWKS) from the Keycloak realm endpoint +func fetchJWKS(ctx context.Context) (keycloakCerts, error) { + base := os.Getenv("KEYCLOAK_BASE_URL") + realm := os.Getenv("KEYCLOAK_REALM") + if base == "" || realm == "" { + return keycloakCerts{}, fmt.Errorf("KEYCLOAK_BASE_URL or KEYCLOAK_REALM not set") + } + url := strings.TrimRight(base, "/") + "/realms/" + realm + "/protocol/openid-connect/certs" + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return keycloakCerts{}, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return keycloakCerts{}, fmt.Errorf("failed to fetch JWKS: %s", resp.Status) + } + var kc keycloakCerts + if err := json.NewDecoder(resp.Body).Decode(&kc); err != nil { + return keycloakCerts{}, err + } + return kc, nil +} + +// getJWKS retrieves the JWKS with in-memory caching to avoid repeated HTTP requests +func getJWKS(ctx context.Context) (keycloakCerts, error) { + jwksMutex.RLock() + if time.Since(jwksFetchedAt) < jwksTTL && len(jwksCache.Keys) > 0 { + defer jwksMutex.RUnlock() + return jwksCache, nil + } + jwksMutex.RUnlock() + + jwksMutex.Lock() + defer jwksMutex.Unlock() + // Double-check condition after acquiring write lock + if time.Since(jwksFetchedAt) < jwksTTL && len(jwksCache.Keys) > 0 { + return jwksCache, nil + } + kc, err := fetchJWKS(ctx) + if err != nil { + return keycloakCerts{}, err + } + jwksCache = kc + jwksFetchedAt = time.Now() + return kc, nil +} + +// parseRSAPublicKey constructs an RSA public key from base64url-encoded modulus and exponent values +func parseRSAPublicKey(nB64, eB64 string) (*rsa.PublicKey, error) { + // n and e are base64url encoded per RFC 7517 + if nB64 == "" || eB64 == "" { + return nil, errors.New("missing modulus or exponent") + } + nBytes, err := base64.RawURLEncoding.DecodeString(nB64) + if err != nil { + return nil, fmt.Errorf("failed to decode n: %w", err) + } + eBytes, err := base64.RawURLEncoding.DecodeString(eB64) + if err != nil { + return nil, fmt.Errorf("failed to decode e: %w", err) + } + // Exponent is big-endian integer; typically small (e.g., 65537) + var eInt int + switch len(eBytes) { + case 3: // common for 65537 (0x01 0x00 0x01) + eInt = int(binary.BigEndian.Uint32(append([]byte{0x00}, eBytes...))) + case 4: + eInt = int(binary.BigEndian.Uint32(eBytes)) + case 1: + eInt = int(eBytes[0]) + default: + // generic parse + eInt = 0 + for _, b := range eBytes { + eInt = eInt<<8 + int(b) + } + } + modulus := new(big.Int).SetBytes(nBytes) + return &rsa.PublicKey{N: modulus, E: eInt}, nil +} + +// GetPublicKey retrieves the RSA public key for a given key ID (kid) from the JWKS +func GetPublicKey(ctx context.Context, keyId string) (interface{}, error) { + jwks, err := getJWKS(ctx) + if err != nil { + return nil, err + } + for _, key := range jwks.Keys { + if key.KeyID != keyId { + continue + } + // Prefer certificate chain if present + if len(key.CertificateChain) > 0 { + pem := "-----BEGIN CERTIFICATE-----\n" + key.CertificateChain[0] + "\n-----END CERTIFICATE-----\n" + pubKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(extractPublicKeyPEM(pem))) + if err == nil { + return pubKey, nil + } + // fall through to n/e if cert parsing fails + } + if key.Modulus != "" && key.Exponent != "" { + rsaKey, err := parseRSAPublicKey(key.Modulus, key.Exponent) + if err == nil { + return rsaKey, nil + } + return nil, err + } + return nil, errors.New("jwks entry missing x5c and n/e") + } + return nil, fmt.Errorf("public key not found for kid %s", keyId) +} + +// extractPublicKeyPEM returns the certificate PEM as-is (for compatibility) +func extractPublicKeyPEM(certPEM string) string { return certPEM } + +// KeycloakClaims represents the JWT claims structure returned by Keycloak +type KeycloakClaims struct { + jwt.RegisteredClaims + PreferredUsername string `json:"preferred_username"` + Email string `json:"email"` + RealmAccess map[string][]string `json:"realm_access"` + ResourceAccess map[string]struct { + Roles []string `json:"roles"` + } `json:"resource_access"` + AuthorizedParty string `json:"azp"` // client id when acting on behalf of +} + +// RequireRoles returns a Gin middleware that checks if the authenticated user has any of the required roles +func RequireRoles(required ...string) gin.HandlerFunc { + reqSet := make(map[string]struct{}, len(required)) + for _, r := range required { + reqSet[r] = struct{}{} + } + return func(ctx *gin.Context) { + v, exists := ctx.Get("claims") + if !exists { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "AuthClaimsMissing"}) + return + } + claims := v.(*KeycloakClaims) + roles := collectRoles(claims) + for r := range roles { + if _, ok := reqSet[r]; ok { + ctx.Next() + return + } + } + ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden", "requiredRoles": required}) + } +} + +// collectRoles extracts all roles (realm and client roles) from the Keycloak JWT claims +func collectRoles(claims *KeycloakClaims) map[string]struct{} { + out := make(map[string]struct{}) + if claims == nil { + return out + } + // realm roles + if claims.RealmAccess != nil { + for _, r := range claims.RealmAccess["roles"] { + out[r] = struct{}{} + } + } + // client roles + for _, ra := range claims.ResourceAccess { + for _, r := range ra.Roles { + out[r] = struct{}{} + } + } + return out +} + +// GetUserID extracts the user ID (preferred username) from the authenticated user's JWT claims +func GetUserID(ctx *gin.Context) string { + v, ok := ctx.Get("claims") + if !ok { + return "" + } + cl := v.(*KeycloakClaims) + return cl.PreferredUsername +} diff --git a/property-tax/enumeration-backend/internal/services/additional_property_details_service.go b/property-tax/enumeration-backend/internal/services/additional_property_details_service.go new file mode 100644 index 0000000..0dbd914 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/additional_property_details_service.go @@ -0,0 +1,158 @@ +// Package services provides business logic and service layer implementations +// for the property tax enumeration system. +package services + +import ( + "context" + "encoding/json" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "enumeration/pkg/logger" + "fmt" + + "github.com/google/uuid" +) + +// Compile-time check to ensure additionalPropertyDetailsService implements AdditionalPropertyDetailsService interface. +var _ AdditionalPropertyDetailsService = (*additionalPropertyDetailsService)(nil) + +// additionalPropertyDetailsService implements the AdditionalPropertyDetailsService interface and provides +// methods for managing additional property details records. +type additionalPropertyDetailsService struct { + repo repositories.AdditionalPropertyDetailsRepository // Repository for additional property details data access +} + +// NewAdditionalPropertyDetailsService creates a new instance of AdditionalPropertyDetailsService with the provided repository. +func NewAdditionalPropertyDetailsService(repo repositories.AdditionalPropertyDetailsRepository) AdditionalPropertyDetailsService { + return &additionalPropertyDetailsService{ + repo: repo, + } +} + +// CreateAdditionalPropertyDetails validates and adds a new additional property details record. +func (s *additionalPropertyDetailsService) CreateAdditionalPropertyDetails(ctx context.Context, details *models.AdditionalPropertyDetails) error { + if details == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + if err := s.validateAdditionalPropertyDetails(details); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + err := s.repo.Create(ctx, details) + if err != nil { + return err + } + logger.Info("Created additional property details with ID: ", details.ID) + return nil +} + +// GetAdditionalPropertyDetailsByID fetches an additional property details record by its unique ID. +func (s *additionalPropertyDetailsService) GetAdditionalPropertyDetailsByID(ctx context.Context, id uuid.UUID) (*models.AdditionalPropertyDetails, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid additional property details ID: cannot be nil") + } + details, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, err + } + logger.Info("Fetched additional property details with ID: ", id) + return details, nil +} + +// UpdateAdditionalPropertyDetails validates and updates an existing additional property details record. +func (s *additionalPropertyDetailsService) UpdateAdditionalPropertyDetails(ctx context.Context, details *models.AdditionalPropertyDetails) error { + if details == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + if details.ID == uuid.Nil { + return fmt.Errorf("invalid additional property details ID: cannot be nil") + } + if err := s.validateAdditionalPropertyDetails(details); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + err := s.repo.Update(ctx, details) + if err != nil { + return err + } + logger.Info("Updated additional property details with ID: ", details.ID) + return nil +} + +// DeleteAdditionalPropertyDetails removes an additional property details record by its ID. +func (s *additionalPropertyDetailsService) DeleteAdditionalPropertyDetails(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("invalid additional property details ID: cannot be nil") + } + err := s.repo.Delete(ctx, id) + if err != nil { + return err + } + logger.Info("Deleted additional property details with ID: ", id) + return nil +} + +// GetAllAdditionalPropertyDetails returns a paginated list of additional property details records and the total count. +// If page or size are invalid, defaults are used. Optionally filters by property ID and field name. +func (s *additionalPropertyDetailsService) GetAllAdditionalPropertyDetails(ctx context.Context, page, size int, propertyID *uuid.UUID, fieldName *string) ([]*models.AdditionalPropertyDetails, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + + details, total, err := s.repo.GetAll(ctx, page, size, propertyID, fieldName) + if err != nil { + return nil, 0, err + } + logger.Info("Fetched all additional property details, total count: ", total) + return details, total, nil +} + +// GetAdditionalPropertyDetailsByPropertyID fetches all additional property details records for a given property ID. +func (s *additionalPropertyDetailsService) GetAdditionalPropertyDetailsByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.AdditionalPropertyDetails, error) { + if propertyID == uuid.Nil { + return nil, fmt.Errorf("invalid property ID: cannot be nil") + } + + details, err := s.repo.GetByPropertyID(ctx, propertyID) + if err != nil { + return nil, err + } + logger.Info("Fetched additional property details for property ID: ", propertyID) + return details, nil +} + +// GetAdditionalPropertyDetailsByFieldName fetches all additional property details records for a given field name. +func (s *additionalPropertyDetailsService) GetAdditionalPropertyDetailsByFieldName(ctx context.Context, fieldName string) ([]*models.AdditionalPropertyDetails, error) { + if fieldName == "" { + return nil, fmt.Errorf("field name is required: cannot be empty") + } + + details, err := s.repo.GetByFieldName(ctx, fieldName) + if err != nil { + return nil, err + } + logger.Info("Fetched additional property details for field name: ", fieldName) + return details, nil +} + +// validateAdditionalPropertyDetails checks the validity of additional property details fields. +// Returns an error if any required field is missing or invalid, or if FieldValue is not valid JSON. +func (s *additionalPropertyDetailsService) validateAdditionalPropertyDetails(details *models.AdditionalPropertyDetails) error { + if details.PropertyID == uuid.Nil { + return fmt.Errorf("property ID is required: cannot be nil") + } + if details.FieldName == "" { + return fmt.Errorf("field name is required: cannot be empty") + } + if len(details.FieldValue) == 0 { + return fmt.Errorf("field value is required: cannot be empty") + } + // Validate that FieldValue is valid JSON + var jsonData interface{} + if err := json.Unmarshal(details.FieldValue, &jsonData); err != nil { + return fmt.Errorf("field value must be valid JSON: %w", err) + } + return nil +} diff --git a/property-tax/enumeration-backend/internal/services/amenity_service.go b/property-tax/enumeration-backend/internal/services/amenity_service.go new file mode 100644 index 0000000..d259928 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/amenity_service.go @@ -0,0 +1,133 @@ +// Package services provides business logic and service layer implementations +// for the property tax enumeration system. +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "enumeration/pkg/logger" + "fmt" +) + +// Compile-time check to ensure amenityService implements AmenityService interface. +var _ AmenityService = (*amenityService)(nil) + +// amenityService implements the AmenityService interface and provides +// methods for managing amenity records. +type amenityService struct { + repo repositories.AmenityRepository // Repository for amenity data access +} + +// NewAmenityService creates a new instance of AmenityService with the provided repository. +func NewAmenityService(repo repositories.AmenityRepository) AmenityService { + return &amenityService{repo} +} + +// GetAll returns all amenities with pagination. +// Service enforces sane defaults and max page size. +func (s *amenityService) GetAll(ctx context.Context, page, size int) ([]models.Amenities, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 { + size = constants.DefaultSize + } + if size > 100 { + size = constants.MaxSize + } + // Reuse repository filter method with no filters + amenities, total, err := s.repo.GetAllWithFilters(ctx, page, size, "", "") + if err != nil { + return nil, 0, err + } + logger.Info("Fetched all amenities, total count: ", total) + return amenities, total, err +} + +// GetAllWithFilters returns amenities applying filters and pagination. +// Service enforces sane defaults and max page size to centralize pagination logic. +func (s *amenityService) GetAllWithFilters(ctx context.Context, page, size int, amenityType, propertyID string) ([]models.Amenities, int64, error) { + // Service layer validation - normalize parameters + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + amenities, total, err := s.repo.GetAllWithFilters(ctx, page, size, amenityType, propertyID) + if err != nil { + return nil, 0, err + } + logger.Info("Fetched amenities with filters, total count: ", total) + return amenities, total, err +} + +// GetByID retrieves an amenity by its unique ID. +func (s *amenityService) GetByID(ctx context.Context, id string) (*models.Amenities, error) { + if id == "" { + return nil, fmt.Errorf("invalid amenity ID: cannot be empty") + } + amenity, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, err + } + logger.Info("Fetched amenity with ID: ", id) + return amenity, nil +} + +// Create adds a new amenity record. +func (s *amenityService) Create(ctx context.Context, amenity *models.Amenities) error { + if amenity == nil { + return fmt.Errorf("amenity cannot be nil") + } + err := s.repo.Create(ctx, amenity) + if err != nil { + return err + } + logger.Info("Created amenity with ID: ", amenity.ID) + return nil +} + +// Update modifies an existing amenity record by its ID. +func (s *amenityService) Update(ctx context.Context, id string, amenity *models.Amenities) error { + if id == "" { + return fmt.Errorf("invalid amenity ID: cannot be empty") + } + if amenity == nil { + return fmt.Errorf("amenity cannot be nil") + } + err := s.repo.Update(ctx, id, amenity) + if err != nil { + return err + } + logger.Info("Updated amenity with ID: ", id) + return nil +} + +// Delete removes an amenity record by its ID. +func (s *amenityService) Delete(ctx context.Context, id string) error { + if id == "" { + return fmt.Errorf("invalid amenity ID: cannot be empty") + } + err := s.repo.Delete(ctx, id) + if err != nil { + return err + } + logger.Info("Deleted amenity with ID: ", id) + return nil +} + +// GetByPropertyID retrieves amenities for a given property ID. +func (s *amenityService) GetByPropertyID(ctx context.Context, propertyID string) (*models.Amenities, error) { + if propertyID == "" { + return nil, fmt.Errorf("invalid property ID: cannot be empty") + } + amenities, err := s.repo.GetByPropertyID(ctx, propertyID) + if err != nil { + return nil, err + } + logger.Info("Fetched amenities for property ID: ", propertyID) + return amenities, nil +} diff --git a/property-tax/enumeration-backend/internal/services/application_log_service.go b/property-tax/enumeration-backend/internal/services/application_log_service.go new file mode 100644 index 0000000..98c8281 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/application_log_service.go @@ -0,0 +1,163 @@ +package services + +import ( + "context" + "encoding/json" + "enumeration/internal/constants" + "enumeration/internal/dto" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +var _ ApplicationLogService = (*applicationLogService)(nil) + +type applicationLogService struct { + repo repositories.ApplicationLogRepository +} + +func NewApplicationLogService(repo repositories.ApplicationLogRepository) ApplicationLogService { + return &applicationLogService{ + repo: repo, + } +} + +func (s *applicationLogService) Create(ctx context.Context, req *dto.CreateApplicationLogRequest) (*models.ApplicationLog, error) { + // Convert metadata to JSON string if provided + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + var metadataJSON string + if req.Metadata != nil { + metadataBytes, err := json.Marshal(req.Metadata) + if err != nil { + return nil, fmt.Errorf("failed to marshal metadata: %w", err) + } + metadataJSON = string(metadataBytes) + } + + log := &models.ApplicationLog{ + Action: req.Action, + PerformedBy: req.PerformedBy, + PerformedDate: time.Now(), + Comments: req.Comments, + Metadata: metadataJSON, + FileStoreID: req.FileStoreID, + ApplicationID: req.ApplicationID, + Actor: req.Actor, + } + + err := s.repo.Create(ctx, log) + if err != nil { + return nil, fmt.Errorf("failed to create application log: %w", err) + } + + return log, nil +} + +func (s *applicationLogService) GetByID(ctx context.Context, id uuid.UUID) (*models.ApplicationLog, error) { + log, err := s.repo.GetByID(ctx, id) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("record not found") + } + return nil, fmt.Errorf("failed to get application log: %w", err) + } + return log, nil +} + +func (s *applicationLogService) Update(ctx context.Context, id uuid.UUID, req *dto.UpdateApplicationLogRequest) (*models.ApplicationLog, error) { + // Check if log exists + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + existingLog, err := s.repo.GetByID(ctx, id) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("record not found") + } + return nil, fmt.Errorf("failed to get application log: %w", err) + } + + // Update fields if provided + if req.Action != "" { + existingLog.Action = req.Action + } + if req.PerformedBy != "" { + existingLog.PerformedBy = req.PerformedBy + } + if req.Comments != "" { + existingLog.Comments = req.Comments + } + if req.FileStoreID != nil { + existingLog.FileStoreID = req.FileStoreID + } + if req.Metadata != nil { + metadataBytes, err := json.Marshal(req.Metadata) + if err != nil { + return nil, fmt.Errorf("failed to marshal metadata: %w", err) + } + existingLog.Metadata = string(metadataBytes) + } + + err = s.repo.Update(ctx, existingLog) + if err != nil { + return nil, fmt.Errorf("failed to update application log: %w", err) + } + + return existingLog, nil +} + +func (s *applicationLogService) Delete(ctx context.Context, id uuid.UUID) error { + // Check if log exists + _, err := s.repo.GetByID(ctx, id) + if err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("record not found") + } + return fmt.Errorf("failed to get application log: %w", err) + } + + err = s.repo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete application log: %w", err) + } + + return nil +} + +func (s *applicationLogService) List(ctx context.Context, applicationID, action string, page, size int) ([]models.ApplicationLog, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 { + size = constants.DefaultSize + } + + logs, total, err := s.repo.List(ctx, applicationID, action, page, size) + if err != nil { + return nil, 0, fmt.Errorf("failed to list application logs: %w", err) + } + + return logs, total, nil +} + +func (s *applicationLogService) GetByApplicationID(ctx context.Context, applicationID uuid.UUID, action string, page, size int) ([]models.ApplicationLog, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 { + size = constants.DefaultSize + } + + logs, total, err := s.repo.GetByApplicationID(ctx, applicationID, action, page, size) + if err != nil { + return nil, 0, fmt.Errorf("failed to get application logs by application ID: %w", err) + } + + return logs, total, nil +} \ No newline at end of file diff --git a/property-tax/enumeration-backend/internal/services/application_service.go b/property-tax/enumeration-backend/internal/services/application_service.go new file mode 100644 index 0000000..3a41cb9 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/application_service.go @@ -0,0 +1,592 @@ +package services + +import ( + "context" + "encoding/json" + workflow "enumeration/internal/clients" + "enumeration/internal/config" + "enumeration/internal/constants" + "enumeration/internal/dto" + "enumeration/internal/models" + "enumeration/internal/repositories" + "enumeration/pkg/logger" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +var _ ApplicationService = (*applicationService)(nil) + +// applicationService handles business logic for applications +type applicationService struct { + appRepo repositories.ApplicationRepository + ownerRepo repositories.PropertyOwnerRepository + workflowClient *workflow.Client + cfg *config.Config + applicationlog ApplicationLogService +} + +// NewApplicationService creates a new application service +func NewApplicationService(appRepo repositories.ApplicationRepository, ownerRepo repositories.PropertyOwnerRepository, workflowClient *workflow.Client, cfg *config.Config, applicationlog ApplicationLogService) ApplicationService { + return &applicationService{ + appRepo: appRepo, + ownerRepo: ownerRepo, + workflowClient: workflowClient, + cfg: cfg, + applicationlog: applicationlog, + } +} +// Create creates a new application with property owners +func (s *applicationService) Create(ctx context.Context, tenantID string, citizenID string, req *dto.CreateApplicationRequest) (*models.Application, error) { + + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + // Generate unique application number + applicationNo := s.generateApplicationNumber() + + process, err := s.workflowClient.GetProcess(ctx, tenantID, "PROP_ENUM") + if err != nil { + + return nil, fmt.Errorf("PROP_ENUM workflow process not found - please ensure workflow is pre-created: %w", err) + } + + noOfDays, err := workflow.FetchNoOfDays(s.cfg.MDMSURL, tenantID) + logger.Info("No of days fetched from MDMS: ", noOfDays) + + if err != nil { + return nil, fmt.Errorf("failed to fetch noOfDays from MDMS: %w", err) + } + req.DueDate = time.Now().AddDate(0, 0, noOfDays) + req.Priority = constants.PriorityLow + + // Validate AssesseeID exists in users table + if err := s.appRepo.CheckUserExists(ctx, req.AssesseeID.String()); err != nil { + return nil, err // Error already has proper context from repository + } + + // Create application entity + application := &models.Application{ + + ApplicationNo: applicationNo, + PropertyID: req.PropertyID, + Priority: req.Priority, + IsDraft: req.IsDraft, + DueDate: req.DueDate, + Status: constants.StatusInitiated, + TenantID: tenantID, + AppliedBy: req.AppliedBy, + AssesseeID: req.AssesseeID.String(), + WorkflowInstanceID: process.ID, + } + + err = s.appRepo.Create(ctx, application) + if err != nil { + return nil, fmt.Errorf(constants.ErrApplicationCreationFailed+": %w", err) + } else { + logger.Info("Application created with ID: ", application.ID) + } + + // Load the complete application with Property data + fullApplication, err := s.appRepo.GetByID(ctx, application.ID) // Add ctx parameter + if err != nil { + return nil, fmt.Errorf("failed to load created application: %w", err) + } + username := ctx.Value("user").(string) + appLog := dto.CreateApplicationLogRequest{ + ApplicationID: fullApplication.ID, + Action: "Application Created", + Actor: ctx.Value("role").(string), + PerformedBy: username, + Metadata: map[string]interface{}{}, + Comments: `Application applied by ` + fullApplication.AppliedBy, + } + s.applicationlog.Create(ctx, &appLog) + + return fullApplication, nil +} + +// GetByID retrieves application by ID +func (s *applicationService) GetByID(ctx context.Context, id uuid.UUID) (*models.Application, error) { + application, err := s.appRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf(constants.ErrApplicationNotFound+":%w", err) + } + return nil, err + } + logger.Info("Fetched application with ID: ", application.ID) + return application, nil +} + +// GetByApplicationNo retrieves application by application number +func (s *applicationService) GetByApplicationNo(ctx context.Context, applicationNo string) (*models.Application, error) { + application, err := s.appRepo.GetByApplicationNo(ctx, applicationNo) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf(constants.ErrApplicationNotFound) + } + return nil, err + } + logger.Info("Fetched application with Application No: ", application.ApplicationNo) + return application, nil +} + +// Update updates an existing application +func (s *applicationService) Update(ctx context.Context, id uuid.UUID, req *dto.UpdateApplicationRequest) (*models.Application, error) { + // Check if application exists + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + existing, err := s.appRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf(constants.ErrApplicationNotFound) + } + return nil, err + } + + // Update fields + if req.Priority != "" { + existing.Priority = req.Priority + } + if req.DueDate != nil { + existing.DueDate = *req.DueDate + } + if req.AssignedAgent != nil { + existing.AssignedAgent = req.AssignedAgent + } + if req.AppliedBy != "" { + existing.AppliedBy = req.AppliedBy + } + if req.IsDraft != nil { + existing.IsDraft = *req.IsDraft + } + if req.ImportantNote != nil { + existing.ImportantNote = *req.ImportantNote + } + + // Update application + if err := s.appRepo.Update(ctx, existing); err != nil { + return nil, fmt.Errorf("failed to update application: %w", err) + } + logger.Info("Updated application with ID: ", existing.ID) + + return existing, nil +} + +// UpdateStatus updates application status +func (s *applicationService) UpdateStatus(ctx context.Context, id uuid.UUID, req *dto.UpdateStatusRequest) error { + // Check if application exists + if req == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + existing, err := s.appRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf(constants.ErrApplicationNotFound) + } + return err + } + + // Update status + existing.Status = req.Status + + if err := s.appRepo.Update(ctx, existing); err != nil { + return fmt.Errorf("failed to update status: %w", err) + } + + return nil +} + +// AssignAgent assigns an agent to application +func (s *applicationService) AssignAgent(ctx context.Context, id uuid.UUID, req *dto.ActionRequest, tenantID string) error { + // Check if application exists + if req == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + existing, err := s.appRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf(constants.ErrApplicationNotFound) + } + return err + } + logger.Info("Fetched application with ID: ", existing.ID) + + err = s.appRepo.CheckUserExists(ctx, req.AgentID.String()) + if err != nil { + return err + } + // Check if workflow process exists + if existing.WorkflowInstanceID == "" { + return fmt.Errorf("workflow process not initialized for application %s", id) + } + // Assign agent + if req.AgentID == uuid.Nil { + return fmt.Errorf("agent ID is required for assignment") + } + existing.AssignedAgent = &req.AgentID + + existing.Status = constants.StatusAssigned + if err := s.appRepo.Update(ctx, existing); err != nil { + return fmt.Errorf("failed to assign agent: %w", err) + } + logger.Info("Assigned agent with ID: ", req.AgentID, " to application ID: ", existing.ID) + // Skip workflow transition creation for re-assign action + if req.Action == "re-assign" { + username, _ := workflow.GetUsername(req.AgentID.String()) + appLog := dto.CreateApplicationLogRequest{ + ApplicationID: existing.ID, + Action: "Application Re-assigned to Agent", + Actor: ctx.Value("role").(string), + PerformedBy: ctx.Value("user").(string), + Metadata: map[string]interface{}{}, + Comments: "application Re-assigned to agent : " + username, + } + s.applicationlog.Create(ctx, &appLog) + return nil + } + // Create workflow transition + transitionReq := &workflow.TransitionRequest{ + ProcessID: existing.WorkflowInstanceID, + EntityID: existing.ApplicationNo, + Action: "Assign to Agent", + Attributes: map[string]interface{}{ + "roles": []string{constants.RoleServiceManager}, + "jurisdiction": []string{"Punjab.Amritsar"}, + "assignedAgent": []string{req.AgentID.String()}, + "comments": []string{req.Comments}, + }, + } + _, err = s.workflowClient.CreateTransition(ctx, "pb.amritsar", transitionReq) + if err != nil { + return fmt.Errorf("failed to create workflow transition: %w", err) + } + username, _ := workflow.GetUsername(req.AgentID.String()) + appLog := dto.CreateApplicationLogRequest{ + ApplicationID: existing.ID, + Action: "Application Assigned to Agent", + PerformedBy: ctx.Value("user").(string), + Actor: ctx.Value("role").(string), + Metadata: map[string]interface{}{}, + Comments: "application assigned to agent : " + username, + } + s.applicationlog.Create(ctx, &appLog) + + return nil +} + +// Delete deletes application by ID +func (s *applicationService) Delete(ctx context.Context, id uuid.UUID) error { + // Check if application exists + _, err := s.appRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf(constants.ErrApplicationNotFound) + } + return err + } + logger.Info("Deleting application with ID: ", id) + return s.appRepo.Delete(ctx, id) +} + +// List retrieves all applications with pagination +func (s *applicationService) List(ctx context.Context, UserID string, tenantID string, Role string, verify string, page, size int) ([]models.Application, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 { + size = constants.DefaultSize + } + + var applications []models.Application + var total int64 + var err error + + switch Role { + case constants.RoleServiceManager: + + applications, total, err = s.appRepo.GetByTenantIDAndStatus(ctx, tenantID, verify, page, size) + + case constants.RoleAgent: + if verify == "" { + logger.Error("Agent List: verify parameter is missing") + return []models.Application{}, 0, errors.New("verify parameter is missing") + } + applications, total, err = s.appRepo.GetByAssignedAgent(ctx, UserID, verify, page, size) + + case constants.RoleCommissioner: + + applications, total, err = s.appRepo.GetByTenantIDAndStatus(ctx, tenantID, verify, page, size) + default: + logger.Error("Invalid role provided") + return []models.Application{}, 0, errors.New("invalid role provided") + + } + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch applications: %w", err) + } + // currentTime := time.Now() + // for i := range applications { + // createdTime := applications[i].CreatedAt + // dueDate := applications[i].DueDate + + // // Skip priority calc when dates are missing or invalid to avoid divide-by-zero + // if createdTime.IsZero() || dueDate.IsZero() { + // // leave priority as-is (or set default) and continue + // continue + // } + + // // Calculate progress + + // progress := s.CalculateProgress(ctx, currentTime, createdTime, dueDate) + // logger.Info("Calculated progress for application ID: ", applications[i].ID, " Progress: ", progress) + + // // Assign priority based on progress + // switch { + // case progress < 0.5: + // applications[i].Priority = constants.PriorityLow + // if err := s.appRepo.Update(ctx, &applications[i]); err != nil { + // logger.Error("failed to update application priority for %s: %v", applications[i].ID, err) + // } + // case progress >= 0.5 && progress < 0.75: + // applications[i].Priority = constants.PriorityMedium + // if err := s.appRepo.Update(ctx, &applications[i]); err != nil { + // logger.Error("failed to update application priority for %s: %v", applications[i].ID, err) + // } + // default: + // applications[i].Priority = constants.PriorityHigh + // if err := s.appRepo.Update(ctx, &applications[i]); err != nil { + // logger.Error("failed to update application priority for %s: %v", applications[i].ID, err) + // } + // } + // } + + return applications, total, err +} + +func (s *applicationService) CalculateProgress(ctx context.Context, currentTime time.Time, createdTime time.Time, dueDate time.Time) float64 { + + totalDuration := dueDate.Sub(createdTime).Hours() + elapsedDuration := currentTime.Sub(createdTime).Hours() + if totalDuration <= 0 { + return 1.0 // If due date is before or same as created date, consider progress as complete + } + progress := elapsedDuration / totalDuration + return progress + +} +func (s *applicationService) ApproveApplication(ctx context.Context, tenantID, commissionerID, applicationID string, req *dto.ActionRequest) error { + + if req == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + appID, err := uuid.Parse(applicationID) + if err != nil { + return fmt.Errorf("invalid application ID format: %w", err) + } + app, err := s.appRepo.GetByID(ctx, appID) + if err != nil { + return fmt.Errorf("application not found: %w", err) + } + + transitionReq := &workflow.TransitionRequest{ + ProcessID: app.WorkflowInstanceID, + EntityID: app.ApplicationNo, + Action: "Approve Application", + Attributes: map[string]interface{}{ + "roles": []string{constants.RoleCommissioner}, + "jurisdiction": []string{"Punjab.Amritsar"}, + "approvalComments": []string{req.Comments}, + }, + } + + _, err = s.workflowClient.CreateTransition(ctx, "pb.amritsar", transitionReq) + + if err != nil { + return fmt.Errorf("failed to create workflow transition: %w", err) + } + + newState := constants.StatusApproved + if !req.Approved { + newState = constants.StatusRejected + } + + app.Status = newState + + if err := s.appRepo.Update(ctx, app); err != nil { + return fmt.Errorf("failed to update application: %w", err) + } + logger.Info("Updated application with ID: ", app.ID, " to status: ", newState) + username, _ := workflow.GetUsername(commissionerID) + appLog := dto.CreateApplicationLogRequest{ + ApplicationID: app.ID, + Action: "Application Approved", + PerformedBy: ctx.Value("user").(string), + Actor: ctx.Value("role").(string), + Metadata: map[string]interface{}{}, + Comments: "application " + newState + " by commissioner : " + username, + } + s.applicationlog.Create(ctx, &appLog) + return nil +} + +func (s *applicationService) VerifyApplication(ctx context.Context, tenantID, serviceManagerID, applicationID string, req *dto.ActionRequest) error { + if req == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + appID, err := uuid.Parse(applicationID) + if err != nil { + return fmt.Errorf("invalid application ID format: %w", err) + } + app, err := s.appRepo.GetByID(ctx, appID) + if err != nil { + return fmt.Errorf("application not found: %w", err) + } + + // Check if workflow process exists + if app.WorkflowInstanceID == "" { + return fmt.Errorf("workflow process not initialized for application %s", applicationID) + } + + newState := constants.StatusAuditVerified + + transitionReq := &workflow.TransitionRequest{ + ProcessID: app.WorkflowInstanceID, + EntityID: app.ApplicationNo, + Action: "Audit Verification", + Attributes: map[string]interface{}{ + "roles": []string{constants.RoleServiceManager}, + "jurisdiction": []string{"Punjab.Amritsar"}, + "auditComments": []string{req.Comments}, + }, + } + + _, err = s.workflowClient.CreateTransition(ctx, "pb.amritsar", transitionReq) + if err != nil { + return fmt.Errorf("failed to create workflow transition: %w", err) + } + app.Status = newState + + if err := s.appRepo.Update(ctx, app); err != nil { + return fmt.Errorf("failed to update application: %w", err) + } + logger.Info("Updated application with ID: ", app.ID, " to status: ", newState) + username, _ := workflow.GetUsername(serviceManagerID) + appLog := dto.CreateApplicationLogRequest{ + ApplicationID: app.ID, + Action: "Application Audit Verified", + PerformedBy: ctx.Value("user").(string), + Metadata: map[string]interface{}{}, + Actor: ctx.Value("role").(string), + Comments: "application " + newState + " by service manager : " + username, + } + s.applicationlog.Create(ctx, &appLog) + return nil +} + +func (s *applicationService) VerifyApplicationByAgent(ctx context.Context, tenantID, agentID, applicationID string, req *dto.ActionRequest) error { + if req == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + appID, err := uuid.Parse(applicationID) + if err != nil { + return fmt.Errorf("invalid application ID format: %w", err) + } + + app, err := s.appRepo.GetByID(ctx, appID) + if err != nil { + return fmt.Errorf("application not found: %w", err) + } + + transitionReq := &workflow.TransitionRequest{ + ProcessID: app.WorkflowInstanceID, + EntityID: app.ApplicationNo, + Action: "Submit for Verification", + Attributes: map[string]interface{}{ + "roles": []string{constants.RoleAgent}, + "jurisdiction": []string{"Punjab.Amritsar"}, + "verificationReport": []string{req.Comments}, + }, + } + + _, err = s.workflowClient.CreateTransition(ctx, "pb.amritsar", transitionReq) + if err != nil { + return fmt.Errorf("failed to create workflow transition: %w", err) + } + + app.Status = constants.StatusVerified + + if err := s.appRepo.Update(ctx, app); err != nil { + return fmt.Errorf("failed to update application: %w", err) + } + logger.Info("Updated application with ID: ", app.ID, " to status: ", constants.StatusVerified) + username, _ := workflow.GetUsername(agentID) + appLog := dto.CreateApplicationLogRequest{ + ApplicationID: app.ID, + Action: "Application Verified", + PerformedBy: ctx.Value("user").(string), + Metadata: map[string]interface{}{}, + Actor: ctx.Value("role").(string), + Comments: "application " + constants.StatusVerified + " by agent : " + username, + } + s.applicationlog.Create(ctx, &appLog) + return nil +} + +// Search retrieves applications based on search criteria +func (s *applicationService) Search(ctx context.Context, criteria *dto.ApplicationSearchCriteria, page, size int) ([]*models.Application, int64, error) { + if criteria == nil { + // If your repo accepts nil criteria, you can allow it; otherwise return validation + return nil, 0, fmt.Errorf("%w: search criteria is nil", ErrValidation) + } + + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 { + size = constants.DefaultSize + } + logger.Info("Searching applications with criteria: ", criteria) + return s.appRepo.Search(ctx, criteria, page, size) +} + +// generateApplicationNo generates a unique application number +func (s *applicationService) generateApplicationNumber() string { + // Generate application number by calling external ID generation service + reqBody := `{ + "templateId": "applId", + "variables": { + "ORG": "APPL" + } + }` + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Post(s.cfg.IDGenURL, "application/json", strings.NewReader(reqBody)) + if err != nil { + // fallback to local generation if service fails + return fmt.Sprintf("PROP-%d-%s", time.Now().Unix(), uuid.New().String()[:8]) + } + defer resp.Body.Close() + + var result struct { + ID string `json:"id"` + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Sprintf("PROP-%d-%s", time.Now().Unix(), uuid.New().String()[:8]) + } + if err := json.Unmarshal(body, &result); err != nil || result.ID == "" { + return fmt.Sprintf("PROP-%d-%s", time.Now().Unix(), uuid.New().String()[:8]) + } + return result.ID +} diff --git a/property-tax/enumeration-backend/internal/services/assessment_details_service.go b/property-tax/enumeration-backend/internal/services/assessment_details_service.go new file mode 100644 index 0000000..ac83334 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/assessment_details_service.go @@ -0,0 +1,114 @@ +// Package services provides business logic and service layer implementations +// for the property tax enumeration system. +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + + "github.com/google/uuid" +) + +// Compile-time check to ensure assessmentDetailsService implements AssessmentDetailsService interface. +var _ AssessmentDetailsService = (*assessmentDetailsService)(nil) + +// assessmentDetailsService implements the AssessmentDetailsService interface and provides +// methods for managing assessment details records. +type assessmentDetailsService struct { + assessmentDetailsRepo repositories.AssessmentDetailsRepository // Repository for assessment details data access +} + +// NewAssessmentDetailsService creates a new instance of AssessmentDetailsService with the provided repository. +func NewAssessmentDetailsService(assessmentDetailsRepo repositories.AssessmentDetailsRepository) AssessmentDetailsService { + return &assessmentDetailsService{ + assessmentDetailsRepo: assessmentDetailsRepo, + } +} + +// CreateAssessmentDetails validates and creates a new assessment details record. +func (s *assessmentDetailsService) CreateAssessmentDetails(ctx context.Context, assessmentDetails *models.AssessmentDetails) error { + if assessmentDetails == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if err := s.validateAssessmentDetails(assessmentDetails); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + return s.assessmentDetailsRepo.Create(ctx, assessmentDetails) +} + +// GetAssessmentDetailsByID retrieves an assessment details record by its unique ID. +func (s *assessmentDetailsService) GetAssessmentDetailsByID(ctx context.Context, id uuid.UUID) (*models.AssessmentDetails, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid assessment details ID: cannot be nil") + } + return s.assessmentDetailsRepo.GetByID(ctx, id) +} + +// UpdateAssessmentDetails validates and updates an existing assessment details record. +func (s *assessmentDetailsService) UpdateAssessmentDetails(ctx context.Context, assessmentDetails *models.AssessmentDetails) error { + if assessmentDetails == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if assessmentDetails.ID == uuid.Nil { + return fmt.Errorf("invalid assessment details ID: cannot be nil") + } + + if err := s.validateAssessmentDetails(assessmentDetails); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + return s.assessmentDetailsRepo.Update(ctx, assessmentDetails) +} + +// DeleteAssessmentDetails removes an assessment details record by its ID. +func (s *assessmentDetailsService) DeleteAssessmentDetails(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("invalid assessment details ID: cannot be nil") + } + return s.assessmentDetailsRepo.Delete(ctx, id) +} + +// GetAllAssessmentDetails returns a paginated list of assessment details records and the total count. +// If page or size are invalid, defaults are used. Optionally filters by property ID. +func (s *assessmentDetailsService) GetAllAssessmentDetails(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.AssessmentDetails, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + return s.assessmentDetailsRepo.GetAll(ctx, page, size, propertyID) +} + +// GetAssessmentDetailsByPropertyID retrieves an assessment details record by property ID. +func (s *assessmentDetailsService) GetAssessmentDetailsByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.AssessmentDetails, error) { + if propertyID == uuid.Nil { + return nil, fmt.Errorf("invalid property ID: cannot be nil") + } + return s.assessmentDetailsRepo.GetByPropertyID(ctx, propertyID) +} + +// validateAssessmentDetails checks the validity of assessment details fields. +// Returns an error if any required field is missing or invalid. +func (s *assessmentDetailsService) validateAssessmentDetails(assessmentDetails *models.AssessmentDetails) error { + if assessmentDetails.PropertyID == uuid.Nil { + return fmt.Errorf("property ID is required: cannot be nil") + } + + // Validate required fields according to OpenAPI spec + if assessmentDetails.ExtentOfSite == "" { + return fmt.Errorf("extent of site is required: cannot be empty") + } + + // Add more validation as needed based on your business rules + if assessmentDetails.OccupancyCertificateNumber != "" && assessmentDetails.OccupancyCertificateDate == nil { + return fmt.Errorf("occupancy certificate date is required when certificate number is provided") + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/services/construction_details_service.go b/property-tax/enumeration-backend/internal/services/construction_details_service.go new file mode 100644 index 0000000..642dcf7 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/construction_details_service.go @@ -0,0 +1,137 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + + "github.com/google/uuid" +) + +// Ensure constructionDetailsService implements ConstructionDetailsService interface at compile time. +var _ ConstructionDetailsService = (*constructionDetailsService)(nil) + +// constructionDetailsService provides business logic for managing ConstructionDetails entities using a repository. +type constructionDetailsService struct { + constructionDetailsRepo repositories.ConstructionDetailsRepository +} + +// NewConstructionDetailsService creates a new instance of constructionDetailsService. +// constructionDetailsRepo: repository for ConstructionDetails data access. +// Returns: ConstructionDetailsService implementation. +func NewConstructionDetailsService(constructionDetailsRepo repositories.ConstructionDetailsRepository) ConstructionDetailsService { + return &constructionDetailsService{ + constructionDetailsRepo: constructionDetailsRepo, + } +} + +// CreateConstructionDetails validates and creates a new ConstructionDetails record. +// ctx: context for the operation. +// constructionDetails: pointer to ConstructionDetails model to be created. +// Returns: error if validation or creation fails. +func (s *constructionDetailsService) CreateConstructionDetails(ctx context.Context, constructionDetails *models.ConstructionDetails) error { + if constructionDetails == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if err := s.validateConstructionDetails(constructionDetails); err != nil { + return err // Service layer validation error - return as is + } + return s.constructionDetailsRepo.Create(ctx, constructionDetails) +} + +// GetConstructionDetailsByID retrieves a ConstructionDetails record by its unique ID. +// ctx: context for the operation. +// id: UUID of the construction details. +// Returns: pointer to ConstructionDetails and error if not found or on failure. +func (s *constructionDetailsService) GetConstructionDetailsByID(ctx context.Context, id uuid.UUID) (*models.ConstructionDetails, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid construction details ID: cannot be nil") + } + // Repository layer error - propagate as is + return s.constructionDetailsRepo.GetByID(ctx, id) +} + +// UpdateConstructionDetails validates and updates an existing ConstructionDetails record. +// ctx: context for the operation. +// constructionDetails: pointer to ConstructionDetails model with updated data. +// Returns: error if validation or update fails. +func (s *constructionDetailsService) UpdateConstructionDetails(ctx context.Context, constructionDetails *models.ConstructionDetails) error { + if constructionDetails == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if constructionDetails.ID == uuid.Nil { + return fmt.Errorf("invalid construction details ID: cannot be nil") + } + + if err := s.validateConstructionDetails(constructionDetails); err != nil { + return err // Service layer validation error - return as is + } + + // Repository layer error - propagate as is + return s.constructionDetailsRepo.Update(ctx, constructionDetails) +} + +// DeleteConstructionDetails removes a ConstructionDetails record by its unique ID. +// ctx: context for the operation. +// id: UUID of the construction details to delete. +// Returns: error if deletion fails or record not found. +func (s *constructionDetailsService) DeleteConstructionDetails(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("invalid construction details ID: cannot be nil") + } + // Repository layer error - propagate as is + return s.constructionDetailsRepo.Delete(ctx, id) +} + +// GetAllConstructionDetails retrieves all ConstructionDetails records, optionally filtered by propertyID, with pagination. +// ctx: context for the operation. +// page: page number (zero-based), size: number of records per page, propertyID: optional filter. +// Returns: slice of ConstructionDetails pointers, total count, and error if any. +func (s *constructionDetailsService) GetAllConstructionDetails(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.ConstructionDetails, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + return s.constructionDetailsRepo.GetAll(ctx, page, size, propertyID) +} + +// GetConstructionDetailsByPropertyID retrieves ConstructionDetails records by their associated property ID. +// ctx: context for the operation. +// propertyID: UUID of the property. +// Returns: slice of ConstructionDetails pointers and error if not found or on failure. +func (s *constructionDetailsService) GetConstructionDetailsByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.ConstructionDetails, error) { + if propertyID == uuid.Nil { + return nil, fmt.Errorf("invalid property ID: cannot be nil") + } + return s.constructionDetailsRepo.GetByPropertyID(ctx, propertyID) +} + +// validateConstructionDetails checks required fields for ConstructionDetails and returns an error if validation fails. +// constructionDetails: pointer to ConstructionDetails model to validate. +// Returns: error if validation fails, nil otherwise. +func (s *constructionDetailsService) validateConstructionDetails(constructionDetails *models.ConstructionDetails) error { + if constructionDetails.PropertyID == uuid.Nil { + return fmt.Errorf("property ID is required") + } + + // Optional validation - you can customize based on your business rules + if constructionDetails.FloorType == "" { + return fmt.Errorf("floor type is required") + } + + if constructionDetails.WallType == "" { + return fmt.Errorf("wall type is required") + } + + if constructionDetails.RoofType == "" { + return fmt.Errorf("roof type is required") + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/services/coordinates_service.go b/property-tax/enumeration-backend/internal/services/coordinates_service.go new file mode 100644 index 0000000..08c28e7 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/coordinates_service.go @@ -0,0 +1,201 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "enumeration/internal/validators" + "fmt" + + "github.com/google/uuid" +) + +// Compile-time check for interface implementation +var _ CoordinatesService = (*coordinatesService)(nil) + +// coordinatesService handles all business logic for coordinates +type coordinatesService struct { + repo repositories.CoordinatesRepository + validator *validators.CoordinatesValidator +} + +// NewCoordinatesService returns a new coordinatesService +func NewCoordinatesService(repo repositories.CoordinatesRepository) CoordinatesService { + return &coordinatesService{ + repo: repo, + validator: validators.NewCoordinatesValidator(), + } +} + +// FindAll gets coordinates with pagination and optional GISDataID filter +func (s *coordinatesService) FindAll(ctx context.Context, page, size int, gisDataID *uuid.UUID) ([]models.Coordinates, int64, error) { + // Nil context check + return s.repo.FindAll(ctx, page, size, gisDataID) +} + +// Create adds a new coordinates record after validation +func (s *coordinatesService) Create(ctx context.Context, coordinates *models.Coordinates) error { + // Nil checks + if coordinates == nil { + return validators.NewValidationError("coordinates", "coordinates object cannot be nil", nil) + } + + // Validate request + if err := s.validator.ValidateRequest(coordinates); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + // Create in repository + if err := s.repo.Create(ctx, coordinates); err != nil { + return fmt.Errorf("failed to create coordinates for GISDataID %s: %w", coordinates.GISDataID, err) + } + + return nil +} + +// GetByID fetches a coordinates record by its ID +func (s *coordinatesService) GetByID(ctx context.Context, id uuid.UUID) (*models.Coordinates, error) { + if id == uuid.Nil { + return nil, validators.NewValidationError("id", "id cannot be nil", id) + } + + // Fetch from repository + coordinates, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get coordinates with ID %s: %w", id, err) + } + + return coordinates, nil +} + +// Update modifies an existing coordinates record after validation +func (s *coordinatesService) Update(ctx context.Context, coordinates *models.Coordinates) error { + // Nil checks + if coordinates == nil { + return validators.NewValidationError("coordinates", "coordinates object cannot be nil", nil) + } + + // Validate update request + if err := s.validator.ValidateUpdateRequest(coordinates); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + // Update in repository + if err := s.repo.Update(ctx, coordinates); err != nil { + return fmt.Errorf("failed to update coordinates with ID %s: %w", coordinates.ID, err) + } + + return nil +} + +// Delete removes a coordinates record by its ID +func (s *coordinatesService) Delete(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return validators.NewValidationError("id", "id cannot be nil", id) + } + + // Delete from repository + if err := s.repo.Delete(ctx, id); err != nil { + return fmt.Errorf("failed to delete coordinates with ID %s: %w", id, err) + } + + return nil +} + +// GetAll returns all coordinates with pagination, as pointers +func (s *coordinatesService) GetAll(ctx context.Context, page, size int, gisDataID *uuid.UUID) ([]*models.Coordinates, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 { + size = constants.DefaultSize + } + + // Fetch from repository + coordinates, total, err := s.repo.FindAll(ctx, page, size, gisDataID) + if err != nil { + gisIDStr := "all" + if gisDataID != nil { + gisIDStr = gisDataID.String() + } + return nil, 0, fmt.Errorf("failed to fetch coordinates for GISDataID %s (page: %d, size: %d): %w", gisIDStr, page, size, err) + } + + // Convert []models.Coordinates to []*models.Coordinates + result := make([]*models.Coordinates, len(coordinates)) + for i := range coordinates { + result[i] = &coordinates[i] + } + + return result, total, nil +} + +// CreateBatch adds multiple coordinates records after validation +func (s *coordinatesService) CreateBatch(ctx context.Context, coords []*models.Coordinates) error { + // Nil checks + if coords == nil { + return validators.NewValidationError("coordinates", "coordinates array cannot be nil", nil) + } + if len(coords) == 0 { + return validators.NewValidationError("coordinates", "coordinates array cannot be empty", len(coords)) + } + + // Validate batch + if err := s.validator.ValidateBatch(coords); err != nil { + return fmt.Errorf("batch validation failed: %w", err) + } + + // Create batch in repository + if err := s.repo.CreateBatch(ctx, coords); err != nil { + return fmt.Errorf("failed to create batch of %d coordinates: %w", len(coords), err) + } + + return nil +} + +// ReplaceByGISDataID replaces all coordinates for a GISDataID with the provided batch +func (s *coordinatesService) ReplaceByGISDataID(ctx context.Context, gisDataID uuid.UUID, coords []*models.Coordinates) error { + if gisDataID == uuid.Nil { + return validators.NewValidationError("gisDataId", "gisDataId cannot be nil", gisDataID) + } + + // Allow empty slice (delete all coordinates for gisDataID) + if len(coords) == 0 { + if err := s.repo.ReplaceByGISDataID(ctx, gisDataID, coords); err != nil { + return fmt.Errorf("failed to clear coordinates for GISDataID %s: %w", gisDataID, err) + } + return nil + } + + // Normalize and validate each coordinate + for i, c := range coords { + if c == nil { + return validators.NewValidationError( + fmt.Sprintf("coordinates[%d]", i), + "coordinate cannot be nil", + nil, + ) + } + + // Ensure the coordinate points to the requested GISDataID + c.GISDataID = gisDataID + + // Assign ID if missing + if c.ID == uuid.Nil { + c.ID = uuid.New() + } + } + + // Validate batch + if err := s.validator.ValidateBatch(coords); err != nil { + return fmt.Errorf("batch validation failed for GISDataID %s: %w", gisDataID, err) + } + + // Replace in repository (transactional) + if err := s.repo.ReplaceByGISDataID(ctx, gisDataID, coords); err != nil { + return fmt.Errorf("failed to replace %d coordinates for GISDataID %s: %w", len(coords), gisDataID, err) + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/services/document_service.go b/property-tax/enumeration-backend/internal/services/document_service.go new file mode 100644 index 0000000..4557a94 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/document_service.go @@ -0,0 +1,186 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "errors" + "fmt" + "time" + + "github.com/google/uuid" +) + +// Compile-time check for interface implementation +var _ DocumentService = (*documentService)(nil) +var ErrValidation = errors.New("validation error") + +// documentService handles business logic for documents +type documentService struct { + repo repositories.DocumentRepository +} + +// NewDocumentService returns a new documentService +func NewDocumentService(repo repositories.DocumentRepository) DocumentService { + return &documentService{repo: repo} +} + +// CreateDocument validates and adds a new document +func (s *documentService) CreateDocument(ctx context.Context, doc *models.Document) error { + // basic validation + if doc == nil { + return fmt.Errorf("%w: document is nil", ErrValidation) + } + + if doc.PropertyID == uuid.Nil { + return fmt.Errorf("%w: property ID is required", ErrValidation) + } + if doc.DocumentType == "" { + return fmt.Errorf("%w: document type is required", ErrValidation) + } + if doc.DocumentName == "" { + return fmt.Errorf("%w: document name is required", ErrValidation) + } + if doc.Action == "" { + return fmt.Errorf("%w: action is required", ErrValidation) + } + + // set upload date if not provided + if doc.UploadDate.IsZero() { + doc.UploadDate = time.Now().UTC() + } + + // populate UploadedBy from context if available + if doc.UploadedBy == "" { + var username, role string + + // Extract username using the same context key type + if v := ctx.Value("user"); v != nil { + username = fmt.Sprint(v) + } + + // Extract role using the same context key type + if v := ctx.Value("role"); v != nil { + role = fmt.Sprint(v) + } + + // Format as "username role:rolename" + if username != "" && role != "" { + doc.UploadedBy = fmt.Sprintf("%s role:%s", username, role) + } else if username != "" { + doc.UploadedBy = username + } + } + + // ensure ID is not nil (DB default will set it, but keep consistency) + if doc.ID == uuid.Nil { + doc.ID = uuid.New() + } + + return s.repo.Create(ctx, doc) +} + +// CreateDocuments validates and adds multiple documents in a batch +func (s *documentService) CreateDocuments(ctx context.Context, docs []*models.Document) error { + if docs == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + for i, d := range docs { + if d == nil { + return fmt.Errorf("%w: document at index %d is nil", ErrValidation, i) + } + } + + // try to extract username and role once from context to reuse + var uploadedByFromCtx string + var username, role string + + // Extract username using the same context key type + if v := ctx.Value("user"); v != nil { + username = fmt.Sprint(v) + } + + // Extract role using the same context key type + if v := ctx.Value("role"); v != nil { + role = fmt.Sprint(v) + } + + // Format as "username role:rolename" + if username != "" && role != "" { + uploadedByFromCtx = fmt.Sprintf("%s role:%s", username, role) + } else if username != "" { + uploadedByFromCtx = username + } + + for _, d := range docs { + if d.PropertyID == uuid.Nil { + return fmt.Errorf("%w: property ID is required for all documents", ErrValidation) + } + if d.DocumentType == "" { + return fmt.Errorf("%w: document type is required for all documents", ErrValidation) + } + if d.DocumentName == "" { + return fmt.Errorf("%w: document name is required for all documents", ErrValidation) + } + if d.UploadDate.IsZero() { + d.UploadDate = time.Now().UTC() + } + if d.ID == uuid.Nil { + d.ID = uuid.New() + } + + // populate UploadedBy if not already set on the document + if d.UploadedBy == "" && uploadedByFromCtx != "" { + d.UploadedBy = uploadedByFromCtx + } + } + return s.repo.CreateBatch(ctx, docs) +} + +// GetDocumentByID fetches a document by its ID +func (s *documentService) GetDocumentByID(ctx context.Context, id uuid.UUID) (*models.Document, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("%w: invalid document ID", ErrValidation) + } + return s.repo.GetByID(ctx, id) +} + +// GetDocumentsByPropertyID fetches all documents for a property +func (s *documentService) GetDocumentsByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.Document, error) { + if propertyID == uuid.Nil { + return nil, fmt.Errorf("%w: invalid property ID", ErrValidation) + } + return s.repo.GetByPropertyID(ctx, propertyID) +} + +// GetAllDocuments returns all documents with pagination, optionally filtered by propertyID +func (s *documentService) GetAllDocuments(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.Document, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + return s.repo.GetAll(ctx, page, size, propertyID) +} + +// DeleteDocument removes a document by its ID +func (s *documentService) DeleteDocument(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("%w: invalid document ID", ErrValidation) + } + return s.repo.Delete(ctx, id) +} + +// UpdateDocument updates an existing document after validation +func (s *documentService) UpdateDocument(ctx context.Context, doc *models.Document) error { + if doc == nil { + return fmt.Errorf("%w: document is nil", ErrValidation) + } + if doc.ID == uuid.Nil { + return fmt.Errorf("%w: document ID is required", ErrValidation) + } + // Optionally, add more validation as needed + return s.repo.Update(ctx, doc) +} diff --git a/property-tax/enumeration-backend/internal/services/floor_details_service.go b/property-tax/enumeration-backend/internal/services/floor_details_service.go new file mode 100644 index 0000000..d42e797 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/floor_details_service.go @@ -0,0 +1,114 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + + "github.com/google/uuid" +) + +// Compile-time check for interface implementation +var _ FloorDetailsService = (*floorDetailsService)(nil) + +// floorDetailsService handles business logic for floor details +type floorDetailsService struct { + floorDetailsRepo repositories.FloorDetailsRepository +} + +// NewFloorDetailsService returns a new floorDetailsService +func NewFloorDetailsService(floorDetailsRepo repositories.FloorDetailsRepository) FloorDetailsService { + return &floorDetailsService{ + floorDetailsRepo: floorDetailsRepo, + } +} + +// CreateFloorDetails validates and adds a new floor details record +func (s *floorDetailsService) CreateFloorDetails(ctx context.Context, floorDetails *models.FloorDetails) error { + if floorDetails == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if err := s.validateFloorDetails(floorDetails); err != nil { + return err + } + + return s.floorDetailsRepo.Create(ctx, floorDetails) +} + +// GetFloorDetailsByID fetches a floor details record by its ID +func (s *floorDetailsService) GetFloorDetailsByID(ctx context.Context, id uuid.UUID) (*models.FloorDetails, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid floor details ID: cannot be nil") + } + + // Repository layer error - propagate as is + return s.floorDetailsRepo.GetByID(ctx, id) +} + +// UpdateFloorDetails validates and updates an existing floor details record +func (s *floorDetailsService) UpdateFloorDetails(ctx context.Context, floorDetails *models.FloorDetails) error { + if floorDetails == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if floorDetails.ID == uuid.Nil { + return fmt.Errorf("invalid floor details ID: cannot be nil") + } + if err := s.validateFloorDetails(floorDetails); err != nil { + return err + } + return s.floorDetailsRepo.Update(ctx, floorDetails) +} + +// DeleteFloorDetails removes a floor details record by its ID +func (s *floorDetailsService) DeleteFloorDetails(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("invalid floor details ID: cannot be nil") + } + return s.floorDetailsRepo.Delete(ctx, id) +} + +// GetAllFloorDetails returns all floor details with pagination, optionally filtered by constructionDetailsID +func (s *floorDetailsService) GetAllFloorDetails(ctx context.Context, page, size int, constructionDetailsID *uuid.UUID) ([]*models.FloorDetails, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + + return s.floorDetailsRepo.GetAll(ctx, page, size, constructionDetailsID) +} + +// GetFloorDetailsByConstructionDetailsID fetches all floor details for a construction details ID +func (s *floorDetailsService) GetFloorDetailsByConstructionDetailsID(ctx context.Context, constructionDetailsID uuid.UUID) ([]*models.FloorDetails, error) { + if constructionDetailsID == uuid.Nil { + return nil, fmt.Errorf("invalid construction details ID: cannot be nil") + } + + return s.floorDetailsRepo.GetByConstructionDetailsID(ctx, constructionDetailsID) +} + +// validateFloorDetails checks required fields for floor details and returns an error if validation fails +func (s *floorDetailsService) validateFloorDetails(floorDetails *models.FloorDetails) error { + if floorDetails.ConstructionDetailsID == uuid.Nil { + return fmt.Errorf("construction details ID is required") + } + + if floorDetails.LengthFt <= 0 { + return fmt.Errorf("length in feet must be greater than 0") + } + + if floorDetails.BreadthFt <= 0 { + return fmt.Errorf("breadth in feet must be greater than 0") + } + + if floorDetails.PlinthAreaSqFt <= 0 { + return fmt.Errorf("plinth area must be greater than 0") + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/services/gis_service.go b/property-tax/enumeration-backend/internal/services/gis_service.go new file mode 100644 index 0000000..dc9fb94 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/gis_service.go @@ -0,0 +1,138 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + + "github.com/google/uuid" +) + +// Compile-time check for interface implementation +var _ GISService = (*gisService)(nil) + +// gisService handles business logic for GIS data +type gisService struct { + gisRepo repositories.GISRepository +} + +// NewGISService returns a new gisService +func NewGISService(gisRepo repositories.GISRepository) GISService { + return &gisService{ + gisRepo: gisRepo, + } +} + +// CreateGISData validates and adds a new GIS data record +func (s *gisService) CreateGISData(ctx context.Context, gisData *models.GISData) error { + // Validate input + if gisData == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if err := s.validateGISData(gisData); err != nil { + return err + } + return s.gisRepo.Create(ctx, gisData) +} + +// GetGISDataByID fetches a GIS data record by its ID +func (s *gisService) GetGISDataByID(ctx context.Context, id uuid.UUID) (*models.GISData, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("GIS data ID cannot be empty") + } + return s.gisRepo.GetByID(ctx, id) +} + +// GetGISDataByPropertyID fetches GIS data for a property +func (s *gisService) GetGISDataByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.GISData, error) { + if propertyID == uuid.Nil { + return nil, fmt.Errorf("property ID cannot be empty") + } + return s.gisRepo.GetByPropertyID(ctx, propertyID) +} + +// UpdateGISData validates and updates an existing GIS data record +func (s *gisService) UpdateGISData(ctx context.Context, gisData *models.GISData) error { + if gisData == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + if gisData.ID == uuid.Nil { + return fmt.Errorf("GIS data ID cannot be empty for update") + } + + // Validate input + if err := s.validateGISData(gisData); err != nil { + return err + } + return s.gisRepo.Update(ctx, gisData) +} + +// DeleteGISData removes a GIS data record by its ID +func (s *gisService) DeleteGISData(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("GIS data ID cannot be empty") + } + return s.gisRepo.Delete(ctx, id) +} + +// GetAllGISData returns all GIS data with pagination +func (s *gisService) GetAllGISData(ctx context.Context, page, size int) ([]*models.GISData, int64, error) { + // Validate pagination parameters + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + //propagate the repository error as is + return s.gisRepo.GetAll(ctx, page, size) +} + +// validateGISData checks required fields and allowed values for GIS data +func (s *gisService) validateGISData(gisData *models.GISData) error { + if gisData == nil { + return fmt.Errorf("GIS data cannot be nil") + } + + if gisData.PropertyID == uuid.Nil { + return fmt.Errorf("property ID is required") + } + + if gisData.Source == "" { + return fmt.Errorf("source is required") + } + + // Validate source enum values + validSources := map[string]bool{ + "GPS": true, + "MANUAL_ENTRY": true, + "IMPORT": true, + } + if !validSources[gisData.Source] { + return fmt.Errorf("invalid source: %s. Valid values are: GPS, MANUAL_ENTRY, IMPORT", gisData.Source) + } + + if gisData.Type == "" { + return fmt.Errorf("type is required") + } + + // Validate type enum values + validTypes := map[string]bool{ + "POINT": true, + "LINE": true, + "POLYGON": true, + } + if !validTypes[gisData.Type] { + return fmt.Errorf("invalid type: %s. Valid values are: POINT, LINE, POLYGON", gisData.Type) + } + + // Entity type is optional, but if provided, should not be empty + if gisData.EntityType != "" && len(gisData.EntityType) > 100 { + return fmt.Errorf("entity type cannot exceed 100 characters") + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/services/igrs_service.go b/property-tax/enumeration-backend/internal/services/igrs_service.go new file mode 100644 index 0000000..46d7d14 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/igrs_service.go @@ -0,0 +1,163 @@ +// Package services provides business logic and service layer implementations +// for the property tax enumeration system. +package services + +import ( + "context" + "errors" + "fmt" + + "enumeration/internal/constants" + "enumeration/internal/dto" + "enumeration/internal/models" + "enumeration/internal/repositories" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Compile-time check to ensure igrsService implements IGRSService interface. +var _ IGRSService = (*igrsService)(nil) + +// igrsService implements the IGRSService interface and provides methods +// for managing IGRS (Integrated Grievance Redressal System) records. +type igrsService struct { + repo repositories.IGRSRepository // Repository for IGRS data access +} + +// NewIGRSService creates a new instance of IGRSService with the provided repository. +func NewIGRSService(repo repositories.IGRSRepository) IGRSService { + return &igrsService{repo: repo} +} + +// Create validates and creates a new IGRS record. +// Ensures only one IGRS exists per property and required fields are present. +func (s *igrsService) Create(ctx context.Context, req *dto.CreateIGRSRequest) (*models.IGRS, error) { + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + + if req.PropertyID == uuid.Nil { + return nil, errors.New("propertyId is required") + } + if req.Habitation == "" { + return nil, errors.New("habitation is required") + } + + // Ensure only one IGRS per property + if existing, err := s.repo.GetByPropertyID(ctx, req.PropertyID); err == nil && existing != nil { + return nil, fmt.Errorf("igrs already exists for property %s", req.PropertyID.String()) + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + // Propagate unexpected db error + return nil, fmt.Errorf("failed to check existing igrs: %w", err) + } + + // Map request fields to IGRS model + m := &models.IGRS{ + PropertyID: req.PropertyID, + Habitation: req.Habitation, + IGRSWard: req.IGRSWard, + IGRSLocality: req.IGRSLocality, + IGRSBlock: req.IGRSBlock, + DoorNoFrom: req.DoorNoFrom, + DoorNoTo: req.DoorNoTo, + IGRSClassification: req.IGRSClassification, + BuiltUpAreaPct: req.BuiltUpAreaPct, + FrontSetback: req.FrontSetback, + RearSetback: req.RearSetback, + SideSetback: req.SideSetback, + TotalPlinthArea: req.TotalPlinthArea, + } + if err := s.repo.Create(ctx, m); err != nil { + return nil, fmt.Errorf("failed to create igrs: %w", err) + } + return m, nil +} + +// GetByID retrieves an IGRS record by its unique ID. +func (s *igrsService) GetByID(ctx context.Context, id uuid.UUID) (*models.IGRS, error) { + return s.repo.GetByID(ctx, id) +} + +// Update modifies an existing IGRS record by its ID. +// Only fields provided in the request are updated. +func (s *igrsService) Update(ctx context.Context, id uuid.UUID, req *dto.UpdateIGRSRequest) (*models.IGRS, error) { + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + + existing, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + // Update only provided fields + if req.Habitation != "" { + existing.Habitation = req.Habitation + } + if req.IGRSWard != "" { + existing.IGRSWard = req.IGRSWard + } + if req.IGRSLocality != "" { + existing.IGRSLocality = req.IGRSLocality + } + if req.IGRSBlock != "" { + existing.IGRSBlock = req.IGRSBlock + } + if req.DoorNoFrom != "" { + existing.DoorNoFrom = req.DoorNoFrom + } + if req.DoorNoTo != "" { + existing.DoorNoTo = req.DoorNoTo + } + if req.IGRSClassification != "" { + existing.IGRSClassification = req.IGRSClassification + } + // Numeric/pointer fields: assign if not nil + if req.BuiltUpAreaPct != nil { + existing.BuiltUpAreaPct = req.BuiltUpAreaPct + } + if req.FrontSetback != nil { + existing.FrontSetback = req.FrontSetback + } + if req.RearSetback != nil { + existing.RearSetback = req.RearSetback + } + if req.SideSetback != nil { + existing.SideSetback = req.SideSetback + } + if req.TotalPlinthArea != nil { + existing.TotalPlinthArea = req.TotalPlinthArea + } + + if err := s.repo.Update(ctx, existing); err != nil { + return nil, fmt.Errorf("failed to update igrs: %w", err) + } + return existing, nil +} + +// Delete removes an IGRS record by its ID. +func (s *igrsService) Delete(ctx context.Context, id uuid.UUID) error { + return s.repo.Delete(ctx, id) +} + +// List returns a paginated list of IGRS records and the total count. +// If page or size are invalid, defaults are used. +func (s *igrsService) List(ctx context.Context, page, size int) ([]*models.IGRS, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 { + size = constants.DefaultSize + } + items, total, err := s.repo.FindAll(ctx, page, size) + if err != nil { + return nil, 0, err + } + out := make([]*models.IGRS, len(items)) + for i := range items { + // Take address of slice element is OK here because items is not reused. + out[i] = &items[i] + } + return out, total, nil +} diff --git a/property-tax/enumeration-backend/internal/services/interfaces.go b/property-tax/enumeration-backend/internal/services/interfaces.go new file mode 100644 index 0000000..d987deb --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/interfaces.go @@ -0,0 +1,195 @@ +package services + +import ( + "context" + "enumeration/internal/dto" + "enumeration/internal/models" + + "github.com/google/uuid" +) + +// ApplicationLogService defines business logic for application logs +type ApplicationLogService interface { + Create(ctx context.Context, req *dto.CreateApplicationLogRequest) (*models.ApplicationLog, error) + GetByID(ctx context.Context, id uuid.UUID) (*models.ApplicationLog, error) + Update(ctx context.Context, id uuid.UUID, req *dto.UpdateApplicationLogRequest) (*models.ApplicationLog, error) + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, applicationID, action string, page, size int) ([]models.ApplicationLog, int64, error) + GetByApplicationID(ctx context.Context, applicationID uuid.UUID, action string, page, size int) ([]models.ApplicationLog, int64, error) +} + +// FloorDetailsService handles business logic for floor details +type FloorDetailsService interface { + CreateFloorDetails(ctx context.Context, floorDetails *models.FloorDetails) error + GetFloorDetailsByID(ctx context.Context, id uuid.UUID) (*models.FloorDetails, error) + UpdateFloorDetails(ctx context.Context, floorDetails *models.FloorDetails) error + DeleteFloorDetails(ctx context.Context, id uuid.UUID) error + GetAllFloorDetails(ctx context.Context, page, size int, constructionDetailsID *uuid.UUID) ([]*models.FloorDetails, int64, error) + GetFloorDetailsByConstructionDetailsID(ctx context.Context, constructionDetailsID uuid.UUID) ([]*models.FloorDetails, error) +} + +// CoordinatesService handles business logic for coordinates +type CoordinatesService interface { + Create(ctx context.Context, coordinates *models.Coordinates) error + GetByID(ctx context.Context, id uuid.UUID) (*models.Coordinates, error) + Update(ctx context.Context, coordinates *models.Coordinates) error + Delete(ctx context.Context, id uuid.UUID) error + FindAll(ctx context.Context, page, size int, gisDataID *uuid.UUID) ([]models.Coordinates, int64, error) + CreateBatch(ctx context.Context, coords []*models.Coordinates) error + GetAll(ctx context.Context, page, size int, gisDataID *uuid.UUID) ([]*models.Coordinates, int64, error) + ReplaceByGISDataID(ctx context.Context, gisDataID uuid.UUID, coords []*models.Coordinates) error +} + +// ApplicationService handles business logic for applications +type ApplicationService interface { + Create(ctx context.Context, tenantID string, citizenID string, req *dto.CreateApplicationRequest) (*models.Application, error) + GetByID(ctx context.Context, id uuid.UUID) (*models.Application, error) + GetByApplicationNo(ctx context.Context, applicationNo string) (*models.Application, error) + Update(ctx context.Context, id uuid.UUID, req *dto.UpdateApplicationRequest) (*models.Application, error) + UpdateStatus(ctx context.Context, id uuid.UUID, req *dto.UpdateStatusRequest) error + AssignAgent(ctx context.Context, id uuid.UUID, req *dto.ActionRequest, tenantID string) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, UserID string, tenantID string, Role string, verify string, page, size int) ([]models.Application, int64, error) + Search(ctx context.Context, criteria *dto.ApplicationSearchCriteria, page, size int) ([]*models.Application, int64, error) + VerifyApplicationByAgent(ctx context.Context, tenantID, agentID, applicationID string, req *dto.ActionRequest) error + VerifyApplication(ctx context.Context, tenantID, serviceManagerID, applicationID string, req *dto.ActionRequest) error + ApproveApplication(ctx context.Context, tenantID, commissionerID, applicationID string, req *dto.ActionRequest) error + // ReassignAgent(ctx context.Context, id uuid.UUID, req *dto.ActionRequest, tenantID string) error +} + +// PropertyOwnerService handles business logic for property owners +type PropertyOwnerService interface { + Create(ctx context.Context, req *dto.CreatePropertyOwnerRequest) (*models.PropertyOwner, error) + CreatePropertyOwners(ctx context.Context, reqs []*dto.CreatePropertyOwnerRequest) ([]*models.PropertyOwner, error) + GetByPropertyID(ctx context.Context, propertyID uuid.UUID, page, size int) ([]*models.PropertyOwner, int64, error) + Update(ctx context.Context, id uuid.UUID, req *dto.UpdatePropertyOwnerRequest) (*models.PropertyOwner, error) + Delete(ctx context.Context, id uuid.UUID) error +} + +// ConstructionDetailsService handles business logic for construction details +type ConstructionDetailsService interface { + CreateConstructionDetails(ctx context.Context, constructionDetails *models.ConstructionDetails) error + GetConstructionDetailsByID(ctx context.Context, id uuid.UUID) (*models.ConstructionDetails, error) + UpdateConstructionDetails(ctx context.Context, constructionDetails *models.ConstructionDetails) error + DeleteConstructionDetails(ctx context.Context, id uuid.UUID) error + GetAllConstructionDetails(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.ConstructionDetails, int64, error) + GetConstructionDetailsByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.ConstructionDetails, error) +} + +// AdditionalPropertyDetailsService handles business logic for additional property details +type AdditionalPropertyDetailsService interface { + CreateAdditionalPropertyDetails(ctx context.Context, details *models.AdditionalPropertyDetails) error + GetAdditionalPropertyDetailsByID(ctx context.Context, id uuid.UUID) (*models.AdditionalPropertyDetails, error) + UpdateAdditionalPropertyDetails(ctx context.Context, details *models.AdditionalPropertyDetails) error + DeleteAdditionalPropertyDetails(ctx context.Context, id uuid.UUID) error + GetAllAdditionalPropertyDetails(ctx context.Context, page, size int, propertyID *uuid.UUID, fieldName *string) ([]*models.AdditionalPropertyDetails, int64, error) + GetAdditionalPropertyDetailsByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.AdditionalPropertyDetails, error) + GetAdditionalPropertyDetailsByFieldName(ctx context.Context, fieldName string) ([]*models.AdditionalPropertyDetails, error) +} + +// AssessmentDetailsService handles business logic for assessment details +type AssessmentDetailsService interface { + CreateAssessmentDetails(ctx context.Context, assessmentDetails *models.AssessmentDetails) error + GetAssessmentDetailsByID(ctx context.Context, id uuid.UUID) (*models.AssessmentDetails, error) + UpdateAssessmentDetails(ctx context.Context, assessmentDetails *models.AssessmentDetails) error + DeleteAssessmentDetails(ctx context.Context, id uuid.UUID) error + GetAllAssessmentDetails(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.AssessmentDetails, int64, error) + GetAssessmentDetailsByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.AssessmentDetails, error) +} + +// PropertyService handles business logic for properties +type PropertyService interface { + CreateProperty(ctx context.Context, property *models.Property) error + GetPropertyByID(ctx context.Context, id uuid.UUID) (*models.Property, error) + UpdateProperty(ctx context.Context, property *models.Property) error + DeleteProperty(ctx context.Context, id uuid.UUID) error + GetAllProperties(ctx context.Context, page, size int, propertyType *string) ([]*models.Property, int64, error) + GetPropertyByPropertyNo(ctx context.Context, propertyNo string) (*models.Property, error) + SearchProperties(ctx context.Context, params SearchPropertyParams) ([]*models.Property, int64, error) + GeneratePropertyNo(ctx context.Context) (string, error) +} + +// SearchPropertyParams holds search and filter options for properties +type SearchPropertyParams struct { + Page int + Size int + PropertyType *string + OwnershipType *string + ComplexName *string + Locality *string + WardNo *string + ZoneNo *string + Street *string + SortBy string + SortOrder string +} + +// PropertyAddressService handles business logic for property addresses +type PropertyAddressService interface { + CreatePropertyAddress(ctx context.Context, address *models.PropertyAddress) error + GetPropertyAddressByID(ctx context.Context, id uuid.UUID) (*models.PropertyAddress, error) + UpdatePropertyAddress(ctx context.Context, address *models.PropertyAddress) error + DeletePropertyAddress(ctx context.Context, id uuid.UUID) error + GetAllPropertyAddresses(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.PropertyAddress, int64, error) + GetPropertyAddressByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.PropertyAddress, error) + SearchPropertyAddresses(ctx context.Context, params SearchPropertyAddressParams) ([]*models.PropertyAddress, int64, error) +} + +// SearchPropertyAddressParams holds search and filter options for property addresses +type SearchPropertyAddressParams struct { + Page int + Size int + PropertyID *uuid.UUID + Locality *string + ZoneNo *string + WardNo *string + BlockNo *string + Street *string + ElectionWard *string + SecretariatWard *string + PinCode *uint64 + SortBy string + SortOrder string +} + +// GISService handles business logic for GIS data +type GISService interface { + CreateGISData(ctx context.Context, gisData *models.GISData) error + GetGISDataByID(ctx context.Context, id uuid.UUID) (*models.GISData, error) + GetGISDataByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.GISData, error) + UpdateGISData(ctx context.Context, gisData *models.GISData) error + DeleteGISData(ctx context.Context, id uuid.UUID) error + GetAllGISData(ctx context.Context, page, size int) ([]*models.GISData, int64, error) +} + +// AmenityService handles business logic for amenities +type AmenityService interface { + // Paginated GetAll: returns items and total count. + GetAll(ctx context.Context, page, size int) ([]models.Amenities, int64, error) + GetAllWithFilters(ctx context.Context, page, size int, amenityType, propertyID string) ([]models.Amenities, int64, error) + GetByID(ctx context.Context, id string) (*models.Amenities, error) + Create(ctx context.Context, amenity *models.Amenities) error + Update(ctx context.Context, id string, amenity *models.Amenities) error + Delete(ctx context.Context, id string) error + GetByPropertyID(ctx context.Context, propertyID string) (*models.Amenities, error) +} + +// DocumentService handles business logic for documents +type DocumentService interface { + CreateDocument(ctx context.Context, doc *models.Document) error + CreateDocuments(ctx context.Context, docs []*models.Document) error + GetDocumentByID(ctx context.Context, id uuid.UUID) (*models.Document, error) + GetDocumentsByPropertyID(ctx context.Context, propertyID uuid.UUID) ([]*models.Document, error) + GetAllDocuments(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.Document, int64, error) + DeleteDocument(ctx context.Context, id uuid.UUID) error + UpdateDocument(ctx context.Context, doc *models.Document) error +} + +// IGRSService handles business logic for IGRS data +type IGRSService interface { + Create(ctx context.Context, req *dto.CreateIGRSRequest) (*models.IGRS, error) + GetByID(ctx context.Context, id uuid.UUID) (*models.IGRS, error) + Update(ctx context.Context, id uuid.UUID, req *dto.UpdateIGRSRequest) (*models.IGRS, error) + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, page, size int) ([]*models.IGRS, int64, error) +} diff --git a/property-tax/enumeration-backend/internal/services/property_address_service.go b/property-tax/enumeration-backend/internal/services/property_address_service.go new file mode 100644 index 0000000..596c2a7 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/property_address_service.go @@ -0,0 +1,156 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + + "github.com/google/uuid" +) + +// Compile-time check for interface implementation +var _ PropertyAddressService = (*propertyAddressService)(nil) + +// propertyAddressService handles business logic for property addresses +type propertyAddressService struct { + propertyAddressRepo repositories.PropertyAddressRepository +} + +// NewPropertyAddressService returns a new propertyAddressService +func NewPropertyAddressService(propertyAddressRepo repositories.PropertyAddressRepository) PropertyAddressService { + return &propertyAddressService{ + propertyAddressRepo: propertyAddressRepo, + } +} + +// CreatePropertyAddress validates and adds a new property address +func (s *propertyAddressService) CreatePropertyAddress(ctx context.Context, address *models.PropertyAddress) error { + if address == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if err := s.validatePropertyAddress(address); err != nil { + return err + } + + return s.propertyAddressRepo.Create(address) +} + +// GetPropertyAddressByID fetches a property address by its ID +func (s *propertyAddressService) GetPropertyAddressByID(ctx context.Context, id uuid.UUID) (*models.PropertyAddress, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid property address ID: cannot be nil") + } + + // Repository layer error - propagate as is + return s.propertyAddressRepo.GetByID(id) +} + +// UpdatePropertyAddress validates and updates an existing property address +func (s *propertyAddressService) UpdatePropertyAddress(ctx context.Context, address *models.PropertyAddress) error { + if address == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + if address.ID == uuid.Nil { + return fmt.Errorf("invalid property address ID: cannot be nil") + } + return s.propertyAddressRepo.Update(address) +} + +// DeletePropertyAddress removes a property address by its ID +func (s *propertyAddressService) DeletePropertyAddress(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("invalid property address ID: cannot be nil") + } + + // Repository layer error - propagate as is + return s.propertyAddressRepo.Delete(id) +} + +// GetAllPropertyAddresses returns all property addresses with pagination, optionally filtered by propertyID +func (s *propertyAddressService) GetAllPropertyAddresses(ctx context.Context, page, size int, propertyID *uuid.UUID) ([]*models.PropertyAddress, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + + return s.propertyAddressRepo.GetAll(page, size, propertyID) +} + +// GetPropertyAddressByPropertyID fetches a property address for a property +func (s *propertyAddressService) GetPropertyAddressByPropertyID(ctx context.Context, propertyID uuid.UUID) (*models.PropertyAddress, error) { + if propertyID == uuid.Nil { + return nil, fmt.Errorf("invalid property ID: cannot be nil") + } + + // Repository layer error - propagate as is + return s.propertyAddressRepo.GetByPropertyID(propertyID) +} + +// SearchPropertyAddresses finds property addresses matching the given search parameters +func (s *propertyAddressService) SearchPropertyAddresses(ctx context.Context, params SearchPropertyAddressParams) ([]*models.PropertyAddress, int64, error) { + if params.Page < 0 { + params.Page = constants.DefaultPage + } + if params.Size <= 0 || params.Size > 100 { + params.Size = constants.DefaultSize + } + + repoParams := repositories.SearchPropertyAddressParams{ + Page: params.Page, + Size: params.Size, + PropertyID: params.PropertyID, + Locality: params.Locality, + ZoneNo: params.ZoneNo, + WardNo: params.WardNo, + BlockNo: params.BlockNo, + Street: params.Street, + ElectionWard: params.ElectionWard, + SecretariatWard: params.SecretariatWard, + PinCode: params.PinCode, + SortBy: params.SortBy, + SortOrder: params.SortOrder, + } + + return s.propertyAddressRepo.Search(repoParams) +} + +// validatePropertyAddress checks required fields for a property address and returns an error if validation fails +func (s *propertyAddressService) validatePropertyAddress(address *models.PropertyAddress) error { + // Validate required fields based on your model + if address.PropertyID == uuid.Nil { + return fmt.Errorf("property ID is required") + } + + if address.Locality == "" { + return fmt.Errorf("locality is required") + } + + if address.ZoneNo == "" { + return fmt.Errorf("zone number is required") + } + + if address.WardNo == "" { + return fmt.Errorf("ward number is required") + } + + if address.BlockNo == "" { + return fmt.Errorf("block number is required") + } + + if address.ElectionWard == "" { + return fmt.Errorf("election ward is required") + } + + // Validate PIN code (6 digits) + if address.PinCode != 0 && (address.PinCode < 100000 || address.PinCode > 999999) { + return fmt.Errorf("PIN code must be 6 digits") + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/services/property_owner_service.go b/property-tax/enumeration-backend/internal/services/property_owner_service.go new file mode 100644 index 0000000..9b6b70a --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/property_owner_service.go @@ -0,0 +1,171 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/dto" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + + "github.com/google/uuid" +) + +// Compile-time check for interface implementation +var _ PropertyOwnerService = (*propertyOwnerService)(nil) + +// propertyOwnerService handles business logic for property owners +type propertyOwnerService struct { + repo repositories.PropertyOwnerRepository +} + +// NewPropertyOwnerService returns a new propertyOwnerService +func NewPropertyOwnerService(repo repositories.PropertyOwnerRepository) PropertyOwnerService { + return &propertyOwnerService{ + repo: repo, + } +} + +// Create adds a new property owner after validation +func (s *propertyOwnerService) Create(ctx context.Context, req *dto.CreatePropertyOwnerRequest) (*models.PropertyOwner, error) { + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + owner := &models.PropertyOwner{ + ID: uuid.New(), + PropertyID: req.PropertyID, + AdhaarNo: req.AdhaarNo, + Name: req.Name, + ContactNo: req.ContactNo, + Email: req.Email, + Gender: req.Gender, + Guardian: req.Guardian, + GuardianType: req.GuardianType, + RelationshipToProperty: req.RelationshipToProperty, + OwnershipShare: req.OwnershipShare, + IsPrimaryOwner: req.IsPrimaryOwner, + } + + if err := s.repo.Create(ctx, owner); err != nil { + return nil, err + } + return owner, nil +} + +// CreatePropertyOwners adds multiple property owners in a batch after validation +func (s *propertyOwnerService) CreatePropertyOwners(ctx context.Context, reqs []*dto.CreatePropertyOwnerRequest) ([]*models.PropertyOwner, error) { + if len(reqs) == 0 { + return []*models.PropertyOwner{}, nil + } + + owners := make([]*models.PropertyOwner, 0, len(reqs)) + for i, r := range reqs { + + if r == nil { + return nil, fmt.Errorf("%w: owner at index %d is nil", ErrValidation, i) + } + + // Basic validation + if r.PropertyID == uuid.Nil { + return nil, fmt.Errorf("%w: propertyId is required for owner at index %d", ErrValidation, i) + } + if r.Name == "" { + return nil, fmt.Errorf("%w: name is required for owner at index %d", ErrValidation, i) + } + if r.ContactNo == "" { + return nil, fmt.Errorf("%w: contactNo is required for owner at index %d", ErrValidation, i) + } + if r.Gender == "" { + return nil, fmt.Errorf("%w: gender is required for owner at index %d", ErrValidation, i) + } + + owner := &models.PropertyOwner{ + ID: uuid.New(), + PropertyID: r.PropertyID, + AdhaarNo: r.AdhaarNo, + Name: r.Name, + ContactNo: r.ContactNo, + Email: r.Email, + Gender: r.Gender, + Guardian: r.Guardian, + GuardianType: r.GuardianType, + RelationshipToProperty: r.RelationshipToProperty, + OwnershipShare: r.OwnershipShare, + IsPrimaryOwner: r.IsPrimaryOwner, + } + owners = append(owners, owner) + } + + if err := s.repo.CreateBatch(ctx, owners); err != nil { + return nil, err + } + return owners, nil +} + +// Update modifies an existing property owner by ID +func (s *propertyOwnerService) Update(ctx context.Context, id uuid.UUID, req *dto.UpdatePropertyOwnerRequest) (*models.PropertyOwner, error) { + if req == nil { + return nil, fmt.Errorf("%w: request is nil", ErrValidation) + } + + existing, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + if req.Name != "" { + existing.Name = req.Name + } + if req.ContactNo != "" { + existing.ContactNo = req.ContactNo + } + if req.Email != "" { + existing.Email = req.Email + } + if req.Gender != "" { + existing.Gender = req.Gender + } + if req.Guardian != "" { + existing.Guardian = req.Guardian + } + if req.GuardianType != "" { + existing.GuardianType = req.GuardianType + } + if req.RelationshipToProperty != "" { + existing.RelationshipToProperty = req.RelationshipToProperty + } + if req.OwnershipShare > 0 { + existing.OwnershipShare = req.OwnershipShare + } + existing.IsPrimaryOwner = req.IsPrimaryOwner + + if err := s.repo.Update(ctx, existing); err != nil { + return nil, err + } + return existing, nil +} + +// Delete removes a property owner by ID +func (s *propertyOwnerService) Delete(ctx context.Context, id uuid.UUID) error { + return s.repo.Delete(ctx, id) +} + +// GetByPropertyID fetches property owners for a property with pagination +func (s *propertyOwnerService) GetByPropertyID(ctx context.Context, propertyID uuid.UUID, page, size int) ([]*models.PropertyOwner, int64, error) { + // clamp + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + + owners, total, err := s.repo.GetByPropertyID(ctx, propertyID, page, size) + if err != nil { + return nil, 0, err + } + if owners == nil { + owners = []*models.PropertyOwner{} + } + return owners, total, nil +} diff --git a/property-tax/enumeration-backend/internal/services/property_service.go b/property-tax/enumeration-backend/internal/services/property_service.go new file mode 100644 index 0000000..94213b9 --- /dev/null +++ b/property-tax/enumeration-backend/internal/services/property_service.go @@ -0,0 +1,171 @@ +package services + +import ( + "context" + "enumeration/internal/constants" + "enumeration/internal/models" + "enumeration/internal/repositories" + "fmt" + "time" + + "github.com/google/uuid" +) + +// Compile-time check for interface implementation +var _ PropertyService = (*propertyService)(nil) + +// propertyService handles business logic for properties +type propertyService struct { + propertyRepo repositories.PropertyRepository +} + +// NewPropertyService returns a new propertyService +func NewPropertyService(propertyRepo repositories.PropertyRepository) PropertyService { + return &propertyService{ + propertyRepo: propertyRepo, + } +} + +// CreateProperty validates and adds a new property, generating a property number if needed +func (s *propertyService) CreateProperty(ctx context.Context, property *models.Property) error { + if property == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + + // Generate property number if not provided + if property.PropertyNo == "" { + propertyNo, err := s.GeneratePropertyNo(ctx) + if err != nil { + return fmt.Errorf("failed to generate property number: %v", err) + } + property.PropertyNo = propertyNo + } + + // Set PropertyID for nested address if provided + if property.Address != nil { + property.Address.PropertyID = property.ID + if property.Address.ID == uuid.Nil { + property.Address.ID = uuid.New() + } + } + + return s.propertyRepo.Create(ctx, property) +} + +// GetPropertyByID fetches a property by its ID +func (s *propertyService) GetPropertyByID(ctx context.Context, id uuid.UUID) (*models.Property, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid property ID: cannot be nil") + } + + // Repository layer error - propagate as is + return s.propertyRepo.GetByID(ctx, id) +} + +// UpdateProperty validates and updates an existing property +func (s *propertyService) UpdateProperty(ctx context.Context, property *models.Property) error { + if property == nil { + return fmt.Errorf("%w: request is nil", ErrValidation) + } + if property.ID == uuid.Nil { + return fmt.Errorf("invalid property ID: cannot be nil") + } + + if err := s.validateProperty(property); err != nil { + return err + } + + // Repository layer error - propagate as is + return s.propertyRepo.Update(ctx, property) +} + +// DeleteProperty removes a property by its ID +func (s *propertyService) DeleteProperty(ctx context.Context, id uuid.UUID) error { + if id == uuid.Nil { + return fmt.Errorf("invalid property ID: cannot be nil") + } + // Repository layer error - propagate as is + return s.propertyRepo.Delete(ctx, id) +} + +// GetAllProperties returns all properties with pagination, optionally filtered by propertyType +func (s *propertyService) GetAllProperties(ctx context.Context, page, size int, propertyType *string) ([]*models.Property, int64, error) { + if page < 0 { + page = constants.DefaultPage + } + if size <= 0 || size > 100 { + size = constants.DefaultSize + } + + return s.propertyRepo.GetAll(ctx, page, size, propertyType) +} + +// GetPropertyByPropertyNo fetches a property by its property number +func (s *propertyService) GetPropertyByPropertyNo(ctx context.Context, propertyNo string) (*models.Property, error) { + if propertyNo == "" { + return nil, fmt.Errorf("property number is required") + } + + // Repository layer error - propagate as is + return s.propertyRepo.GetByPropertyNo(ctx, propertyNo) +} + +// SearchProperties finds properties matching the given search parameters +func (s *propertyService) SearchProperties(ctx context.Context, params SearchPropertyParams) ([]*models.Property, int64, error) { + if params.Page < 0 { + params.Page = 0 + } + if params.Size <= 0 || params.Size > 100 { + params.Size = 20 + } + + repoParams := repositories.SearchPropertyParams{ + Page: params.Page, + Size: params.Size, + PropertyType: params.PropertyType, + OwnershipType: params.OwnershipType, + ComplexName: params.ComplexName, + Locality: params.Locality, + WardNo: params.WardNo, + ZoneNo: params.ZoneNo, + Street: params.Street, + SortBy: params.SortBy, + SortOrder: params.SortOrder, + } + + return s.propertyRepo.Search(ctx, repoParams) +} + +// GeneratePropertyNo creates a new unique property number +func (s *propertyService) GeneratePropertyNo(ctx context.Context) (string, error) { + // Generate property number in format: PROP-YYYY-XXXXXX + year := time.Now().Year() + timestamp := time.Now().Unix() + + propertyNo := fmt.Sprintf("PROP-%d-%06d", year, timestamp%1000000) + + // Check if property number already exists (very unlikely but good to check) + _, err := s.propertyRepo.GetByPropertyNo(ctx, propertyNo) + if err == nil { + // Property number exists, add random suffix + propertyNo = fmt.Sprintf("%s-%d", propertyNo, time.Now().Nanosecond()%1000) + } + + return propertyNo, nil +} + +// validateProperty checks required fields and allowed values for a property +func (s *propertyService) validateProperty(property *models.Property) error { + // Validate required fields + if property.OwnershipType == "" { + return fmt.Errorf("ownership type is required") + } + + if property.PropertyType == "" { + return fmt.Errorf("property type is required") + } + + + return nil +} + diff --git a/property-tax/enumeration-backend/internal/validators/coordinates_validator.go b/property-tax/enumeration-backend/internal/validators/coordinates_validator.go new file mode 100644 index 0000000..7e178c5 --- /dev/null +++ b/property-tax/enumeration-backend/internal/validators/coordinates_validator.go @@ -0,0 +1,188 @@ +package validators + +import ( + "enumeration/internal/constants" + "enumeration/internal/models" + "fmt" + + "github.com/google/uuid" +) + +// CoordinatesValidator provides validation logic for coordinates +type CoordinatesValidator struct{} + +// NewCoordinatesValidator returns a new CoordinatesValidator +func NewCoordinatesValidator() *CoordinatesValidator { + return &CoordinatesValidator{} +} + +// ValidateCoordinates checks if a single coordinates object is valid +func (v *CoordinatesValidator) ValidateCoordinates(coordinates *models.Coordinates) error { + // Nil pointer check + if coordinates == nil { + return NewValidationError("coordinates", "coordinates object cannot be nil", nil) + } + + // Validate latitude range + if coordinates.Latitude < constants.MinLatitude || coordinates.Latitude > constants.MaxLatitude { + return NewValidationError( + "latitude", + fmt.Sprintf("must be between %.1f and %.1f", constants.MinLatitude, constants.MaxLatitude), + coordinates.Latitude, + ) + } + + // Validate longitude range + if coordinates.Longitude < constants.MinLongitude || coordinates.Longitude > constants.MaxLongitude { + return NewValidationError( + "longitude", + fmt.Sprintf("must be between %.1f and %.1f", constants.MinLongitude, constants.MaxLongitude), + coordinates.Longitude, + ) + } + + // Check for exact zero values (potential uninitialized coordinates) + if coordinates.Latitude == 0.0 && coordinates.Longitude == 0.0 { + return NewValidationError( + "coordinates", + "latitude and longitude cannot both be zero (0.0, 0.0)", + fmt.Sprintf("(%.6f, %.6f)", coordinates.Latitude, coordinates.Longitude), + ) + } + + // Check if GISDataID is provided + if coordinates.GISDataID == uuid.Nil { + return NewValidationError("gisDataId", "gisDataId is required", coordinates.GISDataID) + } + + return nil +} + +// ValidateGeographicRange checks if coordinates are within India's bounds +func (v *CoordinatesValidator) ValidateGeographicRange(coordinates *models.Coordinates) error { + if coordinates == nil { + return NewValidationError("coordinates", "coordinates object cannot be nil", nil) + } + + // Check India bounds + if coordinates.Latitude < constants.IndiaMinLatitude || coordinates.Latitude > constants.IndiaMaxLatitude { + return NewValidationError( + "latitude", + fmt.Sprintf("must be within India bounds (%.1f to %.1f)", constants.IndiaMinLatitude, constants.IndiaMaxLatitude), + coordinates.Latitude, + ) + } + + if coordinates.Longitude < constants.IndiaMinLongitude || coordinates.Longitude > constants.IndiaMaxLongitude { + return NewValidationError( + "longitude", + fmt.Sprintf("must be within India bounds (%.1f to %.1f)", constants.IndiaMinLongitude, constants.IndiaMaxLongitude), + coordinates.Longitude, + ) + } + + return nil +} + +// ValidateKarnatakaRange checks if coordinates are within Karnataka's bounds +func (v *CoordinatesValidator) ValidateKarnatakaRange(coordinates *models.Coordinates) error { + if coordinates == nil { + return NewValidationError("coordinates", "coordinates object cannot be nil", nil) + } + + // Check Karnataka bounds + if coordinates.Latitude < constants.KarnatakaMinLatitude || coordinates.Latitude > constants.KarnatakaMaxLatitude { + return NewValidationError( + "latitude", + fmt.Sprintf("must be within Karnataka bounds (%.1f to %.1f)", constants.KarnatakaMinLatitude, constants.KarnatakaMaxLatitude), + coordinates.Latitude, + ) + } + + if coordinates.Longitude < constants.KarnatakaMinLongitude || coordinates.Longitude > constants.KarnatakaMaxLongitude { + return NewValidationError( + "longitude", + fmt.Sprintf("must be within Karnataka bounds (%.1f to %.1f)", constants.KarnatakaMinLongitude, constants.KarnatakaMaxLongitude), + coordinates.Longitude, + ) + } + + return nil +} + +// ValidateRequest checks if a create request for coordinates is valid +func (v *CoordinatesValidator) ValidateRequest(coordinates *models.Coordinates) error { + return v.ValidateCoordinates(coordinates) +} + +// ValidateUpdateRequest checks if an update request for coordinates is valid +func (v *CoordinatesValidator) ValidateUpdateRequest(coordinates *models.Coordinates) error { + if coordinates == nil { + return NewValidationError("coordinates", "coordinates object cannot be nil", nil) + } + + // Check ID for update + if coordinates.ID == uuid.Nil { + return NewValidationError("id", "id is required for update operation", coordinates.ID) + } + + // Validate coordinate values + return v.ValidateCoordinates(coordinates) +} + +// ValidateBatch checks if a batch of coordinates is valid +func (v *CoordinatesValidator) ValidateBatch(coordinates []*models.Coordinates) error { + if coordinates == nil { + return NewValidationError("coordinates", "coordinates array cannot be nil", nil) + } + + if len(coordinates) == 0 { + return NewValidationError("coordinates", "coordinates array cannot be empty", len(coordinates)) + } + + var errors []error + for i, coord := range coordinates { + if coord == nil { + errors = append(errors, NewValidationError( + fmt.Sprintf("coordinates[%d]", i), + "coordinate cannot be nil", + nil, + )) + continue + } + + if err := v.ValidateCoordinates(coord); err != nil { + errors = append(errors, fmt.Errorf("coordinates[%d]: %w", i, err)) + } + } + + if len(errors) > 0 { + return NewBatchValidationError(errors) + } + + return nil +} + +// ValidateBatchWithGeographicRange checks if a batch of coordinates is valid and within India's bounds +func (v *CoordinatesValidator) ValidateBatchWithGeographicRange(coordinates []*models.Coordinates) error { + if err := v.ValidateBatch(coordinates); err != nil { + return err + } + + var errors []error + for i, coord := range coordinates { + if coord == nil { + continue + } + + if err := v.ValidateGeographicRange(coord); err != nil { + errors = append(errors, fmt.Errorf("coordinates[%d]: %w", i, err)) + } + } + + if len(errors) > 0 { + return NewBatchValidationError(errors) + } + + return nil +} diff --git a/property-tax/enumeration-backend/internal/validators/coordinates_validator_test.go b/property-tax/enumeration-backend/internal/validators/coordinates_validator_test.go new file mode 100644 index 0000000..3612249 --- /dev/null +++ b/property-tax/enumeration-backend/internal/validators/coordinates_validator_test.go @@ -0,0 +1,608 @@ +package validators + +import ( + "enumeration/internal/constants" + "enumeration/internal/models" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +// Unit tests for CoordinatesValidator covering validation of coordinates, geographic range, batch operations, and error formatting + +func TestCoordinatesValidator_ValidateCoordinates(t *testing.T) { + // Test various cases for validating a single coordinates object + validator := NewCoordinatesValidator() + gisDataID := uuid.New() + + // Each test case checks a different validation scenario + tests := []struct { + name string + coordinates *models.Coordinates + wantErr bool + errField string + }{ + { + name: "valid coordinates", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "nil coordinates", + coordinates: nil, + wantErr: true, + errField: "coordinates", + }, + { + name: "latitude too low", + coordinates: &models.Coordinates{ + Latitude: -91.0, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "latitude", + }, + { + name: "latitude too high", + coordinates: &models.Coordinates{ + Latitude: 91.0, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "latitude", + }, + { + name: "longitude too low", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: -181.0, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "longitude", + }, + { + name: "longitude too high", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 181.0, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "longitude", + }, + { + name: "both zero coordinates", + coordinates: &models.Coordinates{ + Latitude: 0.0, + Longitude: 0.0, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "coordinates", + }, + { + name: "missing gisDataId", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 77.5946, + GISDataID: uuid.Nil, + }, + wantErr: true, + errField: "gisDataId", + }, + { + name: "edge case: min valid latitude", + coordinates: &models.Coordinates{ + Latitude: constants.MinLatitude, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "edge case: max valid latitude", + coordinates: &models.Coordinates{ + Latitude: constants.MaxLatitude, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "edge case: min valid longitude", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: constants.MinLongitude, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "edge case: max valid longitude", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: constants.MaxLongitude, + GISDataID: gisDataID, + }, + wantErr: false, + }, + } + + // Run all test cases for ValidateCoordinates + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.ValidateCoordinates(tt.coordinates) + if tt.wantErr { + assert.Error(t, err) + if tt.errField != "" { + valErr, ok := err.(*ValidationError) + assert.True(t, ok, "expected ValidationError") + if ok { + assert.Equal(t, tt.errField, valErr.Field) + } + } + } else { + assert.NoError(t, err) + } + }) + } +} + +// Test validation of coordinates within Karnataka's geographic range +func TestCoordinatesValidator_ValidateGeographicRange(t *testing.T) { + validator := NewCoordinatesValidator() + gisDataID := uuid.New() + + // Each test case checks a different geographic range scenario + tests := []struct { + name string + coordinates *models.Coordinates + wantErr bool + errField string + }{ + { + name: "within Karnataka bounds - Bangalore", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "within Karnataka bounds - Mangalore", + coordinates: &models.Coordinates{ + Latitude: 12.9141, + Longitude: 74.8560, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "nil coordinates", + coordinates: nil, + wantErr: true, + errField: "coordinates", + }, + { + name: "latitude below Karnataka minimum", + coordinates: &models.Coordinates{ + Latitude: 10.0, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "latitude", + }, + { + name: "latitude above Karnataka maximum", + coordinates: &models.Coordinates{ + Latitude: 19.0, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "latitude", + }, + { + name: "longitude below Karnataka minimum", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 73.0, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "longitude", + }, + { + name: "longitude above Karnataka maximum", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 79.0, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "longitude", + }, + { + name: "edge case: min Karnataka latitude", + coordinates: &models.Coordinates{ + Latitude: constants.KarnatakaMinLatitude, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "edge case: max Karnataka latitude", + coordinates: &models.Coordinates{ + Latitude: constants.KarnatakaMaxLatitude, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + } + + // Run all test cases for ValidateGeographicRange + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.ValidateGeographicRange(tt.coordinates) + if tt.wantErr { + assert.Error(t, err) + if tt.errField != "" { + valErr, ok := err.(*ValidationError) + assert.True(t, ok, "expected ValidationError") + if ok { + assert.Equal(t, tt.errField, valErr.Field) + } + } + } else { + assert.NoError(t, err) + } + }) + } +} + +// Test batch validation of coordinates +func TestCoordinatesValidator_ValidateBatch(t *testing.T) { + validator := NewCoordinatesValidator() + gisDataID := uuid.New() + + // Each test case checks a different batch validation scenario + tests := []struct { + name string + coordinates []*models.Coordinates + wantErr bool + }{ + { + name: "valid batch", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + {Latitude: 13.0827, Longitude: 77.5828, GISDataID: gisDataID}, + }, + wantErr: false, + }, + { + name: "valid batch - single item", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + }, + wantErr: false, + }, + { + name: "nil batch", + coordinates: nil, + wantErr: true, + }, + { + name: "empty batch", + coordinates: []*models.Coordinates{}, + wantErr: true, + }, + { + name: "batch with nil coordinate", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + nil, + }, + wantErr: true, + }, + { + name: "batch with invalid latitude", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + {Latitude: 91.0, Longitude: 77.5946, GISDataID: gisDataID}, + }, + wantErr: true, + }, + { + name: "batch with invalid longitude", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + {Latitude: 12.9716, Longitude: 181.0, GISDataID: gisDataID}, + }, + wantErr: true, + }, + { + name: "batch with missing GISDataID", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + {Latitude: 13.0827, Longitude: 77.5828, GISDataID: uuid.Nil}, + }, + wantErr: true, + }, + { + name: "batch with multiple errors", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + nil, + {Latitude: 91.0, Longitude: 181.0, GISDataID: uuid.Nil}, + }, + wantErr: true, + }, + } + + // Run all test cases for ValidateBatch + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.ValidateBatch(tt.coordinates) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +// Test batch validation of coordinates with geographic range +func TestCoordinatesValidator_ValidateBatchWithGeographicRange(t *testing.T) { + validator := NewCoordinatesValidator() + gisDataID := uuid.New() + + // Each test case checks a different batch validation scenario for geographic range + tests := []struct { + name string + coordinates []*models.Coordinates + wantErr bool + }{ + { + name: "valid batch within Karnataka", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + {Latitude: 13.0827, Longitude: 77.5828, GISDataID: gisDataID}, + }, + wantErr: false, + }, + { + name: "batch with coordinates outside Karnataka", + coordinates: []*models.Coordinates{ + {Latitude: 12.9716, Longitude: 77.5946, GISDataID: gisDataID}, + {Latitude: 10.0, Longitude: 77.5946, GISDataID: gisDataID}, + }, + wantErr: true, + }, + { + name: "nil batch", + coordinates: nil, + wantErr: true, + }, + { + name: "empty batch", + coordinates: []*models.Coordinates{}, + wantErr: true, + }, + } + + // Run all test cases for ValidateBatchWithGeographicRange + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.ValidateBatchWithGeographicRange(tt.coordinates) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +// Test validation of update requests for coordinates +func TestCoordinatesValidator_ValidateUpdateRequest(t *testing.T) { + validator := NewCoordinatesValidator() + gisDataID := uuid.New() + coordID := uuid.New() + + // Each test case checks a different update validation scenario + tests := []struct { + name string + coordinates *models.Coordinates + wantErr bool + errField string + }{ + { + name: "valid update", + coordinates: &models.Coordinates{ + ID: coordID, + Latitude: 12.9716, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "nil coordinates", + coordinates: nil, + wantErr: true, + errField: "coordinates", + }, + { + name: "missing ID", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "id", + }, + { + name: "invalid latitude with valid ID", + coordinates: &models.Coordinates{ + ID: coordID, + Latitude: 91.0, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "latitude", + }, + { + name: "invalid longitude with valid ID", + coordinates: &models.Coordinates{ + ID: coordID, + Latitude: 12.9716, + Longitude: 181.0, + GISDataID: gisDataID, + }, + wantErr: true, + errField: "longitude", + }, + { + name: "missing GISDataID with valid ID", + coordinates: &models.Coordinates{ + ID: coordID, + Latitude: 12.9716, + Longitude: 77.5946, + GISDataID: uuid.Nil, + }, + wantErr: true, + errField: "gisDataId", + }, + } + + // Run all test cases for ValidateUpdateRequest + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.ValidateUpdateRequest(tt.coordinates) + if tt.wantErr { + assert.Error(t, err) + if tt.errField != "" { + valErr, ok := err.(*ValidationError) + assert.True(t, ok, "expected ValidationError") + if ok { + assert.Equal(t, tt.errField, valErr.Field) + } + } + } else { + assert.NoError(t, err) + } + }) + } +} + +// Test validation of create requests for coordinates +func TestCoordinatesValidator_ValidateRequest(t *testing.T) { + validator := NewCoordinatesValidator() + gisDataID := uuid.New() + + // Each test case checks a different create request scenario + tests := []struct { + name string + coordinates *models.Coordinates + wantErr bool + }{ + { + name: "valid request", + coordinates: &models.Coordinates{ + Latitude: 12.9716, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: false, + }, + { + name: "nil request", + coordinates: nil, + wantErr: true, + }, + { + name: "invalid request", + coordinates: &models.Coordinates{ + Latitude: 91.0, + Longitude: 77.5946, + GISDataID: gisDataID, + }, + wantErr: true, + }, + } + + // Run all test cases for ValidateRequest + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.ValidateRequest(tt.coordinates) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +// Test error formatting for ValidationError +func TestValidationError_Error(t *testing.T) { + // Each test case checks error message formatting + tests := []struct { + name string + err *ValidationError + expected string + }{ + { + name: "error with value", + err: &ValidationError{ + Field: "latitude", + Message: "must be between -90 and 90", + Value: 91.0, + }, + expected: "validation failed for field 'latitude': must be between -90 and 90 (received: 91)", + }, + { + name: "error without value", + err: &ValidationError{ + Field: "coordinates", + Message: "coordinates object cannot be nil", + Value: nil, + }, + expected: "validation failed for field 'coordinates': coordinates object cannot be nil", + }, + } + + // Run all test cases for ValidationError error formatting + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.err.Error()) + }) + } +} + +// Test error formatting for BatchValidationError +func TestBatchValidationError_Error(t *testing.T) { + err1 := NewValidationError("latitude", "invalid", 91.0) + err2 := NewValidationError("longitude", "invalid", 181.0) + + batchErr := NewBatchValidationError([]error{err1, err2}) + expected := "batch validation failed with 2 errors" + + assert.Equal(t, expected, batchErr.Error()) + assert.Len(t, batchErr.Errors, 2) +} diff --git a/property-tax/enumeration-backend/internal/validators/errors.go b/property-tax/enumeration-backend/internal/validators/errors.go new file mode 100644 index 0000000..149202b --- /dev/null +++ b/property-tax/enumeration-backend/internal/validators/errors.go @@ -0,0 +1,40 @@ +package validators + +import "fmt" + +// ValidationError describes a single validation error for a field +type ValidationError struct { + Field string + Message string + Value interface{} +} + +func (e *ValidationError) Error() string { + if e.Value != nil { + return fmt.Sprintf("validation failed for field '%s': %s (received: %v)", e.Field, e.Message, e.Value) + } + return fmt.Sprintf("validation failed for field '%s': %s", e.Field, e.Message) +} + +// NewValidationError returns a new ValidationError +func NewValidationError(field, message string, value interface{}) *ValidationError { + return &ValidationError{ + Field: field, + Message: message, + Value: value, + } +} + +// BatchValidationError groups multiple validation errors together +type BatchValidationError struct { + Errors []error +} + +func (e *BatchValidationError) Error() string { + return fmt.Sprintf("batch validation failed with %d errors", len(e.Errors)) +} + +// NewBatchValidationError returns a new BatchValidationError +func NewBatchValidationError(errors []error) *BatchValidationError { + return &BatchValidationError{Errors: errors} +} diff --git a/property-tax/enumeration-backend/migrations/initial_schema.sql b/property-tax/enumeration-backend/migrations/initial_schema.sql new file mode 100644 index 0000000..47fc76a --- /dev/null +++ b/property-tax/enumeration-backend/migrations/initial_schema.sql @@ -0,0 +1,541 @@ + +-- -- Enums + +-- DROP SCHEMA "DIGIT3"; + +CREATE OR REPLACE FUNCTION uuid_generate_v4() + RETURNS uuid + LANGUAGE c + STRICT +AS '$libdir/uuid-ossp', $function$uuid_generate_v4$function$ +; +CREATE SCHEMA "DIGIT3" AUTHORIZATION root; + +-- DROP TYPE "DIGIT3"."amenity_type_enum"; + +CREATE TYPE "DIGIT3"."amenity_type_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."digit3_action_enum"; + +CREATE TYPE "DIGIT3"."digit3_action_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."document_type_enum"; + +CREATE TYPE "DIGIT3"."document_type_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."gender_enum"; + +CREATE TYPE "DIGIT3"."gender_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."gis_source_enum"; + +CREATE TYPE "DIGIT3"."gis_source_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."gis_type_enum"; + +CREATE TYPE "DIGIT3"."gis_type_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."guardian_type_enum"; + +CREATE TYPE "DIGIT3"."guardian_type_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."log_action_enum"; + +CREATE TYPE "DIGIT3"."log_action_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."ownership_type_enum"; + +CREATE TYPE "DIGIT3"."ownership_type_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."priority_enum"; + +CREATE TYPE "DIGIT3"."priority_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."property_type_enum"; + +CREATE TYPE "DIGIT3"."property_type_enum" AS ENUM ( +); + +-- DROP TYPE "DIGIT3"."relationship_property_enum"; + +CREATE TYPE "DIGIT3"."relationship_property_enum" AS ENUM ( +); +-- "DIGIT3".address definition + +-- Drop table + +-- DROP TABLE "DIGIT3".address; + +CREATE TABLE "DIGIT3".address ( + id uuid DEFAULT "DIGIT3".gen_random_uuid() NOT NULL, + address_line1 varchar(200) NOT NULL, + address_line2 varchar(200) NULL, + city varchar(100) NOT NULL, + state varchar(100) NOT NULL, + pin_code varchar(10) NOT NULL, + CONSTRAINT address_pkey PRIMARY KEY (id) +); + + +-- "DIGIT3".backup_match definition + +-- Drop table + +-- DROP TABLE "DIGIT3".backup_match; + +CREATE TABLE "DIGIT3".backup_match ( + count int8 NULL +); + + +-- "DIGIT3"."document" definition + +-- Drop table + +-- DROP TABLE "DIGIT3"."document"; + +CREATE TABLE "DIGIT3"."document" ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + document_type varchar(100) NOT NULL, + document_name varchar(255) NOT NULL, + file_store_id varchar(200) NULL, + upload_date timestamptz DEFAULT now() NOT NULL, + "action" varchar(100) DEFAULT 'PENDING'::character varying NOT NULL, + uploaded_by varchar(200) NULL, + "size" varchar(50) NULL, + CONSTRAINT chk_document_name_not_empty CHECK ((char_length((COALESCE(document_name, ''::character varying))::text) > 0)), + CONSTRAINT chk_document_type_not_empty CHECK ((char_length((COALESCE(document_type, ''::character varying))::text) > 0)), + CONSTRAINT document_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_document_file_store_id ON "DIGIT3".document USING btree (file_store_id); +CREATE INDEX idx_document_name ON "DIGIT3".document USING btree (document_name); +CREATE INDEX idx_document_property_id ON "DIGIT3".document USING btree (property_id); +CREATE INDEX idx_document_type ON "DIGIT3".document USING btree (document_type); + + +-- "DIGIT3".properties definition + +-- Drop table + +-- DROP TABLE "DIGIT3".properties; + +CREATE TABLE "DIGIT3".properties ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_no varchar(100) NOT NULL, + ownership_type "DIGIT3"."ownership_type_enum" NULL, + property_type "DIGIT3"."property_type_enum" NULL, + complex_name varchar(200) NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT properties_pkey PRIMARY KEY (id), + CONSTRAINT properties_property_no_key UNIQUE (property_no) +); + + +-- "DIGIT3".users definition + +-- Drop table + +-- DROP TABLE "DIGIT3".users; + +CREATE TABLE "DIGIT3".users ( + keycloak_user_id varchar(255) NOT NULL, + username varchar(255) NOT NULL, + email varchar(255) NOT NULL, + "role" varchar(50) NOT NULL, + is_active bool DEFAULT true NOT NULL, + preferred_language varchar(50) NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + created_by varchar(255) NULL, + updated_by varchar(255) NULL, + CONSTRAINT users_email_key UNIQUE (email), + CONSTRAINT users_pkey PRIMARY KEY (keycloak_user_id), + CONSTRAINT users_username_key UNIQUE (username) +); + + +-- "DIGIT3".with_ward definition + +-- Drop table + +-- DROP TABLE "DIGIT3".with_ward; + +CREATE TABLE "DIGIT3".with_ward ( + count int8 NULL +); + + +-- "DIGIT3".additional_property_details definition + +-- Drop table + +-- DROP TABLE "DIGIT3".additional_property_details; + +CREATE TABLE "DIGIT3".additional_property_details ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + field_name varchar(100) NOT NULL, + field_value text NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT additional_property_details_pkey PRIMARY KEY (id), + CONSTRAINT fk_additional_property_details_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); + + +-- "DIGIT3".amenities definition + +-- Drop table + +-- DROP TABLE "DIGIT3".amenities; + +CREATE TABLE "DIGIT3".amenities ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + "type" _text NOT NULL, + description text NULL, + expiry_date date NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT amenities_pkey PRIMARY KEY (id), + CONSTRAINT uk_amenities_property_id UNIQUE (property_id), + CONSTRAINT fk_amenities_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX idx_amenities_property_id ON "DIGIT3".amenities USING btree (property_id); + + +-- "DIGIT3".applications definition + +-- Drop table + +-- DROP TABLE "DIGIT3".applications; + +CREATE TABLE "DIGIT3".applications ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + application_no varchar(50) NULL, + property_id uuid NOT NULL, + priority varchar(10) NULL, + due_date date NULL, + assigned_agent varchar(255) NULL, + status varchar(50) NULL, + workflow_instance_id varchar(100) NULL, + applied_by varchar(200) NOT NULL, + assessee_id varchar(255) NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + tenant_id text NULL, + is_draft bool DEFAULT false NULL, + CONSTRAINT applications_application_no_key UNIQUE (application_no), + CONSTRAINT applications_pkey PRIMARY KEY (id), + CONSTRAINT applications_priority_check CHECK (((priority)::text = ANY ((ARRAY['LOW'::character varying, 'MEDIUM'::character varying, 'HIGH'::character varying, 'NULL'::character varying])::text[]))), + CONSTRAINT fk_application_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT fk_applications_assessee FOREIGN KEY (assessee_id) REFERENCES "DIGIT3".users(keycloak_user_id) +); + + +-- "DIGIT3".assessment_details definition + +-- Drop table + +-- DROP TABLE "DIGIT3".assessment_details; + +CREATE TABLE "DIGIT3".assessment_details ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + reason_of_creation varchar(200) NULL, + occupancy_certificate_number varchar(100) NULL, + occupancy_certificate_date date NULL, + extend_of_site varchar(200) NULL, + is_land_underneath_building bool DEFAULT false NULL, + is_unspecified_share bool DEFAULT false NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT assessment_details_pkey PRIMARY KEY (id), + CONSTRAINT assessment_details_property_id_key UNIQUE (property_id), + CONSTRAINT fk_assessment_details_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); + + +-- "DIGIT3".construction_details definition + +-- Drop table + +-- DROP TABLE "DIGIT3".construction_details; + +CREATE TABLE "DIGIT3".construction_details ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + floor_type varchar(100) NULL, + wall_type varchar(100) NULL, + roof_type varchar(100) NULL, + wood_type varchar(100) NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT construction_details_pkey PRIMARY KEY (id), + CONSTRAINT construction_details_property_id_key UNIQUE (property_id), + CONSTRAINT fk_construction_details_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); + + +-- "DIGIT3".floor_details definition + +-- Drop table + +-- DROP TABLE "DIGIT3".floor_details; + +CREATE TABLE "DIGIT3".floor_details ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + construction_details_id uuid NOT NULL, + floor_no int4 NOT NULL, + classification varchar(100) NULL, + nature_of_usage varchar(100) NULL, + firm_name varchar(200) NULL, + occupancy_type varchar(100) NULL, + occupancy_name varchar(200) NULL, + construction_date date NULL, + effective_from_date date NULL, + unstructured_land varchar(200) NULL, + length_ft numeric(10, 2) NULL, + breadth_ft numeric(10, 2) NULL, + plinth_area_sq_ft numeric(10, 2) NULL, + building_permission_no varchar(100) NULL, + floor_details_entered bool DEFAULT false NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT floor_details_pkey PRIMARY KEY (id), + CONSTRAINT fk_floor_details_construction FOREIGN KEY (construction_details_id) REFERENCES "DIGIT3".construction_details(id) ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX idx_floor_details_construction_id ON "DIGIT3".floor_details USING btree (construction_details_id); + + +-- "DIGIT3".gis_data definition + +-- Drop table + +-- DROP TABLE "DIGIT3".gis_data; + +CREATE TABLE "DIGIT3".gis_data ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + "source" "DIGIT3"."gis_source_enum" NOT NULL, + "type" "DIGIT3"."gis_type_enum" NOT NULL, + entity_type varchar(100) NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT gis_data_pkey PRIMARY KEY (id), + CONSTRAINT gis_data_property_id_key UNIQUE (property_id), + CONSTRAINT fk_gis_data_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +-- Add latitude and longitude columns to gis_data table +ALTER TABLE "DIGIT3".gis_data +ADD COLUMN latitude NUMERIC(10, 8) NULL, +ADD COLUMN longitude NUMERIC(11, 8) NULL; + +-- Add check constraints for valid coordinate ranges +ALTER TABLE "DIGIT3".gis_data +ADD CONSTRAINT chk_gis_data_latitude CHECK (latitude >= -90.0 AND latitude <= 90.0), +ADD CONSTRAINT chk_gis_data_longitude CHECK (longitude >= -180.0 AND longitude <= 180.0); + +-- Add comment for documentation +COMMENT ON COLUMN "DIGIT3".gis_data.latitude IS 'Latitude coordinate (-90 to 90)'; +COMMENT ON COLUMN "DIGIT3".gis_data.longitude IS 'Longitude coordinate (-180 to 180)'; + +-- Optional: Add index for coordinate-based queries +CREATE INDEX idx_gis_data_coordinates ON "DIGIT3".gis_data(latitude, longitude); + + +-- "DIGIT3".igrs definition + +-- Drop table + +-- DROP TABLE "DIGIT3".igrs; + +CREATE TABLE "DIGIT3".igrs ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NULL, + habitation varchar(200) NOT NULL, + igrs_ward varchar(100) NULL, + igrs_locality varchar(100) NULL, + igrs_block varchar(100) NULL, + door_no_from varchar(50) NULL, + door_no_to varchar(50) NULL, + igrs_classification varchar(100) NULL, + built_up_area_pct numeric(7, 2) NULL, + front_setback numeric(8, 2) NULL, + rear_setback numeric(8, 2) NULL, + side_setback numeric(8, 2) NULL, + total_plinth_area numeric(10, 2) NULL, + created_at timestamptz DEFAULT now() NOT NULL, + updated_at timestamptz DEFAULT now() NOT NULL, + CONSTRAINT igrs_pkey PRIMARY KEY (id), + CONSTRAINT igrs_property_id_key UNIQUE (property_id), + CONSTRAINT fk_igrs_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); + + +-- "DIGIT3".property_addresses definition + +-- Drop table + +-- DROP TABLE "DIGIT3".property_addresses; + +CREATE TABLE "DIGIT3".property_addresses ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + locality varchar(200) NULL, + zone_no varchar(50) NULL, + ward_no varchar(50) NULL, + block_no varchar(50) NULL, + street varchar(200) NULL, + election_ward varchar(50) NULL, + secretariat_ward varchar(50) NULL, + pin_code int4 NULL, + different_correspondence_address bool DEFAULT false NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT property_addresses_pkey PRIMARY KEY (id), + CONSTRAINT property_addresses_property_id_key UNIQUE (property_id), + CONSTRAINT fk_property_address_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); + + +-- "DIGIT3".property_owner definition + +-- Drop table + +-- DROP TABLE "DIGIT3".property_owner; + +CREATE TABLE "DIGIT3".property_owner ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + property_id uuid NOT NULL, + adhaar_no int8 NOT NULL, + "name" varchar(200) NOT NULL, + contact_no varchar(15) NOT NULL, + email varchar(100) NULL, + gender varchar(10) NOT NULL, + guardian varchar(200) NULL, + guardian_type varchar(10) NULL, + relationship_to_property varchar(20) NULL, + ownership_share numeric(5, 2) DEFAULT 0 NULL, + is_primary_owner bool DEFAULT false NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT property_owner_gender_check CHECK (((gender)::text = ANY ((ARRAY['MALE'::character varying, 'FEMALE'::character varying, 'OTHER'::character varying])::text[]))), + CONSTRAINT property_owner_guardian_type_check CHECK (((guardian_type)::text = ANY ((ARRAY['FATHER'::character varying, 'MOTHER'::character varying, 'SPOUSE'::character varying, 'OTHER'::character varying])::text[]))), + CONSTRAINT property_owner_pkey PRIMARY KEY (id), + CONSTRAINT property_owner_relationship_to_property_check CHECK (((relationship_to_property)::text = ANY ((ARRAY['OWNER'::character varying, 'CO_OWNER'::character varying, 'JOINT_OWNER'::character varying, 'LEGAL_HEIR'::character varying, 'POWER_OF_ATTORNEY'::character varying, 'OTHER'::character varying])::text[]))), + CONSTRAINT fk_property_owner_property FOREIGN KEY (property_id) REFERENCES "DIGIT3".properties(id) ON DELETE CASCADE ON UPDATE CASCADE +); + + +-- "DIGIT3".user_profiles definition + +-- Drop table + +-- DROP TABLE "DIGIT3".user_profiles; + +CREATE TABLE "DIGIT3".user_profiles ( + user_profile_id varchar(255) NOT NULL, + first_name varchar(100) NOT NULL, + last_name varchar(100) NOT NULL, + full_name varchar(200) NOT NULL, + phone_number varchar(15) NOT NULL, + adhaar_no int8 NOT NULL, + gender varchar(10) NOT NULL, + guardian varchar(100) NULL, + guardian_type varchar(50) NULL, + date_of_birth date NULL, + department varchar(100) NULL, + designation varchar(100) NULL, + work_location varchar(200) NULL, + profile_picture text NULL, + relationship_to_property varchar(50) DEFAULT ''::character varying NOT NULL, + ownership_share float8 DEFAULT 0 NOT NULL, + is_primary_owner bool DEFAULT false NOT NULL, + is_verified bool DEFAULT false NULL, + address_id uuid NULL, + CONSTRAINT userprofile_adhaar_no_key UNIQUE (adhaar_no), + CONSTRAINT userprofile_ownership_share_check CHECK (((ownership_share >= (0)::double precision) AND (ownership_share <= (100)::double precision))), + CONSTRAINT userprofile_pkey PRIMARY KEY (user_profile_id), + CONSTRAINT userprofile_address_id_fkey FOREIGN KEY (address_id) REFERENCES "DIGIT3".address(id) ON DELETE CASCADE, + CONSTRAINT userprofile_id_fkey FOREIGN KEY (user_profile_id) REFERENCES "DIGIT3".users(keycloak_user_id) +); + + +-- "DIGIT3".zone_mapping definition + +-- Drop table + +-- DROP TABLE "DIGIT3".zone_mapping; + +CREATE TABLE "DIGIT3".zone_mapping ( + user_id varchar(255) NOT NULL, + "zone" varchar(50) NOT NULL, + ward _text NOT NULL, + created_at timestamp DEFAULT now() NULL, + updated_at timestamp DEFAULT now() NULL, + CONSTRAINT zone_mapping_pkey PRIMARY KEY (user_id, zone), + CONSTRAINT zone_mapping_user_id_fkey FOREIGN KEY (user_id) REFERENCES "DIGIT3".users(keycloak_user_id) ON DELETE CASCADE +); +CREATE INDEX idx_zone_mapping_user ON "DIGIT3".zone_mapping USING btree (user_id); +CREATE INDEX idx_zone_mapping_ward_gin ON "DIGIT3".zone_mapping USING gin (ward); +CREATE INDEX idx_zone_mapping_zone ON "DIGIT3".zone_mapping USING btree (zone); + + +-- "DIGIT3".application_logs definition + +-- Drop table + +-- DROP TABLE "DIGIT3".application_logs; + +CREATE TABLE "DIGIT3".application_logs ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + "action" "DIGIT3"."digit3_action_enum" NULL, + performed_by varchar(200) NOT NULL, + performed_date timestamp NOT NULL, + "comments" text NULL, + metadata json NULL, + file_store_id uuid NULL, + application_id uuid NOT NULL, + created_at timestamp DEFAULT now() NULL, + CONSTRAINT application_logs_pkey PRIMARY KEY (id), + CONSTRAINT fk_application FOREIGN KEY (application_id) REFERENCES "DIGIT3".applications(id) +); +CREATE INDEX idx_application_logs_application_id ON "DIGIT3".application_logs USING btree (application_id); +CREATE INDEX idx_application_logs_file_store_id ON "DIGIT3".application_logs USING btree (file_store_id); +CREATE INDEX idx_application_logs_performed_date ON "DIGIT3".application_logs USING btree (performed_date); + + +-- "DIGIT3".coordinates definition + +-- Drop table + +-- DROP TABLE "DIGIT3".coordinates; + +CREATE TABLE "DIGIT3".coordinates ( + id uuid DEFAULT "DIGIT3".uuid_generate_v4() NOT NULL, + gis_data_id uuid NOT NULL, + latitude numeric(10, 8) NOT NULL, + longitude numeric(11, 8) NOT NULL, + created_at timestamp DEFAULT now() NOT NULL, + updated_at timestamp DEFAULT now() NOT NULL, + CONSTRAINT coordinates_pkey PRIMARY KEY (id), + CONSTRAINT fk_coordinates_gis_data FOREIGN KEY (gis_data_id) REFERENCES "DIGIT3".gis_data(id) ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX idx_coordinates_gis_data_id ON "DIGIT3".coordinates USING btree (gis_data_id); + + +-- DROP FUNCTION "DIGIT3".uuid_generate_v4(); diff --git a/property-tax/enumeration-backend/migrations/insert_table.sql b/property-tax/enumeration-backend/migrations/insert_table.sql new file mode 100644 index 0000000..cc99357 --- /dev/null +++ b/property-tax/enumeration-backend/migrations/insert_table.sql @@ -0,0 +1,33 @@ +INSERT INTO properties (property_no, ownership_type, property_type, complex_name) VALUES +('PROP001', 'PRIVATE', 'RESIDENTIAL', 'Green Valley Apartments'), +('PROP002', 'PRIVATE', 'NON_RESIDENTIAL', NULL), +('PROP003', 'VACANT_LAND', 'MIXED', NULL), +('PROP004', 'CENTRAL_GOVERNMENT_50', 'RESIDENTIAL', 'Government Housing Complex'), +('PROP005', 'STATE_GOVERNMENT', 'NON_RESIDENTIAL', 'State Office Complex'); + + +INSERT INTO construction_details ( + property_id, + floor_type, + wall_type, + roof_type, + wood_type +) VALUES ( + 'c4d3926c-1e9a-4b3d-951e-2e919637dcfa', + 'RCC Slab', + 'Brick Wall', + 'Concrete Roof', + 'Teak Wood' +); + +INSERT INTO gis_data ( + property_id, + source, + type, + entity_type +) VALUES ( + 'c4d3926c-1e9a-4b3d-951e-2e919637dcfa', + 'GPS', + 'POLYGON', + 'Residential Building' +); \ No newline at end of file diff --git a/property-tax/enumeration-backend/pkg/logger/logger.go b/property-tax/enumeration-backend/pkg/logger/logger.go new file mode 100644 index 0000000..6a8ba2e --- /dev/null +++ b/property-tax/enumeration-backend/pkg/logger/logger.go @@ -0,0 +1,80 @@ +package logger + +import ( + "log" + "os" +) + +// InfoLogger logs informational messages to stdout +// WarnLogger logs warning messages to stdout +// ErrorLogger logs error messages to stderr +// DebugLogger logs debug messages to stdout +var ( + InfoLogger *log.Logger + WarnLogger *log.Logger + ErrorLogger *log.Logger + DebugLogger *log.Logger +) + +// init initializes the loggers with appropriate prefixes and output streams +func init() { + InfoLogger = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) + WarnLogger = log.New(os.Stdout, "WARN: ", log.Ldate|log.Ltime|log.Lshortfile) + ErrorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) + DebugLogger = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile) +} + +// // Init initializes the logger (no-op, initialization happens in init()) +// func Init() { +// // Logger is already initialized in init() +// } + +// Info logs an informational message +func Info(v ...interface{}) { + InfoLogger.Println(v...) +} + +// Infof logs a formatted informational message +func Infof(format string, v ...interface{}) { + InfoLogger.Printf(format, v...) +} + +// Warn logs a warning message +func Warn(v ...interface{}) { + WarnLogger.Println(v...) +} + +// Warnf logs a formatted warning message +func Warnf(format string, v ...interface{}) { + WarnLogger.Printf(format, v...) +} + +// Error logs an error message +func Error(v ...interface{}) { + ErrorLogger.Println(v...) +} + +// Errorf logs a formatted error message +func Errorf(format string, v ...interface{}) { + ErrorLogger.Printf(format, v...) +} + +// Debug logs a debug message +func Debug(v ...interface{}) { + DebugLogger.Println(v...) +} + +// Debugf logs a formatted debug message +func Debugf(format string, v ...interface{}) { + DebugLogger.Printf(format, v...) +} + +// Fatal logs a fatal error and exits the application +func Fatal(v ...interface{}) { + ErrorLogger.Fatal(v...) +} + +// Fatalf logs a formatted fatal error and exits the application +func Fatalf(format string, v ...interface{}) { + ErrorLogger.Fatalf(format, v...) +} diff --git a/property-tax/enumeration-backend/pkg/response/response.go b/property-tax/enumeration-backend/pkg/response/response.go new file mode 100644 index 0000000..84c7a4f --- /dev/null +++ b/property-tax/enumeration-backend/pkg/response/response.go @@ -0,0 +1,137 @@ +// Package response provides standardized response structures and helper functions +// for sending JSON responses in a RESTful API using the Gin framework. +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// ErrorResponse represents a standard error response structure for API errors. +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Code int `json:"code"` +} + +// SuccessResponse represents a standard success response structure for API responses. +type SuccessResponse struct { + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// PaginationMeta contains metadata for paginated API responses. +type PaginationMeta struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + TotalItems int64 `json:"totalItems"` + TotalPages int `json:"totalPages"` +} + +// PaginatedResponse represents a paginated API response with data and pagination info. +type PaginatedResponse struct { + Data interface{} `json:"data"` + Pagination PaginationMeta `json:"pagination"` +} + +// Success sends a 200 OK JSON response with a message and optional data. +func Success(c *gin.Context, message string, data interface{}) { + c.JSON(http.StatusOK, SuccessResponse{ + Message: message, + Data: data, + }) +} + +// Created sends a 201 Created JSON response with the provided data. +func Created(c *gin.Context, data interface{}) { + c.JSON(http.StatusCreated, data) +} + +// BadRequest sends a 400 Bad Request error response with a message. +func BadRequest(c *gin.Context, message string) { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "BadRequest", + Message: message, + Code: http.StatusBadRequest, + }) +} + +// NotFound sends a 404 Not Found error response with a message. +func NotFound(c *gin.Context, message string) { + c.JSON(http.StatusNotFound, ErrorResponse{ + Error: "NotFound", + Message: message, + Code: http.StatusNotFound, + }) +} + +// Unauthorized sends a 401 Unauthorized error response with a message. +func Unauthorized(c *gin.Context, message string) { + c.JSON(http.StatusUnauthorized, ErrorResponse{ + Error: "Unauthorized", + Message: message, + Code: http.StatusUnauthorized, + }) +} + +// Forbidden sends a 403 Forbidden error response with a message. +func Forbidden(c *gin.Context, message string) { + c.JSON(http.StatusForbidden, ErrorResponse{ + Error: "Forbidden", + Message: message, + Code: http.StatusForbidden, + }) +} + +// InternalServerError sends a 500 Internal Server Error response with a message. +func InternalServerError(c *gin.Context, message string) { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: "InternalError", + Message: message, + Code: http.StatusInternalServerError, + }) +} + +// CustomError sends an error response with a custom status code, error type, and message. +func CustomError(c *gin.Context, code int, errorType string, message string) { + c.JSON(code, ErrorResponse{ + Error: errorType, + Message: message, + Code: code, + }) +} + +// Paginated sends a 200 OK paginated response with data and pagination metadata. +func Paginated(c *gin.Context, data interface{}, pagination PaginationMeta) { + c.JSON(http.StatusOK, PaginatedResponse{ + Data: data, + Pagination: pagination, + }) +} + +// ApiResponse is a generic API response structure supporting both success and error cases. +type ApiResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Errors []string `json:"errors,omitempty"` +} + +// SuccessResponseBody creates a success ApiResponse with a message and optional data. +func SuccessResponseBody(message string, data interface{}) ApiResponse { + return ApiResponse{ + Success: true, + Message: message, + Data: data, + } +} + +// ErrorResponseBody creates an error ApiResponse with a message and optional error details. +func ErrorResponseBody(message string, errors ...string) ApiResponse { + return ApiResponse{ + Success: false, + Message: message, + Errors: errors, + } +} diff --git a/property-tax/enumeration-backend/pkg/utils/custom_date.go b/property-tax/enumeration-backend/pkg/utils/custom_date.go new file mode 100644 index 0000000..f021770 --- /dev/null +++ b/property-tax/enumeration-backend/pkg/utils/custom_date.go @@ -0,0 +1,80 @@ +package utils + +import ( + "database/sql/driver" + "fmt" + "strings" + "time" +) + +// Date wraps time.Time to handle date-only and datetime formats +type Date struct { + time.Time +} + +// UnmarshalJSON parses a date from JSON (supports date and datetime) +func (d *Date) UnmarshalJSON(data []byte) error { + str := strings.Trim(string(data), `"`) + if str == "null" || str == "" { + return nil + } + + // Try parsing as date first (YYYY-MM-DD) + if t, err := time.Parse("2006-01-02", str); err == nil { + d.Time = t + return nil + } + + // Try parsing as datetime (RFC3339) + if t, err := time.Parse(time.RFC3339, str); err == nil { + d.Time = t + return nil + } + + // Try parsing as datetime without timezone + if t, err := time.Parse("2006-01-02T15:04:05", str); err == nil { + d.Time = t + return nil + } + + return fmt.Errorf("cannot parse date: %s", str) +} + +// MarshalJSON outputs the date as YYYY-MM-DD for JSON +func (d Date) MarshalJSON() ([]byte, error) { + if d.Time.IsZero() { + return []byte("null"), nil + } + return []byte(`"` + d.Time.Format("2006-01-02") + `"`), nil +} + +// Value returns the date for database storage (driver.Valuer) +func (d Date) Value() (driver.Value, error) { + if d.Time.IsZero() { + return nil, nil + } + return d.Time, nil +} + +// Scan reads a date value from the database (sql.Scanner) +func (d *Date) Scan(value interface{}) error { + if value == nil { + d.Time = time.Time{} + return nil + } + + switch v := value.(type) { + case time.Time: + d.Time = v + return nil + case string: + t, err := time.Parse("2006-01-02", v) + if err != nil { + return err + } + d.Time = t + return nil + } + + return fmt.Errorf("cannot scan %T into Date", value) +}