letter incoming

This commit is contained in:
Aditya Siregar 2025-08-09 18:58:22 +07:00
parent 001d02c587
commit 61d6eed373
37 changed files with 2773 additions and 33 deletions

View File

@ -44,6 +44,9 @@ func (a *App) Initialize(cfg *config.Config) error {
healthHandler := handler.NewHealthHandler() healthHandler := handler.NewHealthHandler()
fileHandler := handler.NewFileHandler(services.fileService) fileHandler := handler.NewFileHandler(services.fileService)
rbacHandler := handler.NewRBACHandler(services.rbacService) rbacHandler := handler.NewRBACHandler(services.rbacService)
masterHandler := handler.NewMasterHandler(services.masterService)
letterHandler := handler.NewLetterHandler(services.letterService)
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
a.router = router.NewRouter( a.router = router.NewRouter(
cfg, cfg,
@ -53,6 +56,9 @@ func (a *App) Initialize(cfg *config.Config) error {
handler.NewUserHandler(services.userService, validator.NewUserValidator()), handler.NewUserHandler(services.userService, validator.NewUserValidator()),
fileHandler, fileHandler,
rbacHandler, rbacHandler,
masterHandler,
letterHandler,
dispositionRouteHandler,
) )
return nil return nil
@ -98,36 +104,70 @@ func (a *App) Shutdown() {
} }
type repositories struct { type repositories struct {
userRepo *repository.UserRepositoryImpl userRepo *repository.UserRepositoryImpl
userProfileRepo *repository.UserProfileRepository userProfileRepo *repository.UserProfileRepository
titleRepo *repository.TitleRepository titleRepo *repository.TitleRepository
rbacRepo *repository.RBACRepository rbacRepo *repository.RBACRepository
labelRepo *repository.LabelRepository
priorityRepo *repository.PriorityRepository
institutionRepo *repository.InstitutionRepository
dispRepo *repository.DispositionActionRepository
letterRepo *repository.LetterIncomingRepository
letterAttachRepo *repository.LetterIncomingAttachmentRepository
activityLogRepo *repository.LetterIncomingActivityLogRepository
dispositionRouteRepo *repository.DispositionRouteRepository
// new repos
letterDispositionRepo *repository.LetterDispositionRepository
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
dispositionNoteRepo *repository.DispositionNoteRepository
letterDiscussionRepo *repository.LetterDiscussionRepository
} }
func (a *App) initRepositories() *repositories { func (a *App) initRepositories() *repositories {
return &repositories{ return &repositories{
userRepo: repository.NewUserRepository(a.db), userRepo: repository.NewUserRepository(a.db),
userProfileRepo: repository.NewUserProfileRepository(a.db), userProfileRepo: repository.NewUserProfileRepository(a.db),
titleRepo: repository.NewTitleRepository(a.db), titleRepo: repository.NewTitleRepository(a.db),
rbacRepo: repository.NewRBACRepository(a.db), rbacRepo: repository.NewRBACRepository(a.db),
labelRepo: repository.NewLabelRepository(a.db),
priorityRepo: repository.NewPriorityRepository(a.db),
institutionRepo: repository.NewInstitutionRepository(a.db),
dispRepo: repository.NewDispositionActionRepository(a.db),
letterRepo: repository.NewLetterIncomingRepository(a.db),
letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db),
activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db),
dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db),
letterDispositionRepo: repository.NewLetterDispositionRepository(a.db),
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
} }
} }
type processors struct { type processors struct {
userProcessor *processor.UserProcessorImpl userProcessor *processor.UserProcessorImpl
letterProcessor *processor.LetterProcessorImpl
activityLogger *processor.ActivityLogProcessorImpl
} }
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
txMgr := repository.NewTxManager(a.db)
activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
return &processors{ return &processors{
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo), userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo),
activityLogger: activity,
} }
} }
type services struct { type services struct {
userService *service.UserServiceImpl userService *service.UserServiceImpl
authService *service.AuthServiceImpl authService *service.AuthServiceImpl
fileService *service.FileServiceImpl fileService *service.FileServiceImpl
rbacService *service.RBACServiceImpl rbacService *service.RBACServiceImpl
masterService *service.MasterServiceImpl
letterService *service.LetterServiceImpl
dispositionRouteService *service.DispositionRouteServiceImpl
} }
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
@ -137,18 +177,25 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
userSvc := service.NewUserService(processors.userProcessor, repos.titleRepo) userSvc := service.NewUserService(processors.userProcessor, repos.titleRepo)
// File storage client and service
fileCfg := cfg.S3Config fileCfg := cfg.S3Config
s3Client := client.NewFileClient(fileCfg) s3Client := client.NewFileClient(fileCfg)
fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents") fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents")
rbacSvc := service.NewRBACService(repos.rbacRepo) rbacSvc := service.NewRBACService(repos.rbacRepo)
masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo)
letterSvc := service.NewLetterService(processors.letterProcessor)
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
return &services{ return &services{
userService: userSvc, userService: userSvc,
authService: authService, authService: authService,
fileService: fileSvc, fileService: fileSvc,
rbacService: rbacSvc, rbacService: rbacSvc,
masterService: masterSvc,
letterService: letterSvc,
dispositionRouteService: dispRouteSvc,
} }
} }

View File

@ -47,3 +47,118 @@ type HealthResponse struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Version string `json:"version"` Version string `json:"version"`
} }
type LabelResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Color *string `json:"color,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateLabelRequest struct {
Name string `json:"name"`
Color *string `json:"color,omitempty"`
}
type UpdateLabelRequest struct {
Name *string `json:"name,omitempty"`
Color *string `json:"color,omitempty"`
}
type ListLabelsResponse struct {
Labels []LabelResponse `json:"labels"`
}
type PriorityResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Level int `json:"level"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreatePriorityRequest struct {
Name string `json:"name"`
Level int `json:"level"`
}
type UpdatePriorityRequest struct {
Name *string `json:"name,omitempty"`
Level *int `json:"level,omitempty"`
}
type ListPrioritiesResponse struct {
Priorities []PriorityResponse `json:"priorities"`
}
type InstitutionResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Address *string `json:"address,omitempty"`
ContactPerson *string `json:"contact_person,omitempty"`
Phone *string `json:"phone,omitempty"`
Email *string `json:"email,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateInstitutionRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Address *string `json:"address,omitempty"`
ContactPerson *string `json:"contact_person,omitempty"`
Phone *string `json:"phone,omitempty"`
Email *string `json:"email,omitempty"`
}
type UpdateInstitutionRequest struct {
Name *string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
Address *string `json:"address,omitempty"`
ContactPerson *string `json:"contact_person,omitempty"`
Phone *string `json:"phone,omitempty"`
Email *string `json:"email,omitempty"`
}
type ListInstitutionsResponse struct {
Institutions []InstitutionResponse `json:"institutions"`
}
type DispositionActionResponse struct {
ID string `json:"id"`
Code string `json:"code"`
Label string `json:"label"`
Description *string `json:"description,omitempty"`
RequiresNote bool `json:"requires_note"`
GroupName *string `json:"group_name,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateDispositionActionRequest struct {
Code string `json:"code"`
Label string `json:"label"`
Description *string `json:"description,omitempty"`
RequiresNote *bool `json:"requires_note,omitempty"`
GroupName *string `json:"group_name,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
}
type UpdateDispositionActionRequest struct {
Code *string `json:"code,omitempty"`
Label *string `json:"label,omitempty"`
Description *string `json:"description,omitempty"`
RequiresNote *bool `json:"requires_note,omitempty"`
GroupName *string `json:"group_name,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
}
type ListDispositionActionsResponse struct {
Actions []DispositionActionResponse `json:"actions"`
}

View File

