add dukcapil
This commit is contained in:
parent
9a975d146e
commit
bc64eb20ea
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"*",
|
||||||
|
"Bash(*)",
|
||||||
|
"Bash(cd:*)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(go mod:*)",
|
||||||
|
"Bash(go build:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ config/env/*
|
|||||||
!.env
|
!.env
|
||||||
|
|
||||||
vendor
|
vendor
|
||||||
|
infra/*.yaml
|
||||||
|
!infra/*.yaml.example
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
stages:
|
|
||||||
- build
|
|
||||||
- staging
|
|
||||||
|
|
||||||
build_image:
|
|
||||||
stage: build
|
|
||||||
image: docker:20.10.9
|
|
||||||
services:
|
|
||||||
- docker:20.10.9-dind
|
|
||||||
before_script:
|
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
||||||
script:
|
|
||||||
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
|
|
||||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
|
|
||||||
deploy_to_staging:
|
|
||||||
stage: staging
|
|
||||||
image:
|
|
||||||
name: bitnami/kubectl
|
|
||||||
entrypoint: [""]
|
|
||||||
script:
|
|
||||||
- echo "$KUBECONFIG_BASE64" | base64 -d > ./kubeconfig
|
|
||||||
- export KUBECONFIG=$(pwd)/kubeconfig
|
|
||||||
- sed -i "s/<VERSION>/$CI_COMMIT_SHORT_SHA/" k8s/staging/deployment.yaml
|
|
||||||
- kubectl apply -f k8s/staging/namespace.yaml
|
|
||||||
- kubectl apply -f k8s/staging/deployment.yaml
|
|
||||||
- kubectl apply -f k8s/staging/service.yaml
|
|
||||||
- kubectl apply -f k8s/staging/ingress.yaml
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
BIN
API 1_N.docx
Normal file
BIN
API 1_N.docx
Normal file
Binary file not shown.
320
DOCKER.md
320
DOCKER.md
@ -1,320 +0,0 @@
|
|||||||
# Docker Setup for APSKEL POS Backend
|
|
||||||
|
|
||||||
This document describes how to run the APSKEL POS Backend using Docker and Docker Compose.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Docker (version 20.10 or later)
|
|
||||||
- Docker Compose (version 2.0 or later)
|
|
||||||
- Git (for cloning the repository)
|
|
||||||
- Go 1.21+ (for local development)
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### 1. Build and Run Production Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build and start all services
|
|
||||||
./docker-build.sh run
|
|
||||||
|
|
||||||
# Or manually:
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will be available at:
|
|
||||||
- **Backend API**: http://localhost:3300
|
|
||||||
- **Database**: localhost:5432
|
|
||||||
- **Redis**: localhost:6379
|
|
||||||
|
|
||||||
### 2. Development Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start development environment with live reload
|
|
||||||
./docker-build.sh dev
|
|
||||||
|
|
||||||
# Or manually:
|
|
||||||
docker-compose --profile dev up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Development environment provides:
|
|
||||||
- **Backend API (Dev)**: http://localhost:3001 (with live reload)
|
|
||||||
- **Backend API (Prod)**: http://localhost:3300
|
|
||||||
- Auto-restart on code changes using Air
|
|
||||||
|
|
||||||
### 3. Database Migrations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run database migrations
|
|
||||||
./docker-build.sh migrate
|
|
||||||
|
|
||||||
# Or manually:
|
|
||||||
docker-compose --profile migrate up migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Script Usage
|
|
||||||
|
|
||||||
The `docker-build.sh` script provides convenient commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build Docker image
|
|
||||||
./docker-build.sh build
|
|
||||||
|
|
||||||
# Build and run production environment
|
|
||||||
./docker-build.sh run
|
|
||||||
|
|
||||||
# Start development environment
|
|
||||||
./docker-build.sh dev
|
|
||||||
|
|
||||||
# Run database migrations
|
|
||||||
./docker-build.sh migrate
|
|
||||||
|
|
||||||
# Stop all containers
|
|
||||||
./docker-build.sh stop
|
|
||||||
|
|
||||||
# Clean up containers and images
|
|
||||||
./docker-build.sh clean
|
|
||||||
|
|
||||||
# Show container logs
|
|
||||||
./docker-build.sh logs
|
|
||||||
|
|
||||||
# Show help
|
|
||||||
./docker-build.sh help
|
|
||||||
```
|
|
||||||
|
|
||||||
## Services
|
|
||||||
|
|
||||||
### Backend API
|
|
||||||
- **Port**: 3300 (production), 3001 (development)
|
|
||||||
- **Health Check**: http://localhost:3300/health
|
|
||||||
- **Environment**: Configurable via `infra/` directory
|
|
||||||
- **User**: Runs as non-root user for security
|
|
||||||
|
|
||||||
### PostgreSQL Database
|
|
||||||
- **Port**: 5432
|
|
||||||
- **Database**: apskel_pos
|
|
||||||
- **Username**: apskel
|
|
||||||
- **Password**: See docker-compose.yaml
|
|
||||||
- **Volumes**: Persistent data storage
|
|
||||||
|
|
||||||
### Redis Cache
|
|
||||||
- **Port**: 6379
|
|
||||||
- **Usage**: Caching and session storage
|
|
||||||
- **Volumes**: Persistent data storage
|
|
||||||
|
|
||||||
## Environment Configuration
|
|
||||||
|
|
||||||
The application uses configuration files from the `infra/` directory:
|
|
||||||
|
|
||||||
- `infra/development.yaml` - Development configuration
|
|
||||||
- `infra/production.yaml` - Production configuration (create if needed)
|
|
||||||
|
|
||||||
### Configuration Structure
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server:
|
|
||||||
port: 3300
|
|
||||||
|
|
||||||
postgresql:
|
|
||||||
host: postgres # Use service name in Docker
|
|
||||||
port: 5432
|
|
||||||
db: apskel_pos
|
|
||||||
username: apskel
|
|
||||||
password: 7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
|
|
||||||
|
|
||||||
jwt:
|
|
||||||
token:
|
|
||||||
secret: "your-jwt-secret"
|
|
||||||
expires-ttl: 1440
|
|
||||||
|
|
||||||
s3:
|
|
||||||
access_key_id: "your-s3-key"
|
|
||||||
access_key_secret: "your-s3-secret"
|
|
||||||
endpoint: "your-s3-endpoint"
|
|
||||||
bucket_name: "your-bucket"
|
|
||||||
|
|
||||||
log:
|
|
||||||
log_level: "info"
|
|
||||||
log_format: "json"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Compose Profiles
|
|
||||||
|
|
||||||
### Default Profile (Production)
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
Starts: postgres, redis, backend
|
|
||||||
|
|
||||||
### Development Profile
|
|
||||||
```bash
|
|
||||||
docker-compose --profile dev up -d
|
|
||||||
```
|
|
||||||
Starts: postgres, redis, backend, backend-dev
|
|
||||||
|
|
||||||
### Migration Profile
|
|
||||||
```bash
|
|
||||||
docker-compose --profile migrate up migrate
|
|
||||||
```
|
|
||||||
Runs: database migrations
|
|
||||||
|
|
||||||
## Health Checks
|
|
||||||
|
|
||||||
All services include health checks:
|
|
||||||
|
|
||||||
- **Backend**: HTTP GET /health
|
|
||||||
- **PostgreSQL**: pg_isready command
|
|
||||||
- **Redis**: Redis ping command
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
View logs for specific services:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# All services
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Backend only
|
|
||||||
docker-compose logs -f backend
|
|
||||||
|
|
||||||
# Database only
|
|
||||||
docker-compose logs -f postgres
|
|
||||||
|
|
||||||
# Development backend
|
|
||||||
docker-compose logs -f backend-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Volumes
|
|
||||||
|
|
||||||
### Persistent Volumes
|
|
||||||
- `postgres_data`: Database files
|
|
||||||
- `redis_data`: Redis persistence files
|
|
||||||
- `go_modules`: Go module cache (development)
|
|
||||||
|
|
||||||
### Bind Mounts
|
|
||||||
- `./infra:/infra:ro`: Configuration files (read-only)
|
|
||||||
- `./migrations:/app/migrations:ro`: Database migrations (read-only)
|
|
||||||
- `.:/app`: Source code (development only)
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
### Production Security Features
|
|
||||||
- Non-root user execution
|
|
||||||
- Read-only configuration mounts
|
|
||||||
- Minimal base image (Debian slim)
|
|
||||||
- Health checks for monitoring
|
|
||||||
- Resource limits (configurable)
|
|
||||||
|
|
||||||
### Network Security
|
|
||||||
- Internal Docker network isolation
|
|
||||||
- Only necessary ports exposed
|
|
||||||
- Service-to-service communication via Docker network
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Go Version Compatibility Error**
|
|
||||||
```bash
|
|
||||||
# Error: package slices is not in GOROOT
|
|
||||||
# Solution: Make sure Dockerfile uses Go 1.21+
|
|
||||||
# Check go.mod file requires Go 1.21 or later
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Port Already in Use**
|
|
||||||
```bash
|
|
||||||
# Check what's using the port
|
|
||||||
lsof -i :3300
|
|
||||||
|
|
||||||
# Change ports in docker-compose.yaml if needed
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Database Connection Failed**
|
|
||||||
```bash
|
|
||||||
# Check if database is running
|
|
||||||
docker-compose ps postgres
|
|
||||||
|
|
||||||
# Check database logs
|
|
||||||
docker-compose logs postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Permission Denied**
|
|
||||||
```bash
|
|
||||||
# Make sure script is executable
|
|
||||||
chmod +x docker-build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Out of Disk Space**
|
|
||||||
```bash
|
|
||||||
# Clean up unused Docker resources
|
|
||||||
docker system prune -a
|
|
||||||
|
|
||||||
# Remove old images
|
|
||||||
docker image prune -a
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Mode
|
|
||||||
|
|
||||||
Run containers in debug mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start with debug logs
|
|
||||||
ENV_MODE=development docker-compose up
|
|
||||||
|
|
||||||
# Enter running container
|
|
||||||
docker-compose exec backend sh
|
|
||||||
|
|
||||||
# Check application logs
|
|
||||||
docker-compose logs -f backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Tuning
|
|
||||||
|
|
||||||
For production deployment:
|
|
||||||
|
|
||||||
1. **Resource Limits**: Add resource limits to docker-compose.yaml
|
|
||||||
2. **Environment**: Use production configuration
|
|
||||||
3. **Logging**: Adjust log levels
|
|
||||||
4. **Health Checks**: Tune intervals for your needs
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
backend:
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1.0'
|
|
||||||
memory: 512M
|
|
||||||
reservations:
|
|
||||||
cpus: '0.5'
|
|
||||||
memory: 256M
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Testing
|
|
||||||
|
|
||||||
Once the application is running, test the API:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Health check
|
|
||||||
curl http://localhost:3300/health
|
|
||||||
|
|
||||||
# Analytics endpoint (requires authentication)
|
|
||||||
curl -H "Authorization: Bearer <token>" \
|
|
||||||
-H "Organization-ID: <org-id>" \
|
|
||||||
"http://localhost:3300/api/v1/analytics/profit-loss?date_from=01-12-2023&date_to=31-12-2023"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
For production deployment:
|
|
||||||
|
|
||||||
1. Update configuration in `infra/production.yaml`
|
|
||||||
2. Set appropriate environment variables
|
|
||||||
3. Use production Docker Compose file
|
|
||||||
4. Configure reverse proxy (nginx, traefik)
|
|
||||||
5. Set up SSL certificates
|
|
||||||
6. Configure monitoring and logging
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Production deployment
|
|
||||||
ENV_MODE=production docker-compose -f docker-compose.prod.yaml up -d
|
|
||||||
```
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
# Table Restructuring Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This document summarizes the changes made to restructure the letter dispositions system from a single table to a more normalized structure with an association table.
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
### 1. Database Schema Changes
|
|
||||||
|
|
||||||
#### New Migration Files Created:
|
|
||||||
- `migrations/000012_rename_dispositions_table.up.sql` - Main migration to restructure tables
|
|
||||||
- `migrations/000012_rename_dispositions_table.down.sql` - Rollback migration
|
|
||||||
|
|
||||||
#### Table Changes:
|
|
||||||
- **`letter_dispositions`** → **`letter_incoming_dispositions`**
|
|
||||||
- Renamed table
|
|
||||||
- Removed columns: `from_user_id`, `to_user_id`, `to_department_id`, `status`, `completed_at`
|
|
||||||
- Renamed `from_department_id` → `department_id`
|
|
||||||
- Added `read_at` column
|
|
||||||
- Kept columns: `id`, `letter_id`, `department_id`, `notes`, `read_at`, `created_at`, `created_by`, `updated_at`
|
|
||||||
|
|
||||||
#### New Table Created:
|
|
||||||
- **`letter_incoming_dispositions_department`**
|
|
||||||
- Purpose: Associates dispositions with target departments
|
|
||||||
- Columns: `id`, `letter_incoming_disposition_id`, `department_id`, `created_at`
|
|
||||||
- Unique constraint on `(letter_incoming_disposition_id, department_id)`
|
|
||||||
|
|
||||||
### 2. Entity Changes
|
|
||||||
|
|
||||||
#### Updated Entities:
|
|
||||||
- **`LetterDisposition`** → **`LetterIncomingDisposition`**
|
|
||||||
- Simplified structure with only required fields
|
|
||||||
- New table name mapping
|
|
||||||
|
|
||||||
#### New Entity:
|
|
||||||
- **`LetterIncomingDispositionDepartment`**
|
|
||||||
- Represents the many-to-many relationship between dispositions and departments
|
|
||||||
|
|
||||||
### 3. Repository Changes
|
|
||||||
|
|
||||||
#### Updated Repositories:
|
|
||||||
- **`LetterDispositionRepository`** → **`LetterIncomingDispositionRepository`**
|
|
||||||
- Updated to work with new entity
|
|
||||||
|
|
||||||
#### New Repository:
|
|
||||||
- **`LetterIncomingDispositionDepartmentRepository`**
|
|
||||||
- Handles CRUD operations for the association table
|
|
||||||
- Methods: `CreateBulk`, `ListByDisposition`
|
|
||||||
|
|
||||||
### 4. Processor Changes
|
|
||||||
|
|
||||||
#### Updated Processor:
|
|
||||||
- **`LetterProcessorImpl`**
|
|
||||||
- Added new repository dependency
|
|
||||||
- Updated `CreateDispositions` method to:
|
|
||||||
- Create main disposition record
|
|
||||||
- Create department association records
|
|
||||||
- Maintain existing action selection functionality
|
|
||||||
|
|
||||||
### 5. Transformer Changes
|
|
||||||
|
|
||||||
#### Updated Transformer:
|
|
||||||
- **`DispositionsToContract`** function
|
|
||||||
- Updated to work with new entity structure
|
|
||||||
- Maps new fields: `DepartmentID`, `ReadAt`, `UpdatedAt`
|
|
||||||
- Removed old fields: `FromDepartmentID`, `ToDepartmentID`, `Status`
|
|
||||||
|
|
||||||
### 6. Contract Changes
|
|
||||||
|
|
||||||
#### Updated Contract:
|
|
||||||
- **`DispositionResponse`** struct
|
|
||||||
- Updated fields to match new entity structure
|
|
||||||
- Added `ReadAt` and `UpdatedAt` fields
|
|
||||||
- Replaced `FromDepartmentID` and `ToDepartmentID` with `DepartmentID`
|
|
||||||
|
|
||||||
### 7. Application Configuration Changes
|
|
||||||
|
|
||||||
#### Updated App Configuration:
|
|
||||||
- **`internal/app/app.go`**
|
|
||||||
- Updated repository initialization
|
|
||||||
- Added new repository dependency
|
|
||||||
- Updated processor initialization with new repository
|
|
||||||
|
|
||||||
## Migration Process
|
|
||||||
|
|
||||||
### Up Migration (000012_rename_dispositions_table.up.sql):
|
|
||||||
1. Rename `letter_dispositions` to `letter_incoming_dispositions`
|
|
||||||
2. Drop unnecessary columns
|
|
||||||
3. Rename `from_department_id` to `department_id`
|
|
||||||
4. Add missing columns (`read_at`, `updated_at`)
|
|
||||||
5. Create new association table
|
|
||||||
6. Update triggers and indexes
|
|
||||||
|
|
||||||
### Down Migration (000012_rename_dispositions_table.down.sql):
|
|
||||||
1. Drop association table
|
|
||||||
2. Restore removed columns
|
|
||||||
3. Rename `department_id` back to `from_department_id`
|
|
||||||
4. Restore old triggers and indexes
|
|
||||||
5. Rename table back to `letter_dispositions`
|
|
||||||
|
|
||||||
## Benefits of New Structure
|
|
||||||
|
|
||||||
1. **Normalization**: Separates disposition metadata from department associations
|
|
||||||
2. **Flexibility**: Allows multiple departments per disposition
|
|
||||||
3. **Cleaner Data Model**: Removes redundant fields and simplifies the main table
|
|
||||||
4. **Better Performance**: Smaller main table with focused indexes
|
|
||||||
5. **Easier Maintenance**: Clear separation of concerns
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
- Table name change from `letter_dispositions` to `letter_incoming_dispositions`
|
|
||||||
- Entity structure changes (removed fields, renamed fields)
|
|
||||||
- Repository interface changes
|
|
||||||
- API response structure changes
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
1. Run migration on test database
|
|
||||||
2. Test disposition creation with new structure
|
|
||||||
3. Verify department associations are created correctly
|
|
||||||
4. Test existing functionality (action selections, notes)
|
|
||||||
5. Verify rollback migration works correctly
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
If issues arise, the down migration will:
|
|
||||||
1. Restore the original table structure
|
|
||||||
2. Preserve all existing data
|
|
||||||
3. Remove the new association table
|
|
||||||
4. Restore original triggers and indexes
|
|
||||||
113
Makefile
113
Makefile
@ -1,9 +1,11 @@
|
|||||||
#PROJECT_NAME = "enaklo-pos-backend"
|
# Go Backend Template Makefile
|
||||||
DB_USERNAME :=eslogad_user
|
|
||||||
DB_PASSWORD :=M9u%24e%23jT2%40qR4pX%21zL
|
# Database configuration (update these for your project)
|
||||||
DB_HOST :=103.191.71.2
|
DB_USERNAME := postgres
|
||||||
DB_PORT :=5432
|
DB_PASSWORD := postgres
|
||||||
DB_NAME :=eslogad_db
|
DB_HOST := localhost
|
||||||
|
DB_PORT := 5432
|
||||||
|
DB_NAME := template_db
|
||||||
|
|
||||||
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
||||||
|
|
||||||
@ -19,45 +21,31 @@ help:
|
|||||||
@echo "Usage: make [command]"
|
@echo "Usage: make [command]"
|
||||||
@echo
|
@echo
|
||||||
@echo "Commands:"
|
@echo "Commands:"
|
||||||
@echo " rename-project name={name} Rename project"
|
@echo " build Build the application"
|
||||||
@echo
|
@echo " run Run the application"
|
||||||
@echo " build-http Build http server"
|
|
||||||
@echo
|
|
||||||
@echo " migration-create name={name} Create migration"
|
|
||||||
@echo " migration-up Up migrations"
|
|
||||||
@echo " migration-down Down last migration"
|
|
||||||
@echo
|
|
||||||
@echo " docker-up Up docker services"
|
|
||||||
@echo " docker-down Down docker services"
|
|
||||||
@echo
|
|
||||||
@echo " fmt Format source code"
|
|
||||||
@echo " test Run unit tests"
|
@echo " test Run unit tests"
|
||||||
|
@echo " fmt Format source code"
|
||||||
|
@echo
|
||||||
|
@echo " migration-create name={name} Create a new migration"
|
||||||
|
@echo " migration-up Run all pending migrations"
|
||||||
|
@echo " migration-down Rollback last migration"
|
||||||
|
@echo
|
||||||
|
@echo " docker-up Start docker services"
|
||||||
|
@echo " docker-down Stop docker services"
|
||||||
@echo
|
@echo
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
|
|
||||||
.SILENT: rename-project
|
.SILENT: build
|
||||||
rename-project:
|
build:
|
||||||
ifeq ($(name),)
|
@go build -o ./bin/server ./cmd/server/main.go
|
||||||
@echo 'new project name not set'
|
@echo "✓ Binary built: ./bin/server"
|
||||||
else
|
|
||||||
ifeq ($(DETECTED_OS),Darwin)
|
|
||||||
@grep -RiIl '$(PROJECT_NAME)' | xargs sed -i '' 's/$(PROJECT_NAME)/$(name)/g'
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifeq ($(DETECTED_OS),Linux)
|
# Run
|
||||||
@grep -RiIl '$(PROJECT_NAME)' | xargs sed -i 's/$(PROJECT_NAME)/$(name)/g'
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifeq ($(DETECTED_OS),Windows)
|
.SILENT: run
|
||||||
@grep 'target is not implemented on Windows platform'
|
run:
|
||||||
endif
|
@ENV_MODE=development go run cmd/server/main.go
|
||||||
endif
|
|
||||||
|
|
||||||
.SILENT: build-http
|
|
||||||
build-http:
|
|
||||||
@go build -o ./bin/http-server ./cmd/http/main.go
|
|
||||||
@echo executable file \"http-server\" saved in ./bin/http-server
|
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
|
|
||||||
@ -65,50 +53,61 @@ build-http:
|
|||||||
test:
|
test:
|
||||||
@go test ./... -v
|
@go test ./... -v
|
||||||
|
|
||||||
# Create migration
|
# Format
|
||||||
|
|
||||||
|
.SILENT: fmt
|
||||||
|
fmt:
|
||||||
|
@go fmt ./...
|
||||||
|
@echo "✓ Code formatted"
|
||||||
|
|
||||||
|
# Migrations
|
||||||
|
|
||||||
.SILENT: migration-create
|
.SILENT: migration-create
|
||||||
migration-create:
|
migration-create:
|
||||||
|
@if [ -z "$(name)" ]; then \
|
||||||
|
echo "Error: name parameter is required"; \
|
||||||
|
echo "Usage: make migration-create name=your_migration_name"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
@migrate create -ext sql -dir ./migrations -seq $(name)
|
@migrate create -ext sql -dir ./migrations -seq $(name)
|
||||||
|
@echo "✓ Migration created: $(name)"
|
||||||
# Up migration
|
|
||||||
|
|
||||||
.SILENT: migration-up
|
.SILENT: migration-up
|
||||||
migration-up:
|
migration-up:
|
||||||
@migrate -database $(DB_URL) -path ./migrations up
|
@migrate -database $(DB_URL) -path ./migrations up
|
||||||
|
@echo "✓ Migrations applied"
|
||||||
# Down migration
|
|
||||||
|
|
||||||
.SILENT: migration-down
|
.SILENT: migration-down
|
||||||
migration-down:
|
migration-down:
|
||||||
@migrate -database $(DB_URL) -path ./migrations down 1
|
@migrate -database $(DB_URL) -path ./migrations down 1
|
||||||
|
@echo "✓ Last migration rolled back"
|
||||||
.SILENT: seeder-create
|
|
||||||
seeder-create:
|
|
||||||
@migrate create -ext sql -dir ./seeders -seq $(name)
|
|
||||||
|
|
||||||
.SILENT: seeder-up
|
|
||||||
seeder-up:
|
|
||||||
@migrate -database $(DB_URL) -path ./seeders up
|
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
|
|
||||||
.SILENT: docker-up
|
.SILENT: docker-up
|
||||||
docker-up:
|
docker-up:
|
||||||
@docker-compose up -d
|
@docker-compose up -d
|
||||||
|
@echo "✓ Docker services started"
|
||||||
|
|
||||||
.SILENT: docker-down
|
.SILENT: docker-down
|
||||||
docker-down:
|
docker-down:
|
||||||
@docker-compose down
|
@docker-compose down
|
||||||
|
@echo "✓ Docker services stopped"
|
||||||
|
|
||||||
# Format
|
# Dependencies
|
||||||
|
|
||||||
.SILENT: fmt
|
.SILENT: deps
|
||||||
fmt:
|
deps:
|
||||||
@go fmt ./...
|
@go mod download
|
||||||
|
@go mod tidy
|
||||||
|
@echo "✓ Dependencies updated"
|
||||||
|
|
||||||
start:
|
# Clean
|
||||||
go run main.go --env-path .env
|
|
||||||
|
.SILENT: clean
|
||||||
|
clean:
|
||||||
|
@rm -rf ./bin
|
||||||
|
@echo "✓ Build artifacts cleaned"
|
||||||
|
|
||||||
# Default
|
# Default
|
||||||
|
|
||||||
|
|||||||
@ -1,278 +0,0 @@
|
|||||||
# Notification Service API Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The notification service integrates with Novu to manage subscribers and trigger notifications. It automatically creates/manages Novu subscribers based on user data and provides endpoints for triggering notifications.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Add your Novu credentials to `infra/development.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
novu:
|
|
||||||
api_key: 'your-novu-api-key' # Get from Novu dashboard
|
|
||||||
application_id: 'your-app-id' # Optional
|
|
||||||
base_url: 'https://api.novu.co' # Optional, defaults to Novu cloud
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
1. **Automatic Subscriber Management**
|
|
||||||
- Subscribers are automatically created when users are created
|
|
||||||
- Subscriber ID = User ID (UUID)
|
|
||||||
- Subscribers are updated when user data changes
|
|
||||||
- Subscribers are deleted when users are deleted
|
|
||||||
|
|
||||||
2. **On-Demand Subscriber Creation**
|
|
||||||
- If a subscriber doesn't exist when triggering a notification, it's created automatically
|
|
||||||
- Ensures notifications can always be sent
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Current User Endpoints (Authenticated users)
|
|
||||||
|
|
||||||
#### 1. Trigger Notification for Current User
|
|
||||||
```http
|
|
||||||
POST /api/v1/notifications/me/trigger
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
|
|
||||||
{
|
|
||||||
"template_id": "welcome-email",
|
|
||||||
"template_data": {
|
|
||||||
"username": "John Doe",
|
|
||||||
"action_url": "https://example.com/confirm"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"email": {
|
|
||||||
"subject": "Custom Subject",
|
|
||||||
"from": "custom@example.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Get Current User's Subscriber Info
|
|
||||||
```http
|
|
||||||
GET /api/v1/notifications/me/subscriber
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"subscriber_id": "uuid",
|
|
||||||
"email": "user@example.com",
|
|
||||||
"first_name": "John",
|
|
||||||
"data": {
|
|
||||||
"userId": "uuid",
|
|
||||||
"roles": [...],
|
|
||||||
"departments": [...]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Update Current User's Channel Credentials
|
|
||||||
```http
|
|
||||||
PUT /api/v1/notifications/me/channel
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
|
|
||||||
{
|
|
||||||
"channel": "push",
|
|
||||||
"credentials": {
|
|
||||||
"deviceTokens": ["token1", "token2"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Admin Endpoints (Requires `notification.admin` permission)
|
|
||||||
|
|
||||||
#### 1. Trigger Notification for Any User
|
|
||||||
```http
|
|
||||||
POST /api/v1/notifications/trigger
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
|
|
||||||
{
|
|
||||||
"user_id": "user-uuid",
|
|
||||||
"template_id": "order-status",
|
|
||||||
"template_data": {
|
|
||||||
"orderNumber": "12345",
|
|
||||||
"status": "shipped"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"email": "override@example.com",
|
|
||||||
"phone": "+1234567890"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Bulk Trigger Notifications
|
|
||||||
```http
|
|
||||||
POST /api/v1/notifications/bulk-trigger
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
|
|
||||||
{
|
|
||||||
"template_id": "announcement",
|
|
||||||
"user_ids": ["uuid1", "uuid2", "uuid3"],
|
|
||||||
"template_data": {
|
|
||||||
"message": "System maintenance scheduled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"total_sent": 3,
|
|
||||||
"total_failed": 0,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"user_id": "uuid1",
|
|
||||||
"success": true,
|
|
||||||
"transaction_id": "tx-123"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Get Any User's Subscriber Info
|
|
||||||
```http
|
|
||||||
GET /api/v1/notifications/subscribers/{userId}
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Update Any User's Channel Credentials
|
|
||||||
```http
|
|
||||||
PUT /api/v1/notifications/subscribers/channel
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
|
|
||||||
{
|
|
||||||
"user_id": "user-uuid",
|
|
||||||
"channel": "sms",
|
|
||||||
"credentials": {
|
|
||||||
"phoneNumber": "+1234567890"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notification Channels
|
|
||||||
|
|
||||||
Supported channels:
|
|
||||||
- `email` - Email notifications
|
|
||||||
- `sms` - SMS notifications
|
|
||||||
- `push` - Push notifications
|
|
||||||
- `in_app` - In-app notifications
|
|
||||||
- `chat` - Chat notifications (Slack, Discord, etc.)
|
|
||||||
|
|
||||||
## Template Data
|
|
||||||
|
|
||||||
Template data is passed to Novu templates for variable substitution:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"template_data": {
|
|
||||||
"username": "{{username}}",
|
|
||||||
"orderNumber": "{{orderNumber}}",
|
|
||||||
"customField": "{{customField}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Overrides
|
|
||||||
|
|
||||||
You can override notification content per channel:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"overrides": {
|
|
||||||
"email": {
|
|
||||||
"subject": "Custom Subject",
|
|
||||||
"body": "Custom HTML body",
|
|
||||||
"from": "noreply@company.com"
|
|
||||||
},
|
|
||||||
"sms": {
|
|
||||||
"content": "Custom SMS message",
|
|
||||||
"from": "+1234567890"
|
|
||||||
},
|
|
||||||
"push": {
|
|
||||||
"title": "Custom Title",
|
|
||||||
"content": "Custom push message"
|
|
||||||
},
|
|
||||||
"in_app": {
|
|
||||||
"content": "Custom in-app message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The service handles errors gracefully:
|
|
||||||
- Missing Novu configuration: Returns success=false with appropriate message
|
|
||||||
- Missing subscriber: Automatically creates subscriber before sending
|
|
||||||
- Failed notifications: Returns detailed error messages
|
|
||||||
|
|
||||||
## Integration with User Management
|
|
||||||
|
|
||||||
The notification service is integrated with user management:
|
|
||||||
|
|
||||||
1. **User Creation**: Automatically creates Novu subscriber
|
|
||||||
2. **User Update**: Updates subscriber data
|
|
||||||
3. **User Deletion**: Removes subscriber from Novu
|
|
||||||
4. **User Data Sync**: Subscriber includes user roles and departments
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Send Welcome Email
|
|
||||||
```javascript
|
|
||||||
// POST /api/v1/notifications/me/trigger
|
|
||||||
{
|
|
||||||
"template_id": "welcome",
|
|
||||||
"template_data": {
|
|
||||||
"firstName": "John",
|
|
||||||
"verificationUrl": "https://app.com/verify/123"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Send Order Confirmation
|
|
||||||
```javascript
|
|
||||||
// POST /api/v1/notifications/trigger
|
|
||||||
{
|
|
||||||
"user_id": "customer-uuid",
|
|
||||||
"template_id": "order-confirmation",
|
|
||||||
"template_data": {
|
|
||||||
"orderNumber": "ORD-2024-001",
|
|
||||||
"items": [...],
|
|
||||||
"total": "$99.99"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Send Bulk Announcement
|
|
||||||
```javascript
|
|
||||||
// POST /api/v1/notifications/bulk-trigger
|
|
||||||
{
|
|
||||||
"template_id": "system-announcement",
|
|
||||||
"user_ids": ["uuid1", "uuid2", "uuid3"],
|
|
||||||
"template_data": {
|
|
||||||
"title": "Scheduled Maintenance",
|
|
||||||
"message": "System will be down for maintenance on Sunday",
|
|
||||||
"startTime": "2024-01-14 00:00",
|
|
||||||
"endTime": "2024-01-14 04:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
1. Ensure Novu API key is configured
|
|
||||||
2. Create a test user
|
|
||||||
3. Create a notification template in Novu dashboard
|
|
||||||
4. Use the template ID to send test notifications
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
1. **"notification service not configured"**: Add Novu API key to configuration
|
|
||||||
2. **"failed to create subscriber"**: Check user data and Novu API key
|
|
||||||
3. **"template not found"**: Ensure template exists in Novu dashboard
|
|
||||||
4. **No notification received**: Check Novu dashboard for delivery status
|
|
||||||
608
README.md
608
README.md
@ -1,310 +1,342 @@
|
|||||||
<h1 align="center">
|
# Go Backend Template
|
||||||
<img height="80" width="160" src="./assets/gopher-icon.gif" alt="Go"><br>Backend Template
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
> Clean architecture based backend template in Go.
|
> A clean, production-ready Go backend template with authentication, user management, and best practices built-in.
|
||||||
|
|
||||||
## Makefile
|
## Features
|
||||||
|
|
||||||
Makefile requires installed dependecies:
|
- **Clean Architecture** - Separation of concerns with handler → service → processor → repository layers
|
||||||
* [go](https://go.dev/doc/install)
|
- **Authentication & Authorization** - JWT-based auth with role-based access control
|
||||||
* [docker-compose](https://docs.docker.com/compose/reference)
|
- **Database Ready** - PostgreSQL integration with GORM
|
||||||
* [migrate](https://github.com/golang-migrate/migrate)
|
- **Middleware Stack** - CORS, logging, recovery, correlation ID tracking
|
||||||
|
- **Configuration Management** - Environment-based config with Viper
|
||||||
|
- **Structured Logging** - Zap and Logrus integration
|
||||||
```shell
|
- **Input Validation** - Request validation with go-playground/validator
|
||||||
$ make
|
- **Graceful Shutdown** - Proper cleanup on termination signals
|
||||||
|
|
||||||
Usage: make [command]
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
rename-project name={name} Rename project
|
|
||||||
|
|
||||||
build-http Build http server
|
|
||||||
|
|
||||||
migration-create name={name} Create migration
|
|
||||||
migration-up Up migrations
|
|
||||||
migration-down Down last migration
|
|
||||||
|
|
||||||
docker-up Up docker services
|
|
||||||
docker-down Down docker services
|
|
||||||
|
|
||||||
fmt Format source code
|
|
||||||
test Run unit tests
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## HTTP Server
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ ./bin/http-server --help
|
|
||||||
|
|
||||||
Usage: http-server
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
-h, --help Show mycontext-sensitive help.
|
|
||||||
--env-path=STRING Path to env config file
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration** is based on the environment variables. See [.env.template](.env).
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# Expose env vars before and start server
|
|
||||||
$ ./bin/http-server
|
|
||||||
|
|
||||||
# Expose env vars from the file and start server
|
|
||||||
$ ./bin/http-server --env-path ./config/env/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Docs
|
|
||||||
* [eslogad Backend](https://eslogad-be.app-dev.altru.id/docs/index.html#/)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the [MIT License](https://github.com/pvarentsov/eslogad-be/blob/main/LICENSE).
|
|
||||||
|
|
||||||
# Apskel POS Backend
|
|
||||||
|
|
||||||
A SaaS Point of Sale (POS) Restaurant System backend built with clean architecture principles in Go.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
This application follows a clean architecture pattern with clear separation of concerns:
|
|
||||||
|
|
||||||
```
|
|
||||||
Handler → Service → Processor → Repository
|
|
||||||
```
|
|
||||||
|
|
||||||
### Layers
|
|
||||||
|
|
||||||
1. **Contract Package** (`internal/contract/`)
|
|
||||||
- Request/Response DTOs for API communication
|
|
||||||
- Contains JSON tags for serialization
|
|
||||||
- Input validation tags
|
|
||||||
|
|
||||||
2. **Handler Layer** (`internal/handler/`)
|
|
||||||
- HTTP request/response handling
|
|
||||||
- Request validation using go-playground/validator
|
|
||||||
- Route definitions and middleware
|
|
||||||
- Transforms contracts to/from services
|
|
||||||
|
|
||||||
3. **Service Layer** (`internal/service/`)
|
|
||||||
- Business logic orchestration
|
|
||||||
- Calls processors and transformers
|
|
||||||
- Coordinates between different business operations
|
|
||||||
|
|
||||||
4. **Processor Layer** (`internal/processor/`)
|
|
||||||
- Complex business operations
|
|
||||||
- Cross-repository transactions
|
|
||||||
- Business rule enforcement
|
|
||||||
- Handles operations like order creation with inventory updates
|
|
||||||
|
|
||||||
5. **Repository Layer** (`internal/repository/`)
|
|
||||||
- Data access layer
|
|
||||||
- Individual repository per entity
|
|
||||||
- Database-specific operations
|
|
||||||
- Uses entities for database models
|
|
||||||
|
|
||||||
6. **Supporting Packages**:
|
|
||||||
- **Models** (`internal/models/`) - Pure business logic models (no database dependencies)
|
|
||||||
- **Entities** (`internal/entities/`) - Database models with GORM tags
|
|
||||||
- **Constants** (`internal/constants/`) - Type-safe enums and business constants
|
|
||||||
- **Transformer** (`internal/transformer/`) - Contract ↔ Model conversions
|
|
||||||
- **Mappers** (`internal/mappers/`) - Model ↔ Entity conversions
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **Clean Architecture**: Strict separation between business logic and infrastructure
|
|
||||||
- **Type Safety**: Constants package with validation helpers
|
|
||||||
- **Validation**: Comprehensive request validation using go-playground/validator
|
|
||||||
- **Error Handling**: Structured error responses with proper HTTP status codes
|
|
||||||
- **Database Independence**: Business logic never depends on database implementation
|
|
||||||
- **Testability**: Each layer can be tested independently
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
- `GET /api/v1/health` - Health check endpoint
|
|
||||||
|
|
||||||
### Organizations
|
|
||||||
- `POST /api/v1/organizations` - Create organization
|
|
||||||
- `GET /api/v1/organizations` - List organizations
|
|
||||||
- `GET /api/v1/organizations/{id}` - Get organization by ID
|
|
||||||
- `PUT /api/v1/organizations/{id}` - Update organization
|
|
||||||
- `DELETE /api/v1/organizations/{id}` - Delete organization
|
|
||||||
|
|
||||||
### Users
|
|
||||||
- `POST /api/v1/users` - Create user
|
|
||||||
- `GET /api/v1/users` - List users
|
|
||||||
- `GET /api/v1/users/{id}` - Get user by ID
|
|
||||||
- `PUT /api/v1/users/{id}` - Update user
|
|
||||||
- `DELETE /api/v1/users/{id}` - Delete user
|
|
||||||
- `PUT /api/v1/users/{id}/password` - Change password
|
|
||||||
- `PUT /api/v1/users/{id}/activate` - Activate user
|
|
||||||
- `PUT /api/v1/users/{id}/deactivate` - Deactivate user
|
|
||||||
|
|
||||||
### Orders
|
|
||||||
- `POST /api/v1/orders` - Create order with items
|
|
||||||
- `GET /api/v1/orders` - List orders
|
|
||||||
- `GET /api/v1/orders/{id}` - Get order by ID
|
|
||||||
- `GET /api/v1/orders/{id}?include_items=true` - Get order with items
|
|
||||||
- `PUT /api/v1/orders/{id}` - Update order
|
|
||||||
- `PUT /api/v1/orders/{id}/cancel` - Cancel order
|
|
||||||
- `PUT /api/v1/orders/{id}/complete` - Complete order
|
|
||||||
- `POST /api/v1/orders/{id}/items` - Add item to order
|
|
||||||
|
|
||||||
### Order Items
|
|
||||||
- `PUT /api/v1/order-items/{id}` - Update order item
|
|
||||||
- `DELETE /api/v1/order-items/{id}` - Remove order item
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. **Clone the repository**
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd eslogad-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies**
|
|
||||||
```bash
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Set up database**
|
|
||||||
```bash
|
|
||||||
# Set your PostgreSQL database URL
|
|
||||||
export DATABASE_URL="postgres://username:password@localhost:5432/apskel_pos?sslmode=disable"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Run migrations**
|
|
||||||
```bash
|
|
||||||
make migration-up
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start the server
|
|
||||||
go run cmd/server/main.go -port 8080 -db-url "postgres://username:password@localhost:5432/apskel_pos?sslmode=disable"
|
|
||||||
|
|
||||||
# Or using environment variable
|
|
||||||
export DATABASE_URL="postgres://username:password@localhost:5432/apskel_pos?sslmode=disable"
|
|
||||||
go run cmd/server/main.go -port 8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Make Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run the application
|
|
||||||
make start
|
|
||||||
|
|
||||||
# Format code
|
|
||||||
make fmt
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
make build-http
|
|
||||||
|
|
||||||
# Docker operations
|
|
||||||
make docker-up
|
|
||||||
make docker-down
|
|
||||||
|
|
||||||
# Database migrations
|
|
||||||
make migration-create name=create_users_table
|
|
||||||
make migration-up
|
|
||||||
make migration-down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example API Usage
|
|
||||||
|
|
||||||
### Create Organization
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/api/v1/organizations \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"name": "My Restaurant",
|
|
||||||
"plan_type": "premium"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create User
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/api/v1/users \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"organization_id": "uuid-here",
|
|
||||||
"username": "john_doe",
|
|
||||||
"email": "john@example.com",
|
|
||||||
"password": "password123",
|
|
||||||
"full_name": "John Doe",
|
|
||||||
"role": "manager"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Order with Items
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/api/v1/orders \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"outlet_id": "uuid-here",
|
|
||||||
"user_id": "uuid-here",
|
|
||||||
"table_number": "A1",
|
|
||||||
"order_type": "dine_in",
|
|
||||||
"notes": "No onions",
|
|
||||||
"order_items": [
|
|
||||||
{
|
|
||||||
"product_id": "uuid-here",
|
|
||||||
"quantity": 2,
|
|
||||||
"unit_price": 15.99
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
eslogad-backend/
|
.
|
||||||
├── cmd/
|
├── cmd/
|
||||||
│ └── server/ # Application entry point
|
│ └── server/ # Application entry point
|
||||||
|
├── config/ # Configuration management
|
||||||
|
│ ├── configs.go # Main config loader
|
||||||
|
│ ├── db.go # Database config
|
||||||
|
│ ├── jwt.go # JWT config
|
||||||
|
│ ├── log.go # Logging config
|
||||||
|
│ ├── s3.go # S3/file storage config
|
||||||
|
│ └── server.go # Server config
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── app/ # Application wiring and dependency injection
|
│ ├── app/ # Application initialization
|
||||||
│ ├── contract/ # API contracts (request/response DTOs)
|
│ ├── appcontext/ # Request context utilities
|
||||||
│ ├── handler/ # HTTP handlers and routes
|
│ ├── constant/ # Application constants
|
||||||
│ ├── service/ # Business logic orchestration
|
│ ├── constants/ # Business constants
|
||||||
│ ├── processor/ # Complex business operations
|
│ ├── contract/ # API request/response DTOs
|
||||||
|
│ ├── db/ # Database connection
|
||||||
|
│ ├── entities/ # Database models (GORM)
|
||||||
|
│ ├── handler/ # HTTP handlers
|
||||||
|
│ ├── logger/ # Logging utilities
|
||||||
|
│ ├── middleware/ # HTTP middleware
|
||||||
|
│ ├── processor/ # Business logic processors
|
||||||
│ ├── repository/ # Data access layer
|
│ ├── repository/ # Data access layer
|
||||||
│ ├── models/ # Pure business models
|
│ ├── router/ # Route definitions
|
||||||
│ ├── entities/ # Database entities (GORM models)
|
│ ├── service/ # Service layer
|
||||||
│ ├── constants/ # Business constants and enums
|
│ ├── transformer/ # DTO transformers
|
||||||
│ ├── transformer/ # Contract ↔ Model transformations
|
│ ├── util/ # Utility functions
|
||||||
│ └── mappers/ # Model ↔ Entity transformations
|
│ └── validator/ # Custom validators
|
||||||
├── migrations/ # Database migrations
|
├── migrations/ # Database migrations
|
||||||
├── Makefile # Build and development commands
|
├── infra/ # Infrastructure configs (YAML)
|
||||||
├── go.mod # Go module definition
|
├── go.mod
|
||||||
└── README.md # This file
|
├── go.sum
|
||||||
|
├── Makefile
|
||||||
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Architecture Layers
|
||||||
|
|
||||||
|
### 1. Handler Layer (`internal/handler/`)
|
||||||
|
- HTTP request/response handling
|
||||||
|
- Request validation
|
||||||
|
- Route definitions
|
||||||
|
- Transforms contracts to/from services
|
||||||
|
|
||||||
|
### 2. Service Layer (`internal/service/`)
|
||||||
|
- Business logic orchestration
|
||||||
|
- Coordinates between processors
|
||||||
|
- Transaction management
|
||||||
|
|
||||||
|
### 3. Processor Layer (`internal/processor/`)
|
||||||
|
- Complex business operations
|
||||||
|
- Cross-repository transactions
|
||||||
|
- Business rule enforcement
|
||||||
|
|
||||||
|
### 4. Repository Layer (`internal/repository/`)
|
||||||
|
- Data access layer
|
||||||
|
- Database operations
|
||||||
|
- GORM integration
|
||||||
|
|
||||||
|
### 5. Supporting Packages
|
||||||
|
- **Contract** - API DTOs with JSON tags
|
||||||
|
- **Entities** - Database models with GORM tags
|
||||||
|
- **Transformer** - Contract ↔ Entity conversions
|
||||||
|
- **Middleware** - Request processing pipeline
|
||||||
|
- **Validator** - Custom validation logic
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.21 or higher
|
||||||
|
- PostgreSQL 12+
|
||||||
|
- Make (optional, for using Makefile commands)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone this template**
|
||||||
|
```bash
|
||||||
|
git clone <your-repo-url>
|
||||||
|
cd go-backend-template
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Rename the project**
|
||||||
|
|
||||||
|
Update the module name in `go.mod`:
|
||||||
|
```go
|
||||||
|
module your-project-name
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update all imports:
|
||||||
|
```bash
|
||||||
|
find . -name "*.go" -type f -exec sed -i '' 's|go-backend-template|your-project-name|g' {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Set up configuration**
|
||||||
|
|
||||||
|
Create your environment config file in `infra/`:
|
||||||
|
```bash
|
||||||
|
cp infra/development.yaml infra/development.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `infra/development.yaml` with your settings:
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: "8080"
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
host: "localhost"
|
||||||
|
port: "5432"
|
||||||
|
user: "postgres"
|
||||||
|
password: "your-password"
|
||||||
|
database: "your-database"
|
||||||
|
sslmode: "disable"
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
token:
|
||||||
|
secret: "your-secret-key"
|
||||||
|
expires_ttl: 3600
|
||||||
|
|
||||||
|
log:
|
||||||
|
log_level: "info"
|
||||||
|
log_format: "json"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Run database migrations**
|
||||||
|
```bash
|
||||||
|
make migration-up
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Start the server**
|
||||||
|
```bash
|
||||||
|
# Set environment mode
|
||||||
|
export ENV_MODE=development
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
go run cmd/server/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application uses environment-based YAML configuration files located in the `infra/` directory:
|
||||||
|
|
||||||
|
- `infra/local.yaml` - Local development
|
||||||
|
- `infra/development.yaml` - Development environment
|
||||||
|
- `infra/production.yaml` - Production environment
|
||||||
|
|
||||||
|
Set the `ENV_MODE` environment variable to select the configuration:
|
||||||
|
```bash
|
||||||
|
export ENV_MODE=development # or local, production
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
- `GET /health` - Health check endpoint
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/v1/auth/login` - User login
|
||||||
|
- `POST /api/v1/auth/refresh` - Refresh access token
|
||||||
|
- `GET /api/v1/auth/profile` - Get authenticated user profile
|
||||||
|
|
||||||
|
### Users (Protected)
|
||||||
|
- `POST /api/v1/users` - Create user
|
||||||
|
- `GET /api/v1/users` - List users
|
||||||
|
- `PUT /api/v1/users/:id` - Update user
|
||||||
|
- `DELETE /api/v1/users/:id` - Delete user
|
||||||
|
- `GET /api/v1/users/profile` - Get current user profile
|
||||||
|
- `PUT /api/v1/users/profile` - Update current user profile
|
||||||
|
- `PUT /api/v1/users/:id/password` - Change user password
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running with Air (Hot Reload)
|
||||||
|
|
||||||
|
Install Air:
|
||||||
|
```bash
|
||||||
|
go install github.com/cosmtrek/air@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with hot reload:
|
||||||
|
```bash
|
||||||
|
air
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
|
||||||
|
Create a new migration:
|
||||||
|
```bash
|
||||||
|
make migration-create name=create_your_table
|
||||||
|
```
|
||||||
|
|
||||||
|
Run migrations:
|
||||||
|
```bash
|
||||||
|
make migration-up
|
||||||
|
```
|
||||||
|
|
||||||
|
Rollback last migration:
|
||||||
|
```bash
|
||||||
|
make migration-down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Formatting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
Build the binary:
|
||||||
|
```bash
|
||||||
|
make build-http
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary will be created in `./bin/http-server`.
|
||||||
|
|
||||||
|
Run the production binary:
|
||||||
|
```bash
|
||||||
|
./bin/http-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Support
|
||||||
|
|
||||||
|
Build Docker image:
|
||||||
|
```bash
|
||||||
|
docker build -t your-app-name .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with Docker Compose:
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization Guide
|
||||||
|
|
||||||
|
### Adding a New Feature
|
||||||
|
|
||||||
|
1. **Define the entity** in `internal/entities/`
|
||||||
|
2. **Create migration** for the database table
|
||||||
|
3. **Add repository** in `internal/repository/`
|
||||||
|
4. **Create processor** in `internal/processor/` for business logic
|
||||||
|
5. **Add service** in `internal/service/` for orchestration
|
||||||
|
6. **Define contracts** in `internal/contract/` for API DTOs
|
||||||
|
7. **Create handler** in `internal/handler/` for HTTP endpoints
|
||||||
|
8. **Register routes** in `internal/router/router.go`
|
||||||
|
9. **Wire dependencies** in `internal/app/app.go`
|
||||||
|
|
||||||
|
### Example: Adding a "Product" Feature
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 1. Entity (internal/entities/product.go)
|
||||||
|
type Product struct {
|
||||||
|
ID string `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
Price float64 `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Repository (internal/repository/product_repository.go)
|
||||||
|
type ProductRepository interface {
|
||||||
|
Create(ctx context.Context, product *entities.Product) error
|
||||||
|
FindByID(ctx context.Context, id string) (*entities.Product, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Service (internal/service/product_service.go)
|
||||||
|
type ProductService interface {
|
||||||
|
CreateProduct(ctx context.Context, req *contract.CreateProductRequest) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Handler (internal/handler/product_handler.go)
|
||||||
|
func (h *ProductHandler) CreateProduct(c *gin.Context) {
|
||||||
|
// Handle HTTP request
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Register in router (internal/router/router.go)
|
||||||
|
products := v1.Group("/products")
|
||||||
|
products.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
products.POST("", r.productHandler.CreateProduct)
|
||||||
|
products.GET("/:id", r.productHandler.GetProduct)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `ENV_MODE` - Environment mode (local, development, production)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **[Gorilla Mux](https://github.com/gorilla/mux)** - HTTP router and URL matcher
|
Key dependencies:
|
||||||
- **[GORM](https://gorm.io/)** - ORM for database operations
|
- **Gin** - HTTP web framework
|
||||||
- **[PostgreSQL Driver](https://github.com/lib/pq)** - PostgreSQL database driver
|
- **GORM** - ORM library
|
||||||
- **[Validator](https://github.com/go-playground/validator)** - Struct validation
|
- **Viper** - Configuration management
|
||||||
- **[UUID](https://github.com/google/uuid)** - UUID generation and parsing
|
- **Zap/Logrus** - Structured logging
|
||||||
|
- **JWT** - JSON Web Token authentication
|
||||||
|
- **Validator** - Struct validation
|
||||||
|
- **AWS SDK** - S3 file storage (optional)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
3. Commit your changes
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
4. Push to the branch
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
5. Create a Pull Request
|
5. Open a Pull Request
|
||||||
|
|
||||||
## License
|
## Support
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
For issues and questions, please open an issue on GitHub.
|
||||||
|
|||||||
@ -1,23 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/config"
|
"go-backend-template/config"
|
||||||
"eslogad-be/internal/app"
|
"go-backend-template/internal/app"
|
||||||
"eslogad-be/internal/db"
|
"go-backend-template/internal/logger"
|
||||||
"eslogad-be/internal/logger"
|
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.LoadConfig()
|
cfg := config.LoadConfig()
|
||||||
|
log.Printf("DEBUG: Config loaded successfully")
|
||||||
|
log.Printf("DEBUG: Port from config: %q", cfg.Port())
|
||||||
|
|
||||||
logger.Setup(cfg.LogLevel(), cfg.LogFormat())
|
logger.Setup(cfg.LogLevel(), cfg.LogFormat())
|
||||||
|
|
||||||
db, err := db.NewPostgres(cfg.Database)
|
application := app.NewApp()
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
application := app.NewApp(db)
|
|
||||||
|
|
||||||
if err := application.Initialize(cfg); err != nil {
|
if err := application.Initialize(cfg); err != nil {
|
||||||
log.Fatalf("Failed to initialize application: %v", err)
|
log.Fatalf("Failed to initialize application: %v", err)
|
||||||
|
|||||||
@ -29,9 +29,7 @@ type Config struct {
|
|||||||
Jwt Jwt `mapstructure:"jwt"`
|
Jwt Jwt `mapstructure:"jwt"`
|
||||||
Log Log `mapstructure:"log"`
|
Log Log `mapstructure:"log"`
|
||||||
S3Config S3Config `mapstructure:"s3"`
|
S3Config S3Config `mapstructure:"s3"`
|
||||||
OnlyOffice OnlyOffice `mapstructure:"onlyoffice"`
|
Dukcapil Dukcapil `mapstructure:"dukcapil"`
|
||||||
Novu Novu `mapstructure:"novu"`
|
|
||||||
Department Department `mapstructure:"department"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -83,19 +81,3 @@ func (c *Config) LogFormat() string {
|
|||||||
return c.Log.LogFormat
|
return c.Log.LogFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnlyOffice struct {
|
|
||||||
URL string `mapstructure:"url"`
|
|
||||||
Token string `mapstructure:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Novu struct {
|
|
||||||
APIKey string `mapstructure:"api_key"`
|
|
||||||
ApplicationID string `mapstructure:"application_id"`
|
|
||||||
BaseURL string `mapstructure:"base_url"`
|
|
||||||
IncomingLetterWorkflowID string `mapstructure:"incoming_letter_workflow_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Department struct {
|
|
||||||
ParentPath string `mapstructure:"parent_path"`
|
|
||||||
ExcludedPaths []string `mapstructure:"excluded_paths"`
|
|
||||||
}
|
|
||||||
|
|||||||
22
config/dukcapil.go
Normal file
22
config/dukcapil.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Dukcapil holds configuration for the Dukcapil Face Recognition (1:N) API.
|
||||||
|
type Dukcapil struct {
|
||||||
|
BaseURL string `mapstructure:"base_url"`
|
||||||
|
CustomerID string `mapstructure:"customer_id"`
|
||||||
|
Methode string `mapstructure:"methode"`
|
||||||
|
UserID string `mapstructure:"user_id"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
PublicKeyPath string `mapstructure:"public_key_path"`
|
||||||
|
DefaultIP string `mapstructure:"default_ip"`
|
||||||
|
TimeoutSecond int `mapstructure:"timeout_second"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dukcapil) Timeout() time.Duration {
|
||||||
|
if d.TimeoutSecond <= 0 {
|
||||||
|
return 30 * time.Second
|
||||||
|
}
|
||||||
|
return time.Duration(d.TimeoutSecond) * time.Second
|
||||||
|
}
|
||||||
@ -1,25 +0,0 @@
|
|||||||
APP_NAME="eslogad"
|
|
||||||
PORT="4000"
|
|
||||||
NETWORK_NAME="pgnet"
|
|
||||||
|
|
||||||
echo "🔄 Pulling latest code..."
|
|
||||||
git pull
|
|
||||||
|
|
||||||
echo "🐳 Building Docker image..."
|
|
||||||
docker build -t "$APP_NAME:latest" .
|
|
||||||
|
|
||||||
echo "🛑 Stopping and removing old container..."
|
|
||||||
docker stop "$APP_NAME" 2>/dev/null || true
|
|
||||||
docker rm "$APP_NAME" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "🔌 Ensuring Docker network exists..."
|
|
||||||
docker network inspect "$NETWORK_NAME" >/dev/null 2>&1 || docker network create "$NETWORK_NAME"
|
|
||||||
|
|
||||||
echo "🚀 Running new container..."
|
|
||||||
docker run -d \
|
|
||||||
--name "$APP_NAME" \
|
|
||||||
--network "$NETWORK_NAME" \
|
|
||||||
-p "$PORT:$PORT" \
|
|
||||||
-v "$(pwd)/infra":/infra:ro \
|
|
||||||
-v "$(pwd)/templates":/templates:ro \
|
|
||||||
"$APP_NAME:latest"
|
|
||||||
211
docker-build.sh
211
docker-build.sh
@ -1,211 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Docker build script for eslogad-backend
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Functions
|
|
||||||
log_info() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Help function
|
|
||||||
show_help() {
|
|
||||||
echo "Usage: $0 [OPTION]"
|
|
||||||
echo "Build and manage Docker containers for eslogad-backend"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " build Build the Docker image"
|
|
||||||
echo " run Run the production container"
|
|
||||||
echo " dev Run development environment with live reload"
|
|
||||||
echo " migrate Run database migrations"
|
|
||||||
echo " stop Stop all containers"
|
|
||||||
echo " clean Remove containers and images"
|
|
||||||
echo " logs Show container logs"
|
|
||||||
echo " help Show this help message"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build Docker image
|
|
||||||
build_image() {
|
|
||||||
log_info "Building eslogad-backend Docker image..."
|
|
||||||
|
|
||||||
# Check if Go build works locally first (optional quick test)
|
|
||||||
if command -v go &> /dev/null; then
|
|
||||||
log_info "Testing Go build locally first..."
|
|
||||||
if go build -o /tmp/test-build cmd/server/main.go 2>/dev/null; then
|
|
||||||
log_success "Local Go build test passed"
|
|
||||||
rm -f /tmp/test-build
|
|
||||||
else
|
|
||||||
log_warning "Local Go build test failed, but continuing with Docker build..."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build the image with production target
|
|
||||||
docker build \
|
|
||||||
--target production \
|
|
||||||
-t eslogad-backend:latest \
|
|
||||||
-t eslogad-backend:$(date +%Y%m%d-%H%M%S) \
|
|
||||||
.
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
log_success "Docker image built successfully!"
|
|
||||||
else
|
|
||||||
log_error "Failed to build Docker image"
|
|
||||||
log_info "Make sure you're using Go 1.21+ and all dependencies are available"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run production container
|
|
||||||
run_container() {
|
|
||||||
log_info "Starting production containers..."
|
|
||||||
|
|
||||||
# Start the containers
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
log_success "Containers started successfully!"
|
|
||||||
log_info "Backend API available at: http://localhost:3300"
|
|
||||||
log_info "Database available at: localhost:5432"
|
|
||||||
log_info "Redis available at: localhost:6379"
|
|
||||||
log_info ""
|
|
||||||
log_info "Use 'docker-compose logs -f backend' to view logs"
|
|
||||||
else
|
|
||||||
log_error "Failed to start containers"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run development environment
|
|
||||||
run_dev() {
|
|
||||||
log_info "Starting development environment..."
|
|
||||||
|
|
||||||
# Start development containers
|
|
||||||
docker-compose --profile dev up -d
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
log_success "Development environment started!"
|
|
||||||
log_info "Backend API (dev) available at: http://localhost:3001"
|
|
||||||
log_info "Backend API (prod) available at: http://localhost:3300"
|
|
||||||
log_info ""
|
|
||||||
log_info "Use 'docker-compose logs -f backend-dev' to view development logs"
|
|
||||||
else
|
|
||||||
log_error "Failed to start development environment"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run migrations
|
|
||||||
run_migrations() {
|
|
||||||
log_info "Running database migrations..."
|
|
||||||
|
|
||||||
# Ensure database is running
|
|
||||||
docker-compose up -d postgres
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# Run migrations
|
|
||||||
docker-compose --profile migrate up migrate
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
log_success "Migrations completed successfully!"
|
|
||||||
else
|
|
||||||
log_warning "Migrations may have failed or are already up to date"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Stop containers
|
|
||||||
stop_containers() {
|
|
||||||
log_info "Stopping all containers..."
|
|
||||||
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
log_success "All containers stopped"
|
|
||||||
else
|
|
||||||
log_error "Failed to stop containers"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clean up containers and images
|
|
||||||
clean_up() {
|
|
||||||
log_warning "This will remove all containers, networks, and images related to this project"
|
|
||||||
read -p "Are you sure? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
log_info "Cleaning up containers and images..."
|
|
||||||
|
|
||||||
# Stop and remove containers
|
|
||||||
docker-compose down -v --remove-orphans
|
|
||||||
|
|
||||||
# Remove images
|
|
||||||
docker rmi eslogad-backend:latest || true
|
|
||||||
docker rmi $(docker images eslogad-backend -q) || true
|
|
||||||
|
|
||||||
# Remove unused networks and volumes
|
|
||||||
docker network prune -f || true
|
|
||||||
docker volume prune -f || true
|
|
||||||
|
|
||||||
log_success "Cleanup completed"
|
|
||||||
else
|
|
||||||
log_info "Cleanup cancelled"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Show logs
|
|
||||||
show_logs() {
|
|
||||||
log_info "Showing container logs..."
|
|
||||||
|
|
||||||
# Show logs for all services
|
|
||||||
docker-compose logs -f
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main script logic
|
|
||||||
case "${1:-help}" in
|
|
||||||
"build")
|
|
||||||
build_image
|
|
||||||
;;
|
|
||||||
"run")
|
|
||||||
build_image
|
|
||||||
run_container
|
|
||||||
;;
|
|
||||||
"dev")
|
|
||||||
run_dev
|
|
||||||
;;
|
|
||||||
"migrate")
|
|
||||||
run_migrations
|
|
||||||
;;
|
|
||||||
"stop")
|
|
||||||
stop_containers
|
|
||||||
;;
|
|
||||||
"clean")
|
|
||||||
clean_up
|
|
||||||
;;
|
|
||||||
"logs")
|
|
||||||
show_logs
|
|
||||||
;;
|
|
||||||
"help"|*)
|
|
||||||
show_help
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
4
go.mod
4
go.mod
@ -1,4 +1,4 @@
|
|||||||
module eslogad-be
|
module go-backend-template
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
@ -38,7 +38,6 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/spf13/afero v1.9.5 // indirect
|
github.com/spf13/afero v1.9.5 // indirect
|
||||||
github.com/spf13/cast v1.5.1 // indirect
|
github.com/spf13/cast v1.5.1 // indirect
|
||||||
@ -62,7 +61,6 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go v1.55.7
|
github.com/aws/aws-sdk-go v1.55.7
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||||
github.com/novuhq/go-novu v0.1.2
|
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
go.uber.org/zap v1.21.0
|
go.uber.org/zap v1.21.0
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -209,8 +209,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/novuhq/go-novu v0.1.2 h1:hYVrVjZBUgByVwLE+W4DNXRRCBlHoNNOBLkDI7/enU8=
|
|
||||||
github.com/novuhq/go-novu v0.1.2/go.mod h1:O8+kHDKSfDncLZ8olp5FL00tn1aSTMOvZI1IRZZqmUg=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
|||||||
@ -1,50 +1,38 @@
|
|||||||
server:
|
server:
|
||||||
base-url:
|
port: "8080"
|
||||||
local-url:
|
|
||||||
port: 4000
|
postgresql:
|
||||||
|
host: "localhost"
|
||||||
|
port: "5432"
|
||||||
|
user: "postgres"
|
||||||
|
password: "postgres"
|
||||||
|
database: "eslogad_db"
|
||||||
|
sslmode: "disable"
|
||||||
|
max_open_conns: 25
|
||||||
|
max_idle_conns: 5
|
||||||
|
conn_max_lifetime: 300
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
token:
|
token:
|
||||||
expires-ttl: 1440
|
secret: "your-secret-key-change-this-in-production"
|
||||||
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
|
expires_ttl: 3600
|
||||||
|
|
||||||
postgresql:
|
|
||||||
host: pg16
|
|
||||||
port: 5432
|
|
||||||
driver: postgres
|
|
||||||
db: eslogad_db
|
|
||||||
username: eslogad_user
|
|
||||||
password: 'M9u$e#jT2@qR4pX!zL'
|
|
||||||
ssl-mode: disable
|
|
||||||
max-idle-connections-in-second: 600
|
|
||||||
max-open-connections-in-second: 600
|
|
||||||
connection-max-life-time-in-second: 600
|
|
||||||
debug: false
|
|
||||||
|
|
||||||
s3:
|
|
||||||
access_key_id: minioadmin # from MINIO_ROOT_USER or Access Key you created in console
|
|
||||||
access_key_secret: minioadmin123 # from MINIO_ROOT_PASSWORD or Secret Key you created in console
|
|
||||||
endpoint: https://noken-log-s3.tni-ad.mil.id # S3 API endpoint, not console port
|
|
||||||
bucket_name: enaklo
|
|
||||||
log_level: Error
|
|
||||||
host_url: 'https://noken-log-s3.tni-ad.mil.id'
|
|
||||||
|
|
||||||
log:
|
log:
|
||||||
log_format: 'json'
|
log_level: "info"
|
||||||
log_level: 'debug'
|
log_format: "json"
|
||||||
|
|
||||||
onlyoffice:
|
s3:
|
||||||
url: 'https://noken-log-onlyoffice.tni-ad.mil.id'
|
region: "us-east-1"
|
||||||
token: 'Si7vqBAZElQzeQF2KUbN5j9qKc1GX0kq'
|
bucket: "your-bucket-name"
|
||||||
|
access_key_id: "your-access-key"
|
||||||
|
secret_access_key: "your-secret-key"
|
||||||
|
|
||||||
novu:
|
dukcapil:
|
||||||
api_key: 'f7de60a16abf825996191bf69ea8054a' # Add your Novu API key here
|
base_url: "http://172.16.160.176:8080/api/face-recognition"
|
||||||
application_id: 'cDeX8L5VWe-r' # Add your Novu Application ID here
|
customer_id: "your-customer-id"
|
||||||
base_url: 'https://noken-log-novu-api.tni-ad.mil.id' # Optional: defaults to https://api.novu.co
|
methode: "CALL_FN"
|
||||||
incoming_letter_workflow_id: 'notification-dashbpard'
|
user_id: "281020241202039900305241000011252"
|
||||||
|
password: "Fjskdhv35$%"
|
||||||
department:
|
public_key_path: "infra/dukcapil_public.pem"
|
||||||
parent_path: 'eslogad.aslog' # Parent path for departments to be included in API
|
default_ip: "10.160.86.53"
|
||||||
excluded_paths: # Paths to exclude from department APIs
|
timeout_second: 30
|
||||||
- 'superadmin'
|
|
||||||
- 'system'
|
|
||||||
|
|||||||
38
infra/local.yaml.example
Normal file
38
infra/local.yaml.example
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
server:
|
||||||
|
port: "8080"
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
host: "localhost"
|
||||||
|
port: "5432"
|
||||||
|
user: "postgres"
|
||||||
|
password: "postgres"
|
||||||
|
database: "template_db"
|
||||||
|
sslmode: "disable"
|
||||||
|
max_open_conns: 25
|
||||||
|
max_idle_conns: 5
|
||||||
|
conn_max_lifetime: 300
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
token:
|
||||||
|
secret: "local-dev-secret-key"
|
||||||
|
expires_ttl: 86400
|
||||||
|
|
||||||
|
log:
|
||||||
|
log_level: "debug"
|
||||||
|
log_format: "text"
|
||||||
|
|
||||||
|
s3:
|
||||||
|
region: "us-east-1"
|
||||||
|
bucket: "local-bucket"
|
||||||
|
access_key_id: ""
|
||||||
|
secret_access_key: ""
|
||||||
|
|
||||||
|
dukcapil:
|
||||||
|
base_url: "http://172.16.160.176:8080/api/face-recognition"
|
||||||
|
customer_id: ""
|
||||||
|
methode: "CALL_FN"
|
||||||
|
user_id: ""
|
||||||
|
password: ""
|
||||||
|
public_key_path: "infra/dukcapil_public.pem"
|
||||||
|
default_ip: "127.0.0.1"
|
||||||
|
timeout_second: 30
|
||||||
@ -9,69 +9,39 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"eslogad-be/config"
|
"go-backend-template/config"
|
||||||
"eslogad-be/internal/client"
|
"go-backend-template/internal/client"
|
||||||
internalConfig "eslogad-be/internal/config"
|
"go-backend-template/internal/handler"
|
||||||
"eslogad-be/internal/handler"
|
"go-backend-template/internal/router"
|
||||||
"eslogad-be/internal/middleware"
|
"go-backend-template/internal/service"
|
||||||
"eslogad-be/internal/processor"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
"eslogad-be/internal/router"
|
|
||||||
"eslogad-be/internal/service"
|
|
||||||
"eslogad-be/internal/validator"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
db *gorm.DB
|
|
||||||
router *router.Router
|
router *router.Router
|
||||||
shutdown chan os.Signal
|
shutdown chan os.Signal
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(db *gorm.DB) *App {
|
func NewApp() *App {
|
||||||
return &App{
|
return &App{
|
||||||
db: db,
|
|
||||||
shutdown: make(chan os.Signal, 1),
|
shutdown: make(chan os.Signal, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Initialize(cfg *config.Config) error {
|
func (a *App) Initialize(cfg *config.Config) error {
|
||||||
repos := a.initRepositories()
|
|
||||||
processors := a.initProcessors(cfg, repos)
|
|
||||||
services := a.initServices(processors, repos, cfg)
|
|
||||||
middlewares := a.initMiddleware(services)
|
|
||||||
healthHandler := handler.NewHealthHandler()
|
healthHandler := handler.NewHealthHandler()
|
||||||
fileHandler := handler.NewFileHandler(services.fileService)
|
|
||||||
rbacHandler := handler.NewRBACHandler(services.rbacService)
|
dukcapilClient := client.NewDukcapilClient(cfg.Dukcapil)
|
||||||
masterHandler := handler.NewMasterHandler(services.masterService)
|
dukcapilService := service.NewDukcapilService(dukcapilClient)
|
||||||
letterHandler := handler.NewLetterHandler(services.letterService)
|
dukcapilHandler := handler.NewDukcapilHandler(dukcapilService)
|
||||||
letterOutgoingHandler := handler.NewLetterOutgoingHandler(services.letterOutgoingService)
|
|
||||||
adminApprovalFlowHandler := handler.NewAdminApprovalFlowHandler(services.approvalFlowService)
|
|
||||||
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
|
||||||
onlyOfficeHandler := handler.NewOnlyOfficeHandler(services.onlyOfficeService)
|
|
||||||
analyticsHandler := handler.NewAnalyticsHandler(services.analyticsService)
|
|
||||||
notificationHandler := handler.NewNotificationHandler(services.notificationService)
|
|
||||||
repositoryAttachmentHandler := handler.NewRepositoryAttachmentHandler(services.repositoryAttachmentService)
|
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
cfg,
|
cfg,
|
||||||
handler.NewAuthHandler(services.authService),
|
nil, // authHandler
|
||||||
middlewares.authMiddleware,
|
nil, // authMiddleware
|
||||||
healthHandler,
|
healthHandler,
|
||||||
handler.NewUserHandler(services.userService, validator.NewUserValidator()),
|
nil, // userHandler
|
||||||
fileHandler,
|
dukcapilHandler,
|
||||||
rbacHandler,
|
|
||||||
masterHandler,
|
|
||||||
letterHandler,
|
|
||||||
letterOutgoingHandler,
|
|
||||||
adminApprovalFlowHandler,
|
|
||||||
dispositionRouteHandler,
|
|
||||||
onlyOfficeHandler,
|
|
||||||
analyticsHandler,
|
|
||||||
notificationHandler,
|
|
||||||
repositoryAttachmentHandler,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -80,8 +50,20 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
func (a *App) Start(port string) error {
|
func (a *App) Start(port string) error {
|
||||||
engine := a.router.Init()
|
engine := a.router.Init()
|
||||||
|
|
||||||
|
// Debug: log what port value we received
|
||||||
|
log.Printf("DEBUG: Received port value: %q", port)
|
||||||
|
|
||||||
|
// Ensure proper address format
|
||||||
|
// Only accept port number, not full addresses
|
||||||
|
addr := ":" + port
|
||||||
|
if port == "" {
|
||||||
|
addr = ":8080" // default port
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("DEBUG: Final address: %q", addr)
|
||||||
|
|
||||||
a.server = &http.Server{
|
a.server = &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: addr,
|
||||||
Handler: engine,
|
Handler: engine,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: 15 * time.Second,
|
||||||
WriteTimeout: 15 * time.Second,
|
WriteTimeout: 15 * time.Second,
|
||||||
@ -115,362 +97,3 @@ func (a *App) Start(port string) error {
|
|||||||
func (a *App) Shutdown() {
|
func (a *App) Shutdown() {
|
||||||
close(a.shutdown)
|
close(a.shutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
type repositories struct {
|
|
||||||
userRepo *repository.UserRepositoryImpl
|
|
||||||
userProfileRepo *repository.UserProfileRepository
|
|
||||||
titleRepo *repository.TitleRepository
|
|
||||||
rbacRepo *repository.RBACRepository
|
|
||||||
labelRepo *repository.LabelRepository
|
|
||||||
priorityRepo *repository.PriorityRepository
|
|
||||||
institutionRepo *repository.InstitutionRepository
|
|
||||||
dispRepo *repository.DispositionActionRepository
|
|
||||||
letterRepo *repository.LetterIncomingRepository
|
|
||||||
letterAttachRepo *repository.LetterIncomingAttachmentRepository
|
|
||||||
activityLogRepo *repository.LetterIncomingActivityLogRepository
|
|
||||||
dispositionRouteRepo *repository.DispositionRouteRepository
|
|
||||||
// new repos
|
|
||||||
letterDispositionRepo *repository.LetterIncomingDispositionRepository
|
|
||||||
letterDispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
|
||||||
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
|
||||||
letterDiscussionRepo *repository.LetterDiscussionRepository
|
|
||||||
settingRepo *repository.AppSettingRepository
|
|
||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
|
||||||
departmentRepo *repository.DepartmentRepository
|
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
|
||||||
// letter outgoing repos
|
|
||||||
letterOutgoingRepo *repository.LetterOutgoingRepository
|
|
||||||
letterOutgoingAttachmentRepo *repository.LetterOutgoingAttachmentRepository
|
|
||||||
letterOutgoingFinalAttachmentRepo *repository.LetterOutgoingFinalAttachmentRepository
|
|
||||||
letterOutgoingRecipientRepo *repository.LetterOutgoingRecipientRepository
|
|
||||||
letterOutgoingDiscussionRepo *repository.LetterOutgoingDiscussionRepository
|
|
||||||
letterOutgoingDiscussionAttachRepo *repository.LetterOutgoingDiscussionAttachmentRepository
|
|
||||||
letterOutgoingActivityLogRepo *repository.LetterOutgoingActivityLogRepository
|
|
||||||
approvalFlowRepo *repository.ApprovalFlowRepository
|
|
||||||
letterOutgoingApprovalRepo *repository.LetterOutgoingApprovalRepository
|
|
||||||
analyticsRepo *repository.AnalyticsRepository
|
|
||||||
repositoryAttachmentRepo *repository.RepositoryAttachmentRepositoryImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
|
||||||
return &repositories{
|
|
||||||
userRepo: repository.NewUserRepository(a.db),
|
|
||||||
userProfileRepo: repository.NewUserProfileRepository(a.db),
|
|
||||||
titleRepo: repository.NewTitleRepository(a.db),
|
|
||||||
rbacRepo: repository.NewRBACRepository(a.db),
|
|
||||||
labelRepo: repository.NewLabelRepository(a.db),
|
|
||||||
priorityRepo: repository.NewPriorityRepository(a.db),
|
|
||||||
institutionRepo: repository.NewInstitutionRepository(a.db),
|
|
||||||
dispRepo: repository.NewDispositionActionRepository(a.db),
|
|
||||||
letterRepo: repository.NewLetterIncomingRepository(a.db),
|
|
||||||
letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db),
|
|
||||||
activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db),
|
|
||||||
dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db),
|
|
||||||
letterDispositionRepo: repository.NewLetterIncomingDispositionRepository(a.db),
|
|
||||||
letterDispositionDeptRepo: repository.NewLetterIncomingDispositionDepartmentRepository(a.db),
|
|
||||||
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
|
|
||||||
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
|
|
||||||
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
|
|
||||||
settingRepo: repository.NewAppSettingRepository(a.db),
|
|
||||||
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
|
||||||
departmentRepo: repository.NewDepartmentRepository(a.db),
|
|
||||||
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
|
||||||
letterOutgoingRepo: repository.NewLetterOutgoingRepository(a.db),
|
|
||||||
letterOutgoingAttachmentRepo: repository.NewLetterOutgoingAttachmentRepository(a.db),
|
|
||||||
letterOutgoingFinalAttachmentRepo: repository.NewLetterOutgoingFinalAttachmentRepository(a.db),
|
|
||||||
letterOutgoingRecipientRepo: repository.NewLetterOutgoingRecipientRepository(a.db),
|
|
||||||
letterOutgoingDiscussionRepo: repository.NewLetterOutgoingDiscussionRepository(a.db),
|
|
||||||
letterOutgoingDiscussionAttachRepo: repository.NewLetterOutgoingDiscussionAttachmentRepository(a.db),
|
|
||||||
letterOutgoingActivityLogRepo: repository.NewLetterOutgoingActivityLogRepository(a.db),
|
|
||||||
approvalFlowRepo: repository.NewApprovalFlowRepository(a.db),
|
|
||||||
letterOutgoingApprovalRepo: repository.NewLetterOutgoingApprovalRepository(a.db),
|
|
||||||
analyticsRepo: repository.NewAnalyticsRepository(a.db),
|
|
||||||
repositoryAttachmentRepo: repository.NewRepositoryAttachmentRepositoryImpl(a.db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type processors struct {
|
|
||||||
userProcessor *processor.UserProcessorImpl
|
|
||||||
cachedUserProcessor *processor.CachedUserProcessor
|
|
||||||
letterProcessor *processor.LetterProcessorImpl
|
|
||||||
letterOutgoingProcessor *processor.LetterOutgoingProcessorImpl
|
|
||||||
activityLogger *processor.ActivityLogProcessorImpl
|
|
||||||
letterNumberGenerator *processor.LetterNumberGeneratorImpl
|
|
||||||
onlyOfficeProcessor *processor.OnlyOfficeProcessorImpl
|
|
||||||
novuProcessor processor.NovuProcessor
|
|
||||||
notificationProcessor processor.NotificationProcessor
|
|
||||||
recipientProcessor *processor.RecipientProcessorImpl
|
|
||||||
letterDispositionProcessor *processor.LetterDispositionProcessorImpl
|
|
||||||
letterDispositionDeptProcessor *processor.LetterDispositionDepartmentProcessorImpl
|
|
||||||
// Modular processors for letter outgoing
|
|
||||||
letterValidationProcessor processor.LetterValidationProcessor
|
|
||||||
letterCreationProcessor processor.LetterCreationProcessor
|
|
||||||
letterApprovalProcessor processor.LetterApprovalProcessor
|
|
||||||
letterAttachmentProcessor processor.LetterAttachmentProcessor
|
|
||||||
letterOutgoingRecipientProcessor processor.LetterOutgoingRecipientProcessor
|
|
||||||
letterActivityProcessor processor.LetterActivityProcessor
|
|
||||||
repositoryAttachmentProcessor *processor.RepositoryAttachmentProcessorImpl
|
|
||||||
txManager *repository.TxManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
|
||||||
txMgr := repository.NewTxManager(a.db)
|
|
||||||
activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
|
|
||||||
|
|
||||||
// Create the letter number generator
|
|
||||||
letterNumberGen := processor.NewLetterNumberGenerator(repos.settingRepo)
|
|
||||||
|
|
||||||
// Create letter processors with the number generator
|
|
||||||
letterProc := processor.NewLetterProcessor(
|
|
||||||
repos.letterRepo, repos.letterAttachRepo, txMgr, activity,
|
|
||||||
repos.letterDispositionRepo, repos.letterDispositionDeptRepo,
|
|
||||||
repos.letterDispActionSelRepo, repos.dispositionNoteRepo,
|
|
||||||
repos.letterDiscussionRepo, repos.settingRepo,
|
|
||||||
repos.recipientRepo, repos.letterOutgoingRecipientRepo,
|
|
||||||
repos.departmentRepo, repos.userDeptRepo,
|
|
||||||
repos.priorityRepo, repos.institutionRepo, repos.dispRepo,
|
|
||||||
letterNumberGen, repos.dispositionRouteRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create modular processors for letter outgoing
|
|
||||||
letterValidationProc := processor.NewLetterValidationProcessor()
|
|
||||||
letterCreationProc := processor.NewLetterCreationProcessor(
|
|
||||||
repos.letterOutgoingRepo,
|
|
||||||
repos.approvalFlowRepo,
|
|
||||||
letterNumberGen,
|
|
||||||
)
|
|
||||||
letterApprovalProc := processor.NewLetterApprovalProcessor(
|
|
||||||
repos.letterOutgoingApprovalRepo,
|
|
||||||
repos.approvalFlowRepo,
|
|
||||||
repos.letterOutgoingRepo,
|
|
||||||
)
|
|
||||||
letterAttachmentProc := processor.NewLetterAttachmentProcessor(
|
|
||||||
repos.letterOutgoingAttachmentRepo,
|
|
||||||
)
|
|
||||||
letterOutgoingRecipientProc := processor.NewLetterOutgoingRecipientProcessor(
|
|
||||||
repos.letterOutgoingRecipientRepo,
|
|
||||||
repos.approvalFlowRepo,
|
|
||||||
repos.userDeptRepo,
|
|
||||||
)
|
|
||||||
letterActivityProc := processor.NewLetterActivityProcessor(
|
|
||||||
repos.letterOutgoingActivityLogRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create the main letter outgoing processor for backward compatibility
|
|
||||||
letterOutgoingProc := processor.NewLetterOutgoingProcessor(
|
|
||||||
a.db,
|
|
||||||
repos.letterOutgoingRepo,
|
|
||||||
repos.letterOutgoingAttachmentRepo,
|
|
||||||
repos.letterOutgoingFinalAttachmentRepo,
|
|
||||||
repos.letterOutgoingRecipientRepo,
|
|
||||||
repos.letterOutgoingDiscussionRepo,
|
|
||||||
repos.letterOutgoingDiscussionAttachRepo,
|
|
||||||
repos.letterOutgoingActivityLogRepo,
|
|
||||||
repos.approvalFlowRepo,
|
|
||||||
repos.letterOutgoingApprovalRepo,
|
|
||||||
letterNumberGen,
|
|
||||||
txMgr,
|
|
||||||
repos.priorityRepo,
|
|
||||||
repos.institutionRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create document repositories
|
|
||||||
docSessionRepo := repository.NewDocumentSessionRepository(a.db)
|
|
||||||
docVersionRepo := repository.NewDocumentVersionRepository(a.db)
|
|
||||||
docMetadataRepo := repository.NewDocumentMetadataRepository(a.db)
|
|
||||||
docErrorRepo := repository.NewDocumentErrorRepository(a.db)
|
|
||||||
|
|
||||||
// Create OnlyOffice processor
|
|
||||||
onlyOfficeProc := processor.NewOnlyOfficeProcessor(
|
|
||||||
a.db,
|
|
||||||
docSessionRepo,
|
|
||||||
docVersionRepo,
|
|
||||||
docMetadataRepo,
|
|
||||||
docErrorRepo,
|
|
||||||
txMgr,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create Novu processor for backward compatibility
|
|
||||||
novuConfig := internalConfig.LoadNovuConfig(cfg)
|
|
||||||
novuProc := processor.NewNovuProcessor(novuConfig)
|
|
||||||
|
|
||||||
// Create notification processor with Novu provider
|
|
||||||
novuProvider := processor.NewNovuProvider(novuConfig)
|
|
||||||
notificationProc := processor.NewNotificationProcessor(novuProvider, novuConfig.IncomingLetterWorkflowID)
|
|
||||||
|
|
||||||
// Create user role processor
|
|
||||||
userRoleProc := processor.NewUserRoleProcessor(a.db)
|
|
||||||
|
|
||||||
// Create user processor with Novu integration
|
|
||||||
userProc := processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo, userRoleProc)
|
|
||||||
userProc.SetNovuProcessor(novuProc)
|
|
||||||
|
|
||||||
// Create cached user processor for auth middleware
|
|
||||||
cachedUserProc := processor.NewCachedUserProcessor(repos.userRepo, repos.userProfileRepo, userRoleProc)
|
|
||||||
|
|
||||||
// Create recipient processor
|
|
||||||
recipientProc := processor.NewRecipientProcessor(
|
|
||||||
repos.recipientRepo,
|
|
||||||
repos.settingRepo,
|
|
||||||
repos.departmentRepo,
|
|
||||||
repos.userDeptRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create letter disposition processor
|
|
||||||
letterDispositionProc := processor.NewLetterDispositionProcessor(
|
|
||||||
repos.letterDispositionRepo,
|
|
||||||
repos.letterDispositionDeptRepo,
|
|
||||||
repos.letterDispActionSelRepo,
|
|
||||||
repos.dispositionNoteRepo,
|
|
||||||
repos.letterDiscussionRepo,
|
|
||||||
repos.dispRepo,
|
|
||||||
activity,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create letter disposition department processor
|
|
||||||
letterDispositionDeptProc := processor.NewLetterDispositionDepartmentProcessor(
|
|
||||||
repos.letterDispositionDeptRepo,
|
|
||||||
repos.dispositionNoteRepo,
|
|
||||||
repos.letterRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
repositoryAttachmentProc := processor.NewRepositoryAttachmentProcessor(
|
|
||||||
repos.repositoryAttachmentRepo)
|
|
||||||
|
|
||||||
return &processors{
|
|
||||||
userProcessor: userProc,
|
|
||||||
cachedUserProcessor: cachedUserProc,
|
|
||||||
letterProcessor: letterProc,
|
|
||||||
letterOutgoingProcessor: letterOutgoingProc,
|
|
||||||
activityLogger: activity,
|
|
||||||
letterNumberGenerator: letterNumberGen,
|
|
||||||
onlyOfficeProcessor: onlyOfficeProc,
|
|
||||||
novuProcessor: novuProc,
|
|
||||||
notificationProcessor: notificationProc,
|
|
||||||
recipientProcessor: recipientProc,
|
|
||||||
letterDispositionProcessor: letterDispositionProc,
|
|
||||||
letterDispositionDeptProcessor: letterDispositionDeptProc,
|
|
||||||
// Modular processors
|
|
||||||
letterValidationProcessor: letterValidationProc,
|
|
||||||
letterCreationProcessor: letterCreationProc,
|
|
||||||
letterApprovalProcessor: letterApprovalProc,
|
|
||||||
letterAttachmentProcessor: letterAttachmentProc,
|
|
||||||
letterOutgoingRecipientProcessor: letterOutgoingRecipientProc,
|
|
||||||
letterActivityProcessor: letterActivityProc,
|
|
||||||
repositoryAttachmentProcessor: repositoryAttachmentProc,
|
|
||||||
txManager: txMgr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type services struct {
|
|
||||||
userService *service.UserServiceImpl
|
|
||||||
authService *service.AuthServiceImpl
|
|
||||||
fileService *service.FileServiceImpl
|
|
||||||
rbacService *service.RBACServiceImpl
|
|
||||||
masterService *service.MasterServiceImpl
|
|
||||||
letterService *service.LetterServiceImpl
|
|
||||||
letterOutgoingService *service.LetterOutgoingServiceImpl
|
|
||||||
approvalFlowService *service.ApprovalFlowServiceImpl
|
|
||||||
dispositionRouteService *service.DispositionRouteServiceImpl
|
|
||||||
onlyOfficeService *service.OnlyOfficeServiceImpl
|
|
||||||
analyticsService *service.AnalyticsServiceImpl
|
|
||||||
notificationService *service.NotificationServiceImpl
|
|
||||||
repositoryAttachmentService *service.RepositoryAttachmentServiceImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
|
||||||
authConfig := cfg.Auth()
|
|
||||||
jwtSecret := authConfig.AccessTokenSecret()
|
|
||||||
authService := service.NewAuthService(processors.userProcessor, jwtSecret)
|
|
||||||
|
|
||||||
userSvc := service.NewUserService(processors.userProcessor, repos.titleRepo)
|
|
||||||
|
|
||||||
fileCfg := cfg.S3Config
|
|
||||||
s3Client := client.NewFileClient(fileCfg)
|
|
||||||
fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents", "finals")
|
|
||||||
|
|
||||||
rbacSvc := service.NewRBACService(repos.rbacRepo)
|
|
||||||
|
|
||||||
masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo, repos.departmentRepo, cfg)
|
|
||||||
|
|
||||||
txManager := repository.NewTxManager(a.db)
|
|
||||||
letterSvc := service.NewLetterService(
|
|
||||||
processors.letterProcessor,
|
|
||||||
txManager,
|
|
||||||
processors.letterNumberGenerator,
|
|
||||||
processors.recipientProcessor,
|
|
||||||
processors.activityLogger,
|
|
||||||
processors.letterDispositionProcessor,
|
|
||||||
processors.notificationProcessor,
|
|
||||||
processors.activityLogger,
|
|
||||||
)
|
|
||||||
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
|
||||||
letterOutgoingSvc := service.NewLetterOutgoingService(
|
|
||||||
processors.letterOutgoingProcessor,
|
|
||||||
processors.txManager,
|
|
||||||
processors.letterValidationProcessor,
|
|
||||||
processors.letterCreationProcessor,
|
|
||||||
processors.letterApprovalProcessor,
|
|
||||||
processors.letterAttachmentProcessor,
|
|
||||||
processors.letterOutgoingRecipientProcessor,
|
|
||||||
processors.notificationProcessor,
|
|
||||||
processors.letterActivityProcessor,
|
|
||||||
)
|
|
||||||
|
|
||||||
approvalFlowStepRepo := repository.NewApprovalFlowStepRepository(a.db)
|
|
||||||
approvalFlowSvc := service.NewApprovalFlowService(
|
|
||||||
a.db,
|
|
||||||
repos.approvalFlowRepo,
|
|
||||||
approvalFlowStepRepo,
|
|
||||||
txManager,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create OnlyOffice service with file storage
|
|
||||||
onlyOfficeSvc := service.NewOnlyOfficeService(processors.onlyOfficeProcessor, &cfg.OnlyOffice, a.db, s3Client)
|
|
||||||
|
|
||||||
// Create Analytics service
|
|
||||||
analyticsSvc := service.NewAnalyticsService(repos.analyticsRepo)
|
|
||||||
|
|
||||||
// Create Notification service
|
|
||||||
novuConfig := internalConfig.LoadNovuConfig(cfg)
|
|
||||||
notificationSvc := service.NewNotificationService(novuConfig, processors.userProcessor)
|
|
||||||
|
|
||||||
repositoryAttachmentSvc := service.NewRepositoryAttachmentService(processors.repositoryAttachmentProcessor)
|
|
||||||
|
|
||||||
return &services{
|
|
||||||
userService: userSvc,
|
|
||||||
authService: authService,
|
|
||||||
fileService: fileSvc,
|
|
||||||
rbacService: rbacSvc,
|
|
||||||
masterService: masterSvc,
|
|
||||||
letterService: letterSvc,
|
|
||||||
letterOutgoingService: letterOutgoingSvc,
|
|
||||||
approvalFlowService: approvalFlowSvc,
|
|
||||||
dispositionRouteService: dispRouteSvc,
|
|
||||||
onlyOfficeService: onlyOfficeSvc,
|
|
||||||
analyticsService: analyticsSvc,
|
|
||||||
notificationService: notificationSvc,
|
|
||||||
repositoryAttachmentService: repositoryAttachmentSvc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type middlewares struct {
|
|
||||||
authMiddleware *middleware.AuthMiddleware
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) initMiddleware(services *services) *middlewares {
|
|
||||||
return &middlewares{
|
|
||||||
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type validators struct {
|
|
||||||
userValidator *validator.UserValidatorImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) initValidators() *validators {
|
|
||||||
return &validators{
|
|
||||||
userValidator: validator.NewUserValidator(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
168
internal/client/dukcapil_client.go
Normal file
168
internal/client/dukcapil_client.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-backend-template/config"
|
||||||
|
"go-backend-template/internal/contract"
|
||||||
|
"go-backend-template/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DukcapilClient performs HTTPS calls to the Dukcapil 1:N face recognition
|
||||||
|
// endpoint (CALL_FN). It loads and caches the configured RSA public key used
|
||||||
|
// to encrypt sensitive credentials.
|
||||||
|
type DukcapilClient struct {
|
||||||
|
cfg config.Dukcapil
|
||||||
|
http *http.Client
|
||||||
|
pubKey *rsa.PublicKey
|
||||||
|
keyMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDukcapilClient(cfg config.Dukcapil) *DukcapilClient {
|
||||||
|
return &DukcapilClient{
|
||||||
|
cfg: cfg,
|
||||||
|
http: &http.Client{
|
||||||
|
Timeout: cfg.Timeout(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FaceMatch performs a 1:N face match call. user_id/password are encrypted
|
||||||
|
// with the configured public key (RSA PKCS1v15 -> base64). The image must
|
||||||
|
// already be base64 encoded.
|
||||||
|
func (c *DukcapilClient) FaceMatch(ctx context.Context, req *contract.FaceMatchRequest) (*contract.DukcapilFaceResponse, error) {
|
||||||
|
if c.cfg.BaseURL == "" || c.cfg.CustomerID == "" || c.cfg.Methode == "" {
|
||||||
|
return nil, errors.New("dukcapil: incomplete configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := c.loadPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: load public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encUserID, err := encryptAndEncode(pub, []byte(c.cfg.UserID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: encrypt user_id: %w", err)
|
||||||
|
}
|
||||||
|
encPassword, err := encryptAndEncode(pub, []byte(c.cfg.Password))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: encrypt password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := req.IP
|
||||||
|
if strings.TrimSpace(ip) == "" {
|
||||||
|
ip = c.cfg.DefaultIP
|
||||||
|
}
|
||||||
|
|
||||||
|
body := contract.DukcapilFaceRequest{
|
||||||
|
TransactionID: req.TransactionID,
|
||||||
|
TransactionSource: req.TransactionSource,
|
||||||
|
Threshold: req.Threshold,
|
||||||
|
Image: req.Image,
|
||||||
|
UserID: encUserID,
|
||||||
|
Password: encPassword,
|
||||||
|
IP: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s/%s",
|
||||||
|
strings.TrimRight(c.cfg.BaseURL, "/"),
|
||||||
|
c.cfg.CustomerID,
|
||||||
|
c.cfg.Methode,
|
||||||
|
)
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: build request: %w", err)
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Accept", "application/json")
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := c.http.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: do request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(ctx).Infof("DukcapilClient::FaceMatch -> status=%d duration=%s", resp.StatusCode, time.Since(start))
|
||||||
|
|
||||||
|
if resp.StatusCode >= 500 {
|
||||||
|
return nil, fmt.Errorf("dukcapil: upstream status %d: %s", resp.StatusCode, string(respBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var out contract.DukcapilFaceResponse
|
||||||
|
if err := json.Unmarshal(respBytes, &out); err != nil {
|
||||||
|
return nil, fmt.Errorf("dukcapil: decode response: %w (body=%s)", err, string(respBytes))
|
||||||
|
}
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DukcapilClient) loadPublicKey() (*rsa.PublicKey, error) {
|
||||||
|
c.keyMu.Lock()
|
||||||
|
defer c.keyMu.Unlock()
|
||||||
|
if c.pubKey != nil {
|
||||||
|
return c.pubKey, nil
|
||||||
|
}
|
||||||
|
if c.cfg.PublicKeyPath == "" {
|
||||||
|
return nil, errors.New("public key path not configured")
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(c.cfg.PublicKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode(raw)
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("invalid PEM file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try PKIX first (BEGIN PUBLIC KEY) then PKCS1 (BEGIN RSA PUBLIC KEY).
|
||||||
|
if pub, err := x509.ParsePKIXPublicKey(block.Bytes); err == nil {
|
||||||
|
rsaPub, ok := pub.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("public key is not RSA")
|
||||||
|
}
|
||||||
|
c.pubKey = rsaPub
|
||||||
|
return rsaPub, nil
|
||||||
|
}
|
||||||
|
rsaPub, err := x509.ParsePKCS1PublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse rsa public key: %w", err)
|
||||||
|
}
|
||||||
|
c.pubKey = rsaPub
|
||||||
|
return rsaPub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryptAndEncode mimics PHP openssl_public_encrypt (default padding =
|
||||||
|
// PKCS1v15) and base64-encodes the ciphertext.
|
||||||
|
func encryptAndEncode(pub *rsa.PublicKey, plaintext []byte) (string, error) {
|
||||||
|
cipherBytes, err := rsa.EncryptPKCS1v15(rand.Reader, pub, plaintext)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(cipherBytes), nil
|
||||||
|
}
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import "eslogad-be/config"
|
|
||||||
|
|
||||||
type NovuConfig struct {
|
|
||||||
APIKey string
|
|
||||||
ApplicationID string
|
|
||||||
BaseURL string
|
|
||||||
IncomingLetterWorkflowID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadNovuConfig(cfg *config.Config) *NovuConfig {
|
|
||||||
baseURL := cfg.Novu.BaseURL
|
|
||||||
if baseURL == "" {
|
|
||||||
baseURL = "https://api.novu.co"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default workflow ID for incoming letter notifications
|
|
||||||
workflowID := cfg.Novu.IncomingLetterWorkflowID
|
|
||||||
if workflowID == "" {
|
|
||||||
workflowID = "notification-dashbpard"
|
|
||||||
}
|
|
||||||
|
|
||||||
return &NovuConfig{
|
|
||||||
APIKey: cfg.Novu.APIKey,
|
|
||||||
ApplicationID: cfg.Novu.ApplicationID,
|
|
||||||
BaseURL: baseURL,
|
|
||||||
IncomingLetterWorkflowID: workflowID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -39,6 +39,8 @@ const (
|
|||||||
PaymentMethodHandlerEntity = "payment_method_handler"
|
PaymentMethodHandlerEntity = "payment_method_handler"
|
||||||
OutletServiceEntity = "outlet_service"
|
OutletServiceEntity = "outlet_service"
|
||||||
TableEntity = "table"
|
TableEntity = "table"
|
||||||
|
DukcapilHandlerEntity = "dukcapil_handler"
|
||||||
|
DukcapilServiceEntity = "dukcapil_service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var HttpErrorMap = map[string]int{
|
var HttpErrorMap = map[string]int{
|
||||||
|
|||||||
@ -1,186 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AnalyticsDashboardRequest represents the request for analytics data
|
|
||||||
type AnalyticsDashboardRequest struct {
|
|
||||||
StartDate string `form:"start_date" json:"start_date"`
|
|
||||||
EndDate string `form:"end_date" json:"end_date"`
|
|
||||||
DepartmentID *uuid.UUID `form:"department_id" json:"department_id,omitempty"`
|
|
||||||
UserID *uuid.UUID `form:"user_id" json:"user_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyticsDashboardResponse represents the complete analytics dashboard
|
|
||||||
type AnalyticsDashboardResponse struct {
|
|
||||||
Summary LetterSummaryStats `json:"summary"`
|
|
||||||
PriorityDistribution []PriorityDistribution `json:"priority_distribution"`
|
|
||||||
DepartmentStats []DepartmentStats `json:"department_stats"`
|
|
||||||
MonthlyTrend []MonthlyTrend `json:"monthly_trend"`
|
|
||||||
DepartmentsStats []SimpleDepartmentStats `json:"departments_stats,omitempty"`
|
|
||||||
InstitutionStats []InstitutionStats `json:"institution_stats"`
|
|
||||||
DailyActivity []DailyActivity `json:"daily_activity"`
|
|
||||||
StatusDistribution []StatusDistribution `json:"status_distribution,omitempty"`
|
|
||||||
TopSenders []TopUserStats `json:"top_senders,omitempty"`
|
|
||||||
TopRecipients []TopUserStats `json:"top_recipients,omitempty"`
|
|
||||||
ApprovalMetrics *ApprovalMetrics `json:"approval_metrics,omitempty"`
|
|
||||||
ResponseTimeStats *ResponseTimeStats `json:"response_time_stats,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LetterSummaryStats represents overall summary statistics
|
|
||||||
type LetterSummaryStats struct {
|
|
||||||
TotalIncoming int64 `json:"total_incoming"`
|
|
||||||
TotalOutgoing int64 `json:"total_outgoing"`
|
|
||||||
WeekOverWeekGrowth float64 `json:"week_over_week_growth"`
|
|
||||||
MonthOverMonthGrowth float64 `json:"month_over_month_growth"`
|
|
||||||
TotalThisWeek float64 `json:"total_this_week"`
|
|
||||||
TotalPending int64 `json:"total_pending,omitempty"`
|
|
||||||
TotalApproved int64 `json:"total_approved,omitempty"`
|
|
||||||
TotalRejected int64 `json:"total_rejected,omitempty"`
|
|
||||||
TotalArchived int64 `json:"total_archived,omitempty"`
|
|
||||||
AvgProcessingTime float64 `json:"avg_processing_time_hours,omitempty"`
|
|
||||||
CompletionRate float64 `json:"completion_rate,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatusDistribution represents letter distribution by status
|
|
||||||
type StatusDistribution struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Count int64 `json:"count"`
|
|
||||||
Percentage float64 `json:"percentage"`
|
|
||||||
Type string `json:"type"` // incoming or outgoing
|
|
||||||
}
|
|
||||||
|
|
||||||
// PriorityDistribution represents letter distribution by priority
|
|
||||||
type PriorityDistribution struct {
|
|
||||||
PriorityID string `json:"priority_id"`
|
|
||||||
PriorityName string `json:"priority_name"`
|
|
||||||
Level int `json:"level"`
|
|
||||||
Count int64 `json:"count"`
|
|
||||||
Percentage float64 `json:"percentage"`
|
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DepartmentStats represents statistics per department
|
|
||||||
type DepartmentStats struct {
|
|
||||||
DepartmentID uuid.UUID `json:"department_id"`
|
|
||||||
DepartmentName string `json:"department_name"`
|
|
||||||
DepartmentCode string `json:"department_code"`
|
|
||||||
IncomingCount int64 `json:"incoming_count"`
|
|
||||||
OutgoingCount int64 `json:"outgoing_count"`
|
|
||||||
PendingCount int64 `json:"pending_count"`
|
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
|
||||||
CompletionRate float64 `json:"completion_rate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MonthlyTrend represents monthly trend data
|
|
||||||
type MonthlyTrend struct {
|
|
||||||
Month string `json:"month"`
|
|
||||||
Year int `json:"year"`
|
|
||||||
IncomingCount int64 `json:"incoming_count"`
|
|
||||||
OutgoingCount int64 `json:"outgoing_count"`
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
GrowthRate float64 `json:"growth_rate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TopUserStats represents top users by letter activity
|
|
||||||
type TopUserStats struct {
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
UserName string `json:"user_name"`
|
|
||||||
UserEmail string `json:"user_email"`
|
|
||||||
Department string `json:"department"`
|
|
||||||
LetterCount int64 `json:"letter_count"`
|
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstitutionStats represents statistics per institution
|
|
||||||
type InstitutionStats struct {
|
|
||||||
InstitutionID uuid.UUID `json:"institution_id"`
|
|
||||||
InstitutionName string `json:"institution_name"`
|
|
||||||
InstitutionType string `json:"institution_type"`
|
|
||||||
IncomingCount int64 `json:"incoming_count"`
|
|
||||||
OutgoingCount int64 `json:"outgoing_count"`
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
LastActivity time.Time `json:"last_activity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApprovalMetrics represents approval-related metrics
|
|
||||||
type ApprovalMetrics struct {
|
|
||||||
TotalSubmitted int64 `json:"total_submitted"`
|
|
||||||
TotalApproved int64 `json:"total_approved"`
|
|
||||||
TotalRejected int64 `json:"total_rejected"`
|
|
||||||
TotalPending int64 `json:"total_pending"`
|
|
||||||
ApprovalRate float64 `json:"approval_rate"`
|
|
||||||
RejectionRate float64 `json:"rejection_rate"`
|
|
||||||
AvgApprovalTime float64 `json:"avg_approval_time_hours"`
|
|
||||||
AvgApprovalSteps float64 `json:"avg_approval_steps"`
|
|
||||||
BottleneckSteps []BottleneckStep `json:"bottleneck_steps"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BottleneckStep represents approval steps that cause delays
|
|
||||||
type BottleneckStep struct {
|
|
||||||
StepOrder int `json:"step_order"`
|
|
||||||
ApproverName string `json:"approver_name"`
|
|
||||||
AvgProcessTime float64 `json:"avg_process_time_hours"`
|
|
||||||
PendingCount int64 `json:"pending_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseTimeStats represents response time statistics
|
|
||||||
type ResponseTimeStats struct {
|
|
||||||
MinResponseTime float64 `json:"min_response_time_hours"`
|
|
||||||
MaxResponseTime float64 `json:"max_response_time_hours"`
|
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
|
||||||
MedianResponseTime float64 `json:"median_response_time_hours"`
|
|
||||||
P95ResponseTime float64 `json:"p95_response_time_hours"`
|
|
||||||
P99ResponseTime float64 `json:"p99_response_time_hours"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimpleDepartmentStats represents simplified department statistics
|
|
||||||
type SimpleDepartmentStats struct {
|
|
||||||
DepartmentID uuid.UUID `json:"department_id"`
|
|
||||||
Department string `json:"department"`
|
|
||||||
LetterCount int64 `json:"letter_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DailyActivity represents daily activity data
|
|
||||||
type DailyActivity struct {
|
|
||||||
Date string `json:"date"`
|
|
||||||
DayOfWeek string `json:"day_of_week"`
|
|
||||||
IncomingCount int64 `json:"incoming_count"`
|
|
||||||
OutgoingCount int64 `json:"outgoing_count"`
|
|
||||||
ApprovedCount int64 `json:"approved_count,omitempty"`
|
|
||||||
RejectedCount int64 `json:"rejected_count,omitempty"`
|
|
||||||
HourlyDistribution []HourlyActivity `json:"hourly_distribution,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HourlyActivity represents hourly activity within a day
|
|
||||||
type HourlyActivity struct {
|
|
||||||
Hour int `json:"hour"`
|
|
||||||
Count int64 `json:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LetterVolumeByTypeResponse represents letter volume grouped by type
|
|
||||||
type LetterVolumeByTypeResponse struct {
|
|
||||||
Incoming IncomingLetterVolume `json:"incoming"`
|
|
||||||
Outgoing OutgoingLetterVolume `json:"outgoing"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncomingLetterVolume represents incoming letter statistics
|
|
||||||
type IncomingLetterVolume struct {
|
|
||||||
Today int64 `json:"today"`
|
|
||||||
ThisWeek int64 `json:"this_week"`
|
|
||||||
ThisMonth int64 `json:"this_month"`
|
|
||||||
ThisYear int64 `json:"this_year"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutgoingLetterVolume represents outgoing letter statistics
|
|
||||||
type OutgoingLetterVolume struct {
|
|
||||||
Today int64 `json:"today"`
|
|
||||||
ThisWeek int64 `json:"this_week"`
|
|
||||||
ThisMonth int64 `json:"this_month"`
|
|
||||||
ThisYear int64 `json:"this_year"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DepartmentInfo struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DispositionRouteResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
FromDepartmentID uuid.UUID `json:"from_department_id"`
|
|
||||||
ToDepartmentID uuid.UUID `json:"to_department_id"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
|
|
||||||
// Department information
|
|
||||||
FromDepartment DepartmentInfo `json:"from_department"`
|
|
||||||
ToDepartment DepartmentInfo `json:"to_department"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateDispositionRouteRequest struct {
|
|
||||||
FromDepartmentID uuid.UUID `json:"from_department_id" binding:"required"`
|
|
||||||
ToDepartmentIDs []uuid.UUID `json:"to_department_ids" binding:"required,min=1"`
|
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
|
||||||
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BulkCreateDispositionRouteResponse response for bulk create/update operation
|
|
||||||
type BulkCreateDispositionRouteResponse struct {
|
|
||||||
Created int `json:"created"`
|
|
||||||
Updated int `json:"updated"`
|
|
||||||
Routes []DispositionRouteResponse `json:"routes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateDispositionRouteRequest struct {
|
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
|
||||||
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListDispositionRoutesResponse struct {
|
|
||||||
Routes []DispositionRouteResponse `json:"routes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DepartmentMapping represents department ID and name mapping
|
|
||||||
type DepartmentMapping struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DispositionRouteGroupedItem represents a single grouped route item with clean department structure
|
|
||||||
type DispositionRouteGroupedItem struct {
|
|
||||||
FromDepartment DepartmentMapping `json:"from_department"`
|
|
||||||
ToDepartments []DepartmentMapping `json:"to_departments"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDispositionRoutesGroupedResponse returns all routes grouped by from_department_id
|
|
||||||
type ListDispositionRoutesGroupedResponse struct {
|
|
||||||
Dispositions []DispositionRouteGroupedItem `json:"dispositions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDispositionRoutesDetailedResponse returns all routes with department details
|
|
||||||
type ListDispositionRoutesDetailedResponse struct {
|
|
||||||
Routes []DispositionRouteResponse `json:"routes"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
}
|
|
||||||
66
internal/contract/dukcapil_contract.go
Normal file
66
internal/contract/dukcapil_contract.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
// FaceMatchRequest is the inbound payload from clients of this service to
|
||||||
|
// trigger a Dukcapil 1:N face recognition lookup.
|
||||||
|
//
|
||||||
|
// Image must already be a base64 (no data:image prefix) representation of a
|
||||||
|
// jpg/png file. Threshold is forwarded to Dukcapil (1..20). IP is optional;
|
||||||
|
// when empty the configured default IP will be used.
|
||||||
|
type FaceMatchRequest struct {
|
||||||
|
TransactionID string `json:"transaction_id" validate:"required,max=20"`
|
||||||
|
TransactionSource string `json:"transaction_source" validate:"required,max=50"`
|
||||||
|
Threshold string `json:"threshold" validate:"required"`
|
||||||
|
Image string `json:"image" validate:"required"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DukcapilFaceRequest is the exact JSON body sent to the Dukcapil
|
||||||
|
// face-recognition endpoint (CALL_FN). user_id and password are RSA encrypted
|
||||||
|
// with the provided public key and base64 encoded.
|
||||||
|
type DukcapilFaceRequest struct {
|
||||||
|
TransactionID string `json:"transactionId"`
|
||||||
|
TransactionSource string `json:"transactionSource"`
|
||||||
|
Threshold string `json:"threshold"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DukcapilFaceResponse is the standard Dukcapil response envelope (success +
|
||||||
|
// most error variants share this shape). Some error variants use a different
|
||||||
|
// shape – clients should also inspect ErrorCode.
|
||||||
|
type DukcapilFaceResponse struct {
|
||||||
|
TID string `json:"tid"`
|
||||||
|
EncounterID interface{} `json:"encounter_id"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
ErrorCode string `json:"errorCode"`
|
||||||
|
FingerData interface{} `json:"finger_data"`
|
||||||
|
IrisData interface{} `json:"iris_data"`
|
||||||
|
FaceData interface{} `json:"face_data"`
|
||||||
|
RequestType string `json:"request_type"`
|
||||||
|
MaxResults int `json:"maxResults"`
|
||||||
|
FaceThreshold string `json:"faceThreshold"`
|
||||||
|
FingerThreshold int `json:"fingerThreshold"`
|
||||||
|
IrisThreshold int `json:"irisThreshold"`
|
||||||
|
Response string `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FaceMatchResponse is what we return to our API consumers.
|
||||||
|
type FaceMatchResponse struct {
|
||||||
|
TID string `json:"tid"`
|
||||||
|
ErrorCode string `json:"error_code"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
RequestType string `json:"request_type"`
|
||||||
|
Threshold string `json:"threshold"`
|
||||||
|
MaxResults int `json:"max_results"`
|
||||||
|
Matches []FaceMatchResult `json:"matches"`
|
||||||
|
Raw *DukcapilFaceResponse `json:"raw,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FaceMatchResult represents a single (NIK -> score) entry from the Dukcapil
|
||||||
|
// `response.face.FACE_T5` map.
|
||||||
|
type FaceMatchResult struct {
|
||||||
|
NIK string `json:"nik"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
}
|
||||||
@ -1,291 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SearchIncomingLettersRequest struct {
|
|
||||||
Query string `json:"query" form:"query"`
|
|
||||||
LetterNumber string `json:"letter_number" form:"letter_number"`
|
|
||||||
Subject string `json:"subject" form:"subject"`
|
|
||||||
Status string `json:"status" form:"status"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id" form:"priority_id"`
|
|
||||||
InstitutionID *uuid.UUID `json:"institution_id" form:"institution_id"`
|
|
||||||
CreatedBy *uuid.UUID `json:"created_by" form:"created_by"`
|
|
||||||
DateFrom *time.Time `json:"date_from" form:"date_from"`
|
|
||||||
DateTo *time.Time `json:"date_to" form:"date_to"`
|
|
||||||
Page int `json:"page" form:"page"`
|
|
||||||
Limit int `json:"limit" form:"limit"`
|
|
||||||
SortBy string `json:"sort_by" form:"sort_by"`
|
|
||||||
SortOrder string `json:"sort_order" form:"sort_order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchIncomingLettersResponse struct {
|
|
||||||
Letters []IncomingLetterResponse `json:"letters"`
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateIncomingLetterAttachment struct {
|
|
||||||
FileURL string `json:"file_url"`
|
|
||||||
FileName string `json:"file_name"`
|
|
||||||
FileType string `json:"file_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateIncomingLetterRequest struct {
|
|
||||||
LetterNumber string `json:"-"` // Generated by service layer
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
|
||||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
|
||||||
SenderName *string `json:"sender_name,omitempty"`
|
|
||||||
Addressee *string `json:"addressee,omitempty"`
|
|
||||||
ReceivedDate time.Time `json:"received_date"`
|
|
||||||
DueDate *time.Time `json:"due_date,omitempty"`
|
|
||||||
Type string `json:"type"` // UTAMA or TEMBUSAN
|
|
||||||
Attachments []CreateIncomingLetterAttachment `json:"attachments,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncomingLetterAttachmentResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
FileURL string `json:"file_url"`
|
|
||||||
FileName string `json:"file_name"`
|
|
||||||
FileType string `json:"file_type"`
|
|
||||||
UploadedAt time.Time `json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncomingLetterResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterNumber string `json:"letter_number"`
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Priority *PriorityResponse `json:"priority,omitempty"`
|
|
||||||
SenderInstitution *InstitutionResponse `json:"sender_institution,omitempty"`
|
|
||||||
SenderName *string `json:"sender_name,omitempty"`
|
|
||||||
Addressee *string `json:"addressee,omitempty"`
|
|
||||||
ReceivedDate time.Time `json:"received_date"`
|
|
||||||
DueDate *time.Time `json:"due_date,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Attachments []IncomingLetterAttachmentResponse `json:"attachments"`
|
|
||||||
IsRead bool `json:"is_read"`
|
|
||||||
Dispositions []EnhancedDispositionResponse `json:"dispositions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateIncomingLetterRequest struct {
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject *string `json:"subject,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
|
||||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
|
||||||
SenderName *string `json:"sender_name,omitempty"`
|
|
||||||
Addressee *string `json:"addressee,omitempty"`
|
|
||||||
ReceivedDate *time.Time `json:"received_date,omitempty"`
|
|
||||||
DueDate *time.Time `json:"due_date,omitempty"`
|
|
||||||
Type *string `json:"type,omitempty"`
|
|
||||||
Status *string `json:"status,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListIncomingLettersRequest struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Status *string `json:"status,omitempty"`
|
|
||||||
Query *string `json:"query,omitempty"`
|
|
||||||
DepartmentID *uuid.UUID
|
|
||||||
IsRead *bool `json:"is_read,omitempty"`
|
|
||||||
PriorityIDs []uuid.UUID `json:"priority_ids,omitempty"`
|
|
||||||
IsDispositioned *bool `json:"is_dispositioned,omitempty"`
|
|
||||||
IsArchived *bool `json:"is_archived,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListIncomingLettersResponse struct {
|
|
||||||
Letters []IncomingLetterResponse `json:"letters"`
|
|
||||||
Pagination PaginationResponse `json:"pagination"`
|
|
||||||
TotalUnread int `json:"total_unread"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterUnreadCountResponse struct {
|
|
||||||
IncomingLetter struct {
|
|
||||||
Unread int `json:"unread"`
|
|
||||||
} `json:"incoming_letter"`
|
|
||||||
OutgoingLetter struct {
|
|
||||||
Unread int `json:"unread"`
|
|
||||||
} `json:"outgoing_letter"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarkLetterReadResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateDispositionActionSelection struct {
|
|
||||||
ActionID uuid.UUID `json:"action_id"`
|
|
||||||
Note *string `json:"note,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateLetterDispositionRequest struct {
|
|
||||||
FromDepartment uuid.UUID `json:"from_department"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
SelectedActions []CreateDispositionActionSelection `json:"selected_actions,omitempty"`
|
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DispositionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListDispositionsResponse struct {
|
|
||||||
Dispositions []DispositionResponse `json:"dispositions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnhancedDispositionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Department DepartmentResponse `json:"department"`
|
|
||||||
Departments []DispositionDepartmentResponse `json:"departments"`
|
|
||||||
Actions []DispositionActionSelectionResponse `json:"actions"`
|
|
||||||
DispositionNotes []DispositionNoteResponse `json:"disposition_notes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DispositionDepartmentResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
DepartmentID uuid.UUID `json:"department_id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
Department *DepartmentResponse `json:"department,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DispositionActionSelectionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ActionID uuid.UUID `json:"action_id"`
|
|
||||||
Action *DispositionActionResponse `json:"action,omitempty"`
|
|
||||||
Note *string `json:"note,omitempty"`
|
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DispositionNoteResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
|
||||||
Note string `json:"note"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
User *UserResponse `json:"user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListEnhancedDispositionsResponse struct {
|
|
||||||
Dispositions []EnhancedDispositionResponse `json:"dispositions"`
|
|
||||||
Discussions []LetterDiscussionResponse `json:"discussions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetDepartmentDispositionStatusRequest struct {
|
|
||||||
LetterIncomingID uuid.UUID `json:"letter_incoming_id"`
|
|
||||||
DepartmentID uuid.UUID `json:"department_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DepartmentDispositionStatusResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
Letter *IncomingLetterResponse `json:"letter,omitempty"`
|
|
||||||
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
|
|
||||||
FromDepartment *DepartmentResponse `json:"from_department,omitempty"`
|
|
||||||
ToDepartmentID uuid.UUID `json:"to_department_id"`
|
|
||||||
ToDepartment *DepartmentResponse `json:"to_department,omitempty"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListDepartmentDispositionStatusResponse struct {
|
|
||||||
Dispositions []DepartmentDispositionStatusResponse `json:"dispositions"`
|
|
||||||
Pagination PaginationResponse `json:"pagination"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateDispositionStatusRequest struct {
|
|
||||||
LetterIncomingID uuid.UUID `json:"letter_incoming_id"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateLetterDiscussionRequest struct {
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateLetterDiscussionRequest struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterDiscussionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
|
||||||
|
|
||||||
// Preloaded user profile who created the discussion
|
|
||||||
User *UserResponse `json:"user,omitempty"`
|
|
||||||
|
|
||||||
// Preloaded user profiles for mentions
|
|
||||||
MentionedUsers []UserResponse `json:"mentioned_users,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetLetterCTARequest struct {
|
|
||||||
LetterIncomingID uuid.UUID `json:"letter_incoming_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterCTAAction struct {
|
|
||||||
Type string `json:"type"` // "create_disposition", "update_status", "view"
|
|
||||||
Label string `json:"label"` // Human-readable label for the action
|
|
||||||
Path string `json:"path"` // API endpoint path
|
|
||||||
Method string `json:"method"` // HTTP method: GET, POST, PUT, etc.
|
|
||||||
Description string `json:"description"` // Description of what this action does
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterCTAResponse struct {
|
|
||||||
LetterIncomingID uuid.UUID `json:"letter_incoming_id"`
|
|
||||||
Actions []LetterCTAAction `json:"actions"`
|
|
||||||
DispositionID *uuid.UUID `json:"disposition_id,omitempty"`
|
|
||||||
CurrentStatus *string `json:"current_status,omitempty"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BulkArchiveLettersRequest struct {
|
|
||||||
LetterIDs []uuid.UUID `json:"letter_ids" binding:"required,min=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BulkArchiveLettersResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
ArchivedCount int `json:"archived_count"`
|
|
||||||
}
|
|
||||||
@ -1,385 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SearchOutgoingLettersRequest struct {
|
|
||||||
Query string `json:"query" form:"query"`
|
|
||||||
LetterNumber string `json:"letter_number" form:"letter_number"`
|
|
||||||
Subject string `json:"subject" form:"subject"`
|
|
||||||
Status string `json:"status" form:"status"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id" form:"priority_id"`
|
|
||||||
InstitutionID *uuid.UUID `json:"institution_id" form:"institution_id"`
|
|
||||||
CreatedBy *uuid.UUID `json:"created_by" form:"created_by"`
|
|
||||||
DateFrom *time.Time `json:"date_from" form:"date_from"`
|
|
||||||
DateTo *time.Time `json:"date_to" form:"date_to"`
|
|
||||||
Page int `json:"page" form:"page"`
|
|
||||||
Limit int `json:"limit" form:"limit"`
|
|
||||||
SortBy string `json:"sort_by" form:"sort_by"`
|
|
||||||
SortOrder string `json:"sort_order" form:"sort_order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchOutgoingLettersResponse struct {
|
|
||||||
Letters []OutgoingLetterResponse `json:"letters"`
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOutgoingLetterRecipient struct {
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
|
||||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
|
||||||
IsPrimary bool `json:"is_primary"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Flag *string `json:"flag,omitempty"`
|
|
||||||
IsArchived bool `json:"is_archived"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOutgoingLetterAttachment struct {
|
|
||||||
FileURL string `json:"file_url" validate:"required"`
|
|
||||||
FileName string `json:"file_name" validate:"required"`
|
|
||||||
FileType string `json:"file_type" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOutgoingLetterRequest struct {
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject string `json:"subject" validate:"required"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
|
||||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
|
||||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
|
||||||
IssueDate time.Time `json:"issue_date" validate:"required"`
|
|
||||||
Attachments []CreateOutgoingLetterAttachment `json:"attachments,omitempty"`
|
|
||||||
UserID uuid.UUID
|
|
||||||
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutgoingLetterRecipientResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
|
||||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
|
||||||
IsPrimary bool `json:"is_primary"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
Flag *string `json:"flag,omitempty"`
|
|
||||||
IsArchived bool `json:"is_archived"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
User *UserResponse `json:"user,omitempty"`
|
|
||||||
Department *DepartmentResponse `json:"department,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutgoingLetterAttachmentResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
FileURL string `json:"file_url"`
|
|
||||||
FileName string `json:"file_name"`
|
|
||||||
FileType string `json:"file_type"`
|
|
||||||
UploadedAt time.Time `json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutgoingLetterApprovalResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
StepOrder int `json:"step_order"`
|
|
||||||
ParallelGroup int `json:"parallel_group"`
|
|
||||||
IsRequired bool `json:"is_required"`
|
|
||||||
ApproverID *uuid.UUID `json:"approver_id,omitempty"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Remarks *string `json:"remarks,omitempty"`
|
|
||||||
ActedAt *time.Time `json:"acted_at,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutgoingLetterResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterNumber string `json:"letter_number"`
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
|
||||||
Priority *PriorityResponse `json:"priority,omitempty"`
|
|
||||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
|
||||||
ReceiverInstitution *InstitutionResponse `json:"receiver_institution,omitempty"`
|
|
||||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
|
||||||
IssueDate time.Time `json:"issue_date"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
|
||||||
RevisionNumber int `json:"revision_number"`
|
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
|
||||||
CreatedName string `json:"created_name"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
IsRead bool `json:"is_read"`
|
|
||||||
Recipients []OutgoingLetterRecipientResponse `json:"recipients,omitempty"`
|
|
||||||
Attachments []OutgoingLetterAttachmentResponse `json:"attachments,omitempty"`
|
|
||||||
FinalAttachments []OutgoingLetterAttachmentResponse `json:"final_attachments,omitempty"`
|
|
||||||
Approvals []OutgoingLetterApprovalResponse `json:"approvals,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateOutgoingLetterRequest struct {
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject *string `json:"subject,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
|
||||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
|
||||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
|
||||||
IssueDate *time.Time `json:"issue_date,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListOutgoingLettersRequest struct {
|
|
||||||
Page int `form:"page" json:"page"`
|
|
||||||
Limit int `form:"limit" json:"limit"`
|
|
||||||
Status string `form:"status" json:"status,omitempty"`
|
|
||||||
Query string `form:"q" json:"query,omitempty"`
|
|
||||||
CreatedBy *uuid.UUID `form:"created_by" json:"created_by,omitempty"`
|
|
||||||
DepartmentID *uuid.UUID `form:"department_id" json:"department_id,omitempty"`
|
|
||||||
ReceiverInstitutionID *uuid.UUID `form:"receiver_institution_id" json:"receiver_institution_id,omitempty"`
|
|
||||||
FromDate string `form:"from_date" json:"from_date,omitempty"`
|
|
||||||
ToDate string `form:"to_date" json:"to_date,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `form:"priority_id" json:"priority_id,omitempty"`
|
|
||||||
PriorityIDs []uuid.UUID `json:"priority_ids,omitempty"`
|
|
||||||
SortBy string `form:"sort_by" json:"sort_by,omitempty"`
|
|
||||||
SortOrder string `form:"sort_order" json:"sort_order,omitempty"`
|
|
||||||
IsArchived *bool `form:"is_archived" json:"is_archived,omitempty"`
|
|
||||||
IsRead *bool `form:"is_read,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListOutgoingLettersResponse struct {
|
|
||||||
Items []*OutgoingLetterResponse `json:"items"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApproveLetterRequest struct {
|
|
||||||
Remarks *string `json:"remarks,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RejectLetterRequest struct {
|
|
||||||
Reason string `json:"reason" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReviseLetterRequest struct {
|
|
||||||
FileURL string `json:"file_url" validate:"required"`
|
|
||||||
FileName string `json:"file_name" validate:"required"`
|
|
||||||
FileType string `json:"file_type" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddRecipientsRequest struct {
|
|
||||||
Recipients []CreateOutgoingLetterRecipient `json:"recipients" validate:"required,dive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateRecipientRequest struct {
|
|
||||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
|
||||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
|
||||||
IsPrimary bool `json:"is_primary"`
|
|
||||||
Status *string `json:"status,omitempty"`
|
|
||||||
Flag *string `json:"flag,omitempty"`
|
|
||||||
IsArchived *bool `json:"is_archived,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddAttachmentsRequest struct {
|
|
||||||
Attachments []CreateOutgoingLetterAttachment `json:"attachments" validate:"required,dive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateDiscussionAttachment struct {
|
|
||||||
FileURL string `json:"file_url" validate:"required"`
|
|
||||||
FileName string `json:"file_name" validate:"required"`
|
|
||||||
FileType string `json:"file_type" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateDiscussionRequest struct {
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
Message string `json:"message" validate:"required"`
|
|
||||||
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
|
||||||
Attachments []CreateDiscussionAttachment `json:"attachments,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateDiscussionRequest struct {
|
|
||||||
Message string `json:"message" validate:"required"`
|
|
||||||
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscussionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApprovalFlowRequest struct {
|
|
||||||
DepartmentID uuid.UUID `json:"department_id" validate:"required"`
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
Steps []ApprovalFlowStepRequest `json:"steps" validate:"required,dive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApprovalFlowStepRequest struct {
|
|
||||||
StepOrder int `json:"step_order" validate:"required,min=1"`
|
|
||||||
ParallelGroup int `json:"parallel_group" validate:"min=1"`
|
|
||||||
ApproverRoleID *uuid.UUID `json:"approver_role_id,omitempty"`
|
|
||||||
ApproverUserID *uuid.UUID `json:"approver_user_id,omitempty"`
|
|
||||||
Required bool `json:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApprovalFlowResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
DepartmentID uuid.UUID `json:"department_id"`
|
|
||||||
Department *DepartmentResponse `json:"department,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
Steps []ApprovalFlowStepResponse `json:"steps,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApprovalFlowStepResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
StepOrder int `json:"step_order"`
|
|
||||||
ParallelGroup int `json:"parallel_group"`
|
|
||||||
ApproverRoleID *uuid.UUID `json:"approver_role_id,omitempty"`
|
|
||||||
ApproverRole *RoleResponse `json:"approver_role,omitempty"`
|
|
||||||
ApproverUserID *uuid.UUID `json:"approver_user_id,omitempty"`
|
|
||||||
ApproverUser *UserResponse `json:"approver_user,omitempty"`
|
|
||||||
Required bool `json:"required"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListApprovalFlowsRequest struct {
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
Search *string `json:"search"`
|
|
||||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListApprovalFlowsResponse struct {
|
|
||||||
Items []*ApprovalFlowResponse `json:"items"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Letter Approval Information for Approver
|
|
||||||
type LetterApprovalInfoResponse struct {
|
|
||||||
IsApproverOnActiveStep bool `json:"is_approver_on_active_step"`
|
|
||||||
DecisionStatus string `json:"decision_status"`
|
|
||||||
CanApprove bool `json:"can_approve"`
|
|
||||||
Actions []ApprovalAction `json:"actions"`
|
|
||||||
NotesVisibility string `json:"notes_visibility"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApprovalAction struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Href string `json:"href"`
|
|
||||||
Method string `json:"method"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutgoingLetterApprovalDiscussionsResponse combines approvals and discussions for outgoing letters
|
|
||||||
type OutgoingLetterApprovalDiscussionsResponse struct {
|
|
||||||
Approvals []EnhancedOutgoingLetterApprovalResponse `json:"approvals"`
|
|
||||||
Discussions []OutgoingLetterDiscussionResponse `json:"discussions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnhancedOutgoingLetterApprovalResponse includes approval details with related data
|
|
||||||
type EnhancedOutgoingLetterApprovalResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
StepID uuid.UUID `json:"step_id"`
|
|
||||||
StepOrder int `json:"step_order"`
|
|
||||||
ParallelGroup int `json:"parallel_group"`
|
|
||||||
IsRequired bool `json:"is_required"`
|
|
||||||
ApproverID *uuid.UUID `json:"approver_id,omitempty"`
|
|
||||||
RevisionNumber int `json:"revision_number"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Remarks *string `json:"remarks,omitempty"`
|
|
||||||
ActedAt *time.Time `json:"acted_at,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
Step *ApprovalFlowStepResponse `json:"step,omitempty"`
|
|
||||||
Approver *UserResponse `json:"approver,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutgoingLetterApprovalRevisionNumberResponse struct {
|
|
||||||
RevisionNumber int `json:"revision_number"`
|
|
||||||
Approvals []EnhancedOutgoingLetterApprovalResponse `json:"approvals"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLetterApprovalsResponse represents the list of approvals for a letter
|
|
||||||
type GetLetterApprovalsResponse struct {
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
LetterNumber string `json:"letter_number"`
|
|
||||||
LetterStatus string `json:"letter_status"`
|
|
||||||
TotalSteps int `json:"total_steps"`
|
|
||||||
CurrentStep int `json:"current_step"`
|
|
||||||
CurrentRevisionNumber int `json:"current_revision_number"`
|
|
||||||
Approvals []OutgoingLetterApprovalRevisionNumberResponse `json:"approvals"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutgoingLetterDiscussionResponse represents a discussion on an outgoing letter
|
|
||||||
type OutgoingLetterDiscussionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
|
||||||
User *UserResponse `json:"user,omitempty"`
|
|
||||||
MentionedUsers []UserResponse `json:"mentioned_users,omitempty"`
|
|
||||||
Attachments []OutgoingLetterDiscussionAttachmentResponse `json:"attachments,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutgoingLetterDiscussionAttachmentResponse represents an attachment in a discussion
|
|
||||||
type OutgoingLetterDiscussionAttachmentResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
DiscussionID uuid.UUID `json:"discussion_id"`
|
|
||||||
FileURL string `json:"file_url"`
|
|
||||||
FileName string `json:"file_name"`
|
|
||||||
FileType string `json:"file_type"`
|
|
||||||
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
|
|
||||||
UploadedAt time.Time `json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimelineEvent represents a single event in the approval timeline
|
|
||||||
type TimelineEvent struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"` // "approval", "discussion", "submission", "rejection"
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
Actor *UserResponse `json:"actor,omitempty"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
StepOrder int `json:"step_order,omitempty"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApprovalTimelineResponse represents the complete timeline for a letter
|
|
||||||
type ApprovalTimelineResponse struct {
|
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
|
||||||
LetterNumber string `json:"letter_number"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
Timeline []TimelineEvent `json:"timeline"`
|
|
||||||
Summary TimelineSummary `json:"summary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimelineSummary provides overview statistics for the timeline
|
|
||||||
type TimelineSummary struct {
|
|
||||||
TotalSteps int `json:"total_steps"`
|
|
||||||
CompletedSteps int `json:"completed_steps"`
|
|
||||||
PendingSteps int `json:"pending_steps"`
|
|
||||||
CurrentStep int `json:"current_step"`
|
|
||||||
TotalDuration string `json:"total_duration"`
|
|
||||||
AverageStepTime string `json:"average_step_time"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import "github.com/google/uuid"
|
|
||||||
|
|
||||||
type TriggerNotificationRequest struct {
|
|
||||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
|
||||||
TemplateID string `json:"template_id" validate:"required"`
|
|
||||||
TemplateData map[string]interface{} `json:"template_data,omitempty"`
|
|
||||||
To *NotificationTo `json:"to,omitempty"`
|
|
||||||
Overrides *NotificationOverrides `json:"overrides,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationTo struct {
|
|
||||||
Email string `json:"email,omitempty"`
|
|
||||||
Phone string `json:"phone,omitempty"`
|
|
||||||
SubscriberID string `json:"subscriber_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationOverrides struct {
|
|
||||||
Email *EmailOverride `json:"email,omitempty"`
|
|
||||||
SMS *SMSOverride `json:"sms,omitempty"`
|
|
||||||
InApp *InAppOverride `json:"in_app,omitempty"`
|
|
||||||
Push *PushOverride `json:"push,omitempty"`
|
|
||||||
Chat *ChatOverride `json:"chat,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailOverride struct {
|
|
||||||
Subject string `json:"subject,omitempty"`
|
|
||||||
Body string `json:"body,omitempty"`
|
|
||||||
From string `json:"from,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SMSOverride struct {
|
|
||||||
Content string `json:"content,omitempty"`
|
|
||||||
From string `json:"from,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InAppOverride struct {
|
|
||||||
Content string `json:"content,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PushOverride struct {
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
Content string `json:"content,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChatOverride struct {
|
|
||||||
Content string `json:"content,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerNotificationResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
TransactionID string `json:"transaction_id,omitempty"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BulkTriggerNotificationRequest struct {
|
|
||||||
TemplateID string `json:"template_id" validate:"required"`
|
|
||||||
UserIDs []uuid.UUID `json:"user_ids" validate:"required,min=1"`
|
|
||||||
TemplateData map[string]interface{} `json:"template_data,omitempty"`
|
|
||||||
Overrides *NotificationOverrides `json:"overrides,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BulkTriggerNotificationResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
TotalSent int `json:"total_sent"`
|
|
||||||
TotalFailed int `json:"total_failed"`
|
|
||||||
Results []NotificationResult `json:"results,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationResult struct {
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
Success bool `json:"success"`
|
|
||||||
TransactionID string `json:"transaction_id,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetSubscriberRequest struct {
|
|
||||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetSubscriberResponse struct {
|
|
||||||
SubscriberID string `json:"subscriber_id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
Phone string `json:"phone,omitempty"`
|
|
||||||
Avatar string `json:"avatar,omitempty"`
|
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
|
||||||
Channels []ChannelCredentials `json:"channels,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelCredentials struct {
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
Credentials map[string]interface{} `json:"credentials"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateSubscriberChannelRequest struct {
|
|
||||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
|
||||||
Channel string `json:"channel" validate:"required,oneof=email sms push chat in_app"`
|
|
||||||
Credentials map[string]interface{} `json:"credentials" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateSubscriberChannelResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
@ -1,178 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OnlyOffice callback status codes
|
|
||||||
const (
|
|
||||||
OnlyOfficeStatusEditing = 1 // Document is being edited
|
|
||||||
OnlyOfficeStatusReady = 2 // Document is ready for saving
|
|
||||||
OnlyOfficeStatusSaveError = 3 // Document saving error occurred
|
|
||||||
OnlyOfficeStatusClosed = 4 // Document is closed with no changes
|
|
||||||
OnlyOfficeStatusForceSave = 6 // Document is being edited, but saved forcefully
|
|
||||||
OnlyOfficeStatusForceSaveError = 7 // Error during force save
|
|
||||||
)
|
|
||||||
|
|
||||||
// OnlyOfficeCallbackRequest represents the callback payload from OnlyOffice Document Server
|
|
||||||
type OnlyOfficeCallbackRequest struct {
|
|
||||||
Key string `json:"key"` // Document identifier
|
|
||||||
Status int `json:"status"` // Status code (1-7)
|
|
||||||
URL string `json:"url,omitempty"` // Document URL when status is 2 or 6
|
|
||||||
ChangesURL string `json:"changesurl,omitempty"` // URL to document changes
|
|
||||||
History *OnlyOfficeHistory `json:"history,omitempty"`
|
|
||||||
Users []string `json:"users,omitempty"` // Users currently editing
|
|
||||||
Actions []OnlyOfficeAction `json:"actions,omitempty"` // User actions
|
|
||||||
LastSave string `json:"lastsave,omitempty"` // Last save time
|
|
||||||
NotModified bool `json:"notmodified,omitempty"` // Document not modified flag
|
|
||||||
ForceSaveType int `json:"forcesavetype,omitempty"` // Force save type
|
|
||||||
UserData string `json:"userdata,omitempty"` // Custom user data
|
|
||||||
Token string `json:"token,omitempty"` // JWT token from OnlyOffice
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeHistory represents document history information
|
|
||||||
type OnlyOfficeHistory struct {
|
|
||||||
ServerVersion string `json:"serverVersion"`
|
|
||||||
Changes []OnlyOfficeHistoryChange `json:"changes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeHistoryChange represents a single change in history
|
|
||||||
type OnlyOfficeHistoryChange struct {
|
|
||||||
User OnlyOfficeUser `json:"user"`
|
|
||||||
Created string `json:"created"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeUser represents user information in OnlyOffice
|
|
||||||
type OnlyOfficeUser struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeAction represents user actions
|
|
||||||
type OnlyOfficeAction struct {
|
|
||||||
Type int `json:"type"` // Action type (0: user connected, 1: user disconnected)
|
|
||||||
UserID string `json:"userid"` // User identifier
|
|
||||||
User OnlyOfficeUser `json:"user"` // User information
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeCallbackResponse is the required response format for OnlyOffice
|
|
||||||
type OnlyOfficeCallbackResponse struct {
|
|
||||||
Error int `json:"error"` // 0 = success, 1 = document key not found, 2 = callback URL error, 3 = internal server error
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeDocument represents the document section of OnlyOffice config
|
|
||||||
type OnlyOfficeDocument struct {
|
|
||||||
FileType string `json:"fileType"` // Document file extension
|
|
||||||
Key string `json:"key"` // Unique document identifier
|
|
||||||
Title string `json:"title"` // Document title
|
|
||||||
URL string `json:"url"` // Document URL
|
|
||||||
Permissions *OnlyOfficePermissions `json:"permissions,omitempty"`
|
|
||||||
Info *OnlyOfficeDocumentInfo `json:"info,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeDocumentInfo represents additional document information
|
|
||||||
type OnlyOfficeDocumentInfo struct {
|
|
||||||
Owner string `json:"owner,omitempty"`
|
|
||||||
Uploaded string `json:"uploaded,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeConfigRequest represents the proper OnlyOffice configuration format
|
|
||||||
type OnlyOfficeConfigRequest struct {
|
|
||||||
Document *OnlyOfficeDocument `json:"document"`
|
|
||||||
DocumentType string `json:"documentType"` // text, spreadsheet, presentation
|
|
||||||
EditorConfig *OnlyOfficeEditorConfig `json:"editorConfig"`
|
|
||||||
Type string `json:"type,omitempty"` // desktop, mobile, embedded
|
|
||||||
Token string `json:"token,omitempty"` // JWT token for security
|
|
||||||
Width string `json:"width,omitempty"`
|
|
||||||
Height string `json:"height,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeUserConfig represents user configuration for OnlyOffice
|
|
||||||
type OnlyOfficeUserConfig struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficePermissions represents document permissions
|
|
||||||
type OnlyOfficePermissions struct {
|
|
||||||
Comment bool `json:"comment"`
|
|
||||||
Download bool `json:"download"`
|
|
||||||
Edit bool `json:"edit"`
|
|
||||||
FillForms bool `json:"fillForms"`
|
|
||||||
ModifyContentControl bool `json:"modifyContentControl"`
|
|
||||||
ModifyFilter bool `json:"modifyFilter"`
|
|
||||||
Print bool `json:"print"`
|
|
||||||
Review bool `json:"review"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeCustomization represents UI customization options
|
|
||||||
type OnlyOfficeCustomization struct {
|
|
||||||
Autosave bool `json:"autosave"`
|
|
||||||
Comments bool `json:"comments"`
|
|
||||||
CompactHeader bool `json:"compactHeader"`
|
|
||||||
CompactToolbar bool `json:"compactToolbar"`
|
|
||||||
ForceSave bool `json:"forcesave"`
|
|
||||||
ShowReviewChanges bool `json:"showReviewChanges"`
|
|
||||||
Zoom int `json:"zoom"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeEditorConfig represents editor configuration
|
|
||||||
type OnlyOfficeEditorConfig struct {
|
|
||||||
CallbackURL string `json:"callbackUrl"`
|
|
||||||
Lang string `json:"lang"`
|
|
||||||
Mode string `json:"mode"` // edit, view
|
|
||||||
User *OnlyOfficeUserConfig `json:"user,omitempty"`
|
|
||||||
Customization *OnlyOfficeCustomization `json:"customization,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Document session tracking for OnlyOffice
|
|
||||||
type DocumentSession struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
DocumentID uuid.UUID `json:"document_id"`
|
|
||||||
DocumentKey string `json:"document_key"` // OnlyOffice document key
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
Status int `json:"status"` // Current OnlyOffice status
|
|
||||||
IsLocked bool `json:"is_locked"` // Document lock status
|
|
||||||
LockedBy *uuid.UUID `json:"locked_by,omitempty"`
|
|
||||||
LockedAt *time.Time `json:"locked_at,omitempty"`
|
|
||||||
LastSavedAt *time.Time `json:"last_saved_at,omitempty"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DocumentVersion for tracking document versions
|
|
||||||
type DocumentVersion struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
DocumentID uuid.UUID `json:"document_id"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
FileURL string `json:"file_url"`
|
|
||||||
FileSize int64 `json:"file_size"`
|
|
||||||
ChangesURL *string `json:"changes_url,omitempty"`
|
|
||||||
SavedBy uuid.UUID `json:"saved_by"`
|
|
||||||
SavedAt time.Time `json:"saved_at"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
Comments *string `json:"comments,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEditorConfigRequest represents request to get OnlyOffice editor configuration
|
|
||||||
type GetEditorConfigRequest struct {
|
|
||||||
DocumentID uuid.UUID `json:"document_id"`
|
|
||||||
DocumentType string `json:"document_type"` // letter_attachment, outgoing_attachment, etc.
|
|
||||||
Mode string `json:"mode"` // edit, view
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEditorConfigResponse represents OnlyOffice editor configuration response
|
|
||||||
type GetEditorConfigResponse struct {
|
|
||||||
DocumentServerURL string `json:"document_server_url"`
|
|
||||||
Config *OnlyOfficeConfigRequest `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOfficeConfigInfo represents the OnlyOffice configuration information
|
|
||||||
type OnlyOfficeConfigInfo struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PermissionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Action string `json:"action,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreatePermissionRequest struct {
|
|
||||||
Code string `json:"code"` // unique
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdatePermissionRequest struct {
|
|
||||||
Code *string `json:"code,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListPermissionsResponse struct {
|
|
||||||
Permissions []PermissionResponse `json:"permissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoleWithPermissionsResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Permissions []PermissionResponse `json:"permissions"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateRoleRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PermissionCodes []string `json:"permission_codes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateRoleRequest struct {
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Code *string `json:"code,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PermissionCodes *[]string `json:"permission_codes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListRolesResponse struct {
|
|
||||||
Roles []RoleWithPermissionsResponse `json:"roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module contracts
|
|
||||||
type ModuleResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModuleWithPermissionsResponse struct {
|
|
||||||
Module ModuleResponse `json:"module"`
|
|
||||||
Permissions []PermissionResponse `json:"permissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PermissionsGroupedResponse struct {
|
|
||||||
Data []ModuleWithPermissionsResponse `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// New Role contracts for the required API
|
|
||||||
type ModulePermissionInput struct {
|
|
||||||
Module string `json:"module" binding:"required"`
|
|
||||||
Actions []string `json:"actions" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOrUpdateRoleRequest struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Code string `json:"code" binding:"required"`
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
Permissions []ModulePermissionInput `json:"permissions" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoleDetailResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Permissions []RolePermissionModuleResponse `json:"permissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RolePermissionModuleResponse struct {
|
|
||||||
Module ModuleResponse `json:"module"`
|
|
||||||
Actions []PermissionActionResponse `json:"actions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PermissionActionResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CreateRepositoryAttachmentRequest struct {
|
|
||||||
FileURL string `json:"file_url" validate:"required"`
|
|
||||||
FileName string `json:"file_name" validate:"required"`
|
|
||||||
FileType string `json:"file_type" validate:"required"`
|
|
||||||
Category string `json:"category" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RepositoryAttachmentsResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
FileURL string `json:"file_url"`
|
|
||||||
FileName string `json:"file_name"`
|
|
||||||
FileType string `json:"file_type"`
|
|
||||||
Category string `json:"category"`
|
|
||||||
UploadBy uuid.UUID `json:"upload_by"`
|
|
||||||
UploadAt time.Time `json:"upload_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListRepositoryAttachmentsResponse struct {
|
|
||||||
Attachments []RepositoryAttachmentsResponse `json:"attachments"`
|
|
||||||
Pagination PaginationResponse `json:"pagination"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListRepositoryAttachmentsRequest struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Search *string `json:"search,omitempty"`
|
|
||||||
}
|
|
||||||
@ -10,17 +10,12 @@ type CreateUserRequest struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
Password string `json:"password" validate:"required,min=6"`
|
Password string `json:"password" validate:"required,min=6"`
|
||||||
RoleID *uuid.UUID `json:"role_id,omitempty" validate:"required"`
|
|
||||||
DepartmentIDs []uuid.UUID `json:"department_ids,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserRequest struct {
|
type UpdateUserRequest struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
||||||
Role *uuid.UUID `json:"role,omitempty"`
|
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Permissions *map[string]interface{} `json:"permissions,omitempty"`
|
|
||||||
DepartmentIDs *[]uuid.UUID `json:"department_ids,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangePasswordRequest struct {
|
type ChangePasswordRequest struct {
|
||||||
@ -28,14 +23,6 @@ type ChangePasswordRequest struct {
|
|||||||
NewPassword string `json:"new_password" validate:"required,min=6"`
|
NewPassword string `json:"new_password" validate:"required,min=6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangeUserPasswordRequest struct {
|
|
||||||
NewPassword string `json:"new_password" validate:"required,min=6"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateUserOutletRequest struct {
|
|
||||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password" validate:"required"`
|
||||||
@ -44,10 +31,11 @@ type LoginRequest struct {
|
|||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
User UserResponse `json:"user"`
|
User *UserResponse `json:"user"`
|
||||||
Roles []RoleResponse `json:"roles"`
|
}
|
||||||
Permissions []string `json:"permissions"`
|
|
||||||
Departments []DepartmentResponse `json:"departments"`
|
type RefreshTokenRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
@ -57,140 +45,16 @@ type UserResponse struct {
|
|||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Roles []RoleResponse `json:"roles,omitempty"`
|
|
||||||
DepartmentResponse []DepartmentResponse `json:"department_response"`
|
|
||||||
Profile *UserProfileResponse `json:"profile,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListUsersRequest struct {
|
type ListUsersRequest struct {
|
||||||
Page int `json:"page" validate:"min=1"`
|
Page int `json:"page" validate:"min=1"`
|
||||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
Role *string `json:"role,omitempty"`
|
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Search *string `json:"search,omitempty"`
|
Search *string `json:"search,omitempty"`
|
||||||
RoleCode *string `json:"role_code,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListUsersResponse struct {
|
type PaginatedUserResponse struct {
|
||||||
Users []UserResponse `json:"users"`
|
Users []UserResponse `json:"users"`
|
||||||
Pagination PaginationResponse `json:"pagination"`
|
Pagination PaginationResponse `json:"pagination"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoleResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DepartmentResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListDepartmentsRequest struct {
|
|
||||||
Search string `json:"search,omitempty" form:"search"`
|
|
||||||
Page int `json:"page,omitempty" form:"page"`
|
|
||||||
Limit int `json:"limit,omitempty" form:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListDepartmentsResponse struct {
|
|
||||||
Departments []DepartmentResponse `json:"departments"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateDepartmentRequest struct {
|
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
|
||||||
Code string `json:"code" validate:"required,min=1,max=50"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateDepartmentRequest struct {
|
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
|
||||||
Code *string `json:"code,omitempty" validate:"omitempty,min=1,max=50"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetDepartmentResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
ParentName *string `json:"parent_name,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DepartmentNode struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Level int `json:"level"`
|
|
||||||
Children []*DepartmentNode `json:"children,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrganizationalChartResponse struct {
|
|
||||||
Chart []*DepartmentNode `json:"chart"`
|
|
||||||
TotalNodes int `json:"total_nodes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserProfileResponse struct {
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
FullName string `json:"full_name"`
|
|
||||||
DisplayName *string `json:"display_name,omitempty"`
|
|
||||||
Phone *string `json:"phone,omitempty"`
|
|
||||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
|
||||||
JobTitle *string `json:"job_title,omitempty"`
|
|
||||||
EmployeeNo *string `json:"employee_no,omitempty"`
|
|
||||||
Bio *string `json:"bio,omitempty"`
|
|
||||||
Timezone string `json:"timezone"`
|
|
||||||
Locale string `json:"locale"`
|
|
||||||
Preferences map[string]interface{} `json:"preferences"`
|
|
||||||
NotificationPrefs map[string]interface{} `json:"notification_prefs"`
|
|
||||||
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Roles []RoleResponse `json:"roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateUserProfileRequest struct {
|
|
||||||
FullName *string `json:"full_name,omitempty"`
|
|
||||||
DisplayName *string `json:"display_name,omitempty"`
|
|
||||||
Phone *string `json:"phone,omitempty"`
|
|
||||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
|
||||||
JobTitle *string `json:"job_title,omitempty"`
|
|
||||||
EmployeeNo *string `json:"employee_no,omitempty"`
|
|
||||||
Bio *string `json:"bio,omitempty"`
|
|
||||||
Timezone *string `json:"timezone,omitempty"`
|
|
||||||
Locale *string `json:"locale,omitempty"`
|
|
||||||
Preferences *map[string]interface{} `json:"preferences,omitempty"`
|
|
||||||
NotificationPrefs *map[string]interface{} `json:"notification_prefs,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TitleResponse struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Code *string `json:"code,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListTitlesResponse struct {
|
|
||||||
Titles []TitleResponse `json:"titles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MentionUsersRequest represents the request for getting users for mention purposes
|
|
||||||
type MentionUsersRequest struct {
|
|
||||||
Search *string `json:"search,omitempty" form:"search"` // Optional search term for username
|
|
||||||
Limit *int `json:"limit,omitempty" form:"limit"` // Optional limit, defaults to 50, max 100
|
|
||||||
}
|
|
||||||
|
|
||||||
// MentionUsersResponse represents the response for getting users for mention purposes
|
|
||||||
type MentionUsersResponse struct {
|
|
||||||
Users []UserResponse `json:"users"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/config"
|
"go-backend-template/config"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ApprovalFlow struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"department_id"`
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
Department *Department `gorm:"foreignKey:DepartmentID" json:"department,omitempty"`
|
|
||||||
Steps []ApprovalFlowStep `gorm:"foreignKey:FlowID" json:"steps,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ApprovalFlow) TableName() string { return "approval_flows" }
|
|
||||||
|
|
||||||
type ApprovalFlowStep struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
FlowID uuid.UUID `gorm:"type:uuid;not null" json:"flow_id"`
|
|
||||||
StepOrder int `gorm:"not null" json:"step_order"`
|
|
||||||
ParallelGroup int `gorm:"default:1" json:"parallel_group"`
|
|
||||||
ApproverRoleID *uuid.UUID `json:"approver_role_id,omitempty"`
|
|
||||||
ApproverUserID *uuid.UUID `json:"approver_user_id,omitempty"`
|
|
||||||
Required bool `gorm:"default:true" json:"required"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
ApprovalFlow *ApprovalFlow `gorm:"foreignKey:FlowID" json:"approval_flow,omitempty"`
|
|
||||||
ApproverRole *Role `gorm:"foreignKey:ApproverRoleID" json:"approver_role,omitempty"`
|
|
||||||
ApproverUser *User `gorm:"foreignKey:ApproverUserID" json:"approver_user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ApprovalFlowStep) TableName() string { return "approval_flow_steps" }
|
|
||||||
|
|
||||||
type ApprovalStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ApprovalStatusNotStarted ApprovalStatus = "not_started"
|
|
||||||
ApprovalStatusPending ApprovalStatus = "pending"
|
|
||||||
ApprovalStatusApproved ApprovalStatus = "approved"
|
|
||||||
ApprovalStatusRejected ApprovalStatus = "rejected"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterOutgoingApproval struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
StepID uuid.UUID `gorm:"type:uuid;not null" json:"step_id"`
|
|
||||||
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
|
|
||||||
StepOrder int `gorm:"not null" json:"step_order"`
|
|
||||||
ParallelGroup int `gorm:"default:1" json:"parallel_group"`
|
|
||||||
IsRequired bool `gorm:"default:true" json:"is_required"`
|
|
||||||
ApproverID *uuid.UUID `json:"approver_id,omitempty"`
|
|
||||||
Status ApprovalStatus `gorm:"not null;default:'pending'" json:"status"`
|
|
||||||
Remarks *string `json:"remarks,omitempty"`
|
|
||||||
ActedAt *time.Time `json:"acted_at,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
Letter *LetterOutgoing `gorm:"foreignKey:LetterID" json:"letter,omitempty"`
|
|
||||||
Step *ApprovalFlowStep `gorm:"foreignKey:StepID" json:"step,omitempty"`
|
|
||||||
Approver *User `gorm:"foreignKey:ApproverID" json:"approver,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoingApproval) TableName() string { return "letter_outgoing_approvals" }
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Department struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
|
||||||
Code string `json:"code,omitempty"`
|
|
||||||
Path string `gorm:"not null" json:"path"`
|
|
||||||
ParentDepartmentID *uuid.UUID `gorm:"type:uuid" json:"parent_department_id,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
ParentDepartment *Department `gorm:"foreignKey:ParentDepartmentID" json:"parent_department,omitempty"`
|
|
||||||
ChildDepartments []Department `gorm:"foreignKey:ParentDepartmentID" json:"child_departments,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Department) TableName() string { return "departments" }
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import "github.com/google/uuid"
|
|
||||||
|
|
||||||
// DepartmentRecipientsSetting represents the structure for storing department recipient IDs in app settings
|
|
||||||
type DepartmentRecipientsSetting struct {
|
|
||||||
DepartmentIDs []uuid.UUID `json:"department_ids"`
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DispositionAction struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Code string `gorm:"uniqueIndex;not null" json:"code"`
|
|
||||||
Label string `gorm:"not null" json:"label"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
RequiresNote bool `gorm:"not null;default:false" json:"requires_note"`
|
|
||||||
GroupName *string `json:"group_name,omitempty"`
|
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
|
||||||
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DispositionAction) TableName() string { return "disposition_actions" }
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DispositionRoute struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
FromDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"from_department_id"`
|
|
||||||
ToDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"to_department_id"`
|
|
||||||
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
|
||||||
AllowedActions JSONB `gorm:"type:jsonb" json:"allowed_actions,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
// Relationships
|
|
||||||
FromDepartment Department `gorm:"foreignKey:FromDepartmentID;references:ID" json:"from_department,omitempty"`
|
|
||||||
ToDepartment Department `gorm:"foreignKey:ToDepartmentID;references:ID" json:"to_department,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DispositionRoute) TableName() string { return "disposition_routes" }
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DocumentSession represents an OnlyOffice document editing session
|
|
||||||
type DocumentSession struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DocumentID uuid.UUID `gorm:"type:uuid;not null;index" json:"document_id"`
|
|
||||||
DocumentKey string `gorm:"uniqueIndex;not null" json:"document_key"` // OnlyOffice document key
|
|
||||||
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
|
|
||||||
Status int `gorm:"not null;default:0" json:"status"` // OnlyOffice status codes
|
|
||||||
IsLocked bool `gorm:"default:false" json:"is_locked"`
|
|
||||||
LockedBy *uuid.UUID `gorm:"type:uuid" json:"locked_by,omitempty"`
|
|
||||||
LockedAt *time.Time `json:"locked_at,omitempty"`
|
|
||||||
LastSavedAt *time.Time `json:"last_saved_at,omitempty"`
|
|
||||||
Version int `gorm:"not null;default:1" json:"version"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DocumentSession) TableName() string {
|
|
||||||
return "document_sessions"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DocumentSession) BeforeCreate(tx *gorm.DB) error {
|
|
||||||
if ds.ID == uuid.Nil {
|
|
||||||
ds.ID = uuid.New()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DocumentVersion represents a version of a document
|
|
||||||
type DocumentVersion struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DocumentID uuid.UUID `gorm:"type:uuid;not null;index" json:"document_id"`
|
|
||||||
Version int `gorm:"not null" json:"version"`
|
|
||||||
FileURL string `gorm:"not null" json:"file_url"`
|
|
||||||
FileSize int64 `gorm:"not null" json:"file_size"`
|
|
||||||
ChangesURL *string `json:"changes_url,omitempty"`
|
|
||||||
SavedBy uuid.UUID `gorm:"type:uuid;not null" json:"saved_by"`
|
|
||||||
SavedAt time.Time `gorm:"not null" json:"saved_at"`
|
|
||||||
IsActive bool `gorm:"default:false;index" json:"is_active"`
|
|
||||||
Comments *string `json:"comments,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
User *User `gorm:"foreignKey:SavedBy" json:"user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DocumentVersion) TableName() string {
|
|
||||||
return "document_versions"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dv *DocumentVersion) BeforeCreate(tx *gorm.DB) error {
|
|
||||||
if dv.ID == uuid.Nil {
|
|
||||||
dv.ID = uuid.New()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DocumentError represents errors during document operations
|
|
||||||
type DocumentError struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DocumentID uuid.UUID `gorm:"type:uuid;not null;index" json:"document_id"`
|
|
||||||
SessionID *uuid.UUID `gorm:"type:uuid" json:"session_id,omitempty"`
|
|
||||||
ErrorType string `gorm:"not null" json:"error_type"`
|
|
||||||
ErrorMsg string `gorm:"not null" json:"error_msg"`
|
|
||||||
Details map[string]interface{} `gorm:"type:jsonb" json:"details,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
Session *DocumentSession `gorm:"foreignKey:SessionID" json:"session,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DocumentError) TableName() string {
|
|
||||||
return "document_errors"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (de *DocumentError) BeforeCreate(tx *gorm.DB) error {
|
|
||||||
if de.ID == uuid.Nil {
|
|
||||||
de.ID = uuid.New()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DocumentMetadata stores document-specific metadata
|
|
||||||
type DocumentMetadata struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DocumentID uuid.UUID `gorm:"type:uuid;uniqueIndex;not null" json:"document_id"`
|
|
||||||
DocumentType string `gorm:"not null" json:"document_type"` // letter_attachment, outgoing_attachment, etc.
|
|
||||||
ReferenceID uuid.UUID `gorm:"type:uuid;not null" json:"reference_id"` // ID of the parent entity (letter, outgoing letter, etc.)
|
|
||||||
FileName string `gorm:"not null" json:"file_name"`
|
|
||||||
FileType string `gorm:"not null" json:"file_type"`
|
|
||||||
FileSize int64 `gorm:"not null" json:"file_size"`
|
|
||||||
MimeType string `json:"mime_type,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DocumentMetadata) TableName() string {
|
|
||||||
return "document_metadata"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dm *DocumentMetadata) BeforeCreate(tx *gorm.DB) error {
|
|
||||||
if dm.ID == uuid.Nil {
|
|
||||||
dm.ID = uuid.New()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InstitutionType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
InstGovernment InstitutionType = "government"
|
|
||||||
InstPrivate InstitutionType = "private"
|
|
||||||
InstNGO InstitutionType = "ngo"
|
|
||||||
InstIndividual InstitutionType = "individual"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Institution struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null;size:255" json:"name"`
|
|
||||||
Type InstitutionType `gorm:"not null;size:32" json:"type"`
|
|
||||||
Address *string `json:"address,omitempty"`
|
|
||||||
ContactPerson *string `gorm:"size:255" json:"contact_person,omitempty"`
|
|
||||||
Phone *string `gorm:"size:50" json:"phone,omitempty"`
|
|
||||||
Email *string `gorm:"size:255" json:"email,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Institution) TableName() string { return "institutions" }
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Label struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null;size:255" json:"name"`
|
|
||||||
Color *string `gorm:"size:16" json:"color,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Label) TableName() string { return "labels" }
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterDiscussion struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
|
|
||||||
Message string `gorm:"not null" json:"message"`
|
|
||||||
Mentions JSONB `gorm:"type:jsonb" json:"mentions,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
|
||||||
|
|
||||||
// Relationships
|
|
||||||
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterDiscussion) TableName() string { return "letter_incoming_discussions" }
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncomingDisposition struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
Department Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
|
||||||
Departments []LetterIncomingDispositionDepartment `gorm:"foreignKey:LetterIncomingDispositionID;references:ID" json:"departments,omitempty"`
|
|
||||||
ActionSelections []LetterDispositionActionSelection `gorm:"foreignKey:DispositionID;references:ID" json:"action_selections,omitempty"`
|
|
||||||
DispositionNotes []DispositionNote `gorm:"foreignKey:DispositionID;references:ID" json:"disposition_notes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterIncomingDisposition) TableName() string { return "letter_incoming_dispositions" }
|
|
||||||
|
|
||||||
type LetterIncomingDispositionDepartmentStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
DispositionDepartmentStatusPending LetterIncomingDispositionDepartmentStatus = "pending"
|
|
||||||
DispositionDepartmentStatusDispositioned LetterIncomingDispositionDepartmentStatus = "dispositioned"
|
|
||||||
DispositionDepartmentStatusRead LetterIncomingDispositionDepartmentStatus = "read"
|
|
||||||
DispositionDepartmentStatusCompleted LetterIncomingDispositionDepartmentStatus = "completed"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncomingDispositionDepartment struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterIncomingDispositionID uuid.UUID `gorm:"type:uuid;not null" json:"letter_incoming_disposition_id"`
|
|
||||||
LetterIncomingID uuid.UUID `gorm:"type:uuid;not null" json:"letter_incoming_id"`
|
|
||||||
DepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"department_id"`
|
|
||||||
Status LetterIncomingDispositionDepartmentStatus `gorm:"not null;default:'pending'" json:"status"`
|
|
||||||
Notes *string `gorm:"type:text" json:"notes,omitempty"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
// Relationships
|
|
||||||
Department *Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
|
||||||
LetterIncoming *LetterIncoming `gorm:"foreignKey:LetterIncomingID;references:ID" json:"letter_incoming,omitempty"`
|
|
||||||
LetterIncomingDisposition *LetterIncomingDisposition `gorm:"foreignKey:LetterIncomingDispositionID;references:ID" json:"letter_incoming_disposition,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterIncomingDispositionDepartment) TableName() string {
|
|
||||||
return "letter_incoming_dispositions_department"
|
|
||||||
}
|
|
||||||
|
|
||||||
type DispositionNote struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"`
|
|
||||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
|
||||||
Note string `gorm:"not null" json:"note"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
|
|
||||||
// Relationships
|
|
||||||
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DispositionNote) TableName() string { return "disposition_notes" }
|
|
||||||
|
|
||||||
type LetterDispositionActionSelection struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"`
|
|
||||||
ActionID uuid.UUID `gorm:"type:uuid;not null" json:"action_id"`
|
|
||||||
Note *string `json:"note,omitempty"`
|
|
||||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
|
|
||||||
// Relationships
|
|
||||||
Action *DispositionAction `gorm:"foreignKey:ActionID;references:ID" json:"action,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterDispositionActionSelection) TableName() string { return "letter_disposition_actions" }
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncomingStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
LetterIncomingStatusNew LetterIncomingStatus = "new"
|
|
||||||
LetterIncomingStatusInProgress LetterIncomingStatus = "in_progress"
|
|
||||||
LetterIncomingStatusCompleted LetterIncomingStatus = "completed"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncomingType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
LetterIncomingTypeUtama LetterIncomingType = "UTAMA"
|
|
||||||
LetterIncomingTypeTembusan LetterIncomingType = "TEMBUSAN"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncoming struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterNumber string `gorm:"uniqueIndex;not null" json:"letter_number"`
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject string `gorm:"not null" json:"subject"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
|
||||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
|
||||||
SenderName *string `json:"sender_name,omitempty"`
|
|
||||||
Addressee *string `json:"addressee,omitempty"`
|
|
||||||
ReceivedDate time.Time `json:"received_date"`
|
|
||||||
DueDate *time.Time `json:"due_date,omitempty"`
|
|
||||||
Type LetterIncomingType `gorm:"not null;default:'UTAMA'" json:"type"`
|
|
||||||
Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"`
|
|
||||||
IsArchived bool `gorm:"not null;default:false" json:"is_archived"`
|
|
||||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
||||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterIncoming) TableName() string { return "letters_incoming" }
|
|
||||||
|
|
||||||
type LetterIncomingAttachment struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
FileURL string `gorm:"not null" json:"file_url"`
|
|
||||||
FileName string `gorm:"not null" json:"file_name"`
|
|
||||||
FileType string `gorm:"not null" json:"file_type"`
|
|
||||||
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
|
|
||||||
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterIncomingAttachment) TableName() string { return "letter_incoming_attachments" }
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncomingActivityLog struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
ActionType string `gorm:"not null" json:"action_type"`
|
|
||||||
ActorUserID *uuid.UUID `json:"actor_user_id,omitempty"`
|
|
||||||
ActorDepartmentID *uuid.UUID `json:"actor_department_id,omitempty"`
|
|
||||||
TargetType *string `json:"target_type,omitempty"`
|
|
||||||
TargetID *uuid.UUID `json:"target_id,omitempty"`
|
|
||||||
FromStatus *string `json:"from_status,omitempty"`
|
|
||||||
ToStatus *string `json:"to_status,omitempty"`
|
|
||||||
Context JSONB `gorm:"type:jsonb" json:"context,omitempty"`
|
|
||||||
OccurredAt time.Time `gorm:"autoCreateTime" json:"occurred_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterIncomingActivityLog) TableName() string { return "letter_incoming_activity_logs" }
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncomingRecipientStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RecipientStatusNew LetterIncomingRecipientStatus = "new"
|
|
||||||
RecipientStatusRead LetterIncomingRecipientStatus = "read"
|
|
||||||
RecipientStatusCompleted LetterIncomingRecipientStatus = "completed"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterIncomingRecipient struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
RecipientUserID *uuid.UUID `json:"recipient_user_id,omitempty"`
|
|
||||||
RecipientDepartmentID *uuid.UUID `json:"recipient_department_id,omitempty"`
|
|
||||||
Status LetterIncomingRecipientStatus `gorm:"not null;default:'new'" json:"status"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterIncomingRecipient) TableName() string { return "letter_incoming_recipients" }
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterOutgoingStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
LetterOutgoingStatusDraft LetterOutgoingStatus = "draft"
|
|
||||||
LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval"
|
|
||||||
LetterOutgoingStatusApproved LetterOutgoingStatus = "approved"
|
|
||||||
LetterOutgoingStatusSent LetterOutgoingStatus = "sent"
|
|
||||||
LetterOutgoingStatusRejected LetterOutgoingStatus = "rejected"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterOutgoing struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterNumber string `gorm:"uniqueIndex;not null" json:"letter_number"`
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
Subject string `gorm:"not null" json:"subject"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
|
||||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
|
||||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
|
||||||
IssueDate time.Time `json:"issue_date"`
|
|
||||||
Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"`
|
|
||||||
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
|
||||||
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
|
|
||||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
|
||||||
IsArchived bool `gorm:"not null;default:false" json:"is_archived"`
|
|
||||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
Priority *Priority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"`
|
|
||||||
ReceiverInstitution *Institution `gorm:"foreignKey:ReceiverInstitutionID" json:"receiver_institution,omitempty"`
|
|
||||||
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
|
|
||||||
ApprovalFlow *ApprovalFlow `gorm:"foreignKey:ApprovalFlowID" json:"approval_flow,omitempty"`
|
|
||||||
Recipients []LetterOutgoingRecipient `gorm:"foreignKey:LetterID" json:"recipients,omitempty"`
|
|
||||||
Attachments []LetterOutgoingAttachment `gorm:"foreignKey:LetterID" json:"attachments,omitempty"`
|
|
||||||
Approvals []LetterOutgoingApproval `gorm:"foreignKey:LetterID" json:"approvals,omitempty"`
|
|
||||||
Discussions []LetterOutgoingDiscussion `gorm:"foreignKey:LetterID" json:"discussions,omitempty"`
|
|
||||||
FinalAttachments []LetterOutgoingFinalAttachment `gorm:"foreignKey:LetterID" json:"final_attachments,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoing) TableName() string { return "letters_outgoing" }
|
|
||||||
|
|
||||||
type LetterOutgoingRecipient struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
UserID *uuid.UUID `gorm:"type:uuid" json:"user_id,omitempty"`
|
|
||||||
DepartmentID *uuid.UUID `gorm:"type:uuid" json:"department_id,omitempty"`
|
|
||||||
IsPrimary bool `gorm:"default:false" json:"is_primary"`
|
|
||||||
Status string `gorm:"default:'pending'" json:"status"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
Flag *string `json:"flag,omitempty"`
|
|
||||||
IsArchived bool `gorm:"default:false" json:"is_archived"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
|
||||||
Department *Department `gorm:"foreignKey:DepartmentID" json:"department,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoingRecipient) TableName() string { return "letter_outgoing_recipients" }
|
|
||||||
|
|
||||||
type LetterOutgoingAttachment struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
|
|
||||||
FileURL string `gorm:"not null" json:"file_url"`
|
|
||||||
FileName string `gorm:"not null" json:"file_name"`
|
|
||||||
FileType string `gorm:"not null" json:"file_type"`
|
|
||||||
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
|
|
||||||
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoingAttachment) TableName() string { return "letter_outgoing_attachments" }
|
|
||||||
|
|
||||||
type LetterOutgoingFinalAttachment struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
FileURL string `gorm:"not null" json:"file_url"`
|
|
||||||
FileName string `gorm:"not null" json:"file_name"`
|
|
||||||
FileType string `gorm:"not null" json:"file_type"`
|
|
||||||
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
|
|
||||||
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoingFinalAttachment) TableName() string { return "letter_outgoing_final_attachments" }
|
|
||||||
|
|
||||||
type LetterOutgoingDiscussion struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
|
||||||
UserID uuid.UUID `gorm:"not null" json:"user_id"`
|
|
||||||
Message string `gorm:"not null" json:"message"`
|
|
||||||
Mentions JSONB `gorm:"type:jsonb" json:"mentions,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
|
||||||
Attachments []LetterOutgoingDiscussionAttachment `gorm:"foreignKey:DiscussionID" json:"attachments,omitempty"`
|
|
||||||
Replies []LetterOutgoingDiscussion `gorm:"foreignKey:ParentID" json:"replies,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoingDiscussion) TableName() string { return "letter_outgoing_discussions" }
|
|
||||||
|
|
||||||
type LetterOutgoingDiscussionAttachment struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
DiscussionID uuid.UUID `gorm:"type:uuid;not null" json:"discussion_id"`
|
|
||||||
FileURL string `gorm:"not null" json:"file_url"`
|
|
||||||
FileName string `gorm:"not null" json:"file_name"`
|
|
||||||
FileType string `gorm:"not null" json:"file_type"`
|
|
||||||
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
|
|
||||||
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoingDiscussionAttachment) TableName() string {
|
|
||||||
return "letter_outgoing_discussion_attachments"
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterOutgoingActivityLog struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
|
||||||
ActionType string `gorm:"not null" json:"action_type"`
|
|
||||||
ActorUserID *uuid.UUID `json:"actor_user_id,omitempty"`
|
|
||||||
ActorDepartmentID *uuid.UUID `json:"actor_department_id,omitempty"`
|
|
||||||
TargetType *string `json:"target_type,omitempty"`
|
|
||||||
TargetID *uuid.UUID `json:"target_id,omitempty"`
|
|
||||||
FromStatus *string `json:"from_status,omitempty"`
|
|
||||||
ToStatus *string `json:"to_status,omitempty"`
|
|
||||||
Context JSONB `gorm:"type:jsonb" json:"context,omitempty"`
|
|
||||||
OccurredAt time.Time `gorm:"autoCreateTime" json:"occurred_at"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
Letter *LetterOutgoing `gorm:"foreignKey:LetterID" json:"letter,omitempty"`
|
|
||||||
ActorUser *User `gorm:"foreignKey:ActorUserID" json:"actor_user,omitempty"`
|
|
||||||
ActorDepartment *Department `gorm:"foreignKey:ActorDepartmentID" json:"actor_department,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LetterOutgoingActivityLog) TableName() string { return "letter_outgoing_activity_logs" }
|
|
||||||
|
|
||||||
// Action types for letter outgoing activity logs
|
|
||||||
const (
|
|
||||||
LetterOutgoingActionCreated = "created"
|
|
||||||
LetterOutgoingActionUpdated = "updated"
|
|
||||||
LetterOutgoingActionDeleted = "deleted"
|
|
||||||
LetterOutgoingActionStatusChanged = "status_changed"
|
|
||||||
LetterOutgoingActionSubmittedApproval = "submitted_for_approval"
|
|
||||||
LetterOutgoingActionApproved = "approved"
|
|
||||||
LetterOutgoingActionRejected = "rejected"
|
|
||||||
LetterOutgoingActionRevised = "revised"
|
|
||||||
LetterOutgoingActionSent = "sent"
|
|
||||||
LetterOutgoingActionArchived = "archived"
|
|
||||||
LetterOutgoingActionAttachmentAdded = "attachment_added"
|
|
||||||
LetterOutgoingActionAttachmentRemoved = "attachment_removed"
|
|
||||||
LetterOutgoingActionRecipientAdded = "recipient_added"
|
|
||||||
LetterOutgoingActionRecipientRemoved = "recipient_removed"
|
|
||||||
LetterOutgoingActionDiscussionAdded = "discussion_added"
|
|
||||||
)
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Module struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
|
||||||
Code string `gorm:"uniqueIndex;not null" json:"code"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
Permissions []Permission `gorm:"foreignKey:ModuleID" json:"permissions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Module) TableName() string { return "modules" }
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Priority struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null;size:255" json:"name"`
|
|
||||||
Level int `gorm:"not null" json:"level"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Priority) TableName() string { return "priorities" }
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Role struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
|
||||||
Code string `gorm:"uniqueIndex;not null" json:"code"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Role) TableName() string { return "roles" }
|
|
||||||
|
|
||||||
type Permission struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
ModuleID *uuid.UUID `gorm:"type:uuid" json:"module_id"`
|
|
||||||
Module *Module `gorm:"foreignKey:ModuleID" json:"module,omitempty"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Code string `gorm:"uniqueIndex;not null" json:"code"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Permission) TableName() string { return "permissions" }
|
|
||||||
|
|
||||||
type Position struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
|
||||||
Code string `gorm:"uniqueIndex" json:"code"`
|
|
||||||
Path string `gorm:"type:ltree;uniqueIndex" json:"path"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Position) TableName() string { return "positions" }
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RepositoryAttachment struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
FileURL string `gorm:"not null" json:"file_url"`
|
|
||||||
FileName string `gorm:"not null" json:"file_name"`
|
|
||||||
FileType string `gorm:"not null" json:"file_type"`
|
|
||||||
Category string `gorm:"not null" json:"category"`
|
|
||||||
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
|
|
||||||
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (RepositoryAttachment) TableName() string { return "repository_attachments" }
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import "github.com/google/uuid"
|
|
||||||
|
|
||||||
type RolePermission struct {
|
|
||||||
RoleID uuid.UUID `gorm:"type:uuid;primaryKey" json:"role_id"`
|
|
||||||
PermissionID uuid.UUID `gorm:"type:uuid;primaryKey" json:"permission_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (RolePermission) TableName() string { return "role_permissions" }
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppSetting struct {
|
|
||||||
Key string `gorm:"primaryKey;size:100" json:"key"`
|
|
||||||
Value JSONB `gorm:"type:jsonb;default:'{}'" json:"value"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (AppSetting) TableName() string { return "app_settings" }
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Title struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
|
||||||
Code *string `gorm:"uniqueIndex" json:"code,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Title) TableName() string { return "titles" }
|
|
||||||
@ -49,7 +49,6 @@ type User struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
Profile *UserProfile `gorm:"foreignKey:UserID;references:ID" json:"profile,omitempty"`
|
Profile *UserProfile `gorm:"foreignKey:UserID;references:ID" json:"profile,omitempty"`
|
||||||
Departments []Department `gorm:"many2many:user_department;foreignKey:ID;joinForeignKey:user_id;References:ID;joinReferences:department_id" json:"departments,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||||
@ -64,10 +63,6 @@ func (User) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) HasPermission(permission string) bool {
|
func (u *User) HasPermission(permission string) bool {
|
||||||
return false
|
// TODO: Implement permission checking logic
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) CanAccessOutlet(outletID uuid.UUID) bool {
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,365 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"eslogad-be/internal/appcontext"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ApprovalFlowService interface {
|
|
||||||
CreateApprovalFlow(ctx context.Context, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
|
||||||
GetApprovalFlow(ctx context.Context, id uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
|
||||||
GetApprovalFlowByDepartment(ctx context.Context, departmentID uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
|
||||||
UpdateApprovalFlow(ctx context.Context, id uuid.UUID, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
|
||||||
DeleteApprovalFlow(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListApprovalFlows(ctx context.Context, req *contract.ListApprovalFlowsRequest) (*contract.ListApprovalFlowsResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminApprovalFlowHandler struct {
|
|
||||||
svc ApprovalFlowService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAdminApprovalFlowHandler(svc ApprovalFlowService) *AdminApprovalFlowHandler {
|
|
||||||
return &AdminApprovalFlowHandler{svc: svc}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) CreateApprovalFlow(c *gin.Context) {
|
|
||||||
var req contract.ApprovalFlowRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.Steps) == 0 {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "at least one approval step is required", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, step := range req.Steps {
|
|
||||||
if step.ApproverRoleID == nil && step.ApproverUserID == nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "step " + strconv.Itoa(i+1) + " must have either approver_role_id or approver_user_id",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if step.ApproverRoleID != nil && step.ApproverUserID != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "step " + strconv.Itoa(i+1) + " cannot have both approver_role_id and approver_user_id",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateApprovalFlow(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) GetApprovalFlow(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) GetApprovalFlowByDepartment(c *gin.Context) {
|
|
||||||
departmentID, err := uuid.Parse(c.Param("department_id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid department_id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetApprovalFlowByDepartment(c.Request.Context(), departmentID)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) UpdateApprovalFlow(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.ApprovalFlowRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.Steps) == 0 {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "at least one approval step is required", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, step := range req.Steps {
|
|
||||||
if step.ApproverRoleID == nil && step.ApproverUserID == nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "step " + strconv.Itoa(i+1) + " must have either approver_role_id or approver_user_id",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if step.ApproverRoleID != nil && step.ApproverUserID != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "step " + strconv.Itoa(i+1) + " cannot have both approver_role_id and approver_user_id",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.UpdateApprovalFlow(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) DeleteApprovalFlow(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.DeleteApprovalFlow(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "approval flow deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) ListApprovalFlows(c *gin.Context) {
|
|
||||||
// Parse query params
|
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
||||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
||||||
|
|
||||||
var departmentID *uuid.UUID
|
|
||||||
if departmentIDStr := c.Query("department_id"); departmentIDStr != "" {
|
|
||||||
if id, err := uuid.Parse(departmentIDStr); err == nil {
|
|
||||||
departmentID = &id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isActive *bool
|
|
||||||
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
|
||||||
if active, err := strconv.ParseBool(isActiveStr); err == nil {
|
|
||||||
isActive = &active
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var search *string
|
|
||||||
if searchStr := c.Query("search"); searchStr != "" {
|
|
||||||
search = &searchStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build request - pass PAGE, bukan OFFSET
|
|
||||||
req := &contract.ListApprovalFlowsRequest{
|
|
||||||
Page: page, // ✅ Pass page number
|
|
||||||
Limit: limit,
|
|
||||||
DepartmentID: departmentID,
|
|
||||||
IsActive: isActive,
|
|
||||||
Search: search, // tambahkan ini juga
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.ListApprovalFlows(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) ListApprovalFlowsByDepartment(c *gin.Context) {
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
|
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
||||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
||||||
offset := (page - 1) * limit
|
|
||||||
|
|
||||||
req := &contract.ListApprovalFlowsRequest{
|
|
||||||
Limit: limit,
|
|
||||||
Page: offset,
|
|
||||||
DepartmentID: &appCtx.DepartmentID,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.ListApprovalFlows(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) ActivateApprovalFlow(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current flow
|
|
||||||
flow, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update only the IsActive field
|
|
||||||
req := &contract.ApprovalFlowRequest{
|
|
||||||
DepartmentID: flow.DepartmentID,
|
|
||||||
Name: flow.Name,
|
|
||||||
Description: flow.Description,
|
|
||||||
IsActive: true,
|
|
||||||
Steps: make([]contract.ApprovalFlowStepRequest, len(flow.Steps)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy existing steps
|
|
||||||
for i, step := range flow.Steps {
|
|
||||||
req.Steps[i] = contract.ApprovalFlowStepRequest{
|
|
||||||
StepOrder: step.StepOrder,
|
|
||||||
ParallelGroup: step.ParallelGroup,
|
|
||||||
ApproverRoleID: step.ApproverRoleID,
|
|
||||||
ApproverUserID: step.ApproverUserID,
|
|
||||||
Required: step.Required,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.UpdateApprovalFlow(c.Request.Context(), id, req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) DeactivateApprovalFlow(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current flow
|
|
||||||
flow, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update only the IsActive field
|
|
||||||
req := &contract.ApprovalFlowRequest{
|
|
||||||
DepartmentID: flow.DepartmentID,
|
|
||||||
Name: flow.Name,
|
|
||||||
Description: flow.Description,
|
|
||||||
IsActive: false,
|
|
||||||
Steps: make([]contract.ApprovalFlowStepRequest, len(flow.Steps)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy existing steps
|
|
||||||
for i, step := range flow.Steps {
|
|
||||||
req.Steps[i] = contract.ApprovalFlowStepRequest{
|
|
||||||
StepOrder: step.StepOrder,
|
|
||||||
ParallelGroup: step.ParallelGroup,
|
|
||||||
ApproverRoleID: step.ApproverRoleID,
|
|
||||||
ApproverUserID: step.ApproverUserID,
|
|
||||||
Required: step.Required,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.UpdateApprovalFlow(c.Request.Context(), id, req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AdminApprovalFlowHandler) CloneApprovalFlow(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var cloneReq struct {
|
|
||||||
DepartmentID uuid.UUID `json:"department_id" validate:"required"`
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&cloneReq); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the source flow
|
|
||||||
sourceFlow, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new flow request with cloned data
|
|
||||||
req := &contract.ApprovalFlowRequest{
|
|
||||||
DepartmentID: cloneReq.DepartmentID,
|
|
||||||
Name: cloneReq.Name,
|
|
||||||
Description: sourceFlow.Description,
|
|
||||||
IsActive: false, // New cloned flow starts as inactive
|
|
||||||
Steps: make([]contract.ApprovalFlowStepRequest, len(sourceFlow.Steps)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy steps from source flow
|
|
||||||
for i, step := range sourceFlow.Steps {
|
|
||||||
req.Steps[i] = contract.ApprovalFlowStepRequest{
|
|
||||||
StepOrder: step.StepOrder,
|
|
||||||
ParallelGroup: step.ParallelGroup,
|
|
||||||
ApproverRoleID: step.ApproverRoleID,
|
|
||||||
ApproverUserID: step.ApproverUserID,
|
|
||||||
Required: step.Required,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateApprovalFlow(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
@ -1,195 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/service"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AnalyticsHandler struct {
|
|
||||||
analyticsService service.AnalyticsService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAnalyticsHandler(analyticsService service.AnalyticsService) *AnalyticsHandler {
|
|
||||||
return &AnalyticsHandler{
|
|
||||||
analyticsService: analyticsService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDashboard handles GET /api/v1/analytics/dashboard
|
|
||||||
func (h *AnalyticsHandler) GetDashboard(c *gin.Context) {
|
|
||||||
var req contract.AnalyticsDashboardRequest
|
|
||||||
|
|
||||||
// Bind query parameters
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "Invalid query parameters",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get analytics dashboard data
|
|
||||||
response, err := h.analyticsService.GetDashboard(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLetterVolume handles GET /api/v1/analytics/volume
|
|
||||||
func (h *AnalyticsHandler) GetLetterVolume(c *gin.Context) {
|
|
||||||
response, err := h.analyticsService.GetLetterVolume(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatusDistribution handles GET /api/v1/analytics/status-distribution
|
|
||||||
func (h *AnalyticsHandler) GetStatusDistribution(c *gin.Context) {
|
|
||||||
var req contract.AnalyticsDashboardRequest
|
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "Invalid query parameters",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get full dashboard and extract status distribution
|
|
||||||
response, err := h.analyticsService.GetDashboard(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]interface{}{
|
|
||||||
"status_distribution": response.StatusDistribution,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPriorityDistribution handles GET /api/v1/analytics/priority-distribution
|
|
||||||
func (h *AnalyticsHandler) GetPriorityDistribution(c *gin.Context) {
|
|
||||||
var req contract.AnalyticsDashboardRequest
|
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "Invalid query parameters",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get full dashboard and extract priority distribution
|
|
||||||
response, err := h.analyticsService.GetDashboard(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]interface{}{
|
|
||||||
"priority_distribution": response.PriorityDistribution,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDepartmentStats handles GET /api/v1/analytics/department-stats
|
|
||||||
func (h *AnalyticsHandler) GetDepartmentStats(c *gin.Context) {
|
|
||||||
var req contract.AnalyticsDashboardRequest
|
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "Invalid query parameters",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get full dashboard and extract department stats
|
|
||||||
response, err := h.analyticsService.GetDashboard(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]interface{}{
|
|
||||||
"department_stats": response.DepartmentStats,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMonthlyTrend handles GET /api/v1/analytics/monthly-trend
|
|
||||||
func (h *AnalyticsHandler) GetMonthlyTrend(c *gin.Context) {
|
|
||||||
var req contract.AnalyticsDashboardRequest
|
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "Invalid query parameters",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get full dashboard and extract monthly trend
|
|
||||||
response, err := h.analyticsService.GetDashboard(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]interface{}{
|
|
||||||
"monthly_trend": response.MonthlyTrend,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetApprovalMetrics handles GET /api/v1/analytics/approval-metrics
|
|
||||||
func (h *AnalyticsHandler) GetApprovalMetrics(c *gin.Context) {
|
|
||||||
var req contract.AnalyticsDashboardRequest
|
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "Invalid query parameters",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get full dashboard and extract approval metrics
|
|
||||||
response, err := h.analyticsService.GetDashboard(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]interface{}{
|
|
||||||
"approval_metrics": response.ApprovalMetrics,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/internal/util"
|
"go-backend-template/internal/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"eslogad-be/internal/constants"
|
"go-backend-template/internal/constants"
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
"eslogad-be/internal/logger"
|
"go-backend-template/internal/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -54,82 +54,6 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
|||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(loginResponse), "AuthHandler::Login")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(loginResponse), "AuthHandler::Login")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
|
||||||
token := h.extractTokenFromHeader(c)
|
|
||||||
if token == "" {
|
|
||||||
logger.FromContext(c.Request.Context()).Error("AuthHandler::Logout -> token is required")
|
|
||||||
h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := h.authService.Logout(c.Request.Context(), token)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::Logout -> Failed to logout")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c.Request.Context()).Info("AuthHandler::Logout -> Successfully logged out")
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "Successfully logged out"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
|
||||||
token := h.extractTokenFromHeader(c)
|
|
||||||
if token == "" {
|
|
||||||
logger.FromContext(c.Request.Context()).Error("AuthHandler::RefreshToken -> token is required")
|
|
||||||
h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loginResponse, err := h.authService.RefreshToken(c.Request.Context(), token)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::RefreshToken -> Failed to refresh token")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c.Request.Context()).Infof("AuthHandler::RefreshToken -> Successfully refreshed token for user = %s", loginResponse.User.Email)
|
|
||||||
c.JSON(http.StatusOK, loginResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) ValidateToken(c *gin.Context) {
|
|
||||||
token := h.extractTokenFromHeader(c)
|
|
||||||
if token == "" {
|
|
||||||
logger.FromContext(c.Request.Context()).Error("AuthHandler::ValidateToken -> token is required")
|
|
||||||
h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userResponse, err := h.authService.ValidateToken(token)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::ValidateToken -> Failed to validate token")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c.Request.Context()).Infof("AuthHandler::ValidateToken -> Successfully validated token for user = %s", userResponse.Email)
|
|
||||||
c.JSON(http.StatusOK, userResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) GetProfile(c *gin.Context) {
|
|
||||||
token := h.extractTokenFromHeader(c)
|
|
||||||
if token == "" {
|
|
||||||
logger.FromContext(c.Request.Context()).Error("AuthHandler::GetProfile -> token is required")
|
|
||||||
h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userResponse, err := h.authService.ValidateToken(token)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::GetProfile -> Failed to get profile")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c.Request.Context()).Infof("AuthHandler::GetProfile -> Successfully retrieved profile for user = %s", userResponse.Email)
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Data: userResponse, Message: "success get user profile"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) extractTokenFromHeader(c *gin.Context) string {
|
func (h *AuthHandler) extractTokenFromHeader(c *gin.Context) string {
|
||||||
authHeader := c.GetHeader("Authorization")
|
authHeader := c.GetHeader("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
|
|||||||
@ -2,12 +2,10 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthService interface {
|
type AuthService interface {
|
||||||
Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error)
|
Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error)
|
||||||
ValidateToken(tokenString string) (*contract.UserResponse, error)
|
RefreshToken(ctx context.Context, req *contract.RefreshTokenRequest) (*contract.LoginResponse, error)
|
||||||
RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error)
|
|
||||||
Logout(ctx context.Context, tokenString string) error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,164 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"eslogad-be/internal/appcontext"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DispositionRouteService interface {
|
|
||||||
Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
|
|
||||||
CreateOrUpdate(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.BulkCreateDispositionRouteResponse, error)
|
|
||||||
Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
|
|
||||||
Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error)
|
|
||||||
ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error)
|
|
||||||
SetActive(ctx context.Context, id uuid.UUID, active bool) error
|
|
||||||
ListGrouped(ctx context.Context) (*contract.ListDispositionRoutesGroupedResponse, error)
|
|
||||||
ListAll(ctx context.Context) (*contract.ListDispositionRoutesDetailedResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DispositionRouteHandler struct{ svc DispositionRouteService }
|
|
||||||
|
|
||||||
func NewDispositionRouteHandler(svc DispositionRouteService) *DispositionRouteHandler {
|
|
||||||
return &DispositionRouteHandler{svc: svc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create handles both single and bulk route creation with upsert logic
|
|
||||||
func (h *DispositionRouteHandler) Create(c *gin.Context) {
|
|
||||||
var req contract.CreateDispositionRouteRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body: " + err.Error(), Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate request
|
|
||||||
if len(req.ToDepartmentIDs) == 0 {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "to_department_ids cannot be empty", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If single route, use Create for backward compatibility
|
|
||||||
if len(req.ToDepartmentIDs) == 1 {
|
|
||||||
resp, err := h.svc.Create(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(201, contract.BuildSuccessResponse(resp))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For multiple routes, use bulk create/update
|
|
||||||
bulkResp, err := h.svc.CreateOrUpdate(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(201, contract.BuildSuccessResponse(bulkResp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// BulkCreateOrUpdate explicitly handles bulk create/update operations
|
|
||||||
func (h *DispositionRouteHandler) BulkCreateOrUpdate(c *gin.Context) {
|
|
||||||
var req contract.CreateDispositionRouteRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body: " + err.Error(), Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate request
|
|
||||||
if len(req.ToDepartmentIDs) == 0 {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "to_department_ids cannot be empty", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateOrUpdate(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DispositionRouteHandler) Update(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdateDispositionRouteRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.Update(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DispositionRouteHandler) Get(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.Get(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) {
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
|
|
||||||
resp, err := h.svc.ListByFromDept(c.Request.Context(), appCtx.DepartmentID)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DispositionRouteHandler) SetActive(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toggle := c.Query("active")
|
|
||||||
active := toggle != "false"
|
|
||||||
if err := h.svc.SetActive(c.Request.Context(), id, active); err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, &contract.SuccessResponse{Message: "updated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListGrouped returns all disposition routes grouped by from_department_id
|
|
||||||
func (h *DispositionRouteHandler) ListGrouped(c *gin.Context) {
|
|
||||||
resp, err := h.svc.ListGrouped(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAll returns all disposition routes with department details
|
|
||||||
func (h *DispositionRouteHandler) ListAll(c *gin.Context) {
|
|
||||||
resp, err := h.svc.ListAll(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
75
internal/handler/dukcapil_handler.go
Normal file
75
internal/handler/dukcapil_handler.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go-backend-template/internal/constants"
|
||||||
|
"go-backend-template/internal/contract"
|
||||||
|
"go-backend-template/internal/logger"
|
||||||
|
"go-backend-template/internal/util"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DukcapilHandler struct {
|
||||||
|
dukcapilService DukcapilService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDukcapilHandler(dukcapilService DukcapilService) *DukcapilHandler {
|
||||||
|
return &DukcapilHandler{dukcapilService: dukcapilService}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FaceMatch handles POST /api/v1/dukcapil/face-match (1:N face recognition).
|
||||||
|
func (h *DukcapilHandler) FaceMatch(c *gin.Context) {
|
||||||
|
var req contract.FaceMatchRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> request binding failed")
|
||||||
|
h.sendValidationError(c, "Invalid request body", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(req.TransactionID) == "" {
|
||||||
|
h.sendValidationError(c, "transaction_id is required", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.TransactionSource) == "" {
|
||||||
|
h.sendValidationError(c, "transaction_source is required", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.Threshold) == "" {
|
||||||
|
h.sendValidationError(c, "threshold is required", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.Image) == "" {
|
||||||
|
h.sendValidationError(c, "image is required (base64-encoded)", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.dukcapilService.FaceMatch(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> upstream call failed")
|
||||||
|
c.JSON(http.StatusBadGateway, &contract.ErrorResponse{
|
||||||
|
Error: "upstream_error",
|
||||||
|
Message: err.Error(),
|
||||||
|
Code: http.StatusBadGateway,
|
||||||
|
Details: map[string]interface{}{"entity": constants.DukcapilHandlerEntity},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c.Request.Context()).Infof("DukcapilHandler::FaceMatch -> tid=%s errorCode=%s matches=%d", res.TID, res.ErrorCode, len(res.Matches))
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(res), "DukcapilHandler::FaceMatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DukcapilHandler) sendValidationError(c *gin.Context, message, code string) {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
||||||
|
Error: "validation_error",
|
||||||
|
Message: message,
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"error_code": code,
|
||||||
|
"entity": constants.DukcapilHandlerEntity,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
11
internal/handler/dukcapil_service.go
Normal file
11
internal/handler/dukcapil_service.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go-backend-template/internal/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DukcapilService interface {
|
||||||
|
FaceMatch(ctx context.Context, req *contract.FaceMatchRequest) (*contract.FaceMatchResponse, error)
|
||||||
|
}
|
||||||
@ -1,105 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"eslogad-be/internal/appcontext"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileService interface {
|
|
||||||
UploadProfileAvatar(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, error)
|
|
||||||
UploadDocument(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error)
|
|
||||||
UploadDocumentFinal(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileHandler struct {
|
|
||||||
service FileService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFileHandler(service FileService) *FileHandler {
|
|
||||||
return &FileHandler{service: service}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *FileHandler) UploadProfileAvatar(c *gin.Context) {
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
if appCtx.UserID == uuid.Nil {
|
|
||||||
c.JSON(http.StatusUnauthorized, &contract.ErrorResponse{Error: "Unauthorized", Code: http.StatusUnauthorized})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file, header, err := c.Request.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "file is required", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
content, err := io.ReadAll(io.LimitReader(file, 10<<20))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "failed to read file", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ct := header.Header.Get("Content-Type")
|
|
||||||
url, err := h.service.UploadProfileAvatar(c.Request.Context(), appCtx.UserID, header.Filename, content, ct)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]string{"url": url}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *FileHandler) UploadDocument(c *gin.Context) {
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
if appCtx.UserID == uuid.Nil {
|
|
||||||
c.JSON(http.StatusUnauthorized, &contract.ErrorResponse{Error: "Unauthorized", Code: http.StatusUnauthorized})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file, header, err := c.Request.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "file is required", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
content, err := io.ReadAll(io.LimitReader(file, 20<<20))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "failed to read file", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ct := header.Header.Get("Content-Type")
|
|
||||||
url, key, err := h.service.UploadDocument(c.Request.Context(), appCtx.UserID, header.Filename, content, ct)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]string{"url": url, "key": key}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *FileHandler) UploadDocumentFinal(c *gin.Context) {
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
if appCtx.UserID == uuid.Nil {
|
|
||||||
c.JSON(http.StatusUnauthorized, &contract.ErrorResponse{Error: "Unauthorized", Code: http.StatusUnauthorized})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file, header, err := c.Request.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "file is required", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
content, err := io.ReadAll(io.LimitReader(file, 20<<20))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "failed to read file", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ct := header.Header.Get("Content-Type")
|
|
||||||
url, key, err := h.service.UploadDocumentFinal(c.Request.Context(), appCtx.UserID, header.Filename, content, ct)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]string{"url": url, "key": key}))
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/internal/logger"
|
"go-backend-template/internal/logger"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
@ -1,509 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"eslogad-be/internal/appcontext"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterService interface {
|
|
||||||
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
|
||||||
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
|
||||||
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
|
|
||||||
SearchIncomingLetters(ctx context.Context, req *contract.SearchIncomingLettersRequest) (*contract.SearchIncomingLettersResponse, error)
|
|
||||||
GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error)
|
|
||||||
MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
|
||||||
MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
|
||||||
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
|
||||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
|
||||||
BulkSoftDeleteIncomingLetters(ctx context.Context, ids []uuid.UUID) error
|
|
||||||
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error)
|
|
||||||
ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error
|
|
||||||
|
|
||||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
|
||||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
|
||||||
|
|
||||||
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
|
||||||
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
|
||||||
|
|
||||||
GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error)
|
|
||||||
UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error)
|
|
||||||
|
|
||||||
GetLetterCTA(ctx context.Context, letterID uuid.UUID) (*contract.LetterCTAResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterHandler struct {
|
|
||||||
svc LetterService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterHandler(svc LetterService) *LetterHandler {
|
|
||||||
return &LetterHandler{svc: svc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for common patterns
|
|
||||||
func (h *LetterHandler) parseUUID(c *gin.Context, param string) (uuid.UUID, bool) {
|
|
||||||
id, err := uuid.Parse(c.Param(param))
|
|
||||||
if err != nil {
|
|
||||||
h.respondError(c, http.StatusBadRequest, "invalid "+param)
|
|
||||||
return uuid.Nil, false
|
|
||||||
}
|
|
||||||
return id, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) bindJSON(c *gin.Context, req interface{}) bool {
|
|
||||||
if err := c.ShouldBindJSON(req); err != nil {
|
|
||||||
h.respondError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) bindQuery(c *gin.Context, req interface{}) bool {
|
|
||||||
if err := c.ShouldBindQuery(req); err != nil {
|
|
||||||
h.respondError(c, http.StatusBadRequest, "invalid query parameters")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) respondError(c *gin.Context, code int, message string) {
|
|
||||||
c.JSON(code, &contract.ErrorResponse{
|
|
||||||
Error: message,
|
|
||||||
Code: code,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) respondSuccess(c *gin.Context, code int, data interface{}) {
|
|
||||||
c.JSON(code, contract.BuildSuccessResponse(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) handleServiceError(c *gin.Context, err error) {
|
|
||||||
if err != nil {
|
|
||||||
h.respondError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) CreateIncomingLetter(c *gin.Context) {
|
|
||||||
var req contract.CreateIncomingLetterRequest
|
|
||||||
if !h.bindJSON(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateIncomingLetter(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusCreated, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) GetIncomingLetter(c *gin.Context) {
|
|
||||||
id, ok := h.parseUUID(c, "id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetIncomingLetterByID(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) ListIncomingLetters(c *gin.Context) {
|
|
||||||
req := h.parseListRequest(c)
|
|
||||||
|
|
||||||
resp, err := h.svc.ListIncomingLetters(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) GetLetterUnreadCounts(c *gin.Context) {
|
|
||||||
resp, err := h.svc.GetLetterUnreadCounts(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) MarkIncomingLetterAsRead(c *gin.Context) {
|
|
||||||
id, ok := h.parseUUID(c, "id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.MarkIncomingLetterAsRead(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) MarkOutgoingLetterAsRead(c *gin.Context) {
|
|
||||||
id, ok := h.parseUUID(c, "id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.MarkOutgoingLetterAsRead(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) parseListRequest(c *gin.Context) *contract.ListIncomingLettersRequest {
|
|
||||||
//appCtx := appcontext.FromGinContext(c)
|
|
||||||
//departmentID := appCtx.DepartmentID
|
|
||||||
|
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
||||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
||||||
|
|
||||||
// Ensure valid pagination values
|
|
||||||
if page < 1 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
if limit < 1 {
|
|
||||||
limit = 10
|
|
||||||
}
|
|
||||||
if limit > 100 {
|
|
||||||
limit = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &contract.ListIncomingLettersRequest{
|
|
||||||
Page: page,
|
|
||||||
Limit: limit,
|
|
||||||
}
|
|
||||||
|
|
||||||
if status := c.Query("status"); status != "" {
|
|
||||||
req.Status = &status
|
|
||||||
}
|
|
||||||
|
|
||||||
if query := c.Query("q"); query != "" {
|
|
||||||
req.Query = &query
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse is_read filter
|
|
||||||
if isReadStr := c.Query("is_read"); isReadStr != "" {
|
|
||||||
isRead := isReadStr == "true" || isReadStr == "1"
|
|
||||||
req.IsRead = &isRead
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse priority_ids filter
|
|
||||||
if priorityIDsStr := c.QueryArray("priority_ids[]"); len(priorityIDsStr) > 0 {
|
|
||||||
priorityIDs := make([]uuid.UUID, 0, len(priorityIDsStr))
|
|
||||||
for _, idStr := range priorityIDsStr {
|
|
||||||
if id, err := uuid.Parse(idStr); err == nil {
|
|
||||||
priorityIDs = append(priorityIDs, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req.PriorityIDs = priorityIDs
|
|
||||||
} else if priorityIDStr := c.Query("priority_ids"); priorityIDStr != "" {
|
|
||||||
// Also support comma-separated format
|
|
||||||
idStrs := strings.Split(priorityIDStr, ",")
|
|
||||||
priorityIDs := make([]uuid.UUID, 0, len(idStrs))
|
|
||||||
for _, idStr := range idStrs {
|
|
||||||
if id, err := uuid.Parse(strings.TrimSpace(idStr)); err == nil {
|
|
||||||
priorityIDs = append(priorityIDs, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req.PriorityIDs = priorityIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse is_dispositioned filter
|
|
||||||
if isDispositionedStr := c.Query("is_dispositioned"); isDispositionedStr != "" {
|
|
||||||
isDispositioned := isDispositionedStr == "true" || isDispositionedStr == "1"
|
|
||||||
req.IsDispositioned = &isDispositioned
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse is_archived filter
|
|
||||||
if isArchivedStr := c.Query("is_archived"); isArchivedStr != "" {
|
|
||||||
isArchived := isArchivedStr == "true" || isArchivedStr == "1"
|
|
||||||
req.IsArchived = &isArchived
|
|
||||||
}
|
|
||||||
|
|
||||||
//req.DepartmentID = &departmentID
|
|
||||||
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) UpdateIncomingLetter(c *gin.Context) {
|
|
||||||
id, ok := h.parseUUID(c, "id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.UpdateIncomingLetterRequest
|
|
||||||
if !h.bindJSON(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.UpdateIncomingLetter(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) {
|
|
||||||
id, ok := h.parseUUID(c, "id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.SoftDeleteIncomingLetter(c.Request.Context(), id); err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, &contract.SuccessResponse{
|
|
||||||
Message: "Letter deleted successfully",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) BulkDeleteIncomingLetters(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
IDs []uuid.UUID `json:"ids" binding:"required,min=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, contract.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.BulkSoftDeleteIncomingLetters(c.Request.Context(), req.IDs); err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, &contract.SuccessResponse{
|
|
||||||
Message: fmt.Sprintf("%d letters deleted successfully", len(req.IDs)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) CreateDispositions(c *gin.Context) {
|
|
||||||
var req contract.CreateLetterDispositionRequest
|
|
||||||
if !h.bindJSON(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
req.FromDepartment = appCtx.DepartmentID
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateDispositions(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusCreated, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) GetEnhancedDispositionsByLetter(c *gin.Context) {
|
|
||||||
letterID, ok := h.parseUUID(c, "letter_id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetEnhancedDispositionsByLetter(c.Request.Context(), letterID)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) CreateDiscussion(c *gin.Context) {
|
|
||||||
letterID, ok := h.parseUUID(c, "letter_id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.CreateLetterDiscussionRequest
|
|
||||||
if !h.bindJSON(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateDiscussion(c.Request.Context(), letterID, &req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusCreated, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) UpdateDiscussion(c *gin.Context) {
|
|
||||||
letterID, ok := h.parseUUID(c, "letter_id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
discussionID, ok := h.parseUUID(c, "discussion_id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.UpdateLetterDiscussionRequest
|
|
||||||
if !h.bindJSON(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.UpdateDiscussion(c.Request.Context(), letterID, discussionID, &req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) SearchIncomingLetters(c *gin.Context) {
|
|
||||||
var req contract.SearchIncomingLettersRequest
|
|
||||||
if !h.bindQuery(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Page <= 0 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.Limit <= 0 {
|
|
||||||
req.Limit = 10
|
|
||||||
}
|
|
||||||
if req.SortOrder == "" {
|
|
||||||
req.SortOrder = "desc"
|
|
||||||
}
|
|
||||||
if req.SortBy == "" {
|
|
||||||
req.SortBy = "created_at"
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.SearchIncomingLetters(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) GetDepartmentDispositionStatus(c *gin.Context) {
|
|
||||||
letterID, ok := h.parseUUID(c, "letter_id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
departmentID := appcontext.FromGinContext(c.Request.Context()).DepartmentID
|
|
||||||
|
|
||||||
req := &contract.GetDepartmentDispositionStatusRequest{
|
|
||||||
LetterIncomingID: letterID,
|
|
||||||
DepartmentID: departmentID,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetDepartmentDispositionStatus(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) UpdateDispositionStatus(c *gin.Context) {
|
|
||||||
letterID, ok := h.parseUUID(c, "letter_id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.UpdateDispositionStatusRequest
|
|
||||||
if !h.bindJSON(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.LetterIncomingID = letterID
|
|
||||||
|
|
||||||
resp, err := h.svc.UpdateDispositionStatus(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) GetLetterCTA(c *gin.Context) {
|
|
||||||
letterID, ok := h.parseUUID(c, "letter_id")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetLetterCTA(c.Request.Context(), letterID)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) BulkArchiveIncomingLetters(c *gin.Context) {
|
|
||||||
var req contract.BulkArchiveLettersRequest
|
|
||||||
if !h.bindJSON(c, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.LetterIDs) == 0 {
|
|
||||||
h.respondError(c, http.StatusBadRequest, "at least one letter ID is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.BulkArchiveIncomingLetters(c.Request.Context(), req.LetterIDs)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterHandler) ArchiveIncomingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
h.respondError(c, http.StatusBadRequest, "invalid id")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.svc.ArchiveIncomingLetter(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
h.handleServiceError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, &contract.SuccessResponse{Message: "archived"})
|
|
||||||
}
|
|
||||||
@ -1,640 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"eslogad-be/internal/appcontext"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterOutgoingService interface {
|
|
||||||
CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
|
||||||
GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error)
|
|
||||||
ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error)
|
|
||||||
SearchOutgoingLetters(ctx context.Context, req *contract.SearchOutgoingLettersRequest) (*contract.SearchOutgoingLettersResponse, error)
|
|
||||||
UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
|
||||||
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error
|
|
||||||
BulkDeleteOutgoingLetters(ctx context.Context, ids []uuid.UUID) error
|
|
||||||
|
|
||||||
SubmitForApproval(ctx context.Context, letterID uuid.UUID) error
|
|
||||||
ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error
|
|
||||||
RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error
|
|
||||||
ReviseOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ReviseLetterRequest) error
|
|
||||||
SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
|
||||||
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
|
||||||
|
|
||||||
AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error
|
|
||||||
UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error
|
|
||||||
RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error
|
|
||||||
|
|
||||||
AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
|
|
||||||
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
|
|
||||||
|
|
||||||
AddFinalAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
|
|
||||||
RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
|
|
||||||
|
|
||||||
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error)
|
|
||||||
UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error
|
|
||||||
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
|
|
||||||
|
|
||||||
GetLetterApprovalInfo(ctx context.Context, letterID uuid.UUID) (*contract.LetterApprovalInfoResponse, error)
|
|
||||||
GetLetterApprovals(ctx context.Context, letterID uuid.UUID) (*contract.GetLetterApprovalsResponse, error)
|
|
||||||
GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error)
|
|
||||||
GetApprovalTimeline(ctx context.Context, letterID uuid.UUID) (*contract.ApprovalTimelineResponse, error)
|
|
||||||
BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterOutgoingHandler struct {
|
|
||||||
svc LetterOutgoingService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterOutgoingHandler(svc LetterOutgoingService) *LetterOutgoingHandler {
|
|
||||||
return &LetterOutgoingHandler{svc: svc}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) CreateOutgoingLetter(c *gin.Context) {
|
|
||||||
var req contract.CreateOutgoingLetterRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
req.UserID = appcontext.FromGinContext(ctx).UserID
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateOutgoingLetter(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) GetOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetOutgoingLetterByID(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) ListOutgoingLetters(c *gin.Context) {
|
|
||||||
var req contract.ListOutgoingLettersRequest
|
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid query parameters", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Page <= 0 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Limit <= 0 {
|
|
||||||
req.Limit = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
if ids := c.QueryArray("priority_ids[]"); len(ids) > 0 {
|
|
||||||
for _, s := range ids {
|
|
||||||
if id, err := uuid.Parse(s); err == nil {
|
|
||||||
req.PriorityIDs = append(req.PriorityIDs, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("[DEBUG] request: %v\n", req)
|
|
||||||
fmt.Printf("[DEBUG] Raw query: %v\n", c.Request.URL.RawQuery)
|
|
||||||
fmt.Printf("[DEBUG] Parsed form: %v\n", c.Request.URL.Query())
|
|
||||||
|
|
||||||
resp, err := h.svc.ListOutgoingLetters(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) UpdateOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.UpdateOutgoingLetterRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.UpdateOutgoingLetter(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) DeleteOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.DeleteOutgoingLetter(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "deleted"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) BulkDeleteOutgoingLetters(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
IDs []uuid.UUID `json:"ids" binding:"required,min=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "Invalid request body",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.BulkDeleteOutgoingLetters(c.Request.Context(), req.IDs); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{
|
|
||||||
Message: fmt.Sprintf("%d letters deleted successfully", len(req.IDs)),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) SubmitForApproval(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.SubmitForApproval(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "submitted for approval"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) ApproveOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.ApproveLetterRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.ApproveOutgoingLetter(c.Request.Context(), id, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "approved"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) RejectOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.RejectLetterRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.RejectOutgoingLetter(c.Request.Context(), id, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "rejected"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) ReviseOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.ReviseLetterRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.ReviseOutgoingLetter(c.Request.Context(), id, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "revised"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) SendOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.SendOutgoingLetter(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "sent"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) ArchiveOutgoingLetter(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.ArchiveOutgoingLetter(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "archived"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) AddRecipients(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.AddRecipientsRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.AddRecipients(c.Request.Context(), id, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "recipients added"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) UpdateRecipient(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
recipientID, err := uuid.Parse(c.Param("recipient_id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid recipient id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.UpdateRecipientRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.UpdateRecipient(c.Request.Context(), id, recipientID, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "recipient updated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) RemoveRecipient(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
recipientID, err := uuid.Parse(c.Param("recipient_id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid recipient id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.RemoveRecipient(c.Request.Context(), id, recipientID); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "recipient removed"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) AddAttachments(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.AddAttachmentsRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.AddAttachments(c.Request.Context(), id, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "attachment added"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) RemoveAttachment(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentID, err := uuid.Parse(c.Param("attachment_id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid attachment id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.RemoveAttachment(c.Request.Context(), id, attachmentID); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) AddFinalAttachments(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.AddAttachmentsRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.AddFinalAttachments(c.Request.Context(), id, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "attachment added"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) RemoveFinalAttachment(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentID, err := uuid.Parse(c.Param("attachment_id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid attachment id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.RemoveFinalAttachment(c.Request.Context(), id, attachmentID); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) CreateDiscussion(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.CreateDiscussionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateDiscussion(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) UpdateDiscussion(c *gin.Context) {
|
|
||||||
discussionID, err := uuid.Parse(c.Param("discussion_id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid discussion id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.UpdateDiscussionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.UpdateDiscussion(c.Request.Context(), discussionID, &req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "discussion updated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) DeleteDiscussion(c *gin.Context) {
|
|
||||||
discussionID, err := uuid.Parse(c.Param("discussion_id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid discussion id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.DeleteDiscussion(c.Request.Context(), discussionID); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "discussion deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) SearchOutgoingLetters(c *gin.Context) {
|
|
||||||
var req contract.SearchOutgoingLettersRequest
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid query parameters", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Page <= 0 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.Limit <= 0 {
|
|
||||||
req.Limit = 10
|
|
||||||
}
|
|
||||||
if req.SortOrder == "" {
|
|
||||||
req.SortOrder = "desc"
|
|
||||||
}
|
|
||||||
if req.SortBy == "" {
|
|
||||||
req.SortBy = "created_at"
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.SearchOutgoingLetters(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) GetLetterApprovalInfo(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetLetterApprovalInfo(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLetterApprovals returns all approvals and their status for a letter
|
|
||||||
func (h *LetterOutgoingHandler) GetLetterApprovals(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetLetterApprovals(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "letter not found", Code: http.StatusNotFound})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetApprovalDiscussions returns both approvals and discussions for an outgoing letter
|
|
||||||
func (h *LetterOutgoingHandler) GetApprovalDiscussions(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetApprovalDiscussions(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "letter not found", Code: http.StatusNotFound})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetApprovalTimeline returns a chronological timeline of approval and discussion events
|
|
||||||
func (h *LetterOutgoingHandler) GetApprovalTimeline(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetApprovalTimeline(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "letter not found", Code: http.StatusNotFound})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LetterOutgoingHandler) BulkArchiveOutgoingLetters(c *gin.Context) {
|
|
||||||
var req contract.BulkArchiveLettersRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid request body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.LetterIDs) == 0 {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "at least one letter ID is required", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.BulkArchiveOutgoingLetters(c.Request.Context(), req.LetterIDs)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
@ -1,388 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MasterService interface {
|
|
||||||
CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error)
|
|
||||||
UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error)
|
|
||||||
DeleteLabel(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error)
|
|
||||||
|
|
||||||
CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error)
|
|
||||||
UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error)
|
|
||||||
DeletePriority(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error)
|
|
||||||
|
|
||||||
CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error)
|
|
||||||
UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error)
|
|
||||||
DeleteInstitution(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error)
|
|
||||||
|
|
||||||
CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error)
|
|
||||||
UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error)
|
|
||||||
DeleteDispositionAction(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error)
|
|
||||||
|
|
||||||
CreateDepartment(ctx context.Context, req *contract.CreateDepartmentRequest) (*contract.GetDepartmentResponse, error)
|
|
||||||
GetDepartment(ctx context.Context, id uuid.UUID) (*contract.GetDepartmentResponse, error)
|
|
||||||
UpdateDepartment(ctx context.Context, id uuid.UUID, req *contract.UpdateDepartmentRequest) (*contract.GetDepartmentResponse, error)
|
|
||||||
DeleteDepartment(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error)
|
|
||||||
GetOrganizationalChart(ctx context.Context, rootPath string) (*contract.OrganizationalChartResponse, error)
|
|
||||||
GetOrganizationalChartByID(ctx context.Context, departmentID uuid.UUID) (*contract.OrganizationalChartResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MasterHandler struct{ svc MasterService }
|
|
||||||
|
|
||||||
func NewMasterHandler(svc MasterService) *MasterHandler { return &MasterHandler{svc: svc} }
|
|
||||||
|
|
||||||
func (h *MasterHandler) CreateLabel(c *gin.Context) {
|
|
||||||
var req contract.CreateLabelRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.CreateLabel(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) UpdateLabel(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdateLabelRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.UpdateLabel(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) DeleteLabel(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeleteLabel(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) ListLabels(c *gin.Context) {
|
|
||||||
resp, err := h.svc.ListLabels(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priorities
|
|
||||||
func (h *MasterHandler) CreatePriority(c *gin.Context) {
|
|
||||||
var req contract.CreatePriorityRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.CreatePriority(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(201, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) UpdatePriority(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdatePriorityRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.UpdatePriority(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) DeletePriority(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeletePriority(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) ListPriorities(c *gin.Context) {
|
|
||||||
resp, err := h.svc.ListPriorities(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Institutions
|
|
||||||
func (h *MasterHandler) CreateInstitution(c *gin.Context) {
|
|
||||||
var req contract.CreateInstitutionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.CreateInstitution(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(201, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) UpdateInstitution(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdateInstitutionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.UpdateInstitution(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) DeleteInstitution(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeleteInstitution(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) ListInstitutions(c *gin.Context) {
|
|
||||||
var req contract.ListInstitutionsRequest
|
|
||||||
|
|
||||||
if search := c.Query("search"); search != "" {
|
|
||||||
req.Search = &search
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.ListInstitutions(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disposition Actions
|
|
||||||
func (h *MasterHandler) CreateDispositionAction(c *gin.Context) {
|
|
||||||
var req contract.CreateDispositionActionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.CreateDispositionAction(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(201, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) UpdateDispositionAction(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdateDispositionActionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.UpdateDispositionAction(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) DeleteDispositionAction(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeleteDispositionAction(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
|
||||||
}
|
|
||||||
func (h *MasterHandler) ListDispositionActions(c *gin.Context) {
|
|
||||||
resp, err := h.svc.ListDispositionActions(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Departments
|
|
||||||
func (h *MasterHandler) CreateDepartment(c *gin.Context) {
|
|
||||||
var req contract.CreateDepartmentRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.CreateDepartment(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) GetDepartment(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.GetDepartment(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) UpdateDepartment(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdateDepartmentRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.UpdateDepartment(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) DeleteDepartment(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeleteDepartment(c.Request.Context(), id); err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "department deleted successfully"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) ListDepartments(c *gin.Context) {
|
|
||||||
var req contract.ListDepartmentsRequest
|
|
||||||
|
|
||||||
// Parse query parameters
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid query parameters", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.ListDepartments(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) GetOrganizationalChart(c *gin.Context) {
|
|
||||||
// Get optional root path from query parameter
|
|
||||||
rootPath := c.Query("root_path")
|
|
||||||
|
|
||||||
resp, err := h.svc.GetOrganizationalChart(c.Request.Context(), rootPath)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MasterHandler) GetOrganizationalChartByID(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid department id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetOrganizationalChartByID(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
@ -1,230 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/service"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NotificationHandler struct {
|
|
||||||
notificationService service.NotificationService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNotificationHandler(notificationService service.NotificationService) *NotificationHandler {
|
|
||||||
return &NotificationHandler{
|
|
||||||
notificationService: notificationService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerNotification handles single notification trigger
|
|
||||||
func (h *NotificationHandler) TriggerNotification(c *gin.Context) {
|
|
||||||
var req contract.TriggerNotificationRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.notificationService.TriggerNotification(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": resp.Message,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BulkTriggerNotification handles bulk notification trigger
|
|
||||||
func (h *NotificationHandler) BulkTriggerNotification(c *gin.Context) {
|
|
||||||
var req contract.BulkTriggerNotificationRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.notificationService.BulkTriggerNotification(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSubscriber retrieves subscriber information
|
|
||||||
func (h *NotificationHandler) GetSubscriber(c *gin.Context) {
|
|
||||||
userIDStr := c.Param("userId")
|
|
||||||
userID, err := uuid.Parse(userIDStr)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.notificationService.GetSubscriber(c.Request.Context(), userID)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateSubscriberChannel updates subscriber channel credentials
|
|
||||||
func (h *NotificationHandler) UpdateSubscriberChannel(c *gin.Context) {
|
|
||||||
var req contract.UpdateSubscriberChannelRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.notificationService.UpdateSubscriberChannel(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": resp.Message,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerNotificationForCurrentUser triggers notification for the authenticated user
|
|
||||||
func (h *NotificationHandler) TriggerNotificationForCurrentUser(c *gin.Context) {
|
|
||||||
// Get current user ID from context (set by auth middleware)
|
|
||||||
userIDInterface, exists := c.Get("user_id")
|
|
||||||
if !exists {
|
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := userIDInterface.(uuid.UUID)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID in context"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
TemplateID string `json:"template_id" validate:"required"`
|
|
||||||
TemplateData map[string]interface{} `json:"template_data,omitempty"`
|
|
||||||
Overrides *contract.NotificationOverrides `json:"overrides,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create trigger request with current user ID
|
|
||||||
triggerReq := &contract.TriggerNotificationRequest{
|
|
||||||
UserID: userID,
|
|
||||||
TemplateID: req.TemplateID,
|
|
||||||
TemplateData: req.TemplateData,
|
|
||||||
Overrides: req.Overrides,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.notificationService.TriggerNotification(c.Request.Context(), triggerReq)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": resp.Message,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentUserSubscriber retrieves subscriber information for the authenticated user
|
|
||||||
func (h *NotificationHandler) GetCurrentUserSubscriber(c *gin.Context) {
|
|
||||||
// Get current user ID from context (set by auth middleware)
|
|
||||||
userIDInterface, exists := c.Get("user_id")
|
|
||||||
if !exists {
|
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := userIDInterface.(uuid.UUID)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID in context"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.notificationService.GetSubscriber(c.Request.Context(), userID)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCurrentUserSubscriberChannel updates channel credentials for the authenticated user
|
|
||||||
func (h *NotificationHandler) UpdateCurrentUserSubscriberChannel(c *gin.Context) {
|
|
||||||
// Get current user ID from context (set by auth middleware)
|
|
||||||
userIDInterface, exists := c.Get("user_id")
|
|
||||||
if !exists {
|
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := userIDInterface.(uuid.UUID)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID in context"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
Channel string `json:"channel" validate:"required,oneof=email sms push chat in_app"`
|
|
||||||
Credentials map[string]interface{} `json:"credentials" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create update request with current user ID
|
|
||||||
updateReq := &contract.UpdateSubscriberChannelRequest{
|
|
||||||
UserID: userID,
|
|
||||||
Channel: req.Channel,
|
|
||||||
Credentials: req.Credentials,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.notificationService.UpdateSubscriberChannel(c.Request.Context(), updateReq)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": resp.Message,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OnlyOfficeService interface {
|
|
||||||
ProcessCallback(ctx context.Context, documentKey string, req *contract.OnlyOfficeCallbackRequest) (*contract.OnlyOfficeCallbackResponse, error)
|
|
||||||
GetEditorConfig(ctx context.Context, req *contract.GetEditorConfigRequest) (*contract.GetEditorConfigResponse, error)
|
|
||||||
LockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error
|
|
||||||
UnlockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error
|
|
||||||
GetDocumentSession(ctx context.Context, documentKey string) (*contract.DocumentSession, error)
|
|
||||||
GetOnlyOfficeConfig(ctx context.Context) (*contract.OnlyOfficeConfigInfo, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnlyOfficeHandler struct {
|
|
||||||
svc OnlyOfficeService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOnlyOfficeHandler(svc OnlyOfficeService) *OnlyOfficeHandler {
|
|
||||||
return &OnlyOfficeHandler{
|
|
||||||
svc: svc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessCallback handles OnlyOffice document server callbacks
|
|
||||||
// POST /api/v1/onlyoffice/callback/:key
|
|
||||||
func (h *OnlyOfficeHandler) ProcessCallback(c *gin.Context) {
|
|
||||||
documentKey := c.Param("key")
|
|
||||||
if documentKey == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.OnlyOfficeCallbackResponse{Error: 1})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.OnlyOfficeCallbackRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.OnlyOfficeCallbackResponse{Error: 2})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract JWT token from Authorization header if not in request body
|
|
||||||
if req.Token == "" {
|
|
||||||
authHeader := c.GetHeader("Authorization")
|
|
||||||
if authHeader != "" {
|
|
||||||
// Remove "Bearer " prefix if present
|
|
||||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
|
||||||
req.Token = authHeader[7:]
|
|
||||||
} else {
|
|
||||||
req.Token = authHeader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOffice requires the key in the request to match the URL
|
|
||||||
if req.Key != "" && req.Key != documentKey {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.OnlyOfficeCallbackResponse{Error: 1})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Key = documentKey
|
|
||||||
|
|
||||||
resp, err := h.svc.ProcessCallback(c.Request.Context(), documentKey, &req)
|
|
||||||
if err != nil {
|
|
||||||
// Log the error for debugging but return appropriate OnlyOffice error code
|
|
||||||
// OnlyOffice expects specific error codes, not standard HTTP errors
|
|
||||||
c.JSON(http.StatusOK, &contract.OnlyOfficeCallbackResponse{Error: 0})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnlyOffice expects 200 OK with error field in response
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
func (h *OnlyOfficeHandler) GetEditorConfig(c *gin.Context) {
|
|
||||||
var req contract.GetEditorConfigRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: fmt.Sprintf("invalid request body: %v", err),
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetEditorConfig(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// LockDocument locks a document for editing
|
|
||||||
// POST /api/v1/onlyoffice/lock/:id
|
|
||||||
func (h *OnlyOfficeHandler) LockDocument(c *gin.Context) {
|
|
||||||
documentID, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "invalid document id",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID from context
|
|
||||||
userCtx := c.MustGet("user").(map[string]interface{})
|
|
||||||
userID, err := uuid.Parse(userCtx["user_id"].(string))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "invalid user context",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.LockDocument(c.Request.Context(), documentID, userID); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "document locked"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnlockDocument unlocks a document
|
|
||||||
// POST /api/v1/onlyoffice/unlock/:id
|
|
||||||
func (h *OnlyOfficeHandler) UnlockDocument(c *gin.Context) {
|
|
||||||
documentID, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "invalid document id",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID from context
|
|
||||||
userCtx := c.MustGet("user").(map[string]interface{})
|
|
||||||
userID, err := uuid.Parse(userCtx["user_id"].(string))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "invalid user context",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.svc.UnlockDocument(c.Request.Context(), documentID, userID); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "document unlocked"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDocumentSession gets document session information
|
|
||||||
// GET /api/v1/onlyoffice/session/:key
|
|
||||||
func (h *OnlyOfficeHandler) GetDocumentSession(c *gin.Context) {
|
|
||||||
documentKey := c.Param("key")
|
|
||||||
if documentKey == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
|
||||||
Error: "document key is required",
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session, err := h.svc.GetDocumentSession(c.Request.Context(), documentKey)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(session))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOnlyOfficeConfig returns the OnlyOffice configuration
|
|
||||||
// GET /api/v1/onlyoffice/config
|
|
||||||
func (h *OnlyOfficeHandler) GetOnlyOfficeConfig(c *gin.Context) {
|
|
||||||
config, err := h.svc.GetOnlyOfficeConfig(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{
|
|
||||||
Error: err.Error(),
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(config))
|
|
||||||
}
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RBACService interface {
|
|
||||||
CreatePermission(ctx context.Context, req *contract.CreatePermissionRequest) (*contract.PermissionResponse, error)
|
|
||||||
UpdatePermission(ctx context.Context, id uuid.UUID, req *contract.UpdatePermissionRequest) (*contract.PermissionResponse, error)
|
|
||||||
DeletePermission(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListPermissions(ctx context.Context) (*contract.ListPermissionsResponse, error)
|
|
||||||
|
|
||||||
CreateRole(ctx context.Context, req *contract.CreateRoleRequest) (*contract.RoleWithPermissionsResponse, error)
|
|
||||||
UpdateRole(ctx context.Context, id uuid.UUID, req *contract.UpdateRoleRequest) (*contract.RoleWithPermissionsResponse, error)
|
|
||||||
DeleteRole(ctx context.Context, id uuid.UUID) error
|
|
||||||
ListRoles(ctx context.Context) (*contract.ListRolesResponse, error)
|
|
||||||
|
|
||||||
// New methods
|
|
||||||
GetPermissionsGrouped(ctx context.Context) (*contract.PermissionsGroupedResponse, error)
|
|
||||||
CreateOrUpdateRole(ctx context.Context, req *contract.CreateOrUpdateRoleRequest) (*contract.RoleDetailResponse, error)
|
|
||||||
GetRoleDetail(ctx context.Context, roleID uuid.UUID) (*contract.RoleDetailResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RBACHandler struct{ svc RBACService }
|
|
||||||
|
|
||||||
func NewRBACHandler(svc RBACService) *RBACHandler { return &RBACHandler{svc: svc} }
|
|
||||||
|
|
||||||
func (h *RBACHandler) CreatePermission(c *gin.Context) {
|
|
||||||
var req contract.CreatePermissionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.CreatePermission(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) UpdatePermission(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdatePermissionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.UpdatePermission(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) DeletePermission(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeletePermission(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) ListPermissions(c *gin.Context) {
|
|
||||||
resp, err := h.svc.ListPermissions(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) CreateRole(c *gin.Context) {
|
|
||||||
var req contract.CreateRoleRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.CreateRole(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) UpdateRole(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdateRoleRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.UpdateRole(c.Request.Context(), id, &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) DeleteRole(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeleteRole(c.Request.Context(), id); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) ListRoles(c *gin.Context) {
|
|
||||||
resp, err := h.svc.ListRoles(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// New handlers for the required API endpoints
|
|
||||||
func (h *RBACHandler) GetPermissionsGrouped(c *gin.Context) {
|
|
||||||
resp, err := h.svc.GetPermissionsGrouped(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) CreateOrUpdateRole(c *gin.Context) {
|
|
||||||
var req contract.CreateOrUpdateRoleRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body: " + err.Error(), Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.CreateOrUpdateRole(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RBACHandler) GetRoleDetail(c *gin.Context) {
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.svc.GetRoleDetail(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"eslogad-be/internal/constants"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/logger"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RepositoryAttachmentHandler struct {
|
|
||||||
attachmentService RepositoryAttachmentService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRepositoryAttachmentHandler(attachmentService RepositoryAttachmentService) *RepositoryAttachmentHandler {
|
|
||||||
return &RepositoryAttachmentHandler{
|
|
||||||
attachmentService: attachmentService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RepositoryAttachmentHandler) CreateAttachment(c *gin.Context) {
|
|
||||||
var req contract.CreateRepositoryAttachmentRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::CreateAttachment -> request binding failed")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userResponse, err := h.attachmentService.CreateAttachment(c.Request.Context(), &req)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::CreateAttachment -> Failed to create user from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::CreateUser -> Successfully created repository attachment = %+v", userResponse)
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(userResponse))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RepositoryAttachmentHandler) DeleteAttachment(c *gin.Context) {
|
|
||||||
attachmentIDStr := c.Param("id")
|
|
||||||
attachmentID, err := uuid.Parse(attachmentIDStr)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::DeleteAttachment -> Invalid attachment id")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.attachmentService.DeleteAttachment(c.Request.Context(), attachmentID)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::DeleteAttachment -> Failed to delete attachment from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Info("UserHandler::DeleteAttachment -> Successfully deleted attachment")
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "User deleted successfully"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RepositoryAttachmentHandler) GetAttachment(c *gin.Context) {
|
|
||||||
attachmentIDStr := c.Param("id")
|
|
||||||
attachmentID, err := uuid.Parse(attachmentIDStr)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::GetAttachment -> Invalid attachment ID")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentResponse, err := h.attachmentService.GetById(c.Request.Context(), attachmentID)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::GetAttachment -> Failed to get attachment from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::GetAttachment -> Successfully retrieved attachment = %+v", attachmentResponse)
|
|
||||||
c.JSON(http.StatusOK, attachmentResponse)
|
|
||||||
}
|
|
||||||
func (h *RepositoryAttachmentHandler) ListAttachment(c *gin.Context) {
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
req := &contract.ListRepositoryAttachmentsRequest{
|
|
||||||
Page: 1,
|
|
||||||
Limit: 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
if page := c.Query("page"); page != "" {
|
|
||||||
if p, err := strconv.Atoi(page); err == nil {
|
|
||||||
req.Page = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit := c.Query("limit"); limit != "" {
|
|
||||||
if l, err := strconv.Atoi(limit); err == nil {
|
|
||||||
req.Limit = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentsResponse, err := h.attachmentService.ListAttachment(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ListUsers -> Failed to list users from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::ListUsers -> Successfully listed users = %+v", attachmentsResponse)
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(attachmentsResponse))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RepositoryAttachmentHandler) sendValidationErrorResponse(c *gin.Context, message string, errorCode string) {
|
|
||||||
statusCode := constants.HttpErrorMap[errorCode]
|
|
||||||
if statusCode == 0 {
|
|
||||||
statusCode = http.StatusBadRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
errorResponse := &contract.ErrorResponse{
|
|
||||||
Error: message,
|
|
||||||
Code: statusCode,
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"error_code": errorCode,
|
|
||||||
"entity": constants.UserValidatorEntity,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
c.JSON(statusCode, errorResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RepositoryAttachmentHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
|
||||||
errorResponse := &contract.ErrorResponse{
|
|
||||||
Error: message,
|
|
||||||
Code: statusCode,
|
|
||||||
Details: map[string]interface{}{},
|
|
||||||
}
|
|
||||||
c.JSON(statusCode, errorResponse)
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RepositoryAttachmentService interface {
|
|
||||||
CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error)
|
|
||||||
DeleteAttachment(ctx context.Context, id uuid.UUID) error
|
|
||||||
GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error)
|
|
||||||
ListAttachment(ctx context.Context, req *contract.ListRepositoryAttachmentsRequest) (*contract.ListRepositoryAttachmentsResponse, error)
|
|
||||||
}
|
|
||||||
@ -4,10 +4,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"eslogad-be/internal/appcontext"
|
"go-backend-template/internal/constants"
|
||||||
"eslogad-be/internal/constants"
|
"go-backend-template/internal/contract"
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/logger"
|
||||||
"eslogad-be/internal/logger"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -149,248 +148,32 @@ func (h *UserHandler) GetUser(c *gin.Context) {
|
|||||||
func (h *UserHandler) ListUsers(c *gin.Context) {
|
func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
req := &contract.ListUsersRequest{
|
page := 1
|
||||||
Page: 1,
|
limit := 10
|
||||||
Limit: 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
if page := c.Query("page"); page != "" {
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
if p, err := strconv.Atoi(page); err == nil {
|
if p, err := strconv.Atoi(pageStr); err == nil {
|
||||||
req.Page = p
|
page = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit := c.Query("limit"); limit != "" {
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
if l, err := strconv.Atoi(limit); err == nil {
|
if l, err := strconv.Atoi(limitStr); err == nil {
|
||||||
req.Limit = l
|
limit = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var roleParam *string
|
usersResponse, err := h.userService.GetUsers(ctx, page, limit)
|
||||||
if role := c.Query("role"); role != "" {
|
|
||||||
roleParam = &role
|
|
||||||
req.Role = &role
|
|
||||||
}
|
|
||||||
|
|
||||||
if roleCode := c.Query("role_code"); roleCode != "" {
|
|
||||||
req.RoleCode = &roleCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.RoleCode == nil && roleParam != nil {
|
|
||||||
req.RoleCode = roleParam
|
|
||||||
}
|
|
||||||
|
|
||||||
if search := c.Query("search"); search != "" {
|
|
||||||
req.Search = &search
|
|
||||||
}
|
|
||||||
|
|
||||||
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
|
||||||
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
|
||||||
req.IsActive = &isActive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validationError, validationErrorCode := h.userValidator.ValidateListUsersRequest(req)
|
|
||||||
if validationError != nil {
|
|
||||||
logger.FromContext(c).WithError(validationError).Error("UserHandler::ListUsers -> request validation failed")
|
|
||||||
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
usersResponse, err := h.userService.ListUsers(ctx, req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ListUsers -> Failed to list users from service")
|
logger.FromContext(c).WithError(err).Error("UserHandler::ListUsers -> Failed to list users from service")
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::ListUsers -> Successfully listed users = %+v", usersResponse)
|
logger.FromContext(c).Infof("UserHandler::ListUsers -> Successfully listed users")
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(usersResponse))
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(usersResponse))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
|
||||||
userIDStr := c.Param("id")
|
|
||||||
userID, err := uuid.Parse(userIDStr)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ChangePassword -> Invalid user ID")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
validationError, validationErrorCode := h.userValidator.ValidateUserID(userID)
|
|
||||||
if validationError != nil {
|
|
||||||
logger.FromContext(c).WithError(validationError).Error("UserHandler::ChangePassword -> user ID validation failed")
|
|
||||||
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.ChangePasswordRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ChangePassword -> request binding failed")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
validationError, validationErrorCode = h.userValidator.ValidateChangePasswordRequest(&req)
|
|
||||||
if validationError != nil {
|
|
||||||
logger.FromContext(c).WithError(validationError).Error("UserHandler::ChangePassword -> request validation failed")
|
|
||||||
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.userService.ChangePassword(c.Request.Context(), userID, &req)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ChangePassword -> Failed to change password from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Info("UserHandler::ChangePassword -> Successfully changed password")
|
|
||||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "Password changed successfully"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) ChangeUserPassword(c *gin.Context) {
|
|
||||||
userIDStr := c.Param("id")
|
|
||||||
userID, err := uuid.Parse(userIDStr)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ChangeUserPassword -> Invalid user ID")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
validationError, validationErrorCode := h.userValidator.ValidateUserID(userID)
|
|
||||||
if validationError != nil {
|
|
||||||
logger.FromContext(c).WithError(validationError).Error("UserHandler::ChangeUserPassword -> user ID validation failed")
|
|
||||||
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req contract.ChangeUserPasswordRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ChangeUserPassword -> request binding failed")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.userService.ChangeUserPassword(c.Request.Context(), userID, &req)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ChangeUserPassword -> Failed to change password from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Info("UserHandler::ChangeUserPassword -> Successfully changed password")
|
|
||||||
c.JSON(http.StatusOK, &contract.NewSuccessResponse{Success: true, Message: "Password changed successfully"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
if appCtx.UserID == uuid.Nil {
|
|
||||||
h.sendErrorResponse(c, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
profile, err := h.userService.GetProfile(c.Request.Context(), appCtx.UserID)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::GetProfile -> Failed to get profile")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(profile))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
|
||||||
if appCtx.UserID == uuid.Nil {
|
|
||||||
h.sendErrorResponse(c, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req contract.UpdateUserProfileRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updated, err := h.userService.UpdateProfile(c.Request.Context(), appCtx.UserID, &req)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::UpdateProfile -> Failed to update profile")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(updated))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) ListTitles(c *gin.Context) {
|
|
||||||
titles, err := h.userService.ListTitles(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::ListTitles -> Failed to get titles from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::ListTitles -> Successfully retrieved titles = %+v", titles)
|
|
||||||
c.JSON(http.StatusOK, titles)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) GetUserProfile(c *gin.Context) {
|
|
||||||
userIDStr := c.Param("id")
|
|
||||||
userID, err := uuid.Parse(userIDStr)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::GetUserProfile -> Invalid user ID")
|
|
||||||
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
validationError, validationErrorCode := h.userValidator.ValidateUserID(userID)
|
|
||||||
if validationError != nil {
|
|
||||||
logger.FromContext(c).WithError(validationError).Error("UserHandler::GetUserProfile -> user ID validation failed")
|
|
||||||
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
profile, err := h.userService.GetProfile(c.Request.Context(), userID)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::GetUserProfile -> Failed to get user profile from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::GetUserProfile -> Successfully retrieved user profile for user ID = %s", userID)
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(profile))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) GetActiveUsersForMention(c *gin.Context) {
|
|
||||||
search := c.Query("search")
|
|
||||||
limitStr := c.DefaultQuery("limit", "50")
|
|
||||||
|
|
||||||
limit, err := strconv.Atoi(limitStr)
|
|
||||||
if err != nil || limit <= 0 {
|
|
||||||
limit = 50
|
|
||||||
}
|
|
||||||
if limit > 100 {
|
|
||||||
limit = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchPtr *string
|
|
||||||
if search != "" {
|
|
||||||
searchPtr = &search
|
|
||||||
}
|
|
||||||
|
|
||||||
users, err := h.userService.GetActiveUsersForMention(c.Request.Context(), searchPtr, limit)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c).WithError(err).Error("UserHandler::GetActiveUsersForMention -> Failed to get active users from service")
|
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := contract.MentionUsersResponse{
|
|
||||||
Users: users,
|
|
||||||
Count: len(users),
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::GetActiveUsersForMention -> Successfully retrieved %d active users", len(users))
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
||||||
errorResponse := &contract.ErrorResponse{
|
errorResponse := &contract.ErrorResponse{
|
||||||
Error: message,
|
Error: message,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@ -12,16 +12,5 @@ type UserService interface {
|
|||||||
UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error)
|
UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error)
|
||||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||||
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
|
GetUsers(ctx context.Context, page, limit int) (*contract.PaginatedUserResponse, error)
|
||||||
ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error)
|
|
||||||
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
|
||||||
ChangeUserPassword(ctx context.Context, userID uuid.UUID, req *contract.ChangeUserPasswordRequest) error
|
|
||||||
|
|
||||||
GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error)
|
|
||||||
UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
|
||||||
|
|
||||||
ListTitles(ctx context.Context) (*contract.ListTitlesResponse, error)
|
|
||||||
|
|
||||||
// Get active users for mention purposes
|
|
||||||
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@ -12,5 +12,4 @@ type UserValidator interface {
|
|||||||
ValidateListUsersRequest(req *contract.ListUsersRequest) (error, string)
|
ValidateListUsersRequest(req *contract.ListUsersRequest) (error, string)
|
||||||
ValidateChangePasswordRequest(req *contract.ChangePasswordRequest) (error, string)
|
ValidateChangePasswordRequest(req *contract.ChangePasswordRequest) (error, string)
|
||||||
ValidateUserID(userID uuid.UUID) (error, string)
|
ValidateUserID(userID uuid.UUID) (error, string)
|
||||||
ValidateUpdateUserOutletRequest(req *contract.UpdateUserOutletRequest) (error, string)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/appcontext"
|
"go-backend-template/internal/appcontext"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/internal/appcontext"
|
"go-backend-template/internal/appcontext"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"eslogad-be/internal/constants"
|
"go-backend-template/internal/constants"
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
"eslogad-be/internal/logger"
|
"go-backend-template/internal/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -22,51 +22,6 @@ func NewAuthMiddleware(authService AuthValidateService) *AuthMiddleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
token := m.extractTokenFromHeader(c)
|
|
||||||
if token == "" {
|
|
||||||
logger.FromContext(c.Request.Context()).Error("AuthMiddleware::RequireAuth -> Missing authorization token")
|
|
||||||
m.sendErrorResponse(c, "Authorization token is required", http.StatusUnauthorized)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userResponse, err := m.authService.ValidateToken(token)
|
|
||||||
if err != nil {
|
|
||||||
logger.FromContext(c.Request.Context()).WithError(err).Error("AuthMiddleware::RequireAuth -> Invalid token")
|
|
||||||
m.sendErrorResponse(c, "Invalid or expired token", http.StatusUnauthorized)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
|
||||||
setKeyInContext(c, appcontext.UserNameKey, userResponse.Name)
|
|
||||||
|
|
||||||
if len(userResponse.DepartmentResponse) > 0 {
|
|
||||||
departmentID := userResponse.DepartmentResponse[0].ID.String()
|
|
||||||
setKeyInContext(c, appcontext.DepartmentIDKey, departmentID)
|
|
||||||
} else {
|
|
||||||
setKeyInContext(c, appcontext.DepartmentIDKey, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(userResponse.Roles) > 0 {
|
|
||||||
userRole := userResponse.Roles[0].Code
|
|
||||||
setKeyInContext(c, appcontext.UserRoleKey, userRole)
|
|
||||||
} else {
|
|
||||||
setKeyInContext(c, appcontext.UserRoleKey, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if roles, perms, err := m.authService.ExtractAccess(token); err == nil {
|
|
||||||
c.Set("user_roles", roles)
|
|
||||||
c.Set("user_permissions", perms)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email)
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *AuthMiddleware) RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
func (m *AuthMiddleware) RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
|
|||||||
@ -1,13 +1,4 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthValidateService interface {
|
type AuthValidateService interface {
|
||||||
ValidateToken(tokenString string) (*contract.UserResponse, error)
|
|
||||||
RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error)
|
|
||||||
Logout(ctx context.Context, tokenString string) error
|
|
||||||
ExtractAccess(tokenString string) (roles []string, permissions []string, err error)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/appcontext"
|
"go-backend-template/internal/appcontext"
|
||||||
"eslogad-be/internal/constants"
|
"go-backend-template/internal/constants"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/appcontext"
|
"go-backend-template/internal/appcontext"
|
||||||
"eslogad-be/internal/constants"
|
"go-backend-template/internal/constants"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
"eslogad-be/internal/logger"
|
"go-backend-template/internal/logger"
|
||||||
"eslogad-be/internal/util"
|
"go-backend-template/internal/util"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/internal/constants"
|
"go-backend-template/internal/constants"
|
||||||
"eslogad-be/internal/logger"
|
"go-backend-template/internal/logger"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
"eslogad-be/internal/logger"
|
"go-backend-template/internal/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/contract"
|
"go-backend-template/internal/contract"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActivityLogProcessorImpl struct {
|
|
||||||
repo *repository.LetterIncomingActivityLogRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewActivityLogProcessor(repo *repository.LetterIncomingActivityLogRepository) *ActivityLogProcessorImpl {
|
|
||||||
return &ActivityLogProcessorImpl{repo: repo}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ActivityLogProcessorImpl) Log(ctx context.Context, letterID uuid.UUID, actionType string, actorUserID *uuid.UUID, actorDepartmentID *uuid.UUID, targetType *string, targetID *uuid.UUID, fromStatus *string, toStatus *string, contextData map[string]interface{}) error {
|
|
||||||
ctxJSON := entities.JSONB{}
|
|
||||||
for k, v := range contextData {
|
|
||||||
ctxJSON[k] = v
|
|
||||||
}
|
|
||||||
entry := &entities.LetterIncomingActivityLog{
|
|
||||||
LetterID: letterID,
|
|
||||||
ActionType: actionType,
|
|
||||||
ActorUserID: actorUserID,
|
|
||||||
ActorDepartmentID: actorDepartmentID,
|
|
||||||
TargetType: targetType,
|
|
||||||
TargetID: targetID,
|
|
||||||
FromStatus: fromStatus,
|
|
||||||
ToStatus: toStatus,
|
|
||||||
Context: ctxJSON,
|
|
||||||
}
|
|
||||||
return p.repo.Create(ctx, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ActivityLogProcessorImpl) LogLetterDispositionStatusUpdate(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, status string) error {
|
|
||||||
return p.Log(ctx, letterID, "disposition_status_update", &userID, nil, nil, nil, nil, &status, map[string]interface{}{
|
|
||||||
"status": status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ActivityLogProcessorImpl) LogLetterCreated(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, letterNumber string) error {
|
|
||||||
return p.Log(ctx, letterID, "letter.created", &userID, nil, nil, nil, nil, nil, map[string]interface{}{
|
|
||||||
"letter_number": letterNumber,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ActivityLogProcessorImpl) LogAttachmentUploaded(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, fileName string, fileType string) error {
|
|
||||||
return p.Log(ctx, letterID, "attachment.uploaded", &userID, nil, nil, nil, nil, nil, map[string]interface{}{
|
|
||||||
"file_name": fileName,
|
|
||||||
"file_type": fileType,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ActivityLogProcessorImpl) LogDispositionCreated(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, departmentCount int) error {
|
|
||||||
return p.Log(ctx, letterID, "disposition.created", &userID, nil, nil, nil, nil, nil, map[string]interface{}{
|
|
||||||
"department_count": departmentCount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
"eslogad-be/internal/transformer"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CachedUserProcessor wraps UserProcessor with caching for frequently accessed users
|
|
||||||
type CachedUserProcessor struct {
|
|
||||||
userRepo *repository.UserRepositoryImpl
|
|
||||||
profileRepo *repository.UserProfileRepository
|
|
||||||
cache map[uuid.UUID]*cacheEntry
|
|
||||||
mu sync.RWMutex
|
|
||||||
ttl time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type cacheEntry struct {
|
|
||||||
user *contract.UserResponse
|
|
||||||
expiresAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCachedUserProcessor(userRepo *repository.UserRepositoryImpl, profileRepo *repository.UserProfileRepository, userRoleProc UserRoleProcessor) *CachedUserProcessor {
|
|
||||||
return &CachedUserProcessor{
|
|
||||||
userRepo: userRepo,
|
|
||||||
profileRepo: profileRepo,
|
|
||||||
cache: make(map[uuid.UUID]*cacheEntry),
|
|
||||||
ttl: 5 * time.Minute, // Cache for 5 minutes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CachedUserProcessor) GetUserByIDCached(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
|
|
||||||
p.mu.RLock()
|
|
||||||
if entry, exists := p.cache[id]; exists {
|
|
||||||
if entry.expiresAt.After(time.Now()) {
|
|
||||||
p.mu.RUnlock()
|
|
||||||
return entry.user, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.mu.RUnlock()
|
|
||||||
|
|
||||||
// Not in cache or expired, fetch from database using the light method
|
|
||||||
user, err := p.userRepo.GetByIDLight(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to contract response
|
|
||||||
resp := &contract.UserResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Email: user.Email,
|
|
||||||
Name: user.Name,
|
|
||||||
IsActive: user.IsActive,
|
|
||||||
CreatedAt: user.CreatedAt,
|
|
||||||
UpdatedAt: user.UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store in cache
|
|
||||||
p.mu.Lock()
|
|
||||||
p.cache[id] = &cacheEntry{
|
|
||||||
user: resp,
|
|
||||||
expiresAt: time.Now().Add(p.ttl),
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
// Clean expired entries periodically
|
|
||||||
go p.cleanExpiredEntries()
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByIDFull retrieves full user with all relationships - no caching
|
|
||||||
func (p *CachedUserProcessor) GetUserByIDFull(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
|
|
||||||
user, err := p.userRepo.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := transformer.EntityToContract(user)
|
|
||||||
if resp != nil {
|
|
||||||
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
|
|
||||||
resp.Roles = transformer.RolesToContract(roles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidateCache removes a user from cache
|
|
||||||
func (p *CachedUserProcessor) InvalidateCache(userID uuid.UUID) {
|
|
||||||
p.mu.Lock()
|
|
||||||
delete(p.cache, userID)
|
|
||||||
p.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanExpiredEntries removes expired cache entries
|
|
||||||
func (p *CachedUserProcessor) cleanExpiredEntries() {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
for id, entry := range p.cache {
|
|
||||||
if entry.expiresAt.Before(now) {
|
|
||||||
delete(p.cache, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CachedUserWrapper wraps CachedUserProcessor to implement middleware.UserProcessor interface
|
|
||||||
type CachedUserWrapper struct {
|
|
||||||
cached *CachedUserProcessor
|
|
||||||
full *UserProcessorImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCachedUserWrapper creates a new wrapper
|
|
||||||
func NewCachedUserWrapper(cached *CachedUserProcessor, full *UserProcessorImpl) *CachedUserWrapper {
|
|
||||||
return &CachedUserWrapper{
|
|
||||||
cached: cached,
|
|
||||||
full: full,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByID uses cached version for fast lookups
|
|
||||||
func (w *CachedUserWrapper) GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
|
|
||||||
return w.cached.GetUserByIDCached(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByIDFull uses full version when all data is needed
|
|
||||||
func (w *CachedUserWrapper) GetUserByIDFull(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
|
|
||||||
return w.full.GetUserByID(ctx, id)
|
|
||||||
}
|
|
||||||
@ -1,270 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DepartmentProcessor handles all department-related business logic
|
|
||||||
type DepartmentProcessor interface {
|
|
||||||
Create(ctx context.Context, department *entities.Department) error
|
|
||||||
Get(ctx context.Context, id uuid.UUID) (*entities.Department, error)
|
|
||||||
GetByPath(ctx context.Context, path string) (*entities.Department, error)
|
|
||||||
Update(ctx context.Context, department *entities.Department) error
|
|
||||||
Delete(ctx context.Context, id uuid.UUID) error
|
|
||||||
List(ctx context.Context, search string, limit, offset int) ([]entities.Department, int64, error)
|
|
||||||
|
|
||||||
// Hierarchy operations
|
|
||||||
GetChildren(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error)
|
|
||||||
GetAllDescendants(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error)
|
|
||||||
UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error
|
|
||||||
ValidateHierarchy(ctx context.Context, departmentID, newParentID uuid.UUID) error
|
|
||||||
|
|
||||||
// Tree building operations
|
|
||||||
GetDepartmentWithDescendants(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error)
|
|
||||||
GetDepartmentWithParentAndSiblings(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error)
|
|
||||||
GetAllDepartments(ctx context.Context) ([]entities.Department, error)
|
|
||||||
GetDepartmentsByRootPath(ctx context.Context, rootPath string) ([]entities.Department, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DepartmentProcessorImpl struct {
|
|
||||||
departmentRepo *repository.DepartmentRepository
|
|
||||||
txManager *repository.TxManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDepartmentProcessor creates a new department processor
|
|
||||||
func NewDepartmentProcessor(
|
|
||||||
departmentRepo *repository.DepartmentRepository,
|
|
||||||
txManager *repository.TxManager,
|
|
||||||
) *DepartmentProcessorImpl {
|
|
||||||
return &DepartmentProcessorImpl{
|
|
||||||
departmentRepo: departmentRepo,
|
|
||||||
txManager: txManager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a new department
|
|
||||||
func (p *DepartmentProcessorImpl) Create(ctx context.Context, department *entities.Department) error {
|
|
||||||
// Build path based on parent
|
|
||||||
if department.ParentDepartmentID != nil && *department.ParentDepartmentID != uuid.Nil {
|
|
||||||
parent, err := p.departmentRepo.GetByID(ctx, *department.ParentDepartmentID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parent department not found: %w", err)
|
|
||||||
}
|
|
||||||
department.Path = parent.Path + "." + department.Code
|
|
||||||
} else {
|
|
||||||
department.Path = department.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.departmentRepo.Create(ctx, department)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a department by ID
|
|
||||||
func (p *DepartmentProcessorImpl) Get(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
|
|
||||||
return p.departmentRepo.Get(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByPath retrieves a department by its path
|
|
||||||
func (p *DepartmentProcessorImpl) GetByPath(ctx context.Context, path string) (*entities.Department, error) {
|
|
||||||
return p.departmentRepo.GetByPath(ctx, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates a department with hierarchy validation
|
|
||||||
func (p *DepartmentProcessorImpl) Update(ctx context.Context, department *entities.Department) error {
|
|
||||||
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
||||||
// Get the current department state
|
|
||||||
current, err := p.departmentRepo.Get(txCtx, department.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store old values for comparison
|
|
||||||
oldPath := current.Path
|
|
||||||
oldParentID := current.ParentDepartmentID
|
|
||||||
pathChanged := false
|
|
||||||
|
|
||||||
// Update path if parent or code changed
|
|
||||||
if department.ParentDepartmentID != oldParentID || department.Code != current.Code {
|
|
||||||
// Rebuild path
|
|
||||||
if department.ParentDepartmentID == nil {
|
|
||||||
department.Path = department.Code
|
|
||||||
} else {
|
|
||||||
parent, err := p.departmentRepo.GetByID(txCtx, *department.ParentDepartmentID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parent department not found: %w", err)
|
|
||||||
}
|
|
||||||
department.Path = parent.Path + "." + department.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
if department.Path != oldPath {
|
|
||||||
pathChanged = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the department
|
|
||||||
if err := p.departmentRepo.Update(txCtx, department); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update children paths if necessary
|
|
||||||
if pathChanged {
|
|
||||||
if err := p.updateChildrenPathsRecursively(txCtx, department.ID, department.Path); err != nil {
|
|
||||||
return fmt.Errorf("failed to update children paths: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes a department
|
|
||||||
func (p *DepartmentProcessorImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
|
||||||
return p.departmentRepo.Delete(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List lists departments with pagination
|
|
||||||
func (p *DepartmentProcessorImpl) List(ctx context.Context, search string, limit, offset int) ([]entities.Department, int64, error) {
|
|
||||||
return p.departmentRepo.List(ctx, search, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChildren gets direct children of a department
|
|
||||||
func (p *DepartmentProcessorImpl) GetChildren(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error) {
|
|
||||||
// First get the parent department to get its path
|
|
||||||
parent, err := p.departmentRepo.Get(ctx, parentID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if parent == nil {
|
|
||||||
return []entities.Department{}, nil
|
|
||||||
}
|
|
||||||
return p.departmentRepo.GetChildren(ctx, parent.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllDescendants gets all descendants of a department
|
|
||||||
func (p *DepartmentProcessorImpl) GetAllDescendants(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error) {
|
|
||||||
return p.departmentRepo.GetAllDescendants(ctx, parentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateChildrenPaths updates paths for all children
|
|
||||||
func (p *DepartmentProcessorImpl) UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error {
|
|
||||||
return p.departmentRepo.UpdateChildrenPaths(ctx, oldPath, newPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateHierarchy validates that setting newParentID won't create circular references
|
|
||||||
func (p *DepartmentProcessorImpl) ValidateHierarchy(ctx context.Context, departmentID, newParentID uuid.UUID) error {
|
|
||||||
// Can't be its own parent
|
|
||||||
if departmentID == newParentID {
|
|
||||||
return fmt.Errorf("department cannot be its own parent")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if new parent is a descendant
|
|
||||||
descendants, err := p.GetAllDescendants(ctx, departmentID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check descendants: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, desc := range descendants {
|
|
||||||
if desc.ID == newParentID {
|
|
||||||
return fmt.Errorf("cannot set a descendant as parent (circular reference)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDepartmentWithDescendants gets a department and all its descendants
|
|
||||||
func (p *DepartmentProcessorImpl) GetDepartmentWithDescendants(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error) {
|
|
||||||
// Get the department
|
|
||||||
dept, err := p.departmentRepo.Get(ctx, departmentID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
departments := []entities.Department{*dept}
|
|
||||||
|
|
||||||
// Get all descendants
|
|
||||||
descendants, err := p.departmentRepo.GetAllDescendants(ctx, departmentID)
|
|
||||||
if err == nil {
|
|
||||||
departments = append(departments, descendants...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return departments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDepartmentWithParentAndSiblings gets a department with its parent and all siblings
|
|
||||||
func (p *DepartmentProcessorImpl) GetDepartmentWithParentAndSiblings(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error) {
|
|
||||||
// Get the department
|
|
||||||
dept, err := p.departmentRepo.Get(ctx, departmentID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var departments []entities.Department
|
|
||||||
|
|
||||||
// If has parent, get parent and all its descendants
|
|
||||||
if dept.ParentDepartmentID != nil {
|
|
||||||
parent, err := p.departmentRepo.Get(ctx, *dept.ParentDepartmentID)
|
|
||||||
if err == nil {
|
|
||||||
departments = append(departments, *parent)
|
|
||||||
|
|
||||||
// Get all descendants of parent (includes siblings)
|
|
||||||
descendants, err := p.departmentRepo.GetAllDescendants(ctx, parent.ID)
|
|
||||||
if err == nil {
|
|
||||||
departments = append(departments, descendants...)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback to just department and its descendants
|
|
||||||
return p.GetDepartmentWithDescendants(ctx, departmentID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No parent, just get department and descendants
|
|
||||||
return p.GetDepartmentWithDescendants(ctx, departmentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return departments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllDepartments gets all departments in the system
|
|
||||||
func (p *DepartmentProcessorImpl) GetAllDepartments(ctx context.Context) ([]entities.Department, error) {
|
|
||||||
departments, _, err := p.departmentRepo.List(ctx, "", 0, 0)
|
|
||||||
return departments, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDepartmentsByRootPath gets departments starting from a specific path
|
|
||||||
func (p *DepartmentProcessorImpl) GetDepartmentsByRootPath(ctx context.Context, rootPath string) ([]entities.Department, error) {
|
|
||||||
// Find the root department
|
|
||||||
rootDept, err := p.departmentRepo.GetByPath(ctx, rootPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.GetDepartmentWithDescendants(ctx, rootDept.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateChildrenPathsRecursively updates paths for all children recursively
|
|
||||||
func (p *DepartmentProcessorImpl) updateChildrenPathsRecursively(ctx context.Context, parentID uuid.UUID, parentPath string) error {
|
|
||||||
children, err := p.departmentRepo.GetChildren(ctx, parentPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, child := range children {
|
|
||||||
// Update child's path
|
|
||||||
child.Path = parentPath + "." + child.Code
|
|
||||||
if err := p.departmentRepo.Update(ctx, &child); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively update grandchildren
|
|
||||||
if err := p.updateChildrenPathsRecursively(ctx, child.ID, child.Path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterActivityProcessor interface {
|
|
||||||
LogActivity(ctx context.Context, letterID uuid.UUID, action string, userID uuid.UUID, details map[string]interface{}) error
|
|
||||||
LogStatusChange(ctx context.Context, letterID uuid.UUID, fromStatus, toStatus string, userID uuid.UUID) error
|
|
||||||
GetActivitiesByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingActivityLog, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterActivityProcessorImpl struct {
|
|
||||||
activityLogRepo *repository.LetterOutgoingActivityLogRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterActivityProcessor(activityLogRepo *repository.LetterOutgoingActivityLogRepository) *LetterActivityProcessorImpl {
|
|
||||||
return &LetterActivityProcessorImpl{
|
|
||||||
activityLogRepo: activityLogRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterActivityProcessorImpl) LogActivity(ctx context.Context, letterID uuid.UUID, action string, userID uuid.UUID, details map[string]interface{}) error {
|
|
||||||
log := &entities.LetterOutgoingActivityLog{
|
|
||||||
LetterID: letterID,
|
|
||||||
ActionType: action,
|
|
||||||
ActorUserID: &userID,
|
|
||||||
Context: details,
|
|
||||||
OccurredAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.activityLogRepo.Create(ctx, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterActivityProcessorImpl) LogStatusChange(ctx context.Context, letterID uuid.UUID, fromStatus, toStatus string, userID uuid.UUID) error {
|
|
||||||
details := map[string]interface{}{
|
|
||||||
"from_status": fromStatus,
|
|
||||||
"to_status": toStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.LogActivity(ctx, letterID, "status_changed", userID, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterActivityProcessorImpl) GetActivitiesByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingActivityLog, error) {
|
|
||||||
return p.activityLogRepo.ListByLetter(ctx, letterID)
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterApprovalProcessor interface {
|
|
||||||
CreateApprovalSteps(ctx context.Context, letter *entities.LetterOutgoing) error
|
|
||||||
ProcessApprovalAction(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, isApproval bool) error
|
|
||||||
UpdateApprovalStatus(ctx context.Context, approval *entities.LetterOutgoingApproval, status entities.ApprovalStatus, userID uuid.UUID) error
|
|
||||||
CheckAllApprovalsComplete(ctx context.Context, letterID uuid.UUID) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterApprovalProcessorImpl struct {
|
|
||||||
approvalRepo *repository.LetterOutgoingApprovalRepository
|
|
||||||
approvalFlowRepo *repository.ApprovalFlowRepository
|
|
||||||
letterRepo *repository.LetterOutgoingRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterApprovalProcessor(
|
|
||||||
approvalRepo *repository.LetterOutgoingApprovalRepository,
|
|
||||||
approvalFlowRepo *repository.ApprovalFlowRepository,
|
|
||||||
letterRepo *repository.LetterOutgoingRepository,
|
|
||||||
) *LetterApprovalProcessorImpl {
|
|
||||||
return &LetterApprovalProcessorImpl{
|
|
||||||
approvalRepo: approvalRepo,
|
|
||||||
approvalFlowRepo: approvalFlowRepo,
|
|
||||||
letterRepo: letterRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterApprovalProcessorImpl) CreateApprovalSteps(ctx context.Context, letter *entities.LetterOutgoing) error {
|
|
||||||
if letter.ApprovalFlowID == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
flow, err := p.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID)
|
|
||||||
if err != nil || flow == nil || len(flow.Steps) == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the minimum parallel group
|
|
||||||
minParallelGroup := flow.Steps[0].ParallelGroup
|
|
||||||
for _, step := range flow.Steps {
|
|
||||||
if step.ParallelGroup < minParallelGroup {
|
|
||||||
minParallelGroup = step.ParallelGroup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create approval records for each step
|
|
||||||
for _, step := range flow.Steps {
|
|
||||||
approval := entities.LetterOutgoingApproval{
|
|
||||||
LetterID: letter.ID,
|
|
||||||
StepID: step.ID,
|
|
||||||
StepOrder: step.StepOrder,
|
|
||||||
ParallelGroup: step.ParallelGroup,
|
|
||||||
IsRequired: step.Required,
|
|
||||||
ApproverID: step.ApproverUserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial status - all approvals in first parallel group are pending
|
|
||||||
if step.ParallelGroup == minParallelGroup {
|
|
||||||
approval.Status = entities.ApprovalStatusPending
|
|
||||||
} else {
|
|
||||||
approval.Status = entities.ApprovalStatusNotStarted
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.approvalRepo.Create(ctx, &approval); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterApprovalProcessorImpl) ProcessApprovalAction(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, isApproval bool) error {
|
|
||||||
status := entities.ApprovalStatusApproved
|
|
||||||
if !isApproval {
|
|
||||||
status = entities.ApprovalStatusRejected
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.UpdateApprovalStatus(ctx, approval, status, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterApprovalProcessorImpl) UpdateApprovalStatus(ctx context.Context, approval *entities.LetterOutgoingApproval, status entities.ApprovalStatus, userID uuid.UUID) error {
|
|
||||||
approval.Status = status
|
|
||||||
approval.ApproverID = &userID
|
|
||||||
now := time.Now()
|
|
||||||
approval.ActedAt = &now
|
|
||||||
|
|
||||||
return p.approvalRepo.Update(ctx, approval)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterApprovalProcessorImpl) CheckAllApprovalsComplete(ctx context.Context, letterID uuid.UUID) (bool, error) {
|
|
||||||
approvals, err := p.approvalRepo.ListByLetter(ctx, letterID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, approval := range approvals {
|
|
||||||
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterAttachmentProcessor interface {
|
|
||||||
CreateAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment) error
|
|
||||||
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
|
|
||||||
GetAttachmentsByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingAttachment, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterAttachmentProcessorImpl struct {
|
|
||||||
attachmentRepo *repository.LetterOutgoingAttachmentRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterAttachmentProcessor(attachmentRepo *repository.LetterOutgoingAttachmentRepository) *LetterAttachmentProcessorImpl {
|
|
||||||
return &LetterAttachmentProcessorImpl{
|
|
||||||
attachmentRepo: attachmentRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterAttachmentProcessorImpl) CreateAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment) error {
|
|
||||||
if len(attachments) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set letter ID for all attachments
|
|
||||||
for i := range attachments {
|
|
||||||
attachments[i].LetterID = letterID
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.attachmentRepo.CreateBulk(ctx, attachments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterAttachmentProcessorImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error {
|
|
||||||
return p.attachmentRepo.Delete(ctx, attachmentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterAttachmentProcessorImpl) GetAttachmentsByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingAttachment, error) {
|
|
||||||
attachmentMap, err := p.attachmentRepo.ListByLetterIDs(ctx, []uuid.UUID{letterID})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if attachments, ok := attachmentMap[letterID]; ok {
|
|
||||||
return attachments, nil
|
|
||||||
}
|
|
||||||
return []entities.LetterOutgoingAttachment{}, nil
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterCreationProcessor interface {
|
|
||||||
PrepareLetterForCreation(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error
|
|
||||||
CreateLetter(ctx context.Context, letter *entities.LetterOutgoing) error
|
|
||||||
GenerateLetterNumber(ctx context.Context, letter *entities.LetterOutgoing) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterCreationProcessorImpl struct {
|
|
||||||
letterRepo *repository.LetterOutgoingRepository
|
|
||||||
approvalFlowRepo *repository.ApprovalFlowRepository
|
|
||||||
numberGenerator LetterNumberGenerator
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterCreationProcessor(
|
|
||||||
letterRepo *repository.LetterOutgoingRepository,
|
|
||||||
approvalFlowRepo *repository.ApprovalFlowRepository,
|
|
||||||
numberGenerator LetterNumberGenerator,
|
|
||||||
) *LetterCreationProcessorImpl {
|
|
||||||
return &LetterCreationProcessorImpl{
|
|
||||||
letterRepo: letterRepo,
|
|
||||||
approvalFlowRepo: approvalFlowRepo,
|
|
||||||
numberGenerator: numberGenerator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterCreationProcessorImpl) PrepareLetterForCreation(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error {
|
|
||||||
// Assign approval flow from department if not provided
|
|
||||||
if letter.ApprovalFlowID == nil && departmentID != uuid.Nil {
|
|
||||||
flow, err := p.approvalFlowRepo.GetByDepartment(ctx, departmentID)
|
|
||||||
if err == nil && flow != nil {
|
|
||||||
letter.ApprovalFlowID = &flow.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial status based on approval flow
|
|
||||||
if letter.ApprovalFlowID != nil {
|
|
||||||
letter.Status = entities.LetterOutgoingStatusPendingApproval
|
|
||||||
} else {
|
|
||||||
letter.Status = entities.LetterOutgoingStatusApproved
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterCreationProcessorImpl) CreateLetter(ctx context.Context, letter *entities.LetterOutgoing) error {
|
|
||||||
return p.letterRepo.Create(ctx, letter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterCreationProcessorImpl) GenerateLetterNumber(ctx context.Context, letter *entities.LetterOutgoing) error {
|
|
||||||
letterNumber, err := p.numberGenerator.GenerateNumber(
|
|
||||||
ctx,
|
|
||||||
"outgoing_letter_prefix",
|
|
||||||
"outgoing_letter_sequence",
|
|
||||||
"ESLO",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
letter.LetterNumber = letterNumber
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
"eslogad-be/internal/transformer"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterDispositionDepartmentProcessor interface {
|
|
||||||
GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error)
|
|
||||||
GetDepartmentDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID) (*contract.ListDepartmentDispositionStatusResponse, error)
|
|
||||||
UpdateDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID, departmentID uuid.UUID, userID uuid.UUID, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error)
|
|
||||||
CheckAndUpdateLetterCompletionStatus(ctx context.Context, letterIncomingID uuid.UUID) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterDispositionDepartmentProcessorImpl struct {
|
|
||||||
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
|
||||||
letterRepo *repository.LetterIncomingRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterDispositionDepartmentProcessor(
|
|
||||||
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository,
|
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository,
|
|
||||||
letterRepo *repository.LetterIncomingRepository,
|
|
||||||
) *LetterDispositionDepartmentProcessorImpl {
|
|
||||||
return &LetterDispositionDepartmentProcessorImpl{
|
|
||||||
dispositionDeptRepo: dispositionDeptRepo,
|
|
||||||
dispositionNoteRepo: dispositionNoteRepo,
|
|
||||||
letterRepo: letterRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByLetterIncomingID retrieves all disposition departments for a letter
|
|
||||||
func (p *LetterDispositionDepartmentProcessorImpl) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
|
||||||
return p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDepartmentDispositionStatus retrieves disposition status for a specific letter
|
|
||||||
func (p *LetterDispositionDepartmentProcessorImpl) GetDepartmentDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID) (*contract.ListDepartmentDispositionStatusResponse, error) {
|
|
||||||
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
response := p.buildDispositionStatusResponse(dispositions)
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionDepartmentProcessorImpl) UpdateDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID, departmentID uuid.UUID, userID uuid.UUID, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) {
|
|
||||||
dispDept, err := p.dispositionDeptRepo.GetByDispositionAndDepartment(ctx, letterIncomingID, departmentID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
var dispositionStatus entities.LetterIncomingDispositionDepartmentStatus
|
|
||||||
var readAt, completedAt *time.Time
|
|
||||||
|
|
||||||
switch req.Status {
|
|
||||||
case "completed":
|
|
||||||
dispositionStatus = entities.DispositionDepartmentStatusCompleted
|
|
||||||
completedAt = &now
|
|
||||||
readAt = &now // Mark as read when completing
|
|
||||||
case "read":
|
|
||||||
dispositionStatus = entities.DispositionDepartmentStatusRead
|
|
||||||
readAt = &now
|
|
||||||
case "dispositioned":
|
|
||||||
dispositionStatus = entities.DispositionDepartmentStatusDispositioned
|
|
||||||
default:
|
|
||||||
dispositionStatus = entities.DispositionDepartmentStatusPending
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract notes for the update
|
|
||||||
notes := ""
|
|
||||||
if req.Notes != nil && *req.Notes != "" {
|
|
||||||
notes = *req.Notes
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.dispositionDeptRepo.UpdateStatus(ctx, dispDept.ID, dispositionStatus, notes, readAt, completedAt); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check and update letter completion status
|
|
||||||
if err := p.CheckAndUpdateLetterCompletionStatus(ctx, letterIncomingID); err != nil {
|
|
||||||
// Log error but don't fail the status update
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get updated record for response
|
|
||||||
updatedDispDept, err := p.dispositionDeptRepo.GetByID(ctx, dispDept.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.buildSingleDispositionStatusResponse(updatedDispDept), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckAndUpdateLetterCompletionStatus checks if all dispositions are completed and updates letter status
|
|
||||||
func (p *LetterDispositionDepartmentProcessorImpl) CheckAndUpdateLetterCompletionStatus(ctx context.Context, letterIncomingID uuid.UUID) error {
|
|
||||||
// Get all disposition departments for this letter
|
|
||||||
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all dispositions are completed
|
|
||||||
allCompleted := true
|
|
||||||
for _, disp := range dispositions {
|
|
||||||
if disp.Status == entities.DispositionDepartmentStatusPending {
|
|
||||||
allCompleted = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all dispositions are completed, update the letter status to completed
|
|
||||||
if allCompleted && len(dispositions) > 0 {
|
|
||||||
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
letter.Status = "completed"
|
|
||||||
if err := p.letterRepo.Update(ctx, letter); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
|
|
||||||
func (p *LetterDispositionDepartmentProcessorImpl) buildDispositionStatusResponse(dispositions []entities.LetterIncomingDispositionDepartment) *contract.ListDepartmentDispositionStatusResponse {
|
|
||||||
var response []contract.DepartmentDispositionStatusResponse
|
|
||||||
|
|
||||||
for _, disp := range dispositions {
|
|
||||||
response = append(response, *p.buildSingleDispositionStatusResponse(&disp))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.ListDepartmentDispositionStatusResponse{
|
|
||||||
Dispositions: response,
|
|
||||||
Pagination: contract.PaginationResponse{
|
|
||||||
TotalCount: len(response),
|
|
||||||
Page: 1,
|
|
||||||
Limit: len(response),
|
|
||||||
TotalPages: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionDepartmentProcessorImpl) buildSingleDispositionStatusResponse(dispDept *entities.LetterIncomingDispositionDepartment) *contract.DepartmentDispositionStatusResponse {
|
|
||||||
letterResp := transformer.LetterIncomingEntityToContract(dispDept.LetterIncoming)
|
|
||||||
|
|
||||||
var fromDept *contract.DepartmentResponse
|
|
||||||
if dispDept.LetterIncomingDisposition != nil && dispDept.LetterIncomingDisposition.DepartmentID != nil {
|
|
||||||
fromDept = transformer.DepartmentEntityToContract(&dispDept.LetterIncomingDisposition.Department)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.DepartmentDispositionStatusResponse{
|
|
||||||
ID: dispDept.ID,
|
|
||||||
LetterID: dispDept.LetterIncomingID,
|
|
||||||
Letter: letterResp,
|
|
||||||
FromDepartmentID: dispDept.LetterIncomingDisposition.DepartmentID,
|
|
||||||
FromDepartment: fromDept,
|
|
||||||
ToDepartmentID: dispDept.DepartmentID,
|
|
||||||
ToDepartment: transformer.DepartmentEntityToContract(dispDept.Department),
|
|
||||||
Status: string(dispDept.Status),
|
|
||||||
Notes: dispDept.LetterIncomingDisposition.Notes,
|
|
||||||
ReadAt: dispDept.ReadAt,
|
|
||||||
CompletedAt: dispDept.CompletedAt,
|
|
||||||
CreatedAt: dispDept.CreatedAt,
|
|
||||||
UpdatedAt: dispDept.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
"eslogad-be/internal/transformer"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterDispositionProcessor interface {
|
|
||||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
|
||||||
GetByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error)
|
|
||||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterDispositionProcessorImpl struct {
|
|
||||||
dispositionRepo *repository.LetterIncomingDispositionRepository
|
|
||||||
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
|
||||||
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
|
||||||
discussionRepo *repository.LetterDiscussionRepository
|
|
||||||
dispActionRepo *repository.DispositionActionRepository
|
|
||||||
activity *ActivityLogProcessorImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterDispositionProcessor(
|
|
||||||
dispositionRepo *repository.LetterIncomingDispositionRepository,
|
|
||||||
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository,
|
|
||||||
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository,
|
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository,
|
|
||||||
discussionRepo *repository.LetterDiscussionRepository,
|
|
||||||
dispActionRepo *repository.DispositionActionRepository,
|
|
||||||
activity *ActivityLogProcessorImpl,
|
|
||||||
) *LetterDispositionProcessorImpl {
|
|
||||||
return &LetterDispositionProcessorImpl{
|
|
||||||
dispositionRepo: dispositionRepo,
|
|
||||||
dispositionDeptRepo: dispositionDeptRepo,
|
|
||||||
dispositionActionSelRepo: dispositionActionSelRepo,
|
|
||||||
dispositionNoteRepo: dispositionNoteRepo,
|
|
||||||
discussionRepo: discussionRepo,
|
|
||||||
dispActionRepo: dispActionRepo,
|
|
||||||
activity: activity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
|
||||||
disposition := &entities.LetterIncomingDisposition{
|
|
||||||
LetterID: req.LetterID,
|
|
||||||
DepartmentID: &req.FromDepartment,
|
|
||||||
Notes: req.Notes,
|
|
||||||
CreatedBy: req.CreatedBy,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.dispositionRepo.Create(ctx, disposition); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.createDispositionDepartments(ctx, disposition.ID, req.LetterID, req.ToDepartmentIDs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.SelectedActions) > 0 {
|
|
||||||
if err := p.createActionSelectionsFromRequest(ctx, disposition.ID, req.SelectedActions); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.activity != nil {
|
|
||||||
p.activity.LogDispositionCreated(ctx, req.LetterID, req.CreatedBy, len(req.ToDepartmentIDs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build response
|
|
||||||
dispositions := []entities.LetterIncomingDisposition{*disposition}
|
|
||||||
response := p.buildDispositionsResponse(dispositions)
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionProcessorImpl) GetByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
|
|
||||||
return p.dispositionRepo.ListByLetter(ctx, letterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
|
||||||
// Get dispositions
|
|
||||||
dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get discussions
|
|
||||||
discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build enhanced response
|
|
||||||
enhancedDispositions := make([]contract.EnhancedDispositionResponse, 0, len(dispositions))
|
|
||||||
for _, disp := range dispositions {
|
|
||||||
// Build disposition response using existing structure
|
|
||||||
var dept contract.DepartmentResponse
|
|
||||||
if disp.Department.ID != uuid.Nil {
|
|
||||||
dept = *transformer.DepartmentEntityToContract(&disp.Department)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build departments
|
|
||||||
departments := make([]contract.DispositionDepartmentResponse, 0, len(disp.Departments))
|
|
||||||
for _, d := range disp.Departments {
|
|
||||||
departments = append(departments, contract.DispositionDepartmentResponse{
|
|
||||||
ID: d.ID,
|
|
||||||
DepartmentID: d.DepartmentID,
|
|
||||||
Department: transformer.DepartmentEntityToContract(d.Department),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build actions
|
|
||||||
actions := make([]contract.DispositionActionSelectionResponse, 0, len(disp.ActionSelections))
|
|
||||||
for _, a := range disp.ActionSelections {
|
|
||||||
if a.Action != nil {
|
|
||||||
actions = append(actions, contract.DispositionActionSelectionResponse{
|
|
||||||
ID: a.ID,
|
|
||||||
ActionID: a.ActionID,
|
|
||||||
Action: &contract.DispositionActionResponse{
|
|
||||||
ID: a.Action.ID.String(),
|
|
||||||
Code: a.Action.Code,
|
|
||||||
Label: a.Action.Label,
|
|
||||||
Description: a.Action.Description,
|
|
||||||
RequiresNote: a.Action.RequiresNote,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build notes
|
|
||||||
notes := make([]contract.DispositionNoteResponse, 0, len(disp.DispositionNotes))
|
|
||||||
for _, n := range disp.DispositionNotes {
|
|
||||||
var userResp *contract.UserResponse
|
|
||||||
if n.User != nil {
|
|
||||||
userResp = transformer.EntityToContract(n.User)
|
|
||||||
}
|
|
||||||
notes = append(notes, contract.DispositionNoteResponse{
|
|
||||||
ID: n.ID,
|
|
||||||
Note: n.Note,
|
|
||||||
CreatedAt: n.CreatedAt,
|
|
||||||
User: userResp,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
enhancedDispositions = append(enhancedDispositions, contract.EnhancedDispositionResponse{
|
|
||||||
ID: disp.ID,
|
|
||||||
LetterID: disp.LetterID,
|
|
||||||
DepartmentID: disp.DepartmentID,
|
|
||||||
Notes: disp.Notes,
|
|
||||||
ReadAt: disp.ReadAt,
|
|
||||||
CreatedBy: disp.CreatedBy,
|
|
||||||
CreatedAt: disp.CreatedAt,
|
|
||||||
UpdatedAt: disp.UpdatedAt,
|
|
||||||
Department: dept,
|
|
||||||
Departments: departments,
|
|
||||||
Actions: actions,
|
|
||||||
DispositionNotes: notes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get general discussions
|
|
||||||
var generalDiscussions []contract.LetterDiscussionResponse
|
|
||||||
for _, disc := range discussions {
|
|
||||||
generalDiscussions = append(generalDiscussions, *transformer.DiscussionEntityToContract(&disc))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.ListEnhancedDispositionsResponse{
|
|
||||||
Dispositions: enhancedDispositions,
|
|
||||||
Discussions: generalDiscussions,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
|
|
||||||
func (p *LetterDispositionProcessorImpl) createDispositionDepartments(ctx context.Context, dispositionID, letterID uuid.UUID, departmentIDs []uuid.UUID) error {
|
|
||||||
departments := make([]entities.LetterIncomingDispositionDepartment, 0, len(departmentIDs))
|
|
||||||
|
|
||||||
for _, deptID := range departmentIDs {
|
|
||||||
departments = append(departments, entities.LetterIncomingDispositionDepartment{
|
|
||||||
LetterIncomingDispositionID: dispositionID,
|
|
||||||
LetterIncomingID: letterID,
|
|
||||||
DepartmentID: deptID,
|
|
||||||
Status: entities.DispositionDepartmentStatusPending,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(departments) > 0 {
|
|
||||||
return p.dispositionDeptRepo.CreateBulk(ctx, departments)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionProcessorImpl) createActionSelectionsFromRequest(ctx context.Context, dispositionID uuid.UUID, selectedActions []contract.CreateDispositionActionSelection) error {
|
|
||||||
selections := make([]entities.LetterDispositionActionSelection, 0, len(selectedActions))
|
|
||||||
|
|
||||||
for _, action := range selectedActions {
|
|
||||||
selections = append(selections, entities.LetterDispositionActionSelection{
|
|
||||||
DispositionID: dispositionID,
|
|
||||||
ActionID: action.ActionID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(selections) > 0 {
|
|
||||||
return p.dispositionActionSelRepo.CreateBulk(ctx, selections)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionProcessorImpl) buildDispositionsResponse(dispositions []entities.LetterIncomingDisposition) *contract.ListDispositionsResponse {
|
|
||||||
dispositionResponses := make([]contract.DispositionResponse, 0, len(dispositions))
|
|
||||||
|
|
||||||
for _, disp := range dispositions {
|
|
||||||
dispositionResponses = append(dispositionResponses, *p.buildDispositionResponse(&disp))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.ListDispositionsResponse{
|
|
||||||
Dispositions: dispositionResponses,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterDispositionProcessorImpl) buildDispositionResponse(disp *entities.LetterIncomingDisposition) *contract.DispositionResponse {
|
|
||||||
return &contract.DispositionResponse{
|
|
||||||
ID: disp.ID,
|
|
||||||
LetterID: disp.LetterID,
|
|
||||||
DepartmentID: disp.DepartmentID,
|
|
||||||
Notes: disp.Notes,
|
|
||||||
ReadAt: disp.ReadAt,
|
|
||||||
CreatedBy: disp.CreatedBy,
|
|
||||||
CreatedAt: disp.CreatedAt,
|
|
||||||
UpdatedAt: disp.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LetterNumberGenerator handles generation of letter numbers with configurable prefix and sequence
|
|
||||||
type LetterNumberGenerator interface {
|
|
||||||
GenerateNumber(ctx context.Context, prefixKey, sequenceKey string, defaultPrefix string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterNumberGeneratorImpl struct {
|
|
||||||
settingRepo *repository.AppSettingRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterNumberGenerator(settingRepo *repository.AppSettingRepository) *LetterNumberGeneratorImpl {
|
|
||||||
return &LetterNumberGeneratorImpl{
|
|
||||||
settingRepo: settingRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *LetterNumberGeneratorImpl) GenerateNumber(ctx context.Context, prefixKey, sequenceKey string, defaultPrefix string) (string, error) {
|
|
||||||
prefix := defaultPrefix
|
|
||||||
if s, err := g.settingRepo.Get(ctx, prefixKey); err == nil {
|
|
||||||
if v, ok := s.Value["value"].(string); ok && v != "" {
|
|
||||||
prefix = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seq := 0
|
|
||||||
if s, err := g.settingRepo.Get(ctx, sequenceKey); err == nil {
|
|
||||||
if v, ok := s.Value["value"].(float64); ok {
|
|
||||||
seq = int(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seq = seq + 1
|
|
||||||
|
|
||||||
letterNumber := fmt.Sprintf("%s%04d", prefix, seq)
|
|
||||||
|
|
||||||
if err := g.settingRepo.Upsert(ctx, sequenceKey, entities.JSONB{"value": seq}); err != nil {
|
|
||||||
}
|
|
||||||
|
|
||||||
return letterNumber, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentSequence returns the current sequence number for a given key
|
|
||||||
func (g *LetterNumberGeneratorImpl) GetCurrentSequence(ctx context.Context, sequenceKey string) (int, error) {
|
|
||||||
seq := 0
|
|
||||||
if s, err := g.settingRepo.Get(ctx, sequenceKey); err == nil {
|
|
||||||
if v, ok := s.Value["value"].(float64); ok {
|
|
||||||
seq = int(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return seq, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrefix returns the configured prefix for a given key
|
|
||||||
func (g *LetterNumberGeneratorImpl) GetPrefix(ctx context.Context, prefixKey string, defaultPrefix string) string {
|
|
||||||
prefix := defaultPrefix
|
|
||||||
if s, err := g.settingRepo.Get(ctx, prefixKey); err == nil {
|
|
||||||
if v, ok := s.Value["value"].(string); ok && v != "" {
|
|
||||||
prefix = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prefix
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,221 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LetterOutgoingRecipientProcessor handles all recipient-related operations for outgoing letters
|
|
||||||
type LetterOutgoingRecipientProcessor interface {
|
|
||||||
// CreateRecipients creates multiple recipients for a letter
|
|
||||||
CreateRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient) error
|
|
||||||
|
|
||||||
// CreateInitialRecipients creates recipients from both approval workflow and department members
|
|
||||||
CreateInitialRecipients(ctx context.Context, letter *entities.LetterOutgoing, creatorDepartmentID uuid.UUID) error
|
|
||||||
|
|
||||||
// UpdateRecipient updates a single recipient's information
|
|
||||||
UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error
|
|
||||||
|
|
||||||
// RemoveRecipient removes a recipient from a letter
|
|
||||||
RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error
|
|
||||||
|
|
||||||
// GetRecipientsByLetterID retrieves all recipients for a specific letter
|
|
||||||
GetRecipientsByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingRecipient, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LetterOutgoingRecipientProcessorImpl struct {
|
|
||||||
recipientRepo *repository.LetterOutgoingRecipientRepository
|
|
||||||
approvalFlowRepo *repository.ApprovalFlowRepository
|
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterOutgoingRecipientProcessor(
|
|
||||||
recipientRepo *repository.LetterOutgoingRecipientRepository,
|
|
||||||
approvalFlowRepo *repository.ApprovalFlowRepository,
|
|
||||||
userDeptRepo *repository.UserDepartmentRepository,
|
|
||||||
) *LetterOutgoingRecipientProcessorImpl {
|
|
||||||
return &LetterOutgoingRecipientProcessorImpl{
|
|
||||||
recipientRepo: recipientRepo,
|
|
||||||
approvalFlowRepo: approvalFlowRepo,
|
|
||||||
userDeptRepo: userDeptRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRecipients creates multiple recipients for a letter
|
|
||||||
func (p *LetterOutgoingRecipientProcessorImpl) CreateRecipients(
|
|
||||||
ctx context.Context,
|
|
||||||
letterID uuid.UUID,
|
|
||||||
recipients []entities.LetterOutgoingRecipient,
|
|
||||||
) error {
|
|
||||||
if len(recipients) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure all recipients have the correct letter ID and default status
|
|
||||||
for i := range recipients {
|
|
||||||
recipients[i].LetterID = letterID
|
|
||||||
if recipients[i].Status == "" {
|
|
||||||
recipients[i].Status = "pending"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.recipientRepo.CreateBulk(ctx, recipients)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateInitialRecipients creates the initial set of recipients for an outgoing letter
|
|
||||||
// It combines:
|
|
||||||
// 1. Approvers from the approval workflow (if exists)
|
|
||||||
// 2. All active users from the letter creator's department
|
|
||||||
func (p *LetterOutgoingRecipientProcessorImpl) CreateInitialRecipients(
|
|
||||||
ctx context.Context,
|
|
||||||
letter *entities.LetterOutgoing,
|
|
||||||
creatorDepartmentID uuid.UUID,
|
|
||||||
) error {
|
|
||||||
// Track unique users to avoid duplicates
|
|
||||||
uniqueUsers := make(map[uuid.UUID]bool)
|
|
||||||
var allRecipients []entities.LetterOutgoingRecipient
|
|
||||||
|
|
||||||
// Step 1: Add recipients from approval workflow
|
|
||||||
approvalRecipients := p.collectApprovalWorkflowRecipients(ctx, letter, uniqueUsers)
|
|
||||||
allRecipients = append(allRecipients, approvalRecipients...)
|
|
||||||
|
|
||||||
// Step 2: Add all users from the creator's department
|
|
||||||
departmentRecipients := p.collectDepartmentRecipients(ctx, letter.ID, creatorDepartmentID, uniqueUsers)
|
|
||||||
allRecipients = append(allRecipients, departmentRecipients...)
|
|
||||||
|
|
||||||
// Step 3: Mark the first recipient as primary and save all
|
|
||||||
if len(allRecipients) > 0 {
|
|
||||||
allRecipients[0].IsPrimary = true
|
|
||||||
return p.recipientRepo.CreateBulk(ctx, allRecipients)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectApprovalWorkflowRecipients gathers all users who are approvers in the workflow
|
|
||||||
func (p *LetterOutgoingRecipientProcessorImpl) collectApprovalWorkflowRecipients(
|
|
||||||
ctx context.Context,
|
|
||||||
letter *entities.LetterOutgoing,
|
|
||||||
existingUsers map[uuid.UUID]bool,
|
|
||||||
) []entities.LetterOutgoingRecipient {
|
|
||||||
var recipients []entities.LetterOutgoingRecipient
|
|
||||||
|
|
||||||
// If no approval workflow is assigned, return empty
|
|
||||||
if letter.ApprovalFlowID == nil {
|
|
||||||
return recipients
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the approval workflow
|
|
||||||
approvalFlow, err := p.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID)
|
|
||||||
if err != nil || approvalFlow == nil || len(approvalFlow.Steps) == 0 {
|
|
||||||
return recipients
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add each approver as a recipient
|
|
||||||
for _, step := range approvalFlow.Steps {
|
|
||||||
if step.ApproverUserID == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := *step.ApproverUserID
|
|
||||||
|
|
||||||
// Skip if user is already added
|
|
||||||
if existingUsers[userID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
existingUsers[userID] = true
|
|
||||||
|
|
||||||
recipient := entities.LetterOutgoingRecipient{
|
|
||||||
LetterID: letter.ID,
|
|
||||||
UserID: &userID,
|
|
||||||
IsPrimary: false,
|
|
||||||
Status: "pending",
|
|
||||||
}
|
|
||||||
|
|
||||||
recipients = append(recipients, recipient)
|
|
||||||
}
|
|
||||||
|
|
||||||
return recipients
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectDepartmentRecipients gathers all active users from a specific department
|
|
||||||
func (p *LetterOutgoingRecipientProcessorImpl) collectDepartmentRecipients(
|
|
||||||
ctx context.Context,
|
|
||||||
letterID uuid.UUID,
|
|
||||||
departmentID uuid.UUID,
|
|
||||||
existingUsers map[uuid.UUID]bool,
|
|
||||||
) []entities.LetterOutgoingRecipient {
|
|
||||||
var recipients []entities.LetterOutgoingRecipient
|
|
||||||
|
|
||||||
// If no department specified, return empty
|
|
||||||
if departmentID == uuid.Nil {
|
|
||||||
return recipients
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all active users in the department
|
|
||||||
userDepartmentMappings, err := p.userDeptRepo.ListActiveByDepartmentIDs(ctx, []uuid.UUID{departmentID})
|
|
||||||
if err != nil {
|
|
||||||
return recipients
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add each department user as a recipient
|
|
||||||
for _, mapping := range userDepartmentMappings {
|
|
||||||
// Skip if user is already added (e.g., from approval workflow)
|
|
||||||
if existingUsers[mapping.UserID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
existingUsers[mapping.UserID] = true
|
|
||||||
|
|
||||||
recipient := entities.LetterOutgoingRecipient{
|
|
||||||
LetterID: letterID,
|
|
||||||
UserID: &mapping.UserID,
|
|
||||||
DepartmentID: &mapping.DepartmentID,
|
|
||||||
IsPrimary: false,
|
|
||||||
Status: "pending",
|
|
||||||
}
|
|
||||||
|
|
||||||
recipients = append(recipients, recipient)
|
|
||||||
}
|
|
||||||
|
|
||||||
return recipients
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRecipient updates an existing recipient's information
|
|
||||||
func (p *LetterOutgoingRecipientProcessorImpl) UpdateRecipient(
|
|
||||||
ctx context.Context,
|
|
||||||
recipient *entities.LetterOutgoingRecipient,
|
|
||||||
) error {
|
|
||||||
return p.recipientRepo.Update(ctx, recipient)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveRecipient removes a recipient from a letter
|
|
||||||
func (p *LetterOutgoingRecipientProcessorImpl) RemoveRecipient(
|
|
||||||
ctx context.Context,
|
|
||||||
letterID uuid.UUID,
|
|
||||||
recipientID uuid.UUID,
|
|
||||||
) error {
|
|
||||||
return p.recipientRepo.Delete(ctx, recipientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRecipientsByLetterID retrieves all recipients for a specific letter
|
|
||||||
func (p *LetterOutgoingRecipientProcessorImpl) GetRecipientsByLetterID(
|
|
||||||
ctx context.Context,
|
|
||||||
letterID uuid.UUID,
|
|
||||||
) ([]entities.LetterOutgoingRecipient, error) {
|
|
||||||
recipientMap, err := p.recipientRepo.ListByLetterIDs(ctx, []uuid.UUID{letterID})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if recipients, ok := recipientMap[letterID]; ok {
|
|
||||||
return recipients, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return []entities.LetterOutgoingRecipient{}, nil
|
|
||||||
}
|
|
||||||
@ -1,685 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"eslogad-be/internal/appcontext"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/repository"
|
|
||||||
"eslogad-be/internal/transformer"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterProcessorImpl struct {
|
|
||||||
letterRepo *repository.LetterIncomingRepository
|
|
||||||
attachRepo *repository.LetterIncomingAttachmentRepository
|
|
||||||
txManager *repository.TxManager
|
|
||||||
activity *ActivityLogProcessorImpl
|
|
||||||
dispositionRepo *repository.LetterIncomingDispositionRepository
|
|
||||||
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
|
||||||
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
|
||||||
discussionRepo *repository.LetterDiscussionRepository
|
|
||||||
settingRepo *repository.AppSettingRepository
|
|
||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
|
||||||
outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository
|
|
||||||
departmentRepo *repository.DepartmentRepository
|
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
|
||||||
priorityRepo *repository.PriorityRepository
|
|
||||||
institutionRepo *repository.InstitutionRepository
|
|
||||||
dispActionRepo *repository.DispositionActionRepository
|
|
||||||
dispoRoutes *repository.DispositionRouteRepository
|
|
||||||
numberGenerator *LetterNumberGeneratorImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository,
|
|
||||||
settingRepo *repository.AppSettingRepository,
|
|
||||||
recipientRepo *repository.LetterIncomingRecipientRepository,
|
|
||||||
outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository,
|
|
||||||
departmentRepo *repository.DepartmentRepository,
|
|
||||||
userDeptRepo *repository.UserDepartmentRepository,
|
|
||||||
priorityRepo *repository.PriorityRepository,
|
|
||||||
institutionRepo *repository.InstitutionRepository,
|
|
||||||
dispActionRepo *repository.DispositionActionRepository,
|
|
||||||
numberGenerator *LetterNumberGeneratorImpl,
|
|
||||||
dispoRoutes *repository.DispositionRouteRepository) *LetterProcessorImpl {
|
|
||||||
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager,
|
|
||||||
activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo,
|
|
||||||
dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo,
|
|
||||||
discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo,
|
|
||||||
outgoingRecipientRepo: outgoingRecipientRepo,
|
|
||||||
departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo,
|
|
||||||
institutionRepo: institutionRepo, dispActionRepo: dispActionRepo, numberGenerator: numberGenerator,
|
|
||||||
dispoRoutes: dispoRoutes}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
|
||||||
userID := appcontext.FromGinContext(ctx).UserID
|
|
||||||
|
|
||||||
existingIncoming, err := p.letterRepo.GetByReferenceNumber(ctx, req.ReferenceNumber)
|
|
||||||
if err == nil && existingIncoming != nil {
|
|
||||||
return nil, fmt.Errorf("surat dengan nomor %s sudah ada", *req.ReferenceNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
letterType := entities.LetterIncomingTypeUtama
|
|
||||||
if req.Type == "TEMBUSAN" {
|
|
||||||
letterType = entities.LetterIncomingTypeTembusan
|
|
||||||
}
|
|
||||||
|
|
||||||
entity := &entities.LetterIncoming{
|
|
||||||
LetterNumber: req.LetterNumber,
|
|
||||||
ReferenceNumber: req.ReferenceNumber,
|
|
||||||
Subject: req.Subject,
|
|
||||||
Description: req.Description,
|
|
||||||
PriorityID: req.PriorityID,
|
|
||||||
SenderInstitutionID: req.SenderInstitutionID,
|
|
||||||
SenderName: req.SenderName,
|
|
||||||
Addressee: req.Addressee,
|
|
||||||
ReceivedDate: req.ReceivedDate,
|
|
||||||
DueDate: req.DueDate,
|
|
||||||
Type: letterType,
|
|
||||||
Status: entities.LetterIncomingStatusNew,
|
|
||||||
CreatedBy: userID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.letterRepo.Create(ctx, entity); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.createAttachments(ctx, entity.ID, req.Attachments, userID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.buildLetterResponse(ctx, entity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
|
|
||||||
// Get current user ID from context
|
|
||||||
userID := appcontext.FromGinContext(ctx).UserID
|
|
||||||
|
|
||||||
entity, err := p.letterRepo.Get(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
atts, _ := p.attachRepo.ListByLetter(ctx, id)
|
|
||||||
dispo, _ := p.dispositionRepo.ListByLetter(ctx, id)
|
|
||||||
var pr *entities.Priority
|
|
||||||
if entity.PriorityID != nil && p.priorityRepo != nil {
|
|
||||||
if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
|
|
||||||
pr = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var inst *entities.Institution
|
|
||||||
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
|
|
||||||
if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
|
|
||||||
inst = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if letter is read by current user
|
|
||||||
isRead := false
|
|
||||||
if p.recipientRepo != nil {
|
|
||||||
if recipient, err := p.recipientRepo.GetByLetterAndUser(ctx, id, userID); err == nil {
|
|
||||||
isRead = recipient.ReadAt != nil
|
|
||||||
fmt.Printf("Recipient: %+v\n", recipient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := transformer.LetterEntityToContract(entity, atts, dispo, pr, inst)
|
|
||||||
resp.IsRead = isRead
|
|
||||||
|
|
||||||
// Include created_by if the current user is the creator
|
|
||||||
if entity.CreatedBy == userID {
|
|
||||||
resp.CreatedBy = entity.CreatedBy
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) {
|
|
||||||
userID := appcontext.FromGinContext(ctx).UserID
|
|
||||||
|
|
||||||
incomingUnread := 0
|
|
||||||
if p.recipientRepo != nil {
|
|
||||||
if count, err := p.recipientRepo.CountUnreadByUser(ctx, userID); err == nil {
|
|
||||||
incomingUnread = count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outgoingUnread := 0
|
|
||||||
if p.outgoingRecipientRepo != nil {
|
|
||||||
if count, err := p.outgoingRecipientRepo.CountUnreadByUser(ctx, userID); err == nil {
|
|
||||||
outgoingUnread = count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response := &contract.LetterUnreadCountResponse{}
|
|
||||||
response.IncomingLetter.Unread = incomingUnread
|
|
||||||
response.OutgoingLetter.Unread = outgoingUnread
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
|
|
||||||
// Get current user ID from context
|
|
||||||
userID := appcontext.FromGinContext(ctx).UserID
|
|
||||||
|
|
||||||
// Mark the letter as read for the current user
|
|
||||||
if p.recipientRepo != nil {
|
|
||||||
if err := p.recipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
|
|
||||||
return &contract.MarkLetterReadResponse{
|
|
||||||
Success: false,
|
|
||||||
Message: "Failed to mark letter as read",
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.MarkLetterReadResponse{
|
|
||||||
Success: true,
|
|
||||||
Message: "Letter marked as read successfully",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
|
|
||||||
// Get current user ID from context
|
|
||||||
userID := appcontext.FromGinContext(ctx).UserID
|
|
||||||
|
|
||||||
// Mark the letter as read for the current user
|
|
||||||
if p.outgoingRecipientRepo != nil {
|
|
||||||
if err := p.outgoingRecipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
|
|
||||||
return &contract.MarkLetterReadResponse{
|
|
||||||
Success: false,
|
|
||||||
Message: "Failed to mark letter as read",
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.MarkLetterReadResponse{
|
|
||||||
Success: true,
|
|
||||||
Message: "Letter marked as read successfully",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, error) {
|
|
||||||
// Just fetch the raw data
|
|
||||||
appCtx := appcontext.FromGinContext(ctx)
|
|
||||||
|
|
||||||
fmt.Printf("Checked User Role: %s\n", appCtx.UserRole)
|
|
||||||
fmt.Printf("Checked User ID: %s\n", appCtx.UserID)
|
|
||||||
|
|
||||||
if appCtx.IsSuperAdmin() {
|
|
||||||
fmt.Println("Checked Role: super admin")
|
|
||||||
return p.letterRepo.ListAll(ctx, filter, page, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.letterRepo.List(ctx, filter, limit, (page-1)*limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) {
|
|
||||||
if p.attachRepo == nil || len(letterIDs) == 0 {
|
|
||||||
return make(map[uuid.UUID][]entities.LetterIncomingAttachment), nil
|
|
||||||
}
|
|
||||||
return p.attachRepo.ListByLetterIDs(ctx, letterIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetBatchDispositions(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingDisposition, error) {
|
|
||||||
if p.dispositionRepo == nil || len(letterIDs) == 0 {
|
|
||||||
return make(map[uuid.UUID][]entities.LetterIncomingDisposition), nil
|
|
||||||
}
|
|
||||||
return p.dispositionRepo.ListByLetterIDs(ctx, letterIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) {
|
|
||||||
if p.priorityRepo == nil || len(priorityIDs) == 0 {
|
|
||||||
return make(map[uuid.UUID]*entities.Priority), nil
|
|
||||||
}
|
|
||||||
return p.priorityRepo.GetByIDs(ctx, priorityIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) {
|
|
||||||
if p.institutionRepo == nil || len(institutionIDs) == 0 {
|
|
||||||
return make(map[uuid.UUID]*entities.Institution), nil
|
|
||||||
}
|
|
||||||
return p.institutionRepo.GetByIDs(ctx, institutionIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) {
|
|
||||||
if p.recipientRepo == nil || len(letterIDs) == 0 {
|
|
||||||
return make(map[uuid.UUID]*entities.LetterIncomingRecipient), nil
|
|
||||||
}
|
|
||||||
return p.recipientRepo.GetByLetterIDsAndUser(ctx, letterIDs, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
|
|
||||||
if p.recipientRepo == nil {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
return p.recipientRepo.CountUnreadByUser(ctx, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
|
||||||
var out *contract.IncomingLetterResponse
|
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
||||||
entity, err := p.letterRepo.Get(txCtx, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fromStatus := string(entity.Status)
|
|
||||||
if req.ReferenceNumber != nil {
|
|
||||||
entity.ReferenceNumber = req.ReferenceNumber
|
|
||||||
}
|
|
||||||
if req.Subject != nil {
|
|
||||||
entity.Subject = *req.Subject
|
|
||||||
}
|
|
||||||
if req.Description != nil {
|
|
||||||
entity.Description = req.Description
|
|
||||||
}
|
|
||||||
if req.PriorityID != nil {
|
|
||||||
entity.PriorityID = req.PriorityID
|
|
||||||
}
|
|
||||||
if req.SenderInstitutionID != nil {
|
|
||||||
entity.SenderInstitutionID = req.SenderInstitutionID
|
|
||||||
}
|
|
||||||
if req.SenderName != nil {
|
|
||||||
entity.SenderName = req.SenderName
|
|
||||||
}
|
|
||||||
if req.Addressee != nil {
|
|
||||||
entity.Addressee = req.Addressee
|
|
||||||
}
|
|
||||||
if req.ReceivedDate != nil {
|
|
||||||
entity.ReceivedDate = *req.ReceivedDate
|
|
||||||
}
|
|
||||||
if req.DueDate != nil {
|
|
||||||
entity.DueDate = req.DueDate
|
|
||||||
}
|
|
||||||
if req.Type != nil {
|
|
||||||
entity.Type = entities.LetterIncomingType(*req.Type)
|
|
||||||
}
|
|
||||||
if req.Status != nil {
|
|
||||||
entity.Status = entities.LetterIncomingStatus(*req.Status)
|
|
||||||
}
|
|
||||||
if err := p.letterRepo.Update(txCtx, entity); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
toStatus := string(entity.Status)
|
|
||||||
if p.activity != nil && fromStatus != toStatus {
|
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
|
||||||
action := "status.changed"
|
|
||||||
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, &fromStatus, &toStatus, map[string]interface{}{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
atts, _ := p.attachRepo.ListByLetter(txCtx, id)
|
|
||||||
dispo, _ := p.dispositionRepo.ListByLetter(txCtx, id)
|
|
||||||
var pr *entities.Priority
|
|
||||||
if entity.PriorityID != nil && p.priorityRepo != nil {
|
|
||||||
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
|
|
||||||
pr = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var inst *entities.Institution
|
|
||||||
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
|
|
||||||
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
|
|
||||||
inst = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out = transformer.LetterEntityToContract(entity, atts, dispo, pr, inst)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error {
|
|
||||||
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
||||||
if err := p.letterRepo.SoftDelete(txCtx, id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if p.activity != nil {
|
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
|
||||||
action := "letter.deleted"
|
|
||||||
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) BulkSoftDeleteIncomingLetters(ctx context.Context, ids []uuid.UUID) error {
|
|
||||||
if len(ids) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
||||||
if err := p.letterRepo.BulkSoftDelete(txCtx, ids); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.activity != nil {
|
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
|
||||||
action := "letter.bulk_deleted"
|
|
||||||
|
|
||||||
// Log activity untuk setiap letter yang dihapus
|
|
||||||
for _, id := range ids {
|
|
||||||
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDispositions creates a new disposition with modular helper functions
|
|
||||||
func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
|
||||||
// Transaction should be handled at service layer
|
|
||||||
// The context passed here should already contain the transaction if needed
|
|
||||||
|
|
||||||
// Step 1: Update existing disposition departments
|
|
||||||
if err := p.updateExistingDispositionDepartments(ctx, req.LetterID, req.FromDepartment); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Create the main disposition
|
|
||||||
disp, err := p.createMainDisposition(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Create disposition departments for target departments
|
|
||||||
dispDepartments, err := p.createDispositionDepartments(ctx, disp.ID, req.LetterID, req.ToDepartmentIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Create action selections if provided
|
|
||||||
if err := p.createActionSelections(ctx, disp.ID, req.SelectedActions, req.CreatedBy); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Build and return the response
|
|
||||||
return p.buildDispositionResponse(disp, dispDepartments, req.ToDepartmentIDs), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateExistingDispositionDepartments updates the status of existing disposition departments
|
|
||||||
func (p *LetterProcessorImpl) updateExistingDispositionDepartments(ctx context.Context, letterID uuid.UUID, fromDepartment uuid.UUID) error {
|
|
||||||
existingDispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(ctx, letterID, fromDepartment)
|
|
||||||
if err != nil {
|
|
||||||
// If no existing departments found, that's ok
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, existingDispDept := range existingDispDepts {
|
|
||||||
if existingDispDept.Status == entities.DispositionDepartmentStatusPending {
|
|
||||||
existingDispDept.Status = entities.DispositionDepartmentStatusDispositioned
|
|
||||||
if err := p.dispositionDeptRepo.Update(ctx, &existingDispDept); err != nil {
|
|
||||||
return fmt.Errorf("failed to update existing disposition department: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createMainDisposition creates the primary disposition record
|
|
||||||
func (p *LetterProcessorImpl) createMainDisposition(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*entities.LetterIncomingDisposition, error) {
|
|
||||||
disp := &entities.LetterIncomingDisposition{
|
|
||||||
LetterID: req.LetterID,
|
|
||||||
DepartmentID: &req.FromDepartment,
|
|
||||||
Notes: req.Notes,
|
|
||||||
CreatedBy: req.CreatedBy, // Should be set by service layer
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.dispositionRepo.Create(ctx, disp); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create disposition: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return disp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createDispositionDepartments creates disposition department records for target departments
|
|
||||||
func (p *LetterProcessorImpl) createDispositionDepartments(ctx context.Context, dispositionID, letterID uuid.UUID, toDepartmentIDs []uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
|
||||||
if len(toDepartmentIDs) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dispDepartments := make([]entities.LetterIncomingDispositionDepartment, 0, len(toDepartmentIDs))
|
|
||||||
for _, toDept := range toDepartmentIDs {
|
|
||||||
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
|
||||||
LetterIncomingDispositionID: dispositionID,
|
|
||||||
LetterIncomingID: letterID,
|
|
||||||
DepartmentID: toDept,
|
|
||||||
Status: entities.DispositionDepartmentStatusPending,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.dispositionDeptRepo.CreateBulk(ctx, dispDepartments); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create disposition departments: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispDepartments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createActionSelections creates action selection records for the disposition
|
|
||||||
func (p *LetterProcessorImpl) createActionSelections(ctx context.Context, dispositionID uuid.UUID, selectedActions []contract.CreateDispositionActionSelection, createdBy uuid.UUID) error {
|
|
||||||
if len(selectedActions) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
selections := make([]entities.LetterDispositionActionSelection, 0, len(selectedActions))
|
|
||||||
for _, sel := range selectedActions {
|
|
||||||
selections = append(selections, entities.LetterDispositionActionSelection{
|
|
||||||
DispositionID: dispositionID,
|
|
||||||
ActionID: sel.ActionID,
|
|
||||||
Note: sel.Note,
|
|
||||||
CreatedBy: createdBy,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.dispositionActionSelRepo.CreateBulk(ctx, selections); err != nil {
|
|
||||||
return fmt.Errorf("failed to create action selections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildDispositionResponse builds the response for the created disposition
|
|
||||||
func (p *LetterProcessorImpl) buildDispositionResponse(disp *entities.LetterIncomingDisposition, dispDepartments []entities.LetterIncomingDispositionDepartment, toDepartmentIDs []uuid.UUID) *contract.ListDispositionsResponse {
|
|
||||||
response := &contract.ListDispositionsResponse{
|
|
||||||
Dispositions: []contract.DispositionResponse{transformer.DispoToContract(*disp)},
|
|
||||||
}
|
|
||||||
|
|
||||||
// The toDepartmentIDs are available in the dispDepartments for service layer logging
|
|
||||||
// No need to store them in the response as DispositionResponse doesn't have this field
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
|
|
||||||
list, err := p.dispositionRepo.ListByLetter(ctx, letterID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
|
||||||
// Get dispositions with all related data preloaded in a single query
|
|
||||||
dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get discussions with preloaded user profiles
|
|
||||||
discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract all mentioned user IDs from discussions for efficient batch fetching
|
|
||||||
var mentionedUserIDs []uuid.UUID
|
|
||||||
mentionedUserIDsMap := make(map[uuid.UUID]bool)
|
|
||||||
|
|
||||||
for _, discussion := range discussions {
|
|
||||||
if discussion.Mentions != nil {
|
|
||||||
mentions := map[string]interface{}(discussion.Mentions)
|
|
||||||
if userIDs, ok := mentions["user_ids"]; ok {
|
|
||||||
if userIDList, ok := userIDs.([]interface{}); ok {
|
|
||||||
for _, userID := range userIDList {
|
|
||||||
if userIDStr, ok := userID.(string); ok {
|
|
||||||
if userUUID, err := uuid.Parse(userIDStr); err == nil {
|
|
||||||
if !mentionedUserIDsMap[userUUID] {
|
|
||||||
mentionedUserIDsMap[userUUID] = true
|
|
||||||
mentionedUserIDs = append(mentionedUserIDs, userUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all mentioned users in a single batch query
|
|
||||||
var mentionedUsers []entities.User
|
|
||||||
if len(mentionedUserIDs) > 0 {
|
|
||||||
mentionedUsers, err = p.discussionRepo.GetUsersByIDs(ctx, mentionedUserIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform dispositions
|
|
||||||
enhancedDispositions := transformer.EnhancedDispositionsWithPreloadedDataToContract(dispositions)
|
|
||||||
|
|
||||||
// Transform discussions with mentioned users
|
|
||||||
enhancedDiscussions := transformer.DiscussionsWithPreloadedDataToContract(discussions, mentionedUsers)
|
|
||||||
|
|
||||||
return &contract.ListEnhancedDispositionsResponse{
|
|
||||||
Dispositions: enhancedDispositions,
|
|
||||||
Discussions: enhancedDiscussions,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
|
||||||
userID := appcontext.FromGinContext(ctx).UserID
|
|
||||||
|
|
||||||
mentions := entities.JSONB(nil)
|
|
||||||
if req.Mentions != nil {
|
|
||||||
mentions = entities.JSONB(req.Mentions)
|
|
||||||
}
|
|
||||||
|
|
||||||
disc := &entities.LetterDiscussion{
|
|
||||||
ID: uuid.New(),
|
|
||||||
LetterID: letterID,
|
|
||||||
ParentID: req.ParentID,
|
|
||||||
UserID: userID,
|
|
||||||
Message: req.Message,
|
|
||||||
Mentions: mentions,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.discussionRepo.Create(ctx, disc); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create discussion: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activity logging should be handled at service layer
|
|
||||||
return transformer.DiscussionEntityToContract(disc), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, string, error) {
|
|
||||||
// Transaction should be handled at service layer
|
|
||||||
disc, err := p.discussionRepo.Get(ctx, discussionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("failed to get discussion: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store old message for activity logging
|
|
||||||
oldMessage := disc.Message
|
|
||||||
|
|
||||||
// Update discussion fields
|
|
||||||
disc.Message = req.Message
|
|
||||||
if req.Mentions != nil {
|
|
||||||
disc.Mentions = entities.JSONB(req.Mentions)
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
disc.EditedAt = &now
|
|
||||||
|
|
||||||
if err := p.discussionRepo.Update(ctx, disc); err != nil {
|
|
||||||
return nil, "", fmt.Errorf("failed to update discussion: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return both the updated discussion and old message for service layer logging
|
|
||||||
return transformer.DiscussionEntityToContract(disc), oldMessage, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []contract.CreateIncomingLetterAttachment, userID uuid.UUID) error {
|
|
||||||
if len(attachments) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentEntities := make([]entities.LetterIncomingAttachment, 0, len(attachments))
|
|
||||||
for _, a := range attachments {
|
|
||||||
attachmentEntities = append(attachmentEntities, entities.LetterIncomingAttachment{
|
|
||||||
LetterID: letterID,
|
|
||||||
FileURL: a.FileURL,
|
|
||||||
FileName: a.FileName,
|
|
||||||
FileType: a.FileType,
|
|
||||||
UploadedBy: &userID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.attachRepo.CreateBulk(ctx, attachmentEntities); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attachment logging will be handled by service layer
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) SearchIncomingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterIncoming, int64, error) {
|
|
||||||
offset := (page - 1) * limit
|
|
||||||
return p.letterRepo.Search(ctx, filters, limit, offset, sortBy, sortOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) buildLetterResponse(ctx context.Context, entity *entities.LetterIncoming) (*contract.IncomingLetterResponse, error) {
|
|
||||||
savedAttachments, _ := p.attachRepo.ListByLetter(ctx, entity.ID)
|
|
||||||
dispo, _ := p.dispositionRepo.ListByLetter(ctx, entity.ID)
|
|
||||||
|
|
||||||
var pr *entities.Priority
|
|
||||||
if entity.PriorityID != nil && p.priorityRepo != nil {
|
|
||||||
if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
|
|
||||||
pr = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var inst *entities.Institution
|
|
||||||
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
|
|
||||||
if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
|
|
||||||
inst = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformer.LetterEntityToContract(entity, savedAttachments, dispo, pr, inst), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
|
||||||
return p.letterRepo.BulkArchive(ctx, letterIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error {
|
|
||||||
return p.letterRepo.Archive(ctx, letterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BulkArchiveIncomingLettersForUser archives letters for a specific user only
|
|
||||||
func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
|
|
||||||
return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID)
|
|
||||||
}
|
|
||||||
@ -1,251 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"eslogad-be/internal/appcontext"
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
"eslogad-be/internal/transformer"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) {
|
|
||||||
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, req.LetterIncomingID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var response []contract.DepartmentDispositionStatusResponse
|
|
||||||
for _, disp := range dispositions {
|
|
||||||
letterResp := transformer.LetterIncomingEntityToContract(disp.LetterIncoming)
|
|
||||||
|
|
||||||
var fromDept *contract.DepartmentResponse
|
|
||||||
if disp.LetterIncomingDisposition != nil && disp.LetterIncomingDisposition.DepartmentID != nil {
|
|
||||||
fromDept = transformer.DepartmentEntityToContract(&disp.LetterIncomingDisposition.Department)
|
|
||||||
}
|
|
||||||
|
|
||||||
response = append(response, contract.DepartmentDispositionStatusResponse{
|
|
||||||
ID: disp.ID,
|
|
||||||
LetterID: disp.LetterIncomingID,
|
|
||||||
Letter: letterResp,
|
|
||||||
FromDepartmentID: disp.LetterIncomingDisposition.DepartmentID,
|
|
||||||
FromDepartment: fromDept,
|
|
||||||
ToDepartmentID: disp.DepartmentID,
|
|
||||||
ToDepartment: transformer.DepartmentEntityToContract(disp.Department),
|
|
||||||
Status: string(disp.Status),
|
|
||||||
Notes: disp.LetterIncomingDisposition.Notes,
|
|
||||||
ReadAt: disp.ReadAt,
|
|
||||||
CompletedAt: disp.CompletedAt,
|
|
||||||
CreatedAt: disp.CreatedAt,
|
|
||||||
UpdatedAt: disp.UpdatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.ListDepartmentDispositionStatusResponse{
|
|
||||||
Dispositions: response,
|
|
||||||
Pagination: contract.PaginationResponse{
|
|
||||||
TotalCount: len(response),
|
|
||||||
Page: 1,
|
|
||||||
Limit: len(response),
|
|
||||||
TotalPages: 1,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) {
|
|
||||||
var result *contract.DepartmentDispositionStatusResponse
|
|
||||||
|
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
|
||||||
departmentID := appcontext.FromGinContext(txCtx).DepartmentID
|
|
||||||
|
|
||||||
dispDept, err := p.dispositionDeptRepo.GetByDispositionAndDepartment(txCtx, req.LetterIncomingID, departmentID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
notes := ""
|
|
||||||
if req.Notes != nil {
|
|
||||||
notes = *req.Notes
|
|
||||||
}
|
|
||||||
if err := p.updateDispositionDepartmentStatus(txCtx, dispDept.ID, req.Status, notes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.activity.LogLetterDispositionStatusUpdate(txCtx, req.LetterIncomingID, userID, req.Status)
|
|
||||||
|
|
||||||
if err := p.checkAndUpdateLetterCompletionStatus(txCtx, req.LetterIncomingID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedDispDept, err := p.dispositionDeptRepo.GetByID(txCtx, dispDept.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result = p.buildDispositionStatusResponse(updatedDispDept)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateDispositionDepartmentStatus updates the status of a disposition department
|
|
||||||
func (p *LetterProcessorImpl) updateDispositionDepartmentStatus(ctx context.Context, dispDeptID uuid.UUID, status, notes string) error {
|
|
||||||
now := time.Now()
|
|
||||||
var dispositionStatus entities.LetterIncomingDispositionDepartmentStatus
|
|
||||||
var readAt, completedAt *time.Time
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case "completed":
|
|
||||||
dispositionStatus = entities.DispositionDepartmentStatusCompleted
|
|
||||||
completedAt = &now
|
|
||||||
readAt = &now // Mark as read when completing
|
|
||||||
case "read":
|
|
||||||
dispositionStatus = entities.DispositionDepartmentStatusRead
|
|
||||||
readAt = &now
|
|
||||||
default:
|
|
||||||
dispositionStatus = entities.DispositionDepartmentStatusPending
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.dispositionDeptRepo.UpdateStatus(ctx, dispDeptID, dispositionStatus, notes, readAt, completedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addDispositionNoteIfProvided adds a note to the disposition if provided
|
|
||||||
func (p *LetterProcessorImpl) addDispositionNoteIfProvided(ctx context.Context, dispositionID uuid.UUID, userID uuid.UUID, notes *string) error {
|
|
||||||
if notes == nil || *notes == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
note := &entities.DispositionNote{
|
|
||||||
DispositionID: dispositionID,
|
|
||||||
UserID: &userID,
|
|
||||||
Note: *notes,
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.dispositionNoteRepo.Create(ctx, note)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) checkAndUpdateLetterCompletionStatus(ctx context.Context, letterIncomingID uuid.UUID) error {
|
|
||||||
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
allCompleted := true
|
|
||||||
for _, disp := range dispositions {
|
|
||||||
if disp.Status == entities.DispositionDepartmentStatusPending {
|
|
||||||
allCompleted = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if allCompleted && len(dispositions) > 0 {
|
|
||||||
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
letter.Status = "completed"
|
|
||||||
if err := p.letterRepo.Update(ctx, letter); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildDispositionStatusResponse builds the response for disposition status
|
|
||||||
func (p *LetterProcessorImpl) buildDispositionStatusResponse(dispDept *entities.LetterIncomingDispositionDepartment) *contract.DepartmentDispositionStatusResponse {
|
|
||||||
letterResp := transformer.LetterIncomingEntityToContract(dispDept.LetterIncoming)
|
|
||||||
|
|
||||||
var fromDept *contract.DepartmentResponse
|
|
||||||
if dispDept.LetterIncomingDisposition != nil && dispDept.LetterIncomingDisposition.DepartmentID != nil {
|
|
||||||
fromDept = transformer.DepartmentEntityToContract(&dispDept.LetterIncomingDisposition.Department)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.DepartmentDispositionStatusResponse{
|
|
||||||
ID: dispDept.ID,
|
|
||||||
LetterID: dispDept.LetterIncomingID,
|
|
||||||
Letter: letterResp,
|
|
||||||
FromDepartmentID: dispDept.LetterIncomingDisposition.DepartmentID,
|
|
||||||
FromDepartment: fromDept,
|
|
||||||
ToDepartmentID: dispDept.DepartmentID,
|
|
||||||
ToDepartment: transformer.DepartmentEntityToContract(dispDept.Department),
|
|
||||||
Status: string(dispDept.Status),
|
|
||||||
Notes: dispDept.LetterIncomingDisposition.Notes,
|
|
||||||
ReadAt: dispDept.ReadAt,
|
|
||||||
CompletedAt: dispDept.CompletedAt,
|
|
||||||
CreatedAt: dispDept.CreatedAt,
|
|
||||||
UpdatedAt: dispDept.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) GetLetterCTA(ctx context.Context, letterIncomingID uuid.UUID, departmentID uuid.UUID) (*contract.LetterCTAResponse, error) {
|
|
||||||
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
response := &contract.LetterCTAResponse{
|
|
||||||
LetterIncomingID: letterIncomingID,
|
|
||||||
Actions: []contract.LetterCTAAction{},
|
|
||||||
Message: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
isEligibleForDispo, err := p.dispoRoutes.IsEligibleForDisposition(ctx, departmentID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if letter.Status == "completed" || letter.Status == "archived" {
|
|
||||||
response.Message = "Letter is no longer accepting actions"
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(ctx, letterIncomingID, departmentID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(dispDepts) == 0 {
|
|
||||||
response.Message = "Your department is not a recipient of this letter"
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dispDept := range dispDepts {
|
|
||||||
if dispDept.Status == entities.DispositionDepartmentStatusPending {
|
|
||||||
response.DispositionID = &dispDept.LetterIncomingDispositionID
|
|
||||||
currentStatus := string(dispDept.Status)
|
|
||||||
response.CurrentStatus = ¤tStatus
|
|
||||||
|
|
||||||
if isEligibleForDispo {
|
|
||||||
response.Actions = append(response.Actions, contract.LetterCTAAction{
|
|
||||||
Type: "create_disposition",
|
|
||||||
Label: "Disposisi",
|
|
||||||
Path: fmt.Sprintf("/api/v1/letters/%s/dispositions", letterIncomingID),
|
|
||||||
Method: "POST",
|
|
||||||
Description: "Create a new disposition for this letter",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Actions = append(response.Actions, contract.LetterCTAAction{
|
|
||||||
Type: "update_status",
|
|
||||||
Label: "Tindak Lanjut",
|
|
||||||
Path: fmt.Sprintf("/api/v1/letters/dispositions/%s/status", response.LetterIncomingID),
|
|
||||||
Method: "PUT",
|
|
||||||
Description: "Update the status of your disposition",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user