From e83eefc614bb9cb03239725000de31ace829b27a Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 6 Dec 2025 11:05:44 +0700 Subject: [PATCH] update letter outgoing final attachment --- internal/app/app.go | 5 +- internal/contract/letter_outgoing_contract.go | 2 - internal/entities/letter_outgoing.go | 31 +++++++---- internal/handler/file_handler.go | 27 ++++++++++ internal/handler/letter_outgoing_handler.go | 45 ++++++++++++++++ .../processor/letter_outgoing_processor.go | 53 +++++++++++++++++- .../repository/letter_outgoing_repository.go | 54 +++++++++++++++++++ internal/router/health_handler.go | 4 ++ internal/router/router.go | 4 ++ internal/service/file_service.go | 21 +++++++- internal/service/letter_outgoing_service.go | 45 +++++++++++++++- ...etter_outgoing_final_attachaments.down.sql | 0 ..._letter_outgoing_final_attachaments.up.sql | 12 +++++ 13 files changed, 285 insertions(+), 18 deletions(-) create mode 100644 migrations/000046_letter_outgoing_final_attachaments.down.sql create mode 100644 migrations/000046_letter_outgoing_final_attachaments.up.sql diff --git a/internal/app/app.go b/internal/app/app.go index 6b526e3..3dbd8d6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -142,6 +142,7 @@ type repositories struct { // letter outgoing repos letterOutgoingRepo *repository.LetterOutgoingRepository letterOutgoingAttachmentRepo *repository.LetterOutgoingAttachmentRepository + letterOutgoingFinalAttachmentRepo *repository.LetterOutgoingFinalAttachmentRepository letterOutgoingRecipientRepo *repository.LetterOutgoingRecipientRepository letterOutgoingDiscussionRepo *repository.LetterOutgoingDiscussionRepository letterOutgoingDiscussionAttachRepo *repository.LetterOutgoingDiscussionAttachmentRepository @@ -177,6 +178,7 @@ func (a *App) initRepositories() *repositories { userDeptRepo: repository.NewUserDepartmentRepository(a.db), letterOutgoingRepo: repository.NewLetterOutgoingRepository(a.db), letterOutgoingAttachmentRepo: repository.NewLetterOutgoingAttachmentRepository(a.db), + letterOutgoingFinalAttachmentRepo: repository.NewLetterOutgoingFinalAttachmentRepository(a.db), letterOutgoingRecipientRepo: repository.NewLetterOutgoingRecipientRepository(a.db), letterOutgoingDiscussionRepo: repository.NewLetterOutgoingDiscussionRepository(a.db), letterOutgoingDiscussionAttachRepo: repository.NewLetterOutgoingDiscussionAttachmentRepository(a.db), @@ -260,6 +262,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor a.db, repos.letterOutgoingRepo, repos.letterOutgoingAttachmentRepo, + repos.letterOutgoingFinalAttachmentRepo, repos.letterOutgoingRecipientRepo, repos.letterOutgoingDiscussionRepo, repos.letterOutgoingDiscussionAttachRepo, @@ -385,7 +388,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con fileCfg := cfg.S3Config 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) diff --git a/internal/contract/letter_outgoing_contract.go b/internal/contract/letter_outgoing_contract.go index 3439b90..1df1961 100644 --- a/internal/contract/letter_outgoing_contract.go +++ b/internal/contract/letter_outgoing_contract.go @@ -43,7 +43,6 @@ type CreateOutgoingLetterAttachment struct { FileURL string `json:"file_url" validate:"required"` FileName string `json:"file_name" validate:"required"` FileType string `json:"file_type" validate:"required"` - IsFinal bool `json:"is_final" validate:"omitempty"` } type CreateOutgoingLetterRequest struct { @@ -80,7 +79,6 @@ type OutgoingLetterAttachmentResponse struct { FileName string `json:"file_name"` FileType string `json:"file_type"` UploadedAt time.Time `json:"uploaded_at"` - IsFinal bool `json:"is_final"` } type OutgoingLetterApprovalResponse struct { diff --git a/internal/entities/letter_outgoing.go b/internal/entities/letter_outgoing.go index 756190e..aff49bb 100644 --- a/internal/entities/letter_outgoing.go +++ b/internal/entities/letter_outgoing.go @@ -37,15 +37,15 @@ type LetterOutgoing struct { DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` // Relations - Priority *Priority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"` - ReceiverInstitution *Institution `gorm:"foreignKey:ReceiverInstitutionID" json:"receiver_institution,omitempty"` - Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"` - ApprovalFlow *ApprovalFlow `gorm:"foreignKey:ApprovalFlowID" json:"approval_flow,omitempty"` - Recipients []LetterOutgoingRecipient `gorm:"foreignKey:LetterID" json:"recipients,omitempty"` - Attachments []LetterOutgoingAttachment `gorm:"foreignKey:LetterID" json:"attachments,omitempty"` - Approvals []LetterOutgoingApproval `gorm:"foreignKey:LetterID" json:"approvals,omitempty"` - Discussions []LetterOutgoingDiscussion `gorm:"foreignKey:LetterID" json:"discussions,omitempty"` - ActivityLogs []LetterOutgoingActivityLog `gorm:"foreignKey:LetterID" json:"activity_logs,omitempty"` + Priority *Priority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"` + ReceiverInstitution *Institution `gorm:"foreignKey:ReceiverInstitutionID" json:"receiver_institution,omitempty"` + Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"` + ApprovalFlow *ApprovalFlow `gorm:"foreignKey:ApprovalFlowID" json:"approval_flow,omitempty"` + Recipients []LetterOutgoingRecipient `gorm:"foreignKey:LetterID" json:"recipients,omitempty"` + Attachments []LetterOutgoingAttachment `gorm:"foreignKey:LetterID" json:"attachments,omitempty"` + Approvals []LetterOutgoingApproval `gorm:"foreignKey:LetterID" json:"approvals,omitempty"` + Discussions []LetterOutgoingDiscussion `gorm:"foreignKey:LetterID" json:"discussions,omitempty"` + FinalAttachments []LetterOutgoingFinalAttachment `gorm:"foreignKey:LetterID" json:"final_attachments,omitempty"` } func (LetterOutgoing) TableName() string { return "letters_outgoing" } @@ -76,11 +76,22 @@ type LetterOutgoingAttachment struct { FileType string `gorm:"not null" json:"file_type"` UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"` UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"` - IsFinal bool `gorm:"default:false" json:"is_final"` } func (LetterOutgoingAttachment) TableName() string { return "letter_outgoing_attachments" } +type LetterOutgoingFinalAttachment struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"` + FileURL string `gorm:"not null" json:"file_url"` + FileName string `gorm:"not null" json:"file_name"` + FileType string `gorm:"not null" json:"file_type"` + UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"` + UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"` +} + +func (LetterOutgoingFinalAttachment) TableName() string { return "letter_outgoing_final_attachments" } + type LetterOutgoingDiscussion struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"` diff --git a/internal/handler/file_handler.go b/internal/handler/file_handler.go index c00b18c..4a1442d 100644 --- a/internal/handler/file_handler.go +++ b/internal/handler/file_handler.go @@ -15,6 +15,7 @@ import ( type FileService interface { UploadProfileAvatar(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, error) UploadDocument(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error) + UploadDocumentFinal(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error) } type FileHandler struct { @@ -76,3 +77,29 @@ func (h *FileHandler) UploadDocument(c *gin.Context) { } 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})) +} diff --git a/internal/handler/letter_outgoing_handler.go b/internal/handler/letter_outgoing_handler.go index b8d190a..3b131b5 100644 --- a/internal/handler/letter_outgoing_handler.go +++ b/internal/handler/letter_outgoing_handler.go @@ -35,6 +35,9 @@ type LetterOutgoingService interface { AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error + AddFinalAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error + RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error + CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error @@ -407,6 +410,48 @@ func (h *LetterOutgoingHandler) RemoveAttachment(c *gin.Context) { c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"}) } +func (h *LetterOutgoingHandler) AddFinalAttachments(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest}) + return + } + + var req contract.AddAttachmentsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest}) + return + } + + if err := h.svc.AddFinalAttachments(c.Request.Context(), id, &req); err != nil { + c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError}) + return + } + + c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "attachment added"})) +} + +func (h *LetterOutgoingHandler) RemoveFinalAttachment(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest}) + return + } + + attachmentID, err := uuid.Parse(c.Param("attachment_id")) + if err != nil { + c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid attachment id", Code: http.StatusBadRequest}) + return + } + + if err := h.svc.RemoveFinalAttachment(c.Request.Context(), id, attachmentID); err != nil { + c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError}) + return + } + + c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"}) +} + func (h *LetterOutgoingHandler) CreateDiscussion(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { diff --git a/internal/processor/letter_outgoing_processor.go b/internal/processor/letter_outgoing_processor.go index a14cf46..e6ffffd 100644 --- a/internal/processor/letter_outgoing_processor.go +++ b/internal/processor/letter_outgoing_processor.go @@ -40,6 +40,9 @@ type LetterOutgoingProcessor interface { 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 + 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 GetDiscussionByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) UpdateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion) error @@ -68,6 +71,7 @@ type LetterOutgoingProcessorImpl struct { db *gorm.DB letterRepo *repository.LetterOutgoingRepository attachmentRepo *repository.LetterOutgoingAttachmentRepository + finalAttachmentRepo *repository.LetterOutgoingFinalAttachmentRepository recipientRepo *repository.LetterOutgoingRecipientRepository discussionRepo *repository.LetterOutgoingDiscussionRepository discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository @@ -84,6 +88,7 @@ func NewLetterOutgoingProcessor( db *gorm.DB, letterRepo *repository.LetterOutgoingRepository, attachmentRepo *repository.LetterOutgoingAttachmentRepository, + finalAttachmentRepo *repository.LetterOutgoingFinalAttachmentRepository, recipientRepo *repository.LetterOutgoingRecipientRepository, discussionRepo *repository.LetterOutgoingDiscussionRepository, discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository, @@ -99,6 +104,7 @@ func NewLetterOutgoingProcessor( db: db, letterRepo: letterRepo, attachmentRepo: attachmentRepo, + finalAttachmentRepo: finalAttachmentRepo, recipientRepo: recipientRepo, discussionRepo: discussionRepo, discussionAttachmentRepo: discussionAttachmentRepo, @@ -359,7 +365,7 @@ func (p *LetterOutgoingProcessorImpl) DeleteOutgoingLetter(ctx context.Context, if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } - + return nil }) } @@ -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 { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.discussionRepo.Create(txCtx, discussion); err != nil { diff --git a/internal/repository/letter_outgoing_repository.go b/internal/repository/letter_outgoing_repository.go index 360fa5b..bc12703 100644 --- a/internal/repository/letter_outgoing_repository.go +++ b/internal/repository/letter_outgoing_repository.go @@ -534,6 +534,60 @@ func (r *LetterOutgoingAttachmentRepository) ListByLetterIDs(ctx context.Context 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 } func NewLetterOutgoingRecipientRepository(db *gorm.DB) *LetterOutgoingRecipientRepository { diff --git a/internal/router/health_handler.go b/internal/router/health_handler.go index 11664c8..a9262fc 100644 --- a/internal/router/health_handler.go +++ b/internal/router/health_handler.go @@ -22,6 +22,7 @@ type UserHandler interface { type FileHandler interface { UploadProfileAvatar(c *gin.Context) UploadDocument(c *gin.Context) + UploadDocumentFinal(c *gin.Context) } type RBACHandler interface { @@ -121,6 +122,9 @@ type LetterOutgoingHandler interface { AddAttachments(c *gin.Context) RemoveAttachment(c *gin.Context) + AddFinalAttachments(c *gin.Context) + RemoveFinalAttachment(c *gin.Context) + CreateDiscussion(c *gin.Context) UpdateDiscussion(c *gin.Context) DeleteDiscussion(c *gin.Context) diff --git a/internal/router/router.go b/internal/router/router.go index 9ed3add..009cdb5 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -112,6 +112,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { files.Use(r.authMiddleware.RequireAuth()) { files.POST("/documents", r.fileHandler.UploadDocument) + files.POST("/documents/final", r.fileHandler.UploadDocumentFinal) } rbac := v1.Group("/rbac") @@ -212,6 +213,9 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { lettersch.POST("/outgoing/:id/attachments", r.letterOutgoingHandler.AddAttachments) 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.PUT("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.UpdateDiscussion) lettersch.DELETE("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.DeleteDiscussion) diff --git a/internal/service/file_service.go b/internal/service/file_service.go index 25cf7e8..1e6eb9a 100644 --- a/internal/service/file_service.go +++ b/internal/service/file_service.go @@ -21,10 +21,11 @@ type FileServiceImpl struct { userProcessor UserProcessor profileBucket string docBucket string + finalBucket string } -func NewFileService(storage FileStorage, userProcessor UserProcessor, profileBucket, docBucket string) *FileServiceImpl { - return &FileServiceImpl{storage: storage, userProcessor: userProcessor, profileBucket: profileBucket, docBucket: docBucket} +func NewFileService(storage FileStorage, userProcessor UserProcessor, profileBucket, docBucket string, finalBucket string) *FileServiceImpl { + 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) { @@ -61,6 +62,22 @@ func (s *FileServiceImpl) UploadDocument(ctx context.Context, userID uuid.UUID, 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 { now := time.Now().UTC() parts := []string{ diff --git a/internal/service/letter_outgoing_service.go b/internal/service/letter_outgoing_service.go index ce3437f..e775502 100644 --- a/internal/service/letter_outgoing_service.go +++ b/internal/service/letter_outgoing_service.go @@ -39,6 +39,9 @@ type LetterOutgoingService interface { AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error + AddFinalAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error + RemoveFinalAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error + CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error @@ -915,7 +918,6 @@ func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID FileURL: a.FileURL, FileName: a.FileName, 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) } +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) { userID := getUserIDFromContext(ctx) @@ -1589,7 +1631,6 @@ func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.Outgoi FileName: attachment.FileName, FileType: attachment.FileType, UploadedAt: attachment.UploadedAt, - IsFinal: attachment.IsFinal, } } } diff --git a/migrations/000046_letter_outgoing_final_attachaments.down.sql b/migrations/000046_letter_outgoing_final_attachaments.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/migrations/000046_letter_outgoing_final_attachaments.up.sql b/migrations/000046_letter_outgoing_final_attachaments.up.sql new file mode 100644 index 0000000..b89ad8f --- /dev/null +++ b/migrations/000046_letter_outgoing_final_attachaments.up.sql @@ -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 +); \ No newline at end of file