Init All Docs
This commit is contained in:
parent
aa662a321f
commit
2319019eb2
278
NOTIFICATION_API.md
Normal file
278
NOTIFICATION_API.md
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
# 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
|
||||||
@ -30,6 +30,7 @@ type Config struct {
|
|||||||
Log Log `mapstructure:"log"`
|
Log Log `mapstructure:"log"`
|
||||||
S3Config S3Config `mapstructure:"s3"`
|
S3Config S3Config `mapstructure:"s3"`
|
||||||
OnlyOffice OnlyOffice `mapstructure:"onlyoffice"`
|
OnlyOffice OnlyOffice `mapstructure:"onlyoffice"`
|
||||||
|
Novu Novu `mapstructure:"novu"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -85,3 +86,10 @@ type OnlyOffice struct {
|
|||||||
URL string `mapstructure:"url"`
|
URL string `mapstructure:"url"`
|
||||||
Token string `mapstructure:"token"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
BIN
eslogad-be
Executable file
BIN
eslogad-be
Executable file
Binary file not shown.
2
go.mod
2
go.mod
@ -38,7 +38,9 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/novuhq/go-novu v0.1.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/spf13/afero v1.9.5 // indirect
|
github.com/spf13/afero v1.9.5 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -209,6 +209,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/novuhq/go-novu v0.1.2 h1:hYVrVjZBUgByVwLE+W4DNXRRCBlHoNNOBLkDI7/enU8=
|
||||||
|
github.com/novuhq/go-novu v0.1.2/go.mod h1:O8+kHDKSfDncLZ8olp5FL00tn1aSTMOvZI1IRZZqmUg=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
|||||||
@ -36,3 +36,9 @@ log:
|
|||||||
onlyoffice:
|
onlyoffice:
|
||||||
url: 'https://onlyoffice.apskel.org/'
|
url: 'https://onlyoffice.apskel.org/'
|
||||||
token: '2DmKgd5PT3n1vH3f2v2iRZUqTVHj9GQx'
|
token: '2DmKgd5PT3n1vH3f2v2iRZUqTVHj9GQx'
|
||||||
|
|
||||||
|
novu:
|
||||||
|
api_key: 'f7de60a16abf825996191bf69ea8054a' # Add your Novu API key here
|
||||||
|
application_id: 'cDeX8L5VWe-r' # Add your Novu Application ID here
|
||||||
|
base_url: 'https://novu-api.apskel.org' # Optional: defaults to https://api.novu.co
|
||||||
|
incoming_letter_workflow_id: 'notification-dashbpard'
|
||||||
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"eslogad-be/config"
|
"eslogad-be/config"
|
||||||
"eslogad-be/internal/client"
|
"eslogad-be/internal/client"
|
||||||
|
internalConfig "eslogad-be/internal/config"
|
||||||
"eslogad-be/internal/handler"
|
"eslogad-be/internal/handler"
|
||||||
"eslogad-be/internal/middleware"
|
"eslogad-be/internal/middleware"
|
||||||
"eslogad-be/internal/processor"
|
"eslogad-be/internal/processor"
|
||||||
@ -51,6 +52,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
||||||
onlyOfficeHandler := handler.NewOnlyOfficeHandler(services.onlyOfficeService)
|
onlyOfficeHandler := handler.NewOnlyOfficeHandler(services.onlyOfficeService)
|
||||||
analyticsHandler := handler.NewAnalyticsHandler(services.analyticsService)
|
analyticsHandler := handler.NewAnalyticsHandler(services.analyticsService)
|
||||||
|
notificationHandler := handler.NewNotificationHandler(services.notificationService)
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
cfg,
|
cfg,
|
||||||
@ -67,6 +69,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
dispositionRouteHandler,
|
dispositionRouteHandler,
|
||||||
onlyOfficeHandler,
|
onlyOfficeHandler,
|
||||||
analyticsHandler,
|
analyticsHandler,
|
||||||
|
notificationHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -183,11 +186,17 @@ func (a *App) initRepositories() *repositories {
|
|||||||
|
|
||||||
type processors struct {
|
type processors struct {
|
||||||
userProcessor *processor.UserProcessorImpl
|
userProcessor *processor.UserProcessorImpl
|
||||||
|
cachedUserProcessor *processor.CachedUserProcessor
|
||||||
letterProcessor *processor.LetterProcessorImpl
|
letterProcessor *processor.LetterProcessorImpl
|
||||||
letterOutgoingProcessor *processor.LetterOutgoingProcessorImpl
|
letterOutgoingProcessor *processor.LetterOutgoingProcessorImpl
|
||||||
activityLogger *processor.ActivityLogProcessorImpl
|
activityLogger *processor.ActivityLogProcessorImpl
|
||||||
letterNumberGenerator *processor.LetterNumberGeneratorImpl
|
letterNumberGenerator *processor.LetterNumberGeneratorImpl
|
||||||
onlyOfficeProcessor *processor.OnlyOfficeProcessorImpl
|
onlyOfficeProcessor *processor.OnlyOfficeProcessorImpl
|
||||||
|
novuProcessor processor.NovuProcessor
|
||||||
|
notificationProcessor processor.NotificationProcessor
|
||||||
|
recipientProcessor *processor.RecipientProcessorImpl
|
||||||
|
letterDispositionProcessor *processor.LetterDispositionProcessorImpl
|
||||||
|
letterDispositionDeptProcessor *processor.LetterDispositionDepartmentProcessorImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||||
@ -203,9 +212,10 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
repos.letterDispositionRepo, repos.letterDispositionDeptRepo,
|
repos.letterDispositionRepo, repos.letterDispositionDeptRepo,
|
||||||
repos.letterDispActionSelRepo, repos.dispositionNoteRepo,
|
repos.letterDispActionSelRepo, repos.dispositionNoteRepo,
|
||||||
repos.letterDiscussionRepo, repos.settingRepo,
|
repos.letterDiscussionRepo, repos.settingRepo,
|
||||||
repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo,
|
repos.recipientRepo, repos.letterOutgoingRecipientRepo,
|
||||||
|
repos.departmentRepo, repos.userDeptRepo,
|
||||||
repos.priorityRepo, repos.institutionRepo, repos.dispRepo,
|
repos.priorityRepo, repos.institutionRepo, repos.dispRepo,
|
||||||
letterNumberGen,
|
letterNumberGen, repos.dispositionRouteRepo,
|
||||||
)
|
)
|
||||||
|
|
||||||
letterOutgoingProc := processor.NewLetterOutgoingProcessor(
|
letterOutgoingProc := processor.NewLetterOutgoingProcessor(
|
||||||
@ -220,6 +230,8 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
repos.letterOutgoingApprovalRepo,
|
repos.letterOutgoingApprovalRepo,
|
||||||
letterNumberGen,
|
letterNumberGen,
|
||||||
txMgr,
|
txMgr,
|
||||||
|
repos.priorityRepo,
|
||||||
|
repos.institutionRepo,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create document repositories
|
// Create document repositories
|
||||||
@ -238,13 +250,60 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
txMgr,
|
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 processor with Novu integration
|
||||||
|
userProc := processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo)
|
||||||
|
userProc.SetNovuProcessor(novuProc)
|
||||||
|
|
||||||
|
// Create cached user processor for auth middleware
|
||||||
|
cachedUserProc := processor.NewCachedUserProcessor(repos.userRepo, repos.userProfileRepo)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
)
|
||||||
|
|
||||||
return &processors{
|
return &processors{
|
||||||
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
|
userProcessor: userProc,
|
||||||
|
cachedUserProcessor: cachedUserProc,
|
||||||
letterProcessor: letterProc,
|
letterProcessor: letterProc,
|
||||||
letterOutgoingProcessor: letterOutgoingProc,
|
letterOutgoingProcessor: letterOutgoingProc,
|
||||||
activityLogger: activity,
|
activityLogger: activity,
|
||||||
letterNumberGenerator: letterNumberGen,
|
letterNumberGenerator: letterNumberGen,
|
||||||
onlyOfficeProcessor: onlyOfficeProc,
|
onlyOfficeProcessor: onlyOfficeProc,
|
||||||
|
novuProcessor: novuProc,
|
||||||
|
notificationProcessor: notificationProc,
|
||||||
|
recipientProcessor: recipientProc,
|
||||||
|
letterDispositionProcessor: letterDispositionProc,
|
||||||
|
letterDispositionDeptProcessor: letterDispositionDeptProc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,6 +319,7 @@ type services struct {
|
|||||||
dispositionRouteService *service.DispositionRouteServiceImpl
|
dispositionRouteService *service.DispositionRouteServiceImpl
|
||||||
onlyOfficeService *service.OnlyOfficeServiceImpl
|
onlyOfficeService *service.OnlyOfficeServiceImpl
|
||||||
analyticsService *service.AnalyticsServiceImpl
|
analyticsService *service.AnalyticsServiceImpl
|
||||||
|
notificationService *service.NotificationServiceImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
@ -277,10 +337,17 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
|
|
||||||
masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo, repos.departmentRepo)
|
masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo, repos.departmentRepo)
|
||||||
|
|
||||||
letterSvc := service.NewLetterService(processors.letterProcessor)
|
|
||||||
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
|
||||||
|
|
||||||
txManager := repository.NewTxManager(a.db)
|
txManager := repository.NewTxManager(a.db)
|
||||||
|
letterSvc := service.NewLetterService(
|
||||||
|
processors.letterProcessor,
|
||||||
|
txManager,
|
||||||
|
processors.letterNumberGenerator,
|
||||||
|
processors.recipientProcessor,
|
||||||
|
processors.activityLogger,
|
||||||
|
processors.letterDispositionProcessor,
|
||||||
|
processors.notificationProcessor,
|
||||||
|
)
|
||||||
|
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
||||||
letterOutgoingSvc := service.NewLetterOutgoingService(processors.letterOutgoingProcessor)
|
letterOutgoingSvc := service.NewLetterOutgoingService(processors.letterOutgoingProcessor)
|
||||||
|
|
||||||
approvalFlowStepRepo := repository.NewApprovalFlowStepRepository(a.db)
|
approvalFlowStepRepo := repository.NewApprovalFlowStepRepository(a.db)
|
||||||
@ -297,6 +364,10 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
// Create Analytics service
|
// Create Analytics service
|
||||||
analyticsSvc := service.NewAnalyticsService(repos.analyticsRepo)
|
analyticsSvc := service.NewAnalyticsService(repos.analyticsRepo)
|
||||||
|
|
||||||
|
// Create Notification service
|
||||||
|
novuConfig := internalConfig.LoadNovuConfig(cfg)
|
||||||
|
notificationSvc := service.NewNotificationService(novuConfig, processors.userProcessor)
|
||||||
|
|
||||||
return &services{
|
return &services{
|
||||||
userService: userSvc,
|
userService: userSvc,
|
||||||
authService: authService,
|
authService: authService,
|
||||||
@ -309,6 +380,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
dispositionRouteService: dispRouteSvc,
|
dispositionRouteService: dispRouteSvc,
|
||||||
onlyOfficeService: onlyOfficeSvc,
|
onlyOfficeService: onlyOfficeSvc,
|
||||||
analyticsService: analyticsSvc,
|
analyticsService: analyticsSvc,
|
||||||
|
notificationService: notificationSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
internal/config/novu.go
Normal file
30
internal/config/novu.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
24
internal/constant/system.go
Normal file
24
internal/constant/system.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SystemUUID is the UUID used for system-generated entities
|
||||||
|
SystemUUIDString = "11111111-2222-3333-4444-555555555555"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SystemUserID is the UUID for the system user
|
||||||
|
SystemUserID = uuid.MustParse(SystemUUIDString)
|
||||||
|
|
||||||
|
// SystemDepartmentID is the UUID for the system department
|
||||||
|
SystemDepartmentID = uuid.MustParse(SystemUUIDString)
|
||||||
|
)
|
||||||
|
|
||||||
|
// System entity constants
|
||||||
|
const (
|
||||||
|
SystemUserEmail = "system@eslogad.internal"
|
||||||
|
SystemUserName = "System User"
|
||||||
|
SystemDeptCode = "SYSTEM"
|
||||||
|
SystemDeptName = "System"
|
||||||
|
)
|
||||||
@ -15,3 +15,7 @@ var ValidCountryCodeMap = map[string]bool{
|
|||||||
"SG": true,
|
"SG": true,
|
||||||
"TH": true,
|
"TH": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppSettingLetterIncomingReceipients = ""
|
||||||
|
)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import "time"
|
|||||||
const (
|
const (
|
||||||
SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX"
|
SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX"
|
||||||
SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE"
|
SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE"
|
||||||
SettingIncomingLetterRecipients = "INCOMING_LETTER_RECIPIENTS"
|
SettingIncomingLetterDepartmentRecipients = "INCOMING_LETTER_DEPARTMENT_RECIPIENTS"
|
||||||
SettingOutgoingLetterPrefix = "OUTGOING_LETTER_PREFIX"
|
SettingOutgoingLetterPrefix = "OUTGOING_LETTER_PREFIX"
|
||||||
SettingOutgoingLetterSequence = "OUTGOING_LETTER_SEQUENCE"
|
SettingOutgoingLetterSequence = "OUTGOING_LETTER_SEQUENCE"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -27,12 +27,19 @@ type DispositionRouteResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateDispositionRouteRequest struct {
|
type CreateDispositionRouteRequest struct {
|
||||||
FromDepartmentID uuid.UUID `json:"from_department_id"`
|
FromDepartmentID uuid.UUID `json:"from_department_id" binding:"required"`
|
||||||
ToDepartmentID uuid.UUID `json:"to_department_id"`
|
ToDepartmentIDs []uuid.UUID `json:"to_department_ids" binding:"required,min=1"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
AllowedActions *map[string]interface{} `json:"allowed_actions,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 {
|
type UpdateDispositionRouteRequest struct {
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
|
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
|
||||||
@ -41,3 +48,26 @@ type UpdateDispositionRouteRequest struct {
|
|||||||
type ListDispositionRoutesResponse struct {
|
type ListDispositionRoutesResponse struct {
|
||||||
Routes []DispositionRouteResponse `json:"routes"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type CreateIncomingLetterAttachment struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateIncomingLetterRequest struct {
|
type CreateIncomingLetterRequest struct {
|
||||||
|
LetterNumber string `json:"-"` // Generated by service layer
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
@ -46,6 +47,7 @@ type IncomingLetterResponse struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Attachments []IncomingLetterAttachmentResponse `json:"attachments"`
|
Attachments []IncomingLetterAttachmentResponse `json:"attachments"`
|
||||||
|
IsRead bool `json:"is_read"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateIncomingLetterRequest struct {
|
type UpdateIncomingLetterRequest struct {
|
||||||
@ -65,11 +67,30 @@ type ListIncomingLettersRequest struct {
|
|||||||
Status *string `json:"status,omitempty"`
|
Status *string `json:"status,omitempty"`
|
||||||
Query *string `json:"query,omitempty"`
|
Query *string `json:"query,omitempty"`
|
||||||
DepartmentID *uuid.UUID
|
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 {
|
type ListIncomingLettersResponse struct {
|
||||||
Letters []IncomingLetterResponse `json:"letters"`
|
Letters []IncomingLetterResponse `json:"letters"`
|
||||||
Pagination PaginationResponse `json:"pagination"`
|
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 {
|
type CreateDispositionActionSelection struct {
|
||||||
@ -83,6 +104,7 @@ type CreateLetterDispositionRequest struct {
|
|||||||
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
|
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
|
||||||
Notes *string `json:"notes,omitempty"`
|
Notes *string `json:"notes,omitempty"`
|
||||||
SelectedActions []CreateDispositionActionSelection `json:"selected_actions,omitempty"`
|
SelectedActions []CreateDispositionActionSelection `json:"selected_actions,omitempty"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DispositionResponse struct {
|
type DispositionResponse struct {
|
||||||
@ -144,6 +166,38 @@ type ListEnhancedDispositionsResponse struct {
|
|||||||
Discussions []LetterDiscussionResponse `json:"discussions"`
|
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 {
|
type CreateLetterDiscussionRequest struct {
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@ -172,3 +226,33 @@ type LetterDiscussionResponse struct {
|
|||||||
// Preloaded user profiles for mentions
|
// Preloaded user profiles for mentions
|
||||||
MentionedUsers []UserResponse `json:"mentioned_users,omitempty"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@ -111,6 +111,7 @@ type ListOutgoingLettersRequest struct {
|
|||||||
PriorityID *uuid.UUID `form:"priority_id" json:"priority_id,omitempty"`
|
PriorityID *uuid.UUID `form:"priority_id" json:"priority_id,omitempty"`
|
||||||
SortBy string `form:"sort_by" json:"sort_by,omitempty"`
|
SortBy string `form:"sort_by" json:"sort_by,omitempty"`
|
||||||
SortOrder string `form:"sort_order" json:"sort_order,omitempty"`
|
SortOrder string `form:"sort_order" json:"sort_order,omitempty"`
|
||||||
|
IsArchived *bool `form:"is_archived" json:"is_archived,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListOutgoingLettersResponse struct {
|
type ListOutgoingLettersResponse struct {
|
||||||
|
|||||||
107
internal/contract/notification_contract.go
Normal file
107
internal/contract/notification_contract.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
8
internal/entities/department_recipients_setting.go
Normal file
8
internal/entities/department_recipients_setting.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
@ -23,14 +23,31 @@ type LetterIncomingDisposition struct {
|
|||||||
|
|
||||||
func (LetterIncomingDisposition) TableName() string { return "letter_incoming_dispositions" }
|
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 {
|
type LetterIncomingDispositionDepartment struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
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"`
|
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"`
|
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"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
Department *Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
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 {
|
func (LetterIncomingDispositionDepartment) TableName() string {
|
||||||
|
|||||||
@ -12,10 +12,13 @@ import (
|
|||||||
|
|
||||||
type DispositionRouteService interface {
|
type DispositionRouteService interface {
|
||||||
Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
|
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)
|
Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
|
||||||
Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error)
|
Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error)
|
||||||
ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error)
|
ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error)
|
||||||
SetActive(ctx context.Context, id uuid.UUID, active bool) 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 }
|
type DispositionRouteHandler struct{ svc DispositionRouteService }
|
||||||
@ -24,18 +27,61 @@ func NewDispositionRouteHandler(svc DispositionRouteService) *DispositionRouteHa
|
|||||||
return &DispositionRouteHandler{svc: svc}
|
return &DispositionRouteHandler{svc: svc}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create handles both single and bulk route creation with upsert logic
|
||||||
func (h *DispositionRouteHandler) Create(c *gin.Context) {
|
func (h *DispositionRouteHandler) Create(c *gin.Context) {
|
||||||
var req contract.CreateDispositionRouteRequest
|
var req contract.CreateDispositionRouteRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body: " + err.Error(), Code: 400})
|
||||||
return
|
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)
|
resp, err := h.svc.Create(c.Request.Context(), &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(201, contract.BuildSuccessResponse(resp))
|
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) {
|
func (h *DispositionRouteHandler) Update(c *gin.Context) {
|
||||||
@ -96,3 +142,23 @@ func (h *DispositionRouteHandler) SetActive(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(200, &contract.SuccessResponse{Message: "updated"})
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"eslogad-be/internal/appcontext"
|
"eslogad-be/internal/appcontext"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
@ -16,14 +17,23 @@ type LetterService interface {
|
|||||||
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
||||||
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
|
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, 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)
|
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error)
|
||||||
|
|
||||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
||||||
|
|
||||||
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, 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)
|
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 {
|
type LetterHandler struct {
|
||||||
@ -119,6 +129,46 @@ func (h *LetterHandler) ListIncomingLetters(c *gin.Context) {
|
|||||||
h.respondSuccess(c, http.StatusOK, resp)
|
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 {
|
func (h *LetterHandler) parseListRequest(c *gin.Context) *contract.ListIncomingLettersRequest {
|
||||||
//appCtx := appcontext.FromGinContext(c)
|
//appCtx := appcontext.FromGinContext(c)
|
||||||
//departmentID := appCtx.DepartmentID
|
//departmentID := appCtx.DepartmentID
|
||||||
@ -145,10 +195,50 @@ func (h *LetterHandler) parseListRequest(c *gin.Context) *contract.ListIncomingL
|
|||||||
if status := c.Query("status"); status != "" {
|
if status := c.Query("status"); status != "" {
|
||||||
req.Status = &status
|
req.Status = &status
|
||||||
}
|
}
|
||||||
|
|
||||||
if query := c.Query("q"); query != "" {
|
if query := c.Query("q"); query != "" {
|
||||||
req.Query = &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
|
//req.DepartmentID = &departmentID
|
||||||
|
|
||||||
return req
|
return req
|
||||||
@ -268,3 +358,82 @@ func (h *LetterHandler) UpdateDiscussion(c *gin.Context) {
|
|||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ type LetterOutgoingService interface {
|
|||||||
GetLetterApprovals(ctx context.Context, letterID uuid.UUID) (*contract.GetLetterApprovalsResponse, error)
|
GetLetterApprovals(ctx context.Context, letterID uuid.UUID) (*contract.GetLetterApprovalsResponse, error)
|
||||||
GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error)
|
GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error)
|
||||||
GetApprovalTimeline(ctx context.Context, letterID uuid.UUID) (*contract.ApprovalTimelineResponse, error)
|
GetApprovalTimeline(ctx context.Context, letterID uuid.UUID) (*contract.ApprovalTimelineResponse, error)
|
||||||
|
BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LetterOutgoingHandler struct {
|
type LetterOutgoingHandler struct {
|
||||||
@ -479,3 +480,24 @@ func (h *LetterOutgoingHandler) GetApprovalTimeline(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) BulkArchiveOutgoingLetters(c *gin.Context) {
|
||||||
|
var req contract.BulkArchiveLettersRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid request body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.LetterIDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "at least one letter ID is required", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.BulkArchiveOutgoingLetters(c.Request.Context(), req.LetterIDs)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|||||||
@ -1,190 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockMasterService is a mock implementation of MasterService
|
|
||||||
type MockMasterService struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) {
|
|
||||||
args := m.Called(ctx, req)
|
|
||||||
return args.Get(0).(*contract.LabelResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) {
|
|
||||||
args := m.Called(ctx, id, req)
|
|
||||||
return args.Get(0).(*contract.LabelResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) DeleteLabel(ctx context.Context, id uuid.UUID) error {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) {
|
|
||||||
args := m.Called(ctx)
|
|
||||||
return args.Get(0).(*contract.ListLabelsResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) {
|
|
||||||
args := m.Called(ctx, req)
|
|
||||||
return args.Get(0).(*contract.PriorityResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) {
|
|
||||||
args := m.Called(ctx, id, req)
|
|
||||||
return args.Get(0).(*contract.PriorityResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) DeletePriority(ctx context.Context, id uuid.UUID) error {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) {
|
|
||||||
args := m.Called(ctx)
|
|
||||||
return args.Get(0).(*contract.ListPrioritiesResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
|
||||||
args := m.Called(ctx, req)
|
|
||||||
return args.Get(0).(*contract.InstitutionResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
|
||||||
args := m.Called(ctx, id, req)
|
|
||||||
return args.Get(0).(*contract.InstitutionResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) {
|
|
||||||
args := m.Called(ctx, req)
|
|
||||||
return args.Get(0).(*contract.ListInstitutionsResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
|
||||||
args := m.Called(ctx, req)
|
|
||||||
return args.Get(0).(*contract.DispositionActionResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
|
||||||
args := m.Called(ctx, id, req)
|
|
||||||
return args.Get(0).(*contract.DispositionActionResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMasterService) ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) {
|
|
||||||
args := m.Called(ctx)
|
|
||||||
return args.Get(0).(*contract.ListDispositionActionsResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMasterHandler_ListInstitutions_WithSearch(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
mockService := new(MockMasterService)
|
|
||||||
handler := NewMasterHandler(mockService)
|
|
||||||
|
|
||||||
// Test data
|
|
||||||
searchTerm := "university"
|
|
||||||
expectedResponse := &contract.ListInstitutionsResponse{
|
|
||||||
Institutions: []contract.InstitutionResponse{
|
|
||||||
{
|
|
||||||
ID: "123",
|
|
||||||
Name: "Test University",
|
|
||||||
Type: "university",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup mock expectations
|
|
||||||
mockService.On("ListInstitutions", mock.Anything, &contract.ListInstitutionsRequest{
|
|
||||||
Search: &searchTerm,
|
|
||||||
}).Return(expectedResponse, nil)
|
|
||||||
|
|
||||||
// Create request
|
|
||||||
req, _ := http.NewRequest("GET", "/institutions?search="+searchTerm, nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Create gin context
|
|
||||||
c, _ := gin.CreateTestContext(w)
|
|
||||||
c.Request = req
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
handler.ListInstitutions(c)
|
|
||||||
|
|
||||||
// Assertions
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var response map[string]interface{}
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify mock was called correctly
|
|
||||||
mockService.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMasterHandler_ListInstitutions_WithoutSearch(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
mockService := new(MockMasterService)
|
|
||||||
handler := NewMasterHandler(mockService)
|
|
||||||
|
|
||||||
// Test data
|
|
||||||
expectedResponse := &contract.ListInstitutionsResponse{
|
|
||||||
Institutions: []contract.InstitutionResponse{
|
|
||||||
{
|
|
||||||
ID: "123",
|
|
||||||
Name: "Test Institution",
|
|
||||||
Type: "company",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup mock expectations
|
|
||||||
mockService.On("ListInstitutions", mock.Anything, &contract.ListInstitutionsRequest{
|
|
||||||
Search: nil,
|
|
||||||
}).Return(expectedResponse, nil)
|
|
||||||
|
|
||||||
// Create request
|
|
||||||
req, _ := http.NewRequest("GET", "/institutions", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Create gin context
|
|
||||||
c, _ := gin.CreateTestContext(w)
|
|
||||||
c.Request = req
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
handler.ListInstitutions(c)
|
|
||||||
|
|
||||||
// Assertions
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
|
||||||
|
|
||||||
var response map[string]interface{}
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify mock was called correctly
|
|
||||||
mockService.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
230
internal/handler/notification_handler.go
Normal file
230
internal/handler/notification_handler.go
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -35,3 +35,28 @@ func (p *ActivityLogProcessorImpl) Log(ctx context.Context, letterID uuid.UUID,
|
|||||||
}
|
}
|
||||||
return p.repo.Create(ctx, entry)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
112
internal/processor/cached_user_processor.go
Normal file
112
internal/processor/cached_user_processor.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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) *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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
internal/processor/cached_user_wrapper.go
Normal file
33
internal/processor/cached_user_wrapper.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
180
internal/processor/letter_disposition_department_processor.go
Normal file
180
internal/processor/letter_disposition_department_processor.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
240
internal/processor/letter_disposition_processor.go
Normal file
240
internal/processor/letter_disposition_processor.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,6 +43,13 @@ type LetterOutgoingProcessor interface {
|
|||||||
// GetOutgoingLetterWithDetails fetches letter with all related data
|
// GetOutgoingLetterWithDetails fetches letter with all related data
|
||||||
GetOutgoingLetterWithDetails(ctx context.Context, letterID uuid.UUID) (*entities.LetterOutgoing, error)
|
GetOutgoingLetterWithDetails(ctx context.Context, letterID uuid.UUID) (*entities.LetterOutgoing, error)
|
||||||
GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error)
|
GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error)
|
||||||
|
BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error)
|
||||||
|
|
||||||
|
// Batch loading methods for efficient querying
|
||||||
|
GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error)
|
||||||
|
GetBatchRecipients(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, error)
|
||||||
|
GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error)
|
||||||
|
GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LetterOutgoingProcessorImpl struct {
|
type LetterOutgoingProcessorImpl struct {
|
||||||
@ -57,6 +64,8 @@ type LetterOutgoingProcessorImpl struct {
|
|||||||
approvalRepo *repository.LetterOutgoingApprovalRepository
|
approvalRepo *repository.LetterOutgoingApprovalRepository
|
||||||
numberGenerator *LetterNumberGeneratorImpl
|
numberGenerator *LetterNumberGeneratorImpl
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
|
priorityRepo *repository.PriorityRepository
|
||||||
|
institutionRepo *repository.InstitutionRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLetterOutgoingProcessor(
|
func NewLetterOutgoingProcessor(
|
||||||
@ -71,6 +80,8 @@ func NewLetterOutgoingProcessor(
|
|||||||
approvalRepo *repository.LetterOutgoingApprovalRepository,
|
approvalRepo *repository.LetterOutgoingApprovalRepository,
|
||||||
numberGenerator *LetterNumberGeneratorImpl,
|
numberGenerator *LetterNumberGeneratorImpl,
|
||||||
txManager *repository.TxManager,
|
txManager *repository.TxManager,
|
||||||
|
priorityRepo *repository.PriorityRepository,
|
||||||
|
institutionRepo *repository.InstitutionRepository,
|
||||||
) *LetterOutgoingProcessorImpl {
|
) *LetterOutgoingProcessorImpl {
|
||||||
return &LetterOutgoingProcessorImpl{
|
return &LetterOutgoingProcessorImpl{
|
||||||
db: db,
|
db: db,
|
||||||
@ -84,6 +95,8 @@ func NewLetterOutgoingProcessor(
|
|||||||
approvalRepo: approvalRepo,
|
approvalRepo: approvalRepo,
|
||||||
numberGenerator: numberGenerator,
|
numberGenerator: numberGenerator,
|
||||||
txManager: txManager,
|
txManager: txManager,
|
||||||
|
priorityRepo: priorityRepo,
|
||||||
|
institutionRepo: institutionRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,3 +779,39 @@ func (p *LetterOutgoingProcessorImpl) GetUsersByIDs(ctx context.Context, userIDs
|
|||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *LetterOutgoingProcessorImpl) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
||||||
|
return p.letterRepo.BulkArchive(ctx, letterIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBatchAttachments fetches attachments for multiple letters in a single query
|
||||||
|
func (p *LetterOutgoingProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error) {
|
||||||
|
if p.attachmentRepo == nil || len(letterIDs) == 0 {
|
||||||
|
return make(map[uuid.UUID][]entities.LetterOutgoingAttachment), nil
|
||||||
|
}
|
||||||
|
return p.attachmentRepo.ListByLetterIDs(ctx, letterIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBatchRecipients fetches recipients for multiple letters in a single query
|
||||||
|
func (p *LetterOutgoingProcessorImpl) GetBatchRecipients(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, error) {
|
||||||
|
if p.recipientRepo == nil || len(letterIDs) == 0 {
|
||||||
|
return make(map[uuid.UUID][]entities.LetterOutgoingRecipient), nil
|
||||||
|
}
|
||||||
|
return p.recipientRepo.ListByLetterIDs(ctx, letterIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBatchPriorities fetches priorities by IDs in a single query
|
||||||
|
func (p *LetterOutgoingProcessorImpl) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBatchInstitutions fetches institutions by IDs in a single query
|
||||||
|
func (p *LetterOutgoingProcessorImpl) 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)
|
||||||
|
}
|
||||||
|
|||||||
@ -25,34 +25,42 @@ type LetterProcessorImpl struct {
|
|||||||
discussionRepo *repository.LetterDiscussionRepository
|
discussionRepo *repository.LetterDiscussionRepository
|
||||||
settingRepo *repository.AppSettingRepository
|
settingRepo *repository.AppSettingRepository
|
||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
|
outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository
|
||||||
departmentRepo *repository.DepartmentRepository
|
departmentRepo *repository.DepartmentRepository
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
priorityRepo *repository.PriorityRepository
|
priorityRepo *repository.PriorityRepository
|
||||||
institutionRepo *repository.InstitutionRepository
|
institutionRepo *repository.InstitutionRepository
|
||||||
dispActionRepo *repository.DispositionActionRepository
|
dispActionRepo *repository.DispositionActionRepository
|
||||||
|
dispoRoutes *repository.DispositionRouteRepository
|
||||||
numberGenerator *LetterNumberGeneratorImpl
|
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, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository, priorityRepo *repository.PriorityRepository, institutionRepo *repository.InstitutionRepository, dispActionRepo *repository.DispositionActionRepository, numberGenerator *LetterNumberGeneratorImpl) *LetterProcessorImpl {
|
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,
|
||||||
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo, institutionRepo: institutionRepo, dispActionRepo: dispActionRepo, numberGenerator: numberGenerator}
|
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) {
|
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
var result *contract.IncomingLetterResponse
|
userID := appcontext.FromGinContext(ctx).UserID
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
|
||||||
|
|
||||||
letterNumber, err := p.numberGenerator.GenerateNumber(
|
|
||||||
txCtx,
|
|
||||||
contract.SettingIncomingLetterPrefix,
|
|
||||||
contract.SettingIncomingLetterSequence,
|
|
||||||
"ESLI",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
entity := &entities.LetterIncoming{
|
entity := &entities.LetterIncoming{
|
||||||
|
LetterNumber: req.LetterNumber,
|
||||||
ReferenceNumber: req.ReferenceNumber,
|
ReferenceNumber: req.ReferenceNumber,
|
||||||
Subject: req.Subject,
|
Subject: req.Subject,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
@ -63,103 +71,22 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
|
|||||||
Status: entities.LetterIncomingStatusNew,
|
Status: entities.LetterIncomingStatusNew,
|
||||||
CreatedBy: userID,
|
CreatedBy: userID,
|
||||||
}
|
}
|
||||||
entity.LetterNumber = letterNumber
|
|
||||||
if err := p.letterRepo.Create(txCtx, entity); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultDeptCodes := []string{}
|
if err := p.letterRepo.Create(ctx, entity); err != nil {
|
||||||
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterRecipients); err == nil {
|
|
||||||
if arr, ok := s.Value["department_codes"].([]interface{}); ok {
|
|
||||||
for _, it := range arr {
|
|
||||||
if str, ok := it.(string); ok {
|
|
||||||
defaultDeptCodes = append(defaultDeptCodes, str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes))
|
|
||||||
for _, code := range defaultDeptCodes {
|
|
||||||
dep, err := p.departmentRepo.GetByCode(txCtx, code)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
depIDs = append(depIDs, dep.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
|
|
||||||
var recipients []entities.LetterIncomingRecipient
|
|
||||||
|
|
||||||
mapsUsers := map[string]bool{}
|
|
||||||
for _, row := range userMemberships {
|
|
||||||
uid := row.UserID
|
|
||||||
if _, ok := mapsUsers[uid.String()]; !ok {
|
|
||||||
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, RecipientDepartmentID: &row.DepartmentID, Status: entities.RecipientStatusNew})
|
|
||||||
}
|
|
||||||
mapsUsers[uid.String()] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(recipients) > 0 {
|
|
||||||
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.activity != nil {
|
|
||||||
action := "letter.created"
|
|
||||||
if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{"letter_number": letterNumber}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments))
|
|
||||||
for _, a := range req.Attachments {
|
|
||||||
attachments = append(attachments, entities.LetterIncomingAttachment{LetterID: entity.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID})
|
|
||||||
}
|
|
||||||
if len(attachments) > 0 {
|
|
||||||
if err := p.attachRepo.CreateBulk(txCtx, attachments); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if p.activity != nil {
|
|
||||||
action := "attachment.uploaded"
|
|
||||||
for _, a := range attachments {
|
|
||||||
ctxMap := map[string]interface{}{"file_name": a.FileName, "file_type": a.FileType}
|
|
||||||
if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, ctxMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID)
|
|
||||||
var pr *entities.Priority
|
|
||||||
if entity.PriorityID != nil {
|
|
||||||
if p.priorityRepo != nil {
|
|
||||||
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
|
|
||||||
pr = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var inst *entities.Institution
|
|
||||||
if entity.SenderInstitutionID != nil {
|
|
||||||
if p.institutionRepo != nil {
|
|
||||||
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
|
|
||||||
inst = got
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = transformer.LetterEntityToContract(entity, savedAttachments, pr, inst)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return result, nil
|
|
||||||
|
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) {
|
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)
|
entity, err := p.letterRepo.Get(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -177,45 +104,128 @@ func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid
|
|||||||
inst = got
|
inst = got
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return transformer.LetterEntityToContract(entity, atts, pr, inst), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
|
// Check if letter is read by current user
|
||||||
page, limit := req.Page, req.Limit
|
isRead := false
|
||||||
|
if p.recipientRepo != nil {
|
||||||
filter := repository.ListIncomingLettersFilter{
|
if recipient, err := p.recipientRepo.GetByLetterAndUser(ctx, id, userID); err == nil {
|
||||||
Status: req.Status,
|
isRead = recipient.ReadAt != nil
|
||||||
Query: req.Query,
|
|
||||||
DepartmentID: req.DepartmentID,
|
|
||||||
}
|
|
||||||
|
|
||||||
list, total, err := p.letterRepo.List(ctx, filter, limit, (page-1)*limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
respList := make([]contract.IncomingLetterResponse, 0, len(list))
|
|
||||||
|
|
||||||
for _, e := range list {
|
|
||||||
atts, _ := p.attachRepo.ListByLetter(ctx, e.ID)
|
|
||||||
var pr *entities.Priority
|
|
||||||
if e.PriorityID != nil && p.priorityRepo != nil {
|
|
||||||
if got, err := p.priorityRepo.Get(ctx, *e.PriorityID); err == nil {
|
|
||||||
pr = got
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var inst *entities.Institution
|
resp := transformer.LetterEntityToContract(entity, atts, pr, inst)
|
||||||
if e.SenderInstitutionID != nil && p.institutionRepo != nil {
|
resp.IsRead = isRead
|
||||||
if got, err := p.institutionRepo.Get(ctx, *e.SenderInstitutionID); err == nil {
|
|
||||||
inst = got
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := transformer.LetterEntityToContract(&e, atts, pr, inst)
|
outgoingUnread := 0
|
||||||
respList = append(respList, *resp)
|
if p.outgoingRecipientRepo != nil {
|
||||||
|
if count, err := p.outgoingRecipientRepo.CountUnreadByUser(ctx, userID); err == nil {
|
||||||
|
outgoingUnread = count
|
||||||
}
|
}
|
||||||
return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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) 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) {
|
func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
@ -304,6 +314,18 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
|
|||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
|
||||||
|
existingDispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(txCtx, req.LetterID, req.FromDepartment)
|
||||||
|
if err == nil && len(existingDispDepts) > 0 {
|
||||||
|
for _, existingDispDept := range existingDispDepts {
|
||||||
|
if existingDispDept.Status == entities.DispositionDepartmentStatusPending {
|
||||||
|
existingDispDept.Status = entities.DispositionDepartmentStatusDispositioned
|
||||||
|
if err := p.dispositionDeptRepo.Update(txCtx, &existingDispDept); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disp := entities.LetterIncomingDisposition{
|
disp := entities.LetterIncomingDisposition{
|
||||||
LetterID: req.LetterID,
|
LetterID: req.LetterID,
|
||||||
DepartmentID: &req.FromDepartment,
|
DepartmentID: &req.FromDepartment,
|
||||||
@ -318,7 +340,9 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
|
|||||||
for _, toDept := range req.ToDepartmentIDs {
|
for _, toDept := range req.ToDepartmentIDs {
|
||||||
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
||||||
LetterIncomingDispositionID: disp.ID,
|
LetterIncomingDispositionID: disp.ID,
|
||||||
|
LetterIncomingID: req.LetterID,
|
||||||
DepartmentID: toDept,
|
DepartmentID: toDept,
|
||||||
|
Status: entities.DispositionDepartmentStatusPending,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,3 +511,57 @@ func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uui
|
|||||||
}
|
}
|
||||||
return out, nil
|
return out, 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) buildLetterResponse(ctx context.Context, entity *entities.LetterIncoming) (*contract.IncomingLetterResponse, error) {
|
||||||
|
savedAttachments, _ := p.attachRepo.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, pr, inst), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
||||||
|
return p.letterRepo.BulkArchive(ctx, letterIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|||||||
251
internal/processor/letter_processor_status.go
Normal file
251
internal/processor/letter_processor_status.go
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) {
|
||||||
|
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, req.LetterIncomingID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response []contract.DepartmentDispositionStatusResponse
|
||||||
|
for _, disp := range dispositions {
|
||||||
|
letterResp := transformer.LetterIncomingEntityToContract(disp.LetterIncoming)
|
||||||
|
|
||||||
|
var fromDept *contract.DepartmentResponse
|
||||||
|
if disp.LetterIncomingDisposition != nil && disp.LetterIncomingDisposition.DepartmentID != nil {
|
||||||
|
fromDept = transformer.DepartmentEntityToContract(&disp.LetterIncomingDisposition.Department)
|
||||||
|
}
|
||||||
|
|
||||||
|
response = append(response, contract.DepartmentDispositionStatusResponse{
|
||||||
|
ID: disp.ID,
|
||||||
|
LetterID: disp.LetterIncomingID,
|
||||||
|
Letter: letterResp,
|
||||||
|
FromDepartmentID: disp.LetterIncomingDisposition.DepartmentID,
|
||||||
|
FromDepartment: fromDept,
|
||||||
|
ToDepartmentID: disp.DepartmentID,
|
||||||
|
ToDepartment: transformer.DepartmentEntityToContract(disp.Department),
|
||||||
|
Status: string(disp.Status),
|
||||||
|
Notes: disp.LetterIncomingDisposition.Notes,
|
||||||
|
ReadAt: disp.ReadAt,
|
||||||
|
CompletedAt: disp.CompletedAt,
|
||||||
|
CreatedAt: disp.CreatedAt,
|
||||||
|
UpdatedAt: disp.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.ListDepartmentDispositionStatusResponse{
|
||||||
|
Dispositions: response,
|
||||||
|
Pagination: contract.PaginationResponse{
|
||||||
|
TotalCount: len(response),
|
||||||
|
Page: 1,
|
||||||
|
Limit: len(response),
|
||||||
|
TotalPages: 1,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) {
|
||||||
|
var result *contract.DepartmentDispositionStatusResponse
|
||||||
|
|
||||||
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
departmentID := appcontext.FromGinContext(txCtx).DepartmentID
|
||||||
|
|
||||||
|
dispDept, err := p.dispositionDeptRepo.GetByDispositionAndDepartment(txCtx, req.LetterIncomingID, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
notes := ""
|
||||||
|
if req.Notes != nil {
|
||||||
|
notes = *req.Notes
|
||||||
|
}
|
||||||
|
if err := p.updateDispositionDepartmentStatus(txCtx, dispDept.ID, req.Status, notes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.activity.LogLetterDispositionStatusUpdate(txCtx, req.LetterIncomingID, userID, req.Status)
|
||||||
|
|
||||||
|
if err := p.checkAndUpdateLetterCompletionStatus(txCtx, req.LetterIncomingID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedDispDept, err := p.dispositionDeptRepo.GetByID(txCtx, dispDept.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result = p.buildDispositionStatusResponse(updatedDispDept)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateDispositionDepartmentStatus updates the status of a disposition department
|
||||||
|
func (p *LetterProcessorImpl) updateDispositionDepartmentStatus(ctx context.Context, dispDeptID uuid.UUID, status, notes string) error {
|
||||||
|
now := time.Now()
|
||||||
|
var dispositionStatus entities.LetterIncomingDispositionDepartmentStatus
|
||||||
|
var readAt, completedAt *time.Time
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "completed":
|
||||||
|
dispositionStatus = entities.DispositionDepartmentStatusCompleted
|
||||||
|
completedAt = &now
|
||||||
|
readAt = &now // Mark as read when completing
|
||||||
|
case "read":
|
||||||
|
dispositionStatus = entities.DispositionDepartmentStatusRead
|
||||||
|
readAt = &now
|
||||||
|
default:
|
||||||
|
dispositionStatus = entities.DispositionDepartmentStatusPending
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.dispositionDeptRepo.UpdateStatus(ctx, dispDeptID, dispositionStatus, notes, readAt, completedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDispositionNoteIfProvided adds a note to the disposition if provided
|
||||||
|
func (p *LetterProcessorImpl) addDispositionNoteIfProvided(ctx context.Context, dispositionID uuid.UUID, userID uuid.UUID, notes *string) error {
|
||||||
|
if notes == nil || *notes == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
note := &entities.DispositionNote{
|
||||||
|
DispositionID: dispositionID,
|
||||||
|
UserID: &userID,
|
||||||
|
Note: *notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.dispositionNoteRepo.Create(ctx, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) checkAndUpdateLetterCompletionStatus(ctx context.Context, letterIncomingID uuid.UUID) error {
|
||||||
|
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allCompleted := true
|
||||||
|
for _, disp := range dispositions {
|
||||||
|
if disp.Status == entities.DispositionDepartmentStatusPending {
|
||||||
|
allCompleted = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allCompleted && len(dispositions) > 0 {
|
||||||
|
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
letter.Status = "completed"
|
||||||
|
if err := p.letterRepo.Update(ctx, letter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDispositionStatusResponse builds the response for disposition status
|
||||||
|
func (p *LetterProcessorImpl) buildDispositionStatusResponse(dispDept *entities.LetterIncomingDispositionDepartment) *contract.DepartmentDispositionStatusResponse {
|
||||||
|
letterResp := transformer.LetterIncomingEntityToContract(dispDept.LetterIncoming)
|
||||||
|
|
||||||
|
var fromDept *contract.DepartmentResponse
|
||||||
|
if dispDept.LetterIncomingDisposition != nil && dispDept.LetterIncomingDisposition.DepartmentID != nil {
|
||||||
|
fromDept = transformer.DepartmentEntityToContract(&dispDept.LetterIncomingDisposition.Department)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.DepartmentDispositionStatusResponse{
|
||||||
|
ID: dispDept.ID,
|
||||||
|
LetterID: dispDept.LetterIncomingID,
|
||||||
|
Letter: letterResp,
|
||||||
|
FromDepartmentID: dispDept.LetterIncomingDisposition.DepartmentID,
|
||||||
|
FromDepartment: fromDept,
|
||||||
|
ToDepartmentID: dispDept.DepartmentID,
|
||||||
|
ToDepartment: transformer.DepartmentEntityToContract(dispDept.Department),
|
||||||
|
Status: string(dispDept.Status),
|
||||||
|
Notes: dispDept.LetterIncomingDisposition.Notes,
|
||||||
|
ReadAt: dispDept.ReadAt,
|
||||||
|
CompletedAt: dispDept.CompletedAt,
|
||||||
|
CreatedAt: dispDept.CreatedAt,
|
||||||
|
UpdatedAt: dispDept.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) GetLetterCTA(ctx context.Context, letterIncomingID uuid.UUID, departmentID uuid.UUID) (*contract.LetterCTAResponse, error) {
|
||||||
|
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &contract.LetterCTAResponse{
|
||||||
|
LetterIncomingID: letterIncomingID,
|
||||||
|
Actions: []contract.LetterCTAAction{},
|
||||||
|
Message: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
isEligibleForDispo, err := p.dispoRoutes.IsEligibleForDisposition(ctx, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status == "completed" || letter.Status == "archived" {
|
||||||
|
response.Message = "Letter is no longer accepting actions"
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(ctx, letterIncomingID, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dispDepts) == 0 {
|
||||||
|
response.Message = "Your department is not a recipient of this letter"
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dispDept := range dispDepts {
|
||||||
|
if dispDept.Status == entities.DispositionDepartmentStatusPending {
|
||||||
|
response.DispositionID = &dispDept.LetterIncomingDispositionID
|
||||||
|
currentStatus := string(dispDept.Status)
|
||||||
|
response.CurrentStatus = ¤tStatus
|
||||||
|
|
||||||
|
if isEligibleForDispo {
|
||||||
|
response.Actions = append(response.Actions, contract.LetterCTAAction{
|
||||||
|
Type: "create_disposition",
|
||||||
|
Label: "Disposisi",
|
||||||
|
Path: fmt.Sprintf("/api/v1/letters/%s/dispositions", letterIncomingID),
|
||||||
|
Method: "POST",
|
||||||
|
Description: "Create a new disposition for this letter",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Actions = append(response.Actions, contract.LetterCTAAction{
|
||||||
|
Type: "update_status",
|
||||||
|
Label: "Tindak Lanjut",
|
||||||
|
Path: fmt.Sprintf("/api/v1/letters/dispositions/%s/status", response.LetterIncomingID),
|
||||||
|
Method: "PUT",
|
||||||
|
Description: "Update the status of your disposition",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
361
internal/processor/notification_processor.go
Normal file
361
internal/processor/notification_processor.go
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"eslogad-be/internal/config"
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
novu "github.com/novuhq/go-novu/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationProcessor interface {
|
||||||
|
// User management
|
||||||
|
CreateSubscriber(ctx context.Context, user *entities.User) error
|
||||||
|
UpdateSubscriber(ctx context.Context, user *entities.User) error
|
||||||
|
DeleteSubscriber(ctx context.Context, userID uuid.UUID) error
|
||||||
|
CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error
|
||||||
|
BulkCreateSubscribers(ctx context.Context, users []*entities.User) error
|
||||||
|
|
||||||
|
// Letter notifications
|
||||||
|
SendIncomingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationProcessorImpl struct {
|
||||||
|
provider NotificationProvider
|
||||||
|
workflowID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationProcessor(provider NotificationProvider, workflowID string) *NotificationProcessorImpl {
|
||||||
|
return &NotificationProcessorImpl{
|
||||||
|
provider: provider,
|
||||||
|
workflowID: workflowID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProcessorImpl) CreateSubscriber(ctx context.Context, user *entities.User) error {
|
||||||
|
return p.provider.CreateSubscriber(ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProcessorImpl) UpdateSubscriber(ctx context.Context, user *entities.User) error {
|
||||||
|
return p.provider.UpdateSubscriber(ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProcessorImpl) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error {
|
||||||
|
return p.provider.DeleteSubscriber(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProcessorImpl) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error {
|
||||||
|
return p.provider.CreateSubscriberFromContract(ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProcessorImpl) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error {
|
||||||
|
return p.provider.BulkCreateSubscribers(ctx, users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProcessorImpl) SendIncomingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error {
|
||||||
|
// Ensure subscriber exists
|
||||||
|
if err := p.provider.EnsureSubscriberExists(ctx, recipientUserID); err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure subscriber exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build notification URL
|
||||||
|
url := fmt.Sprintf("/en/apps/surat-menyurat/masuk-detail/%s", letterID.String())
|
||||||
|
|
||||||
|
// Use workflow ID from config (defaults to "notification-dashbpard")
|
||||||
|
workflowID := p.workflowID
|
||||||
|
if workflowID == "" {
|
||||||
|
workflowID = "notification-dashbpard"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
return p.provider.SendNotification(ctx, NotificationPayload{
|
||||||
|
RecipientID: recipientUserID,
|
||||||
|
EventName: workflowID,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"subject": subject,
|
||||||
|
"body": body,
|
||||||
|
"url": url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationProvider interface for different notification services
|
||||||
|
type NotificationProvider interface {
|
||||||
|
// User management
|
||||||
|
CreateSubscriber(ctx context.Context, user *entities.User) error
|
||||||
|
UpdateSubscriber(ctx context.Context, user *entities.User) error
|
||||||
|
DeleteSubscriber(ctx context.Context, userID uuid.UUID) error
|
||||||
|
CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error
|
||||||
|
BulkCreateSubscribers(ctx context.Context, users []*entities.User) error
|
||||||
|
|
||||||
|
// Core notification methods
|
||||||
|
EnsureSubscriberExists(ctx context.Context, userID uuid.UUID) error
|
||||||
|
SendNotification(ctx context.Context, payload NotificationPayload) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationPayload struct {
|
||||||
|
RecipientID uuid.UUID
|
||||||
|
EventName string
|
||||||
|
Data map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NovuProvider implements NotificationProvider using Novu
|
||||||
|
type NovuProvider struct {
|
||||||
|
client *novu.APIClient
|
||||||
|
config *config.NovuConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNovuProvider(cfg *config.NovuConfig) *NovuProvider {
|
||||||
|
if cfg.APIKey == "" {
|
||||||
|
return &NovuProvider{
|
||||||
|
client: nil,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Novu config with backend URL
|
||||||
|
novuConfig := &novu.Config{}
|
||||||
|
if cfg.BaseURL != "" {
|
||||||
|
backendURL, err := url.Parse(cfg.BaseURL)
|
||||||
|
if err == nil {
|
||||||
|
novuConfig.BackendURL = backendURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := novu.NewAPIClient(cfg.APIKey, novuConfig)
|
||||||
|
|
||||||
|
return &NovuProvider{
|
||||||
|
client: client,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProvider) CreateSubscriber(ctx context.Context, user *entities.User) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := user.ID.String()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"createdAt": user.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Departments != nil && len(user.Departments) > 0 {
|
||||||
|
depts := make([]map[string]interface{}, len(user.Departments))
|
||||||
|
for i, dept := range user.Departments {
|
||||||
|
depts[i] = map[string]interface{}{
|
||||||
|
"id": dept.ID.String(),
|
||||||
|
"name": dept.Name,
|
||||||
|
"code": dept.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["departments"] = depts
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProvider) UpdateSubscriber(ctx context.Context, user *entities.User) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := user.ID.String()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"updatedAt": user.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Departments != nil && len(user.Departments) > 0 {
|
||||||
|
depts := make([]map[string]interface{}, len(user.Departments))
|
||||||
|
for i, dept := range user.Departments {
|
||||||
|
depts[i] = map[string]interface{}{
|
||||||
|
"id": dept.ID.String(),
|
||||||
|
"name": dept.Name,
|
||||||
|
"code": dept.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["departments"] = depts
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Update(ctx, subscriberID, updateData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProvider) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := userID.String()
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Delete(ctx, subscriberID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProvider) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := user.ID.String()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"createdAt": user.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Roles != nil && len(user.Roles) > 0 {
|
||||||
|
roles := make([]map[string]interface{}, len(user.Roles))
|
||||||
|
for i, role := range user.Roles {
|
||||||
|
roles[i] = map[string]interface{}{
|
||||||
|
"id": role.ID.String(),
|
||||||
|
"name": role.Name,
|
||||||
|
"code": role.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["roles"] = roles
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 {
|
||||||
|
depts := make([]map[string]interface{}, len(user.DepartmentResponse))
|
||||||
|
for i, dept := range user.DepartmentResponse {
|
||||||
|
depts[i] = map[string]interface{}{
|
||||||
|
"id": dept.ID.String(),
|
||||||
|
"name": dept.Name,
|
||||||
|
"code": dept.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["departments"] = depts
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create subscriber from contract: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProvider) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
err := p.CreateSubscriber(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil && successCount == 0 {
|
||||||
|
return fmt.Errorf("failed to create any subscribers, last error: %w", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
return fmt.Errorf("created %d out of %d subscribers, last error: %w", successCount, len(users), lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProvider) EnsureSubscriberExists(ctx context.Context, userID uuid.UUID) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := userID.String()
|
||||||
|
|
||||||
|
// Check if subscriber exists
|
||||||
|
_, err := p.client.SubscriberApi.Get(ctx, subscriberID)
|
||||||
|
if err != nil {
|
||||||
|
// Subscriber doesn't exist, create a basic one
|
||||||
|
subscriber := novu.SubscriberPayload{
|
||||||
|
Email: fmt.Sprintf("%s@placeholder.com", subscriberID),
|
||||||
|
}
|
||||||
|
_, err = p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure subscriber exists: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProvider) SendNotification(ctx context.Context, payload NotificationPayload) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerPayload := novu.ITriggerPayloadOptions{
|
||||||
|
To: payload.RecipientID.String(),
|
||||||
|
Payload: payload.Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.EventApi.Trigger(ctx, payload.EventName, triggerPayload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
280
internal/processor/novu_processor.go
Normal file
280
internal/processor/novu_processor.go
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"eslogad-be/internal/config"
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
novu "github.com/novuhq/go-novu/lib"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NovuProcessor interface {
|
||||||
|
CreateSubscriber(ctx context.Context, user *entities.User) error
|
||||||
|
UpdateSubscriber(ctx context.Context, user *entities.User) error
|
||||||
|
DeleteSubscriber(ctx context.Context, userID uuid.UUID) error
|
||||||
|
CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error
|
||||||
|
BulkCreateSubscribers(ctx context.Context, users []*entities.User) error
|
||||||
|
SendLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type NovuProcessorImpl struct {
|
||||||
|
client *novu.APIClient
|
||||||
|
config *config.NovuConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNovuProcessor(cfg *config.NovuConfig) *NovuProcessorImpl {
|
||||||
|
if cfg.APIKey == "" {
|
||||||
|
return &NovuProcessorImpl{
|
||||||
|
client: nil,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Novu config with backend URL
|
||||||
|
novuConfig := &novu.Config{}
|
||||||
|
if cfg.BaseURL != "" {
|
||||||
|
backendURL, err := url.Parse(cfg.BaseURL)
|
||||||
|
if err == nil {
|
||||||
|
novuConfig.BackendURL = backendURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := novu.NewAPIClient(cfg.APIKey, novuConfig)
|
||||||
|
|
||||||
|
return &NovuProcessorImpl{
|
||||||
|
client: client,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProcessorImpl) CreateSubscriber(ctx context.Context, user *entities.User) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := user.ID.String()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"createdAt": user.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Departments != nil && len(user.Departments) > 0 {
|
||||||
|
depts := make([]map[string]interface{}, len(user.Departments))
|
||||||
|
for i, dept := range user.Departments {
|
||||||
|
depts[i] = map[string]interface{}{
|
||||||
|
"id": dept.ID.String(),
|
||||||
|
"name": dept.Name,
|
||||||
|
"code": dept.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["departments"] = depts
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProcessorImpl) UpdateSubscriber(ctx context.Context, user *entities.User) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := user.ID.String()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"updatedAt": user.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Departments != nil && len(user.Departments) > 0 {
|
||||||
|
depts := make([]map[string]interface{}, len(user.Departments))
|
||||||
|
for i, dept := range user.Departments {
|
||||||
|
depts[i] = map[string]interface{}{
|
||||||
|
"id": dept.ID.String(),
|
||||||
|
"name": dept.Name,
|
||||||
|
"code": dept.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["departments"] = depts
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Update(ctx, subscriberID, updateData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProcessorImpl) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := userID.String()
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Delete(ctx, subscriberID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProcessorImpl) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := user.ID.String()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"createdAt": user.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Roles != nil && len(user.Roles) > 0 {
|
||||||
|
roles := make([]map[string]interface{}, len(user.Roles))
|
||||||
|
for i, role := range user.Roles {
|
||||||
|
roles[i] = map[string]interface{}{
|
||||||
|
"id": role.ID.String(),
|
||||||
|
"name": role.Name,
|
||||||
|
"code": role.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["roles"] = roles
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 {
|
||||||
|
depts := make([]map[string]interface{}, len(user.DepartmentResponse))
|
||||||
|
for i, dept := range user.DepartmentResponse {
|
||||||
|
depts[i] = map[string]interface{}{
|
||||||
|
"id": dept.ID.String(),
|
||||||
|
"name": dept.Name,
|
||||||
|
"code": dept.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["departments"] = depts
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create subscriber from contract: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProcessorImpl) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
err := p.CreateSubscriber(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil && successCount == 0 {
|
||||||
|
return fmt.Errorf("failed to create any subscribers, last error: %w", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
return fmt.Errorf("created %d out of %d subscribers, last error: %w", successCount, len(users), lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NovuProcessorImpl) SendLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("novu client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := recipientUserID.String()
|
||||||
|
|
||||||
|
// Check if subscriber exists, create if not
|
||||||
|
_, err := p.client.SubscriberApi.Get(ctx, subscriberID)
|
||||||
|
if err != nil {
|
||||||
|
// Subscriber doesn't exist, create a basic one
|
||||||
|
subscriber := novu.SubscriberPayload{
|
||||||
|
Email: fmt.Sprintf("%s@placeholder.com", subscriberID),
|
||||||
|
}
|
||||||
|
_, err = p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure subscriber exists: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare notification payload
|
||||||
|
url := fmt.Sprintf("en/apps/surat-menyurat/masuk-detail/%s", letterID.String())
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"subject": subject,
|
||||||
|
"body": body,
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the notification
|
||||||
|
triggerPayload := novu.ITriggerPayloadOptions{
|
||||||
|
To: subscriberID,
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.client.EventApi.Trigger(ctx, "notification-dashboard", triggerPayload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send letter notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
96
internal/processor/recipient_processor.go
Normal file
96
internal/processor/recipient_processor.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecipientProcessor interface {
|
||||||
|
CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error)
|
||||||
|
CreateRecipients(ctx context.Context, letterID uuid.UUID, departmentIDs []uuid.UUID) ([]entities.LetterIncomingRecipient, error)
|
||||||
|
CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecipientProcessorImpl struct {
|
||||||
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
|
settingRepo *repository.AppSettingRepository
|
||||||
|
departmentRepo *repository.DepartmentRepository
|
||||||
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRecipientProcessor(
|
||||||
|
recipientRepo *repository.LetterIncomingRecipientRepository,
|
||||||
|
settingRepo *repository.AppSettingRepository,
|
||||||
|
departmentRepo *repository.DepartmentRepository,
|
||||||
|
userDeptRepo *repository.UserDepartmentRepository,
|
||||||
|
) *RecipientProcessorImpl {
|
||||||
|
return &RecipientProcessorImpl{
|
||||||
|
recipientRepo: recipientRepo,
|
||||||
|
settingRepo: settingRepo,
|
||||||
|
departmentRepo: departmentRepo,
|
||||||
|
userDeptRepo: userDeptRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RecipientProcessorImpl) CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
|
||||||
|
departmentIDs, err := p.settingRepo.GetDepartmentRecipients(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return []entities.LetterIncomingRecipient{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(departmentIDs) == 0 {
|
||||||
|
return []entities.LetterIncomingRecipient{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.CreateRecipients(ctx, letterID, departmentIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RecipientProcessorImpl) CreateRecipients(ctx context.Context, letterID uuid.UUID, departmentIDs []uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
|
||||||
|
if len(departmentIDs) == 0 {
|
||||||
|
return []entities.LetterIncomingRecipient{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userMemberships, err := p.userDeptRepo.ListActiveByDepartmentIDs(ctx, departmentIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients := p.buildUniqueRecipients(letterID, userMemberships)
|
||||||
|
|
||||||
|
if len(recipients) > 0 {
|
||||||
|
if err := p.recipientRepo.CreateBulk(ctx, recipients); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RecipientProcessorImpl) buildUniqueRecipients(letterID uuid.UUID, userMemberships []repository.UserDepartmentRow) []entities.LetterIncomingRecipient {
|
||||||
|
var recipients []entities.LetterIncomingRecipient
|
||||||
|
userMap := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, membership := range userMemberships {
|
||||||
|
userIDStr := membership.UserID.String()
|
||||||
|
|
||||||
|
if !userMap[userIDStr] {
|
||||||
|
recipients = append(recipients, entities.LetterIncomingRecipient{
|
||||||
|
LetterID: letterID,
|
||||||
|
RecipientUserID: &membership.UserID,
|
||||||
|
RecipientDepartmentID: &membership.DepartmentID,
|
||||||
|
Status: entities.RecipientStatusNew,
|
||||||
|
})
|
||||||
|
userMap[userIDStr] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RecipientProcessorImpl) CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error {
|
||||||
|
return p.recipientRepo.Create(ctx, recipient)
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ import (
|
|||||||
type UserProcessorImpl struct {
|
type UserProcessorImpl struct {
|
||||||
userRepo UserRepository
|
userRepo UserRepository
|
||||||
profileRepo UserProfileRepository
|
profileRepo UserProfileRepository
|
||||||
|
novuProcessor NovuProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserProfileRepository interface {
|
type UserProfileRepository interface {
|
||||||
@ -35,6 +36,10 @@ func NewUserProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *UserProcessorImpl) SetNovuProcessor(novuProcessor NovuProcessor) {
|
||||||
|
p.novuProcessor = novuProcessor
|
||||||
|
}
|
||||||
|
|
||||||
func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) {
|
func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) {
|
||||||
existingUser, err := p.userRepo.GetByEmail(ctx, req.Email)
|
existingUser, err := p.userRepo.GetByEmail(ctx, req.Email)
|
||||||
if err == nil && existingUser != nil {
|
if err == nil && existingUser != nil {
|
||||||
@ -65,6 +70,15 @@ func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.Create
|
|||||||
}
|
}
|
||||||
_ = p.profileRepo.Create(ctx, profile)
|
_ = p.profileRepo.Create(ctx, profile)
|
||||||
|
|
||||||
|
// Create Novu subscriber
|
||||||
|
if p.novuProcessor != nil {
|
||||||
|
if err := p.novuProcessor.CreateSubscriber(ctx, userEntity); err != nil {
|
||||||
|
// Log error but don't fail user creation
|
||||||
|
// You might want to add proper logging here
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return transformer.EntityToContract(userEntity), nil
|
return transformer.EntityToContract(userEntity), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,6 +102,13 @@ func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *c
|
|||||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Novu subscriber
|
||||||
|
if p.novuProcessor != nil {
|
||||||
|
if err := p.novuProcessor.UpdateSubscriber(ctx, updated); err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return transformer.EntityToContract(updated), nil
|
return transformer.EntityToContract(updated), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +123,14 @@ func (p *UserProcessorImpl) DeleteUser(ctx context.Context, id uuid.UUID) error
|
|||||||
return fmt.Errorf("failed to delete user: %w", err)
|
return fmt.Errorf("failed to delete user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete Novu subscriber
|
||||||
|
if p.novuProcessor != nil {
|
||||||
|
if err := p.novuProcessor.DeleteSubscriber(ctx, id); err != nil {
|
||||||
|
// Log error but don't fail user deletion
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +150,25 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByIDLight retrieves user without relationships - optimized for auth checks
|
||||||
|
func (p *UserProcessorImpl) GetUserByIDLight(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
|
||||||
|
user, err := p.userRepo.GetByIDLight(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &contract.UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.Email,
|
||||||
|
Name: user.Name,
|
||||||
|
IsActive: user.IsActive,
|
||||||
|
CreatedAt: user.CreatedAt,
|
||||||
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
|
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
|
||||||
user, err := p.userRepo.GetByEmail(ctx, email)
|
user, err := p.userRepo.GetByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
Create(ctx context.Context, user *entities.User) error
|
Create(ctx context.Context, user *entities.User) error
|
||||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error)
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error)
|
||||||
|
GetByIDLight(ctx context.Context, id uuid.UUID) (*entities.User, error)
|
||||||
GetByEmail(ctx context.Context, email string) (*entities.User, error)
|
GetByEmail(ctx context.Context, email string) (*entities.User, error)
|
||||||
GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error)
|
GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error)
|
||||||
GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)
|
GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)
|
||||||
|
|||||||
@ -177,14 +177,18 @@ func (r *AnalyticsRepository) GetLetterSummaryStats(ctx context.Context, startDa
|
|||||||
stats["total_archived"] = archivedCount
|
stats["total_archived"] = archivedCount
|
||||||
|
|
||||||
// Calculate average processing time
|
// Calculate average processing time
|
||||||
var avgProcessingTime float64
|
var avgProcessingTime *float64
|
||||||
db.Table("letters_outgoing").
|
db.Table("letters_outgoing").
|
||||||
Select("AVG(EXTRACT(EPOCH FROM (letters_outgoing.updated_at - letters_outgoing.created_at))/3600) as avg_hours").
|
Select("AVG(EXTRACT(EPOCH FROM (letters_outgoing.updated_at - letters_outgoing.created_at))/3600) as avg_hours").
|
||||||
Where("letters_outgoing.status IN ('approved', 'sent', 'archived')").
|
Where("letters_outgoing.status IN ('approved', 'sent', 'archived')").
|
||||||
Where("letters_outgoing.deleted_at IS NULL").
|
Where("letters_outgoing.deleted_at IS NULL").
|
||||||
Scan(&avgProcessingTime)
|
Scan(&avgProcessingTime)
|
||||||
|
|
||||||
stats["avg_processing_time"] = avgProcessingTime
|
if avgProcessingTime != nil {
|
||||||
|
stats["avg_processing_time"] = *avgProcessingTime
|
||||||
|
} else {
|
||||||
|
stats["avg_processing_time"] = float64(0)
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate completion rate
|
// Calculate completion rate
|
||||||
var completedCount int64
|
var completedCount int64
|
||||||
|
|||||||
@ -2,7 +2,11 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
"eslogad-be/internal/entities"
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,3 +30,33 @@ func (r *AppSettingRepository) Upsert(ctx context.Context, key string, value ent
|
|||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Exec("INSERT INTO app_settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP", key, value).Error
|
return db.WithContext(ctx).Exec("INSERT INTO app_settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP", key, value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AppSettingRepository) GetDepartmentRecipients(ctx context.Context) ([]uuid.UUID, error) {
|
||||||
|
setting, err := r.Get(ctx, contract.SettingIncomingLetterDepartmentRecipients)
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return []uuid.UUID{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.Marshal(setting.Value)
|
||||||
|
if err != nil {
|
||||||
|
return []uuid.UUID{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to unmarshal as the structured format first
|
||||||
|
var recipientSetting entities.DepartmentRecipientsSetting
|
||||||
|
if err := json.Unmarshal(jsonBytes, &recipientSetting); err == nil {
|
||||||
|
return recipientSetting.DepartmentIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If that fails, try to unmarshal as a direct array of UUIDs
|
||||||
|
var departmentIDs []uuid.UUID
|
||||||
|
if err := json.Unmarshal(jsonBytes, &departmentIDs); err == nil {
|
||||||
|
return departmentIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both fail, return empty array
|
||||||
|
return []uuid.UUID{}, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -19,10 +19,106 @@ func (r *DispositionRouteRepository) Create(ctx context.Context, e *entities.Dis
|
|||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Create(e).Error
|
return db.WithContext(ctx).Create(e).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upsert creates or updates a disposition route based on from_department_id and to_department_id
|
||||||
|
func (r *DispositionRouteRepository) Upsert(ctx context.Context, e *entities.DispositionRoute) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
|
||||||
|
// Check if route exists
|
||||||
|
var existing entities.DispositionRoute
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Where("from_department_id = ? AND to_department_id = ?", e.FromDepartmentID, e.ToDepartmentID).
|
||||||
|
First(&existing).Error
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// Create new route
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing route
|
||||||
|
e.ID = existing.ID
|
||||||
|
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).
|
||||||
|
Where("id = ?", existing.ID).
|
||||||
|
Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkUpsert performs bulk create or update for multiple routes
|
||||||
|
func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID uuid.UUID, toDeptIDs []uuid.UUID, isActive bool, allowedActions entities.JSONB) (created int, updated int, err error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
tx := db.WithContext(ctx).Begin()
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Get existing routes for this from_department_id
|
||||||
|
var existingRoutes []entities.DispositionRoute
|
||||||
|
if err = tx.Where("from_department_id = ?", fromDeptID).Find(&existingRoutes).Error; err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create map of existing routes
|
||||||
|
existingMap := make(map[uuid.UUID]entities.DispositionRoute)
|
||||||
|
for _, route := range existingRoutes {
|
||||||
|
existingMap[route.ToDepartmentID] = route
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each to_department_id
|
||||||
|
for _, toDeptID := range toDeptIDs {
|
||||||
|
route := entities.DispositionRoute{
|
||||||
|
FromDepartmentID: fromDeptID,
|
||||||
|
ToDepartmentID: toDeptID,
|
||||||
|
IsActive: isActive,
|
||||||
|
AllowedActions: allowedActions,
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing, exists := existingMap[toDeptID]; exists {
|
||||||
|
// Update existing route
|
||||||
|
route.ID = existing.ID
|
||||||
|
if err = tx.Model(&entities.DispositionRoute{}).
|
||||||
|
Where("id = ?", existing.ID).
|
||||||
|
Updates(&route).Error; err != nil {
|
||||||
|
return created, updated, err
|
||||||
|
}
|
||||||
|
updated++
|
||||||
|
// Remove from map to track which routes to delete
|
||||||
|
delete(existingMap, toDeptID)
|
||||||
|
} else {
|
||||||
|
// Create new route
|
||||||
|
if err = tx.Create(&route).Error; err != nil {
|
||||||
|
return created, updated, err
|
||||||
|
}
|
||||||
|
created++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally deactivate routes that are no longer in the list
|
||||||
|
// (routes that exist in DB but not in the new list)
|
||||||
|
for _, oldRoute := range existingMap {
|
||||||
|
if err = tx.Model(&entities.DispositionRoute{}).
|
||||||
|
Where("id = ?", oldRoute.ID).
|
||||||
|
Update("is_active", false).Error; err != nil {
|
||||||
|
return created, updated, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
if err = tx.Commit().Error; err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return created, updated, nil
|
||||||
|
}
|
||||||
func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error {
|
func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", e.ID).Updates(e).Error
|
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) {
|
func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var e entities.DispositionRoute
|
var e entities.DispositionRoute
|
||||||
@ -46,7 +142,71 @@ func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDep
|
|||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *DispositionRouteRepository) IsEligibleForDisposition(ctx context.Context, fromDept uuid.UUID) (bool, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.DispositionRoute
|
||||||
|
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).Find(&list).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(list) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
|
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
|
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAllGrouped returns all disposition routes grouped by from_department_id
|
||||||
|
func (r *DispositionRouteRepository) ListAllGrouped(ctx context.Context) (map[uuid.UUID][]uuid.UUID, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var routes []entities.DispositionRoute
|
||||||
|
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("is_active = ?", true).
|
||||||
|
Order("from_department_id, to_department_id").
|
||||||
|
Find(&routes).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by from_department_id
|
||||||
|
grouped := make(map[uuid.UUID][]uuid.UUID)
|
||||||
|
for _, route := range routes {
|
||||||
|
grouped[route.FromDepartmentID] = append(grouped[route.FromDepartmentID], route.ToDepartmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllGroupedWithDepartments returns all disposition routes grouped by from_department_id with department details
|
||||||
|
func (r *DispositionRouteRepository) ListAllGroupedWithDepartments(ctx context.Context) ([]entities.DispositionRoute, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var routes []entities.DispositionRoute
|
||||||
|
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("FromDepartment").
|
||||||
|
Preload("ToDepartment").
|
||||||
|
Where("is_active = ?", true).
|
||||||
|
Order("from_department_id, to_department_id").
|
||||||
|
Find(&routes).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll returns all disposition routes with department details
|
||||||
|
func (r *DispositionRouteRepository) ListAll(ctx context.Context) ([]entities.DispositionRoute, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var routes []entities.DispositionRoute
|
||||||
|
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("FromDepartment").
|
||||||
|
Preload("ToDepartment").
|
||||||
|
Where("is_active = ?", true).
|
||||||
|
Order("from_department_id, to_department_id").
|
||||||
|
Find(&routes).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -51,6 +51,15 @@ func (r *LetterOutgoingRepository) SoftDelete(ctx context.Context, id uuid.UUID)
|
|||||||
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", id).Update("deleted_at", now).Error
|
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", id).Update("deleted_at", now).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
result := db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterOutgoing{}).
|
||||||
|
Where("id IN ? AND deleted_at IS NULL", letterIDs).
|
||||||
|
Update("status", "archived")
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterOutgoingRepository) GetWithRelations(ctx context.Context, id uuid.UUID, relations []string) (*entities.LetterOutgoing, error) {
|
func (r *LetterOutgoingRepository) GetWithRelations(ctx context.Context, id uuid.UUID, relations []string) (*entities.LetterOutgoing, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id)
|
query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id)
|
||||||
@ -83,12 +92,22 @@ type ListOutgoingLettersFilter struct {
|
|||||||
PriorityID *uuid.UUID
|
PriorityID *uuid.UUID
|
||||||
SortBy *string
|
SortBy *string
|
||||||
SortOrder *string
|
SortOrder *string
|
||||||
|
IsArchived *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) {
|
func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
query := db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("deleted_at IS NULL")
|
query := db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("deleted_at IS NULL")
|
||||||
|
|
||||||
|
// Apply is_archived filter
|
||||||
|
if filter.IsArchived != nil {
|
||||||
|
if *filter.IsArchived {
|
||||||
|
query = query.Where("status = 'archived'")
|
||||||
|
} else {
|
||||||
|
query = query.Where("status != 'archived'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if filter.Status != nil {
|
if filter.Status != nil {
|
||||||
query = query.Where("status = ?", *filter.Status)
|
query = query.Where("status = ?", *filter.Status)
|
||||||
}
|
}
|
||||||
@ -207,6 +226,27 @@ func (r *LetterOutgoingAttachmentRepository) Delete(ctx context.Context, id uuid
|
|||||||
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingAttachment{}).Error
|
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingAttachment{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListByLetterIDs fetches attachments for multiple letters in a single query
|
||||||
|
func (r *LetterOutgoingAttachmentRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error) {
|
||||||
|
if len(letterIDs) == 0 {
|
||||||
|
return make(map[uuid.UUID][]entities.LetterOutgoingAttachment), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var attachments []entities.LetterOutgoingAttachment
|
||||||
|
if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).Order("uploaded_at ASC").Find(&attachments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group attachments by letter ID
|
||||||
|
result := make(map[uuid.UUID][]entities.LetterOutgoingAttachment)
|
||||||
|
for _, att := range attachments {
|
||||||
|
result[att.LetterID] = append(result[att.LetterID], att)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
type LetterOutgoingRecipientRepository struct{ db *gorm.DB }
|
type LetterOutgoingRecipientRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewLetterOutgoingRecipientRepository(db *gorm.DB) *LetterOutgoingRecipientRepository {
|
func NewLetterOutgoingRecipientRepository(db *gorm.DB) *LetterOutgoingRecipientRepository {
|
||||||
@ -226,6 +266,27 @@ func (r *LetterOutgoingRecipientRepository) CreateBulk(ctx context.Context, list
|
|||||||
return db.WithContext(ctx).Create(&list).Error
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var count int64
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterOutgoingRecipient{}).
|
||||||
|
Where("user_id = ? AND read_at IS NULL", userID).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(count), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) MarkAsRead(ctx context.Context, letterID, userID uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
now := time.Now()
|
||||||
|
return db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterOutgoingRecipient{}).
|
||||||
|
Where("letter_id = ? AND user_id = ?", letterID, userID).
|
||||||
|
Update("read_at", now).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterOutgoingRecipientRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingRecipient, error) {
|
func (r *LetterOutgoingRecipientRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingRecipient, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var list []entities.LetterOutgoingRecipient
|
var list []entities.LetterOutgoingRecipient
|
||||||
@ -255,6 +316,33 @@ func (r *LetterOutgoingRecipientRepository) DeleteByLetter(ctx context.Context,
|
|||||||
return db.WithContext(ctx).Where("letter_id = ?", letterID).Delete(&entities.LetterOutgoingRecipient{}).Error
|
return db.WithContext(ctx).Where("letter_id = ?", letterID).Delete(&entities.LetterOutgoingRecipient{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListByLetterIDs fetches recipients for multiple letters in a single query
|
||||||
|
func (r *LetterOutgoingRecipientRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, error) {
|
||||||
|
if len(letterIDs) == 0 {
|
||||||
|
return make(map[uuid.UUID][]entities.LetterOutgoingRecipient), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var recipients []entities.LetterOutgoingRecipient
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("User").
|
||||||
|
Preload("User.Profile").
|
||||||
|
Preload("Department").
|
||||||
|
Where("letter_id IN ?", letterIDs).
|
||||||
|
Order("is_primary DESC, created_at ASC").
|
||||||
|
Find(&recipients).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group recipients by letter ID
|
||||||
|
result := make(map[uuid.UUID][]entities.LetterOutgoingRecipient)
|
||||||
|
for _, rec := range recipients {
|
||||||
|
result[rec.LetterID] = append(result[rec.LetterID], rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
type LetterOutgoingDiscussionRepository struct{ db *gorm.DB }
|
type LetterOutgoingDiscussionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewLetterOutgoingDiscussionRepository(db *gorm.DB) *LetterOutgoingDiscussionRepository {
|
func NewLetterOutgoingDiscussionRepository(db *gorm.DB) *LetterOutgoingDiscussionRepository {
|
||||||
|
|||||||
@ -29,6 +29,10 @@ func (r *LetterIncomingRepository) Get(ctx context.Context, id uuid.UUID) (*enti
|
|||||||
return &e, nil
|
return &e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) {
|
||||||
|
return r.Get(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingRepository) Update(ctx context.Context, e *entities.LetterIncoming) error {
|
func (r *LetterIncomingRepository) Update(ctx context.Context, e *entities.LetterIncoming) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error
|
return db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error
|
||||||
@ -39,19 +43,95 @@ func (r *LetterIncomingRepository) SoftDelete(ctx context.Context, id uuid.UUID)
|
|||||||
return db.WithContext(ctx).Exec("UPDATE letters_incoming SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL", id).Error
|
return db.WithContext(ctx).Exec("UPDATE letters_incoming SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL", id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
// For incoming letters, we archive the recipients, not the letter itself
|
||||||
|
// The letter status remains as is (new, in_progress, or completed)
|
||||||
|
result := db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterIncomingRecipient{}).
|
||||||
|
Where("letter_id IN ?", letterIDs).
|
||||||
|
Update("is_archived", true)
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkArchiveForUser archives letters for a specific user only
|
||||||
|
func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
// Archive only the recipient records for the specific user
|
||||||
|
// Note: letter_incoming_recipients uses recipient_user_id column
|
||||||
|
result := db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterIncomingRecipient{}).
|
||||||
|
Where("letter_id IN ? AND recipient_user_id = ?", letterIDs, userID).
|
||||||
|
Update("is_archived", true)
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
type ListIncomingLettersFilter struct {
|
type ListIncomingLettersFilter struct {
|
||||||
Status *string
|
Status *string
|
||||||
Query *string
|
Query *string
|
||||||
DepartmentID *uuid.UUID
|
DepartmentID *uuid.UUID
|
||||||
|
UserID *uuid.UUID
|
||||||
|
IsRead *bool
|
||||||
|
PriorityIDs []uuid.UUID
|
||||||
|
IsDispositioned *bool
|
||||||
|
IsArchived *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncomingLettersFilter, limit, offset int) ([]entities.LetterIncoming, int64, error) {
|
func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncomingLettersFilter, limit, offset int) ([]entities.LetterIncoming, int64, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
|
query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
|
||||||
|
|
||||||
|
joinedRecipients := false
|
||||||
|
needsGroupBy := false
|
||||||
|
|
||||||
if filter.DepartmentID != nil {
|
if filter.DepartmentID != nil {
|
||||||
query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
|
query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
|
||||||
Where("letter_incoming_recipients.recipient_department_id = ?", *filter.DepartmentID)
|
Where("letter_incoming_recipients.recipient_department_id = ?", *filter.DepartmentID)
|
||||||
|
joinedRecipients = true
|
||||||
|
needsGroupBy = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply is_read filter if UserID is provided
|
||||||
|
if filter.UserID != nil && filter.IsRead != nil {
|
||||||
|
if !joinedRecipients {
|
||||||
|
query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id")
|
||||||
|
joinedRecipients = true
|
||||||
|
needsGroupBy = true
|
||||||
|
}
|
||||||
|
query = query.Where("letter_incoming_recipients.recipient_user_id = ?", *filter.UserID)
|
||||||
|
|
||||||
|
if *filter.IsRead {
|
||||||
|
query = query.Where("letter_incoming_recipients.read_at IS NOT NULL")
|
||||||
|
} else {
|
||||||
|
query = query.Where("letter_incoming_recipients.read_at IS NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply is_dispositioned filter if DepartmentID is provided
|
||||||
|
if filter.DepartmentID != nil && filter.IsDispositioned != nil {
|
||||||
|
query = query.Joins("LEFT JOIN letter_incoming_dispositions_department lidd ON lidd.letter_incoming_id = letters_incoming.id AND lidd.department_id = ?", *filter.DepartmentID)
|
||||||
|
|
||||||
|
if *filter.IsDispositioned {
|
||||||
|
// Has been dispositioned (status is not 'pending' or record exists with non-pending status)
|
||||||
|
query = query.Where("lidd.id IS NOT NULL AND lidd.status != 'pending'")
|
||||||
|
} else {
|
||||||
|
// Not yet dispositioned (no record or status is 'pending')
|
||||||
|
query = query.Where("lidd.id IS NULL OR lidd.status = 'pending'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply priority filter
|
||||||
|
if len(filter.PriorityIDs) > 0 {
|
||||||
|
query = query.Where("letters_incoming.priority_id IN ?", filter.PriorityIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply is_archived filter based on recipient's is_archived field
|
||||||
|
if filter.IsArchived != nil {
|
||||||
|
if *filter.IsArchived {
|
||||||
|
query = query.Where("letter_incoming_recipients.is_archived = ?", true)
|
||||||
|
} else {
|
||||||
|
query = query.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter.Status != nil {
|
if filter.Status != nil {
|
||||||
@ -61,12 +141,76 @@ func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncoming
|
|||||||
q := "%" + *filter.Query + "%"
|
q := "%" + *filter.Query + "%"
|
||||||
query = query.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ?", q, q)
|
query = query.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ?", q, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use GROUP BY instead of DISTINCT to handle joins properly
|
||||||
|
if needsGroupBy {
|
||||||
|
query = query.Group("letters_incoming.id")
|
||||||
|
}
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := query.Count(&total).Error; err != nil {
|
if err := query.Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For the actual data fetch, we need to select all columns
|
||||||
var list []entities.LetterIncoming
|
var list []entities.LetterIncoming
|
||||||
if err := query.Order("letters_incoming.created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
|
dataQuery := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
|
||||||
|
|
||||||
|
if filter.DepartmentID != nil {
|
||||||
|
dataQuery = dataQuery.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
|
||||||
|
Where("letter_incoming_recipients.recipient_department_id = ?", *filter.DepartmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.UserID != nil && filter.IsRead != nil {
|
||||||
|
if filter.DepartmentID == nil {
|
||||||
|
dataQuery = dataQuery.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id")
|
||||||
|
}
|
||||||
|
dataQuery = dataQuery.Where("letter_incoming_recipients.recipient_user_id = ?", *filter.UserID)
|
||||||
|
|
||||||
|
if *filter.IsRead {
|
||||||
|
dataQuery = dataQuery.Where("letter_incoming_recipients.read_at IS NOT NULL")
|
||||||
|
} else {
|
||||||
|
dataQuery = dataQuery.Where("letter_incoming_recipients.read_at IS NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.DepartmentID != nil && filter.IsDispositioned != nil {
|
||||||
|
dataQuery = dataQuery.Joins("LEFT JOIN letter_incoming_dispositions_department lidd ON lidd.letter_incoming_id = letters_incoming.id AND lidd.department_id = ?", *filter.DepartmentID)
|
||||||
|
|
||||||
|
if *filter.IsDispositioned {
|
||||||
|
dataQuery = dataQuery.Where("lidd.id IS NOT NULL AND lidd.status != 'pending'")
|
||||||
|
} else {
|
||||||
|
dataQuery = dataQuery.Where("lidd.id IS NULL OR lidd.status = 'pending'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.PriorityIDs) > 0 {
|
||||||
|
dataQuery = dataQuery.Where("letters_incoming.priority_id IN ?", filter.PriorityIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply is_archived filter based on recipient's is_archived field
|
||||||
|
if filter.IsArchived != nil {
|
||||||
|
if *filter.IsArchived {
|
||||||
|
dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ?", true)
|
||||||
|
} else {
|
||||||
|
dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Status != nil {
|
||||||
|
dataQuery = dataQuery.Where("letters_incoming.status = ?", *filter.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Query != nil {
|
||||||
|
q := "%" + *filter.Query + "%"
|
||||||
|
dataQuery = dataQuery.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ?", q, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsGroupBy {
|
||||||
|
dataQuery = dataQuery.Group("letters_incoming.id, letters_incoming.letter_number, letters_incoming.reference_number, letters_incoming.subject, letters_incoming.description, letters_incoming.priority_id, letters_incoming.sender_institution_id, letters_incoming.received_date, letters_incoming.due_date, letters_incoming.status, letters_incoming.created_by, letters_incoming.created_at, letters_incoming.updated_at, letters_incoming.deleted_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dataQuery.Order("letters_incoming.created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return list, total, nil
|
return list, total, nil
|
||||||
@ -91,6 +235,26 @@ func (r *LetterIncomingAttachmentRepository) ListByLetter(ctx context.Context, l
|
|||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingAttachmentRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) {
|
||||||
|
if len(letterIDs) == 0 {
|
||||||
|
return make(map[uuid.UUID][]entities.LetterIncomingAttachment), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var attachments []entities.LetterIncomingAttachment
|
||||||
|
if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).Order("uploaded_at ASC").Find(&attachments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group attachments by letter ID
|
||||||
|
result := make(map[uuid.UUID][]entities.LetterIncomingAttachment)
|
||||||
|
for _, att := range attachments {
|
||||||
|
result[att.LetterID] = append(result[att.LetterID], att)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
type LetterIncomingActivityLogRepository struct{ db *gorm.DB }
|
type LetterIncomingActivityLogRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewLetterIncomingActivityLogRepository(db *gorm.DB) *LetterIncomingActivityLogRepository {
|
func NewLetterIncomingActivityLogRepository(db *gorm.DB) *LetterIncomingActivityLogRepository {
|
||||||
@ -120,6 +284,21 @@ func (r *LetterIncomingDispositionRepository) Create(ctx context.Context, e *ent
|
|||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Create(e).Error
|
return db.WithContext(ctx).Create(e).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingDisposition, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterIncomingDisposition
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("Departments.Department").
|
||||||
|
Preload("ActionSelections.Action").
|
||||||
|
Preload("DispositionNotes.User").
|
||||||
|
First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
|
func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var list []entities.LetterIncomingDisposition
|
var list []entities.LetterIncomingDisposition
|
||||||
@ -136,15 +315,149 @@ func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context,
|
|||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionRepository) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingDisposition
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_id = ?", letterIncomingID).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("Departments.Department").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
type LetterIncomingDispositionDepartmentRepository struct{ db *gorm.DB }
|
type LetterIncomingDispositionDepartmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewLetterIncomingDispositionDepartmentRepository(db *gorm.DB) *LetterIncomingDispositionDepartmentRepository {
|
func NewLetterIncomingDispositionDepartmentRepository(db *gorm.DB) *LetterIncomingDispositionDepartmentRepository {
|
||||||
return &LetterIncomingDispositionDepartmentRepository{db: db}
|
return &LetterIncomingDispositionDepartmentRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) DB(ctx context.Context) *gorm.DB {
|
||||||
|
return DBFromContext(ctx, r.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) Create(ctx context.Context, e *entities.LetterIncomingDispositionDepartment) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingDispositionDepartmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingDispositionDepartment) error {
|
func (r *LetterIncomingDispositionDepartmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingDispositionDepartment) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Create(&list).Error
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) Update(ctx context.Context, e *entities.LetterIncomingDispositionDepartment) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Save(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingDispositionDepartment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterIncomingDispositionDepartment
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("LetterIncoming").
|
||||||
|
Preload("LetterIncomingDisposition").
|
||||||
|
First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) GetByDispositionAndDepartment(ctx context.Context, letterIncomingID, departmentID uuid.UUID) (*entities.LetterIncomingDispositionDepartment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterIncomingDispositionDepartment
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_incoming_id = ? AND department_id = ?", letterIncomingID, departmentID).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("LetterIncoming").
|
||||||
|
Preload("LetterIncomingDisposition").
|
||||||
|
First(&e).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) GetByLetterAndDepartment(ctx context.Context, letterID, departmentID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingDispositionDepartment
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_incoming_id = ? AND department_id = ?", letterID, departmentID).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("LetterIncoming").
|
||||||
|
Preload("LetterIncomingDisposition").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) ListByDepartmentWithPagination(ctx context.Context, departmentID uuid.UUID, status *string, offset, limit int) ([]entities.LetterIncomingDispositionDepartment, int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
query := db.WithContext(ctx).Where("department_id = ?", departmentID)
|
||||||
|
|
||||||
|
if status != nil && *status != "" {
|
||||||
|
query = query.Where("status = ?", *status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Model(&entities.LetterIncomingDispositionDepartment{}).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var list []entities.LetterIncomingDispositionDepartment
|
||||||
|
if err := query.
|
||||||
|
Preload("Department").
|
||||||
|
Preload("LetterIncoming").
|
||||||
|
Preload("LetterIncomingDisposition.Department").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(limit).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingDispositionDepartment
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_incoming_id = ?", letterIncomingID).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("LetterIncoming").
|
||||||
|
Preload("LetterIncomingDisposition.Department").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.LetterIncomingDispositionDepartmentStatus, notes string, readAt, completedAt *time.Time) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
if readAt != nil {
|
||||||
|
updates["read_at"] = readAt
|
||||||
|
}
|
||||||
|
if completedAt != nil {
|
||||||
|
updates["completed_at"] = completedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if notes != "" {
|
||||||
|
updates["notes"] = notes
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterIncomingDispositionDepartment{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingDispositionDepartmentRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
func (r *LetterIncomingDispositionDepartmentRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var list []entities.LetterIncomingDispositionDepartment
|
var list []entities.LetterIncomingDispositionDepartment
|
||||||
@ -292,11 +605,63 @@ type LetterIncomingRecipientRepository struct{ db *gorm.DB }
|
|||||||
func NewLetterIncomingRecipientRepository(db *gorm.DB) *LetterIncomingRecipientRepository {
|
func NewLetterIncomingRecipientRepository(db *gorm.DB) *LetterIncomingRecipientRepository {
|
||||||
return &LetterIncomingRecipientRepository{db: db}
|
return &LetterIncomingRecipientRepository{db: db}
|
||||||
}
|
}
|
||||||
|
func (r *LetterIncomingRecipientRepository) DB(ctx context.Context) *gorm.DB {
|
||||||
|
return DBFromContext(ctx, r.db)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingRecipientRepository) CreateBulk(ctx context.Context, recs []entities.LetterIncomingRecipient) error {
|
func (r *LetterIncomingRecipientRepository) CreateBulk(ctx context.Context, recs []entities.LetterIncomingRecipient) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Create(&recs).Error
|
return db.WithContext(ctx).Create(&recs).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) Create(ctx context.Context, recipient *entities.LetterIncomingRecipient) error {
|
||||||
|
return r.DB(ctx).Create(recipient).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) Update(ctx context.Context, recipient *entities.LetterIncomingRecipient) error {
|
||||||
|
return r.DB(ctx).Save(recipient).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingRecipient, error) {
|
||||||
|
var recipient entities.LetterIncomingRecipient
|
||||||
|
if err := r.DB(ctx).Where("id = ?", id).First(&recipient).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recipient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) GetByLetterAndDepartment(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (*entities.LetterIncomingRecipient, error) {
|
||||||
|
var recipient entities.LetterIncomingRecipient
|
||||||
|
if err := r.DB(ctx).Where("letter_id = ? AND recipient_department_id = ?", letterID, departmentID).First(&recipient).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recipient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) GetByLetterAndUser(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) (*entities.LetterIncomingRecipient, error) {
|
||||||
|
var recipient entities.LetterIncomingRecipient
|
||||||
|
if err := r.DB(ctx).Where("letter_id = ? AND recipient_user_id = ?", letterID, userID).First(&recipient).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recipient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
|
||||||
|
var recipients []entities.LetterIncomingRecipient
|
||||||
|
if err := r.DB(ctx).Where("letter_id = ?", letterID).Find(&recipients).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return recipients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) ListByDepartment(ctx context.Context, departmentID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
|
||||||
|
var recipients []entities.LetterIncomingRecipient
|
||||||
|
if err := r.DB(ctx).Where("recipient_department_id = ?", departmentID).Find(&recipients).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return recipients, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingRecipientRepository) GetLetterIDsByDepartment(ctx context.Context, departmentID uuid.UUID) ([]uuid.UUID, error) {
|
func (r *LetterIncomingRecipientRepository) GetLetterIDsByDepartment(ctx context.Context, departmentID uuid.UUID) ([]uuid.UUID, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var letterIDs []uuid.UUID
|
var letterIDs []uuid.UUID
|
||||||
@ -310,6 +675,60 @@ func (r *LetterIncomingRecipientRepository) GetLetterIDsByDepartment(ctx context
|
|||||||
return letterIDs, nil
|
return letterIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) CountReadByLetter(ctx context.Context, letterID uuid.UUID) (int, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var count int64
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterIncomingRecipient{}).
|
||||||
|
Where("letter_id = ? AND read_at IS NOT NULL", letterID).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(count), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var count int64
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterIncomingRecipient{}).
|
||||||
|
Where("recipient_user_id = ? AND read_at IS NULL", userID).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(count), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) MarkAsRead(ctx context.Context, letterID, userID uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
now := time.Now()
|
||||||
|
return db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterIncomingRecipient{}).
|
||||||
|
Where("letter_id = ? AND recipient_user_id = ?", letterID, userID).
|
||||||
|
Update("read_at", now).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRecipientRepository) GetByLetterIDsAndUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) {
|
||||||
|
if len(letterIDs) == 0 {
|
||||||
|
return make(map[uuid.UUID]*entities.LetterIncomingRecipient), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var recipients []entities.LetterIncomingRecipient
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_id IN ? AND recipient_user_id = ?", letterIDs, userID).
|
||||||
|
Find(&recipients).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uuid.UUID]*entities.LetterIncomingRecipient)
|
||||||
|
for i := range recipients {
|
||||||
|
result[recipients[i].LetterID] = &recipients[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterIncomingRecipientRepository) HasDepartmentAccess(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (bool, error) {
|
func (r *LetterIncomingRecipientRepository) HasDepartmentAccess(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (bool, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var count int64
|
var count int64
|
||||||
|
|||||||
@ -59,6 +59,24 @@ func (r *PriorityRepository) Get(ctx context.Context, id uuid.UUID) (*entities.P
|
|||||||
return &e, nil
|
return &e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *PriorityRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return make(map[uuid.UUID]*entities.Priority), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var priorities []entities.Priority
|
||||||
|
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&priorities).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uuid.UUID]*entities.Priority)
|
||||||
|
for i := range priorities {
|
||||||
|
result[priorities[i].ID] = &priorities[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
type InstitutionRepository struct{ db *gorm.DB }
|
type InstitutionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewInstitutionRepository(db *gorm.DB) *InstitutionRepository {
|
func NewInstitutionRepository(db *gorm.DB) *InstitutionRepository {
|
||||||
@ -100,6 +118,24 @@ func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entitie
|
|||||||
return &e, nil
|
return &e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *InstitutionRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return make(map[uuid.UUID]*entities.Institution), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var institutions []entities.Institution
|
||||||
|
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&institutions).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uuid.UUID]*entities.Institution)
|
||||||
|
for i := range institutions {
|
||||||
|
result[institutions[i].ID] = &institutions[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
type DispositionActionRepository struct{ db *gorm.DB }
|
type DispositionActionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewDispositionActionRepository(db *gorm.DB) *DispositionActionRepository {
|
func NewDispositionActionRepository(db *gorm.DB) *DispositionActionRepository {
|
||||||
@ -188,3 +224,12 @@ func (r *DepartmentRepository) List(ctx context.Context, search string, limit, o
|
|||||||
|
|
||||||
return list, total, nil
|
return list, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.Department
|
||||||
|
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -13,15 +13,14 @@ func NewUserDepartmentRepository(db *gorm.DB) *UserDepartmentRepository {
|
|||||||
return &UserDepartmentRepository{db: db}
|
return &UserDepartmentRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
type userDepartmentRow struct {
|
type UserDepartmentRow struct {
|
||||||
UserID uuid.UUID `gorm:"column:user_id"`
|
UserID uuid.UUID `gorm:"column:user_id"`
|
||||||
DepartmentID uuid.UUID `gorm:"column:department_id"`
|
DepartmentID uuid.UUID `gorm:"column:department_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListActiveByDepartmentIDs returns active user-department memberships for given department IDs.
|
func (r *UserDepartmentRepository) ListActiveByDepartmentIDs(ctx context.Context, departmentIDs []uuid.UUID) ([]UserDepartmentRow, error) {
|
||||||
func (r *UserDepartmentRepository) ListActiveByDepartmentIDs(ctx context.Context, departmentIDs []uuid.UUID) ([]userDepartmentRow, error) {
|
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
rows := make([]userDepartmentRow, 0)
|
rows := make([]UserDepartmentRow, 0)
|
||||||
if len(departmentIDs) == 0 {
|
if len(departmentIDs) == 0 {
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,18 @@ func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entiti
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByIDLight retrieves user without preloading relationships - for faster auth checks
|
||||||
|
func (r *UserRepositoryImpl) GetByIDLight(ctx context.Context, id uuid.UUID) (*entities.User, error) {
|
||||||
|
var user entities.User
|
||||||
|
err := r.b.WithContext(ctx).
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
|
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||||
var user entities.User
|
var user entities.User
|
||||||
err := r.b.WithContext(ctx).
|
err := r.b.WithContext(ctx).
|
||||||
|
|||||||
@ -67,12 +67,18 @@ type LetterHandler interface {
|
|||||||
CreateIncomingLetter(c *gin.Context)
|
CreateIncomingLetter(c *gin.Context)
|
||||||
GetIncomingLetter(c *gin.Context)
|
GetIncomingLetter(c *gin.Context)
|
||||||
ListIncomingLetters(c *gin.Context)
|
ListIncomingLetters(c *gin.Context)
|
||||||
|
GetLetterUnreadCounts(c *gin.Context)
|
||||||
|
MarkIncomingLetterAsRead(c *gin.Context)
|
||||||
|
MarkOutgoingLetterAsRead(c *gin.Context)
|
||||||
UpdateIncomingLetter(c *gin.Context)
|
UpdateIncomingLetter(c *gin.Context)
|
||||||
DeleteIncomingLetter(c *gin.Context)
|
DeleteIncomingLetter(c *gin.Context)
|
||||||
|
BulkArchiveIncomingLetters(c *gin.Context)
|
||||||
|
|
||||||
CreateDispositions(c *gin.Context)
|
CreateDispositions(c *gin.Context)
|
||||||
//ListDispositionsByLetter(c *gin.Context)
|
|
||||||
GetEnhancedDispositionsByLetter(c *gin.Context)
|
GetEnhancedDispositionsByLetter(c *gin.Context)
|
||||||
|
GetDepartmentDispositionStatus(c *gin.Context)
|
||||||
|
UpdateDispositionStatus(c *gin.Context)
|
||||||
|
GetLetterCTA(c *gin.Context)
|
||||||
|
|
||||||
CreateDiscussion(c *gin.Context)
|
CreateDiscussion(c *gin.Context)
|
||||||
UpdateDiscussion(c *gin.Context)
|
UpdateDiscussion(c *gin.Context)
|
||||||
@ -106,6 +112,7 @@ type LetterOutgoingHandler interface {
|
|||||||
|
|
||||||
GetApprovalDiscussions(c *gin.Context)
|
GetApprovalDiscussions(c *gin.Context)
|
||||||
GetApprovalTimeline(c *gin.Context)
|
GetApprovalTimeline(c *gin.Context)
|
||||||
|
BulkArchiveOutgoingLetters(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminApprovalFlowHandler interface {
|
type AdminApprovalFlowHandler interface {
|
||||||
@ -122,10 +129,13 @@ type AdminApprovalFlowHandler interface {
|
|||||||
|
|
||||||
type DispositionRouteHandler interface {
|
type DispositionRouteHandler interface {
|
||||||
Create(c *gin.Context)
|
Create(c *gin.Context)
|
||||||
|
BulkCreateOrUpdate(c *gin.Context)
|
||||||
Update(c *gin.Context)
|
Update(c *gin.Context)
|
||||||
Get(c *gin.Context)
|
Get(c *gin.Context)
|
||||||
ListByFromDept(c *gin.Context)
|
ListByFromDept(c *gin.Context)
|
||||||
SetActive(c *gin.Context)
|
SetActive(c *gin.Context)
|
||||||
|
ListGrouped(c *gin.Context)
|
||||||
|
ListAll(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnlyOfficeHandler interface {
|
type OnlyOfficeHandler interface {
|
||||||
@ -146,3 +156,13 @@ type AnalyticsHandler interface {
|
|||||||
GetMonthlyTrend(c *gin.Context)
|
GetMonthlyTrend(c *gin.Context)
|
||||||
GetApprovalMetrics(c *gin.Context)
|
GetApprovalMetrics(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationHandler interface {
|
||||||
|
TriggerNotification(c *gin.Context)
|
||||||
|
BulkTriggerNotification(c *gin.Context)
|
||||||
|
GetSubscriber(c *gin.Context)
|
||||||
|
UpdateSubscriberChannel(c *gin.Context)
|
||||||
|
TriggerNotificationForCurrentUser(c *gin.Context)
|
||||||
|
GetCurrentUserSubscriber(c *gin.Context)
|
||||||
|
UpdateCurrentUserSubscriberChannel(c *gin.Context)
|
||||||
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type Router struct {
|
|||||||
dispRouteHandler DispositionRouteHandler
|
dispRouteHandler DispositionRouteHandler
|
||||||
onlyOfficeHandler OnlyOfficeHandler
|
onlyOfficeHandler OnlyOfficeHandler
|
||||||
analyticsHandler AnalyticsHandler
|
analyticsHandler AnalyticsHandler
|
||||||
|
notificationHandler NotificationHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(
|
func NewRouter(
|
||||||
@ -39,6 +40,7 @@ func NewRouter(
|
|||||||
dispRouteHandler DispositionRouteHandler,
|
dispRouteHandler DispositionRouteHandler,
|
||||||
onlyOfficeHandler OnlyOfficeHandler,
|
onlyOfficeHandler OnlyOfficeHandler,
|
||||||
analyticsHandler AnalyticsHandler,
|
analyticsHandler AnalyticsHandler,
|
||||||
|
notificationHandler NotificationHandler,
|
||||||
) *Router {
|
) *Router {
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -55,6 +57,7 @@ func NewRouter(
|
|||||||
dispRouteHandler: dispRouteHandler,
|
dispRouteHandler: dispRouteHandler,
|
||||||
onlyOfficeHandler: onlyOfficeHandler,
|
onlyOfficeHandler: onlyOfficeHandler,
|
||||||
analyticsHandler: analyticsHandler,
|
analyticsHandler: analyticsHandler,
|
||||||
|
notificationHandler: notificationHandler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +92,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
users := v1.Group("/users")
|
users := v1.Group("/users")
|
||||||
users.Use(r.authMiddleware.RequireAuth())
|
users.Use(r.authMiddleware.RequireAuth())
|
||||||
{
|
{
|
||||||
users.GET("", r.authMiddleware.RequirePermissions("user.read"), r.userHandler.ListUsers)
|
users.GET("", r.userHandler.ListUsers)
|
||||||
users.GET("/profile", r.userHandler.GetProfile)
|
users.GET("/profile", r.userHandler.GetProfile)
|
||||||
users.GET("/:id/profile", r.userHandler.GetUserProfile)
|
users.GET("/:id/profile", r.userHandler.GetUserProfile)
|
||||||
users.PUT("/profile", r.userHandler.UpdateProfile)
|
users.PUT("/profile", r.userHandler.UpdateProfile)
|
||||||
@ -156,16 +159,22 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
lettersch := v1.Group("/letters")
|
lettersch := v1.Group("/letters")
|
||||||
lettersch.Use(r.authMiddleware.RequireAuth())
|
lettersch.Use(r.authMiddleware.RequireAuth())
|
||||||
{
|
{
|
||||||
lettersch.POST("/incoming", r.letterHandler.CreateIncomingLetter)
|
lettersch.GET("/unread-counts", r.letterHandler.GetLetterUnreadCounts)
|
||||||
lettersch.GET("/incoming/:id", r.letterHandler.GetIncomingLetter)
|
|
||||||
lettersch.GET("/incoming", r.letterHandler.ListIncomingLetters)
|
lettersch.GET("/incoming", r.letterHandler.ListIncomingLetters)
|
||||||
|
lettersch.POST("/incoming", r.letterHandler.CreateIncomingLetter)
|
||||||
|
lettersch.GET("/incoming/cta/:letter_id", r.letterHandler.GetLetterCTA)
|
||||||
|
lettersch.GET("/incoming/:id", r.letterHandler.GetIncomingLetter)
|
||||||
lettersch.PUT("/incoming/:id", r.letterHandler.UpdateIncomingLetter)
|
lettersch.PUT("/incoming/:id", r.letterHandler.UpdateIncomingLetter)
|
||||||
|
lettersch.PUT("/incoming/:id/read", r.letterHandler.MarkIncomingLetterAsRead)
|
||||||
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
||||||
|
lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters)
|
||||||
|
|
||||||
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
|
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
|
||||||
lettersch.GET("/outgoing/:id", r.letterOutgoingHandler.GetOutgoingLetter)
|
lettersch.GET("/outgoing/:id", r.letterOutgoingHandler.GetOutgoingLetter)
|
||||||
lettersch.GET("/outgoing", r.letterOutgoingHandler.ListOutgoingLetters)
|
lettersch.GET("/outgoing", r.letterOutgoingHandler.ListOutgoingLetters)
|
||||||
lettersch.PUT("/outgoing/:id", r.letterOutgoingHandler.UpdateOutgoingLetter)
|
lettersch.PUT("/outgoing/:id", r.letterOutgoingHandler.UpdateOutgoingLetter)
|
||||||
|
lettersch.PUT("/outgoing/:id/read", r.letterHandler.MarkOutgoingLetterAsRead)
|
||||||
lettersch.DELETE("/outgoing/:id", r.letterOutgoingHandler.DeleteOutgoingLetter)
|
lettersch.DELETE("/outgoing/:id", r.letterOutgoingHandler.DeleteOutgoingLetter)
|
||||||
|
|
||||||
lettersch.POST("/outgoing/:id/submit", r.letterOutgoingHandler.SubmitForApproval)
|
lettersch.POST("/outgoing/:id/submit", r.letterOutgoingHandler.SubmitForApproval)
|
||||||
@ -173,6 +182,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
lettersch.POST("/outgoing/:id/reject", r.letterOutgoingHandler.RejectOutgoingLetter)
|
lettersch.POST("/outgoing/:id/reject", r.letterOutgoingHandler.RejectOutgoingLetter)
|
||||||
lettersch.POST("/outgoing/:id/send", r.letterOutgoingHandler.SendOutgoingLetter)
|
lettersch.POST("/outgoing/:id/send", r.letterOutgoingHandler.SendOutgoingLetter)
|
||||||
lettersch.POST("/outgoing/:id/archive", r.letterOutgoingHandler.ArchiveOutgoingLetter)
|
lettersch.POST("/outgoing/:id/archive", r.letterOutgoingHandler.ArchiveOutgoingLetter)
|
||||||
|
lettersch.POST("/outgoing/archive", r.letterOutgoingHandler.BulkArchiveOutgoingLetters)
|
||||||
lettersch.GET("/outgoing/:id/cta", r.letterOutgoingHandler.GetLetterApprovalInfo)
|
lettersch.GET("/outgoing/:id/cta", r.letterOutgoingHandler.GetLetterApprovalInfo)
|
||||||
lettersch.GET("/outgoing/:id/approvals", r.letterOutgoingHandler.GetLetterApprovals)
|
lettersch.GET("/outgoing/:id/approvals", r.letterOutgoingHandler.GetLetterApprovals)
|
||||||
|
|
||||||
@ -192,6 +202,8 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
|
|
||||||
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
|
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
|
||||||
lettersch.GET("/dispositions/:letter_id", r.letterHandler.GetEnhancedDispositionsByLetter)
|
lettersch.GET("/dispositions/:letter_id", r.letterHandler.GetEnhancedDispositionsByLetter)
|
||||||
|
lettersch.GET("/dispositions/:letter_id/department/status", r.letterHandler.GetDepartmentDispositionStatus)
|
||||||
|
lettersch.PUT("/dispositions/:letter_id/status", r.letterHandler.UpdateDispositionStatus)
|
||||||
|
|
||||||
lettersch.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion)
|
lettersch.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion)
|
||||||
lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion)
|
lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion)
|
||||||
@ -200,11 +212,14 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
droutes := v1.Group("/disposition-routes")
|
droutes := v1.Group("/disposition-routes")
|
||||||
droutes.Use(r.authMiddleware.RequireAuth())
|
droutes.Use(r.authMiddleware.RequireAuth())
|
||||||
{
|
{
|
||||||
droutes.POST("", r.dispRouteHandler.Create)
|
droutes.POST("", r.dispRouteHandler.Create) // Create with upsert logic
|
||||||
droutes.GET(":id", r.dispRouteHandler.Get)
|
droutes.PUT("/bulk", r.dispRouteHandler.BulkCreateOrUpdate) // Explicit bulk create/update
|
||||||
droutes.PUT(":id", r.dispRouteHandler.Update)
|
droutes.GET("", r.dispRouteHandler.ListAll) // List all routes with details
|
||||||
droutes.GET("department", r.dispRouteHandler.ListByFromDept)
|
droutes.GET("/grouped", r.dispRouteHandler.ListGrouped) // List grouped by from_department_id
|
||||||
droutes.PUT(":id/active", r.dispRouteHandler.SetActive)
|
droutes.GET("/department", r.dispRouteHandler.ListByFromDept)
|
||||||
|
droutes.GET("/:id", r.dispRouteHandler.Get)
|
||||||
|
droutes.PUT("/:id", r.dispRouteHandler.Update)
|
||||||
|
droutes.PUT("/:id/active", r.dispRouteHandler.SetActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
admin := v1.Group("/setting")
|
admin := v1.Group("/setting")
|
||||||
@ -224,13 +239,10 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnlyOffice routes
|
|
||||||
onlyoffice := v1.Group("/onlyoffice")
|
onlyoffice := v1.Group("/onlyoffice")
|
||||||
{
|
{
|
||||||
// Callback endpoint - no auth required (OnlyOffice will call this)
|
|
||||||
onlyoffice.POST("/callback/:key", r.onlyOfficeHandler.ProcessCallback)
|
onlyoffice.POST("/callback/:key", r.onlyOfficeHandler.ProcessCallback)
|
||||||
|
|
||||||
// Protected endpoints
|
|
||||||
onlyofficeAuth := onlyoffice.Group("")
|
onlyofficeAuth := onlyoffice.Group("")
|
||||||
onlyofficeAuth.Use(r.authMiddleware.RequireAuth())
|
onlyofficeAuth.Use(r.authMiddleware.RequireAuth())
|
||||||
{
|
{
|
||||||
@ -242,7 +254,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analytics routes
|
|
||||||
analytics := v1.Group("/analytics")
|
analytics := v1.Group("/analytics")
|
||||||
analytics.Use(r.authMiddleware.RequireAuth())
|
analytics.Use(r.authMiddleware.RequireAuth())
|
||||||
{
|
{
|
||||||
@ -254,5 +265,22 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
analytics.GET("/monthly-trend", r.analyticsHandler.GetMonthlyTrend)
|
analytics.GET("/monthly-trend", r.analyticsHandler.GetMonthlyTrend)
|
||||||
analytics.GET("/approval-metrics", r.analyticsHandler.GetApprovalMetrics)
|
analytics.GET("/approval-metrics", r.analyticsHandler.GetApprovalMetrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifications := v1.Group("/notifications")
|
||||||
|
notifications.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
notifications.POST("/me/trigger", r.notificationHandler.TriggerNotificationForCurrentUser)
|
||||||
|
notifications.GET("/me/subscriber", r.notificationHandler.GetCurrentUserSubscriber)
|
||||||
|
notifications.PUT("/me/channel", r.notificationHandler.UpdateCurrentUserSubscriberChannel)
|
||||||
|
|
||||||
|
notifAdmin := notifications.Group("")
|
||||||
|
notifAdmin.Use(r.authMiddleware.RequirePermissions("notification.admin"))
|
||||||
|
{
|
||||||
|
notifAdmin.POST("/trigger", r.notificationHandler.TriggerNotification)
|
||||||
|
notifAdmin.POST("/bulk-trigger", r.notificationHandler.BulkTriggerNotification)
|
||||||
|
notifAdmin.GET("/subscribers/:userId", r.notificationHandler.GetSubscriber)
|
||||||
|
notifAdmin.PUT("/subscribers/channel", r.notificationHandler.UpdateSubscriberChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
|
|||||||
return nil, fmt.Errorf("user account is deactivated")
|
return nil, fmt.Errorf("user account is deactivated")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Departments are now preloaded, so they're already in the response
|
// Note: Departments are not loaded in light version, add if needed
|
||||||
return userResponse, nil
|
return userResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
"eslogad-be/internal/entities"
|
"eslogad-be/internal/entities"
|
||||||
@ -19,20 +20,96 @@ func NewDispositionRouteService(repo *repository.DispositionRouteRepository) *Di
|
|||||||
return &DispositionRouteServiceImpl{repo: repo}
|
return &DispositionRouteServiceImpl{repo: repo}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdate handles bulk create or update of disposition routes
|
||||||
|
func (s *DispositionRouteServiceImpl) CreateOrUpdate(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.BulkCreateDispositionRouteResponse, error) {
|
||||||
|
// Set default values
|
||||||
|
isActive := true
|
||||||
|
if req.IsActive != nil {
|
||||||
|
isActive = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedActions entities.JSONB
|
||||||
|
if req.AllowedActions != nil {
|
||||||
|
allowedActions = entities.JSONB(*req.AllowedActions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform bulk upsert
|
||||||
|
created, updated, err := s.repo.BulkUpsert(ctx, req.FromDepartmentID, req.ToDepartmentIDs, isActive, allowedActions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all routes for the from_department_id to return
|
||||||
|
routes, err := s.repo.ListByFromDept(ctx, req.FromDepartmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform to response
|
||||||
|
routeResponses := transformer.DispositionRoutesToContract(routes)
|
||||||
|
|
||||||
|
return &contract.BulkCreateDispositionRouteResponse{
|
||||||
|
Created: created,
|
||||||
|
Updated: updated,
|
||||||
|
Routes: routeResponses,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create maintains backward compatibility for single route creation
|
||||||
func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||||
entity := &entities.DispositionRoute{FromDepartmentID: req.FromDepartmentID, ToDepartmentID: req.ToDepartmentID}
|
// If only one to_department_id is provided, create a single route
|
||||||
|
if len(req.ToDepartmentIDs) == 1 {
|
||||||
|
entity := &entities.DispositionRoute{
|
||||||
|
FromDepartmentID: req.FromDepartmentID,
|
||||||
|
ToDepartmentID: req.ToDepartmentIDs[0],
|
||||||
|
}
|
||||||
if req.IsActive != nil {
|
if req.IsActive != nil {
|
||||||
entity.IsActive = *req.IsActive
|
entity.IsActive = *req.IsActive
|
||||||
|
} else {
|
||||||
|
entity.IsActive = true
|
||||||
}
|
}
|
||||||
if req.AllowedActions != nil {
|
if req.AllowedActions != nil {
|
||||||
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
||||||
}
|
}
|
||||||
if err := s.repo.Create(ctx, entity); err != nil {
|
|
||||||
|
// Use upsert to handle create or update
|
||||||
|
if err := s.repo.Upsert(ctx, entity); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
|
|
||||||
|
// Fetch the created/updated route
|
||||||
|
route, err := s.repo.Get(ctx, entity.ID)
|
||||||
|
if err != nil {
|
||||||
|
// If we can't get by ID (new creation), try to get by from/to combination
|
||||||
|
routes, err := s.repo.ListByFromDept(ctx, req.FromDepartmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, r := range routes {
|
||||||
|
if r.ToDepartmentID == req.ToDepartmentIDs[0] {
|
||||||
|
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{r})[0]
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*route})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multiple to_department_ids, use bulk create/update
|
||||||
|
bulkResp, err := s.CreateOrUpdate(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first route as response for backward compatibility
|
||||||
|
if len(bulkResp.Routes) > 0 {
|
||||||
|
return &bulkResp.Routes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
func (s *DispositionRouteServiceImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
func (s *DispositionRouteServiceImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||||
entity, err := s.repo.Get(ctx, id)
|
entity, err := s.repo.Get(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -68,3 +145,79 @@ func (s *DispositionRouteServiceImpl) ListByFromDept(ctx context.Context, from u
|
|||||||
func (s *DispositionRouteServiceImpl) SetActive(ctx context.Context, id uuid.UUID, active bool) error {
|
func (s *DispositionRouteServiceImpl) SetActive(ctx context.Context, id uuid.UUID, active bool) error {
|
||||||
return s.repo.SetActive(ctx, id, active)
|
return s.repo.SetActive(ctx, id, active)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListGrouped returns all disposition routes grouped by from_department_id with clean department structure
|
||||||
|
func (s *DispositionRouteServiceImpl) ListGrouped(ctx context.Context) (*contract.ListDispositionRoutesGroupedResponse, error) {
|
||||||
|
// Get routes with department details
|
||||||
|
routes, err := s.repo.ListAllGroupedWithDepartments(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group routes by from_department_id and collect department info
|
||||||
|
type groupedData struct {
|
||||||
|
fromDept contract.DepartmentMapping
|
||||||
|
toDepts []contract.DepartmentMapping
|
||||||
|
toDeptMap map[uuid.UUID]bool // To avoid duplicates
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped := make(map[uuid.UUID]*groupedData)
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
if _, exists := grouped[route.FromDepartmentID]; !exists {
|
||||||
|
grouped[route.FromDepartmentID] = &groupedData{
|
||||||
|
fromDept: contract.DepartmentMapping{
|
||||||
|
ID: route.FromDepartmentID,
|
||||||
|
Name: route.FromDepartment.Name,
|
||||||
|
},
|
||||||
|
toDepts: []contract.DepartmentMapping{},
|
||||||
|
toDeptMap: make(map[uuid.UUID]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to_department if not already added (avoid duplicates)
|
||||||
|
if !grouped[route.FromDepartmentID].toDeptMap[route.ToDepartmentID] {
|
||||||
|
grouped[route.FromDepartmentID].toDepts = append(
|
||||||
|
grouped[route.FromDepartmentID].toDepts,
|
||||||
|
contract.DepartmentMapping{
|
||||||
|
ID: route.ToDepartmentID,
|
||||||
|
Name: route.ToDepartment.Name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
grouped[route.FromDepartmentID].toDeptMap[route.ToDepartmentID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to response format
|
||||||
|
var dispositions []contract.DispositionRouteGroupedItem
|
||||||
|
for _, data := range grouped {
|
||||||
|
dispositions = append(dispositions, contract.DispositionRouteGroupedItem{
|
||||||
|
FromDepartment: data.fromDept,
|
||||||
|
ToDepartments: data.toDepts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by from department name for consistent ordering
|
||||||
|
sort.Slice(dispositions, func(i, j int) bool {
|
||||||
|
return dispositions[i].FromDepartment.Name < dispositions[j].FromDepartment.Name
|
||||||
|
})
|
||||||
|
|
||||||
|
return &contract.ListDispositionRoutesGroupedResponse{
|
||||||
|
Dispositions: dispositions,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll returns all disposition routes with department details
|
||||||
|
func (s *DispositionRouteServiceImpl) ListAll(ctx context.Context) (*contract.ListDispositionRoutesDetailedResponse, error) {
|
||||||
|
routes, err := s.repo.ListAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
routeResponses := transformer.DispositionRoutesToContract(routes)
|
||||||
|
|
||||||
|
return &contract.ListDispositionRoutesDetailedResponse{
|
||||||
|
Routes: routeResponses,
|
||||||
|
Total: len(routeResponses),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -32,8 +32,8 @@ func (s *FileServiceImpl) UploadProfileAvatar(ctx context.Context, userID uuid.U
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||||
if ext := mimeExtFromContentType(contentType); ext != "" {
|
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
|
||||||
ext = ext
|
ext = mimeExt
|
||||||
}
|
}
|
||||||
key := buildObjectKey("profile", userID, ext)
|
key := buildObjectKey("profile", userID, ext)
|
||||||
url, err := s.storage.Upload(ctx, s.profileBucket, key, content, contentType)
|
url, err := s.storage.Upload(ctx, s.profileBucket, key, content, contentType)
|
||||||
@ -50,8 +50,8 @@ func (s *FileServiceImpl) UploadDocument(ctx context.Context, userID uuid.UUID,
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||||
if ext := mimeExtFromContentType(contentType); ext != "" {
|
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
|
||||||
ext = ext
|
ext = mimeExt
|
||||||
}
|
}
|
||||||
key := buildObjectKey("documents", userID, ext)
|
key := buildObjectKey("documents", userID, ext)
|
||||||
url, err := s.storage.Upload(ctx, s.docBucket, key, content, contentType)
|
url, err := s.storage.Upload(ctx, s.docBucket, key, content, contentType)
|
||||||
|
|||||||
@ -114,6 +114,11 @@ func (s *LetterOutgoingServiceImpl) GetOutgoingLetterByID(ctx context.Context, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) {
|
func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) {
|
||||||
|
// Extract user context from gin context
|
||||||
|
appCtx := appcontext.FromGinContext(ctx)
|
||||||
|
userID := appCtx.UserID
|
||||||
|
departmentID := appCtx.DepartmentID
|
||||||
|
|
||||||
offset := (req.Page - 1) * req.Limit
|
offset := (req.Page - 1) * req.Limit
|
||||||
if offset < 0 {
|
if offset < 0 {
|
||||||
offset = 0
|
offset = 0
|
||||||
@ -124,6 +129,11 @@ func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req
|
|||||||
DepartmentID: req.DepartmentID,
|
DepartmentID: req.DepartmentID,
|
||||||
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||||
PriorityID: req.PriorityID,
|
PriorityID: req.PriorityID,
|
||||||
|
UserID: &userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if departmentID != uuid.Nil {
|
||||||
|
filter.DepartmentID = &departmentID
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Status != "" {
|
if req.Status != "" {
|
||||||
@ -152,16 +162,104 @@ func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply access control overrides based on user context
|
filter.IsArchived = req.IsArchived
|
||||||
ApplyLetterFilterOverrides(ctx, &filter)
|
|
||||||
|
|
||||||
|
// Get raw letters data
|
||||||
letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, offset)
|
letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect IDs for batch loading
|
||||||
|
letterIDs := make([]uuid.UUID, len(letters))
|
||||||
|
priorityIDs := make(map[uuid.UUID]bool)
|
||||||
|
institutionIDs := make(map[uuid.UUID]bool)
|
||||||
|
|
||||||
|
for i, letter := range letters {
|
||||||
|
letterIDs[i] = letter.ID
|
||||||
|
if letter.PriorityID != nil {
|
||||||
|
priorityIDs[*letter.PriorityID] = true
|
||||||
|
}
|
||||||
|
if letter.ReceiverInstitutionID != nil {
|
||||||
|
institutionIDs[*letter.ReceiverInstitutionID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert maps to slices
|
||||||
|
priorityIDSlice := make([]uuid.UUID, 0, len(priorityIDs))
|
||||||
|
for id := range priorityIDs {
|
||||||
|
priorityIDSlice = append(priorityIDSlice, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
institutionIDSlice := make([]uuid.UUID, 0, len(institutionIDs))
|
||||||
|
for id := range institutionIDs {
|
||||||
|
institutionIDSlice = append(institutionIDSlice, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parallel batch loading
|
||||||
|
type batchResult struct {
|
||||||
|
attachments map[uuid.UUID][]entities.LetterOutgoingAttachment
|
||||||
|
recipients map[uuid.UUID][]entities.LetterOutgoingRecipient
|
||||||
|
priorities map[uuid.UUID]*entities.Priority
|
||||||
|
institutions map[uuid.UUID]*entities.Institution
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
result := batchResult{}
|
||||||
|
errChan := make(chan error, 4)
|
||||||
|
|
||||||
|
// Load attachments
|
||||||
|
go func() {
|
||||||
|
result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Load recipients
|
||||||
|
go func() {
|
||||||
|
result.recipients, err = s.processor.GetBatchRecipients(ctx, letterIDs)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Load priorities
|
||||||
|
go func() {
|
||||||
|
result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDSlice)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Load institutions
|
||||||
|
go func() {
|
||||||
|
result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDSlice)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for all goroutines and check for errors
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
if err := <-errChan; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform letters with batch loaded data
|
||||||
items := make([]*contract.OutgoingLetterResponse, len(letters))
|
items := make([]*contract.OutgoingLetterResponse, len(letters))
|
||||||
for i, letter := range letters {
|
for i, letter := range letters {
|
||||||
|
// Attach batch loaded data to letter
|
||||||
|
if attachments, ok := result.attachments[letter.ID]; ok {
|
||||||
|
letter.Attachments = attachments
|
||||||
|
}
|
||||||
|
if recipients, ok := result.recipients[letter.ID]; ok {
|
||||||
|
letter.Recipients = recipients
|
||||||
|
}
|
||||||
|
if letter.PriorityID != nil {
|
||||||
|
if priority, ok := result.priorities[*letter.PriorityID]; ok {
|
||||||
|
letter.Priority = priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if letter.ReceiverInstitutionID != nil {
|
||||||
|
if institution, ok := result.institutions[*letter.ReceiverInstitutionID]; ok {
|
||||||
|
letter.ReceiverInstitution = institution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items[i] = transformLetterToResponse(&letter)
|
items[i] = transformLetterToResponse(&letter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1361,3 +1459,16 @@ func ApplyLetterFilterOverrides(ctx context.Context, filter *repository.ListOutg
|
|||||||
filter.UserID = &appCtx.UserID
|
filter.UserID = &appCtx.UserID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
|
||||||
|
archivedCount, err := s.processor.BulkArchiveOutgoingLetters(ctx, letterIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.BulkArchiveLettersResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Letters archived successfully",
|
||||||
|
ArchivedCount: int(archivedCount),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -2,42 +2,418 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"eslogad-be/internal/logger"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
|
"eslogad-be/internal/constant"
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/processor"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultIncomingLetterID = "ESLI"
|
||||||
|
)
|
||||||
|
|
||||||
type LetterProcessor interface {
|
type LetterProcessor interface {
|
||||||
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
||||||
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
|
ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, 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)
|
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error)
|
||||||
|
BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error)
|
||||||
|
|
||||||
|
// Batch loading methods
|
||||||
|
GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error)
|
||||||
|
GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error)
|
||||||
|
GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error)
|
||||||
|
GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error)
|
||||||
|
CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error)
|
||||||
|
|
||||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
||||||
|
|
||||||
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, 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)
|
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, departmentID uuid.UUID) (*contract.LetterCTAResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LetterServiceImpl struct {
|
type LetterServiceImpl struct {
|
||||||
processor LetterProcessor
|
processor LetterProcessor
|
||||||
|
txManager *repository.TxManager
|
||||||
|
numberGenerator NumberGenerator
|
||||||
|
recipientProcessor RecipientProcessor
|
||||||
|
activityLogger ActivityLogger
|
||||||
|
letterDispositionProcessor LetterDispositionProcessor
|
||||||
|
notificationProcessor processor.NotificationProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLetterService(processor LetterProcessor) *LetterServiceImpl {
|
type NumberGenerator interface {
|
||||||
return &LetterServiceImpl{processor: processor}
|
GenerateNumber(ctx context.Context, prefixKey, sequenceKey, defaultPrefix string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecipientProcessor interface {
|
||||||
|
CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error)
|
||||||
|
CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogger interface {
|
||||||
|
LogLetterCreated(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, letterNumber string) error
|
||||||
|
LogAttachmentUploaded(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, fileName string, fileType string) error
|
||||||
|
LogLetterDispositionStatusUpdate(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, status string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterDispositionProcessor interface {
|
||||||
|
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterService(
|
||||||
|
processor LetterProcessor,
|
||||||
|
txManager *repository.TxManager,
|
||||||
|
numberGenerator NumberGenerator,
|
||||||
|
recipientProcessor RecipientProcessor,
|
||||||
|
activityLogger ActivityLogger,
|
||||||
|
letterDispositionProcessor LetterDispositionProcessor,
|
||||||
|
notificationProcessor processor.NotificationProcessor,
|
||||||
|
) *LetterServiceImpl {
|
||||||
|
return &LetterServiceImpl{
|
||||||
|
processor: processor,
|
||||||
|
txManager: txManager,
|
||||||
|
numberGenerator: numberGenerator,
|
||||||
|
recipientProcessor: recipientProcessor,
|
||||||
|
activityLogger: activityLogger,
|
||||||
|
letterDispositionProcessor: letterDispositionProcessor,
|
||||||
|
notificationProcessor: notificationProcessor,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
func (s *LetterServiceImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
return s.processor.CreateIncomingLetter(ctx, req)
|
var result *contract.IncomingLetterResponse
|
||||||
|
var recipients []entities.LetterIncomingRecipient
|
||||||
|
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
letterNumber, err := s.generateLetterNumber(txCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
req.LetterNumber = letterNumber
|
||||||
|
|
||||||
|
result, err = s.processor.CreateIncomingLetter(txCtx, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients, err = s.createDefaultRecipients(txCtx, result.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.createDispositionsForRecipients(txCtx, result.ID, recipients); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logLetterCreation(txCtx, result.ID, letterNumber)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notifications to all recipients after successful creation
|
||||||
|
if s.notificationProcessor != nil && len(recipients) > 0 {
|
||||||
|
go s.sendLetterNotifications(context.Background(), result, recipients)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) generateLetterNumber(ctx context.Context) (string, error) {
|
||||||
|
return s.numberGenerator.GenerateNumber(
|
||||||
|
ctx,
|
||||||
|
contract.SettingIncomingLetterPrefix,
|
||||||
|
contract.SettingIncomingLetterSequence,
|
||||||
|
DefaultIncomingLetterID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) createDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
|
||||||
|
return s.recipientProcessor.CreateDefaultRecipients(ctx, letterID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) createDispositionsForRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterIncomingRecipient) error {
|
||||||
|
if len(recipients) == 0 || s.letterDispositionProcessor == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
departmentIDs := s.extractUniqueDepartmentIDs(recipients)
|
||||||
|
if len(departmentIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
systemDeptID := constant.SystemDepartmentID
|
||||||
|
systemUserID := constant.SystemUserID
|
||||||
|
|
||||||
|
dispositionReq := &contract.CreateLetterDispositionRequest{
|
||||||
|
FromDepartment: systemDeptID,
|
||||||
|
LetterID: letterID,
|
||||||
|
ToDepartmentIDs: departmentIDs,
|
||||||
|
Notes: nil,
|
||||||
|
CreatedBy: systemUserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.letterDispositionProcessor.CreateDispositions(ctx, dispositionReq)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) extractUniqueDepartmentIDs(recipients []entities.LetterIncomingRecipient) []uuid.UUID {
|
||||||
|
deptMap := make(map[uuid.UUID]bool)
|
||||||
|
var departmentIDs []uuid.UUID
|
||||||
|
|
||||||
|
for _, recipient := range recipients {
|
||||||
|
if recipient.RecipientDepartmentID != nil && !deptMap[*recipient.RecipientDepartmentID] {
|
||||||
|
deptMap[*recipient.RecipientDepartmentID] = true
|
||||||
|
departmentIDs = append(departmentIDs, *recipient.RecipientDepartmentID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return departmentIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) logLetterCreation(ctx context.Context, letterID uuid.UUID, letterNumber string) {
|
||||||
|
if s.activityLogger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := appcontext.FromGinContext(ctx).UserID
|
||||||
|
err := s.activityLogger.LogLetterCreated(ctx, letterID, userID, letterNumber)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).Error("error when insert into log", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) addCreatorAsRecipient(ctx context.Context, letterID uuid.UUID, creatorID uuid.UUID) (*entities.LetterIncomingRecipient, error) {
|
||||||
|
// Check if creator is already a recipient (to avoid duplicates)
|
||||||
|
existingRecipients, err := s.processor.GetBatchRecipientsByUser(ctx, []uuid.UUID{letterID}, creatorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If creator is already a recipient, skip
|
||||||
|
if _, exists := existingRecipients[letterID]; exists {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create recipient entry for the creator
|
||||||
|
recipient := entities.LetterIncomingRecipient{
|
||||||
|
ID: uuid.New(),
|
||||||
|
LetterID: letterID,
|
||||||
|
RecipientUserID: &creatorID,
|
||||||
|
Status: entities.RecipientStatusNew,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the recipient
|
||||||
|
if err := s.recipientProcessor.CreateSingleRecipient(ctx, &recipient); err != nil {
|
||||||
|
// Log error but don't fail the whole operation
|
||||||
|
logger.FromContext(ctx).Error("failed to add creator as recipient", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &recipient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) sendLetterNotifications(ctx context.Context, letter *contract.IncomingLetterResponse, recipients []entities.LetterIncomingRecipient) {
|
||||||
|
for _, recipient := range recipients {
|
||||||
|
// Only send notification to user recipients (not department recipients)
|
||||||
|
// Also exclude the creator from receiving notifications
|
||||||
|
if recipient.RecipientUserID != nil && *recipient.RecipientUserID != letter.CreatedBy {
|
||||||
|
// Use description if available, otherwise use subject
|
||||||
|
err := s.notificationProcessor.SendIncomingLetterNotification(
|
||||||
|
ctx,
|
||||||
|
letter.ID,
|
||||||
|
*recipient.RecipientUserID,
|
||||||
|
"Surat Masuk",
|
||||||
|
letter.Subject)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Log error but don't fail the entire operation
|
||||||
|
logger.FromContext(ctx).Error("failed to send notification", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
|
func (s *LetterServiceImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
|
||||||
return s.processor.GetIncomingLetterByID(ctx, id)
|
return s.processor.GetIncomingLetterByID(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
|
func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
|
||||||
return s.processor.ListIncomingLetters(ctx, req)
|
appCtx := appcontext.FromGinContext(ctx)
|
||||||
|
userID := appCtx.UserID
|
||||||
|
departmentID := appCtx.DepartmentID
|
||||||
|
|
||||||
|
filter := repository.ListIncomingLettersFilter{
|
||||||
|
Status: req.Status,
|
||||||
|
Query: req.Query,
|
||||||
|
DepartmentID: &departmentID,
|
||||||
|
UserID: &userID,
|
||||||
|
IsRead: req.IsRead,
|
||||||
|
PriorityIDs: req.PriorityIDs,
|
||||||
|
IsDispositioned: req.IsDispositioned,
|
||||||
|
IsArchived: req.IsArchived,
|
||||||
|
}
|
||||||
|
|
||||||
|
letters, total, err := s.processor.ListIncomingLetters(ctx, filter, req.Page, req.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(letters) == 0 {
|
||||||
|
return &contract.ListIncomingLettersResponse{
|
||||||
|
Letters: []contract.IncomingLetterResponse{},
|
||||||
|
Pagination: transformer.CreatePaginationResponse(int(total), req.Page, req.Limit),
|
||||||
|
TotalUnread: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
letterIDs := make([]uuid.UUID, 0, len(letters))
|
||||||
|
priorityIDSet := make(map[uuid.UUID]bool)
|
||||||
|
institutionIDSet := make(map[uuid.UUID]bool)
|
||||||
|
|
||||||
|
for _, letter := range letters {
|
||||||
|
letterIDs = append(letterIDs, letter.ID)
|
||||||
|
if letter.PriorityID != nil {
|
||||||
|
priorityIDSet[*letter.PriorityID] = true
|
||||||
|
}
|
||||||
|
if letter.SenderInstitutionID != nil {
|
||||||
|
institutionIDSet[*letter.SenderInstitutionID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
priorityIDs := make([]uuid.UUID, 0, len(priorityIDSet))
|
||||||
|
for id := range priorityIDSet {
|
||||||
|
priorityIDs = append(priorityIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
institutionIDs := make([]uuid.UUID, 0, len(institutionIDSet))
|
||||||
|
for id := range institutionIDSet {
|
||||||
|
institutionIDs = append(institutionIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
type batchResult struct {
|
||||||
|
attachments map[uuid.UUID][]entities.LetterIncomingAttachment
|
||||||
|
priorities map[uuid.UUID]*entities.Priority
|
||||||
|
institutions map[uuid.UUID]*entities.Institution
|
||||||
|
recipients map[uuid.UUID]*entities.LetterIncomingRecipient
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChan := make(chan batchResult, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := batchResult{
|
||||||
|
attachments: make(map[uuid.UUID][]entities.LetterIncomingAttachment),
|
||||||
|
priorities: make(map[uuid.UUID]*entities.Priority),
|
||||||
|
institutions: make(map[uuid.UUID]*entities.Institution),
|
||||||
|
recipients: make(map[uuid.UUID]*entities.LetterIncomingRecipient),
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error, 4)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDs)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDs)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
result.recipients, err = s.processor.GetBatchRecipientsByUser(ctx, letterIDs, userID)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
if err := <-errChan; err != nil {
|
||||||
|
logger.FromContext(ctx).Error("batch load error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChan <- result
|
||||||
|
}()
|
||||||
|
|
||||||
|
batchData := <-resultChan
|
||||||
|
|
||||||
|
respList := make([]contract.IncomingLetterResponse, 0, len(letters))
|
||||||
|
for _, letter := range letters {
|
||||||
|
attachments := batchData.attachments[letter.ID]
|
||||||
|
if attachments == nil {
|
||||||
|
attachments = []entities.LetterIncomingAttachment{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var priority *entities.Priority
|
||||||
|
if letter.PriorityID != nil {
|
||||||
|
priority = batchData.priorities[*letter.PriorityID]
|
||||||
|
}
|
||||||
|
|
||||||
|
var institution *entities.Institution
|
||||||
|
if letter.SenderInstitutionID != nil {
|
||||||
|
institution = batchData.institutions[*letter.SenderInstitutionID]
|
||||||
|
}
|
||||||
|
|
||||||
|
isRead := false
|
||||||
|
if recipient, exists := batchData.recipients[letter.ID]; exists && recipient != nil {
|
||||||
|
isRead = recipient.ReadAt != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := transformer.LetterEntityToContract(&letter, attachments, priority, institution)
|
||||||
|
resp.IsRead = isRead
|
||||||
|
respList = append(respList, *resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalUnread, _ := s.processor.CountUnreadByUser(ctx, userID)
|
||||||
|
|
||||||
|
return &contract.ListIncomingLettersResponse{
|
||||||
|
Letters: respList,
|
||||||
|
Pagination: transformer.CreatePaginationResponse(int(total), req.Page, req.Limit),
|
||||||
|
TotalUnread: totalUnread,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) {
|
||||||
|
return s.processor.GetLetterUnreadCounts(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
|
||||||
|
return s.processor.MarkIncomingLetterAsRead(ctx, letterID)
|
||||||
|
}
|
||||||
|
func (s *LetterServiceImpl) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
|
||||||
|
return s.processor.MarkOutgoingLetterAsRead(ctx, letterID)
|
||||||
}
|
}
|
||||||
func (s *LetterServiceImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
func (s *LetterServiceImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
return s.processor.UpdateIncomingLetter(ctx, id, req)
|
return s.processor.UpdateIncomingLetter(ctx, id, req)
|
||||||
@ -47,7 +423,25 @@ func (s *LetterServiceImpl) SoftDeleteIncomingLetter(ctx context.Context, id uui
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
||||||
return s.processor.CreateDispositions(ctx, req)
|
userID := appcontext.FromGinContext(ctx).UserID
|
||||||
|
req.CreatedBy = userID
|
||||||
|
|
||||||
|
if req.FromDepartment == uuid.Nil {
|
||||||
|
req.FromDepartment = appcontext.FromGinContext(ctx).DepartmentID
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *contract.ListDispositionsResponse
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
var err error
|
||||||
|
result, err = s.processor.CreateDispositions(txCtx, req)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
||||||
@ -61,3 +455,36 @@ func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.
|
|||||||
func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
return s.processor.UpdateDiscussion(ctx, letterID, discussionID, req)
|
return s.processor.UpdateDiscussion(ctx, letterID, discussionID, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) {
|
||||||
|
return s.processor.GetDepartmentDispositionStatus(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) {
|
||||||
|
// For now, delegate to the processor which handles this
|
||||||
|
// The processor needs to be refactored to remove context extraction
|
||||||
|
return s.processor.UpdateDispositionStatus(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) GetLetterCTA(ctx context.Context, letterID uuid.UUID) (*contract.LetterCTAResponse, error) {
|
||||||
|
departmentID := appcontext.FromGinContext(ctx).DepartmentID
|
||||||
|
return s.processor.GetLetterCTA(ctx, letterID, departmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
|
||||||
|
// Extract user context to archive only for the current user
|
||||||
|
appCtx := appcontext.FromGinContext(ctx)
|
||||||
|
userID := appCtx.UserID
|
||||||
|
|
||||||
|
// Archive letters only for the current user
|
||||||
|
archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.BulkArchiveLettersResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Letters archived successfully",
|
||||||
|
ArchivedCount: int(archivedCount),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
385
internal/service/notification_service.go
Normal file
385
internal/service/notification_service.go
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"eslogad-be/internal/config"
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
novu "github.com/novuhq/go-novu/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationService interface {
|
||||||
|
TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error)
|
||||||
|
BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error)
|
||||||
|
GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error)
|
||||||
|
UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationServiceImpl struct {
|
||||||
|
client *novu.APIClient
|
||||||
|
config *config.NovuConfig
|
||||||
|
userProcessor UserProcessorForNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProcessorForNotification interface {
|
||||||
|
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationService(cfg *config.NovuConfig, userProcessor UserProcessorForNotification) *NotificationServiceImpl {
|
||||||
|
var client *novu.APIClient
|
||||||
|
if cfg.APIKey != "" {
|
||||||
|
// Create Novu config with backend URL
|
||||||
|
novuConfig := &novu.Config{}
|
||||||
|
if cfg.BaseURL != "" {
|
||||||
|
backendURL, err := url.Parse(cfg.BaseURL)
|
||||||
|
if err == nil {
|
||||||
|
novuConfig.BackendURL = backendURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client = novu.NewAPIClient(cfg.APIKey, novuConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NotificationServiceImpl{
|
||||||
|
client: client,
|
||||||
|
config: cfg,
|
||||||
|
userProcessor: userProcessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error) {
|
||||||
|
if s.client == nil {
|
||||||
|
return &contract.TriggerNotificationResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "notification service not configured",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := req.UserID.String()
|
||||||
|
|
||||||
|
_, err := s.ensureSubscriberExists(ctx, req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return &contract.TriggerNotificationResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the trigger payload
|
||||||
|
to := map[string]interface{}{
|
||||||
|
"subscriberId": subscriberID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add additional recipient information if provided
|
||||||
|
if req.To != nil {
|
||||||
|
if req.To.Email != "" {
|
||||||
|
to["email"] = req.To.Email
|
||||||
|
}
|
||||||
|
if req.To.Phone != "" {
|
||||||
|
to["phone"] = req.To.Phone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare overrides
|
||||||
|
overrides := make(map[string]interface{})
|
||||||
|
if req.Overrides != nil {
|
||||||
|
if req.Overrides.Email != nil {
|
||||||
|
overrides["email"] = req.Overrides.Email
|
||||||
|
}
|
||||||
|
if req.Overrides.SMS != nil {
|
||||||
|
overrides["sms"] = req.Overrides.SMS
|
||||||
|
}
|
||||||
|
if req.Overrides.InApp != nil {
|
||||||
|
overrides["in_app"] = req.Overrides.InApp
|
||||||
|
}
|
||||||
|
if req.Overrides.Push != nil {
|
||||||
|
overrides["push"] = req.Overrides.Push
|
||||||
|
}
|
||||||
|
if req.Overrides.Chat != nil {
|
||||||
|
overrides["chat"] = req.Overrides.Chat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the notification using the template ID
|
||||||
|
triggerPayload := novu.ITriggerPayloadOptions{
|
||||||
|
To: to,
|
||||||
|
Payload: req.TemplateData,
|
||||||
|
Overrides: overrides,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.client.EventApi.Trigger(ctx, req.TemplateID, triggerPayload)
|
||||||
|
if err != nil {
|
||||||
|
return &contract.TriggerNotificationResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("failed to trigger notification: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract transaction ID from response
|
||||||
|
transactionID := ""
|
||||||
|
if respData, ok := resp.Data.(map[string]interface{}); ok {
|
||||||
|
if txID, exists := respData["transactionId"]; exists {
|
||||||
|
transactionID = fmt.Sprintf("%v", txID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.TriggerNotificationResponse{
|
||||||
|
Success: true,
|
||||||
|
TransactionID: transactionID,
|
||||||
|
Message: "notification triggered successfully",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error) {
|
||||||
|
if s.client == nil {
|
||||||
|
return &contract.BulkTriggerNotificationResponse{
|
||||||
|
Success: false,
|
||||||
|
TotalSent: 0,
|
||||||
|
TotalFailed: len(req.UserIDs),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]contract.NotificationResult, 0, len(req.UserIDs))
|
||||||
|
successCount := 0
|
||||||
|
failedCount := 0
|
||||||
|
|
||||||
|
for _, userID := range req.UserIDs {
|
||||||
|
// Create individual trigger request
|
||||||
|
triggerReq := &contract.TriggerNotificationRequest{
|
||||||
|
UserID: userID,
|
||||||
|
TemplateID: req.TemplateID,
|
||||||
|
TemplateData: req.TemplateData,
|
||||||
|
Overrides: req.Overrides,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.TriggerNotification(ctx, triggerReq)
|
||||||
|
|
||||||
|
result := contract.NotificationResult{
|
||||||
|
UserID: userID,
|
||||||
|
Success: resp.Success,
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Success {
|
||||||
|
result.TransactionID = resp.TransactionID
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
} else {
|
||||||
|
result.Error = resp.Message
|
||||||
|
}
|
||||||
|
failedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.BulkTriggerNotificationResponse{
|
||||||
|
Success: failedCount == 0,
|
||||||
|
TotalSent: successCount,
|
||||||
|
TotalFailed: failedCount,
|
||||||
|
Results: results,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error) {
|
||||||
|
if s.client == nil {
|
||||||
|
return nil, fmt.Errorf("notification service not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := userID.String()
|
||||||
|
|
||||||
|
// Try to get the subscriber
|
||||||
|
subscriber, err := s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||||
|
if err != nil {
|
||||||
|
// If subscriber doesn't exist, create it
|
||||||
|
_, createErr := s.ensureSubscriberExists(ctx, userID)
|
||||||
|
if createErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get or create subscriber: %w", createErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get again after creation
|
||||||
|
subscriber, err = s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get subscriber after creation: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Novu subscriber to our response format
|
||||||
|
response := &contract.GetSubscriberResponse{
|
||||||
|
SubscriberID: subscriberID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if subData, ok := subscriber.Data.(map[string]interface{}); ok {
|
||||||
|
if email, exists := subData["email"]; exists {
|
||||||
|
response.Email = fmt.Sprintf("%v", email)
|
||||||
|
}
|
||||||
|
if firstName, exists := subData["firstName"]; exists {
|
||||||
|
response.FirstName = fmt.Sprintf("%v", firstName)
|
||||||
|
}
|
||||||
|
if lastName, exists := subData["lastName"]; exists {
|
||||||
|
response.LastName = fmt.Sprintf("%v", lastName)
|
||||||
|
}
|
||||||
|
if phone, exists := subData["phone"]; exists {
|
||||||
|
response.Phone = fmt.Sprintf("%v", phone)
|
||||||
|
}
|
||||||
|
if avatar, exists := subData["avatar"]; exists {
|
||||||
|
response.Avatar = fmt.Sprintf("%v", avatar)
|
||||||
|
}
|
||||||
|
if data, exists := subData["data"]; exists {
|
||||||
|
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||||
|
response.Data = dataMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if channels, exists := subData["channels"]; exists {
|
||||||
|
if channelList, ok := channels.([]interface{}); ok {
|
||||||
|
response.Channels = make([]contract.ChannelCredentials, 0, len(channelList))
|
||||||
|
for _, ch := range channelList {
|
||||||
|
if chMap, ok := ch.(map[string]interface{}); ok {
|
||||||
|
channelCred := contract.ChannelCredentials{}
|
||||||
|
if chType, exists := chMap["providerId"]; exists {
|
||||||
|
channelCred.Channel = fmt.Sprintf("%v", chType)
|
||||||
|
}
|
||||||
|
if creds, exists := chMap["credentials"]; exists {
|
||||||
|
if credMap, ok := creds.(map[string]interface{}); ok {
|
||||||
|
channelCred.Credentials = credMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.Channels = append(response.Channels, channelCred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error) {
|
||||||
|
if s.client == nil {
|
||||||
|
return &contract.UpdateSubscriberChannelResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "notification service not configured",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberID := req.UserID.String()
|
||||||
|
|
||||||
|
// Ensure subscriber exists
|
||||||
|
_, err := s.ensureSubscriberExists(ctx, req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return &contract.UpdateSubscriberChannelResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the Novu Go SDK doesn't have UpdateCredentials, we'll update the subscriber data instead
|
||||||
|
// Get user info to update subscriber
|
||||||
|
user, err := s.userProcessor.GetUserByID(ctx, req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return &contract.UpdateSubscriberChannelResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("failed to get user data: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare subscriber data with channel credentials stored in data
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"updatedAt": user.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store channel credentials in the subscriber data
|
||||||
|
channelKey := fmt.Sprintf("channel_%s", req.Channel)
|
||||||
|
data[channelKey] = req.Credentials
|
||||||
|
|
||||||
|
// Update subscriber with new data
|
||||||
|
updateData := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.client.SubscriberApi.Update(ctx, subscriberID, updateData)
|
||||||
|
if err != nil {
|
||||||
|
return &contract.UpdateSubscriberChannelResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("failed to update subscriber channel: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.UpdateSubscriberChannelResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "subscriber channel updated successfully",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) ensureSubscriberExists(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||||
|
subscriberID := userID.String()
|
||||||
|
|
||||||
|
_, err := s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.userProcessor.GetUserByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get user data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"userId": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"isActive": user.IsActive,
|
||||||
|
"createdAt": user.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Roles != nil && len(user.Roles) > 0 {
|
||||||
|
roles := make([]map[string]interface{}, len(user.Roles))
|
||||||
|
for i, role := range user.Roles {
|
||||||
|
roles[i] = map[string]interface{}{
|
||||||
|
"id": role.ID.String(),
|
||||||
|
"name": role.Name,
|
||||||
|
"code": role.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["roles"] = roles
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 {
|
||||||
|
depts := make([]map[string]interface{}, len(user.DepartmentResponse))
|
||||||
|
for i, dept := range user.DepartmentResponse {
|
||||||
|
depts[i] = map[string]interface{}{
|
||||||
|
"id": dept.ID.String(),
|
||||||
|
"name": dept.Name,
|
||||||
|
"code": dept.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["departments"] = depts
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber := novu.SubscriberPayload{
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.Name,
|
||||||
|
LastName: "",
|
||||||
|
Phone: "",
|
||||||
|
Avatar: "",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ type UserProcessor interface {
|
|||||||
CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error)
|
CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error)
|
||||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||||
|
GetUserByIDLight(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||||
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
|
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
|
||||||
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
|
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
|
||||||
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
||||||
|
|||||||
@ -65,6 +65,38 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LetterIncomingEntityToContract(e *entities.LetterIncoming) *contract.IncomingLetterResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &contract.IncomingLetterResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
LetterNumber: e.LetterNumber,
|
||||||
|
ReferenceNumber: e.ReferenceNumber,
|
||||||
|
Subject: e.Subject,
|
||||||
|
Description: e.Description,
|
||||||
|
ReceivedDate: e.ReceivedDate,
|
||||||
|
DueDate: e.DueDate,
|
||||||
|
Status: string(e.Status),
|
||||||
|
CreatedBy: e.CreatedBy, // Will be set conditionally
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
Attachments: make([]contract.IncomingLetterAttachmentResponse, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DepartmentEntityToContract(e *entities.Department) *contract.DepartmentResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &contract.DepartmentResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Code: e.Code,
|
||||||
|
Name: e.Name,
|
||||||
|
Path: e.Path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func DispositionsToContract(list []entities.LetterIncomingDisposition) []contract.DispositionResponse {
|
func DispositionsToContract(list []entities.LetterIncomingDisposition) []contract.DispositionResponse {
|
||||||
out := make([]contract.DispositionResponse, 0, len(list))
|
out := make([]contract.DispositionResponse, 0, len(list))
|
||||||
for _, d := range list {
|
for _, d := range list {
|
||||||
|
|||||||
25
migrations/000028_add_status_disposition_department.down.sql
Normal file
25
migrations/000028_add_status_disposition_department.down.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- Rollback: Remove status and letter_incoming_id columns from letter_incoming_dispositions_department table
|
||||||
|
|
||||||
|
-- Drop the constraint first
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
DROP CONSTRAINT IF EXISTS check_disposition_department_status;
|
||||||
|
|
||||||
|
-- Drop indexes
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_dispositions_department_status;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_dispositions_department_letter_id;
|
||||||
|
|
||||||
|
-- Drop the columns
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
DROP COLUMN IF EXISTS status;
|
||||||
|
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
DROP COLUMN IF EXISTS letter_incoming_id;
|
||||||
|
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
DROP COLUMN IF EXISTS read_at;
|
||||||
|
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
DROP COLUMN IF EXISTS completed_at;
|
||||||
|
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
DROP COLUMN IF EXISTS updated_at;
|
||||||
38
migrations/000028_add_status_disposition_department.up.sql
Normal file
38
migrations/000028_add_status_disposition_department.up.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
-- Add status and letter_incoming_id columns to letter_incoming_dispositions_department table
|
||||||
|
|
||||||
|
-- Add letter_incoming_id column
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
ADD COLUMN IF NOT EXISTS letter_incoming_id UUID NOT NULL REFERENCES letters_incoming(id);
|
||||||
|
|
||||||
|
-- Add status column with default value
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'pending';
|
||||||
|
|
||||||
|
-- Add read_at column
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
ADD COLUMN IF NOT EXISTS read_at TIMESTAMP;
|
||||||
|
|
||||||
|
-- Add completed_at column
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP;
|
||||||
|
|
||||||
|
-- Add updated_at column
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- Create index for faster queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_status
|
||||||
|
ON letter_incoming_dispositions_department(department_id, status);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_letter_id
|
||||||
|
ON letter_incoming_dispositions_department(letter_incoming_id);
|
||||||
|
|
||||||
|
-- Update existing records to have status 'dispositioned' (assuming existing records are already dispositioned)
|
||||||
|
UPDATE letter_incoming_dispositions_department
|
||||||
|
SET status = 'dispositioned'
|
||||||
|
WHERE status = 'pending';
|
||||||
|
|
||||||
|
-- Add constraint to ensure valid status values
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
ADD CONSTRAINT check_disposition_department_status
|
||||||
|
CHECK (status IN ('pending', 'dispositioned', 'read', 'completed'));
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Delete new setting
|
||||||
|
DELETE FROM app_settings WHERE key = 'INCOMING_LETTER_DEPARTMENT_RECIPIENTS';
|
||||||
|
|
||||||
|
-- Restore old setting
|
||||||
|
INSERT INTO app_settings(key, value)
|
||||||
|
VALUES
|
||||||
|
('INCOMING_LETTER_RECIPIENTS', '{"department_codes": ["aslog"]}'::jsonb)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Delete old setting if exists
|
||||||
|
DELETE FROM app_settings WHERE key = 'INCOMING_LETTER_RECIPIENTS';
|
||||||
|
|
||||||
|
-- Insert new setting with department IDs as a direct array
|
||||||
|
-- Using empty array as placeholder that should be replaced with actual department IDs
|
||||||
|
INSERT INTO app_settings(key, value)
|
||||||
|
VALUES
|
||||||
|
('INCOMING_LETTER_DEPARTMENT_RECIPIENTS', '[]'::jsonb)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Example to set actual department IDs (uncomment and modify as needed):
|
||||||
|
-- UPDATE app_settings
|
||||||
|
-- SET value = '["ae66af10-bcb7-4939-8110-940d2a387e19"]'::jsonb
|
||||||
|
-- WHERE key = 'INCOMING_LETTER_DEPARTMENT_RECIPIENTS';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
16
migrations/000030_add_performance_indexes.down.sql
Normal file
16
migrations/000030_add_performance_indexes.down.sql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop indexes
|
||||||
|
DROP INDEX IF EXISTS idx_users_email;
|
||||||
|
DROP INDEX IF EXISTS idx_user_department_user_id;
|
||||||
|
DROP INDEX IF EXISTS idx_user_department_department_id;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_user_id;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_letter_id;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_outgoing_status;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_outgoing_deleted_at;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_incoming_status;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_incoming_deleted_at;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_recipients_dept_id;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_recipients_user_id;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
26
migrations/000030_add_performance_indexes.up.sql
Normal file
26
migrations/000030_add_performance_indexes.up.sql
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Add index for users.email for faster email lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
|
|
||||||
|
-- Add indexes for user_department relationship (singular table name)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_department_user_id ON user_department(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_department_department_id ON user_department(department_id);
|
||||||
|
|
||||||
|
-- Add indexes for letter_outgoing_recipients for faster joins
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_user_id ON letter_outgoing_recipients(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_letter_id ON letter_outgoing_recipients(letter_id);
|
||||||
|
|
||||||
|
-- Add index for letters_outgoing status queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_status ON letters_outgoing(status) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_deleted_at ON letters_outgoing(deleted_at);
|
||||||
|
|
||||||
|
-- Add index for letters_incoming status queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_incoming_status ON letters_incoming(status) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_incoming_deleted_at ON letters_incoming(deleted_at);
|
||||||
|
|
||||||
|
-- Add index for letter_incoming_recipients for department lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_dept_id ON letter_incoming_recipients(recipient_department_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_user_id ON letter_incoming_recipients(recipient_user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
9
migrations/000031_add_missing_indexes.down.sql
Normal file
9
migrations/000031_add_missing_indexes.down.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop indexes
|
||||||
|
DROP INDEX IF EXISTS idx_user_profiles_user_id;
|
||||||
|
DROP INDEX IF EXISTS idx_departments_id;
|
||||||
|
DROP INDEX IF EXISTS idx_user_department_composite;
|
||||||
|
DROP INDEX IF EXISTS idx_users_id_not_deleted;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
11
migrations/000031_add_missing_indexes.up.sql
Normal file
11
migrations/000031_add_missing_indexes.up.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_profiles_user_id ON user_profiles(user_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_departments_id ON departments(id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_department_composite ON user_department(user_id, department_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_id_not_deleted ON users(id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
20
migrations/000032_add_system_user_and_department.down.sql
Normal file
20
migrations/000032_add_system_user_and_department.down.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Remove system user from department (cascade should handle this, but being explicit)
|
||||||
|
DELETE FROM user_department
|
||||||
|
WHERE user_id = '11111111-2222-3333-4444-555555555555'::uuid
|
||||||
|
AND department_id = '11111111-2222-3333-4444-555555555555'::uuid;
|
||||||
|
|
||||||
|
-- Remove system user profile
|
||||||
|
DELETE FROM user_profiles
|
||||||
|
WHERE user_id = '11111111-2222-3333-4444-555555555555'::uuid;
|
||||||
|
|
||||||
|
-- Remove system user
|
||||||
|
DELETE FROM users
|
||||||
|
WHERE id = '11111111-2222-3333-4444-555555555555'::uuid;
|
||||||
|
|
||||||
|
-- Remove system department
|
||||||
|
DELETE FROM departments
|
||||||
|
WHERE id = '11111111-2222-3333-4444-555555555555'::uuid;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
88
migrations/000032_add_system_user_and_department.up.sql
Normal file
88
migrations/000032_add_system_user_and_department.up.sql
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
INSERT INTO departments (id, name, code, path, created_at, updated_at)
|
||||||
|
VALUES (
|
||||||
|
'11111111-2222-3333-4444-555555555555'::uuid,
|
||||||
|
'System',
|
||||||
|
'SYSTEM',
|
||||||
|
'system'::ltree,
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO users (
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
password_hash,
|
||||||
|
status,
|
||||||
|
is_active,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'11111111-2222-3333-4444-555555555555'::uuid,
|
||||||
|
'system',
|
||||||
|
'system@eslogad.internal',
|
||||||
|
'System User',
|
||||||
|
'$2a$10$SYSTEM.USER.SHOULD.NEVER.LOGIN.WITH.PASSWORD.HASH',
|
||||||
|
'active',
|
||||||
|
true,
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Create user profile for system user
|
||||||
|
INSERT INTO user_profiles (
|
||||||
|
user_id,
|
||||||
|
full_name,
|
||||||
|
display_name,
|
||||||
|
job_title,
|
||||||
|
preferences,
|
||||||
|
notification_prefs,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'11111111-2222-3333-4444-555555555555'::uuid,
|
||||||
|
'System User',
|
||||||
|
'System',
|
||||||
|
'Automated Process',
|
||||||
|
'{}'::jsonb,
|
||||||
|
'{}'::jsonb,
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
) ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Link system user to system department
|
||||||
|
INSERT INTO user_department (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
department_id,
|
||||||
|
is_primary,
|
||||||
|
assigned_at,
|
||||||
|
removed_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
gen_random_uuid(),
|
||||||
|
'11111111-2222-3333-4444-555555555555'::uuid,
|
||||||
|
'11111111-2222-3333-4444-555555555555'::uuid,
|
||||||
|
true,
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
NULL
|
||||||
|
) ON CONFLICT (user_id, department_id) WHERE removed_at IS NULL DO NOTHING;
|
||||||
|
|
||||||
|
-- Optionally, create a system role if your application uses roles
|
||||||
|
-- Uncomment and modify if needed:
|
||||||
|
-- INSERT INTO roles (id, code, name, description, created_at, updated_at)
|
||||||
|
-- VALUES (
|
||||||
|
-- gen_random_uuid(),
|
||||||
|
-- 'SYSTEM',
|
||||||
|
-- 'System Role',
|
||||||
|
-- 'Role for system automated processes',
|
||||||
|
-- CURRENT_TIMESTAMP,
|
||||||
|
-- CURRENT_TIMESTAMP
|
||||||
|
-- ) ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop the index first
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_disposition_dept_status_notes;
|
||||||
|
|
||||||
|
-- Remove notes column from letter_incoming_disposition_department table
|
||||||
|
ALTER TABLE letter_incoming_disposition_department
|
||||||
|
DROP COLUMN IF EXISTS notes;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
12
migrations/000033_add_notes_to_disposition_department.up.sql
Normal file
12
migrations/000033_add_notes_to_disposition_department.up.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Add notes column to letter_incoming_dispositions_department table
|
||||||
|
ALTER TABLE letter_incoming_dispositions_department
|
||||||
|
ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||||
|
|
||||||
|
-- Add index for better performance when querying by status with notes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_disposition_dept_status_notes
|
||||||
|
ON letter_incoming_dispositions_department(status)
|
||||||
|
WHERE notes IS NOT NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
17
migrations/000034_add_archived_indexes.down.sql
Normal file
17
migrations/000034_add_archived_indexes.down.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-- Drop indexes for letter_incoming_recipients
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_recipients_archived;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_recipients_user_archived;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_recipients_letter_archived;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_incoming_recipients_dept_archived;
|
||||||
|
|
||||||
|
-- Drop indexes for letter_outgoing_recipients
|
||||||
|
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_archived;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_user_archived;
|
||||||
|
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_letter_archived;
|
||||||
|
|
||||||
|
-- Drop the is_archived columns
|
||||||
|
ALTER TABLE letter_incoming_recipients
|
||||||
|
DROP COLUMN IF EXISTS is_archived;
|
||||||
|
|
||||||
|
ALTER TABLE letter_outgoing_recipients
|
||||||
|
DROP COLUMN IF EXISTS is_archived;
|
||||||
36
migrations/000034_add_archived_indexes.up.sql
Normal file
36
migrations/000034_add_archived_indexes.up.sql
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
-- Add is_archived column to letter_incoming_recipients if it doesn't exist
|
||||||
|
ALTER TABLE letter_incoming_recipients
|
||||||
|
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Add is_archived column to letter_outgoing_recipients if it doesn't exist
|
||||||
|
ALTER TABLE letter_outgoing_recipients
|
||||||
|
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Add indexes for efficient archiving queries on letter_incoming_recipients
|
||||||
|
-- Note: letter_incoming_recipients uses recipient_user_id and recipient_department_id
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_archived
|
||||||
|
ON letter_incoming_recipients(is_archived, recipient_user_id, letter_id)
|
||||||
|
WHERE is_archived = false;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_user_archived
|
||||||
|
ON letter_incoming_recipients(recipient_user_id, is_archived, letter_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_letter_archived
|
||||||
|
ON letter_incoming_recipients(letter_id, is_archived);
|
||||||
|
|
||||||
|
-- Add indexes for efficient archiving queries on letter_outgoing_recipients
|
||||||
|
-- Note: letter_outgoing_recipients uses user_id and department_id
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_archived
|
||||||
|
ON letter_outgoing_recipients(is_archived, user_id, letter_id)
|
||||||
|
WHERE is_archived = false;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_user_archived
|
||||||
|
ON letter_outgoing_recipients(user_id, is_archived, letter_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_letter_archived
|
||||||
|
ON letter_outgoing_recipients(letter_id, is_archived);
|
||||||
|
|
||||||
|
-- Add composite index for filtering non-archived letters by department
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_dept_archived
|
||||||
|
ON letter_incoming_recipients(recipient_department_id, is_archived, letter_id)
|
||||||
|
WHERE is_archived = false;
|
||||||
Loading…
x
Reference in New Issue
Block a user