add dukcapil

This commit is contained in:
Aditya Siregar 2026-05-07 04:01:32 +07:00
parent 9a975d146e
commit bc64eb20ea
235 changed files with 1197 additions and 28187 deletions

View File

@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"*",
"Bash(*)",
"Bash(cd:*)",
"Bash(mkdir:*)",
"Bash(cat:*)",
"Bash(go mod:*)",
"Bash(go build:*)"
]
}
}

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ config/env/*
!.env
vendor
infra/*.yaml
!infra/*.yaml.example

View File

@ -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

Binary file not shown.

320
DOCKER.md
View File

@ -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
```

View File

@ -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
View File

@ -1,9 +1,11 @@
#PROJECT_NAME = "enaklo-pos-backend"
DB_USERNAME :=eslogad_user
DB_PASSWORD :=M9u%24e%23jT2%40qR4pX%21zL
DB_HOST :=103.191.71.2
DB_PORT :=5432
DB_NAME :=eslogad_db
# Go Backend Template Makefile
# Database configuration (update these for your project)
DB_USERNAME := postgres
DB_PASSWORD := postgres
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
@ -19,45 +21,31 @@ help:
@echo "Usage: make [command]"
@echo
@echo "Commands:"
@echo " rename-project name={name} Rename project"
@echo
@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 " build Build the application"
@echo " run Run the application"
@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
# Build
.SILENT: rename-project
rename-project:
ifeq ($(name),)
@echo 'new project name not set'
else
ifeq ($(DETECTED_OS),Darwin)
@grep -RiIl '$(PROJECT_NAME)' | xargs sed -i '' 's/$(PROJECT_NAME)/$(name)/g'
endif
.SILENT: build
build:
@go build -o ./bin/server ./cmd/server/main.go
@echo "✓ Binary built: ./bin/server"
ifeq ($(DETECTED_OS),Linux)
@grep -RiIl '$(PROJECT_NAME)' | xargs sed -i 's/$(PROJECT_NAME)/$(name)/g'
endif
# Run
ifeq ($(DETECTED_OS),Windows)
@grep 'target is not implemented on Windows platform'
endif
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
.SILENT: run
run:
@ENV_MODE=development go run cmd/server/main.go
# Test
@ -65,50 +53,61 @@ build-http:
test:
@go test ./... -v
# Create migration
# Format
.SILENT: fmt
fmt:
@go fmt ./...
@echo "✓ Code formatted"
# Migrations
.SILENT: 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)
# Up migration
@echo "✓ Migration created: $(name)"
.SILENT: migration-up
migration-up:
@migrate -database $(DB_URL) -path ./migrations up
# Down migration
@echo "✓ Migrations applied"
.SILENT: migration-down
migration-down:
@migrate -database $(DB_URL) -path ./migrations down 1
.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
@echo "✓ Last migration rolled back"
# Docker
.SILENT: docker-up
docker-up:
@docker-compose up -d
@echo "✓ Docker services started"
.SILENT: docker-down
docker-down:
@docker-compose down
@echo "✓ Docker services stopped"
# Format
# Dependencies
.SILENT: fmt
fmt:
@go fmt ./...
.SILENT: deps
deps:
@go mod download
@go mod tidy
@echo "✓ Dependencies updated"
start:
go run main.go --env-path .env
# Clean
.SILENT: clean
clean:
@rm -rf ./bin
@echo "✓ Build artifacts cleaned"
# Default

View File

@ -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
View File

@ -1,310 +1,342 @@
<h1 align="center">
<img height="80" width="160" src="./assets/gopher-icon.gif" alt="Go"><br>Backend Template
</h1>
# Go Backend Template
> 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:
* [go](https://go.dev/doc/install)
* [docker-compose](https://docs.docker.com/compose/reference)
* [migrate](https://github.com/golang-migrate/migrate)
```shell
$ make
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
}
]
}'
```
- **Clean Architecture** - Separation of concerns with handler → service → processor → repository layers
- **Authentication & Authorization** - JWT-based auth with role-based access control
- **Database Ready** - PostgreSQL integration with GORM
- **Middleware Stack** - CORS, logging, recovery, correlation ID tracking
- **Configuration Management** - Environment-based config with Viper
- **Structured Logging** - Zap and Logrus integration
- **Input Validation** - Request validation with go-playground/validator
- **Graceful Shutdown** - Proper cleanup on termination signals
## Project Structure
```
eslogad-backend/
.
├── cmd/
│ └── 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/
│ ├── app/ # Application wiring and dependency injection
│ ├── contract/ # API contracts (request/response DTOs)
│ ├── handler/ # HTTP handlers and routes
│ ├── service/ # Business logic orchestration
│ ├── processor/ # Complex business operations
│ ├── app/ # Application initialization
│ ├── appcontext/ # Request context utilities
│ ├── constant/ # Application constants
│ ├── constants/ # Business constants
│ ├── 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
│ ├── models/ # Pure business models
│ ├── entities/ # Database entities (GORM models)
│ ├── constants/ # Business constants and enums
│ ├── transformer/ # Contract ↔ Model transformations
│ └── mappers/ # Model ↔ Entity transformations
│ ├── router/ # Route definitions
│ ├── service/ # Service layer
│ ├── transformer/ # DTO transformers
│ ├── util/ # Utility functions
│ └── validator/ # Custom validators
├── migrations/ # Database migrations
├── Makefile # Build and development commands
├── go.mod # Go module definition
└── README.md # This file
├── infra/ # Infrastructure configs (YAML)
├── go.mod
├── 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
- **[Gorilla Mux](https://github.com/gorilla/mux)** - HTTP router and URL matcher
- **[GORM](https://gorm.io/)** - ORM for database operations
- **[PostgreSQL Driver](https://github.com/lib/pq)** - PostgreSQL database driver
- **[Validator](https://github.com/go-playground/validator)** - Struct validation
- **[UUID](https://github.com/google/uuid)** - UUID generation and parsing
Key dependencies:
- **Gin** - HTTP web framework
- **GORM** - ORM library
- **Viper** - Configuration management
- **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
1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
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.

View File

@ -1,23 +1,20 @@
package main
import (
"eslogad-be/config"
"eslogad-be/internal/app"
"eslogad-be/internal/db"
"eslogad-be/internal/logger"
"go-backend-template/config"
"go-backend-template/internal/app"
"go-backend-template/internal/logger"
"log"
)
func main() {
cfg := config.LoadConfig()
log.Printf("DEBUG: Config loaded successfully")
log.Printf("DEBUG: Port from config: %q", cfg.Port())
logger.Setup(cfg.LogLevel(), cfg.LogFormat())
db, err := db.NewPostgres(cfg.Database)
if err != nil {
log.Fatal(err)
}
application := app.NewApp(db)
application := app.NewApp()
if err := application.Initialize(cfg); err != nil {
log.Fatalf("Failed to initialize application: %v", err)

View File

@ -29,9 +29,7 @@ type Config struct {
Jwt Jwt `mapstructure:"jwt"`
Log Log `mapstructure:"log"`
S3Config S3Config `mapstructure:"s3"`
OnlyOffice OnlyOffice `mapstructure:"onlyoffice"`
Novu Novu `mapstructure:"novu"`
Department Department `mapstructure:"department"`
Dukcapil Dukcapil `mapstructure:"dukcapil"`
}
var (
@ -83,19 +81,3 @@ func (c *Config) LogFormat() string {
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
View 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
}

View File

@ -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"

View File

@ -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
View File

@ -1,4 +1,4 @@
module eslogad-be
module go-backend-template
go 1.21
@ -38,7 +38,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
@ -62,7 +61,6 @@ require (
require (
github.com/aws/aws-sdk-go v1.55.7
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/novuhq/go-novu v0.1.2
github.com/sirupsen/logrus v1.9.3
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.28.0

2
go.sum
View File

@ -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/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
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/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@ -1,50 +1,38 @@
server:
base-url:
local-url:
port: 4000
port: "8080"
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:
token:
expires-ttl: 1440
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
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'
secret: "your-secret-key-change-this-in-production"
expires_ttl: 3600
log:
log_format: 'json'
log_level: 'debug'
log_level: "info"
log_format: "json"
onlyoffice:
url: 'https://noken-log-onlyoffice.tni-ad.mil.id'
token: 'Si7vqBAZElQzeQF2KUbN5j9qKc1GX0kq'
s3:
region: "us-east-1"
bucket: "your-bucket-name"
access_key_id: "your-access-key"
secret_access_key: "your-secret-key"
novu:
api_key: 'f7de60a16abf825996191bf69ea8054a' # Add your Novu API key here
application_id: 'cDeX8L5VWe-r' # Add your Novu Application ID here
base_url: 'https://noken-log-novu-api.tni-ad.mil.id' # Optional: defaults to https://api.novu.co
incoming_letter_workflow_id: 'notification-dashbpard'
department:
parent_path: 'eslogad.aslog' # Parent path for departments to be included in API
excluded_paths: # Paths to exclude from department APIs
- 'superadmin'
- 'system'
dukcapil:
base_url: "http://172.16.160.176:8080/api/face-recognition"
customer_id: "your-customer-id"
methode: "CALL_FN"
user_id: "281020241202039900305241000011252"
password: "Fjskdhv35$%"
public_key_path: "infra/dukcapil_public.pem"
default_ip: "10.160.86.53"
timeout_second: 30

38
infra/local.yaml.example Normal file
View 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

View File

@ -9,69 +9,39 @@ import (
"syscall"
"time"
"eslogad-be/config"
"eslogad-be/internal/client"
internalConfig "eslogad-be/internal/config"
"eslogad-be/internal/handler"
"eslogad-be/internal/middleware"
"eslogad-be/internal/processor"
"eslogad-be/internal/repository"
"eslogad-be/internal/router"
"eslogad-be/internal/service"
"eslogad-be/internal/validator"
"gorm.io/gorm"
"go-backend-template/config"
"go-backend-template/internal/client"
"go-backend-template/internal/handler"
"go-backend-template/internal/router"
"go-backend-template/internal/service"
)
type App struct {
server *http.Server
db *gorm.DB
router *router.Router
shutdown chan os.Signal
}
func NewApp(db *gorm.DB) *App {
func NewApp() *App {
return &App{
db: db,
shutdown: make(chan os.Signal, 1),
}
}
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()
fileHandler := handler.NewFileHandler(services.fileService)
rbacHandler := handler.NewRBACHandler(services.rbacService)
masterHandler := handler.NewMasterHandler(services.masterService)
letterHandler := handler.NewLetterHandler(services.letterService)
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)
dukcapilClient := client.NewDukcapilClient(cfg.Dukcapil)
dukcapilService := service.NewDukcapilService(dukcapilClient)
dukcapilHandler := handler.NewDukcapilHandler(dukcapilService)
a.router = router.NewRouter(
cfg,
handler.NewAuthHandler(services.authService),
middlewares.authMiddleware,
nil, // authHandler
nil, // authMiddleware
healthHandler,
handler.NewUserHandler(services.userService, validator.NewUserValidator()),
fileHandler,
rbacHandler,
masterHandler,
letterHandler,
letterOutgoingHandler,
adminApprovalFlowHandler,
dispositionRouteHandler,
onlyOfficeHandler,
analyticsHandler,
notificationHandler,
repositoryAttachmentHandler,
nil, // userHandler
dukcapilHandler,
)
return nil
@ -80,8 +50,20 @@ func (a *App) Initialize(cfg *config.Config) error {
func (a *App) Start(port string) error {
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{
Addr: ":" + port,
Addr: addr,
Handler: engine,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
@ -115,362 +97,3 @@ func (a *App) Start(port string) error {
func (a *App) 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(),
}
}

View 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
}

View File

@ -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,
}
}

View File

@ -39,6 +39,8 @@ const (
PaymentMethodHandlerEntity = "payment_method_handler"
OutletServiceEntity = "outlet_service"
TableEntity = "table"
DukcapilHandlerEntity = "dukcapil_handler"
DukcapilServiceEntity = "dukcapil_service"
)
var HttpErrorMap = map[string]int{

View File

@ -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"`
}

View File

@ -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"`
}

View 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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -10,17 +10,12 @@ type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Email string `json:"email" validate:"required,email"`
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 {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Role *uuid.UUID `json:"role,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Permissions *map[string]interface{} `json:"permissions,omitempty"`
DepartmentIDs *[]uuid.UUID `json:"department_ids,omitempty"`
}
type ChangePasswordRequest struct {
@ -28,14 +23,6 @@ type ChangePasswordRequest struct {
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 {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
@ -44,10 +31,11 @@ type LoginRequest struct {
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
User UserResponse `json:"user"`
Roles []RoleResponse `json:"roles"`
Permissions []string `json:"permissions"`
Departments []DepartmentResponse `json:"departments"`
User *UserResponse `json:"user"`
}
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
type UserResponse struct {
@ -57,140 +45,16 @@ type UserResponse struct {
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_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 {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Role *string `json:"role,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Search *string `json:"search,omitempty"`
RoleCode *string `json:"role_code,omitempty"`
}
type ListUsersResponse struct {
type PaginatedUserResponse struct {
Users []UserResponse `json:"users"`
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"`
}

View File

@ -1,7 +1,7 @@
package db
import (
"eslogad-be/config"
"go-backend-template/config"
"fmt"
"io/fs"
"os"

View File

@ -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" }

View File

@ -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" }

View File

@ -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"`
}

View File

@ -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" }

View File

@ -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" }

View File

@ -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
}

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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"
}

View File

@ -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"
)

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -49,7 +49,6 @@ type User struct {
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
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 {
@ -64,10 +63,6 @@ func (User) TableName() string {
}
func (u *User) HasPermission(permission string) bool {
return false
}
func (u *User) CanAccessOutlet(outletID uuid.UUID) bool {
// TODO: Implement permission checking logic
return false
}

View File

@ -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))
}

View File

@ -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,
}))
}

View File

@ -1,13 +1,13 @@
package handler
import (
"eslogad-be/internal/util"
"go-backend-template/internal/util"
"net/http"
"strings"
"eslogad-be/internal/constants"
"eslogad-be/internal/contract"
"eslogad-be/internal/logger"
"go-backend-template/internal/constants"
"go-backend-template/internal/contract"
"go-backend-template/internal/logger"
"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")
}
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 {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {

View File

@ -2,12 +2,10 @@ package handler
import (
"context"
"eslogad-be/internal/contract"
"go-backend-template/internal/contract"
)
type AuthService interface {
Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error)
ValidateToken(tokenString string) (*contract.UserResponse, error)
RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error)
Logout(ctx context.Context, tokenString string) error
RefreshToken(ctx context.Context, req *contract.RefreshTokenRequest) (*contract.LoginResponse, error)
}

View File

@ -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))
}

View 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,
},
})
}

View 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)
}

View File

@ -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}))
}

View File

@ -1,7 +1,7 @@
package handler
import (
"eslogad-be/internal/logger"
"go-backend-template/internal/logger"
"net/http"
"github.com/gin-gonic/gin"

View File

@ -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"})
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -4,10 +4,9 @@ import (
"net/http"
"strconv"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/constants"
"eslogad-be/internal/contract"
"eslogad-be/internal/logger"
"go-backend-template/internal/constants"
"go-backend-template/internal/contract"
"go-backend-template/internal/logger"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@ -149,248 +148,32 @@ func (h *UserHandler) GetUser(c *gin.Context) {
func (h *UserHandler) ListUsers(c *gin.Context) {
ctx := c.Request.Context()
req := &contract.ListUsersRequest{
Page: 1,
Limit: 10,
}
page := 1
limit := 10
if page := c.Query("page"); page != "" {
if p, err := strconv.Atoi(page); err == nil {
req.Page = p
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil {
page = p
}
}
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
req.Limit = l
if limitStr := c.Query("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil {
limit = l
}
}
var roleParam *string
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)
usersResponse, err := h.userService.GetUsers(ctx, page, limit)
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", usersResponse)
logger.FromContext(c).Infof("UserHandler::ListUsers -> Successfully listed users")
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) {
errorResponse := &contract.ErrorResponse{
Error: message,

View File

@ -2,7 +2,7 @@ package handler
import (
"context"
"eslogad-be/internal/contract"
"go-backend-template/internal/contract"
"github.com/google/uuid"
)
@ -12,16 +12,5 @@ type UserService interface {
UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error)
DeleteUser(ctx context.Context, id uuid.UUID) error
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, 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)
GetUsers(ctx context.Context, page, limit int) (*contract.PaginatedUserResponse, error)
}

View File

@ -1,7 +1,7 @@
package handler
import (
"eslogad-be/internal/contract"
"go-backend-template/internal/contract"
"github.com/google/uuid"
)
@ -12,5 +12,4 @@ type UserValidator interface {
ValidateListUsersRequest(req *contract.ListUsersRequest) (error, string)
ValidateChangePasswordRequest(req *contract.ChangePasswordRequest) (error, string)
ValidateUserID(userID uuid.UUID) (error, string)
ValidateUpdateUserOutletRequest(req *contract.UpdateUserOutletRequest) (error, string)
}

View File

@ -2,7 +2,7 @@ package logger
import (
"context"
"eslogad-be/internal/appcontext"
"go-backend-template/internal/appcontext"
"log"
"os"

View File

@ -1,13 +1,13 @@
package middleware
import (
"eslogad-be/internal/appcontext"
"go-backend-template/internal/appcontext"
"net/http"
"strings"
"eslogad-be/internal/constants"
"eslogad-be/internal/contract"
"eslogad-be/internal/logger"
"go-backend-template/internal/constants"
"go-backend-template/internal/contract"
"go-backend-template/internal/logger"
"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 {
return func(c *gin.Context) {
appCtx := appcontext.FromGinContext(c.Request.Context())

View File

@ -1,13 +1,4 @@
package middleware
import (
"context"
"eslogad-be/internal/contract"
)
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)
}

View File

@ -2,8 +2,8 @@ package middleware
import (
"context"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/constants"
"go-backend-template/internal/appcontext"
"go-backend-template/internal/constants"
"github.com/gin-gonic/gin"
)

View File

@ -2,8 +2,8 @@ package middleware
import (
"context"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/constants"
"go-backend-template/internal/appcontext"
"go-backend-template/internal/constants"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)

View File

@ -1,9 +1,9 @@
package middleware
import (
"eslogad-be/internal/contract"
"eslogad-be/internal/logger"
"eslogad-be/internal/util"
"go-backend-template/internal/contract"
"go-backend-template/internal/logger"
"go-backend-template/internal/util"
"github.com/gin-gonic/gin"
"net/http"
"runtime/debug"

View File

@ -1,8 +1,8 @@
package middleware
import (
"eslogad-be/internal/constants"
"eslogad-be/internal/logger"
"go-backend-template/internal/constants"
"go-backend-template/internal/logger"
"fmt"
"github.com/gin-gonic/gin"
"net/http"

View File

@ -1,8 +1,8 @@
package middleware
import (
"eslogad-be/internal/contract"
"eslogad-be/internal/logger"
"go-backend-template/internal/contract"
"go-backend-template/internal/logger"
"github.com/gin-gonic/gin"
"github.com/google/uuid"

View File

@ -2,7 +2,7 @@ package middleware
import (
"context"
"eslogad-be/internal/contract"
"go-backend-template/internal/contract"
"github.com/google/uuid"
)

View File

@ -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,
})
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 = &currentStatus
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