@ -0,0 +1,33 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type DispositionRouteResponse struct {
ID uuid.UUID `json:"id"`
FromDepartmentID uuid.UUID `json:"from_department_id"`
ToDepartmentID uuid.UUID `json:"to_department_id"`
IsActive bool `json:"is_active"`
AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateDispositionRouteRequest struct {
FromDepartmentID uuid.UUID `json:"from_department_id"`
ToDepartmentID uuid.UUID `json:"to_department_id"`
IsActive *bool `json:"is_active,omitempty"`
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
}
type UpdateDispositionRouteRequest struct {
IsActive *bool `json:"is_active,omitempty"`
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
}
type ListDispositionRoutesResponse struct {
Routes []DispositionRouteResponse `json:"routes"`
}

View File

@ -0,0 +1,122 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateIncomingLetterAttachment struct {
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
FileType string `json:"file_type"`
}
type CreateIncomingLetterRequest struct {
ReferenceNumber *string `json:"reference_number,omitempty"`
Subject string `json:"subject"`
Description *string `json:"description,omitempty"`
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
ReceivedDate time.Time `json:"received_date"`
DueDate *time.Time `json:"due_date,omitempty"`
Attachments []CreateIncomingLetterAttachment `json:"attachments,omitempty"`
}
type IncomingLetterAttachmentResponse struct {
ID uuid.UUID `json:"id"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
FileType string `json:"file_type"`
UploadedAt time.Time `json:"uploaded_at"`
}
type IncomingLetterResponse struct {
ID uuid.UUID `json:"id"`
LetterNumber string `json:"letter_number"`
ReferenceNumber *string `json:"reference_number,omitempty"`
Subject string `json:"subject"`
Description *string `json:"description,omitempty"`
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
ReceivedDate time.Time `json:"received_date"`
DueDate *time.Time `json:"due_date,omitempty"`
Status string `json:"status"`
CreatedBy uuid.UUID `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Attachments []IncomingLetterAttachmentResponse `json:"attachments"`
}
type UpdateIncomingLetterRequest struct {
ReferenceNumber *string `json:"reference_number,omitempty"`
Subject *string `json:"subject,omitempty"`
Description *string `json:"description,omitempty"`
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
ReceivedDate *time.Time `json:"received_date,omitempty"`
DueDate *time.Time `json:"due_date,omitempty"`
Status *string `json:"status,omitempty"`
}
type ListIncomingLettersRequest struct {
Page int `json:"page"`
Limit int `json:"limit"`
Status *string `json:"status,omitempty"`
Query *string `json:"query,omitempty"`
}
type ListIncomingLettersResponse struct {
Letters []IncomingLetterResponse `json:"letters"`
Pagination PaginationResponse `json:"pagination"`
}
type CreateDispositionActionSelection struct {
ActionID uuid.UUID `json:"action_id"`
Note *string `json:"note,omitempty"`
}
type CreateLetterDispositionRequest struct {
LetterID uuid.UUID `json:"letter_id"`
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
Notes *string `json:"notes,omitempty"`
SelectedActions []CreateDispositionActionSelection `json:"selected_actions,omitempty"`
}
type DispositionResponse struct {
ID uuid.UUID `json:"id"`
LetterID uuid.UUID `json:"letter_id"`
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
Notes *string `json:"notes,omitempty"`
Status string `json:"status"`
CreatedBy uuid.UUID `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
type ListDispositionsResponse struct {
Dispositions []DispositionResponse `json:"dispositions"`
}
type CreateLetterDiscussionRequest struct {
ParentID *uuid.UUID `json:"parent_id,omitempty"`
Message string `json:"message"`
Mentions map[string]interface{} `json:"mentions,omitempty"`
}
type UpdateLetterDiscussionRequest struct {
Message string `json:"message"`
Mentions map[string]interface{} `json:"mentions,omitempty"`
}
type LetterDiscussionResponse struct {
ID uuid.UUID `json:"id"`
LetterID uuid.UUID `json:"letter_id"`
ParentID *uuid.UUID `json:"parent_id,omitempty"`
UserID uuid.UUID `json:"user_id"`
Message string `json:"message"`
Mentions map[string]interface{} `json:"mentions,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
EditedAt *time.Time `json:"edited_at,omitempty"`
}

View File

@ -0,0 +1,22 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type DispositionAction struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Code string `gorm:"uniqueIndex;not null" json:"code"`
Label string `gorm:"not null" json:"label"`
Description *string `json:"description,omitempty"`
RequiresNote bool `gorm:"not null;default:false" json:"requires_note"`
GroupName *string `json:"group_name,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
IsActive bool `gorm:"not null;default:true" json:"is_active"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (DispositionAction) TableName() string { return "disposition_actions" }

View File

@ -0,0 +1,19 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type DispositionRoute struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
FromDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"from_department_id"`
ToDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"to_department_id"`
IsActive bool `gorm:"not null;default:true" json:"is_active"`
AllowedActions JSONB `gorm:"type:jsonb" json:"allowed_actions,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (DispositionRoute) TableName() string { return "disposition_routes" }

View File

@ -0,0 +1,30 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type InstitutionType string
const (
InstGovernment InstitutionType = "government"
InstPrivate InstitutionType = "private"
InstNGO InstitutionType = "ngo"
InstIndividual InstitutionType = "individual"
)
type Institution struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Name string `gorm:"not null;size:255" json:"name"`
Type InstitutionType `gorm:"not null;size:32" json:"type"`
Address *string `json:"address,omitempty"`
ContactPerson *string `gorm:"size:255" json:"contact_person,omitempty"`
Phone *string `gorm:"size:50" json:"phone,omitempty"`
Email *string `gorm:"size:255" json:"email,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Institution) TableName() string { return "institutions" }

View File

@ -0,0 +1,17 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type Label struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Name string `gorm:"not null;size:255" json:"name"`
Color *string `gorm:"size:16" json:"color,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Label) TableName() string { return "labels" }

View File

@ -0,0 +1,21 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type LetterDiscussion struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
ParentID *uuid.UUID `json:"parent_id,omitempty"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
Message string `gorm:"not null" json:"message"`
Mentions JSONB `gorm:"type:jsonb" json:"mentions,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
EditedAt *time.Time `json:"edited_at,omitempty"`
}
func (LetterDiscussion) TableName() string { return "letter_incoming_discussions" }

View File

@ -0,0 +1,55 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type LetterDispositionStatus string
const (
DispositionPending LetterDispositionStatus = "pending"
DispositionRead LetterDispositionStatus = "read"
DispositionRejected LetterDispositionStatus = "rejected"
DispositionCompleted LetterDispositionStatus = "completed"
)
type LetterDisposition struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
FromUserID *uuid.UUID `json:"from_user_id,omitempty"`
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
ToUserID *uuid.UUID `json:"to_user_id,omitempty"`
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
Notes *string `json:"notes,omitempty"`
Status LetterDispositionStatus `gorm:"not null;default:'pending'" json:"status"`
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
ReadAt *time.Time `json:"read_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (LetterDisposition) TableName() string { return "letter_dispositions" }
type DispositionNote struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"`
UserID *uuid.UUID `json:"user_id,omitempty"`
Note string `gorm:"not null" json:"note"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
func (DispositionNote) TableName() string { return "disposition_notes" }
type LetterDispositionActionSelection struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"`
ActionID uuid.UUID `gorm:"type:uuid;not null" json:"action_id"`
Note *string `json:"note,omitempty"`
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
func (LetterDispositionActionSelection) TableName() string { return "letter_disposition_actions" }

View File

@ -0,0 +1,45 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type LetterIncomingStatus string
const (
LetterIncomingStatusNew LetterIncomingStatus = "new"
LetterIncomingStatusInProgress LetterIncomingStatus = "in_progress"
LetterIncomingStatusCompleted LetterIncomingStatus = "completed"
)
type LetterIncoming struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterNumber string `gorm:"uniqueIndex;not null" json:"letter_number"`
ReferenceNumber *string `json:"reference_number,omitempty"`
Subject string `gorm:"not null" json:"subject"`
Description *string `json:"description,omitempty"`
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
ReceivedDate time.Time `json:"received_date"`
DueDate *time.Time `json:"due_date,omitempty"`
Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"`
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (LetterIncoming) TableName() string { return "letters_incoming" }
type LetterIncomingAttachment struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
FileURL string `gorm:"not null" json:"file_url"`
FileName string `gorm:"not null" json:"file_name"`
FileType string `gorm:"not null" json:"file_type"`
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
}
func (LetterIncomingAttachment) TableName() string { return "letter_incoming_attachments" }

View File

