From 05139d83a906739954230baf50a47817db23633b Mon Sep 17 00:00:00 2001 From: vinayak3010 Date: Fri, 4 Apr 2025 00:05:28 +0530 Subject: [PATCH 1/2] Implemented Beneficiary Manager microservice --- .github/ISSUE_TEMPLATE/c4gt_community.yml | 313 ++++++++++++++++++ README.md | 2 - beneficiary/.env.example | 18 + beneficiary/Dockerfile | 21 ++ beneficiary/README.md | 71 ++++ beneficiary/cmd/server/main.go | 99 ++++++ beneficiary/docker-compose.yml | 33 ++ beneficiary/go.mod | 21 ++ beneficiary/go.sum | 78 +++++ beneficiary/internal/api/errors.go | 47 +++ beneficiary/internal/api/handlers.go | 121 +++++++ beneficiary/internal/api/handlers_test.go | 98 ++++++ beneficiary/internal/config/config.go | 54 +++ beneficiary/internal/db/db.go | 14 + beneficiary/internal/db/migrate/migrate.go | 38 +++ beneficiary/internal/db/mock/mock.go | 76 +++++ beneficiary/internal/db/postgres.go | 148 +++++++++ beneficiary/internal/logger/logger.go | 86 +++++ beneficiary/internal/logger/mock/mock.go | 41 +++ beneficiary/internal/models/application.go | 18 + beneficiary/internal/models/scheme.go | 20 ++ beneficiary/internal/models/validation.go | 128 +++++++ .../internal/models/validation_test.go | 117 +++++++ beneficiary/internal/service/beneficiary.go | 129 ++++++++ .../internal/service/beneficiary_test.go | 121 +++++++ beneficiary/internal/service/errors.go | 12 + .../000001_create_schemes_table.down.sql | 1 + .../000001_create_schemes_table.up.sql | 17 + .../000002_create_applications_table.down.sql | 1 + .../000002_create_applications_table.up.sql | 15 + 30 files changed, 1956 insertions(+), 2 deletions(-) delete mode 100644 README.md create mode 100644 beneficiary/.env.example create mode 100644 beneficiary/Dockerfile create mode 100644 beneficiary/README.md create mode 100644 beneficiary/cmd/server/main.go create mode 100644 beneficiary/docker-compose.yml create mode 100644 beneficiary/go.mod create mode 100644 beneficiary/go.sum create mode 100644 beneficiary/internal/api/errors.go create mode 100644 beneficiary/internal/api/handlers.go create mode 100644 beneficiary/internal/api/handlers_test.go create mode 100644 beneficiary/internal/config/config.go create mode 100644 beneficiary/internal/db/db.go create mode 100644 beneficiary/internal/db/migrate/migrate.go create mode 100644 beneficiary/internal/db/mock/mock.go create mode 100644 beneficiary/internal/db/postgres.go create mode 100644 beneficiary/internal/logger/logger.go create mode 100644 beneficiary/internal/logger/mock/mock.go create mode 100644 beneficiary/internal/models/application.go create mode 100644 beneficiary/internal/models/scheme.go create mode 100644 beneficiary/internal/models/validation.go create mode 100644 beneficiary/internal/models/validation_test.go create mode 100644 beneficiary/internal/service/beneficiary.go create mode 100644 beneficiary/internal/service/beneficiary_test.go create mode 100644 beneficiary/internal/service/errors.go create mode 100644 beneficiary/migrations/postgres/000001_create_schemes_table.down.sql create mode 100644 beneficiary/migrations/postgres/000001_create_schemes_table.up.sql create mode 100644 beneficiary/migrations/postgres/000002_create_applications_table.down.sql create mode 100644 beneficiary/migrations/postgres/000002_create_applications_table.up.sql diff --git a/.github/ISSUE_TEMPLATE/c4gt_community.yml b/.github/ISSUE_TEMPLATE/c4gt_community.yml index 87ce53f..fd7cc2a 100644 --- a/.github/ISSUE_TEMPLATE/c4gt_community.yml +++ b/.github/ISSUE_TEMPLATE/c4gt_community.yml @@ -4,6 +4,16 @@ title: "[C4GT Community]: " labels: - "C4GT Community" +body: + - type: textarea + id: ticket-description + validations: + required: truename: "C4GT Open Community Template" +description: "Create a new Ticket for C4GT Open Community" +title: "[C4GT Community]: " +labels: + - "C4GT Community" + body: - type: textarea id: ticket-description @@ -305,6 +315,309 @@ body: validations: required: true + - type: textarea + id: ticket-monetary-incentive + attributes: + label: Monetary Incentive + description: "Please mention the amount (in INR/USD) in case of a bounty / paid ticket. Please mention NA in case it is an unpaid ticket" + placeholder: "Enter amount in INR/USD or NA" + validations: + required: true + + description: "Provide a detailed description of the ticket" + placeholder: | + ## Description + [Provide a brief project description, outlining the need and measurable goals] + + **Key functionalities**: + - Feature 1 + - Feature 2 + - Feature 3 + + **Reference links**: + - Link 1 + - Link 2 + - Link 3 + + **Expected Timeline**: + **Bounty (if applicable)**: + + - type: textarea + id: ticket-goals + validations: + required: true + attributes: + label: Goals + description: "List the goals of the feature" + placeholder: | + ## Goals + - [ ] Goal 1 + - [ ] Goal 2 + - [ ] Goal 3 + - [ ] Goal 4 + - [ ] Goal 5 + + [Setup Guide/Documentation links if any] + + - type: textarea + id: ticket-expected-outcome-contributor + validations: + required: true + attributes: + label: Expected Outcome from Contributor + description: "Describe in detail what the final product or result should look like and how it should behave" + placeholder: | + ## Expected Outcome + Describe the expected deliverables and outcomes from the contributor: + - Outcome 1 + - Outcome 2 + - Outcome 3 + + - type: textarea + id: ticket-expected-outcome-mentor + validations: + required: true + attributes: + label: Expected Outcome from Mentor + description: "Describe in detail the expectations from the mentors" + placeholder: | + ## Mentor Expectations + - Expectation 1 + - Expectation 2 + - Expectation 3 + + - type: textarea + id: ticket-acceptance-criteria + validations: + required: true + attributes: + label: Acceptance Criteria + description: "List the acceptance criteria for this feature" + placeholder: | + ## Acceptance Criteria + - [ ] Criteria 1 + - [ ] Criteria 2 + - [ ] Criteria 3 + + - type: textarea + id: ticket-implementation-details + validations: + required: true + attributes: + label: Implementation Details + description: "List any technical details about the proposed implementation, including any specific technologies that will be used" + placeholder: | + ## Implementation Details + - **Language**: + - **Architecture**: + - **Database**: + - **Deployment**: + - **Security**: + - **Design Paradigm**: + + - type: textarea + id: ticket-mockups + attributes: + label: Mockups/Wireframes + description: "Include links to any visual aids, mockups, or diagrams" + placeholder: | + ## Mockups/Wireframes + [Add links or descriptions of any visual aids] + + - type: input + id: ticket-product + attributes: + label: Product Name + placeholder: "Enter Product Name" + validations: + required: true + + - type: dropdown + id: ticket-organisation + attributes: + label: Organisation Name + description: "Enter Organisation Name" + multiple: false + options: + - C4GT + - Dhiway + - FIDE + - SamagraX + - ShikshaLokam + - Tech4Dev + - Tibil + - ONDC + - ONEST + validations: + required: true + + - type: input + id: ticket-governance-domain + attributes: + label: Domain + placeholder: "Enter Area of Governance" + validations: + required: true + + - type: dropdown + id: ticket-technical-skills-required + attributes: + label: Tech Skills Needed + description: "Select the technologies needed for this ticket (use Ctrl or Command to select multiple)" + multiple: true + options: + - .NET + - Agile + - Angular + - Artificial Intelligence + - ASP.NET + - AWS + - Babel + - Bootstrap + - C# + - Chart.js + - CI/CD + - Computer Vision + - CORS + - cURL + - Cypress + - D3.js + - Database + - Debugging + - Design + - DevOps + - Django + - Docker + - Electron + - ESLint + - Express.js + - Feature + - Flask + - Go + - GraphQL + - HTML + - Ionic + - Jest + - Java + - JavaScript + - Jenkins + - JWT + - Kubernetes + - Laravel + - Machine Learning + - Maintenance + - Markdown + - Material-UI + - Microservices + - MongoDB + - Mobile + - Mockups + - Mocha + - Natural Language Processing + - NestJS + - Node.js + - NUnit + - OAuth + - Performance Improvement + - Prettier + - Python + - Question + - React + - React Native + - Redux + - RESTful APIs + - Ruby + - Ruby on Rails + - Rust + - Scala + - Security + - Selenium + - SEO + - Serverless + - Solidity + - Spring Boot + - SQL + - Swagger + - Tailwind CSS + - Test + - Testing Library + - Three.js + - TypeScript + - UI/UX/Design + - Virtual Reality + - Vue.js + - WebSockets + - Webpack + - Other + validations: + required: true + + - type: dropdown + id: ticket-mentorship + attributes: + label: Mentorship Status + description: "Choose the mentorship status for this ticket" + multiple: false + options: + - Mentor Required + - Mentor Selected + - Mentorship Completed + - Mentor not required + validations: + required: true + + - type: textarea + id: ticket-mentors + attributes: + label: Mentor(s) + description: "Please tag relevant mentors for the ticket" + placeholder: "Tag mentors using their GitHub handles (@username)" + validations: + required: true + + - type: dropdown + id: ticket-complexity + attributes: + label: Complexity + description: "Choose a complexity describing the complexity of your ticket" + multiple: false + options: + - Foundational + - Low + - Medium + - High + - Advanced + validations: + required: true + + - type: dropdown + id: ticket-project-type + attributes: + label: Contribution Type + description: "Choose the labels that best describe this project" + multiple: true + options: + - Code contribution + - UI / UX design + - Advisory + - Mentorship + - Others + validations: + required: true + + - type: dropdown + id: ticket-issue-type + attributes: + label: Issue Type + description: "Choose the labels that best describe this ticket" + multiple: true + options: + - Unpaid ticket + - Paid ticket + - Bounty ticket + validations: + required: true + - type: textarea id: ticket-monetary-incentive attributes: diff --git a/README.md b/README.md deleted file mode 100644 index 010e8c5..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Job-Manager - diff --git a/beneficiary/.env.example b/beneficiary/.env.example new file mode 100644 index 0000000..a49f8e8 --- /dev/null +++ b/beneficiary/.env.example @@ -0,0 +1,18 @@ +# Server Configuration +PORT=8080 +SERVER_READ_TIMEOUT=5s +SERVER_WRITE_TIMEOUT=10s + +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_USER=beneficiary_user +DB_PASSWORD=your_secure_password +DB_NAME=beneficiary +DB_SSLMODE=disable +DB_MAX_OPEN_CONNS=25 +DB_MAX_IDLE_CONNS=25 + +# Logging Configuration +LOG_LEVEL=info +LOG_PRETTY_PRINT=true \ No newline at end of file diff --git a/beneficiary/Dockerfile b/beneficiary/Dockerfile new file mode 100644 index 0000000..623ffb9 --- /dev/null +++ b/beneficiary/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.21-alpine + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o main ./cmd/server + +# Default configuration +ENV PORT=8080 \ + LOG_LEVEL=info \ + LOG_PRETTY_PRINT=false \ + DB_MAX_OPEN_CONNS=25 \ + DB_MAX_IDLE_CONNS=25 + +EXPOSE 8080 + +CMD ["./main"] \ No newline at end of file diff --git a/beneficiary/README.md b/beneficiary/README.md new file mode 100644 index 0000000..6057642 --- /dev/null +++ b/beneficiary/README.md @@ -0,0 +1,71 @@ +# Implementation Progress Report + +I have implemented the Beneficiary Manager service with all core functionalities as requested. The service is ready for ONEST protocol integration. + +## Completed Features ✅ + +### API Endpoints + +- `GET /schemes` - Fetch schemes with filtering support +- `POST /applications` - Submit applications with credentials +- `GET /status` - Track application status + +### Core Implementation + +- PostgreSQL database integration with migrations +- Configuration management using environment variables +- Structured logging with different levels +- Input validation and error handling +- Unit tests with mocks +- Docker containerization + +### Project Structure +. +├── cmd/server/ # Application entrypoint +├── internal/ +│ ├── api/ # HTTP handlers +│ ├── config/ # Configuration management +│ ├── db/ # Database operations +│ ├── logger/ # Logging functionality +│ ├── models/ # Data models +│ └── service/ # Business logic +├── migrations/ # Database migrations +├── Dockerfile +├── docker-compose.yml +└── .env.example + + +### Integration-Ready Features +- Clean interface-based design for easy protocol integration +- Stateless service architecture +- Configurable database connection pooling +- Graceful shutdown handling +- Comprehensive error types +- Request/response logging + +## Next Steps +- [ ] Integrate with ONEST financial support protocol + - Implement protocol client + - Add protocol-specific validation + - Map local models to protocol formats + - Handle protocol-specific errors + +## Setup Instructions +1. Clone the repository +2. Copy `.env.example` to `.env` and configure +3. Run `docker-compose up` + +## Testing +Run tests with: +``` +go test ./... +``` + +## Docker Containerization +Build and run with: +``` +docker-compose build +docker-compose up +``` + +Looking forward to guidance on ONEST protocol integration specifications to complete the implementation. diff --git a/beneficiary/cmd/server/main.go b/beneficiary/cmd/server/main.go new file mode 100644 index 0000000..fb2a2d8 --- /dev/null +++ b/beneficiary/cmd/server/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/VinVorteX/beneficiary-manager/internal/api" + "github.com/VinVorteX/beneficiary-manager/internal/config" + "github.com/VinVorteX/beneficiary-manager/internal/db" + "github.com/VinVorteX/beneficiary-manager/internal/logger" + "github.com/VinVorteX/beneficiary-manager/internal/service" +) + +func main() { + // Load configuration + cfg, err := config.Load() + if err != nil { + fmt.Printf("Failed to load config: %v\n", err) + os.Exit(1) + } + + // Initialize logger + if err := logger.Initialize(cfg.Logging.Level, cfg.Logging.PrettyPrint); err != nil { + fmt.Printf("Failed to initialize logger: %v\n", err) + os.Exit(1) + } + + appLogger := logger.NewLogger() + appLogger.Info("Starting Beneficiary Manager service", map[string]interface{}{ + "version": "1.0.0", + }) + + // Initialize DB with migrations + postgres, err := db.NewPostgresDB( + cfg.Database.GetDSN(), + "migrations/postgres", + appLogger, + ) + if err != nil { + appLogger.Error("Failed to initialize database", err, nil) + os.Exit(1) + } + + // Configure database connection pool + if err := postgres.ConfigurePool(cfg.Database.MaxOpenConns, cfg.Database.MaxIdleConns); err != nil { + appLogger.Error("Failed to configure database pool", err, nil) + os.Exit(1) + } + + // Initialize Service + beneficiaryService := service.NewBeneficiaryService(postgres, appLogger) + + // Initialize Handler + handler := api.NewHandler(beneficiaryService) + + // Create server + server := &http.Server{ + Addr: ":" + cfg.Server.Port, + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + } + + // Setup routes + http.HandleFunc("/schemes", handler.GetSchemes) + http.HandleFunc("/applications", handler.SubmitApplication) + http.HandleFunc("/status", handler.GetApplicationStatus) + + // Start server + go func() { + appLogger.Info("Server starting", map[string]interface{}{ + "port": cfg.Server.Port, + }) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + appLogger.Error("Server failed to start", err, nil) + os.Exit(1) + } + }() + + // Graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + appLogger.Info("Server shutting down", nil) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + appLogger.Error("Server forced to shutdown", err, nil) + } + + appLogger.Info("Server exited", nil) +} diff --git a/beneficiary/docker-compose.yml b/beneficiary/docker-compose.yml new file mode 100644 index 0000000..7a45654 --- /dev/null +++ b/beneficiary/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + app: + build: . + ports: + - "8080:8080" + environment: + - PORT=8080 + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=beneficiary_user + - DB_PASSWORD=your_secure_password + - DB_NAME=beneficiary + - DB_SSLMODE=disable + - LOG_LEVEL=info + - LOG_PRETTY_PRINT=true + depends_on: + - db + + db: + image: postgres:15-alpine + environment: + - POSTGRES_USER=beneficiary_user + - POSTGRES_PASSWORD=your_secure_password + - POSTGRES_DB=beneficiary + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: \ No newline at end of file diff --git a/beneficiary/go.mod b/beneficiary/go.mod new file mode 100644 index 0000000..c0aa414 --- /dev/null +++ b/beneficiary/go.mod @@ -0,0 +1,21 @@ +module github.com/VinVorteX/beneficiary-manager + +go 1.22.0 + +toolchain go1.24.1 + +require ( + github.com/golang-migrate/migrate/v4 v4.18.2 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/lib/pq v1.10.9 + github.com/rs/zerolog v1.31.0 +) + +require ( + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/sys v0.28.0 // indirect +) diff --git a/beneficiary/go.sum b/beneficiary/go.sum new file mode 100644 index 0000000..feadec5 --- /dev/null +++ b/beneficiary/go.sum @@ -0,0 +1,78 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= +github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= +github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/beneficiary/internal/api/errors.go b/beneficiary/internal/api/errors.go new file mode 100644 index 0000000..8ec5f3b --- /dev/null +++ b/beneficiary/internal/api/errors.go @@ -0,0 +1,47 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" +) + +// ErrorResponse represents the structure of error responses +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Code int `json:"code"` +} + +// Helper functions for response handling +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + response, err := json.Marshal(payload) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to marshal response") + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + w.Write(response) +} + +func respondWithError(w http.ResponseWriter, code int, message string) { + respondWithJSON(w, code, ErrorResponse{ + Error: http.StatusText(code), + Message: message, + Code: code, + }) +} + +// parseFloat safely converts string to float64 +func parseFloat(s string) float64 { + if s == "" { + return 0 + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0 + } + return f +} \ No newline at end of file diff --git a/beneficiary/internal/api/handlers.go b/beneficiary/internal/api/handlers.go new file mode 100644 index 0000000..59b29b6 --- /dev/null +++ b/beneficiary/internal/api/handlers.go @@ -0,0 +1,121 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "github.com/VinVorteX/beneficiary-manager/internal/models" + "github.com/VinVorteX/beneficiary-manager/internal/service" +) + +type Handler struct { + service *service.BeneficiaryService +} + +func NewHandler(service *service.BeneficiaryService) *Handler { + return &Handler{service: service} +} + +func (h *Handler) GetSchemes(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + respondWithError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + filter := models.SchemeFilter{ + Provider: r.URL.Query().Get("provider"), + MinAmount: parseFloat(r.URL.Query().Get("min_amount")), + MaxAmount: parseFloat(r.URL.Query().Get("max_amount")), + Status: r.URL.Query().Get("status"), + } + + if err := filter.Validate(); err != nil { + respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Invalid filter parameters: %v", err)) + return + } + + schemes, err := h.service.GetSchemes(filter) + if err != nil { + switch err { + case service.ErrInvalidFilter: + respondWithError(w, http.StatusBadRequest, "Invalid filter parameters") + default: + respondWithError(w, http.StatusInternalServerError, "Failed to fetch schemes") + } + return + } + + respondWithJSON(w, http.StatusOK, schemes) +} + +func (h *Handler) SubmitApplication(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondWithError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + var app models.Application + if err := json.NewDecoder(r.Body).Decode(&app); err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload") + return + } + + if err := app.Validate(); err != nil { + respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Invalid application data: %v", err)) + return + } + + if err := h.service.SubmitApplication(app); err != nil { + switch err { + case service.ErrInvalidApplication: + respondWithError(w, http.StatusBadRequest, "Invalid application data") + case service.ErrSchemeNotFound: + respondWithError(w, http.StatusNotFound, "Scheme not found") + case service.ErrSchemeInactive: + respondWithError(w, http.StatusBadRequest, "Scheme is not active") + case service.ErrSchemeExpired: + respondWithError(w, http.StatusBadRequest, "Scheme has expired") + default: + respondWithError(w, http.StatusInternalServerError, "Failed to submit application") + } + return + } + + respondWithJSON(w, http.StatusCreated, map[string]string{ + "message": "Application submitted successfully", + "application_id": app.ID, + }) +} + +func (h *Handler) GetApplicationStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + respondWithError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + applicationID := r.URL.Query().Get("application_id") + if applicationID == "" { + respondWithError(w, http.StatusBadRequest, "Application ID is required") + return + } + + if !models.IsValidID(applicationID) { + respondWithError(w, http.StatusBadRequest, "Invalid application ID format") + return + } + + status, err := h.service.GetApplicationStatus(applicationID) + if err != nil { + switch err { + case service.ErrApplicationNotFound: + respondWithError(w, http.StatusNotFound, "Application not found") + default: + respondWithError(w, http.StatusInternalServerError, "Failed to fetch application status") + } + return + } + + respondWithJSON(w, http.StatusOK, status) +} + +// Implement other handler methods... diff --git a/beneficiary/internal/api/handlers_test.go b/beneficiary/internal/api/handlers_test.go new file mode 100644 index 0000000..a476386 --- /dev/null +++ b/beneficiary/internal/api/handlers_test.go @@ -0,0 +1,98 @@ +package api + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "github.com/VinVorteX/beneficiary-manager/internal/db/mock" + loggerMock "github.com/VinVorteX/beneficiary-manager/internal/logger/mock" + "github.com/VinVorteX/beneficiary-manager/internal/models" + "github.com/VinVorteX/beneficiary-manager/internal/service" +) + +func setupTestHandler() *Handler { + mockDB := mock.NewMockDB() + mockLogger := loggerMock.NewMockLogger() + beneficiaryService := service.NewBeneficiaryService(mockDB, mockLogger) + return NewHandler(beneficiaryService) +} + +func TestGetSchemes(t *testing.T) { + handler := setupTestHandler() + + tests := []struct { + name string + queryParams string + expectedStatus int + }{ + { + name: "Valid request", + queryParams: "?provider=gov&min_amount=1000", + expectedStatus: http.StatusOK, + }, + { + name: "Invalid amount filter", + queryParams: "?min_amount=-100", + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/schemes"+tt.queryParams, nil) + w := httptest.NewRecorder() + + handler.GetSchemes(w, req) + + if w.Code != tt.expectedStatus { + t.Errorf("expected status %d, got %d", tt.expectedStatus, w.Code) + } + }) + } +} + +func TestSubmitApplication(t *testing.T) { + handler := setupTestHandler() + + tests := []struct { + name string + application models.Application + expectedStatus int + }{ + { + name: "Valid application", + application: models.Application{ + SchemeID: "scheme1", + ApplicantID: "user1", + Credentials: map[string]interface{}{ + "aadhar": "1234", + "pan": "ABCD1234", + }, + }, + expectedStatus: http.StatusCreated, + }, + { + name: "Invalid application", + application: models.Application{ + SchemeID: "", // Missing required field + }, + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body, _ := json.Marshal(tt.application) + req := httptest.NewRequest("POST", "/applications", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + + handler.SubmitApplication(w, req) + + if w.Code != tt.expectedStatus { + t.Errorf("expected status %d, got %d", tt.expectedStatus, w.Code) + } + }) + } +} diff --git a/beneficiary/internal/config/config.go b/beneficiary/internal/config/config.go new file mode 100644 index 0000000..ec927ff --- /dev/null +++ b/beneficiary/internal/config/config.go @@ -0,0 +1,54 @@ +package config + +import ( + "fmt" + "time" + + "github.com/kelseyhightower/envconfig" +) + +// Config holds all configuration for the service +type Config struct { + Server ServerConfig + Database DatabaseConfig + Logging LoggingConfig +} + +type ServerConfig struct { + Port string `envconfig:"PORT" default:"8080"` + ReadTimeout time.Duration `envconfig:"SERVER_READ_TIMEOUT" default:"5s"` + WriteTimeout time.Duration `envconfig:"SERVER_WRITE_TIMEOUT" default:"10s"` +} + +type DatabaseConfig struct { + Host string `envconfig:"DB_HOST" required:"true"` + Port int `envconfig:"DB_PORT" default:"5432"` + User string `envconfig:"DB_USER" required:"true"` + Password string `envconfig:"DB_PASSWORD" required:"true"` + Database string `envconfig:"DB_NAME" required:"true"` + SSLMode string `envconfig:"DB_SSLMODE" default:"disable"` + MaxOpenConns int `envconfig:"DB_MAX_OPEN_CONNS" default:"25"` + MaxIdleConns int `envconfig:"DB_MAX_IDLE_CONNS" default:"25"` +} + +type LoggingConfig struct { + Level string `envconfig:"LOG_LEVEL" default:"info"` + PrettyPrint bool `envconfig:"LOG_PRETTY_PRINT" default:"false"` +} + +// Load reads configuration from environment variables +func Load() (*Config, error) { + var config Config + if err := envconfig.Process("", &config); err != nil { + return nil, fmt.Errorf("failed to load config: %v", err) + } + return &config, nil +} + +// GetDSN returns the PostgreSQL connection string +func (c *DatabaseConfig) GetDSN() string { + return fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode, + ) +} diff --git a/beneficiary/internal/db/db.go b/beneficiary/internal/db/db.go new file mode 100644 index 0000000..70d8fc0 --- /dev/null +++ b/beneficiary/internal/db/db.go @@ -0,0 +1,14 @@ +package db + +import ( + "github.com/VinVorteX/beneficiary-manager/internal/models" +) + +// DB defines the interface for database operations +type DB interface { + GetSchemes(filter models.SchemeFilter) ([]models.Scheme, error) + GetSchemeByID(id string) (*models.Scheme, error) + SaveApplication(app models.Application) error + GetApplicationStatus(applicationID string) (*models.ApplicationStatus, error) + ConfigurePool(maxOpen, maxIdle int) error +} diff --git a/beneficiary/internal/db/migrate/migrate.go b/beneficiary/internal/db/migrate/migrate.go new file mode 100644 index 0000000..5fe2bc7 --- /dev/null +++ b/beneficiary/internal/db/migrate/migrate.go @@ -0,0 +1,38 @@ +package migrate + +import ( + "database/sql" + "errors" + "fmt" + "log" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +// MigrateDB handles database migrations +func MigrateDB(db *sql.DB, migrationsPath string) error { + driver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + return fmt.Errorf("could not create migration driver: %v", err) + } + + m, err := migrate.NewWithDatabaseInstance( + fmt.Sprintf("file://%s", migrationsPath), + "postgres", + driver, + ) + if err != nil { + return fmt.Errorf("could not create migrate instance: %v", err) + } + + if err := m.Up(); err != nil { + if !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("could not run migrations: %v", err) + } + log.Println("No migrations to run") + } + + return nil +} diff --git a/beneficiary/internal/db/mock/mock.go b/beneficiary/internal/db/mock/mock.go new file mode 100644 index 0000000..84c4e2f --- /dev/null +++ b/beneficiary/internal/db/mock/mock.go @@ -0,0 +1,76 @@ +package mock + +import ( + "github.com/VinVorteX/beneficiary-manager/internal/models" +) + +type MockDB struct { + schemes []models.Scheme + applications map[string]models.Application +} + +func NewMockDB() *MockDB { + return &MockDB{ + applications: make(map[string]models.Application), + } +} + +func (m *MockDB) GetSchemes(filter models.SchemeFilter) ([]models.Scheme, error) { + var filtered []models.Scheme + for _, s := range m.schemes { + if matchesFilter(s, filter) { + filtered = append(filtered, s) + } + } + return filtered, nil +} + +func (m *MockDB) GetSchemeByID(id string) (*models.Scheme, error) { + for _, s := range m.schemes { + if s.ID == id { + return &s, nil + } + } + return nil, nil +} + +func (m *MockDB) SaveApplication(app models.Application) error { + m.applications[app.ID] = app + return nil +} + +func (m *MockDB) GetApplicationStatus(id string) (*models.ApplicationStatus, error) { + if app, exists := m.applications[id]; exists { + return &models.ApplicationStatus{ + ApplicationID: app.ID, + Status: app.Status, + UpdatedAt: app.LastUpdatedAt, + }, nil + } + return nil, nil +} + +func (m *MockDB) AddScheme(scheme models.Scheme) { + m.schemes = append(m.schemes, scheme) +} + +func matchesFilter(s models.Scheme, f models.SchemeFilter) bool { + if f.Provider != "" && f.Provider != s.Provider { + return false + } + if f.MinAmount > 0 && s.Amount < f.MinAmount { + return false + } + if f.MaxAmount > 0 && s.Amount > f.MaxAmount { + return false + } + if f.Status != "" && f.Status != s.Status { + return false + } + return true +} + +// ConfigurePool implements the DB interface +func (m *MockDB) ConfigurePool(maxOpen, maxIdle int) error { + return nil +} \ No newline at end of file diff --git a/beneficiary/internal/db/postgres.go b/beneficiary/internal/db/postgres.go new file mode 100644 index 0000000..200e0fa --- /dev/null +++ b/beneficiary/internal/db/postgres.go @@ -0,0 +1,148 @@ +package db + +import ( + "database/sql" + "fmt" + "time" + "github.com/VinVorteX/beneficiary-manager/internal/db/migrate" + "github.com/VinVorteX/beneficiary-manager/internal/logger" + "github.com/VinVorteX/beneficiary-manager/internal/models" + + _ "github.com/lib/pq" +) + +type PostgresDB struct { + db *sql.DB + logger logger.Logger +} + +func NewPostgresDB(connStr, migrationsPath string, logger logger.Logger) (*PostgresDB, error) { + db, err := sql.Open("postgres", connStr) + if err != nil { + logger.Error("Failed to connect to database", err, nil) + return nil, fmt.Errorf("failed to connect to database: %v", err) + } + + if err = db.Ping(); err != nil { + logger.Error("Failed to ping database", err, nil) + return nil, fmt.Errorf("failed to ping database: %v", err) + } + + logger.Info("Successfully connected to database", nil) + + // Run migrations + if err := migrate.MigrateDB(db, migrationsPath); err != nil { + return nil, fmt.Errorf("failed to run migrations: %v", err) + } + + return &PostgresDB{db: db, logger: logger}, nil +} + +func (p *PostgresDB) GetSchemes(filter models.SchemeFilter) ([]models.Scheme, error) { + p.logger.Debug("Executing GetSchemes query", map[string]interface{}{ + "filter": filter, + }) + + query := `SELECT id, name, description, provider, criteria, amount, start_date, end_date, status + FROM schemes + WHERE ($1 = '' OR provider = $1) + AND ($2 = 0 OR amount >= $2) + AND ($3 = 0 OR amount <= $3) + AND ($4 = '' OR status = $4)` + + rows, err := p.db.Query(query, filter.Provider, filter.MinAmount, filter.MaxAmount, filter.Status) + if err != nil { + return nil, fmt.Errorf("failed to query schemes: %v", err) + } + defer rows.Close() + + var schemes []models.Scheme + for rows.Next() { + var s models.Scheme + if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Provider, &s.Criteria, + &s.Amount, &s.StartDate, &s.EndDate, &s.Status); err != nil { + return nil, fmt.Errorf("failed to scan scheme row: %v", err) + } + schemes = append(schemes, s) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating scheme rows: %v", err) + } + + return schemes, nil +} + +// Add implementation for GetSchemeByID +func (p *PostgresDB) GetSchemeByID(id string) (*models.Scheme, error) { + query := `SELECT id, name, description, provider, criteria, amount, start_date, end_date, status + FROM schemes + WHERE id = $1` + + var scheme models.Scheme + err := p.db.QueryRow(query, id).Scan( + &scheme.ID, &scheme.Name, &scheme.Description, &scheme.Provider, + &scheme.Criteria, &scheme.Amount, &scheme.StartDate, &scheme.EndDate, &scheme.Status, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get scheme: %v", err) + } + + return &scheme, nil +} + +// Add implementation for SaveApplication +func (p *PostgresDB) SaveApplication(app models.Application) error { + query := `INSERT INTO applications (id, scheme_id, applicant_id, status, credentials, submitted_at, last_updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)` + + _, err := p.db.Exec(query, + app.ID, app.SchemeID, app.ApplicantID, app.Status, + app.Credentials, app.SubmittedAt, app.LastUpdatedAt, + ) + if err != nil { + return fmt.Errorf("failed to save application: %v", err) + } + + return nil +} + +// Add implementation for GetApplicationStatus +func (p *PostgresDB) GetApplicationStatus(applicationID string) (*models.ApplicationStatus, error) { + query := `SELECT id, status, last_updated_at + FROM applications + WHERE id = $1` + + var status models.ApplicationStatus + err := p.db.QueryRow(query, applicationID).Scan( + &status.ApplicationID, &status.Status, &status.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get application status: %v", err) + } + + return &status, nil +} + +// ConfigurePool sets up the database connection pool +func (p *PostgresDB) ConfigurePool(maxOpen, maxIdle int) error { + p.db.SetMaxOpenConns(maxOpen) + p.db.SetMaxIdleConns(maxIdle) + p.db.SetConnMaxLifetime(time.Hour) + + p.logger.Info("Configured database connection pool", map[string]interface{}{ + "max_open_conns": maxOpen, + "max_idle_conns": maxIdle, + }) + return nil +} + +// Add other necessary database methods diff --git a/beneficiary/internal/logger/logger.go b/beneficiary/internal/logger/logger.go new file mode 100644 index 0000000..abc26dd --- /dev/null +++ b/beneficiary/internal/logger/logger.go @@ -0,0 +1,86 @@ +package logger + +import ( + "fmt" + "os" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// Initialize sets up the global logger with the specified configuration +func Initialize(level string, prettyPrint bool) error { + // Parse log level + logLevel, err := zerolog.ParseLevel(level) + if err != nil { + return fmt.Errorf("invalid log level %q: %v", level, err) + } + zerolog.SetGlobalLevel(logLevel) + + // Configure logger output + if prettyPrint { + log.Logger = log.Output(zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + }) + } + + return nil +} + +// Logger interface defines the logging methods +type Logger interface { + Info(msg string, fields map[string]interface{}) + Error(msg string, err error, fields map[string]interface{}) + Debug(msg string, fields map[string]interface{}) + Warn(msg string, fields map[string]interface{}) +} + +// AppLogger implements the Logger interface using zerolog +type AppLogger struct { + logger zerolog.Logger +} + +// NewLogger creates a new AppLogger instance +func NewLogger() *AppLogger { + return &AppLogger{ + logger: log.With().Caller().Logger(), + } +} + +// Info logs an info level message +func (l *AppLogger) Info(msg string, fields map[string]interface{}) { + event := l.logger.Info() + for k, v := range fields { + event = event.Interface(k, v) + } + event.Msg(msg) +} + +// Error logs an error level message +func (l *AppLogger) Error(msg string, err error, fields map[string]interface{}) { + event := l.logger.Error().Err(err) + for k, v := range fields { + event = event.Interface(k, v) + } + event.Msg(msg) +} + +// Debug logs a debug level message +func (l *AppLogger) Debug(msg string, fields map[string]interface{}) { + event := l.logger.Debug() + for k, v := range fields { + event = event.Interface(k, v) + } + event.Msg(msg) +} + +// Warn logs a warning level message +func (l *AppLogger) Warn(msg string, fields map[string]interface{}) { + event := l.logger.Warn() + for k, v := range fields { + event = event.Interface(k, v) + } + event.Msg(msg) +} diff --git a/beneficiary/internal/logger/mock/mock.go b/beneficiary/internal/logger/mock/mock.go new file mode 100644 index 0000000..166a06a --- /dev/null +++ b/beneficiary/internal/logger/mock/mock.go @@ -0,0 +1,41 @@ +package mock + +type MockLogger struct { + InfoCalls []struct{ Msg string; Fields map[string]interface{} } + ErrorCalls []struct{ Msg string; Err error; Fields map[string]interface{} } + WarnCalls []struct{ Msg string; Fields map[string]interface{} } + DebugCalls []struct{ Msg string; Fields map[string]interface{} } +} + +func NewMockLogger() *MockLogger { + return &MockLogger{} +} + +func (m *MockLogger) Info(msg string, fields map[string]interface{}) { + m.InfoCalls = append(m.InfoCalls, struct { + Msg string + Fields map[string]interface{} + }{msg, fields}) +} + +func (m *MockLogger) Error(msg string, err error, fields map[string]interface{}) { + m.ErrorCalls = append(m.ErrorCalls, struct { + Msg string + Err error + Fields map[string]interface{} + }{msg, err, fields}) +} + +func (m *MockLogger) Debug(msg string, fields map[string]interface{}) { + m.DebugCalls = append(m.DebugCalls, struct { + Msg string + Fields map[string]interface{} + }{msg, fields}) +} + +func (m *MockLogger) Warn(msg string, fields map[string]interface{}) { + m.WarnCalls = append(m.WarnCalls, struct { + Msg string + Fields map[string]interface{} + }{msg, fields}) +} \ No newline at end of file diff --git a/beneficiary/internal/models/application.go b/beneficiary/internal/models/application.go new file mode 100644 index 0000000..ae7eb37 --- /dev/null +++ b/beneficiary/internal/models/application.go @@ -0,0 +1,18 @@ +package models + +type Application struct { + ID string `json:"id"` + SchemeID string `json:"scheme_id"` + ApplicantID string `json:"applicant_id"` + Status string `json:"status"` + Credentials map[string]interface{} `json:"credentials"` + SubmittedAt string `json:"submitted_at"` + LastUpdatedAt string `json:"last_updated_at"` +} + +type ApplicationStatus struct { + ApplicationID string `json:"application_id"` + Status string `json:"status"` + Message string `json:"message"` + UpdatedAt string `json:"updated_at"` +} diff --git a/beneficiary/internal/models/scheme.go b/beneficiary/internal/models/scheme.go new file mode 100644 index 0000000..58e0c36 --- /dev/null +++ b/beneficiary/internal/models/scheme.go @@ -0,0 +1,20 @@ +package models + +type Scheme struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Provider string `json:"provider"` + Criteria []string `json:"criteria"` + Amount float64 `json:"amount"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + Status string `json:"status"` +} + +type SchemeFilter struct { + Provider string `query:"provider"` + MinAmount float64 `query:"min_amount"` + MaxAmount float64 `query:"max_amount"` + Status string `query:"status"` +} diff --git a/beneficiary/internal/models/validation.go b/beneficiary/internal/models/validation.go new file mode 100644 index 0000000..5cf48a9 --- /dev/null +++ b/beneficiary/internal/models/validation.go @@ -0,0 +1,128 @@ +package models + +import ( + "fmt" + "regexp" + "time" +) + +var ( + // Regex for basic ID format validation + idRegex = regexp.MustCompile(`^[a-zA-Z0-9-_]+$`) +) + +// ValidateSchemeFilter validates the scheme filter parameters +func (f *SchemeFilter) Validate() error { + if f.MinAmount < 0 { + return fmt.Errorf("min_amount cannot be negative") + } + if f.MaxAmount < 0 { + return fmt.Errorf("max_amount cannot be negative") + } + if f.MaxAmount > 0 && f.MinAmount > f.MaxAmount { + return fmt.Errorf("min_amount cannot be greater than max_amount") + } + if f.Status != "" && !isValidStatus(f.Status) { + return fmt.Errorf("invalid status value: %s", f.Status) + } + return nil +} + +// ValidateApplication validates the application data +func (a *Application) Validate() error { + if !idRegex.MatchString(a.SchemeID) { + return fmt.Errorf("invalid scheme_id format") + } + if !idRegex.MatchString(a.ApplicantID) { + return fmt.Errorf("invalid applicant_id format") + } + if a.Credentials == nil { + return fmt.Errorf("credentials cannot be nil") + } + if len(a.Credentials) == 0 { + return fmt.Errorf("credentials cannot be empty") + } + + // Validate required credentials + if err := validateCredentials(a.Credentials); err != nil { + return fmt.Errorf("invalid credentials: %v", err) + } + + return nil +} + +// ValidateScheme validates the scheme data +func (s *Scheme) Validate() error { + if !idRegex.MatchString(s.ID) { + return fmt.Errorf("invalid id format") + } + if s.Name == "" { + return fmt.Errorf("name is required") + } + if s.Provider == "" { + return fmt.Errorf("provider is required") + } + if s.Amount < 0 { + return fmt.Errorf("amount cannot be negative") + } + + // Validate dates + if err := validateDates(s.StartDate, s.EndDate); err != nil { + return err + } + + return nil +} + +// Helper functions + +func isValidStatus(status string) bool { + validStatuses := map[string]bool{ + "active": true, + "inactive": true, + "pending": true, + "approved": true, + "rejected": true, + "completed": true, + } + return validStatuses[status] +} + +func validateCredentials(creds map[string]interface{}) error { + requiredFields := []string{"aadhar", "pan"} // Add required fields as needed + + for _, field := range requiredFields { + if _, exists := creds[field]; !exists { + return fmt.Errorf("missing required credential: %s", field) + } + } + return nil +} + +func validateDates(start, end string) error { + if start == "" { + return fmt.Errorf("start_date is required") + } + + startDate, err := time.Parse("2006-01-02", start) + if err != nil { + return fmt.Errorf("invalid start_date format: use YYYY-MM-DD") + } + + if end != "" { + endDate, err := time.Parse("2006-01-02", end) + if err != nil { + return fmt.Errorf("invalid end_date format: use YYYY-MM-DD") + } + if endDate.Before(startDate) { + return fmt.Errorf("end_date cannot be before start_date") + } + } + + return nil +} + +// IsValidID checks if the provided ID matches the required format +func IsValidID(id string) bool { + return idRegex.MatchString(id) +} \ No newline at end of file diff --git a/beneficiary/internal/models/validation_test.go b/beneficiary/internal/models/validation_test.go new file mode 100644 index 0000000..843ff18 --- /dev/null +++ b/beneficiary/internal/models/validation_test.go @@ -0,0 +1,117 @@ +package models + +import ( + "testing" + "time" +) + +func TestSchemeValidation(t *testing.T) { + tests := []struct { + name string + scheme Scheme + expectError bool + }{ + { + name: "Valid scheme", + scheme: Scheme{ + ID: "scheme1", + Name: "Test Scheme", + Provider: "gov", + Amount: 1000, + StartDate: time.Now().Format("2006-01-02"), + Status: "active", + }, + expectError: false, + }, + { + name: "Invalid ID", + scheme: Scheme{ + ID: "", + Name: "Test Scheme", + Provider: "gov", + StartDate: time.Now().Format("2006-01-02"), + }, + expectError: true, + }, + { + name: "Invalid date format", + scheme: Scheme{ + ID: "scheme1", + Name: "Test Scheme", + Provider: "gov", + StartDate: "invalid-date", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.scheme.Validate() + + if tt.expectError && err == nil { + t.Error("expected error but got none") + } + + if !tt.expectError && err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} + +func TestApplicationValidation(t *testing.T) { + tests := []struct { + name string + application Application + expectError bool + }{ + { + name: "Valid application", + application: Application{ + SchemeID: "scheme1", + ApplicantID: "user1", + Credentials: map[string]interface{}{ + "aadhar": "1234", + "pan": "ABCD1234", + }, + }, + expectError: false, + }, + { + name: "Missing credentials", + application: Application{ + SchemeID: "scheme1", + ApplicantID: "user1", + Credentials: nil, + }, + expectError: true, + }, + { + name: "Missing required credential", + application: Application{ + SchemeID: "scheme1", + ApplicantID: "user1", + Credentials: map[string]interface{}{ + "aadhar": "1234", + // Missing PAN + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.application.Validate() + + if tt.expectError && err == nil { + t.Error("expected error but got none") + } + + if !tt.expectError && err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} diff --git a/beneficiary/internal/service/beneficiary.go b/beneficiary/internal/service/beneficiary.go new file mode 100644 index 0000000..0c4fc29 --- /dev/null +++ b/beneficiary/internal/service/beneficiary.go @@ -0,0 +1,129 @@ +package service + +import ( + "fmt" + "time" + "github.com/VinVorteX/beneficiary-manager/internal/db" + "github.com/VinVorteX/beneficiary-manager/internal/logger" + "github.com/VinVorteX/beneficiary-manager/internal/models" +) + +type BeneficiaryService struct { + db db.DB + logger logger.Logger +} + +func NewBeneficiaryService(db db.DB, logger logger.Logger) *BeneficiaryService { + return &BeneficiaryService{ + db: db, + logger: logger, + } +} + +func (s *BeneficiaryService) GetSchemes(filter models.SchemeFilter) ([]models.Scheme, error) { + s.logger.Debug("Getting schemes with filter", map[string]interface{}{ + "provider": filter.Provider, + "min_amount": filter.MinAmount, + "max_amount": filter.MaxAmount, + "status": filter.Status, + }) + + if err := filter.Validate(); err != nil { + s.logger.Error("Invalid filter parameters", err, nil) + return nil, ErrInvalidFilter + } + + schemes, err := s.db.GetSchemes(filter) + if err != nil { + s.logger.Error("Failed to fetch schemes", err, nil) + return nil, err + } + + for _, scheme := range schemes { + if err := scheme.Validate(); err != nil { + s.logger.Error("Invalid scheme data in database", err, map[string]interface{}{ + "scheme_id": scheme.ID, + }) + return nil, fmt.Errorf("invalid scheme data in database: %v", err) + } + } + + s.logger.Info("Successfully fetched schemes", map[string]interface{}{ + "count": len(schemes), + }) + return schemes, nil +} + +func (s *BeneficiaryService) SubmitApplication(app models.Application) error { + s.logger.Debug("Submitting application", map[string]interface{}{ + "scheme_id": app.SchemeID, + "applicant_id": app.ApplicantID, + }) + + if err := app.Validate(); err != nil { + s.logger.Error("Invalid application data", err, nil) + return ErrInvalidApplication + } + + scheme, err := s.db.GetSchemeByID(app.SchemeID) + if err != nil { + s.logger.Error("Failed to fetch scheme", err, map[string]interface{}{ + "scheme_id": app.SchemeID, + }) + return err + } + if scheme == nil { + s.logger.Warn("Scheme not found", map[string]interface{}{ + "scheme_id": app.SchemeID, + }) + return ErrSchemeNotFound + } + + currentDate := time.Now().Format("2006-01-02") + if scheme.Status != "active" { + s.logger.Warn("Attempt to apply for inactive scheme", map[string]interface{}{ + "scheme_id": app.SchemeID, + "status": scheme.Status, + }) + return ErrSchemeInactive + } + + if scheme.EndDate != "" && scheme.EndDate < currentDate { + s.logger.Warn("Attempt to apply for expired scheme", map[string]interface{}{ + "scheme_id": app.SchemeID, + "end_date": scheme.EndDate, + }) + return ErrSchemeExpired + } + + // Set metadata + app.Status = "pending" + app.SubmittedAt = time.Now().Format(time.RFC3339) + app.LastUpdatedAt = app.SubmittedAt + + if err := s.db.SaveApplication(app); err != nil { + s.logger.Error("Failed to save application", err, map[string]interface{}{ + "scheme_id": app.SchemeID, + "applicant_id": app.ApplicantID, + }) + return err + } + + s.logger.Info("Application submitted successfully", map[string]interface{}{ + "application_id": app.ID, + "scheme_id": app.SchemeID, + "applicant_id": app.ApplicantID, + }) + return nil +} + +func (s *BeneficiaryService) GetApplicationStatus(applicationID string) (models.ApplicationStatus, error) { + status, err := s.db.GetApplicationStatus(applicationID) + if err != nil { + return models.ApplicationStatus{}, err + } + if status == nil { + return models.ApplicationStatus{}, ErrApplicationNotFound + } + return *status, nil +} diff --git a/beneficiary/internal/service/beneficiary_test.go b/beneficiary/internal/service/beneficiary_test.go new file mode 100644 index 0000000..a618813 --- /dev/null +++ b/beneficiary/internal/service/beneficiary_test.go @@ -0,0 +1,121 @@ +package service + +import ( + "testing" + "time" + "github.com/VinVorteX/beneficiary-manager/internal/db/mock" + loggerMock "github.com/VinVorteX/beneficiary-manager/internal/logger/mock" + "github.com/VinVorteX/beneficiary-manager/internal/models" +) + +func TestGetSchemes(t *testing.T) { + mockDB := mock.NewMockDB() + mockLogger := loggerMock.NewMockLogger() + service := NewBeneficiaryService(mockDB, mockLogger) + + // Add test schemes + mockDB.AddScheme(models.Scheme{ + ID: "scheme1", + Name: "Test Scheme 1", + Provider: "gov", + Amount: 1000, + Status: "active", + StartDate: time.Now().Format("2006-01-02"), + }) + + tests := []struct { + name string + filter models.SchemeFilter + expectedCount int + expectError bool + }{ + { + name: "No filter", + filter: models.SchemeFilter{}, + expectedCount: 1, + expectError: false, + }, + { + name: "Invalid amount filter", + filter: models.SchemeFilter{ + MinAmount: 2000, + MaxAmount: 1000, + }, + expectedCount: 0, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + schemes, err := service.GetSchemes(tt.filter) + + if tt.expectError && err == nil { + t.Error("expected error but got none") + } + + if !tt.expectError && err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(schemes) != tt.expectedCount { + t.Errorf("expected %d schemes, got %d", tt.expectedCount, len(schemes)) + } + }) + } +} + +func TestSubmitApplication(t *testing.T) { + mockDB := mock.NewMockDB() + mockLogger := loggerMock.NewMockLogger() + service := NewBeneficiaryService(mockDB, mockLogger) + + // Add active scheme + mockDB.AddScheme(models.Scheme{ + ID: "scheme1", + Status: "active", + StartDate: time.Now().Format("2006-01-02"), + }) + + tests := []struct { + name string + application models.Application + expectError bool + }{ + { + name: "Valid application", + application: models.Application{ + ID: "app1", + SchemeID: "scheme1", + ApplicantID: "user1", + Credentials: map[string]interface{}{ + "aadhar": "1234", + "pan": "ABCD1234", + }, + }, + expectError: false, + }, + { + name: "Invalid scheme ID", + application: models.Application{ + SchemeID: "nonexistent", + ApplicantID: "user1", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := service.SubmitApplication(tt.application) + + if tt.expectError && err == nil { + t.Error("expected error but got none") + } + + if !tt.expectError && err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} diff --git a/beneficiary/internal/service/errors.go b/beneficiary/internal/service/errors.go new file mode 100644 index 0000000..981f442 --- /dev/null +++ b/beneficiary/internal/service/errors.go @@ -0,0 +1,12 @@ +package service + +import "errors" + +var ( + ErrInvalidFilter = errors.New("invalid filter parameters") + ErrInvalidApplication = errors.New("invalid application data") + ErrSchemeNotFound = errors.New("scheme not found") + ErrApplicationNotFound = errors.New("application not found") + ErrSchemeInactive = errors.New("scheme is not active") + ErrSchemeExpired = errors.New("scheme has expired") +) diff --git a/beneficiary/migrations/postgres/000001_create_schemes_table.down.sql b/beneficiary/migrations/postgres/000001_create_schemes_table.down.sql new file mode 100644 index 0000000..d9726a1 --- /dev/null +++ b/beneficiary/migrations/postgres/000001_create_schemes_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS schemes; \ No newline at end of file diff --git a/beneficiary/migrations/postgres/000001_create_schemes_table.up.sql b/beneficiary/migrations/postgres/000001_create_schemes_table.up.sql new file mode 100644 index 0000000..2de2716 --- /dev/null +++ b/beneficiary/migrations/postgres/000001_create_schemes_table.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS schemes ( + id VARCHAR(50) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + provider VARCHAR(100) NOT NULL, + criteria TEXT[], + amount DECIMAL(12,2) NOT NULL, + start_date DATE NOT NULL, + end_date DATE, + status VARCHAR(20) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_schemes_provider ON schemes(provider); +CREATE INDEX idx_schemes_status ON schemes(status); +CREATE INDEX idx_schemes_amount ON schemes(amount); \ No newline at end of file diff --git a/beneficiary/migrations/postgres/000002_create_applications_table.down.sql b/beneficiary/migrations/postgres/000002_create_applications_table.down.sql new file mode 100644 index 0000000..16381f8 --- /dev/null +++ b/beneficiary/migrations/postgres/000002_create_applications_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS applications; \ No newline at end of file diff --git a/beneficiary/migrations/postgres/000002_create_applications_table.up.sql b/beneficiary/migrations/postgres/000002_create_applications_table.up.sql new file mode 100644 index 0000000..3d81a07 --- /dev/null +++ b/beneficiary/migrations/postgres/000002_create_applications_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS applications ( + id VARCHAR(50) PRIMARY KEY, + scheme_id VARCHAR(50) NOT NULL REFERENCES schemes(id), + applicant_id VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL, + credentials JSONB NOT NULL, + submitted_at TIMESTAMP WITH TIME ZONE NOT NULL, + last_updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_applications_scheme_id ON applications(scheme_id); +CREATE INDEX idx_applications_applicant_id ON applications(applicant_id); +CREATE INDEX idx_applications_status ON applications(status); \ No newline at end of file From d72c5a567e8b39ccf25ee468965cdb1970ebaf42 Mon Sep 17 00:00:00 2001 From: vinayak3010 Date: Fri, 25 Apr 2025 23:59:59 +0530 Subject: [PATCH 2/2] fixed the YAML validation key appeared malformed as suggested --- .github/ISSUE_TEMPLATE/c4gt_community.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/c4gt_community.yml b/.github/ISSUE_TEMPLATE/c4gt_community.yml index fd7cc2a..a6bc662 100644 --- a/.github/ISSUE_TEMPLATE/c4gt_community.yml +++ b/.github/ISSUE_TEMPLATE/c4gt_community.yml @@ -8,7 +8,7 @@ body: - type: textarea id: ticket-description validations: - required: truename: "C4GT Open Community Template" + required: true description: "Create a new Ticket for C4GT Open Community" title: "[C4GT Community]: " labels: