update letter outgoing final attachment

This commit is contained in:
efrilm 2025-12-06 11:05:44 +07:00
parent 7b9db24c83
commit e83eefc614
13 changed files with 285 additions and 18 deletions

View File

@ -142,6 +142,7 @@ type repositories struct {
// letter outgoing repos // letter outgoing repos
letterOutgoingRepo *repository.LetterOutgoingRepository letterOutgoingRepo *repository.LetterOutgoingRepository
letterOutgoingAttachmentRepo *repository.LetterOutgoingAttachmentRepository letterOutgoingAttachmentRepo *repository.LetterOutgoingAttachmentRepository
letterOutgoingFinalAttachmentRepo *repository.LetterOutgoingFinalAttachmentRepository
letterOutgoingRecipientRepo *repository.LetterOutgoingRecipientRepository letterOutgoingRecipientRepo *repository.LetterOutgoingRecipientRepository
letterOutgoingDiscussionRepo *repository.LetterOutgoingDiscussionRepository letterOutgoingDiscussionRepo *repository.LetterOutgoingDiscussionRepository
letterOutgoingDiscussionAttachRepo *repository.LetterOutgoingDiscussionAttachmentRepository letterOutgoingDiscussionAttachRepo *repository.LetterOutgoingDiscussionAttachmentRepository
@ -177,6 +178,7 @@ func (a *App) initRepositories() *repositories {
userDeptRepo: repository.NewUserDepartmentRepository(a.db), userDeptRepo: repository.NewUserDepartmentRepository(a.db),
letterOutgoingRepo: repository.NewLetterOutgoingRepository(a.db), letterOutgoingRepo: repository.NewLetterOutgoingRepository(a.db),
letterOutgoingAttachmentRepo: repository.NewLetterOutgoingAttachmentRepository(a.db), letterOutgoingAttachmentRepo: repository.NewLetterOutgoingAttachmentRepository(a.db),
letterOutgoingFinalAttachmentRepo: repository.NewLetterOutgoingFinalAttachmentRepository(a.db),
letterOutgoingRecipientRepo: repository.NewLetterOutgoingRecipientRepository(a.db), letterOutgoingRecipientRepo: repository.NewLetterOutgoingRecipientRepository(a.db),
letterOutgoingDiscussionRepo: repository.NewLetterOutgoingDiscussionRepository(a.db), letterOutgoingDiscussionRepo: repository.NewLetterOutgoingDiscussionRepository(a.db),
letterOutgoingDiscussionAttachRepo: repository.NewLetterOutgoingDiscussionAttachmentRepository(a.db), letterOutgoingDiscussionAttachRepo: repository.NewLetterOutgoingDiscussionAttachmentRepository(a.db),
@ -260,6 +262,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
a.db, a.db,
repos.letterOutgoingRepo, repos.letterOutgoingRepo,
repos.letterOutgoingAttachmentRepo, repos.letterOutgoingAttachmentRepo,
repos.letterOutgoingFinalAttachmentRepo,
repos.letterOutgoingRecipientRepo, repos.letterOutgoingRecipientRepo,
repos.letterOutgoingDiscussionRepo, repos.letterOutgoingDiscussionRepo,
repos.letterOutgoingDiscussionAttachRepo, repos.letterOutgoingDiscussionAttachRepo,
@ -385,7 +388,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
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", "finals")
rbacSvc := service.NewRBACService(repos.rbacRepo) rbacSvc := service.NewRBACService(repos.rbacRepo)

View File

@ -43,7 +43,6 @@ type CreateOutgoingLetterAttachment struct {
FileURL string `json:"file_url" validate:"required"` FileURL string `json:"file_url" validate:"required"`
FileName string `json:"file_name" validate:"required"` FileName string `json:"file_name" validate:"required"`
FileType string `json:"file_type" validate:"required"` FileType string `json:"file_type" validate:"required"`
IsFinal bool `json:"is_final" validate:"omitempty"`
} }
type CreateOutgoingLetterRequest struct { type CreateOutgoingLetterRequest struct {
@ -80,7 +79,6 @@ type OutgoingLetterAttachmentResponse struct {
FileName string `json:"file_name"` FileName string `json:"file_name"`
FileType string `json:"file_type"` FileType string `json:"file_type"`
UploadedAt time.Time `json:"uploaded_at"` UploadedAt time.Time `json:"uploaded_at"`
IsFinal bool `json:"is_final"`
} }
type OutgoingLetterApprovalResponse struct { type OutgoingLetterApprovalResponse struct {

View File

@ -37,15 +37,15 @@ type LetterOutgoing struct {
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
// Relations // Relations
Priority *Priority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"` Priority *Priority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"`
ReceiverInstitution *Institution `gorm:"foreignKey:ReceiverInstitutionID" json:"receiver_institution,omitempty"` ReceiverInstitution *Institution `gorm:"foreignKey:ReceiverInstitutionID" json:"receiver_institution,omitempty"`
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"` Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
ApprovalFlow *ApprovalFlow `gorm:"foreignKey:ApprovalFlowID" json:"approval_flow,omitempty"` ApprovalFlow *ApprovalFlow `gorm:"foreignKey:ApprovalFlowID" json:"approval_flow,omitempty"`
Recipients []LetterOutgoingRecipient `gorm:"foreignKey:LetterID" json:"recipients,omitempty"` Recipients []LetterOutgoingRecipient `gorm:"foreignKey:LetterID" json:"recipients,omitempty"`
Attachments []LetterOutgoingAttachment `gorm:"foreignKey:LetterID" json:"attachments,omitempty"` Attachments []LetterOutgoingAttachment `gorm:"foreignKey:LetterID" json:"attachments,omitempty"`
Approvals []LetterOutgoingApproval `gorm:"foreignKey:LetterID" json:"approvals,omitempty"` Approvals []LetterOutgoingApproval `gorm:"foreignKey:LetterID" json:"approvals,omitempty"`
Discussions []LetterOutgoingDiscussion `gorm:"foreignKey:LetterID" json:"discussions,omitempty"` Discussions []LetterOutgoingDiscussion `gorm:"foreignKey:LetterID" json:"discussions,omitempty"`
ActivityLogs []LetterOutgoingActivityLog `gorm:"foreignKey:LetterID" json:"activity_logs,omitempty"` FinalAttachments []LetterOutgoingFinalAttachment `gorm:"foreignKey:LetterID" json:"final_attachments,omitempty"`
} }
func (LetterOutgoing) TableName() string { return "letters_outgoing" } func (LetterOutgoing) TableName() string { return "letters_outgoing" }
@ -76,11 +76,22 @@ type LetterOutgoingAttachment struct {
FileType string `gorm:"not null" json:"file_type"` FileType string `gorm:"not null" json:"file_type"`
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"` UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"` UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
IsFinal bool `gorm:"default:false" json:"is_final"`
} }
func (LetterOutgoingAttachment) TableName() string { return "letter_outgoing_attachments" } func (LetterOutgoingAttachment) TableName() string { return "letter_outgoing_attachments" }
type LetterOutgoingFinalAttachment struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
FileURL string `gorm:"not null" json:"file_url"`
FileName string `gorm:"not null" json:"file_name"`
FileType string `gorm:"not null" json:"file_type"`
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
}
func (LetterOutgoingFinalAttachment) TableName() string { return "letter_outgoing_final_attachments" }
type LetterOutgoingDiscussion struct { type LetterOutgoingDiscussion struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"` LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`

View File

@ -15,6 +15,7 @@ import (
type FileService interface { type FileService interface {
UploadProfileAvatar(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, error) UploadProfileAvatar(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, error)
UploadDocument(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error) UploadDocument(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error)
UploadDocumentFinal(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error)
} }
type FileHandler struct { type FileHandler struct {
@ -76,3 +77,29 @@ func (h *FileHandler) UploadDocument(c *gin.Context) {
} }
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]string{"url": url, "key": key})) c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]string{"url": url, "key": key}))
} }
func (h *FileHandler) UploadDocumentFinal(c *gin.Context) {
appCtx := appcontext.FromGinContext(c.Request.Context())
if appCtx.UserID == uuid.Nil {
c.JSON(http.StatusUnauthorized, &contract.ErrorResponse{Error: "Unauthorized", Code: http.StatusUnauthorized})
return
}
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "file is required", Code: http.StatusBadRequest})
return
}
defer file.Close()
content, err := io.ReadAll(io.LimitReader(file, 20<<20))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "failed to read file", Code: http.StatusBadRequest})
return
}
ct := header.Header.Get("Content-Type")
url, key, err := h.service.UploadDocumentFinal(c.Request.Context(), appCtx.UserID, header.Filename, content, ct)
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
return
}
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]string{"url": url, "key": key}))
}

View File

@ -35,6 +35,9 @@ type LetterOutgoingService interface {
AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
AddFinalAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error)
UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
@ -407,6 +410,48 @@ func (h *LetterOutgoingHandler) RemoveAttachment(c *gin.Context) {
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"}) c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"})
} }
func (h *LetterOutgoingHandler) AddFinalAttachments(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
return
}
var req contract.AddAttachmentsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
return
}
if err := h.svc.AddFinalAttachments(c.Request.Context(), id, &req); err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
return
}
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "attachment added"}))
}
func (h *LetterOutgoingHandler) RemoveFinalAttachment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
return
}
attachmentID, err := uuid.Parse(c.Param("attachment_id"))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid attachment id", Code: http.StatusBadRequest})
return
}
if err := h.svc.RemoveFinalAttachment(c.Request.Context(), id, attachmentID); err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
return
}
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"})
}
func (h *LetterOutgoingHandler) CreateDiscussion(c *gin.Context) { func (h *LetterOutgoingHandler) CreateDiscussion(c *gin.Context) {
id, err := uuid.Parse(c.Param("id")) id, err := uuid.Parse(c.Param("id"))
if err != nil { if err != nil {

View File

@ -40,6 +40,9 @@ type LetterOutgoingProcessor interface {
AddAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment, userID uuid.UUID) error AddAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment, userID uuid.UUID) error
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error
AddFinalAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingFinalAttachment, userID uuid.UUID) error
RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error
CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error
GetDiscussionByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) GetDiscussionByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error)
UpdateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion) error UpdateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion) error
@ -68,6 +71,7 @@ type LetterOutgoingProcessorImpl struct {
db *gorm.DB db *gorm.DB
letterRepo *repository.LetterOutgoingRepository letterRepo *repository.LetterOutgoingRepository
attachmentRepo *repository.LetterOutgoingAttachmentRepository attachmentRepo *repository.LetterOutgoingAttachmentRepository
finalAttachmentRepo *repository.LetterOutgoingFinalAttachmentRepository
recipientRepo *repository.LetterOutgoingRecipientRepository recipientRepo *repository.LetterOutgoingRecipientRepository
discussionRepo *repository.LetterOutgoingDiscussionRepository discussionRepo *repository.LetterOutgoingDiscussionRepository
discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository
@ -84,6 +88,7 @@ func NewLetterOutgoingProcessor(
db *gorm.DB, db *gorm.DB,
letterRepo *repository.LetterOutgoingRepository, letterRepo *repository.LetterOutgoingRepository,
attachmentRepo *repository.LetterOutgoingAttachmentRepository, attachmentRepo *repository.LetterOutgoingAttachmentRepository,
finalAttachmentRepo *repository.LetterOutgoingFinalAttachmentRepository,
recipientRepo *repository.LetterOutgoingRecipientRepository, recipientRepo *repository.LetterOutgoingRecipientRepository,
discussionRepo *repository.LetterOutgoingDiscussionRepository, discussionRepo *repository.LetterOutgoingDiscussionRepository,
discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository, discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository,
@ -99,6 +104,7 @@ func NewLetterOutgoingProcessor(
db: db, db: db,
letterRepo: letterRepo, letterRepo: letterRepo,
attachmentRepo: attachmentRepo, attachmentRepo: attachmentRepo,
finalAttachmentRepo: finalAttachmentRepo,
recipientRepo: recipientRepo, recipientRepo: recipientRepo,
discussionRepo: discussionRepo, discussionRepo: discussionRepo,
discussionAttachmentRepo: discussionAttachmentRepo, discussionAttachmentRepo: discussionAttachmentRepo,
@ -1128,6 +1134,51 @@ func (p *LetterOutgoingProcessorImpl) RemoveAttachment(ctx context.Context, lett
}) })
} }
func (p *LetterOutgoingProcessorImpl) AddFinalAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingFinalAttachment, userID uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Get the letter to get the current revision number
_, err := p.letterRepo.Get(txCtx, letterID)
if err != nil {
return err
}
if err := p.finalAttachmentRepo.CreateBulk(txCtx, attachments); err != nil {
return err
}
activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID,
ActionType: entities.LetterOutgoingActionAttachmentAdded,
ActorUserID: &userID,
}
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
return err
}
return nil
})
}
func (p *LetterOutgoingProcessorImpl) RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
if err := p.finalAttachmentRepo.Delete(txCtx, attachmentID); err != nil {
return err
}
activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID,
ActionType: entities.LetterOutgoingActionAttachmentRemoved,
ActorUserID: &userID,
TargetID: &attachmentID,
}
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
return err
}
return nil
})
}
func (p *LetterOutgoingProcessorImpl) CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error { func (p *LetterOutgoingProcessorImpl) CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
if err := p.discussionRepo.Create(txCtx, discussion); err != nil { if err := p.discussionRepo.Create(txCtx, discussion); err != nil {

View File

@ -534,6 +534,60 @@ func (r *LetterOutgoingAttachmentRepository) ListByLetterIDs(ctx context.Context
return result, nil return result, nil
} }
type LetterOutgoingFinalAttachmentRepository struct{ db *gorm.DB }
func NewLetterOutgoingFinalAttachmentRepository(db *gorm.DB) *LetterOutgoingFinalAttachmentRepository {
return &LetterOutgoingFinalAttachmentRepository{db: db}
}
func (r *LetterOutgoingFinalAttachmentRepository) Create(ctx context.Context, e *entities.LetterOutgoingFinalAttachment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingFinalAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingFinalAttachment) error {
db := DBFromContext(ctx, r.db)
if len(list) == 0 {
return nil
}
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterOutgoingFinalAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingFinalAttachment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingFinalAttachment
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterOutgoingFinalAttachmentRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingAttachment{}).Error
}
// ListByLetterIDs fetches attachments for multiple letters in a single query
func (r *LetterOutgoingFinalAttachmentRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingFinalAttachment, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterOutgoingFinalAttachment), nil
}
db := DBFromContext(ctx, r.db)
var attachments []entities.LetterOutgoingFinalAttachment
if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).Order("uploaded_at ASC").Find(&attachments).Error; err != nil {
return nil, err
}
// Group attachments by letter ID
result := make(map[uuid.UUID][]entities.LetterOutgoingFinalAttachment)
for _, att := range attachments {
result[att.LetterID] = append(result[att.LetterID], att)
}
return result, nil
}
type LetterOutgoingRecipientRepository struct{ db *gorm.DB } type LetterOutgoingRecipientRepository struct{ db *gorm.DB }
func NewLetterOutgoingRecipientRepository(db *gorm.DB) *LetterOutgoingRecipientRepository { func NewLetterOutgoingRecipientRepository(db *gorm.DB) *LetterOutgoingRecipientRepository {

View File

@ -22,6 +22,7 @@ type UserHandler interface {
type FileHandler interface { type FileHandler interface {
UploadProfileAvatar(c *gin.Context) UploadProfileAvatar(c *gin.Context)
UploadDocument(c *gin.Context) UploadDocument(c *gin.Context)
UploadDocumentFinal(c *gin.Context)
} }
type RBACHandler interface { type RBACHandler interface {
@ -121,6 +122,9 @@ type LetterOutgoingHandler interface {
AddAttachments(c *gin.Context) AddAttachments(c *gin.Context)
RemoveAttachment(c *gin.Context) RemoveAttachment(c *gin.Context)
AddFinalAttachments(c *gin.Context)
RemoveFinalAttachment(c *gin.Context)
CreateDiscussion(c *gin.Context) CreateDiscussion(c *gin.Context)
UpdateDiscussion(c *gin.Context) UpdateDiscussion(c *gin.Context)
DeleteDiscussion(c *gin.Context) DeleteDiscussion(c *gin.Context)

View File

@ -112,6 +112,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
files.Use(r.authMiddleware.RequireAuth()) files.Use(r.authMiddleware.RequireAuth())
{ {
files.POST("/documents", r.fileHandler.UploadDocument) files.POST("/documents", r.fileHandler.UploadDocument)
files.POST("/documents/final", r.fileHandler.UploadDocumentFinal)
} }
rbac := v1.Group("/rbac") rbac := v1.Group("/rbac")
@ -212,6 +213,9 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
lettersch.POST("/outgoing/:id/attachments", r.letterOutgoingHandler.AddAttachments) lettersch.POST("/outgoing/:id/attachments", r.letterOutgoingHandler.AddAttachments)
lettersch.DELETE("/outgoing/:id/attachments/:attachment_id", r.letterOutgoingHandler.RemoveAttachment) lettersch.DELETE("/outgoing/:id/attachments/:attachment_id", r.letterOutgoingHandler.RemoveAttachment)
lettersch.POST("/outgoing/:id/attachments/final", r.letterOutgoingHandler.AddFinalAttachments)
lettersch.DELETE("/outgoing/:id/attachments/final/:attachment_id", r.letterOutgoingHandler.RemoveFinalAttachment)
lettersch.POST("/outgoing/:id/discussions", r.letterOutgoingHandler.CreateDiscussion) lettersch.POST("/outgoing/:id/discussions", r.letterOutgoingHandler.CreateDiscussion)
lettersch.PUT("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.UpdateDiscussion) lettersch.PUT("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.UpdateDiscussion)
lettersch.DELETE("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.DeleteDiscussion) lettersch.DELETE("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.DeleteDiscussion)

View File

@ -21,10 +21,11 @@ type FileServiceImpl struct {
userProcessor UserProcessor userProcessor UserProcessor
profileBucket string profileBucket string
docBucket string docBucket string
finalBucket string
} }
func NewFileService(storage FileStorage, userProcessor UserProcessor, profileBucket, docBucket string) *FileServiceImpl { func NewFileService(storage FileStorage, userProcessor UserProcessor, profileBucket, docBucket string, finalBucket string) *FileServiceImpl {
return &FileServiceImpl{storage: storage, userProcessor: userProcessor, profileBucket: profileBucket, docBucket: docBucket} return &FileServiceImpl{storage: storage, userProcessor: userProcessor, profileBucket: profileBucket, docBucket: docBucket, finalBucket: finalBucket}
} }
func (s *FileServiceImpl) UploadProfileAvatar(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, error) { func (s *FileServiceImpl) UploadProfileAvatar(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, error) {
@ -61,6 +62,22 @@ func (s *FileServiceImpl) UploadDocument(ctx context.Context, userID uuid.UUID,
return url, key, nil return url, key, nil
} }
func (s *FileServiceImpl) UploadDocumentFinal(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error) {
if err := s.storage.EnsureBucket(ctx, s.docBucket); err != nil {
return "", "", err
}
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
ext = mimeExt
}
key := buildObjectKey("finals", userID, ext)
url, err := s.storage.Upload(ctx, s.docBucket, key, content, contentType)
if err != nil {
return "", "", err
}
return url, key, nil
}
func buildObjectKey(prefix string, userID uuid.UUID, ext string) string { func buildObjectKey(prefix string, userID uuid.UUID, ext string) string {
now := time.Now().UTC() now := time.Now().UTC()
parts := []string{ parts := []string{

View File

@ -39,6 +39,9 @@ type LetterOutgoingService interface {
AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
AddFinalAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error)
UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
@ -915,7 +918,6 @@ func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID
FileURL: a.FileURL, FileURL: a.FileURL,
FileName: a.FileName, FileName: a.FileName,
FileType: a.FileType, FileType: a.FileType,
IsFinal: a.IsFinal,
} }
} }
@ -937,6 +939,46 @@ func (s *LetterOutgoingServiceImpl) RemoveAttachment(ctx context.Context, letter
return s.processor.RemoveAttachment(ctx, letterID, attachmentID, userID) return s.processor.RemoveAttachment(ctx, letterID, attachmentID, userID)
} }
func (s *LetterOutgoingServiceImpl) AddFinalAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error {
userID := getUserIDFromContext(ctx)
_, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
if err != nil {
return err
}
//if letter.Status != entities.LetterOutgoingStatusDraft {
// return gorm.ErrInvalidData
//}
attachments := make([]entities.LetterOutgoingFinalAttachment, len(req.Attachments))
for i, a := range req.Attachments {
attachments[i] = entities.LetterOutgoingFinalAttachment{
LetterID: letterID,
FileURL: a.FileURL,
FileName: a.FileName,
FileType: a.FileType,
}
}
return s.processor.AddFinalAttachments(ctx, letterID, attachments, userID)
}
func (s *LetterOutgoingServiceImpl) RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error {
userID := getUserIDFromContext(ctx)
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
if err != nil {
return err
}
if letter.Status != entities.LetterOutgoingStatusDraft {
return gorm.ErrInvalidData
}
return s.processor.RemoveFinalAttachment(ctx, letterID, attachmentID, userID)
}
func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) { func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) {
userID := getUserIDFromContext(ctx) userID := getUserIDFromContext(ctx)
@ -1589,7 +1631,6 @@ func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.Outgoi
FileName: attachment.FileName, FileName: attachment.FileName,
FileType: attachment.FileType, FileType: attachment.FileType,
UploadedAt: attachment.UploadedAt, UploadedAt: attachment.UploadedAt,
IsFinal: attachment.IsFinal,
} }
} }
} }

View File

@ -0,0 +1,12 @@
ALTER TABLE letter_outgoing_attachments
DROP COLUMN IF EXISTS is_final;
CREATE TABLE IF NOT EXISTS letter_outgoing_final_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letter_id UUID NOT NULL REFERENCES letters_outgoing(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
);