@ -0,0 +1,23 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type LetterIncomingActivityLog struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
ActionType string `gorm:"not null" json:"action_type"`
ActorUserID *uuid.UUID `json:"actor_user_id,omitempty"`
ActorDepartmentID *uuid.UUID `json:"actor_department_id,omitempty"`
TargetType *string `json:"target_type,omitempty"`
TargetID *uuid.UUID `json:"target_id,omitempty"`
FromStatus *string `json:"from_status,omitempty"`
ToStatus *string `json:"to_status,omitempty"`
Context JSONB `gorm:"type:jsonb" json:"context,omitempty"`
OccurredAt time.Time `gorm:"autoCreateTime" json:"occurred_at"`
}
func (LetterIncomingActivityLog) TableName() string { return "letter_incoming_activity_logs" }

View File

@ -0,0 +1,17 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type Priority struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Name string `gorm:"not null;size:255" json:"name"`
Level int `gorm:"not null" json:"level"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Priority) TableName() string { return "priorities" }

View File

@ -0,0 +1,100 @@
package handler
import (
"context"
"eslogad-be/internal/contract"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type DispositionRouteService interface {
Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error)
ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error)
SetActive(ctx context.Context, id uuid.UUID, active bool) error
}
type DispositionRouteHandler struct{ svc DispositionRouteService }
func NewDispositionRouteHandler(svc DispositionRouteService) *DispositionRouteHandler {
return &DispositionRouteHandler{svc: svc}
}
func (h *DispositionRouteHandler) Create(c *gin.Context) {
var req contract.CreateDispositionRouteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.Create(c.Request.Context(), &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(201, contract.BuildSuccessResponse(resp))
}
func (h *DispositionRouteHandler) Update(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdateDispositionRouteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.Update(c.Request.Context(), id, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *DispositionRouteHandler) Get(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
resp, err := h.svc.Get(c.Request.Context(), id)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) {
fromID, err := uuid.Parse(c.Param("from_department_id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid from_department_id", Code: 400})
return
}
resp, err := h.svc.ListByFromDept(c.Request.Context(), fromID)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *DispositionRouteHandler) SetActive(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
toggle := c.Query("active")
active := toggle != "false"
if err := h.svc.SetActive(c.Request.Context(), id, active); err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, &contract.SuccessResponse{Message: "updated"})
}

View File

@ -0,0 +1,183 @@
package handler
import (
"context"
"net/http"
"strconv"
"eslogad-be/internal/contract"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type LetterService interface {
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, 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)
}
type LetterHandler struct{ svc LetterService }
func NewLetterHandler(svc LetterService) *LetterHandler { return &LetterHandler{svc: svc} }
func (h *LetterHandler) CreateIncomingLetter(c *gin.Context) {
var req contract.CreateIncomingLetterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
return
}
resp, err := h.svc.CreateIncomingLetter(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
}
func (h *LetterHandler) GetIncomingLetter(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
resp, err := h.svc.GetIncomingLetterByID(c.Request.Context(), id)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *LetterHandler) ListIncomingLetters(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
status := c.Query("status")
query := c.Query("q")
var statusPtr *string
var queryPtr *string
if status != "" {
statusPtr = &status
}
if query != "" {
queryPtr = &query
}
req := &contract.ListIncomingLettersRequest{Page: page, Limit: limit, Status: statusPtr, Query: queryPtr}
resp, err := h.svc.ListIncomingLetters(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 *LetterHandler) UpdateIncomingLetter(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdateIncomingLetterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdateIncomingLetter(c.Request.Context(), id, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
if err := h.svc.SoftDeleteIncomingLetter(c.Request.Context(), id); err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
}
func (h *LetterHandler) CreateDispositions(c *gin.Context) {
var req contract.CreateLetterDispositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.CreateDispositions(c.Request.Context(), &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(201, contract.BuildSuccessResponse(resp))
}
func (h *LetterHandler) ListDispositionsByLetter(c *gin.Context) {
letterID, err := uuid.Parse(c.Param("letter_id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
return
}
resp, err := h.svc.ListDispositionsByLetter(c.Request.Context(), letterID)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *LetterHandler) CreateDiscussion(c *gin.Context) {
letterID, err := uuid.Parse(c.Param("letter_id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
return
}
var req contract.CreateLetterDiscussionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.CreateDiscussion(c.Request.Context(), letterID, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(201, contract.BuildSuccessResponse(resp))
}
func (h *LetterHandler) UpdateDiscussion(c *gin.Context) {
letterID, err := uuid.Parse(c.Param("letter_id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
return
}
discussionID, err := uuid.Parse(c.Param("discussion_id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid discussion_id", Code: 400})
return
}
var req contract.UpdateLetterDiscussionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdateDiscussion(c.Request.Context(), letterID, discussionID, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}

View File

@ -0,0 +1,252 @@
package handler
import (
"context"
"net/http"
"eslogad-be/internal/contract"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type MasterService interface {
CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error)
UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error)
DeleteLabel(ctx context.Context, id uuid.UUID) error
ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error)
CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error)
UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error)
DeletePriority(ctx context.Context, id uuid.UUID) error
ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error)
CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error)
UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error)
DeleteInstitution(ctx context.Context, id uuid.UUID) error
ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error)
CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error)
UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error)
DeleteDispositionAction(ctx context.Context, id uuid.UUID) error
ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error)
}
type MasterHandler struct{ svc MasterService }
func NewMasterHandler(svc MasterService) *MasterHandler { return &MasterHandler{svc: svc} }
func (h *MasterHandler) CreateLabel(c *gin.Context) {
var req contract.CreateLabelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.CreateLabel(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) UpdateLabel(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdateLabelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdateLabel(c.Request.Context(), id, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) DeleteLabel(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
if err := h.svc.DeleteLabel(c.Request.Context(), id); err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
}
func (h *MasterHandler) ListLabels(c *gin.Context) {
resp, err := h.svc.ListLabels(c.Request.Context())
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
// Priorities
func (h *MasterHandler) CreatePriority(c *gin.Context) {
var req contract.CreatePriorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.CreatePriority(c.Request.Context(), &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(201, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) UpdatePriority(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdatePriorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdatePriority(c.Request.Context(), id, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) DeletePriority(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
if err := h.svc.DeletePriority(c.Request.Context(), id); err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
}
func (h *MasterHandler) ListPriorities(c *gin.Context) {
resp, err := h.svc.ListPriorities(c.Request.Context())
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
// Institutions
func (h *MasterHandler) CreateInstitution(c *gin.Context) {
var req contract.CreateInstitutionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.CreateInstitution(c.Request.Context(), &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(201, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) UpdateInstitution(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdateInstitutionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdateInstitution(c.Request.Context(), id, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) DeleteInstitution(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
if err := h.svc.DeleteInstitution(c.Request.Context(), id); err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
}
func (h *MasterHandler) ListInstitutions(c *gin.Context) {
resp, err := h.svc.ListInstitutions(c.Request.Context())
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
// Disposition Actions
func (h *MasterHandler) CreateDispositionAction(c *gin.Context) {
var req contract.CreateDispositionActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.CreateDispositionAction(c.Request.Context(), &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(201, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) UpdateDispositionAction(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdateDispositionActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdateDispositionAction(c.Request.Context(), id, &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}
func (h *MasterHandler) DeleteDispositionAction(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
if err := h.svc.DeleteDispositionAction(c.Request.Context(), id); err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
}
func (h *MasterHandler) ListDispositionActions(c *gin.Context) {
resp, err := h.svc.ListDispositionActions(c.Request.Context())
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(200, contract.BuildSuccessResponse(resp))
}

View File

@ -0,0 +1,37 @@
package processor
import (
"context"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"github.com/google/uuid"
)
type ActivityLogProcessorImpl struct {
repo *repository.LetterIncomingActivityLogRepository
}
func NewActivityLogProcessor(repo *repository.LetterIncomingActivityLogRepository) *ActivityLogProcessorImpl {
return &ActivityLogProcessorImpl{repo: repo}
}
func (p *ActivityLogProcessorImpl) Log(ctx context.Context, letterID uuid.UUID, actionType string, actorUserID *uuid.UUID, actorDepartmentID *uuid.UUID, targetType *string, targetID *uuid.UUID, fromStatus *string, toStatus *string, contextData map[string]interface{}) error {
ctxJSON := entities.JSONB{}
for k, v := range contextData {
ctxJSON[k] = v
}
entry := &entities.LetterIncomingActivityLog{
LetterID: letterID,
ActionType: actionType,
ActorUserID: actorUserID,
ActorDepartmentID: actorDepartmentID,
TargetType: targetType,
TargetID: targetID,
FromStatus: fromStatus,
ToStatus: toStatus,
Context: ctxJSON,
}
return p.repo.Create(ctx, entry)
}

View File

@ -0,0 +1,319 @@
package processor
import (
"context"
"time"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type LetterProcessorImpl struct {
letterRepo *repository.LetterIncomingRepository
attachRepo *repository.LetterIncomingAttachmentRepository
txManager *repository.TxManager
activity *ActivityLogProcessorImpl
// new repos for dispositions
dispositionRepo *repository.LetterDispositionRepository
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
dispositionNoteRepo *repository.DispositionNoteRepository
// discussion repo
discussionRepo *repository.LetterDiscussionRepository
}
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository) *LetterProcessorImpl {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo}
}
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
var result *contract.IncomingLetterResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
entity := &entities.LetterIncoming{
ReferenceNumber: req.ReferenceNumber,
Subject: req.Subject,
Description: req.Description,
PriorityID: req.PriorityID,
SenderInstitutionID: req.SenderInstitutionID,
ReceivedDate: req.ReceivedDate,
DueDate: req.DueDate,
Status: entities.LetterIncomingStatusNew,
CreatedBy: userID,
}
if err := p.letterRepo.Create(txCtx, entity); 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{}{}); 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)
result = transformer.LetterEntityToContract(entity, savedAttachments)
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
entity, err := p.letterRepo.Get(ctx, id)
if err != nil {
return nil, err
}
atts, _ := p.attachRepo.ListByLetter(ctx, id)
return transformer.LetterEntityToContract(entity, atts), nil
}
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
page, limit := req.Page, req.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
filter := repository.ListIncomingLettersFilter{Status: req.Status, Query: req.Query}
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)
resp := transformer.LetterEntityToContract(&e, atts)
respList = append(respList, *resp)
}
return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil
}
func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
var out *contract.IncomingLetterResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
entity, err := p.letterRepo.Get(txCtx, id)
if err != nil {
return err
}
fromStatus := string(entity.Status)
if req.ReferenceNumber != nil {
entity.ReferenceNumber = req.ReferenceNumber
}
if req.Subject != nil {
entity.Subject = *req.Subject
}
if req.Description != nil {
entity.Description = req.Description
}
if req.PriorityID != nil {
entity.PriorityID = req.PriorityID
}
if req.SenderInstitutionID != nil {
entity.SenderInstitutionID = req.SenderInstitutionID
}
if req.ReceivedDate != nil {
entity.ReceivedDate = *req.ReceivedDate
}
if req.DueDate != nil {
entity.DueDate = req.DueDate
}
if req.Status != nil {
entity.Status = entities.LetterIncomingStatus(*req.Status)
}
if err := p.letterRepo.Update(txCtx, entity); err != nil {
return err
}
toStatus := string(entity.Status)
if p.activity != nil && fromStatus != toStatus {
userID := appcontext.FromGinContext(txCtx).UserID
action := "status.changed"
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, &fromStatus, &toStatus, map[string]interface{}{}); err != nil {
return err
}
}
atts, _ := p.attachRepo.ListByLetter(txCtx, id)
out = transformer.LetterEntityToContract(entity, atts)
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (p *LetterProcessorImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
if err := p.letterRepo.SoftDelete(txCtx, id); err != nil {
return err
}
if p.activity != nil {
userID := appcontext.FromGinContext(txCtx).UserID
action := "letter.deleted"
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil {
return err
}
}
return nil
})
}
func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
var out *contract.ListDispositionsResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
created := make([]entities.LetterDisposition, 0, len(req.ToDepartmentIDs))
for _, toDept := range req.ToDepartmentIDs {
disp := entities.LetterDisposition{
LetterID: req.LetterID,
FromDepartmentID: nil,
ToDepartmentID: &toDept,
Notes: req.Notes,
Status: entities.DispositionPending,
CreatedBy: userID,
}
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
return err
}
created = append(created, disp)
if len(req.SelectedActions) > 0 {
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
for _, sel := range req.SelectedActions {
selections = append(selections, entities.LetterDispositionActionSelection{
DispositionID: disp.ID,
ActionID: sel.ActionID,
Note: sel.Note,
CreatedBy: userID,
})
}
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
return err
}
}
if p.activity != nil {
action := "disposition.created"
for _, d := range created {
ctxMap := map[string]interface{}{"to_department_id": d.ToDepartmentID}
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &d.ID, nil, nil, ctxMap); err != nil {
return err
}
}
}
}
out = &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(created)}
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
list, err := p.dispositionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
}
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
var out *contract.LetterDiscussionResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
mentions := entities.JSONB(nil)
if req.Mentions != nil {
mentions = entities.JSONB(req.Mentions)
}
disc := &entities.LetterDiscussion{LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: mentions}
if err := p.discussionRepo.Create(txCtx, disc); err != nil {
return err
}
if p.activity != nil {
action := "discussion.created"
tgt := "discussion"
ctxMap := map[string]interface{}{"message": req.Message, "parent_id": req.ParentID}
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
return err
}
}
out = transformer.DiscussionEntityToContract(disc)
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
var out *contract.LetterDiscussionResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
disc, err := p.discussionRepo.Get(txCtx, discussionID)
if err != nil {
return err
}
oldMessage := disc.Message
disc.Message = req.Message
if req.Mentions != nil {
disc.Mentions = entities.JSONB(req.Mentions)
}
now := time.Now()
disc.EditedAt = &now
if err := p.discussionRepo.Update(txCtx, disc); err != nil {
return err
}
if p.activity != nil {
userID := appcontext.FromGinContext(txCtx).UserID
action := "discussion.updated"
tgt := "discussion"
ctxMap := map[string]interface{}{"old_message": oldMessage, "new_message": req.Message}
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
return err
}
}
out = transformer.DiscussionEntityToContract(disc)
return nil
})
if err != nil {
return nil, err
}
return out, nil
}

View File

@ -0,0 +1,45 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type DispositionRouteRepository struct{ db *gorm.DB }
func NewDispositionRouteRepository(db *gorm.DB) *DispositionRouteRepository {
return &DispositionRouteRepository{db: db}
}
func (r *DispositionRouteRepository) Create(ctx context.Context, e *entities.DispositionRoute) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error {
db := DBFromContext(ctx, r.db)
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) {
db := DBFromContext(ctx, r.db)
var e entities.DispositionRoute
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionRoute
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).Order("to_department_id").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
}

View File

@ -0,0 +1,180 @@
package repository
import (
"context"
"time"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type LetterIncomingRepository struct{ db *gorm.DB }
func NewLetterIncomingRepository(db *gorm.DB) *LetterIncomingRepository {
return &LetterIncomingRepository{db: db}
}
func (r *LetterIncomingRepository) Create(ctx context.Context, e *entities.LetterIncoming) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterIncomingRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterIncoming
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterIncomingRepository) Update(ctx context.Context, e *entities.LetterIncoming) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error
}
func (r *LetterIncomingRepository) SoftDelete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Exec("UPDATE letters_incoming SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL", id).Error
}
type ListIncomingLettersFilter struct {
Status *string
Query *string
}
func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncomingLettersFilter, limit, offset int) ([]entities.LetterIncoming, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
if filter.Status != nil {
query = query.Where("status = ?", *filter.Status)
}
if filter.Query != nil {
q := "%" + *filter.Query + "%"
query = query.Where("subject ILIKE ? OR reference_number ILIKE ?", q, q)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []entities.LetterIncoming
if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
type LetterIncomingAttachmentRepository struct{ db *gorm.DB }
func NewLetterIncomingAttachmentRepository(db *gorm.DB) *LetterIncomingAttachmentRepository {
return &LetterIncomingAttachmentRepository{db: db}
}
func (r *LetterIncomingAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingAttachment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterIncomingAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingAttachment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingAttachment
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterIncomingActivityLogRepository struct{ db *gorm.DB }
func NewLetterIncomingActivityLogRepository(db *gorm.DB) *LetterIncomingActivityLogRepository {
return &LetterIncomingActivityLogRepository{db: db}
}
func (r *LetterIncomingActivityLogRepository) Create(ctx context.Context, e *entities.LetterIncomingActivityLog) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterIncomingActivityLogRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingActivityLog, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingActivityLog
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("occurred_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterDispositionRepository struct{ db *gorm.DB }
func NewLetterDispositionRepository(db *gorm.DB) *LetterDispositionRepository {
return &LetterDispositionRepository{db: db}
}
func (r *LetterDispositionRepository) Create(ctx context.Context, e *entities.LetterDisposition) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDisposition, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDisposition
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type DispositionNoteRepository struct{ db *gorm.DB }
func NewDispositionNoteRepository(db *gorm.DB) *DispositionNoteRepository {
return &DispositionNoteRepository{db: db}
}
func (r *DispositionNoteRepository) Create(ctx context.Context, e *entities.DispositionNote) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
type LetterDispositionActionSelectionRepository struct{ db *gorm.DB }
func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository {
return &LetterDispositionActionSelectionRepository{db: db}
}
func (r *LetterDispositionActionSelectionRepository) CreateBulk(ctx context.Context, list []entities.LetterDispositionActionSelection) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterDispositionActionSelectionRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterDispositionActionSelection, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDispositionActionSelection
if err := db.WithContext(ctx).Where("disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterDiscussionRepository struct{ db *gorm.DB }
func NewLetterDiscussionRepository(db *gorm.DB) *LetterDiscussionRepository {
return &LetterDiscussionRepository{db: db}
}
func (r *LetterDiscussionRepository) Create(ctx context.Context, e *entities.LetterDiscussion) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterDiscussionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterDiscussion, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterDiscussion
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterDiscussionRepository) Update(ctx context.Context, e *entities.LetterDiscussion) error {
db := DBFromContext(ctx, r.db)
// ensure edited_at is set when updating
if e.EditedAt == nil {
now := time.Now()
e.EditedAt = &now
}
return db.WithContext(ctx).Model(&entities.LetterDiscussion{}).
Where("id = ?", e.ID).
Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error
}

View File

@ -0,0 +1,114 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type LabelRepository struct{ db *gorm.DB }
func NewLabelRepository(db *gorm.DB) *LabelRepository { return &LabelRepository{db: db} }
func (r *LabelRepository) Create(ctx context.Context, e *entities.Label) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *LabelRepository) Update(ctx context.Context, e *entities.Label) error {
return r.db.WithContext(ctx).Model(&entities.Label{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *LabelRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Label{}, "id = ?", id).Error
}
func (r *LabelRepository) List(ctx context.Context) ([]entities.Label, error) {
var list []entities.Label
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
return list, err
}
func (r *LabelRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Label, error) {
var e entities.Label
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
type PriorityRepository struct{ db *gorm.DB }
func NewPriorityRepository(db *gorm.DB) *PriorityRepository { return &PriorityRepository{db: db} }
func (r *PriorityRepository) Create(ctx context.Context, e *entities.Priority) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *PriorityRepository) Update(ctx context.Context, e *entities.Priority) error {
return r.db.WithContext(ctx).Model(&entities.Priority{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *PriorityRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Priority{}, "id = ?", id).Error
}
func (r *PriorityRepository) List(ctx context.Context) ([]entities.Priority, error) {
var list []entities.Priority
err := r.db.WithContext(ctx).Order("level ASC").Find(&list).Error
return list, err
}
func (r *PriorityRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Priority, error) {
var e entities.Priority
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
type InstitutionRepository struct{ db *gorm.DB }
func NewInstitutionRepository(db *gorm.DB) *InstitutionRepository {
return &InstitutionRepository{db: db}
}
func (r *InstitutionRepository) Create(ctx context.Context, e *entities.Institution) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *InstitutionRepository) Update(ctx context.Context, e *entities.Institution) error {
return r.db.WithContext(ctx).Model(&entities.Institution{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *InstitutionRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Institution{}, "id = ?", id).Error
}
func (r *InstitutionRepository) List(ctx context.Context) ([]entities.Institution, error) {
var list []entities.Institution
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
return list, err
}
func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Institution, error) {
var e entities.Institution
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
type DispositionActionRepository struct{ db *gorm.DB }
func NewDispositionActionRepository(db *gorm.DB) *DispositionActionRepository {
return &DispositionActionRepository{db: db}
}
func (r *DispositionActionRepository) Create(ctx context.Context, e *entities.DispositionAction) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *DispositionActionRepository) Update(ctx context.Context, e *entities.DispositionAction) error {
return r.db.WithContext(ctx).Model(&entities.DispositionAction{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *DispositionActionRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.DispositionAction{}, "id = ?", id).Error
}
func (r *DispositionActionRepository) List(ctx context.Context) ([]entities.DispositionAction, error) {
var list []entities.DispositionAction
err := r.db.WithContext(ctx).Order("sort_order NULLS LAST, label ASC").Find(&list).Error
return list, err
}
func (r *DispositionActionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionAction, error) {
var e entities.DispositionAction
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}

View File

@ -0,0 +1,35 @@
package repository
import (
"context"
"gorm.io/gorm"
)
type txKeyType struct{}
var txKey = txKeyType{}
// DBFromContext returns the transactional *gorm.DB from context if present; otherwise returns base.
func DBFromContext(ctx context.Context, base *gorm.DB) *gorm.DB {
if v := ctx.Value(txKey); v != nil {
if tx, ok := v.(*gorm.DB); ok && tx != nil {
return tx
}
}
return base
}
type TxManager struct {
db *gorm.DB
}
func NewTxManager(db *gorm.DB) *TxManager { return &TxManager{db: db} }
// WithTransaction runs fn inside a DB transaction, injecting the *gorm.DB tx into ctx.
func (m *TxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
ctxTx := context.WithValue(ctx, txKey, tx)
return fn(ctxTx)
})
}

View File

@ -30,3 +30,48 @@ type RBACHandler interface {
DeleteRole(c *gin.Context) DeleteRole(c *gin.Context)
ListRoles(c *gin.Context) ListRoles(c *gin.Context)
} }
type MasterHandler interface {
// labels
CreateLabel(c *gin.Context)
UpdateLabel(c *gin.Context)
DeleteLabel(c *gin.Context)
ListLabels(c *gin.Context)
// priorities
CreatePriority(c *gin.Context)
UpdatePriority(c *gin.Context)
DeletePriority(c *gin.Context)
ListPriorities(c *gin.Context)
// institutions
CreateInstitution(c *gin.Context)
UpdateInstitution(c *gin.Context)
DeleteInstitution(c *gin.Context)
ListInstitutions(c *gin.Context)
// disposition actions
CreateDispositionAction(c *gin.Context)
UpdateDispositionAction(c *gin.Context)
DeleteDispositionAction(c *gin.Context)
ListDispositionActions(c *gin.Context)
}
type LetterHandler interface {
CreateIncomingLetter(c *gin.Context)
GetIncomingLetter(c *gin.Context)
ListIncomingLetters(c *gin.Context)
UpdateIncomingLetter(c *gin.Context)
DeleteIncomingLetter(c *gin.Context)
CreateDispositions(c *gin.Context)
ListDispositionsByLetter(c *gin.Context)
CreateDiscussion(c *gin.Context)
UpdateDiscussion(c *gin.Context)
}
type DispositionRouteHandler interface {
Create(c *gin.Context)
Update(c *gin.Context)
Get(c *gin.Context)
ListByFromDept(c *gin.Context)
SetActive(c *gin.Context)
}

View File

@ -8,13 +8,16 @@ import (
) )
type Router struct { type Router struct {
config *config.Config config *config.Config
authHandler AuthHandler authHandler AuthHandler
healthHandler HealthHandler healthHandler HealthHandler
authMiddleware AuthMiddleware authMiddleware AuthMiddleware
userHandler UserHandler userHandler UserHandler
fileHandler FileHandler fileHandler FileHandler
rbacHandler RBACHandler rbacHandler RBACHandler
masterHandler MasterHandler
letterHandler LetterHandler
dispRouteHandler DispositionRouteHandler
} }
func NewRouter( func NewRouter(
@ -25,15 +28,21 @@ func NewRouter(
userHandler UserHandler, userHandler UserHandler,
fileHandler FileHandler, fileHandler FileHandler,
rbacHandler RBACHandler, rbacHandler RBACHandler,
masterHandler MasterHandler,
letterHandler LetterHandler,
dispRouteHandler DispositionRouteHandler,
) *Router { ) *Router {
return &Router{ return &Router{
config: cfg, config: cfg,
authHandler: authHandler, authHandler: authHandler,
authMiddleware: authMiddleware, authMiddleware: authMiddleware,
healthHandler: healthHandler, healthHandler: healthHandler,
userHandler: userHandler, userHandler: userHandler,
fileHandler: fileHandler, fileHandler: fileHandler,
rbacHandler: rbacHandler, rbacHandler: rbacHandler,
masterHandler: masterHandler,
letterHandler: letterHandler,
dispRouteHandler: dispRouteHandler,
} }
} }
@ -88,10 +97,61 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
rbac.POST("/permissions", r.rbacHandler.CreatePermission) rbac.POST("/permissions", r.rbacHandler.CreatePermission)
rbac.PUT("/permissions/:id", r.rbacHandler.UpdatePermission) rbac.PUT("/permissions/:id", r.rbacHandler.UpdatePermission)
rbac.DELETE("/permissions/:id", r.rbacHandler.DeletePermission) rbac.DELETE("/permissions/:id", r.rbacHandler.DeletePermission)
rbac.GET("/roles", r.rbacHandler.ListRoles) rbac.GET("/roles", r.rbacHandler.ListRoles)
rbac.POST("/roles", r.rbacHandler.CreateRole) rbac.POST("/roles", r.rbacHandler.CreateRole)
rbac.PUT("/roles/:id", r.rbacHandler.UpdateRole) rbac.PUT("/roles/:id", r.rbacHandler.UpdateRole)
rbac.DELETE("/roles/:id", r.rbacHandler.DeleteRole) rbac.DELETE("/roles/:id", r.rbacHandler.DeleteRole)
} }
master := v1.Group("/master")
master.Use(r.authMiddleware.RequireAuth())
{
master.GET("/labels", r.masterHandler.ListLabels)
master.POST("/labels", r.masterHandler.CreateLabel)
master.PUT("/labels/:id", r.masterHandler.UpdateLabel)
master.DELETE("/labels/:id", r.masterHandler.DeleteLabel)
master.GET("/priorities", r.masterHandler.ListPriorities)
master.POST("/priorities", r.masterHandler.CreatePriority)
master.PUT("/priorities/:id", r.masterHandler.UpdatePriority)
master.DELETE("/priorities/:id", r.masterHandler.DeletePriority)
master.GET("/institutions", r.masterHandler.ListInstitutions)
master.POST("/institutions", r.masterHandler.CreateInstitution)
master.PUT("/institutions/:id", r.masterHandler.UpdateInstitution)
master.DELETE("/institutions/:id", r.masterHandler.DeleteInstitution)
master.GET("/disposition-actions", r.masterHandler.ListDispositionActions)
master.POST("/disposition-actions", r.masterHandler.CreateDispositionAction)
master.PUT("/disposition-actions/:id", r.masterHandler.UpdateDispositionAction)
master.DELETE("/disposition-actions/:id", r.masterHandler.DeleteDispositionAction)
}
lettersch := v1.Group("/letters")
lettersch.Use(r.authMiddleware.RequireAuth())
{
lettersch.POST("/incoming", r.letterHandler.CreateIncomingLetter)
lettersch.GET("/incoming/:id", r.letterHandler.GetIncomingLetter)
lettersch.GET("/incoming", r.letterHandler.ListIncomingLetters)
lettersch.PUT("/incoming/:id", r.letterHandler.UpdateIncomingLetter)
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
lettersch.GET("/dispositions/:letter_id", r.letterHandler.ListDispositionsByLetter)
lettersch.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion)
lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion)
}
droutes := v1.Group("/disposition-routes")
droutes.Use(r.authMiddleware.RequireAuth())
{
droutes.POST("", r.dispRouteHandler.Create)
droutes.GET(":id", r.dispRouteHandler.Get)
droutes.PUT(":id", r.dispRouteHandler.Update)
droutes.GET("from/:from_department_id", r.dispRouteHandler.ListByFromDept)
droutes.PUT(":id/active", r.dispRouteHandler.SetActive)
}
} }
} }

View File

@ -0,0 +1,70 @@
package service
import (
"context"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type DispositionRouteServiceImpl struct {
repo *repository.DispositionRouteRepository
}
func NewDispositionRouteService(repo *repository.DispositionRouteRepository) *DispositionRouteServiceImpl {
return &DispositionRouteServiceImpl{repo: repo}
}
func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
entity := &entities.DispositionRoute{FromDepartmentID: req.FromDepartmentID, ToDepartmentID: req.ToDepartmentID}
if req.IsActive != nil {
entity.IsActive = *req.IsActive
}
if req.AllowedActions != nil {
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
}
if err := s.repo.Create(ctx, entity); err != nil {
return nil, err
}
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
return &resp, nil
}
func (s *DispositionRouteServiceImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
entity, err := s.repo.Get(ctx, id)
if err != nil {
return nil, err
}
if req.IsActive != nil {
entity.IsActive = *req.IsActive
}
if req.AllowedActions != nil {
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
}
if err := s.repo.Update(ctx, entity); err != nil {
return nil, err
}
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
return &resp, nil
}
func (s *DispositionRouteServiceImpl) Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error) {
entity, err := s.repo.Get(ctx, id)
if err != nil {
return nil, err
}
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
return &resp, nil
}
func (s *DispositionRouteServiceImpl) ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error) {
list, err := s.repo.ListByFromDept(ctx, from)
if err != nil {
return nil, err
}
return &contract.ListDispositionRoutesResponse{Routes: transformer.DispositionRoutesToContract(list)}, nil
}
func (s *DispositionRouteServiceImpl) SetActive(ctx context.Context, id uuid.UUID, active bool) error {
return s.repo.SetActive(ctx, id, active)
}

View File

@ -0,0 +1,63 @@
package service
import (
"context"
"eslogad-be/internal/contract"
"github.com/google/uuid"
)
type LetterProcessor interface {
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, 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)
}
type LetterServiceImpl struct {
processor LetterProcessor
}
func NewLetterService(processor LetterProcessor) *LetterServiceImpl {
return &LetterServiceImpl{processor: processor}
}
func (s *LetterServiceImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
return s.processor.CreateIncomingLetter(ctx, req)
}
func (s *LetterServiceImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
return s.processor.GetIncomingLetterByID(ctx, id)
}
func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
return s.processor.ListIncomingLetters(ctx, req)
}
func (s *LetterServiceImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
return s.processor.UpdateIncomingLetter(ctx, id, req)
}
func (s *LetterServiceImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error {
return s.processor.SoftDeleteIncomingLetter(ctx, id)
}
func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
return s.processor.CreateDispositions(ctx, req)
}
func (s *LetterServiceImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
return s.processor.ListDispositionsByLetter(ctx, letterID)
}
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
return s.processor.CreateDiscussion(ctx, letterID, req)
}
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)
}

View File

@ -0,0 +1,214 @@
package service
import (
"context"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type MasterServiceImpl struct {
labelRepo *repository.LabelRepository
priorityRepo *repository.PriorityRepository
institutionRepo *repository.InstitutionRepository
dispRepo *repository.DispositionActionRepository
}
func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository) *MasterServiceImpl {
return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp}
}
// Labels
func (s *MasterServiceImpl) CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) {
entity := &entities.Label{Name: req.Name, Color: req.Color}
if err := s.labelRepo.Create(ctx, entity); err != nil {
return nil, err
}
resp := transformer.LabelsToContract([]entities.Label{*entity})[0]
return &resp, nil
}
func (s *MasterServiceImpl) UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) {
entity := &entities.Label{ID: id}
if req.Name != nil {
entity.Name = *req.Name
}
if req.Color != nil {
entity.Color = req.Color
}
if err := s.labelRepo.Update(ctx, entity); err != nil {
return nil, err
}
e, err := s.labelRepo.Get(ctx, id)
if err != nil {
return nil, err
}
resp := transformer.LabelsToContract([]entities.Label{*e})[0]
return &resp, nil
}
func (s *MasterServiceImpl) DeleteLabel(ctx context.Context, id uuid.UUID) error {
return s.labelRepo.Delete(ctx, id)
}
func (s *MasterServiceImpl) ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) {
list, err := s.labelRepo.List(ctx)
if err != nil {
return nil, err
}
return &contract.ListLabelsResponse{Labels: transformer.LabelsToContract(list)}, nil
}
// Priorities
func (s *MasterServiceImpl) CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) {
entity := &entities.Priority{Name: req.Name, Level: req.Level}
if err := s.priorityRepo.Create(ctx, entity); err != nil {
return nil, err
}
resp := transformer.PrioritiesToContract([]entities.Priority{*entity})[0]
return &resp, nil
}
func (s *MasterServiceImpl) UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) {
entity := &entities.Priority{ID: id}
if req.Name != nil {
entity.Name = *req.Name
}
if req.Level != nil {
entity.Level = *req.Level
}
if err := s.priorityRepo.Update(ctx, entity); err != nil {
return nil, err
}
e, err := s.priorityRepo.Get(ctx, id)
if err != nil {
return nil, err
}
resp := transformer.PrioritiesToContract([]entities.Priority{*e})[0]
return &resp, nil
}
func (s *MasterServiceImpl) DeletePriority(ctx context.Context, id uuid.UUID) error {
return s.priorityRepo.Delete(ctx, id)
}
func (s *MasterServiceImpl) ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) {
list, err := s.priorityRepo.List(ctx)
if err != nil {
return nil, err
}
return &contract.ListPrioritiesResponse{Priorities: transformer.PrioritiesToContract(list)}, nil
}
// Institutions
func (s *MasterServiceImpl) CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) {
entity := &entities.Institution{Name: req.Name, Type: entities.InstitutionType(req.Type), Address: req.Address, ContactPerson: req.ContactPerson, Phone: req.Phone, Email: req.Email}
if err := s.institutionRepo.Create(ctx, entity); err != nil {
return nil, err
}
resp := transformer.InstitutionsToContract([]entities.Institution{*entity})[0]
return &resp, nil
}
func (s *MasterServiceImpl) UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) {
entity := &entities.Institution{ID: id}
if req.Name != nil {
entity.Name = *req.Name
}
if req.Type != nil {
entity.Type = entities.InstitutionType(*req.Type)
}
if req.Address != nil {
entity.Address = req.Address
}
if req.ContactPerson != nil {
entity.ContactPerson = req.ContactPerson
}
if req.Phone != nil {
entity.Phone = req.Phone
}
if req.Email != nil {
entity.Email = req.Email
}
if err := s.institutionRepo.Update(ctx, entity); err != nil {
return nil, err
}
e, err := s.institutionRepo.Get(ctx, id)
if err != nil {
return nil, err
}
resp := transformer.InstitutionsToContract([]entities.Institution{*e})[0]
return &resp, nil
}
func (s *MasterServiceImpl) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
return s.institutionRepo.Delete(ctx, id)
}
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error) {
list, err := s.institutionRepo.List(ctx)
if err != nil {
return nil, err
}
return &contract.ListInstitutionsResponse{Institutions: transformer.InstitutionsToContract(list)}, nil
}
// Disposition Actions
func (s *MasterServiceImpl) CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
entity := &entities.DispositionAction{Code: req.Code, Label: req.Label, Description: req.Description}
if req.RequiresNote != nil {
entity.RequiresNote = *req.RequiresNote
}
if req.GroupName != nil {
entity.GroupName = req.GroupName
}
if req.SortOrder != nil {
entity.SortOrder = req.SortOrder
}
if req.IsActive != nil {
entity.IsActive = *req.IsActive
}
if err := s.dispRepo.Create(ctx, entity); err != nil {
return nil, err
}
resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*entity})[0]
return &resp, nil
}
func (s *MasterServiceImpl) UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
entity := &entities.DispositionAction{ID: id}
if req.Code != nil {
entity.Code = *req.Code
}
if req.Label != nil {
entity.Label = *req.Label
}
if req.Description != nil {
entity.Description = req.Description
}
if req.RequiresNote != nil {
entity.RequiresNote = *req.RequiresNote
}
if req.GroupName != nil {
entity.GroupName = req.GroupName
}
if req.SortOrder != nil {
entity.SortOrder = req.SortOrder
}
if req.IsActive != nil {
entity.IsActive = *req.IsActive
}
if err := s.dispRepo.Update(ctx, entity); err != nil {
return nil, err
}
e, err := s.dispRepo.Get(ctx, id)
if err != nil {
return nil, err
}
resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*e})[0]
return &resp, nil
}
func (s *MasterServiceImpl) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error {
return s.dispRepo.Delete(ctx, id)
}
func (s *MasterServiceImpl) ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) {
list, err := s.dispRepo.List(ctx)
if err != nil {
return nil, err
}
return &contract.ListDispositionActionsResponse{Actions: transformer.DispositionActionsToContract(list)}, nil
}

View File

@ -190,3 +190,66 @@ func RoleWithPermissionsToContract(role entities.Role, perms []entities.Permissi
UpdatedAt: role.UpdatedAt, UpdatedAt: role.UpdatedAt,
} }
} }
func LabelsToContract(list []entities.Label) []contract.LabelResponse {
out := make([]contract.LabelResponse, 0, len(list))
for _, e := range list {
out = append(out, contract.LabelResponse{ID: e.ID.String(), Name: e.Name, Color: e.Color, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt})
}
return out
}
func PrioritiesToContract(list []entities.Priority) []contract.PriorityResponse {
out := make([]contract.PriorityResponse, 0, len(list))
for _, e := range list {
out = append(out, contract.PriorityResponse{ID: e.ID.String(), Name: e.Name, Level: e.Level, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt})
}
return out
}
func InstitutionsToContract(list []entities.Institution) []contract.InstitutionResponse {
out := make([]contract.InstitutionResponse, 0, len(list))
for _, e := range list {
out = append(out, contract.InstitutionResponse{ID: e.ID.String(), Name: e.Name, Type: string(e.Type), Address: e.Address, ContactPerson: e.ContactPerson, Phone: e.Phone, Email: e.Email, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt})
}
return out
}
func DispositionActionsToContract(list []entities.DispositionAction) []contract.DispositionActionResponse {
out := make([]contract.DispositionActionResponse, 0, len(list))
for _, e := range list {
out = append(out, contract.DispositionActionResponse{
ID: e.ID.String(),
Code: e.Code,
Label: e.Label,
Description: e.Description,
RequiresNote: e.RequiresNote,
GroupName: e.GroupName,
SortOrder: e.SortOrder,
IsActive: e.IsActive,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
})
}
return out
}
func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.DispositionRouteResponse {
out := make([]contract.DispositionRouteResponse, 0, len(list))
for _, e := range list {
var allowed map[string]interface{}
if e.AllowedActions != nil {
allowed = map[string]interface{}(e.AllowedActions)
}
out = append(out, contract.DispositionRouteResponse{
ID: e.ID,
FromDepartmentID: e.FromDepartmentID,
ToDepartmentID: e.ToDepartmentID,
IsActive: e.IsActive,
AllowedActions: allowed,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
})
}
return out
}

View File

@ -0,0 +1,70 @@
package transformer
import (
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
)
func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment) *contract.IncomingLetterResponse {
resp := &contract.IncomingLetterResponse{
ID: e.ID,
LetterNumber: e.LetterNumber,
ReferenceNumber: e.ReferenceNumber,
Subject: e.Subject,
Description: e.Description,
PriorityID: e.PriorityID,
SenderInstitutionID: e.SenderInstitutionID,
ReceivedDate: e.ReceivedDate,
DueDate: e.DueDate,
Status: string(e.Status),
CreatedBy: e.CreatedBy,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)),
}
for _, a := range attachments {
resp.Attachments = append(resp.Attachments, contract.IncomingLetterAttachmentResponse{
ID: a.ID,
FileURL: a.FileURL,
FileName: a.FileName,
FileType: a.FileType,
UploadedAt: a.UploadedAt,
})
}
return resp
}
func DispositionsToContract(list []entities.LetterDisposition) []contract.DispositionResponse {
out := make([]contract.DispositionResponse, 0, len(list))
for _, d := range list {
out = append(out, contract.DispositionResponse{
ID: d.ID,
LetterID: d.LetterID,
FromDepartmentID: d.FromDepartmentID,
ToDepartmentID: d.ToDepartmentID,
Notes: d.Notes,
Status: string(d.Status),
CreatedBy: d.CreatedBy,
CreatedAt: d.CreatedAt,
})
}
return out
}
func DiscussionEntityToContract(e *entities.LetterDiscussion) *contract.LetterDiscussionResponse {
var mentions map[string]interface{}
if e.Mentions != nil {
mentions = map[string]interface{}(e.Mentions)
}
return &contract.LetterDiscussionResponse{
ID: e.ID,
LetterID: e.LetterID,
ParentID: e.ParentID,
UserID: e.UserID,
Message: e.Message,
Mentions: mentions,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
EditedAt: e.EditedAt,
}
}

View File

@ -0,0 +1,7 @@
BEGIN;
DROP TABLE IF EXISTS institutions;
DROP TABLE IF EXISTS priorities;
DROP TABLE IF EXISTS labels;
COMMIT;

View File

@ -0,0 +1,52 @@
BEGIN;
-- =======================
-- LABELS
-- =======================
CREATE TABLE IF NOT EXISTS labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
color VARCHAR(16), -- HEX color code (e.g., #FF0000)
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_labels_updated_at
BEFORE UPDATE ON labels
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- PRIORITIES
-- =======================
CREATE TABLE IF NOT EXISTS priorities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
level INT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_priorities_updated_at
BEFORE UPDATE ON priorities
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- INSTITUTIONS
-- =======================
CREATE TABLE IF NOT EXISTS institutions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
type TEXT NOT NULL CHECK (type IN ('government','private','ngo','individual')),
address TEXT,
contact_person VARCHAR(255),
phone VARCHAR(50),
email VARCHAR(255),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_institutions_updated_at
BEFORE UPDATE ON institutions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
COMMIT;

View File

@ -0,0 +1,5 @@
BEGIN;
DROP TABLE IF EXISTS disposition_actions;
COMMIT;

View File

@ -0,0 +1,23 @@
BEGIN;
-- =======================
-- DISPOSITION ACTIONS
-- =======================
CREATE TABLE IF NOT EXISTS disposition_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
description TEXT,
requires_note BOOLEAN NOT NULL DEFAULT FALSE,
group_name TEXT,
sort_order INT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_disposition_actions_updated_at
BEFORE UPDATE ON disposition_actions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
COMMIT;

View File

@ -0,0 +1,16 @@
BEGIN;
DROP TABLE IF EXISTS letter_incoming_activity_logs;
DROP TABLE IF EXISTS letter_incoming_discussion_attachments;
DROP TABLE IF EXISTS letter_incoming_discussions;
DROP TABLE IF EXISTS letter_disposition_actions;
DROP TABLE IF EXISTS disposition_notes;
DROP TABLE IF EXISTS letter_dispositions;
DROP TABLE IF EXISTS letter_incoming_attachments;
DROP TABLE IF EXISTS letter_incoming_labels;
DROP TABLE IF EXISTS letter_incoming_recipients;
DROP TABLE IF EXISTS letters_incoming;
DROP SEQUENCE IF EXISTS letters_incoming_seq;
COMMIT;

View File

@ -0,0 +1,189 @@
BEGIN;
-- =======================
-- SEQUENCE FOR LETTER NUMBER
-- =======================
CREATE SEQUENCE IF NOT EXISTS letters_incoming_seq;
-- =======================
-- LETTERS INCOMING
-- =======================
CREATE TABLE IF NOT EXISTS letters_incoming (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_number TEXT NOT NULL UNIQUE DEFAULT ('IN-' || lpad(nextval('letters_incoming_seq')::text, 8, '0')),
reference_number TEXT,
subject TEXT NOT NULL,
description TEXT,
priority_id UUID REFERENCES priorities(id) ON DELETE SET NULL,
sender_institution_id UUID REFERENCES institutions(id) ON DELETE SET NULL,
received_date DATE NOT NULL,
due_date DATE,
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','in_progress','completed')),
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE INDEX IF NOT EXISTS idx_letters_incoming_status ON letters_incoming(status);
CREATE INDEX IF NOT EXISTS idx_letters_incoming_received_date ON letters_incoming(received_date);
CREATE TRIGGER trg_letters_incoming_updated_at
BEFORE UPDATE ON letters_incoming
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- LETTER INCOMING RECIPIENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_recipients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
recipient_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
recipient_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','read','completed')),
read_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_letter ON letter_incoming_recipients(letter_id);
-- =======================
-- LETTER INCOMING LABELS (M:N)
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE (letter_id, label_id)
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_labels_letter ON letter_incoming_labels(letter_id);
-- =======================
-- LETTER INCOMING ATTACHMENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_attachments_letter ON letter_incoming_attachments(letter_id);
-- =======================
-- LETTER DISPOSITIONS
-- =======================
CREATE TABLE IF NOT EXISTS letter_dispositions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
from_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
from_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
to_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
to_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
notes TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','read','rejected','completed')),
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
read_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_dispositions_letter ON letter_dispositions(letter_id);
CREATE TRIGGER trg_letter_dispositions_updated_at
BEFORE UPDATE ON letter_dispositions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- DISPOSITION NOTES
-- =======================
CREATE TABLE IF NOT EXISTS disposition_notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
note TEXT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_disposition_notes_disposition ON disposition_notes(disposition_id);
-- =======================
-- LETTER DISPOSITION ACTIONS (Selections)
-- =======================
CREATE TABLE IF NOT EXISTS letter_disposition_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE,
action_id UUID NOT NULL REFERENCES disposition_actions(id) ON DELETE RESTRICT,
note TEXT,
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE (disposition_id, action_id)
);
CREATE INDEX IF NOT EXISTS idx_letter_disposition_actions_disposition ON letter_disposition_actions(disposition_id);
-- =======================
-- LETTER INCOMING DISCUSSIONS (Threaded)
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_discussions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
parent_id UUID REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
message TEXT NOT NULL,
mentions JSONB,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_letter ON letter_incoming_discussions(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_parent ON letter_incoming_discussions(parent_id);
CREATE TRIGGER trg_letter_incoming_discussions_updated_at
BEFORE UPDATE ON letter_incoming_discussions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =======================
-- LETTER INCOMING DISCUSSION ATTACHMENTS
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_discussion_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discussion_id UUID NOT NULL REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussion_attachments_discussion ON letter_incoming_discussion_attachments(discussion_id);
-- =======================
-- LETTER INCOMING ACTIVITY LOGS (Immutable)
-- =======================
CREATE TABLE IF NOT EXISTS letter_incoming_activity_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
action_type TEXT NOT NULL,
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
actor_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
target_type TEXT,
target_id UUID,
from_status TEXT,
to_status TEXT,
context JSONB,
occurred_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_letter ON letter_incoming_activity_logs(letter_id);
CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_action ON letter_incoming_activity_logs(action_type);
COMMIT;

View File

@ -0,0 +1,5 @@
BEGIN;
DROP TABLE IF EXISTS disposition_routes;
COMMIT;

View File

@ -0,0 +1,27 @@
BEGIN;
-- =======================
-- DISPOSITION ROUTES
-- =======================
CREATE TABLE IF NOT EXISTS disposition_routes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
to_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
allowed_actions JSONB,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_disposition_routes_from_dept ON disposition_routes(from_department_id);
-- Prevent duplicate active routes from -> to
CREATE UNIQUE INDEX IF NOT EXISTS uq_disposition_routes_active
ON disposition_routes(from_department_id, to_department_id)
WHERE is_active = TRUE;
CREATE TRIGGER trg_disposition_routes_updated_at
BEFORE UPDATE ON disposition_routes
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
COMMIT;