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
@ -102,6 +108,19 @@ type repositories struct {
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 {
@ -110,16 +129,34 @@ func (a *App) initRepositories() *repositories {
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,
} }
} }
@ -128,6 +165,9 @@ type services struct {
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

@ -15,6 +15,9 @@ type Router struct {
userHandler UserHandler userHandler UserHandler
fileHandler FileHandler fileHandler FileHandler
rbacHandler RBACHandler rbacHandler RBACHandler
masterHandler MasterHandler
letterHandler LetterHandler
dispRouteHandler DispositionRouteHandler
} }
func NewRouter( func NewRouter(
@ -25,6 +28,9 @@ 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,
@ -34,6 +40,9 @@ func NewRouter(
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;